Set Up Many-to-Many Relations ============================= djangocms-custom-content provides two complementary approaches for managing M2M relationships: 1. **``invite_m2m_relations``** - Content model requests relations from target models 2. **``relate_to``** - Model pushes relations onto target models Understanding the Direction ---------------------------- The key difference is **who initiates the relationship**: **``invite_m2m_relations`` (Content Model → Target Model)** - The source model **knows about** and **requests** relations from target models - If the target model is not installed or lacks a relation model, a dummy accessor is created (graceful degradation) - Typical use: A content model inviting a grouper model to participate **``relate_to`` (Model → Unknown Targets)** - The source model **pushes** itself onto target models - Target models are unaware of the source and its configuration - The source unilaterally adds accessors to target models - Typical use: A model relating to multiple content types, including already installed ones. - Since the target model does not know about the source, it will not copy the relation when copied. Hence avoid using ``relate_to`` to relate to potentially versioned content models, since they are copied regularly when new versions are created. Example Comparison ------------------ **Scenario: BlogPostContent wants authors** Using ``invite_m2m_relations`` (content model requests): .. code-block:: python class BlogPostContent(AbstractCustomContent): class CMSConfig: # "I invite Person to give me authors" invite_m2m_relations = [("authors", "my_app.Person")] The source knows: "I want authors from Person". ``Person`` needs to provide the m2m relation through model. Result: ``blog_post_content.authors.add(person)`` **Scenario: FlatCategory relates to BlogPost** Using ``relate_to`` (model pushes to target): .. code-block:: python class FlatCategory(AbstractCustomContent): class CMSConfig: # "I'm adding categories accessor to BlogPost" relate_to = [("categories", "my_app.BlogPost")] FlatCategoryRelation = custom_relation_factory(FlatCategory) The ``BlogPost`` doesn't know about ``FlatCategory`` internals. ``FlatCategory`` has to provide the m2m relation through model. Result: ``blog_post.categories.add(category)`` Approach 1: Using ``invite_m2m_relations`` (Content Model Requests) ------------------------------------------------------------------ Use ``invite_m2m_relations`` when: - Your content model knows about and depends on target models - You want graceful handling if targets are not available - Target models should provide a specific relation interface **Example: BlogPostContent inviting authors from Person** .. code-block:: python from djangocms_custom_content.models import ( AbstractCustomContent, custom_relation_factory, ) class BlogPostContent(AbstractCustomContent): """Blog post content with versioning.""" post = models.ForeignKey("my_app.BlogPost", on_delete=models.CASCADE) title = models.CharField(max_length=200) body = models.TextField() class CMSConfig: enable_versioning = True # Invite authors from Person model invite_m2m_relations = [ ("authors", "my_app.Person"), ] def __str__(self): return self.title Usage: .. code-block:: python blog_post_content = BlogPostContent.objects.first() person = Person.objects.first() blog_post_content.authors.add(person) blog_post_content.authors.all() # QuerySet of Person objects **Graceful Degradation:** If ``Person`` model is not installed or lacks a relation model, ``invite_m2m_relations`` creates a dummy accessor that returns empty querysets instead of failing. Approach 2: Using ``relate_to`` (Model Pushes to Target) ------------------------------------------------------- Use ``relate_to`` when: - Your model should relate to target models independently - Target models don't need to know about your model - You want to add accessors to multiple unrelated models **Example: FlatCategory adding categories to BlogPost** .. code-block:: python class FlatCategory(AbstractCustomContent): """A simple category model.""" title = models.CharField(max_length=100) slug = models.SlugField(unique=True) class CMSConfig: # Push categories accessor to BlogPost relate_to = [ ("categories", "my_app.BlogPost"), ("categories", "my_app.Article"), # Multiple targets possible ] def __str__(self): return self.title # Create the relation model (required) FlatCategoryRelation = custom_relation_factory(FlatCategory) Usage: .. code-block:: python blog_post = BlogPost.objects.first() category = FlatCategory.objects.first() blog_post.categories.add(category) blog_post.categories.all() # QuerySet of FlatCategory objects Key Differences --------------- .. list-table:: :widths: 25 38 37 :header-rows: 1 * - Aspect - ``invite_m2m_relations`` - ``relate_to`` * - Semantic - "I request from you" - "I push to you" * - Initiation - Source knows target - Source may not know target * - Direction - Content model → Target - Model → Unknown targets * - If target unavailable - Creates dummy accessor - Skips silently (LookupError) * - Typical use - Authors, Contributors - Categories, Tags * - Target awareness - Target expects source - Target unaware of source * - Best for - Related models with awareness - Simple models relating widely Using the Accessors ------------------- Both provide identical manager interfaces: .. code-block:: python # add() - Add objects model.accessor.add(obj1, obj2) # all() - Get all related related = model.accessor.all() # filter() - Filter related related = model.accessor.all().filter(name="Django") # remove() - Remove specific model.accessor.remove(obj1) # clear() - Remove all model.accessor.clear() # count() - Count relations count = model.accessor.all().count() # exists() - Check existence if model.accessor.all().exists(): ... In Templates ~~~~~~~~~~~~ .. code-block:: django {# invite_m2m_relations example #} {% for author in blog_post_content.authors.all %} {{ author.get_admin_content.name }} {% endfor %} {# relate_to example #} {% for category in blog_post.categories.all %} {{ category.title }} {% endfor %} Multiple Accessors ------------------- You can define multiple accessors from a single model: **Multiple targets with ``relate_to``:** .. code-block:: python class Tag(AbstractCustomContent): name = models.CharField(max_length=100) class CMSConfig: relate_to = [ ("tags", "blog.BlogPost"), ("tags", "article.Article"), ("tags", "product.Product"), ] TagRelation = custom_relation_factory(Tag) **Multiple accessors from different target with ``invite_m2m_relations``:** .. code-block:: python class PersonContent(AbstractCustomContent): class CMSConfig: invite_m2m_relations = [ ("authors", "blog.BlogPost"), ("reviewers", "article.Article"), ] When to Use Which ----------------- **Use ``invite_m2m_relations`` when:** - Your content model needs specific relations - You want graceful handling of missing targets - Relations are a desired extension to your model's logic - Target models are expected to have relation support **Use ``relate_to`` when:** - You want to add relations to models you don't control - The source model should work independently - You're adding a cross-cutting concern (categories, tags) - Target models shouldn't need to know about your model See Also -------- - :doc:`../reference/index` - API reference - :doc:`../explanation/relationships` - Relationship architecture - :doc:`../tutorials/model_with_m2m` - Step-by-step tutorial