Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #13679, #13231, #7287 -- Ensured that models that have ForeignK…

…eys/ManyToManyField can use a a callable default that returns a model instance/queryset. #13679 was a regression in behavior; the other two tickets are pleasant side effects. Thanks to 3point2 for the report.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@13577 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit b3dc3a0106062bf28a28dc426885e7002a16a761 1 parent d69cdc6
Russell Keith-Magee authored August 14, 2010
3  django/forms/fields.py
@@ -127,6 +127,9 @@ def __init__(self, required=True, widget=None, label=None, initial=None,
127 127
 
128 128
         self.validators = self.default_validators + validators
129 129
 
  130
+    def prepare_value(self, value):
  131
+        return value
  132
+
130 133
     def to_python(self, value):
131 134
         return value
132 135
 
12  django/forms/forms.py
@@ -18,10 +18,10 @@
18 18
 NON_FIELD_ERRORS = '__all__'
19 19
 
20 20
 def pretty_name(name):
21  
-    """Converts 'first_name' to 'First name'""" 
22  
-    if not name: 
23  
-        return u'' 
24  
-    return name.replace('_', ' ').capitalize() 
  21
+    """Converts 'first_name' to 'First name'"""
  22
+    if not name:
  23
+        return u''
  24
+    return name.replace('_', ' ').capitalize()
25 25
 
26 26
 def get_declared_fields(bases, attrs, with_base_fields=True):
27 27
     """
@@ -423,6 +423,7 @@ def as_widget(self, widget=None, attrs=None, only_initial=False):
423 423
         """
424 424
         if not widget:
425 425
             widget = self.field.widget
  426
+
426 427
         attrs = attrs or {}
427 428
         auto_id = self.auto_id
428 429
         if auto_id and 'id' not in attrs and 'id' not in widget.attrs:
@@ -430,6 +431,7 @@ def as_widget(self, widget=None, attrs=None, only_initial=False):
430 431
                 attrs['id'] = auto_id
431 432
             else:
432 433
                 attrs['id'] = self.html_initial_id
  434
+
433 435
         if not self.form.is_bound:
434 436
             data = self.form.initial.get(self.name, self.field.initial)
435 437
             if callable(data):
@@ -439,6 +441,8 @@ def as_widget(self, widget=None, attrs=None, only_initial=False):
439 441
                 data = self.form.initial.get(self.name, self.field.initial)
440 442
             else:
441 443
                 data = self.data
  444
+        data = self.field.prepare_value(data)
  445
+
442 446
         if not only_initial:
443 447
             name = self.html_name
444 448
         else:
24  django/forms/models.py
@@ -906,12 +906,7 @@ def __len__(self):
906 906
         return len(self.queryset)
907 907
 
908 908
     def choice(self, obj):
909  
-        if self.field.to_field_name:
910  
-            key = obj.serializable_value(self.field.to_field_name)
911  
-        else:
912  
-            key = obj.pk
913  
-        return (key, self.field.label_from_instance(obj))
914  
-
  909
+        return (self.field.prepare_value(obj), self.field.label_from_instance(obj))
915 910
 
916 911
 class ModelChoiceField(ChoiceField):
917 912
     """A ChoiceField whose choices are a model QuerySet."""
@@ -971,8 +966,8 @@ def _get_choices(self):
971 966
             return self._choices
972 967
 
973 968
         # Otherwise, execute the QuerySet in self.queryset to determine the
974  
-        # choices dynamically. Return a fresh QuerySetIterator that has not been
975  
-        # consumed. Note that we're instantiating a new QuerySetIterator *each*
  969
+        # choices dynamically. Return a fresh ModelChoiceIterator that has not been
  970
+        # consumed. Note that we're instantiating a new ModelChoiceIterator *each*
976 971
         # time _get_choices() is called (and, thus, each time self.choices is
977 972
         # accessed) so that we can ensure the QuerySet has not been consumed. This
978 973
         # construct might look complicated but it allows for lazy evaluation of
@@ -981,6 +976,14 @@ def _get_choices(self):
981 976
 
982 977
     choices = property(_get_choices, ChoiceField._set_choices)
983 978
 
  979
+    def prepare_value(self, value):
  980
+        if hasattr(value, '_meta'):
  981
+            if self.to_field_name:
  982
+                return value.serializable_value(self.to_field_name)
  983
+            else:
  984
+                return value.pk
  985
+        return super(ModelChoiceField, self).prepare_value(value)
  986
+
984 987
     def to_python(self, value):
985 988
         if value in EMPTY_VALUES:
986 989
             return None
@@ -1030,3 +1033,8 @@ def clean(self, value):
1030 1033
             if force_unicode(val) not in pks:
1031 1034
                 raise ValidationError(self.error_messages['invalid_choice'] % val)
1032 1035
         return qs
  1036
+
  1037
+    def prepare_value(self, value):
  1038
+        if hasattr(value, '__iter__'):
  1039
+            return [super(ModelMultipleChoiceField, self).prepare_value(v) for v in value]
  1040
+        return super(ModelMultipleChoiceField, self).prepare_value(value)
17  django/forms/widgets.py
@@ -450,13 +450,14 @@ def render(self, name, value, attrs=None, choices=()):
450 450
         output.append(u'</select>')
451 451
         return mark_safe(u'\n'.join(output))
452 452
 
  453
+    def render_option(self, selected_choices, option_value, option_label):
  454
+        option_value = force_unicode(option_value)
  455
+        selected_html = (option_value in selected_choices) and u' selected="selected"' or ''
  456
+        return u'<option value="%s"%s>%s</option>' % (
  457
+            escape(option_value), selected_html,
  458
+            conditional_escape(force_unicode(option_label)))
  459
+
453 460
     def render_options(self, choices, selected_choices):
454  
-        def render_option(option_value, option_label):
455  
-            option_value = force_unicode(option_value)
456  
-            selected_html = (option_value in selected_choices) and u' selected="selected"' or ''
457  
-            return u'<option value="%s"%s>%s</option>' % (
458  
-                escape(option_value), selected_html,
459  
-                conditional_escape(force_unicode(option_label)))
460 461
         # Normalize to strings.
461 462
         selected_choices = set([force_unicode(v) for v in selected_choices])
462 463
         output = []
@@ -464,10 +465,10 @@ def render_option(option_value, option_label):
464 465
             if isinstance(option_label, (list, tuple)):
465 466
                 output.append(u'<optgroup label="%s">' % escape(force_unicode(option_value)))
466 467
                 for option in option_label:
467  
-                    output.append(render_option(*option))
  468
+                    output.append(self.render_option(selected_choices, *option))
468 469
                 output.append(u'</optgroup>')
469 470
             else:
470  
-                output.append(render_option(option_value, option_label))
  471
+                output.append(self.render_option(selected_choices, option_value, option_label))
471 472
         return u'\n'.join(output)
472 473
 
473 474
 class NullBooleanSelect(Select):
100  tests/regressiontests/forms/models.py
@@ -38,11 +38,28 @@ class ChoiceOptionModel(models.Model):
38 38
     Can't reuse ChoiceModel because error_message tests require that it have no instances."""
39 39
     name = models.CharField(max_length=10)
40 40
 
  41
+    class Meta:
  42
+        ordering = ('name',)
  43
+
  44
+    def __unicode__(self):
  45
+        return u'ChoiceOption %d' % self.pk
  46
+
41 47
 class ChoiceFieldModel(models.Model):
42 48
     """Model with ForeignKey to another model, for testing ModelForm
43 49
     generation with ModelChoiceField."""
44 50
     choice = models.ForeignKey(ChoiceOptionModel, blank=False,
45  
-                               default=lambda: ChoiceOptionModel.objects.all()[0])
  51
+                               default=lambda: ChoiceOptionModel.objects.get(name='default'))
  52
