Relationships
Relationships appear inside the record's create form and unified record page. They save
with scalar fields through Create record or Save changes. One-to-one and many-to-one relations use a single selector; one-to-many
and many-to-many relations allow multiple selections. Targets must already exist.
The record page displays values and allows editing immediately when permitted. Users with only
view permission see the same page with read-only fields and relationships.
Supported backends: synchronous/asynchronous SQLAlchemy, synchronous/asynchronous SQL reflection,
Peewee, Tortoise and GINO 1.0.x / PostgreSQL. ORM adapters use native ORM operations; SQL reflection uses database constraints.
Scalar changes and all submitted relationship changes commit together or roll back together.
from quite_admin import ModelOptions
admin.register(Product, options=ModelOptions(
relations=("tags",),
readonly_relations=(),
record_label="name",
))
relations=None discovers relationships automatically; relations=() hides relationship editors
in create and edit forms.
readonly_relations displays selected records without allowing changes.
Set record_label on the target model to choose its selector label. Otherwise common names
such as name and title are used, falling back to primary keys. A source field's
relation_label overrides this label for its picker only. Without that option, relationship
editors keep the target model's usual label. Excluded fields are never labels.
Both ends must be registered with the same adapter. A target must allow listing and viewing.
Changing a reverse foreign key also requires edit permission and writable foreign-key fields
on the target. Hidden/read-only source fields and hidden target foreign keys remain protected.
Scalar foreign-key inputs are replaced by selectors when the relationship is displayed.
Omitting a relationship preserves it. Submitting an empty selection clears it when nullable.
Required foreign keys and delete-orphan relationships cannot be detached by the editor;
it never deletes target records. Move a required child by choosing another parent on its own form.
Invalid keys, unavailable targets and failed constraints reject the entire save.
Foreign-key labels
A foreign key to a registered table appears as a link to that record, showing the
stored ID by default. Choose a visible target field with relation_label to use it
in list links. When the relationship editor is enabled, the same option sets the
label for selected records and picker results in create and edit forms:
admin.register(Link, options=ModelOptions(fields={
"user_id": FieldOptions(relation_label="username"),
"organization_id": FieldOptions(relation_label="name"),
}))
Import FieldOptions from quite_admin. This works with native adapters, including
GINO, without a formatter or adapter subclass. List links also use this label with
relations=(); that setting disables relationship editors in forms.
The target must allow listing and viewing or editing for a list link; form pickers
require listing and viewing. Hidden or unavailable label fields fall back to the ID
and are never exposed or searched. Inaccessible, missing or unregistered targets
remain plain values in lists. NULL remains empty. Related list labels are fetched
in one bounded adapter list per FK column, not per row, using the admin's read
deadlines. Labels never replace stored values: saves, filtering, scalar exports
and history continue to use raw IDs.
Search and selection
Use Choose a record for a single relationship or Choose records for multiple
relationships to open a picker. Search uses the target table's visible
search_fields (visible strings by default). If the source field sets relation_label
to a permitted string field, the picker also searches that field, even when it is
absent from the target's search_fields or that setting is empty. An exact ID search
is available independently of search_fields; text and ID matches are combined
before pagination. Results load 25 at a time in stable
primary-key order. Previous/Next reaches the complete candidate set. Enter a
complete JSON object for a composite key. Hidden keys do not enable the ID shortcut. The options endpoint
returns keys, permitted labels, visible identity and searchable-field descriptions;
list filters display the additional context.
Selected records appear separately from search results, with a count. They remain
selected across search queries and pages; × removes one selection and Clear selection
clears the current selection. Single relationships use radio buttons; multiple relationships
use checkboxes, and the picker stays open while adding records. The picker supports
Tab, arrow keys, Space/Enter and Escape. Searching never submits the record form.
Selections are committed together with scalar fields by Save changes.
The compact search panel has a limited width rather than spanning the whole form.
It opens over the form without shifting its fields; when there is more room above
the selector, it opens upward. Click anywhere on a result row, including its text,
to select a single record or toggle a record in a multiple selection. Change selection
reopens the panel for an existing single selection. Click outside the picker, press
Escape or Tab out of it to close the panel; the current selection is preserved.
Up to 1000 records can be selected in one relationship. Larger existing memberships
are read-only to prevent partial updates. Without JavaScript, the native selector
contains the first 25 candidates plus existing selections; enable JavaScript to search
all records. A failed request preserves selections and offers Retry.
The read-only options endpoint checks parent create/edit permission, target list/view
permission, field restrictions and reverse-link edit permission on every request.
Search does not include hidden fields. The built-in script is self-hosted and loaded
with SRI; CSP permits its exact SHA-256 hash on editable relationship forms only.
Inline scripts and arbitrary same-origin scripts remain blocked. Custom form.html
overrides can reuse the standard picker markup, picker_messages and editor.endpoint;
base.html automatically loads the supplied scripts.
Reflection recognizes pure many-to-many junctions with two foreign-key constraints and a
unique combined key. Junctions with extra payload columns require an application adapter.
One-to-one detection accepts primary keys, unique constraints and full unique indexes;
partial unique indexes do not guarantee global cardinality. MySQL represents reflected
unique constraints as indexes, which are supported as well.
SQLAlchemy uses mapped relationship() definitions; view-only and dynamic/write-only relationships
are read-only. Peewee uses foreign keys, backrefs and ManyToManyField; Tortoise uses native relations.
Custom adapters may optionally implement the exported RelationAdapter protocol:
relations(), paginated related() and atomic save_relations(). The scalar Adapter contract
is unchanged. Metadata is represented by Relation and has no template dependencies.
The local demo includes Product–Tag (M:N), Customer–Order (1:N) and Customer–Profile (1:1).
Use list filters to find records linked to a target without
editing a relationship. The database schema shows relationship
structure and junctions independently of record values.