Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/docs_deploy.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
name: docs

on:
push:
branches: [main]
paths:
- "docs/**"
release:
types: [published]
workflow_dispatch:

permissions:
Expand Down
12 changes: 9 additions & 3 deletions docs/development.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,15 @@ test binding and unbinding. It is idempotent and skips if data already exists.

The test models registered in the admin include:

- **Company** — single-select ``departments`` and multi-select ``projects``
virtual fields
- **Department** — single-select ``employees`` virtual field
- **Company** — multi-select ``departments`` and ``projects`` virtual fields,
plus single-select ``settings``
- **Department** — multi-select ``employees`` virtual field

The interactive admin examples use an ``unbound_or_current`` queryset pattern
for reverse fields so users can choose objects that are either unassigned or
already assigned to the object being edited.
Those reverse fields also include ``help_text`` in the admin UI to explain why
some options are intentionally filtered out.

Edit a company to see the mixin in action. The ``db.sqlite3`` file is
git-ignored.
Expand Down
13 changes: 10 additions & 3 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,8 @@ mixin and declaring at least one reverse relation:

.. literalinclude:: ../tests/admin.py
:language: python
:lines: 16-46
:caption: Minimal admin exposing two reverse bindings
:emphasize-lines: 13-31
:lines: 17-91
:caption: Minimal admin exposing reverse bindings with qualifying filters

1. ``reverse_relations`` is a ``dict`` keyed by virtual field name.
2. Each :class:`~django_admin_reversefields.mixins.ReverseRelationConfig`
Expand Down Expand Up @@ -90,6 +89,7 @@ instance.
from django.db.models import Q

def unbound_or_current(queryset, instance, request):
"""Offer unassigned rows plus rows already bound to this company."""
if instance and instance.pk:
return queryset.filter(Q(company__isnull=True) | Q(company=instance))
return queryset.filter(company__isnull=True)
Expand All @@ -100,6 +100,10 @@ instance.
model=Department,
fk_field="company",
limit_choices_to=unbound_or_current,
help_text=(
"Choices are limited to departments that are unassigned or "
"already assigned to this company."
),
)
}

Expand All @@ -108,6 +112,9 @@ instance.
- ``limit_choices_to`` accepts either a callable ``(queryset, instance,
request) -> queryset`` or a ``dict`` that is passed to
:meth:`~django.db.models.query.QuerySet.filter`.
- Add short docstrings to limiter helpers and pair them with
``help_text`` on the virtual field so users understand why some rows are
not selectable.
- ``multiple=True`` switches a field to a
:class:`~django.forms.ModelMultipleChoiceField` and synchronises the entire
set on save.
Expand Down
5 changes: 5 additions & 0 deletions docs/recipes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Single binding (Company ↔ Department)
from django_admin_reversefields.mixins import ReverseRelationAdminMixin, ReverseRelationConfig

def unbound_or_current(queryset, instance, request):
"""Offer unassigned rows plus rows already bound to this company."""
if instance and instance.pk:
return queryset.filter(Q(company__isnull=True) | Q(company=instance))
return queryset.filter(company__isnull=True)
Expand All @@ -28,6 +29,10 @@ Single binding (Company ↔ Department)
model=Department,
fk_field="company",
limit_choices_to=unbound_or_current,
help_text=(
"Choices are limited to departments that are unassigned or "
"already assigned to this company."
),
# Add bulk=True for better performance with large datasets
# bulk=True, # Uncomment if you don't need model signals
)
Expand Down
50 changes: 50 additions & 0 deletions tests/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

# Required imports from quickstart guide
from django.contrib import admin
from django.db.models import Q

from django_admin_reversefields.mixins import (
ReverseRelationAdminMixin,
Expand All @@ -13,6 +14,40 @@
from .models import Assignment, Company, CompanySettings, Department, Employee, Project


def unbound_or_current_company(queryset, instance, _request):
"""Return company-scoped choices for reverse FK bindings.

Args:
queryset: Base queryset for reverse-side objects.
instance: Company currently edited in the admin form.
_request: Active admin request (unused).

Returns:
Filtered queryset containing objects that are either unbound or already
bound to the current company.
"""
if instance and instance.pk:
return queryset.filter(Q(company__isnull=True) | Q(company=instance))
return queryset.filter(company__isnull=True)


def unbound_or_current_department(queryset, instance, _request):
"""Return department-scoped choices for reverse FK bindings.

Args:
queryset: Base queryset for reverse-side objects.
instance: Department currently edited in the admin form.
_request: Active admin request (unused).

Returns:
Filtered queryset containing objects that are either unbound or already
bound to the current department.
"""
if instance and instance.pk:
return queryset.filter(Q(department__isnull=True) | Q(department=instance))
return queryset.filter(department__isnull=True)


@admin.register(Company)
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
"""
Expand All @@ -31,12 +66,22 @@ class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
model=Department,
fk_field="company",
multiple=True,
limit_choices_to=unbound_or_current_company,
help_text=(
"Choices are limited to departments that are unassigned or already "
"assigned to this company."
),
),
# Multi-select: manage which projects belong to this company
"projects": ReverseRelationConfig(
model=Project,
fk_field="company",
multiple=True,
limit_choices_to=unbound_or_current_company,
help_text=(
"Choices are limited to projects that are unassigned or already "
"assigned to this company."
),
),
# Single-select: bind one CompanySettings instance (OneToOne)
"settings": ReverseRelationConfig(
Expand Down Expand Up @@ -69,6 +114,11 @@ class DepartmentAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
model=Employee,
fk_field="department",
multiple=True,
limit_choices_to=unbound_or_current_department,
help_text=(
"Choices are limited to employees who are unassigned or already "
"assigned to this department."
),
),
}

Expand Down
Loading