+    choice_int = models.ForeignKey(ChoiceOptionModel, blank=False, related_name='choice_int',
  53
+                                   default=lambda: 1)
  54
+
  55
+    multi_choice = models.ManyToManyField(ChoiceOptionModel, blank=False, related_name='multi_choice',
  56
+                                          default=lambda: ChoiceOptionModel.objects.filter(name='default'))
  57
+    multi_choice_int = models.ManyToManyField(ChoiceOptionModel, blank=False, related_name='multi_choice_int',
  58
+                                              default=lambda: [1])
  59
+
  60
+class ChoiceFieldForm(django_forms.ModelForm):
  61
+    class Meta:
  62
+        model = ChoiceFieldModel
46 63
 
47 64
 class FileModel(models.Model):
48 65
     file = models.FileField(storage=temp_storage, upload_to='tests')
@@ -74,6 +91,74 @@ def test_choices_not_fetched_when_not_rendering(self):
74 91
         # only one query is required to pull the model from DB
75 92
         self.assertEqual(initial_queries+1, len(connection.queries))
76 93
 
  94
+class ModelFormCallableModelDefault(TestCase):
  95
+    def test_no_empty_option(self):
  96
+        "If a model's ForeignKey has blank=False and a default, no empty option is created (Refs #10792)."
  97
