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

168 Views Asked by At

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', )
        }),
    ]
1

There are 1 best solutions below

3
willeM_ Van Onsem On BEST ANSWER

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