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.

Required imports in 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:

Minimal admin exposing two reverse bindings
@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")}),
    )
  1. reverse_relations is a dict keyed by virtual field name.

  2. Each ReverseRelationConfig declares the reverse model, the ForeignKey that points back, and any optional UI behaviour (labels, widgets, ordering).

  3. Include the virtual field names inside fieldsets (or fields) 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_to accepts either a callable (queryset, instance, request) -> queryset or a dict that is passed to filter().

  • multiple=True switches a field to a ModelMultipleChoiceField and synchronises the entire set on save.

  • widget can be any Django form widget (including AJAX options such as Django Autocomplete Light). See Advanced for an end-to-end walkthrough.

  • bulk=True enables 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_save or post_save signals

  • ❌ You need granular error handling per object

  • ❌ Small datasets where performance isn’t a concern

What happens on save?

Form lifecycle
  1. During get_form() the mixin injects the virtual fields and computes their querysets.

  2. The generated form sets initial selections based on currently bound objects.

  3. When reverse_permissions_enabled is true, permission policies run at render, validation, and persistence time to guard the field.

  4. On form.save(), the mixin applies transactional updates—unbind first, then bind—to keep the database consistent. When bulk=True, operations use .update() for better performance. For details, see Data Integrity & Transactions.

Next steps

Build on the basics