Fixed #3534 -- newforms ModelChoiceField and ModelMultipleChoiceField no longer cache choices. Instead, they calculate choices via a fresh database query each time the widget is rendered and clean() is called

git-svn-id: http://code.djangoproject.com/svn/django/trunk@4552 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Adrian Holovaty 2007-02-21 05:14:28 +00:00
parent 5bec651a61
commit ee96c7eb2d
2 changed files with 153 additions and 28 deletions

View File

@ -3,9 +3,11 @@ Helper functions for creating Form classes from Django models
and database field objects. and database field objects.
""" """
from django.utils.translation import gettext
from util import ValidationError
from forms import BaseForm, DeclarativeFieldsMetaclass, SortedDictFromList from forms import BaseForm, DeclarativeFieldsMetaclass, SortedDictFromList
from fields import ChoiceField, MultipleChoiceField from fields import Field, ChoiceField
from widgets import Select, SelectMultiple from widgets import Select, SelectMultiple, MultipleHiddenInput
__all__ = ('save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields', __all__ = ('save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
'ModelChoiceField', 'ModelMultipleChoiceField') 'ModelChoiceField', 'ModelMultipleChoiceField')
@ -104,33 +106,81 @@ def form_for_fields(field_list):
fields = SortedDictFromList([(f.name, f.formfield()) for f in field_list if f.editable]) fields = SortedDictFromList([(f.name, f.formfield()) for f in field_list if f.editable])
return type('FormForFields', (BaseForm,), {'base_fields': fields}) return type('FormForFields', (BaseForm,), {'base_fields': fields})
class QuerySetIterator(object):
def __init__(self, queryset, empty_label, cache_choices):
self.queryset, self.empty_label, self.cache_choices = queryset, empty_label, cache_choices
def __iter__(self):
if self.empty_label is not None:
yield (u"", self.empty_label)
for obj in self.queryset:
yield (obj._get_pk_val(), str(obj))
# Clear the QuerySet cache if required.
if not self.cache_choices:
self.queryset._result_cache = None
class ModelChoiceField(ChoiceField): class ModelChoiceField(ChoiceField):
"A ChoiceField whose choices are a model QuerySet." "A ChoiceField whose choices are a model QuerySet."
def __init__(self, queryset, empty_label=u"---------", **kwargs): # This class is a subclass of ChoiceField for purity, but it doesn't
self.model = queryset.model # actually use any of ChoiceField's implementation.
choices = [(obj._get_pk_val(), str(obj)) for obj in queryset] def __init__(self, queryset, empty_label=u"---------", cache_choices=False,
if empty_label is not None: required=True, widget=Select, label=None, initial=None, help_text=None):
choices = [(u"", empty_label)] + choices self.queryset = queryset
ChoiceField.__init__(self, choices=choices, **kwargs) self.empty_label = empty_label
self.cache_choices = cache_choices
# Call Field instead of ChoiceField __init__() because we don't need
# ChoiceField.__init__().
Field.__init__(self, required, widget, label, initial, help_text)
self.widget.choices = self.choices
def _get_choices(self):
# If self._choices is set, then somebody must have manually set
# the property self.choices. In this case, just return self._choices.
if hasattr(self, '_choices'):
return self._choices
# Otherwise, execute the QuerySet in self.queryset to determine the
# choices dynamically.
return QuerySetIterator(self.queryset, self.empty_label, self.cache_choices)
def _set_choices(self, value):
# This method is copied from ChoiceField._set_choices(). It's necessary
# because property() doesn't allow a subclass to overwrite only
# _get_choices without implementing _set_choices.
self._choices = self.widget.choices = list(value)
choices = property(_get_choices, _set_choices)
def clean(self, value): def clean(self, value):
value = ChoiceField.clean(self, value) Field.clean(self, value)
if not value: if value in ('', None):
return None return None
try: try:
value = self.model._default_manager.get(pk=value) value = self.queryset.model._default_manager.get(pk=value)
except self.model.DoesNotExist: except self.queryset.model.DoesNotExist:
raise ValidationError(gettext(u'Select a valid choice. That choice is not one of the available choices.')) raise ValidationError(gettext(u'Select a valid choice. That choice is not one of the available choices.'))
return value return value
class ModelMultipleChoiceField(MultipleChoiceField): class ModelMultipleChoiceField(ModelChoiceField):
"A MultipleChoiceField whose choices are a model QuerySet." "A MultipleChoiceField whose choices are a model QuerySet."
def __init__(self, queryset, **kwargs): hidden_widget = MultipleHiddenInput
self.model = queryset.model def __init__(self, queryset, cache_choices=False, required=True,
MultipleChoiceField.__init__(self, choices=[(obj._get_pk_val(), str(obj)) for obj in queryset], **kwargs) widget=SelectMultiple, label=None, initial=None, help_text=None):
super(ModelMultipleChoiceField, self).__init__(queryset, None, cache_choices,
required, widget, label, initial, help_text)
def clean(self, value): def clean(self, value):
value = MultipleChoiceField.clean(self, value) if self.required and not value:
if not value: raise ValidationError(gettext(u'This field is required.'))
elif not self.required and not value:
return [] return []
return self.model._default_manager.filter(pk__in=value) if not isinstance(value, (list, tuple)):
raise ValidationError(gettext(u'Enter a list of values.'))
final_values = []
for val in value:
try:
obj = self.queryset.model._default_manager.get(pk=val)
except self.queryset.model.DoesNotExist:
raise ValidationError(gettext(u'Select a valid choice. %s is not one of the available choices.') % val)
else:
final_values.append(obj)
return final_values

View File

