Relations Explained¶
djangocms-custom-content models many-to-many relations between content with a
single declarative field,
RelationField, declared on a
grouper model. This page explains how it works under the hood and why it is
designed that way.
Two ideas make it correct and familiar¶
Relations are anchored to the grouper’s stable primary key, never to a versioned content row. djangocms-versioning copies content into new rows with new primary keys; anchoring to the grouper means relations survive version copies untouched.
It leans on the Django ORM. Storage is an ordinary through model with a concrete
sourceForeignKeyand aGenericForeignKeytarget. The accessors return real querysets, so.filter(),.order_by(),.count()and friends just work.
The Through Model¶
A standard Django ManyToManyField generates an automatic through model with
two concrete foreign keys — one to each side. That only works for relating to
one specific model type.
Instead, relation_through_factory()
builds a concrete through model per relation that uses:
one concrete ForeignKey (
source) to the owning grouper, andone GenericForeignKey (
target, viacontent_type+object_id) that can point at a grouper of any type.
Column |
Purpose |
|---|---|
|
FK to the owning grouper (e.g. |
|
Which grouper type the edge points at |
|
Primary key of the target grouper |
|
GenericForeignKey resolving |
|
Position column, only when |
Each through table carries a uniqueness constraint over
(source, content_type, object_id) so an edge cannot be duplicated, plus an
index on (content_type, object_id) for fast reverse lookups. Ordered
relations use the OrderedCustomRelation
base, which adds the indexed order column.
Grouper resolution¶
A RelationField target may name either a grouper or its content model. Both
resolve to the grouper via
grouper_model_of(), which finds the
foreign key from a content model to its
AbstractCustomGrouper. Likewise,
add() / remove() accept either a grouper or a content instance and store
the stable grouper primary key (grouper_of). A model with no grouper (such as
the standalone FlatCategory) simply resolves to itself.
Forward and reverse accessors¶
Declaring the field installs a
RelationManager as the forward
accessor on the owner. If related_name is given, a
ReverseRelationManager is installed
on the target grouper once the app registry is ready — the target model imports
or declares nothing.
class BlogPost(AbstractCustomGrouper):
authors = RelationField(
"people.Person", related_name="authored_posts", ordered=True,
)
post.authors.all() # forward: Person groupers for this post
person.authored_posts.all() # reverse: BlogPost groupers for this person
Both managers expose the same read API (all, filter, count,
exists, iteration) and write API (add, remove, clear; the
forward manager additionally has set and, for ordered relations,
reorder). Reads always go through a genuine queryset of the related groupers.
Target-side cascade¶
The concrete source foreign key cascades natively on delete. The generic
target has no database-level constraint, so a post_delete receiver sweeps
dangling rows when a target grouper is deleted. It is cheap: skipped entirely
unless relation tables exist, and it only runs for grouper instances.
Copying: versioning vs. duplicating a grouper¶
Because edges live on the grouper, there are two distinct cases — and only one of them needs your attention:
Creating a new version (djangocms-versioning copies the content row): the grouper keeps its primary key, so the edges are untouched and the new version sees the same relations. Nothing to do — this is the point of anchoring to the grouper.
Duplicating the grouper itself (a hand-rolled “duplicate this object” action that creates a new grouper): edges are not copied automatically, exactly as a Django
ManyToManyFieldis not copied when you save an instance withpk=None. Copy them explicitly:from djangocms_custom_content.relations import iter_relation_fields for name, _field in iter_relation_fields(type(source_grouper)): getattr(new_grouper, name).set(getattr(source_grouper, name).all())
.set()re-adds in queryset order, soordered=Truerelations keep their positions.Copying edges is only half of it: a fresh grouper has no content, since the editable data lives in the content rows, not the grouper. A complete “duplicate this object” also re-creates the relevant content object(s) — typically one per language, or a new draft version — pointing their grouper foreign key at
new_grouperbefore (or after) you copy the relations. How many content rows to copy is app-specific, so the framework leaves it to you.
See Also¶
Set Up Many-to-Many Relations - Practical implementation guide
Model with M2M Relations - Step-by-step tutorial
Reference - API reference