Quickstart¶
Get up and running with
ReverseRelationAdminMixin in just a
few steps.
See also
Explore Core Concepts for the lifecycle of the injected virtual fields and dive into Recipes for end-to-end examples, including permissions and validation hooks.
Install the package¶
python -m pip install django-admin-reversefields
uv pip install django-admin-reversefields
Note
The docs extras (sphinx extensions) are listed in
docs/requirements.txt. Install them alongside the package when you plan
to build the documentation locally.
Wire up your admin¶
Import the mixin and declare a mapping of virtual field
names to ReverseRelationConfig
objects. Each configuration describes which reverse-side model and
ForeignKey should be controlled from the parent admin.
admin.py¶from django.contrib import admin
from django_admin_reversefields.mixins import (
ReverseRelationAdminMixin,
ReverseRelationConfig,
)
from .models import Assignment, Company, CompanySettings, Department, Employee, Project
Register your ModelAdmin by inheriting from the
mixin and declaring at least one reverse relation:
@admin.register(Company)
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
"""
Company admin following quickstart patterns.
Following the quickstart guide step-by-step:
1. Inherit from ReverseRelationAdminMixin and admin.ModelAdmin
2. Declare reverse_relations dict with virtual field names as keys
3. Include virtual field names in fieldsets
"""
# Step 1: Declare reverse_relations dict keyed by virtual field name
reverse_relations = {
# Single department selection - following minimal example pattern
"departments": ReverseRelationConfig(
model=Department,
fk_field="company",
),
# Multiple projects - using multiple=True from configuration highlights
"projects": ReverseRelationConfig(
model=Project,
fk_field="company",
multiple=True,
),
}
# Step 2: Include virtual field names in fieldsets as instructed
fieldsets = (
("Company Information", {"fields": ("name", "founded_year")}),
("Related Items", {"fields": ("departments", "projects")}),
)
reverse_relationsis adictkeyed by virtual field name.Each
ReverseRelationConfigdeclares the reverse model, theForeignKeythat points back, and any optional UI behaviour (labels, widgets, ordering).Include the virtual field names inside
fieldsets(orfields) so Django renders them on the form. When neither is declared, Django will render all fields automatically.
Warning
If the underlying reverse ForeignKey is null=False you must set
required=True on the virtual field to avoid
django.db.IntegrityError when a user attempts to unbind the
relation. See Caveats for details.
Limit choices per request¶
The mixin resolves querysets on demand, so your limiter can be request-aware and object-aware while still including items that are already bound to the current instance.
from django.db.models import Q
def unbound_or_current(queryset, instance, request):
if instance and instance.pk:
return queryset.filter(Q(company__isnull=True) | Q(company=instance))
return queryset.filter(company__isnull=True)
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
reverse_relations = {
"department_binding": ReverseRelationConfig(
model=Department,
fk_field="company",
limit_choices_to=unbound_or_current,
)
}
Configuration highlights
limit_choices_toaccepts either a callable(queryset, instance, request) -> querysetor adictthat is passed tofilter().multiple=Trueswitches a field to aModelMultipleChoiceFieldand synchronises the entire set on save.widgetcan be any Django form widget (including AJAX options such as Django Autocomplete Light). See Advanced for an end-to-end walkthrough.bulk=Trueenables bulk operations using.update()for better performance with large datasets, but bypasses model signals.
Enable bulk operations for performance¶
For large datasets where model signals aren’t required, enable bulk mode to use
Django’s .update() method instead of individual saves:
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
reverse_relations = {
# Single-select with bulk operations
"department_binding": ReverseRelationConfig(
model=Department,
fk_field="company",
bulk=True, # Use .update() for better performance
limit_choices_to=unbound_or_current,
),
# Multi-select with bulk operations
"assigned_projects": ReverseRelationConfig(
model=Project,
fk_field="company",
multiple=True,
bulk=True, # Bulk operations for multiple selections
ordering=("name",),
)
}
Warning
When to use bulk mode:
✅ Large datasets (hundreds/thousands of objects)
✅ Performance is critical
✅ No dependency on model signals (
pre_save,post_save, etc.)
When NOT to use bulk mode:
❌ Your models rely on
pre_saveorpost_savesignals❌ You need granular error handling per object
❌ Small datasets where performance isn’t a concern
What happens on save?¶
Form lifecycle
During
get_form()the mixin injects the virtual fields and computes their querysets.The generated form sets initial selections based on currently bound objects.
When
reverse_permissions_enabledis true, permission policies run at render, validation, and persistence time to guard the field.On
form.save(), the mixin applies transactional updates—unbind first, then bind—to keep the database consistent. Whenbulk=True, operations use.update()for better performance. For details, see Data Integrity & Transactions.
Next steps¶
Build on the basics
Follow the Single binding (Company ↔ Department) and Multiple binding (Company ↔ Projects) walkthroughs for complete admin setups.
Enforce permissions with Permissions and Permissions on reverse fields.
Consult the API reference when you need to override lifecycle methods such as
has_reverse_change_permission().