Set Up Many-to-Many Relations ============================= Relating content to content takes one line. Add a :class:`~djangocms_custom_content.relations.RelationField` to a **grouper** model — it reads just like Django's ``ManyToManyField``: .. code-block:: python from djangocms_custom_content.relations import RelationField class BlogPost(AbstractCustomGrouper): authors = RelationField("people.Person", related_name="authored_posts", ordered=True) Run ``makemigrations`` and you're done — ``post.authors.add(person)`` and ``person.authored_posts.all()`` both work, and the admin renders ``authors`` as a sortable autocomplete. The rest of this guide unpacks the options. Why a custom field? ------------------- 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. A single relation can also target groupers of any type, because storage uses a ``GenericForeignKey`` for the target. Mental model: it is django-taggit, but the "tags" are grouper objects and each relation gets its own through table. Declaring a relation --------------------- Declare ``RelationField`` on the grouper that *owns* the relation: .. code-block:: python from djangocms_custom_content.models import AbstractCustomGrouper from djangocms_custom_content.relations import RelationField class BlogPost(AbstractCustomGrouper): authors = RelationField( "people.Person", related_name="authored_posts", ordered=True, ) categories = RelationField( "categories.FlatCategory", related_name="blog_posts", ) ``RelationField`` arguments: ``target`` A model class or an ``"app_label.Model"`` string. You may name either the grouper or its content model; both resolve to the grouper. The string form is resolved lazily, so the target app need not be importable at declaration time. ``related_name`` Installs a reverse accessor of this name on the *target* grouper. The target model does not import or declare anything — the reverse accessor simply appears once the app registry is ready. ``ordered`` (default ``False``) Opt in to an explicit ordering column on the through table. Only then is a :meth:`~djangocms_custom_content.relations.RelationManager.reorder` method available and results returned in stored order. ``through_name`` (optional) Override the auto-generated through model class name. Each ``RelationField`` creates its own concrete through table with a uniqueness constraint (no duplicate edges) and an index for fast reverse lookups. No migration boilerplate is required beyond running ``makemigrations`` for the app that declares the field. Using the accessors ------------------- The forward accessor lives on the owner grouper; the reverse accessor (if ``related_name`` was given) lives on the target grouper. Both return real querysets, so ``.filter()``, ``.order_by()``, ``.count()`` and friends just work. .. code-block:: python post = BlogPost.objects.first() person = Person.objects.first() # Write API — accepts a grouper OR a content object (normalised to grouper) post.authors.add(person) post.authors.remove(person) post.authors.set([person]) post.authors.clear() # Read API — backed by a queryset of target groupers post.authors.all() post.authors.filter(...) post.authors.count() post.authors.exists() # Reverse accessor invited by related_name person.authored_posts.all() # BlogPost groupers authored by this person Ordered relations ----------------- When the field is declared with ``ordered=True``, ``add()`` appends to the end and you can set an explicit order with ``reorder()``: .. code-block:: python post.authors.reorder([alice, bob, carol]) post.authors.all() # alice, bob, carol ``reorder()`` raises ``TypeError`` on a relation that is not ordered. In templates ~~~~~~~~~~~~ Because accessors return groupers, reach the displayable content through the grouper's ``get_admin_content`` (or ``get_content``): .. code-block:: django {% for person in post.authors.all %} {% with profile=person.get_admin_content %} {{ profile.name }} {% endwith %} {% endfor %} {% for category in post.categories.all %} {{ category.title }} {% endfor %} .. note:: ``FlatCategory`` is a grouper-less content model, so its accessor yields the ``FlatCategory`` objects directly (``category.title`` above), while ``Person`` is a grouper, so its content is reached via ``get_admin_content``. Deleting targets ---------------- The concrete source foreign key cascades natively. The generic target has no database constraint, so when a target grouper is deleted the framework sweeps its dangling relation rows automatically. Copying ------- Edges are anchored to the grouper, so **creating a new version copies nothing** — the new version sees the same relations. They are only not carried over when you **duplicate the grouper itself** (just like a Django ``ManyToManyField``); copy them explicitly in that case: .. code-block:: python 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()) A full duplicate usually also re-creates the grouper's **content** object(s) (pointed at ``new_grouper``) — a brand-new grouper has no content of its own. See :doc:`../explanation/relationships` for the full rationale. See Also -------- - :doc:`../reference/index` - API reference - :doc:`../explanation/relationships` - How the relation storage works - :doc:`../tutorials/model_with_m2m` - Step-by-step tutorial