Search code examples
djangopython-3.xdjango-grappelli

GenericForeignKey in Grappelli admin: filter content_type to display only relevant models


With related lookups, I can easily get access to all the models I have to have a generic foreign key. Obviously, this is not what I want to do. I want to restrict it to just a sub set of the models I have -- specifically all the inherit from the abstract model Registry.

My models look like thus:

class Registry(models.Model):
    """A base registry class."""

    number = models.BigAutoField(primary_key=True)
    when = models.DateField(default=timezone.now)
    title = models.CharField(
        max_length=1024, default='', blank=True, null=True)

    class Meta:
        """The meta class."""

        abstract = True

    […]


class Revision(models.Model):
    """A revision model."""

    when = models.DateTimeField(default=timezone.now)
    identification = models.BigIntegerField()
    content_type = models.ForeignKey(
        ContentType, on_delete=models.CASCADE, related_name='+')
    object_id = models.PositiveIntegerField()
    parent = GenericForeignKey('content_type', 'object_id')

    […]


class Document(Registry):

    […]

class Drawing(Registry):

    […]

So that each Registry derived instances can have many different revisions.

And the relevant admin:

class RevisionAdmin(admin.ModelAdmin):
    """Revision administration definition."""

    fieldsets = [
        ('Revision', {
            'fields': [
                'when',
                'identification',
            ]
        }),
        ('Registry', {
            'classes': ('grp-collapse grp-open',),
            'fields': ('content_type', 'object_id', )
        }),
    ]

Solution

  • You can use a limit_choices_to [Django-doc]. Since you want to limit the choices to the descendants, we will need to write some extra logic to calculate these first:

    We can for example first calculate all the subclasses with this function:

    def get_descendants(klass):
        gen = { klass }
        desc = set()
        while gen:
            gen = { skls for kls in gen for skls in kls.__subclasses__() }
            desc.update(gen)
        return desc
    

    Now we can define a callable to obtain the primary keys of the ContentTypes that are subclasses of a class, in this case Registry:

    from django.db.models import Q
    from django.contrib.contenttypes.models import ContentType
    
    def filter_qs():
        if not hasattr(filter_qs_registry, '_q'):
            models = get_descendants(Registry)
            pks = [v.pk for v in ContentType.objects.get_for_models(*models).values()]
            filter_qs_registry._q = Q(pk__in=pks)
        return filter_qs_registry._q

    In the ForeignKey to the ContentType, we can then use the limited_choices_to field:

    class Revision(models.Model):
        """A revision model."""
        when = models.DateTimeField(default=timezone.now)
        identification = models.BigIntegerField()
        content_type = models.ForeignKey(
            ContentType,
            on_delete=models.CASCADE,
            limit_choices_to=filter_qs_registry,
            related_name='+'
        )
        object_id = models.PositiveIntegerField()
        parent = GenericForeignKey('content_type', 'object_id')

    Variable number of ascents

    We can generalize the number of ascents, by generalizing for example the get_descendants function:

    def get_descendants(*klass):
        gen = { *klass }
        desc = set()
        while gen:
            gen = { skls for kls in gen for skls in kls.__subclasses__() }
            desc.update(gen)
        return desc
    

    Next we can simply call it with:

    from django.db.models import Q
    from django.contrib.contenttypes.models import ContentType
    
    def filter_qs():
        if not hasattr(filter_qs_registry, '_q'):
            models = get_descendants(Registry, OtherAbstractModel)
            pks = [v.pk for v in ContentType.objects.get_for_models(*models).values()]
            filter_qs_registry._q = Q(pk__in=pks)
        return filter_qs_registry._q