Describing Relationships: Django's ManyToMany Through

In this post I'll describe extending many-to-many relationships in Django to support additional columns on the junction table. I'll be using the following table structure as the starting place. Imagine an RSS aggregator consisting of feeds, articles and categories. Articles come from a feed and may belong to any number of categories.

ManyToMany

Suppose I wanted to implement a white-listing feature, wherein articles would only get added to categories if they matched a list of keywords assigned to that category. This is a good time to use a many-to-many through relationship. The idea here is that we are describing the intersection of two objects:

ManyToMany Through

The intersection of feeds and categories contains an extra piece of data: which filter to apply to that feed before inserting articles into the category.

Django Implementation

Here is how the schema translates into Django model definitions:

class Feed(models.Model):
    name = models.CharField(max_length=255)
    url = models.URLField()
    # Note the `through` keyword argument:
    categories = models.ManyToManyField(Category, through='FeedCategoryRelationship')
    source = models.ForeignKey(Source)
    last_download = models.DateField(auto_now=True)
    new_articles_added = models.PositiveSmallIntegerField(default=0, editable=False)
    active = models.BooleanField(default=True)
    ...

class FeedCategoryRelationship(models.Model):
    feed = models.ForeignKey(Feed)
    category = models.ForeignKey(Category)
    white_list = models.ManyToManyField(WhiteListFilter, blank=True)
    ...

Accessing related data is a snap:

def perform_download(self):
    """Download articles associated with this feed"""
    for category in self.categories.all():
        # Directly query the junction model:
        relationship_queryset = FeedCategoryRelationship.objects.filter(feed=self, category=category)

        for relationship in relationship_queryset.all():
            whitelist = []
            for white_list in relationship.white_list.all():
                whitelist += white_list.keywords.split(',')
            ...

Admin Interface

ManyToMany Through Admin

By default, the FeedCategoryRelationship is not exposed in either the Category or the Feed admin. To include the junction table in the Feed and Category admin interfaces, you can create an Inline:

class FeedCategoryRelationshipInline(admin.TabularInline):
    model = FeedCategoryRelationship
    extra = 1

class FeedAdmin(admin.ModelAdmin):
    inlines = (FeedCategoryRelationshipInline,)

class CategoryAdmin(admin.ModelAdmin):
    inlines = (FeedCategoryRelationshipInline,)
    prepopulated_fields = { "slug": ("name",) }

Further Reading

Comments (0)


Commenting has been closed.