Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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
  • Loading branch information...
commit ee96c7eb2dea86e5bdaf93f7afa91b6f0128dd72 1 parent 5bec651
Adrian Holovaty authored February 21, 2007
88  django/newforms/models.py
@@ -3,9 +3,11 @@
3 3
 and database field objects.
4 4
 """
5 5
 
  6
+from django.utils.translation import gettext
  7
+from util import ValidationError
6 8
 from forms import BaseForm, DeclarativeFieldsMetaclass, SortedDictFromList
7  
-from fields import ChoiceField, MultipleChoiceField
8  
-from widgets import Select, SelectMultiple
  9
+from fields import Field, ChoiceField
  10
+from widgets import Select, SelectMultiple, MultipleHiddenInput
9 11
 
10 12
 __all__ = ('save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
11 13
            'ModelChoiceField', 'ModelMultipleChoiceField')
@@ -104,33 +106,81 @@ def form_for_fields(field_list):
104 106
     fields = SortedDictFromList([(f.name, f.formfield()) for f in field_list if f.editable])
105 107
     return type('FormForFields', (BaseForm,), {'base_fields': fields})
106 108
 
  109
+class QuerySetIterator(object):
  110
+    def __init__(self, queryset, empty_label, cache_choices):
  111
+        self.queryset, self.empty_label, self.cache_choices = queryset, empty_label, cache_choices
  112
+
  113
+    def __iter__(self):
  114
+        if self.empty_label is not None:
  115
+            yield (u"", self.empty_label)
  116
+        for obj in self.queryset:
  117
+            yield (obj._get_pk_val(), str(obj))
  118
+        # Clear the QuerySet cache if required.
  119
+        if not self.cache_choices:
  120
+            self.queryset._result_cache = None
  121
+
107 122
 class ModelChoiceField(ChoiceField):
108 123
     "A ChoiceField whose choices are a model QuerySet."
109  
-    def __init__(self, queryset, empty_label=u"---------", **kwargs):
110  
-        self.model = queryset.model
111  
-        choices = [(obj._get_pk_val(), str(obj)) for obj in queryset]
112  
-        if empty_label is not None:
113  
-            choices = [(u"", empty_label)] + choices
114  
-        ChoiceField.__init__(self, choices=choices, **kwargs)
  124
+    # This class is a subclass of ChoiceField for purity, but it doesn't
  125
+    # actually use any of ChoiceField's implementation.
  126
+    def __init__(self, queryset, empty_label=u"---------", cache_choices=False,
  127
+            required=True, widget=Select, label=None, initial=None, help_text=None):
  128
+        self.queryset = queryset
  129
+        self.empty_label = empty_label
  130
+        self.cache_choices = cache_choices
  131
+        # Call Field instead of ChoiceField __init__() because we don't need
  132
+        # ChoiceField.__init__().
  133
+        Field.__init__(self, required, widget, label, initial, help_text)
  134
+        self.widget.choices = self.choices
  135
+
  136
+    def _get_choices(self):
  137
+        # If self._choices is set, then somebody must have manually set
  138
+        # the property self.choices. In this case, just return self._choices.
  139
+        if hasattr(self, '_choices'):
  140
+            return self._choices
  141
+        # Otherwise, execute the QuerySet in self.queryset to determine the
  142
+        # choices dynamically.
  143
+        return QuerySetIterator(self.queryset, self.empty_label, self.cache_choices)
  144
+
  145
+    def _set_choices(self, value):
  146
+        # This method is copied from ChoiceField._set_choices(). It's necessary
  147
+        # because property() doesn't allow a subclass to overwrite only
  148
+        # _get_choices without implementing _set_choices.
  149
+        self._choices = self.widget.choices = list(value)
  150
+
  151
+    choices = property(_get_choices, _set_choices)
115 152
 
116 153
     def clean(self, value):
117  
-        value = ChoiceField.clean(self, value)
118  
-        if not value:
  154
+        Field.clean(self, value)
  155
+        if value in ('', None):
119 156
             return None
120 157
         try:
121  
-            value = self.model._default_manager.get(pk=value)
122  
-        except self.model.DoesNotExist:
  158
+            value = self.queryset.model._default_manager.get(pk=value)
  159
+        except self.queryset.model.DoesNotExist:
123 160
             raise ValidationError(gettext(u'Select a valid choice. That choice is not one of the available choices.'))
124 161
         return value
125 162
 
126  
-class ModelMultipleChoiceField(MultipleChoiceField):
  163
+class ModelMultipleChoiceField(ModelChoiceField):
127 164
     "A MultipleChoiceField whose choices are a model QuerySet."
128  
-    def __init__(self, queryset, **kwargs):
129  
-        self.model = queryset.model
130  
-        MultipleChoiceField.__init__(self, choices=[(obj._get_pk_val(), str(obj)) for obj in queryset], **kwargs)
  165
+    hidden_widget = MultipleHiddenInput
  166
+    def __init__(self, queryset, cache_choices=False, required=True,
  167
+            widget=SelectMultiple, label=None, initial=None, help_text=None):
  168
+        super(ModelMultipleChoiceField, self).__init__(queryset, None, cache_choices,
  169
+            required, widget, label, initial, help_text)
131 170
 
132 171
     def clean(self, value):
133  
-        value = MultipleChoiceField.clean(self, value)
134  
-        if not value:
  172
+        if self.required and not value:
  173
+            raise ValidationError(gettext(u'This field is required.'))
  174
+        elif not self.required and not value:
135 175
             return []
136  
-        return self.model._default_manager.filter(pk__in=value)
  176
+        if not isinstance(value, (list, tuple)):
  177
+            raise ValidationError(gettext(u'Enter a list of values.'))
  178
+        final_values = []
  179
+        for val in value:
  180
+            try:
  181
+                obj = self.queryset.model._default_manager.get(pk=val)
  182
+            except self.queryset.model.DoesNotExist:
  183
+                raise ValidationError(gettext(u'Select a valid choice. %s is not one of the available choices.') % val)
  184
+            else:
  185
+                final_values.append(obj)
  186
+        return final_values
93  tests/modeltests/model_forms/models.py
@@ -289,6 +289,46 @@ def __str__(self):
289 289
 >>> Category.objects.get(id=3)
290 290
 <Category: Third>
291 291
 
  292
+Here, we demonstrate that choices for a ForeignKey ChoiceField are determined
  293
+at runtime, based on the data in the database when the form is displayed, not
  294
+the data in the database when the form is instantiated.
  295
+>>> ArticleForm = form_for_model(Article)
  296
+>>> f = ArticleForm(auto_id=False)
  297
+>>> print f.as_ul()
  298
+<li>Headline: <input type="text" name="headline" maxlength="50" /></li>
  299
+<li>Pub date: <input type="text" name="pub_date" /></li>
  300
+<li>Writer: <select name="writer">
  301
+<option value="" selected="selected">---------</option>
  302
+<option value="1">Mike Royko</option>
  303
+<option value="2">Bob Woodward</option>
  304
+</select></li>
  305
+<li>Article: <textarea name="article"></textarea></li>
  306
+<li>Categories: <select multiple="multiple" name="categories">
  307
+<option value="1">Entertainment</option>
  308
+<option value="2">It&#39;s a test</option>
  309
+<option value="3">Third</option>
  310
+</select>  Hold down "Control", or "Command" on a Mac, to select more than one.</li>
  311
+>>> Category.objects.create(name='Fourth', url='4th')
  312
+<Category: Fourth>
  313
+>>> Writer.objects.create(name='Carl Bernstein')
  314
+<Writer: Carl Bernstein>
  315
+>>> print f.as_ul()
  316
+<li>Headline: <input type="text" name="headline" maxlength="50" /></li>
  317
+<li>Pub date: <input type="text" name="pub_date" /></li>
  318
+<li>Writer: <select name="writer">
  319
+<option value="" selected="selected">---------</option>
  320
+<option value="1">Mike Royko</option>
  321
+<option value="2">Bob Woodward</option>
  322
+<option value="3">Carl Bernstein</option>
  323
+</select></li>
  324
+<li>Article: <textarea name="article"></textarea></li>
  325
+<li>Categories: <select multiple="multiple" name="categories">
  326
+<option value="1">Entertainment</option>
  327
+<option value="2">It&#39;s a test</option>
  328
+<option value="3">Third</option>
  329
+<option value="4">Fourth</option>
  330
+</select>  Hold down "Control", or "Command" on a Mac, to select more than one.</li>
  331
+
292 332
 # ModelChoiceField ############################################################
293 333
 
294 334
 >>> from django.newforms import ModelChoiceField, ModelMultipleChoiceField
@@ -311,13 +351,30 @@ def __str__(self):
311 351
 >>> f.clean(2)
312 352
 <Category: It's a test>
313 353
 
  354
+# Add a Category object *after* the ModelChoiceField has already been
  355
+# instantiated. This proves clean() checks the database during clean() rather
  356
+# than caching it at time of instantiation.
  357
+>>> Category.objects.create(name='Fifth', url='5th')
  358
+<Category: Fifth>
  359
+>>> f.clean(5)
  360
+<Category: Fifth>
  361
+
  362
+# Delete a Category object *after* the ModelChoiceField has already been
  363
+# instantiated. This proves clean() checks the database during clean() rather
  364
+# than caching it at time of instantiation.
  365
+>>> Category.objects.get(url='5th').delete()
  366
+>>> f.clean(5)
  367
+Traceback (most recent call last):
  368
+...
  369
+ValidationError: [u'Select a valid choice. That choice is not one of the available choices.']
  370
+
314 371
 >>> f = ModelChoiceField(Category.objects.filter(pk=1), required=False)
315 372
 >>> print f.clean('')
316 373
 None
317 374
 >>> f.clean('')
318 375
 >>> f.clean('1')
319 376
 <Category: Entertainment>
320  
->>> f.clean('2')
  377
+>>> f.clean('100')
321 378
 Traceback (most recent call last):
322 379
 ...
323 380
 ValidationError: [u'Select a valid choice. That choice is not one of the available choices.']
@@ -345,29 +402,47 @@ def __str__(self):
345 402
 [<Category: Entertainment>, <Category: It's a test>]
346 403
 >>> f.clean((1, '2'))
347 404
 [<Category: Entertainment>, <Category: It's a test>]
348  
->>> f.clean(['nonexistent'])
  405
+>>> f.clean(['100'])
349 406
 Traceback (most recent call last):
350 407
 ...
351  
-ValidationError: [u'Select a valid choice. nonexistent is not one of the available choices.']
  408
+ValidationError: [u'Select a valid choice. 100 is not one of the available choices.']
352 409
 >>> f.clean('hello')
353 410
 Traceback (most recent call last):
354 411
 ...
355 412
 ValidationError: [u'Enter a list of values.']
  413
+
  414
+# Add a Category object *after* the ModelChoiceField has already been
  415
+# instantiated. This proves clean() checks the database during clean() rather
  416
+# than caching it at time of instantiation.
  417
+>>> Category.objects.create(id=6, name='Sixth', url='6th')
  418
+<Category: Sixth>
  419
+>>> f.clean([6])
  420
+[<Category: Sixth>]
  421
+
  422
+# Delete a Category object *after* the ModelChoiceField has already been
  423
+# instantiated. This proves clean() checks the database during clean() rather
  424
+# than caching it at time of instantiation.
  425
+>>> Category.objects.get(url='6th').delete()
  426
+>>> f.clean([6])
  427
+Traceback (most recent call last):
  428
+...
  429
+ValidationError: [u'Select a valid choice. 6 is not one of the available choices.']
  430
+
356 431
 >>> f = ModelMultipleChoiceField(Category.objects.all(), required=False)
357 432
 >>> f.clean([])
358 433
 []
359 434
 >>> f.clean(())
360 435
 []
361  
->>> f.clean(['4'])
  436
+>>> f.clean(['10'])
362 437
 Traceback (most recent call last):
363 438
 ...
364  
-ValidationError: [u'Select a valid choice. 4 is not one of the available choices.']
365  
->>> f.clean(['3', '4'])
  439
+ValidationError: [u'Select a valid choice. 10 is not one of the available choices.']
  440
+>>> f.clean(['3', '10'])
366 441
 Traceback (most recent call last):
367 442
 ...
368  
-ValidationError: [u'Select a valid choice. 4 is not one of the available choices.']
369  
->>> f.clean(['1', '5'])
  443
+ValidationError: [u'Select a valid choice. 10 is not one of the available choices.']
  444
+>>> f.clean(['1', '10'])
370 445
 Traceback (most recent call last):
371 446
 ...
372  
-ValidationError: [u'Select a valid choice. 5 is not one of the available choices.']
  447
+ValidationError: [u'Select a valid choice. 10 is not one of the available choices.']
373 448
 """}

0 notes on commit ee96c7e

Please sign in to comment.
Something went wrong with that request. Please try again.