Skip to main content

A Django mixin for easily including reverse relationships in your changeforms in the admin

Project description

django-admin-reversefields

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; 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,
)


def only_unbound_or_current(qs, instance, request):
    if instance and instance.pk:
        return qs.filter(Q(service__isnull=True) | Q(service=instance))
    return qs.filter(service__isnull=True)


@admin.register(Service)
class ServiceAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
    reverse_relations = {
        "site_binding": ReverseRelationConfig(
            model=Site,
            fk_field="service",
            limit_choices_to=only_unbound_or_current,
        )
    }

    fieldsets = (("Binding", {"fields": ("site_binding",)}),)
  • Include the virtual field name (e.g. "site_binding") in fieldsets so Django renders it.
  • Limiters run per request/object; use them to include unbound items plus the current binding.

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.

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).

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

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/*

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_admin_reversefields-0.1.0.tar.gz (16.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_admin_reversefields-0.1.0-py3-none-any.whl (16.4 kB view details)

Uploaded Python 3

File details

Details for the file django_admin_reversefields-0.1.0.tar.gz.

File metadata

File hashes

Hashes for django_admin_reversefields-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3a639067aa655f8c349f9ed69d4aba9f5a125f884716a71842ffe79aa5f3d6b0
MD5 ad5791a58b51c25b423466506628b7fc
BLAKE2b-256 baa6172daedf54d3e3f0be64a79c8ee45e8f18bda23dc8b44afcc2a56fcbce16

See more details on using hashes here.

File details

Details for the file django_admin_reversefields-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_admin_reversefields-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f24033c6ba7fc245bb92a7d36e7f7fd51db63b9867304bbf32c8b6912b247c5f
MD5 e01d7b780de58793f9298d443291f79d
BLAKE2b-256 0e118723773d7746665a4a11e68a6622c326d8cd7cf4b883d378b3d45e14cfc4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page