Admin view offer filtered options for many-to-many relationship with intermediate model

I am using the admin view for adding model instances.

I have a model Option with a name and an end date (which can be empty, in which case it never ends), a model Thing with a name and a registration date, and an explicit intermediate model ThingToOptions with a ForeignKey for Option, a ForeignKey for Thing and a comment field. The comment field is necessary, which is why I cannot use an implicit ManyToManyField.

In reality the classes have different names and a few have some extra fields, they all have defined str functions, etc. But focusing the current problem, the models would look like this:

class Option(models.Model):
    name = models.CharField(max_length=200)
    enddate = models.DateTimeField(blank=True, null=True)

class Thing(models.Model):
    name = models.CharField(max_length=200)
    regdate = models.DateTimeField()

class ThingToOption(models.Model):
    options = models.ForeignKey(Option, on_delete=models.CASCADE)
    things = models.ForeignKey(Thing, on_delete=models.CASCADE)
    comment = models.CharField(max_length=2000, blank=True, null=True)

I am adding the ThingToOption through a TabularInline:

class ThingToOptionInline(admin.TabularInline):
    model = ThingToOption
    extra = 1
    

class ThingAdmin(admin.ModelAdmin):
    inlines = [ThingToOptionInline]
    ....continues but is irrelevant....

But now I cannot figure out how to only offer a filtered set of options. I would like to offer only those where option__enddate is either Null or greater than thing__regdate.

Implement a queryset for the option model that lists future dates and dates without an enddate for the related thing but list all available options when choosing an option for a thing without an existing relation.

from django.db.models import Exists, F, Q, Subquery

class OptionQuerySet(models.QuerySet):
    def all_or_not_expired(self, thing_id=None):
        related_thing = Thing.objects.filter(pk=thing_id) if thing_id is not None else Thing.objects.none()
        return self.annotate(
            thing_exists=Exists(related_thing),
            thing_regdate=Subquery(related_thing.values('regdate')[:1])
        ).filter(
            Q(thing_exists=False) | 
            Q(thing_exists=True, thing_regdate__isnull=True) | 
            Q(enddate__isnull=True) | 
            Q(enddate__gt=F('thing_regdate'))
        )

admin_option_qs = OptionQuerySet(model=Option)

Then, declare a custom ModelForm that overrides the queryset for our options field so that it uses our custom one.

class ThingToOptionForm(forms.ModelForm):
    class Meta:
        model = ThingToOption
        fields = "__all__"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["options"].queryset = admin_option_qs.all_or_not_expired(
            self.instance.things_id)

Finally, set our inline form to use our custom model form.

class ThingToOptionInline(admin.TabularInline):
    model = ThingToOption
    form = ThingToOptionForm
    extra = 1
Вернуться на верх