+        option = ChoiceOptionModel.objects.create(name='default')
  98
+
  99
+        choices = list(ChoiceFieldForm().fields['choice'].choices)
  100
+        self.assertEquals(len(choices), 1)
  101
+        self.assertEquals(choices[0], (option.pk, unicode(option)))
  102
+
  103
+    def test_callable_initial_value(self):
  104
+        "The initial value for a callable default returning a queryset is the pk (refs #13769)"
  105
+        obj1 = ChoiceOptionModel.objects.create(id=1, name='default')
  106
+        obj2 = ChoiceOptionModel.objects.create(id=2, name='option 2')
  107
+        obj3 = ChoiceOptionModel.objects.create(id=3, name='option 3')
  108
+        self.assertEquals(ChoiceFieldForm().as_p(), """<p><label for="id_choice">Choice:</label> <select name="choice" id="id_choice">
  109
+<option value="1" selected="selected">ChoiceOption 1</option>
  110
+<option value="2">ChoiceOption 2</option>
  111
+<option value="3">ChoiceOption 3</option>
  112
+</select><input type="hidden" name="initial-choice" value="1" id="initial-id_choice" /></p>
  113
+<p><label for="id_choice_int">Choice int:</label> <select name="choice_int" id="id_choice_int">
  114
+<option value="1" selected="selected">ChoiceOption 1</option>
  115
+<option value="2">ChoiceOption 2</option>
  116
+<option value="3">ChoiceOption 3</option>
  117
+</select><input type="hidden" name="initial-choice_int" value="1" id="initial-id_choice_int" /></p>
  118
+<p><label for="id_multi_choice">Multi choice:</label> <select multiple="multiple" name="multi_choice" id="id_multi_choice">
  119
+<option value="1" selected="selected">ChoiceOption 1</option>
  120
+<option value="2">ChoiceOption 2</option>
  121
+<option value="3">ChoiceOption 3</option>
  122
+</select><input type="hidden" name="initial-multi_choice" value="1" id="initial-id_multi_choice_0" /> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></p>
  123
+<p><label for="id_multi_choice_int">Multi choice int:</label> <select multiple="multiple" name="multi_choice_int" id="id_multi_choice_int">
  124
+<option value="1" selected="selected">ChoiceOption 1</option>
  125
+<option value="2">ChoiceOption 2</option>
  126
+<option value="3">ChoiceOption 3</option>
  127
+</select><input type="hidden" name="initial-multi_choice_int" value="1" id="initial-id_multi_choice_int_0" /> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></p>""")
  128
