django-polymorphic: Admin website error: displaying inline admin with ManyToMany using intermediate polymorphic model

Question:

I’m a django novice. It’s my first Django project. As you can guess it from the title of the question, my problem is a bit long to explain.

In short: I want to know how to make a basic Admin Inline display for a ManyToMany relationship with intermediate polymorphic model. I’m having an TemplateDoesNotExist at /admin/blu_forms/contactentry/e6529155-82a3-43b6-9e14-34951c88471a/change/ error.

The initial idea is simple: I have two classes ContactEntry and ContactField. A contact entry can have many fields and vice-versa, so we have a ManyToMany relationship between the two. Since I want to store typed data in this relationship (because a given field is linked to typed data for a given entry – field relation) so I use an intermediate model (through model) to hold that data. The correct typing is handled with Polymorphism (django-polymorphic).

This works well. I made a custom script to populate the database (PostgreSQL) and it works nicely.

My issue is with the admin website. When I register ContactEntry and ContactField, I see the list of data, but when I click to see the details of a specific instance, I get an error.

The correct lists of instances:

Correct list of ContactFields in Admin Website

Correct list of ContactEntries in Admin Website

And the error I get when I click for instance on a ContactEntry instance:

Django error on detail admin view

Below is my code in my project/app/admin.py:

from django.contrib import admin

from polymorphic.admin import PolymorphicParentModelAdmin
from polymorphic.admin import PolymorphicChildModelAdmin
from polymorphic.admin import PolymorphicChildModelFilter
from polymorphic.admin import StackedPolymorphicInline
from polymorphic.admin import PolymorphicInlineSupportMixin

from .models import *





# TODO: complete admin with ManyToMany relationship
class ContactFieldDataInline(StackedPolymorphicInline):
    model = ContactFieldData
    
    class BooleanContactFieldDataInline(StackedPolymorphicInline.Child):
        model = BooleanContactFieldData

    class CharContactFieldDataInline(StackedPolymorphicInline.Child):
        model = CharContactFieldData

    class TextContactFieldDataInline(StackedPolymorphicInline.Child):
        model = TextContactFieldData

    class IntegerContactFieldDataInline(StackedPolymorphicInline.Child):
        model = IntegerContactFieldData

    class FloatContactFieldDataInline(StackedPolymorphicInline.Child):
        model = FloatContactFieldData

    class DateContactFieldDataInline(StackedPolymorphicInline.Child):
        model = DateContactFieldData

    class DateTimeContactFieldDataInline(StackedPolymorphicInline.Child):
        model = DateTimeContactFieldData

    class EmailContactFieldDataInline(StackedPolymorphicInline.Child):
        model = EmailContactFieldData

    class URLContactFieldDataInline(StackedPolymorphicInline.Child):
        model = URLContactFieldData
    
    child_inlines = [
        BooleanContactFieldDataInline,
        CharContactFieldDataInline,
        TextContactFieldDataInline,
        IntegerContactFieldDataInline,
        FloatContactFieldDataInline,
        DateContactFieldDataInline,
        DateTimeContactFieldDataInline,
        EmailContactFieldDataInline,
        URLContactFieldDataInline
    ]

class ContactFieldAdmin(
    PolymorphicInlineSupportMixin,
    admin.ModelAdmin
):
    inlines = [ContactFieldDataInline]

class ContactEntryAdmin(
    PolymorphicInlineSupportMixin,
    admin.ModelAdmin
):
    inlines = [ContactFieldDataInline]

admin.site.register(ContactEntry, ContactEntryAdmin)
admin.site.register(ContactField, ContactFieldAdmin)

And my project/app/models.py:

import datetime
import uuid

from django.db import models
from django.utils import timezone
from django.contrib import admin
from polymorphic.models import PolymorphicModel


class ContactField(models.Model):
    """
    A Blumorpho custom field.
    """
    uuid = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )

    # mandatory fields
    name = models.CharField(
        unique=True, max_length=100
    )
    #other_names = models.JSONField(null=True, blank=True)

    # generated
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name


class ContactEntry(models.Model):
    """
    Blumorpho contact entry containing 
    fields with information about companies 
    and startups.
    """
    uuid = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    
    # mandatory fields (cannot be missing)
    # NOTE: defaults: null=False, blank=False
    entry_id = models.IntegerField(unique=True)
    entry_title = models.CharField(max_length=200)
    entry_date = models.DateTimeField()

    # generated fields
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    # relations
    contact_field_datas = models.ManyToManyField(
        'ContactField', 
        related_name='contact_entries', 
        through='ContactFieldData'
    )
    # TODO: abstract causes error

    def __str__(self):
        return f"ContactEntry[id: {self.entry_id}] " + 
            f"title: {self.entry_title}"


class ContactFieldData(PolymorphicModel):
    """
    Intermediate model for storing value for a 
    ManyToMany ContactEntry to ContactField relationship.
    NOTE: Does not allow duplicates.
    NOTE: Cannot be abstract. Using polymorphic models.
    """
    # mandatory fields
    contact_field = models.ForeignKey(
        ContactField, on_delete=models.CASCADE
    )
    contact_entry = models.ForeignKey(
        ContactEntry, on_delete=models.CASCADE
    )

    field_number = models.IntegerField()

    def __str__(self):
        return f"FieldData[{self.contact_field.name}, " + 
            f"nb: {self.field_number}]: " + 
            f"{self.value}"
    
    class Meta:
        #abstract = True # cannot be abstract for polymorphic models
        unique_together = ('contact_field', 'contact_entry')

