M2M Relationships Explained

djangocms-custom-content provides flexible many-to-many relationships through two approaches that differ in direction and semantics:

  1. invite_m2m_relations - Content model requests relations from target models (pull)

  2. relate_to - Model pushes relations onto target models (push)

The Through Model: custom_relation_factory

Traditional M2M relationships in Django use an automatic through model (join table) that links two specific models:

# Standard Django M2M
class Article(models.Model):
    authors = models.ManyToManyField(Author)  # Creates automatic through model

The through model has two ForeignKeys (one to each side).

Problem: This only works for relating to ONE specific model type. You can’t use one M2M field for relating to different model types.

Solution: custom_relation_factory

djangocms-custom-content creates a manual through model that uses:

  • One ForeignKey to the source model (specific)

  • One GenericForeignKey to ANY model (flexible)

This enables relating a source model to objects of ANY type.

How it Works

from djangocms_custom_content.models import custom_relation_factory

class PersonContent(AbstractCustomContent):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    # ... other fields ...

# Manually create the through model for the Grouper (Person)
PersonRelation = custom_relation_factory(Person)

Generated Through Model Structure

The custom_relation_factory creates a relation model like:

class PersonContentRelation(AbstractCustomRelation):
    # FK to source (specific model)
    instance = models.ForeignKey(PersonContent, on_delete=models.CASCADE)

    # Can relate to ANY model
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

The GenericForeignKey Magic

Field

Purpose

instance

FK to PersonContent (always the same model)

content_type

Stores which model type is related (BlogPost, Article, etc.)

object_id

Stores the ID of the related object

content_object

Virtual field that returns the actual object (BlogPost #5, Article #3, etc.)

Example Data

One PersonContentRelation table can store relations to MULTIPLE model types:

-- Person 2 as author of BlogPost 1
INSERT INTO person_content_relation
  (instance_id, content_type_id, object_id)
VALUES (2, 42, 1);  -- content_type_id 42 = BlogPost

-- Person 2 as author of Article 5
INSERT INTO person_content_relation
  (instance_id, content_type_id, object_id)
VALUES (2, 43, 5);  -- content_type_id 43 = Article

-- Person 2 as author of Product 3
INSERT INTO person_content_relation
  (instance_id, content_type_id, object_id)
VALUES (2, 44, 3);  -- content_type_id 44 = Product

Single PersonContent can relate to thousands of BlogPosts, Articles, Products, etc.

Comparison: Django M2M vs. Generic M2M

Django M2M Example

# Separate through models needed for each relation type
class BlogPost(models.Model):
    authors = models.ManyToManyField(Person)      # Through: BlogPost_authors

class Article(models.Model):
    authors = models.ManyToManyField(Person)      # Through: Article_authors

class Product(models.Model):
    creators = models.ManyToManyField(Person)     # Through: Product_creators

Result: Three separate though tables, each only works for its specific pair.

Generic M2M Example

class PersonContent(AbstractCustomContent):
    pass

# Single through model for ALL relations
# Note: Factory is created for the Grouper (Person), not the Content model
PersonRelation = custom_relation_factory(Person)

# PersonRelation table can manage relations to:
# - BlogPost
# - Article
# - Product
# - ANY Django model

Result: One table handles all relations!

How the Two Approaches Use custom_relation_factory

Both relate_to and invite_m2m_relations use the through model created by custom_relation_factory:

``invite_m2m_relations`` (Content Model Requests)

The source model knows about and requests relations from target models.

Semantics: “I invite you to provide relations for me. Do you have a suitable generic through table?”

Direction: Known target → Source

Flow:

  1. Source model declares invite_m2m_relations listing targets

  2. custom_relation_factory creates through model for source

  3. CustomContentExtension reads the declaration

  4. GenericM2MDescriptor is added to SOURCE model (not target)

  5. If target unavailable: Creates dummy accessor (graceful degradation)

Example:

class BlogPostContent(AbstractCustomContent):
    class CMSConfig:
        # "I request relations from Person"
        invite_m2m_relations = [("authors", "people.Person")]

# Creates the through model with GenericForeignKey
# The factory is for the target model (Person), not the source
PersonRelation = custom_relation_factory(Person)

Result:

blog_post = BlogPostContent.objects.first()
person = Person.objects.first()

# Source model gets the accessor (via GenericM2MDescriptor)
blog_post.authors.add(person)
blog_post.authors.all()  # Uses PersonRelation table

Approach 2: relate_to (Model Pushes to Targets)

The source model pushes itself onto target models you have no control over.

Semantics: “I’m adding an accessor to you, so you can see me.”

Direction: Source → Multiple unknown targets

Flow:

  1. Source model declares relate_to listing target paths

  2. custom_relation_factory creates through model for source

  3. register_m2m_relations() is called

  4. GenericM2MDescriptor is added to TARGET models (not source)

  5. Target models remain unaware of source

Example:

class FlatCategory(AbstractCustomContent):
    class CMSConfig:
        # "I'm adding categories accessor to BlogPost and Article"
        relate_to = [
            ("categories", "blog.BlogPost"),
            ("categories", "article.Article"),
        ]

# Creates the through model with GenericForeignKey
FlatCategoryRelation = custom_relation_factory(FlatCategory)

Result:

blog_post = BlogPost.objects.first()
article = Article.objects.first()
category = FlatCategory.objects.first()

# Target models get the accessor (via GenericM2MDescriptor)
blog_post.categories.add(category)
article.categories.add(category)

# Both use same FlatCategoryRelation table!

Design Differences

Aspect

invite_m2m_relations

relate_to

Who requests

TARGET model

SOURCE model

Semantics

“I request from you”

“I push to you”

Accessor added to

TARGET model

TARGET model(s)

Source awareness

Unaware of target

Knows target

Target awareness

Expected to support it

Unaware of source

If target unavailable

Dummy accessor created

Raises improperly configured exception

Typical sources

Grouper models

Grouper models

Typical targets

Content models

Content models

Accessing Relations

Both provide identical manager interfaces (backed by the through model):

# add() - Add objects
model.accessor.add(obj1, obj2)

# all() - Get all related
related = model.accessor.all()

# filter() - Filter related
related = model.accessor.filter(name="Django")

# remove() - Remove specific
model.accessor.remove(obj1)

# clear() - Remove all
model.accessor.clear()

# count() - Count relations
count = model.accessor.count()

# exists() - Check existence
if model.accessor.exists():
    ...

See Also