Skip to content

Commit 8a006e1

Browse files
committed
[1.2.X] Fixed #13679, #13231, #7287 -- Ensured that models that have ForeignKeys/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.
Backport of r13577 from trunk. git-svn-id: http://code.djangoproject.com/svn/django/branches/releases/1.2.X@13578 bcc190cf-cafb-0310-a4f2-bffc1f526a37
1 parent 653da2d commit 8a006e1

File tree

5 files changed

+122
-34
lines changed

5 files changed

+122
-34
lines changed

django/forms/fields.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ def __init__(self, required=True, widget=None, label=None, initial=None,
127127

128128
self.validators = self.default_validators + validators
129129

130+
def prepare_value(self, value):
131+
return value
132+
130133
def to_python(self, value):
131134
return value
132135

django/forms/forms.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818
NON_FIELD_ERRORS = '__all__'
1919

2020
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()
2525

2626
def get_declared_fields(bases, attrs, with_base_fields=True):
2727
"""
@@ -423,13 +423,15 @@ def as_widget(self, widget=None, attrs=None, only_initial=False):
423423
"""
424424
if not widget:
425425
widget = self.field.widget
426+
426427
attrs = attrs or {}
427428
auto_id = self.auto_id
428429
if auto_id and 'id' not in attrs and 'id' not in widget.attrs:
429430
if not only_initial:
430431
attrs['id'] = auto_id
431432
else:
432433
attrs['id'] = self.html_initial_id
434+
433435
if not self.form.is_bound:
434436
data = self.form.initial.get(self.name, self.field.initial)
435437
if callable(data):
@@ -439,6 +441,8 @@ def as_widget(self, widget=None, attrs=None, only_initial=False):
439441
data = self.form.initial.get(self.name, self.field.initial)
440442
else:
441443
data = self.data
444+
data = self.field.prepare_value(data)
445+
442446
if not only_initial:
443447
name = self.html_name
444448
else:

django/forms/models.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -906,12 +906,7 @@ def __len__(self):
906906
return len(self.queryset)
907907

908908
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))
915910

916911
class ModelChoiceField(ChoiceField):
917912
"""A ChoiceField whose choices are a model QuerySet."""
@@ -971,8 +966,8 @@ def _get_choices(self):
971966
return self._choices
972967

973968
# 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*
976971
# time _get_choices() is called (and, thus, each time self.choices is
977972
# accessed) so that we can ensure the QuerySet has not been consumed. This
978973
# construct might look complicated but it allows for lazy evaluation of
@@ -981,6 +976,14 @@ def _get_choices(self):
981976

982977
choices = property(_get_choices, ChoiceField._set_choices)
983978

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+
984987
def to_python(self, value):
985988
if value in EMPTY_VALUES:
986989
return None
@@ -1030,3 +1033,8 @@ def clean(self, value):
10301033
if force_unicode(val) not in pks:
10311034
raise ValidationError(self.error_messages['invalid_choice'] % val)
10321035
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)

django/forms/widgets.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -450,24 +450,25 @@ def render(self, name, value, attrs=None, choices=()):
450450
output.append(u'</select>')
451451
return mark_safe(u'\n'.join(output))
452452

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+
453460
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)))
460461
# Normalize to strings.
461462
selected_choices = set([force_unicode(v) for v in selected_choices])
462463
output = []
463464
for option_value, option_label in chain(self.choices, choices):
464465
if isinstance(option_label, (list, tuple)):
465466
output.append(u'<optgroup label="%s">' % escape(force_unicode(option_value)))
466467
for option in option_label:
467-
output.append(render_option(*option))
468+
output.append(self.render_option(selected_choices, *option))
468469
output.append(u'</optgroup>')
469470
else:
470-
output.append(render_option(option_value, option_label))
471+
output.append(self.render_option(selected_choices, option_value, option_label))
471472
return u'\n'.join(output)
472473

473474
class NullBooleanSelect(Select):

tests/regressiontests/forms/models.py

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,28 @@ class ChoiceOptionModel(models.Model):
3838
Can't reuse ChoiceModel because error_message tests require that it have no instances."""
3939
name = models.CharField(max_length=10)
4040

41+
class Meta:
42+
ordering = ('name',)
43+
44+
def __unicode__(self):
45+
return u'ChoiceOption %d' % self.pk
46+
4147
class ChoiceFieldModel(models.Model):
4248
"""Model with ForeignKey to another model, for testing ModelForm
4349
generation with ModelChoiceField."""
4450
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
4663

4764
class FileModel(models.Model):
4865
file = models.FileField(storage=temp_storage, upload_to='tests')
@@ -74,6 +91,74 @@ def test_choices_not_fetched_when_not_rendering(self):
7491
# only one query is required to pull the model from DB
7592
self.assertEqual(initial_queries+1, len(connection.queries))
7693

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+
77162

78163
__test__ = {'API_TESTS': """
79164
>>> from django.forms.models import ModelForm
@@ -155,18 +240,5 @@ def test_choices_not_fetched_when_not_rendering(self):
155240
datetime.date(1999, 3, 2)
156241
>>> shutil.rmtree(temp_storage_location)
157242
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')]
171243
172244
"""}

0 commit comments

Comments
 (0)