@ -289,6 +289,46 @@ existing Category instance.
>>> Category.objects.get(id=3) >>> Category.objects.get(id=3)
<Category: Third> <Category: Third>
Here, we demonstrate that choices for a ForeignKey ChoiceField are determined
at runtime, based on the data in the database when the form is displayed, not
the data in the database when the form is instantiated.
>>> ArticleForm = form_for_model(Article)
>>> f = ArticleForm(auto_id=False)
>>> print f.as_ul()
<li>Headline: <input type="text" name="headline" maxlength="50" /></li>
<li>Pub date: <input type="text" name="pub_date" /></li>
<li>Writer: <select name="writer">
<option value="" selected="selected">---------</option>
<option value="1">Mike Royko</option>
<option value="2">Bob Woodward</option>
</select></li>
<li>Article: <textarea name="article"></textarea></li>
<li>Categories: <select multiple="multiple" name="categories">
<option value="1">Entertainment</option>
<option value="2">It&#39;s a test</option>
<option value="3">Third</option>
</select> Hold down "Control", or "Command" on a Mac, to select more than one.</li>
>>> Category.objects.create(name='Fourth', url='4th')
<Category: Fourth>
>>> Writer.objects.create(name='Carl Bernstein')
<Writer: Carl Bernstein>
>>> print f.as_ul()
<li>Headline: <input type="text" name="headline" maxlength="50" /></li>
<li>Pub date: <input type="text" name="pub_date" /></li>
<li>Writer: <select name="writer">
<option value="" selected="selected">---------</option>
<option value="1">Mike Royko</option>
<option value="2">Bob Woodward</option>
<option value="3">Carl Bernstein</option>
</select></li>
<li>Article: <textarea name="article"></textarea></li>
<li>Categories: <select multiple="multiple" name="categories">
<option value="1">Entertainment</option>
<option value="2">It&#39;s a test</option>
<option value="3">Third</option>
<option value="4">Fourth</option>
</select> Hold down "Control", or "Command" on a Mac, to select more than one.</li>
# ModelChoiceField ############################################################ # ModelChoiceField ############################################################
>>> from django.newforms import ModelChoiceField, ModelMultipleChoiceField >>> from django.newforms import ModelChoiceField, ModelMultipleChoiceField
@ -311,13 +351,30 @@ ValidationError: [u'Select a valid choice. That choice is not one of the availab
>>> f.clean(2) >>> f.clean(2)
<Category: It's a test> <Category: It's a test>
# Add a Category object *after* the ModelChoiceField has already been
# instantiated. This proves clean() checks the database during clean() rather
# than caching it at time of instantiation.
>>> Category.objects.create(name='Fifth', url='5th')
<Category: Fifth>
>>> f.clean(5)
<Category: Fifth>
# Delete a Category object *after* the ModelChoiceField has already been
# instantiated. This proves clean() checks the database during clean() rather
# than caching it at time of instantiation.
>>> Category.objects.get(url='5th').delete()
>>> f.clean(5)
Traceback (most recent call last):
...
ValidationError: [u'Select a valid choice. That choice is not one of the available choices.']
>>> f = ModelChoiceField(Category.objects.filter(pk=1), required=False) >>> f = ModelChoiceField(Category.objects.filter(pk=1), required=False)
>>> print f.clean('') >>> print f.clean('')
None None
>>> f.clean('') >>> f.clean('')
>>> f.clean('1') >>> f.clean('1')
<Category: Entertainment> <Category: Entertainment>
>>> f.clean('2') >>> f.clean('100')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] ValidationError: [u'Select a valid choice. That choice is not one of the available choices.']
@ -345,29 +402,47 @@ ValidationError: [u'This field is required.']
[<Category: Entertainment>, <Category: It's a test>] [<Category: Entertainment>, <Category: It's a test>]
>>> f.clean((1, '2')) >>> f.clean((1, '2'))
[<Category: Entertainment>, <Category: It's a test>] [<Category: Entertainment>, <Category: It's a test>]
>>> f.clean(['nonexistent']) >>> f.clean(['100'])
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Select a valid choice. nonexistent is not one of the available choices.'] ValidationError: [u'Select a valid choice. 100 is not one of the available choices.']
>>> f.clean('hello') >>> f.clean('hello')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a list of values.'] ValidationError: [u'Enter a list of values.']
# Add a Category object *after* the ModelChoiceField has already been
# instantiated. This proves clean() checks the database during clean() rather
# than caching it at time of instantiation.
>>> Category.objects.create(id=6, name='Sixth', url='6th')
<Category: Sixth>
>>> f.clean([6])
[<Category: Sixth>]
# Delete a Category object *after* the ModelChoiceField has already been
# instantiated. This proves clean() checks the database during clean() rather
# than caching it at time of instantiation.
>>> Category.objects.get(url='6th').delete()
>>> f.clean([6])
Traceback (most recent call last):
...
ValidationError: [u'Select a valid choice. 6 is not one of the available choices.']
>>> f = ModelMultipleChoiceField(Category.objects.all(), required=False) >>> f = ModelMultipleChoiceField(Category.objects.all(), required=False)
>>> f.clean([]) >>> f.clean([])
[] []
>>> f.clean(()) >>> f.clean(())
[] []
>>> f.clean(['4']) >>> f.clean(['10'])
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Select a valid choice. 4 is not one of the available choices.'] ValidationError: [u'Select a valid choice. 10 is not one of the available choices.']
>>> f.clean(['3', '4']) >>> f.clean(['3', '10'])
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Select a valid choice. 4 is not one of the available choices.'] ValidationError: [u'Select a valid choice. 10 is not one of the available choices.']
>>> f.clean(['1', '5']) >>> f.clean(['1', '10'])
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Select a valid choice. 5 is not one of the available choices.'] ValidationError: [u'Select a valid choice. 10 is not one of the available choices.']
"""} """}