Integrations

When integrating polymorphic models into third party apps you have three primary options:

  1. Hope it just works (it might!).

  2. Ensure the querysets the third party apps see are not polymorphic.

  3. Override or extend relevant third party app code to work with polymorphic querysets.

If it does not just work, option 1 is usually the easiest. We provide some integrations in polymorphic.contrib for popular third party apps and provide guidance for others below.

This page does not exhaustively cover all integrations. If you feel your integration need is very common you may consider opening a PR to either provide support in code or documentation here.

This page covers supported and tested integration advice. For all other integration advice please refer to our integrations discussion page.

For the integration examples on this page, we use the following polymorphic model hierarchy:

 1from django.db import models
 2from polymorphic.models import PolymorphicModel
 3
 4
 5class Article(PolymorphicModel):
 6    title = models.CharField(max_length=100)
 7    content = models.TextField()
 8    created = models.DateTimeField(auto_now_add=True)
 9
10    def __str__(self):
11        return self.title
12
13
14class BlogPost(Article):
15    author = models.CharField(max_length=100)
16
17
18class NewsArticle(Article):
19    source = models.CharField(max_length=100)

django-guardian

Added in version 1.0.2.

No special modifications are required to integrate with django-guardian. However, if you would like all object level permissions to be managed at the base model level, rather than have unique permissions for each polymorphic subclass, then you can use the helper function polymorphic.contrib.guardian.get_polymorphic_base_content_type() to unify the permissions for your entire polymorphic model tree into a single namespace a the base level:

GUARDIAN_GET_CONTENT_TYPE = \
    "polymorphic.contrib.guardian.get_polymorphic_base_content_type"

This option requires django-guardian >= 1.4.6. Details about how this option works are available in the django-guardian documentation.

django-extra-views

Added in version 1.1.

The polymorphic.contrib.extra_views package provides classes to display polymorphic formsets using the classes from django-extra-views. See the documentation of:

Tip

The complete working code for this example can be found in the extra_views integration test.

Example View

Here’s how to create a view using PolymorphicFormSetView to handle polymorphic formsets:

 1from polymorphic.contrib.extra_views import PolymorphicFormSetView
 2from polymorphic.formsets import PolymorphicFormSetChild
 3from django.urls import reverse_lazy
 4from ..models import Article, BlogPost, NewsArticle
 5
 6
 7class ArticleFormSetView(PolymorphicFormSetView):
 8    model = Article
 9    template_name = "extra_views/article_formset.html"
10    success_url = reverse_lazy("extra_views:articles")
11    fields = "__all__"
12
13    # extra will add two empty forms for models in the order of their appearance
14    # in formset_children
15    factory_kwargs = {"extra": 2, "can_delete": True}
16
17    formset_children = [
18        PolymorphicFormSetChild(BlogPost, fields="__all__"),
19        PolymorphicFormSetChild(NewsArticle, fields="__all__"),
20    ]

URL Configuration

Configure the URL patterns to route to your formset view:

1from django.urls import path
2from .views import ArticleFormSetView
3
4app_name = "extra_views"
5
6urlpatterns = [
7    path("articles/", ArticleFormSetView.as_view(), name="articles"),
8]

Template

The template for rendering the formset:

{% load extra_views_tags %}
<!DOCTYPE html>
<html>
<head>
    <title>Article Formset</title>
</head>
<body>
    <h1>Article Formset</h1>
    <form method="post">
        {% csrf_token %}
        {{ formset.management_form }}
        {% for form in formset %}
            <div class="formset-form">
                <h3>{{ form.instance|model_name }}</h3>
                {{ form.as_p }}
            </div>
        {% endfor %}
        <button type="submit">Save</button>
    </form>
</body>
</html>

model_name is a template tag implemented like so:

@register.filter
def model_name(instance):
    """Get the model class name of an instance."""
    return instance._meta.verbose_name.title()

django-reversion

Support for django-reversion works as expected with polymorphic models. We just need to do two things:

  1. Inherit our admin classes from both PolymorphicParentModelAdmin / PolymorphicChildModelAdmin and VersionAdmin.

  2. Override the admin/polymorphic/object_history.html template.

Tip

The complete working code for this example can be found in the reversion integration test.

Admin Configuration

The admin configuration combines PolymorphicParentModelAdmin and PolymorphicChildModelAdmin with VersionAdmin:

 1from django.contrib import admin
 2from polymorphic.admin import (
 3    PolymorphicParentModelAdmin,
 4    PolymorphicChildModelAdmin,
 5)
 6from reversion.admin import VersionAdmin
 7from ..models import Article, BlogPost, NewsArticle
 8
 9
10class ArticleChildAdmin(PolymorphicChildModelAdmin, VersionAdmin):
11    base_model = Article
12
13
14@admin.register(BlogPost)
15class BlogPostAdmin(ArticleChildAdmin):
16    pass
17
18
19@admin.register(NewsArticle)
20class NewsArticleAdmin(ArticleChildAdmin):
21    pass
22
23
24class ArticleParentAdmin(VersionAdmin, PolymorphicParentModelAdmin):
25    """
26    Parent admin for Article model with reversion support.
27
28    Note: VersionAdmin must come before PolymorphicParentModelAdmin
29    in the inheritance order.
30    """
31
32    base_model = Article
33    child_models = (BlogPost, NewsArticle)
34    list_display = ("title", "created")
35
36
37admin.site.register(Article, ArticleParentAdmin)

Custom Template

Since both PolymorphicParentModelAdmin and VersionAdmin. define object_history.html template, you need to create a custom template that combines both:

{% extends 'reversion/object_history.html' %}
{% load polymorphic_admin_tags %}

{% block breadcrumbs %}
    {% breadcrumb_scope base_opts %}{{ block.super }}{% endbreadcrumb_scope %}
{% endblock %}

This makes sure both the reversion template is used, and the breadcrumb is corrected for the polymorphic model using the breadcrumb_scope tag.

model-bakery

model-bakery does not work without without special configuration for polymorphic models because it overrides the polymorphic_ctype field. The best option to make it work in all cases is to supply a custom Baker class that fills in all fields except polymorphic_ctype:

yoursite/tests/baker.py
 1from polymorphic.models import PolymorphicModel
 2from model_bakery import baker
 3
 4
 5class PolymorphicAwareBaker(baker.Baker):
 6    """
 7    Our custom model baker ignores the polymorphic_ctype field on all polymorphic
 8    models - this allows the base class to set it correctly.
 9    See https://github.com/python/pythondotorg/issues/2567
10    """
11
12    def get_fields(self):
13        fields = super().get_fields()
14        if issubclass(self.model, PolymorphicModel):
15            fields = {
16                field
17                for field in fields
18                if field.name != "polymorphic_ctype"
19            }
20        return fields

Then in your test settings file:

BAKER_CUSTOM_CLASS = "yoursite.tests.baker.PolymorphicAwareBaker"

You may also simply pass the correct ContentType instance to the polymorphic_ctype field when creating polymorphic model instances with make()

Other Integrations