+
  129
+    def test_initial_instance_value(self):
  130
+        "Initial instances for model fields may also be instances (refs #7287)"
  131
+        obj1 = ChoiceOptionModel.objects.create(id=1, name='default')
  132
+        obj2 = ChoiceOptionModel.objects.create(id=2, name='option 2')
  133
+        obj3 = ChoiceOptionModel.objects.create(id=3, name='option 3')
  134
+        self.assertEquals(ChoiceFieldForm(initial={
  135
+                'choice': obj2,
  136
+                'choice_int': obj2,
  137
+                'multi_choice': [obj2,obj3],
  138
+                'multi_choice_int': ChoiceOptionModel.objects.exclude(name="default"),
  139
+            }).as_p(), """<p><label for="id_choice">Choice:</label> <select name="choice" id="id_choice">
  140
+<option value="1">ChoiceOption 1</option>
  141
+<option value="2" selected="selected">ChoiceOption 2</option>
  142
+<option value="3">ChoiceOption 3</option>
  143
+</select><input type="hidden" name="initial-choice" value="2" id="initial-id_choice" /></p>
  144
+<p><label for="id_choice_int">Choice int:</label> <select name="choice_int" id="id_choice_int">
  145
+<option value="1">ChoiceOption 1</option>
  146
+<option value="2" selected="selected">ChoiceOption 2</option>
  147
+<option value="3">ChoiceOption 3</option>
  148
+</select><input type="hidden" name="initial-choice_int" value="2" id="initial-id_choice_int" /></p>
  149
+<p><label for="id_multi_choice">Multi choice:</label> <select multiple="multiple" name="multi_choice" id="id_multi_choice">
  150
+<option value="1">ChoiceOption 1</option>
  151
+<option value="2" selected="selected">ChoiceOption 2</option>
  152
+<option value="3" selected="selected">ChoiceOption 3</option>
  153
+</select><input type="hidden" name="initial-multi_choice" value="2" id="initial-id_multi_choice_0" />
  154
+<input type="hidden" name="initial-multi_choice" value="3" id="initial-id_multi_choice_1" /> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></p>
  155
+<p><label for="id_multi_choice_int">Multi choice int:</label> <select multiple="multiple" name="multi_choice_int" id="id_multi_choice_int">
  156
+<option value="1">ChoiceOption 1</option>
  157
+<option value="2" selected="selected">ChoiceOption 2</option>
  158
+<option value="3" selected="selected">ChoiceOption 3</option>
  159
+</select><input type="hidden" name="initial-multi_choice_int" value="2" id="initial-id_multi_choice_int_0" />
  160
+<input type="hidden" name="initial-multi_choice_int" value="3" id="initial-id_multi_choice_int_1" /> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></p>""")
  161
+
77 162
 
78 163
 __test__ = {'API_TESTS': """
79 164
 >>> from django.forms.models import ModelForm
@@ -155,18 +240,5 @@ def test_choices_not_fetched_when_not_rendering(self):
155 240
 datetime.date(1999, 3, 2)
156 241
 >>> shutil.rmtree(temp_storage_location)
157 242
 
158  
-In a ModelForm with a ModelChoiceField, if the model's ForeignKey has blank=False and a default,
159  
-no empty option is created (regression test for #10792).
160  
-
161  
-First we need at least one instance of ChoiceOptionModel:
162  
-
163  
->>> ChoiceOptionModel.objects.create(name='default')
164  
-<ChoiceOptionModel: ChoiceOptionModel object>
165  
-
166  
->>> class ChoiceFieldForm(ModelForm):
167  
-...     class Meta:
168  
-...         model = ChoiceFieldModel
169  
->>> list(ChoiceFieldForm().fields['choice'].choices)
170  
-[(1, u'ChoiceOptionModel object')]
171 243
 
172 244
 """}

0 notes on commit b3dc3a0

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