ReverseRelationConfig

class django_admin_reversefields.mixins.ReverseRelationConfig(model: type[Model], fk_field: str, label: str | None = None, help_text: str | None = None, required: bool = False, multiple: bool = False, limit_choices_to: Callable[[QuerySet, Any, HttpRequest], QuerySet] | dict[str, Any] | None = None, widget: Widget | type[Widget] | None = None, ordering: Iterable[str] | None = None, clean: Callable[[Model, Any, HttpRequest], None] | None = None, permission: ReversePermissionPolicy | Callable[[HttpRequest, Model | None, ReverseRelationConfig, Any | None], bool] | object | None = None, permission_denied_message: str | None = None, bulk: bool = False)[source]

Bases: object

Configuration for a virtual reverse-relation admin field.

This dataclass describes how a virtual field should be rendered on an admin form to manage a reverse ForeignKey relationship. The virtual field does not exist on the model; instead, it controls one or more rows on the reverse-side model that hold a ForeignKey pointing back to the current object.

model

The reverse-side Django model that holds the ForeignKey.

Type:

type[models.Model]

fk_field

The name of the ForeignKey field on the reverse-side model (the one specified in model) that points back to the current admin object.

Type:

str

label

Optional human-friendly label for the form field. If omitted, a label is derived from model._meta.verbose_name (or plural when multiple is True).

Type:

str | None

help_text

Optional help text displayed with the form field.

Type:

str | None

required

Whether a selection is required. If True, the form will enforce that at least one value is selected (for multi) or a value is present (for single).

Type:

bool

multiple

If True, a multi-select is rendered and the resulting set of objects on the reverse side will be synchronized on save. If False, a single-select is rendered and only one object may point to the current instance (others will be unbound on save).

Type:

bool

limit_choices_to

Either a callable (queryset, instance, request) -> queryset that can apply dynamic, per-request filtering (recommended), or a dict used as queryset.filter(**dict) for static filtering. Common usage: include only unbound objects, plus those already bound to the current instance.

Type:

Callable | dict | None

widget

Optional widget instance or class to use for rendering. Defaults to Django’s forms.Select for single-select and FilteredSelectMultiple for multi-select. You can supply Unfold, DAL, or other custom widgets here.

Type:

forms.Widget | type[forms.Widget] | None

ordering

Optional ordering to apply to the limited queryset (e.g., ("displayName",)).

Type:

Iterable[str] | None

clean

Optional per-field validation hook. When provided, it is invoked from the derived form’s clean() with (instance, selection, request). Raise forms.ValidationError to attach an error to this field and block save; return None for success.

Type:

Callable[[models.Model, Any, HttpRequest], None] | None

permission

Optional per-field permission policy controlling whether the user may modify this virtual field. Supported values include:

  • A callable (request, obj, config, selection) -> bool

  • An object with has_perm(request, obj, config, selection) -> bool

  • An object implementing __call__ following ReversePermissionPolicy

Policy objects may expose permission_denied_message for UI feedback during validation.

Type:

ReversePermissionPolicy | PermissionCallable | object | None

permission_denied_message

Optional custom error message attached to the field when a selection is denied by permission checks during form validation.

Type:

str | None

bulk

When True, use Django’s .update() method for bind/unbind operations instead of individual model saves. This bypasses model signals (pre_save, post_save, etc.) but provides better performance for large datasets. Defaults to False for backward compatibility.

Type:

bool

Example

>>> ReverseRelationConfig(
...     model=Site,
...     fk_field="meraki",
...     label="Site",
...     multiple=False,
...     ordering=("displayName",),
...     limit_choices_to=lambda qs, instance, request: qs.filter(meraki__isnull=True)
... )