ModelChoiceField's queryset attribute persists between two form instances
|Reported by:||Baptiste Mispelon||Owned by:||Spark23|
|Cc:||Triage Stage:||Design decision needed|
|Has patch:||no||Needs documentation:||no|
|Needs tests:||no||Patch needs improvement:||no|
Using the models from the tutorial:
from django.db import models class Poll(models.Model): question = models.CharField(max_length=200) pub_date = models.DateTimeField('date published') class Choice(models.Model): poll = models.ForeignKey(Poll) choice = models.CharField(max_length=200) votes = models.IntegerField()
And the following form:
from django import forms from polls.models import Poll class ChoiceForm(forms.Form): poll = forms.ModelChoiceField(queryset=Poll.objects.all()) def __init__(self, *args, **kwargs): """Change the choices attribute of the field `poll`.""" super(ChoiceForm, self).__init__(*args, **kwargs) queryset = self.fields['poll'].queryset choices = [(poll.pk, poll.pk) for poll in queryset] self.fields['poll'].choices = choices
When doing so, the queryset gets "stuck" on what it was the first time the form was instanciated: added Poll objects don't show up in it, and deleted polls are still present.
from datetime import datetime from polls.forms import ChoiceForm from polls.models import Poll Poll.objects.all().delete() # Just making sure. f = ChoiceForm() print f['poll'].as_widget() # '<select name="poll" id="id_poll">\n</select>' Poll.objects.create(question="What is your favourite colour?", pub_date=datetime.now()) f = ChoiceForm() print f['poll'].as_widget() # Expected: '<select name="poll" id="id_poll">\n<option value="1">1</option>\n</select>' # Result: '<select name="poll" id="id_poll">\n</select>'
One workaround is to copy the queryset before iterating over it:
queryset = self.fields['poll'].queryset.all()
I believe the issue lies in
ModelChoiceField.__deepcopy__(), in particular the line:
result.queryset = result.queryset
queryset being a property, this ends up doing (among other things which I think aren't relevant here):
result._queryset = result._queryset
_queryset has been iterated over, its cache is filled and it gets attached onto the deepcopied instance.
My naive fix would be to make a copy of result.queryset in the
__deepcopy__ method, like so:
result.queryset = result.queryset.all()
But my understanding of the issue is limited and I probably don't know the ins and outs of this (especially considering the lengthy discussion around ticket #11183).