TL;DR :
Filtering a queryset according to a related object's value may cause duplicate values in the result.
This behaviour spreads on the limit_choices_to
FK's attribute in a model field when using it in a similar way, causing a MultipleObjectsReturned
error when using a modelform associated with this model and selecting a duplicate value.
Is it possible to apply distinct()
or equivalent on a model's foreign key's limit_choices_to
in order to avoid duplicate in the options of a modelform's field?
Reproducing the problem :
With python manage.py shell
(and solving it) :
Let two models A
and B
:
class A(models.Model):
pass
class B(models.Model):
a = models.ForeignKey(A)
d = models.BooleanField(default=False)
and the following entries :
>>> a = A.objects.create()
>>> b1 = B.objects.create(a=a, d=True)
>>> b2 = B.objects.create(a=a, d=True)
The following queryset using a get()
causes an error :
>>> A.objects.filter(b__d=True).get(id=1)
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/home/vmonteco/.venvs/django/lib/python3.6/site-packages/django/db/models/query.py", line 384, in get
(self.model._meta.object_name, num)
app.models.MultipleObjectsReturned: get() returned more than one A -- it returned 2!
Which sounds normal since a
is present twice in the filter()
's result :
>>> A.objects.filter(b__d=True)
<QuerySet [<A: A object>, <A: A object>]>
This error can be solved easily with a simple distinct()
:
>>> A.objects.filter(b__d=True).distinct().get(id=1)
<A: A object>
With a third model and it's associated modelform :
Let's add a third model :
class C(models.Model):
a = models.ForeignKey(A, limit_choices_to={'b__d': True})
I could create/edit instances with a modelform :
class CForm(forms.ModelForm):
class Meta:
model = C
fields = ['a',]
The queryset populating the a
field's choices should look like something like this :
>>> A.objects.filter(b__d=True)
<QuerySet [<A: A object>, <A: A object>]>
Which only contains the same object twice :
>>> A.objects.filter(b__d=True).values('id')
<QuerySet [{'id': 1}, {'id': 1}]>
Then, at the form submission, django applies a get(id=selected_value)
on the field's queryset. If the selected value is a duplicate value, the problem I exposed in the previous part occurs.
Current solution :
The only solution I found so far is to overwrite the field's queryset in my modelform in order to ensure there is no duplicate :
class CForm(forms.ModelForm):
class Meta:
model = C
fields = ['a',]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['a'].queryset = self.fields['a'].queryset.distinct()
But since this queryset is defined directly after the model field's definition, this solution feels unsatisfying and looks more like a workaround. limit_choices_to
doesn't seem to document this case.
Could there be a more appropriate way to avoid duplicates in a field's queryset when limit_choices_to
is used?