pythondjangodjango-contenttypesdjango-generic-relations

Django Admin Generic content type multiple models inline form


I'm getting started with Django and I'm a bit stuck on a multi-models field, AKA Generic Relation (Content Type)

I have a generic content type "student_solution" that can belong to either:

Therefore, in each of those 3 models, I have a reversed relationship as follow, in each models.py:

# Reverse generic relation - XXX See https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#reverse-generic-relations
student_solutions = GenericRelation('student_solution.StudentSolution')

I'm not sure whether this is the right approach, I think so, but a confirmation is welcome :)


It's working fine as it is now, but it's not user-friendly in the Django Admin UI, see how it display on django admin, when creating a Student Solution (I would expect a select box showing a label field, instead of entering the Content Type ID by hand):

django admin create entity

When creating either an Org, Institution or Campus, the field doesn't show at all in the Django Admin (so I probably misconfigured something)

I tried following How to replace content_type and object_id fields by a field with actual object in admin inline? to improve the UI by allowing to select the right Content Type and "object" using the object's label. But it doesn't work at this time.


student_solution/models.py:

from django.contrib.contenttypes import fields
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models import Q
from jsonfield import JSONField

from tfp_backoffice.apps.institution.models import Institution

CONTENT_TYPE_CHOICES = (
  Q(app_label='org', model='org') |
  Q(app_label='institution', model='institution') |
  Q(app_label='campus', model='campus')
)

class StudentSolution(models.Model):
  # Dynamic relationship to either Org, Institution or Campus entities
  # XXX https://simpleisbetterthancomplex.com/tutorial/2016/10/13/how-to-use-generic-relations.html
  content_type = models.ForeignKey(
    ContentType,
    on_delete=models.CASCADE,  # TODO check if good thing
    limit_choices_to=CONTENT_TYPE_CHOICES,
  )
  object_id = models.PositiveIntegerField()
  content_object = fields.GenericForeignKey(
    'content_type',
    'object_id'
  )

student_solution/admin.py:

from django.contrib import admin
from modeltranslation.admin import TranslationAdmin

from tfp_backoffice.apps.org.models import Org
from tfp_backoffice.apps.student_solution.forms import StudentSolutionAdminForm, GenericStudentSolutionOwnerChoicesFieldForm
from tfp_backoffice.apps.student_solution.models import StudentSolution

    
class StudentSolutionInlineAdmin(admin.TabularInline):
  form = GenericStudentSolutionOwnerChoicesFieldForm
  model = Org  # TODO not sure at all about that, should be either of 3 related ContentTypes (Org | Institution | Campus)
  # This throw error "<class 'tfp_backoffice.apps.student_solution.admin.StudentSolutionInlineAdmin'>: (admin.E202) 'org.Org' has no ForeignKey to 'student_solution.StudentSolution'."


class StudentSolutionAdmin(TranslationAdmin):
  form = StudentSolutionAdminForm
  inlines = [
    StudentSolutionInlineAdmin,
  ]


admin.site.register(StudentSolution, StudentSolutionAdmin)

student_solution/forms.py:

from django import forms
from django.contrib.contenttypes.models import ContentType

from tfp_backoffice.apps.org.models import Org
from tfp_backoffice.apps.student_solution.models import CONTENT_TYPE_CHOICES, StudentSolution


class StudentSolutionAdminForm(forms.ModelForm):
  class Meta:
    model = StudentSolution
    fields = '__all__'  # Keep all fields    

class GenericStudentSolutionOwnerChoicesFieldForm(forms.ModelForm):
  ct_place_type = ContentType.objects.get_for_model(Org)  # TODO not sure at all about that, should be either of 3 related ContentTypes (Org | Institution | Campus)

  object_id = forms.ModelChoiceField(
    Org.objects.all(),
    limit_choices_to=CONTENT_TYPE_CHOICES,
    label='Student solution'
  )
  content_type = forms.ModelChoiceField(
    ContentType.objects.all(),
    initial=ct_place_type,  
    limit_choices_to=CONTENT_TYPE_CHOICES,  # should I use this here?
    widget=forms.HiddenInput()
  )

  def clean_object_id(self):
    return self.cleaned_data['object_id'].pk

  def clean_content_type(self):
    return self.ct_place_type

But this code isn't working and throw this error when starting the server

django.core.management.base.SystemCheckError: SystemCheckError: System check identified some issues:

<class 'tfp_backoffice.apps.student_solution.admin.StudentSolutionInlineAdmin'>: (admin.E202) 'org.Org' has no ForeignKey to 'student_solution.StudentSolution'.


Solution

  • If I am understanding what you would like to do correctly you should have your StudentSolutionInlineAdmin in each of the Org, Institution, and Campus admins and it should be a GenericTabularInline (https://docs.djangoproject.com/en/2.2/ref/contrib/contenttypes/#generic-relations-in-admin).

    So you would have something like this in (for example) your org/admin.py:

    from django.contrib import admin
    from django.contrib.contenttypes.admin import GenericTabularInline
    from django import forms
    
    from .models import Org
    from student_solution.models import StudentSolution
    
    class StudentSolutionInlineAdmin(GenericTabularInline):
        model = StudentSolution
        extra = 1
    
    class StudentSolutionAdminForm(forms.ModelForm):
        class Meta:
            model = StudentSolution
            fields = '__all__'  # Keep all fields 
    
    @admin.register(Org)
    class OrgAdmin(admin.ModelAdmin):
        form = StudentSolutionAdminForm
        inlines = [StudentSolutionInlineAdmin]
    

    This will allow you to add a StudentSolution related to an Org from within the Org admin.