Recipes¶
Single binding (Company ↔ Department)¶
from django.contrib import admin
from django.db.models import Q
from django_admin_reversefields.mixins import ReverseRelationAdminMixin, ReverseRelationConfig
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)
@admin.register(Company)
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
reverse_relations = {
"department_binding": ReverseRelationConfig(
model=Department,
fk_field="company",
limit_choices_to=unbound_or_current,
# Add bulk=True for better performance with large datasets
# bulk=True, # Uncomment if you don't need model signals
)
}
fieldsets = (("Departments", {"fields": ("department_binding",)}),)
Note
Rendering Rules
If you declare
fieldsetsorfields, include the virtual name (e.g.,"department_binding") so Django renders it.If neither is declared, Django renders all form fields and the injected virtual fields appear automatically. The mixin appends the virtual names in
get_fieldsand injects their form fields inget_form.If you override
get_fieldswithout callingsuper(), or you return a customfieldslist that omits the virtual names, the admin template will not render them (even though the form contains them).
Multiple binding (Company ↔ Projects)¶
from django.db.models import Q
def available_projects_queryset(queryset, instance, request):
if instance and instance.pk:
return queryset.filter(Q(company__isnull=True) | Q(company=instance))
return queryset.filter(company__isnull=True)
@admin.register(Company)
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
reverse_relations = {
"assigned_projects": ReverseRelationConfig(
model=Project,
fk_field="company",
multiple=True,
ordering=("name",),
limit_choices_to=available_projects_queryset,
# Enable bulk operations for better performance with many projects
# bulk=True, # Uncomment if you don't need model signals
)
}
fieldsets = (("Projects", {"fields": ("assigned_projects",)}),)
# Rendering rules are the same as the single-binding recipe.
# Ensure all updates occur as a single unit (default True)
reverse_relations_atomic = True
Validation hooks (business rules)¶
Forbid unbinding unless a condition is met:
from django import forms
def forbid_unbind(instance, selection, request):
if selection is None:
raise forms.ValidationError("Cannot unbind department right now")
@admin.register(Company)
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
reverse_relations = {
"department_binding": ReverseRelationConfig(
model=Department,
fk_field="company",
multiple=False,
clean=forbid_unbind,
)
}
Permissions on reverse fields¶
Baseline change-policy setup
See also
For a deep dive into the permission system, see the Permissions.
Require change permission on the reverse model, with disabled field mode:
@admin.register(Company)
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
reverse_permissions_enabled = True
reverse_permission_mode = "disable" # or "hide"
reverse_relations = {
"department_binding": ReverseRelationConfig(
model=Department,
fk_field="company",
multiple=False,
)
}
# Optional: let per-field/global policies decide visibility at render time
# (policy must handle selection=None)
# reverse_render_uses_field_policy = True
Adapt legacy has_perm helpers
class CanBindAdapter:
permission_denied_message = "Not allowed to bind this item."
def has_perm(self, request, obj, config, selection):
# delegate to some legacy checker
return legacy_can_bind(request.user, selection)
@admin.register(Company)
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
reverse_permissions_enabled = True
reverse_relations = {
"department_binding": ReverseRelationConfig(
model=Department,
fk_field="company",
multiple=False,
permission=CanBindAdapter(),
)
}
Customise has_reverse_change_permission()
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
reverse_permissions_enabled = True
def has_reverse_change_permission(self, request, obj, config, selection=None):
# Example: object-level check (works with backends that support object perms)
app, model = config.model._meta.app_label, config.model._meta.model_name
codename = f"{app}.change_{model}"
if selection is None:
return request.user.has_perm(codename)
if isinstance(selection, (list, tuple)):
return all(request.user.has_perm(codename, s) for s in selection)
return request.user.has_perm(codename, selection)
Swap the permission codename
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
reverse_permissions_enabled = True
def has_reverse_change_permission(self, request, obj, config, selection=None):
app, model = config.model._meta.app_label, config.model._meta.model_name
return request.user.has_perm(f"{app}.add_{model}") # require add instead of change
Callable per-field policy
# Callable policy per field (signature must include config)
def only_allow_special(request, obj, config, selection):
return getattr(selection, "name", "") == "Special"
@admin.register(Company)
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
reverse_permissions_enabled = True
reverse_relations = {
"department_binding": ReverseRelationConfig(
model=Department,
fk_field="company",
multiple=False,
permission=only_allow_special,
# Optional field override for the message; otherwise the policy
# object's message (if any) or a default will be used.
permission_denied_message="You do not have permission to choose this value.",
)
}
Policy object (Protocol implementation)
class StaffOnlyPolicy:
permission_denied_message = "Staff access required"
def __call__(self, request, obj, config, selection):
return getattr(request.user, "is_staff", False)
@admin.register(Company)
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
reverse_permissions_enabled = True
reverse_relations = {
"department_binding": ReverseRelationConfig(
model=Department,
fk_field="company",
multiple=False,
permission=StaffOnlyPolicy(),
)
}
Global policy (admin-wide)
def can_bind(request, obj, config, selection):
# Example: require a custom permission codename on the reverse model
app = config.model._meta.app_label
model = config.model._meta.model_name
return request.user.has_perm(f"{app}.can_bind_{model}")
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
reverse_permissions_enabled = True
reverse_permission_policy = staticmethod(can_bind)
Note
Using staticmethod prevents Python from binding self to the callable.
The mixin expects a callable with the signature (request, obj, config, selection).
Alternative (instance assignment)
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
reverse_permissions_enabled = True
def get_form(self, request, obj=None, **kwargs):
# Assign policy on the instance; no staticmethod needed
self.reverse_permission_policy = can_bind
return super().get_form(request, obj, **kwargs)
OneToOneField (Company ↔ CompanySettings)¶
def only_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 = {
"settings_binding": ReverseRelationConfig(
model=CompanySettings,
fk_field="company",
multiple=False,
required=True,
limit_choices_to=only_unbound_or_current,
)
}
fieldsets = (("Settings", {"fields": ("settings_binding",)}),)
Request-aware validation hook (use request.user)¶
Sometimes validation must depend on the current user or other request context.
The clean hook receives the request so you can implement user-specific rules.
from django import forms
from django.contrib import admin
from django_admin_reversefields.mixins import ReverseRelationAdminMixin, ReverseRelationConfig
def staff_only(instance, selection, request):
if not getattr(request, "user", None) or not request.user.is_staff:
raise forms.ValidationError("Not permitted")
@admin.register(Company)
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
reverse_relations = {
"department_binding": ReverseRelationConfig(
model=Department,
fk_field="company",
multiple=False,
required=False,
clean=staff_only, # gets (instance, selection, request)
)
}
If the user is not staff, the form will include a field error on department_binding
with the message “Not permitted” and the update will be blocked.
Render-time policy (control visibility/editability before selection)¶
Enable reverse_render_uses_field_policy to let per-field or global policies
decide whether a virtual field is visible or editable at render time. This is
useful when you want to hide or disable a field based on request.user or
other context before any selection exists.
from django.contrib import admin
from django_admin_reversefields.mixins import (
ReverseRelationAdminMixin,
ReverseRelationConfig,
)
# Hide binding from non-staff users at render time
def staff_only_policy(request, obj, config, selection):
return getattr(request.user, "is_staff", False)
@admin.register(Company)
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
reverse_permissions_enabled = True
reverse_permission_mode = "hide" # or "disable"
reverse_render_uses_field_policy = True
reverse_relations = {
"department_binding": ReverseRelationConfig(
model=Department,
fk_field="company",
multiple=False,
permission=staff_only_policy,
)
}
With hide mode, a field is removed entirely for non-staff users. Switch to
disable to keep it visible but read-only. Policies are evaluated with
selection=None during render.
Bulk operations for performance optimization¶
When managing large numbers of reverse relationships, enable bulk mode to use
Django’s .update() method instead of individual model saves. This provides
significant performance improvements but bypasses model signals.
from django.contrib import admin
from django.db.models import Q
from django_admin_reversefields.mixins import ReverseRelationAdminMixin, ReverseRelationConfig
def available_departments_queryset(queryset, instance, request):
if instance and instance.pk:
return queryset.filter(Q(company__isnull=True) | Q(company=instance))
return queryset.filter(company__isnull=True)
@admin.register(Company)
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
reverse_relations = {
# Single-select with bulk operations
"primary_department": ReverseRelationConfig(
model=Department,
fk_field="company",
bulk=True, # Use bulk operations for performance
limit_choices_to=available_departments_queryset,
),
# Multi-select with bulk operations - ideal for large datasets
"all_projects": ReverseRelationConfig(
model=Project,
fk_field="company",
multiple=True,
bulk=True, # Bulk operations for multiple selections
ordering=("name",),
limit_choices_to=available_departments_queryset,
),
# Mixed configuration - some bulk, some individual
"critical_departments": ReverseRelationConfig(
model=Department,
fk_field="company",
multiple=True,
bulk=False, # Keep individual saves for signal processing
ordering=("name",),
)
}
fieldsets = (
("Department Assignments", {"fields": ("primary_department", "all_projects")}),
("Critical Departments", {"fields": ("critical_departments",)}),
)
Note
Performance Guidelines
Use bulk mode when: Managing hundreds/thousands of objects, performance is critical, no signal dependencies
Avoid bulk mode when: Models rely on
pre_save/post_savesignals, need granular error handlingMixed approach: Use bulk for high-volume fields, individual saves for signal-dependent fields
Performance comparison example
# Example performance difference with 1000 Department objects:
# Individual saves (bulk=False):
# - 1000+ database queries (one per save)
# - All model signals triggered
# - ~2-5 seconds for large operations
# Bulk operations (bulk=True):
# - 2 database queries (one unbind, one bind)
# - No model signals triggered
# - ~0.1-0.2 seconds for same operation
# Use bulk mode for this scenario:
reverse_relations = {
"department_assignments": ReverseRelationConfig(
model=Department,
fk_field="company",
multiple=True,
bulk=True, # 10-50x performance improvement
ordering=("name",),
)
}
Warning
Signal Bypass Warning: Bulk operations use .update() which bypasses:
pre_saveandpost_savesignalsModel
save()method overridesCustom validation in
save()methodsAudit logging that depends on save signals
Only enable bulk mode when these features aren’t required for your reverse relationship model.
Third-party widgets (AJAX search)¶
End-to-end DAL integration
See also
See Advanced for more details on widget customisation.
You can use any compatible third-party widget, which is especially useful for fields with many choices. The example below shows a conceptual integration with django-autocomplete-light to provide an AJAX-powered search widget.
First, define an autocomplete view.
# In forms.py or a dedicated file
from dal import autocomplete
from .models import Department
class DepartmentAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
# Don't forget to filter out results based on user permissions
if not self.request.user.is_authenticated:
return Department.objects.none()
qs = Department.objects.all()
if self.q:
qs = qs.filter(name__icontains=self.q)
return qs
Next, register the autocomplete view’s URL.
# In urls.py
from .forms import DepartmentAutocomplete
urlpatterns = [
path(
"department-autocomplete/",
DepartmentAutocomplete.as_view(),
name="department-autocomplete",
),
]
Finally, configure the reverse relation to use the widget.
# In admin.py
from dal import autocomplete
@admin.register(Company)
class CompanyAdmin(ReverseRelationAdminMixin, admin.ModelAdmin):
reverse_relations = {
"department_binding": ReverseRelationConfig(
model=Department,
fk_field="company",
# The widget is instantiated with the URL of the autocomplete view
widget=autocomplete.ModelSelect2(url="department-autocomplete"),
)
}
fieldsets = (("Departments", {"fields": ("department_binding",)}),)
class Media:
js = ("admin/js/jquery.init.js",) # Ensure jQuery is loaded
Note
The admin must include the necessary form media for the widget to work. Ensure jQuery is loaded if your widget depends on it.