Skip to content

A Django admin mixin that exposes reverse ForeignKey relationships directly on the parent object’s change form. Administrators can bind/unbind related objects from either side without custom forms or inlines.

License

Notifications You must be signed in to change notification settings

tnware/django-admin-reversefields

Repository files navigation

django-admin-reversefields

PyPI - Downloads PyPI - Version

Manage reverse ForeignKey/OneToOne bindings directly from a parent model’s Django admin form using a small, declarative mixin.

  • Add virtual fields to your ModelAdmin to bind/unbind reverse-side rows
  • Keep selections in sync with transactional, unbind-before-bind updates
  • Use stock admin widgets or plug in Unfold/DAL/custom widgets
  • Optional, flexible permission gating with clear UX (hide/disable)

Install

pip install django-admin-reversefields

Supported: Django 4.2/5.0/5.1/5.2; Python 3.10–3.13.


Quickstart

from django.contrib import admin
from django.db.models import Q

from django_admin_reversefields.mixins import (
    ReverseRelationAdminMixin,
    ReverseRelationConfig,
)
from .models import Company, Department, Project


def unbound_or_current(qs, instance, request):
    if instance and instance.pk:
        return qs.filter(Q(company__isnull=True) | Q(company=instance))
    return qs.filter(company__isnull=True)


@admin.register(Company)
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
    reverse_relations = {
        # Single-select: bind exactly one Department via its FK to Company
        "department_binding": ReverseRelationConfig(
            model=Department,
            fk_field="company",
            limit_choices_to=unbound_or_current,
        ),
        # Multi-select: manage the entire set of Projects pointing at the Company
        "assigned_projects": ReverseRelationConfig(
            model=Project,
            fk_field="company",
            multiple=True,
            # optional: ordering=("name",),
        ),
    }

    fieldsets = (("Relations", {"fields": ("department_binding", "assigned_projects")}),)
  • Include each virtual field name (e.g. "department_binding") in fieldsets or fields so the admin template renders it (or omit both fields and fieldsets and Django will render all fields, including the injected virtual fields).
  • Limiters run per request/object; commonly: “unbound or currently bound”.

Core concepts (tl;dr)

  • Reverse fields are virtual ModelChoiceField / ModelMultipleChoiceField instances that point to the reverse-side model and its ForeignKey back to the admin’s model.
  • Querysets and initial values are computed per request/object.
  • On save, the mixin synchronizes the reverse-side ForeignKey(s) to match the submitted selection.
    • Single-select: sets the chosen row’s FK to the parent and unbinds any other rows pointing to it.
    • Multi-select: represents the entire desired set; rows not in the selection are unbound before binds.
  • Transactions: by default reverse_relations_atomic=True wraps all updates in one transaction.atomic() block and applies unbinds before binds to minimize uniqueness conflicts.

Performance: enable bulk=True on a ReverseRelationConfig to use .update() for unbind/bind operations. This improves performance with large datasets but bypasses model signals. Use only if your app doesn’t depend on pre_save/post_save on the reverse model.

Important: for single-select, unbinding others requires the reverse FK to be null=True, or set required=True on the virtual field when it must never be empty; otherwise an unbind can raise IntegrityError.


Permissions (optional)

Enable enforcement:

class ServiceAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
    reverse_permissions_enabled = True
    reverse_permission_mode = "disable"  # or "hide"
  • Precedence for allow/deny:
    1. Per-field ReverseRelationConfig.permission
    2. reverse_permission_policy (admin-wide)
    3. Default user.has_perm("app.change_model") on the reverse model
  • Error message precedence: field override → per-field policy object → global policy object → default
  • Disable vs hide:
    • "disable": render read-only and ignore posted changes. To avoid spurious validation, the mixin sets required=False on disabled reverse fields so forms won’t raise “This field is required.” when there is no initial value.
    • "hide": remove the field entirely.
  • Optional: set reverse_render_uses_field_policy=True to have render-time visibility/disabled state decided by your per-field/global policy (called with selection=None).

Hidden/disabled fields are always ignored on save, so crafted POSTs cannot change unauthorized reverse fields.


API surface

Import:

from django_admin_reversefields.mixins import ReverseRelationAdminMixin, ReverseRelationConfig

ReverseRelationConfig (per virtual field):

  • model: reverse-side models.Model that holds the ForeignKey to the admin model
  • fk_field: name of that ForeignKey on model
  • label, help_text: optional display strings
  • required: enforce non-empty selection (default False)
  • multiple: multi-select that syncs many rows (default False)
  • limit_choices_to: callable (qs, instance, request) -> qs or dict passed to .filter(**dict)
  • widget: widget instance or class; defaults to admin Select/FilteredSelectMultiple
  • ordering: iterable for .order_by()
  • clean(instance, selection, request): optional domain validation; raise forms.ValidationError to block
  • permission: optional policy (callable or object with has_perm(...)) to allow/deny edits
  • permission_denied_message: message used when a denial becomes a field error
  • bulk: when True, perform unbind/bind via .update() (bypasses model signals)

Mixin knobs:

  • reverse_relations: mapping of virtual field name → config
  • reverse_relations_atomic: wrap all updates in one transaction (default True)
  • reverse_permissions_enabled: enforce permission checks (default False)
  • reverse_permission_mode: "disable" | "hide"
  • reverse_permission_policy: optional global policy
  • reverse_render_uses_field_policy: use per-field/global policy at render time (selection=None)

Recipes and docs


Development

We use uv for tooling.

  • uv sync — install project + docs deps
  • uv run ruff check . — lint
  • uv run django-admin test or uv run python manage.py test — tests
  • uv run sphinx-build -b html docs docs/_build/html -W — docs build

Release:

uv build
Twine upload dist/*

About

A Django admin mixin that exposes reverse ForeignKey relationships directly on the parent object’s change form. Administrators can bind/unbind related objects from either side without custom forms or inlines.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages