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