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
ModelAdminto 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") infieldsetsso 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/ModelMultipleChoiceFieldinstances 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=Truewraps all updates in onetransaction.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:
- Per-field
ReverseRelationConfig.permission reverse_permission_policy(admin-wide)- Default
user.has_perm("app.change_model")on the reverse model
- Per-field
- 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=Falseon disabled reverse fields so forms won’t raise “This field is required.” when there is no initial value. - "hide": remove the field entirely.
- "disable": render read-only and ignore posted changes. To avoid spurious validation, the mixin sets
- Optional: set
reverse_render_uses_field_policy=Trueto have render-time visibility/disabled state decided by your per-field/global policy (called withselection=None).
API surface
Import:
from django_admin_reversefields.mixins import ReverseRelationAdminMixin, ReverseRelationConfig
ReverseRelationConfig (per virtual field):
model: reverse-sidemodels.Modelthat holds the ForeignKey to the admin modelfk_field: name of that ForeignKey onmodellabel,help_text: optional display stringsrequired: enforce non-empty selection (default False)multiple: multi-select that syncs many rows (default False)limit_choices_to: callable(qs, instance, request) -> qsordictpassed to.filter(**dict)widget: widget instance or class; defaults to adminSelect/FilteredSelectMultipleordering: iterable for.order_by()clean(instance, selection, request): optional domain validation; raiseforms.ValidationErrorto blockpermission: optional policy (callable or object withhas_perm(...)) to allow/deny editspermission_denied_message: message used when a denial becomes a field error
Mixin knobs:
reverse_relations: mapping of virtual field name → configreverse_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 policyreverse_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 depsuv run ruff check .— lintuv run django-admin testoruv run python manage.py test— testsuv run sphinx-build -b html docs docs/_build/html -W— docs build
Release:
uv build
Twine upload dist/*
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_admin_reversefields-0.1.0.tar.gz.
File metadata
- Download URL: django_admin_reversefields-0.1.0.tar.gz
- Upload date:
- Size: 16.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3a639067aa655f8c349f9ed69d4aba9f5a125f884716a71842ffe79aa5f3d6b0
|
|
| MD5 |
ad5791a58b51c25b423466506628b7fc
|
|
| BLAKE2b-256 |
baa6172daedf54d3e3f0be64a79c8ee45e8f18bda23dc8b44afcc2a56fcbce16
|
File details
Details for the file django_admin_reversefields-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_admin_reversefields-0.1.0-py3-none-any.whl
- Upload date:
- Size: 16.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f24033c6ba7fc245bb92a7d36e7f7fd51db63b9867304bbf32c8b6912b247c5f
|
|
| MD5 |
e01d7b780de58793f9298d443291f79d
|
|
| BLAKE2b-256 |
0e118723773d7746665a4a11e68a6622c326d8cd7cf4b883d378b3d45e14cfc4
|