class BooleanContactFieldData(ContactFieldData):
    value = models.BooleanField(default=False)

class CharContactFieldData(ContactFieldData):
    value = models.CharField(max_length=200)

class TextContactFieldData(ContactFieldData):
    value = models.TextField()

class IntegerContactFieldData(ContactFieldData):
    value = models.IntegerField()

class FloatContactFieldData(ContactFieldData):
    value = models.FloatField()

class DateContactFieldData(ContactFieldData):
    value = models.DateField()

class DateTimeContactFieldData(ContactFieldData):
    value = models.DateTimeField()

class EmailContactFieldData(ContactFieldData):
    value = models.EmailField(max_length=200)

class URLContactFieldData(ContactFieldData):
    value = models.URLField(max_length=300)


And the full error message:

Environment:


Request Method: GET
Request URL: http://127.0.0.1:8000/admin/blu_forms/contactentry/e6529155-82a3-43b6-9e14-34951c88471a/change/

Django Version: 4.1
Python Version: 3.10.4
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'blu_forms.apps.BluFormsConfig']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']

Template loader postmortem
Django tried loading these templates, in this order:

Using engine Django:
    This engine did not provide a list of tried templates.


Template error:
In template /home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/contrib/admin/templates/admin/change_form.html, error at line 58
   admin/polymorphic/edit_inline/stacked.html
   48 : {% block field_sets %}
   49 : {% for fieldset in adminform %}
   50 :   {% include "admin/includes/fieldset.html" %}
   51 : {% endfor %}
   52 : {% endblock %}
   53 : 
   54 : {% block after_field_sets %}{% endblock %}
   55 : 
   56 : {% block inline_field_sets %}
   57 : {% for inline_admin_formset in inline_admin_formsets %}
   58 :      {% include inline_admin_formset.opts.template %} 
   59 : {% endfor %}
   60 : {% endblock %}
   61 : 
   62 : {% block after_related_objects %}{% endblock %}
   63 : 
   64 : {% block submit_buttons_bottom %}{% submit_row %}{% endblock %}
   65 : 
   66 : {% block admin_change_form_document_ready %}
   67 :     <script id="django-admin-form-add-constants"
   68 :             src="{% static 'admin/js/change_form.js' %}"


Traceback (most recent call last):
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/backends/django.py", line 62, in render
    return self.template.render(context)
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 175, in render
    return self._render(context)
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 167, in _render
    return self.nodelist.render(context)
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 1005, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 1005, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 966, in render_annotated
    return self.render(context)
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/loader_tags.py", line 157, in render
    return compiled_parent._render(context)
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 167, in _render
    return self.nodelist.render(context)
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 1005, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 1005, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 966, in render_annotated
    return self.render(context)
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/loader_tags.py", line 157, in render
    return compiled_parent._render(context)
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 167, in _render
    return self.nodelist.render(context)
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 1005, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 1005, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 966, in render_annotated
    return self.render(context)
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/loader_tags.py", line 63, in render
    result = block.nodelist.render(context)
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 1005, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 1005, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 966, in render_annotated
    return self.render(context)
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/loader_tags.py", line 63, in render
    result = block.nodelist.render(context)
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 1005, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 1005, in <listcomp>
    return SafeString("".join([node.render_annotated(context) for node in self]))
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 966, in render_annotated
    return self.render(context)
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/defaulttags.py", line 238, in render
    nodelist.append(node.render_annotated(context))
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/base.py", line 966, in render_annotated
    return self.render(context)
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/loader_tags.py", line 197, in render
    template = context.template.engine.select_template(template_name)
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/engine.py", line 212, in select_template
    raise TemplateDoesNotExist(", ".join(not_found))

The above exception (admin/polymorphic/edit_inline/stacked.html) was the direct cause of the following exception:
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/core/handlers/base.py", line 220, in _get_response
    response = response.render()
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/response.py", line 114, in render
    self.content = self.rendered_content
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/response.py", line 92, in rendered_content
    return template.render(context, self._request)
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/backends/django.py", line 64, in render
    reraise(exc, self.backend)
  File "/home/onyr/anaconda3/envs/django/lib/python3.10/site-packages/django/template/backends/django.py", line 85, in reraise
    raise new from exc

Exception Type: TemplateDoesNotExist at /admin/blu_forms/contactentry/e6529155-82a3-43b6-9e14-34951c88471a/change/
Exception Value: admin/polymorphic/edit_inline/stacked.html

I have read ManyToMany relationship on Django Model doc, django-polymorphic admin integration and especially Working with many-to-many intermediary models or searched StackOverflow for an answer but I still don’t know how to fix the error. I also stumbled upon this unfixed issue but I’m not sure what to conclude from it…

I have the feeling I should try creating my own template but have no idea how to do it. Thanks for any help.

Note: I know I could remove the use of django-polymorphic and just make a ManyToMany relation to every derived intermediate object… but it’s not my goal. I really do wish to be using "real" polymorphism because it will save me a lot of work in the future, especially while dealing with templating.

Asked By: Onyr

||

Answers:

Ok… the answer is really short…

Just add polymorphic to INSTALLED_APPS in settings.py.

Just like in this settings.py example:

INSTALLED_APPS = [
    'django.contrib.admin',
    ...
    "polymorphic",
]

The result panel

Answered By: Onyr