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.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@13577 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Russell Keith-Magee 2010-08-14 12:05:41 +00:00
parent d69cdc6d70
commit b3dc3a0106
5 changed files with 122 additions and 34 deletions

View File

@ -127,6 +127,9 @@ class Field(object):
self.validators = self.default_validators + validators self.validators = self.default_validators + validators
def prepare_value(self, value):
return value
def to_python(self, value): def to_python(self, value):
return value return value

View File

@ -423,6 +423,7 @@ class BoundField(StrAndUnicode):
""" """
if not widget: if not widget:
widget = self.field.widget widget = self.field.widget
attrs = attrs or {} attrs = attrs or {}
auto_id = self.auto_id auto_id = self.auto_id
if auto_id and 'id' not in attrs and 'id' not in widget.attrs: if auto_id and 'id' not in attrs and 'id' not in widget.attrs:
@ -430,6 +431,7 @@ class BoundField(StrAndUnicode):
attrs['id'] = auto_id attrs['id'] = auto_id
else: else:
attrs['id'] = self.html_initial_id attrs['id'] = self.html_initial_id
if not self.form.is_bound: if not self.form.is_bound:
data = self.form.initial.get(self.name, self.field.initial) data = self.form.initial.get(self.name, self.field.initial)
if callable(data): if callable(data):
@ -439,6 +441,8 @@ class BoundField(StrAndUnicode):
data = self.form.initial.get(self.name, self.field.initial) data = self.form.initial.get(self.name, self.field.initial)
else: else:
data = self.data data = self.data
data = self.field.prepare_value(data)
if not only_initial: if not only_initial:
name = self.html_name name = self.html_name
else: else:

View File

@ -906,12 +906,7 @@ class ModelChoiceIterator(object):
return len(self.queryset) return len(self.queryset)
def choice(self, obj): def choice(self, obj):
if self.field.to_field_name: return (self.field.prepare_value(obj), self.field.label_from_instance(obj))
key = obj.serializable_value(self.field.to_field_name)
else:
key = obj.pk
return (key, self.field.label_from_instance(obj))
class ModelChoiceField(ChoiceField): class ModelChoiceField(ChoiceField):
"""A ChoiceField whose choices are a model QuerySet.""" """A ChoiceField whose choices are a model QuerySet."""
@ -971,8 +966,8 @@ class ModelChoiceField(ChoiceField):
return self._choices return self._choices
# Otherwise, execute the QuerySet in self.queryset to determine the # Otherwise, execute the QuerySet in self.queryset to determine the
# choices dynamically. Return a fresh QuerySetIterator that has not been # choices dynamically. Return a fresh ModelChoiceIterator that has not been
# consumed. Note that we're instantiating a new QuerySetIterator *each* # consumed. Note that we're instantiating a new ModelChoiceIterator *each*
# time _get_choices() is called (and, thus, each time self.choices is # time _get_choices() is called (and, thus, each time self.choices is
# accessed) so that we can ensure the QuerySet has not been consumed. This # accessed) so that we can ensure the QuerySet has not been consumed. This
# construct might look complicated but it allows for lazy evaluation of # construct might look complicated but it allows for lazy evaluation of
@ -981,6 +976,14 @@ class ModelChoiceField(ChoiceField):
choices = property(_get_choices, ChoiceField._set_choices) choices = property(_get_choices, ChoiceField._set_choices)
def prepare_value(self, value):
if hasattr(value, '_meta'):
if self.to_field_name:
return value.serializable_value(self.to_field_name)
else:
return value.pk
return super(ModelChoiceField, self).prepare_value(value)
def to_python(self, value): def to_python(self, value):
if value in EMPTY_VALUES: if value in EMPTY_VALUES:
return None return None
@ -1030,3 +1033,8 @@ class ModelMultipleChoiceField(ModelChoiceField):
if force_unicode(val) not in pks: if force_unicode(val) not in pks:
raise ValidationError(self.error_messages['invalid_choice'] % val) raise ValidationError(self.error_messages['invalid_choice'] % val)
return qs return qs
def prepare_value(self, value):
if hasattr(value, '__iter__'):
return [super(ModelMultipleChoiceField, self).prepare_value(v) for v in value]
return super(ModelMultipleChoiceField, self).prepare_value(value)

View File

@ -450,13 +450,14 @@ class Select(Widget):
output.append(u'</select>') output.append(u'</select>')
return mark_safe(u'\n'.join(output)) return mark_safe(u'\n'.join(output))
def render_options(self, choices, selected_choices): def render_option(self, selected_choices, option_value, option_label):
def render_option(option_value, option_label):
option_value = force_unicode(option_value) option_value = force_unicode(option_value)
selected_html = (option_value in selected_choices) and u' selected="selected"' or '' selected_html = (option_value in selected_choices) and u' selected="selected"' or ''
return u'<option value="%s"%s>%s</option>' % ( return u'<option value="%s"%s>%s</option>' % (
escape(option_value), selected_html, escape(option_value), selected_html,
conditional_escape(force_unicode(option_label))) conditional_escape(force_unicode(option_label)))
def render_options(self, choices, selected_choices):
# Normalize to strings. # Normalize to strings.
selected_choices = set([force_unicode(v) for v in selected_choices]) selected_choices = set([force_unicode(v) for v in selected_choices])
output = [] output = []
@ -464,10 +465,10 @@ class Select(Widget):
if isinstance(option_label, (list, tuple)): if isinstance(option_label, (list, tuple)):
output.append(u'<optgroup label="%s">' % escape(force_unicode(option_value))) output.append(u'<optgroup label="%s">' % escape(force_unicode(option_value)))
for option in option_label: for option in option_label:
output.append(render_option(*option)) output.append(self.render_option(selected_choices, *option))
output.append(u'</optgroup>') output.append(u'</optgroup>')
else: else:
output.append(render_option(option_value, option_label)) output.append(self.render_option(selected_choices, option_value, option_label))
return u'\n'.join(output) return u'\n'.join(output)
class NullBooleanSelect(Select): class NullBooleanSelect(Select):

View File

@ -38,11 +38,28 @@ class ChoiceOptionModel(models.Model):
Can't reuse ChoiceModel because error_message tests require that it have no instances.""" Can't reuse ChoiceModel because error_message tests require that it have no instances."""
name = models.CharField(max_length=10) name = models.CharField(max_length=10)
class Meta:
ordering = ('name',)
def __unicode__(self):
return u'ChoiceOption %d' % self.pk
class ChoiceFieldModel(models.Model): class ChoiceFieldModel(models.Model):
"""Model with ForeignKey to another model, for testing ModelForm """Model with ForeignKey to another model, for testing ModelForm
generation with ModelChoiceField.""" generation with ModelChoiceField."""
choice = models.ForeignKey(ChoiceOptionModel, blank=False, choice = models.ForeignKey(ChoiceOptionModel, blank=False,
default=lambda: ChoiceOptionModel.objects.all()[0]) default=lambda: ChoiceOptionModel.objects.get(name='default'))
choice_int = models.ForeignKey(ChoiceOptionModel, blank=False, related_name='choice_int',
default=lambda: 1)
multi_choice = models.ManyToManyField(ChoiceOptionModel, blank=False, related_name='multi_choice',
default=lambda: ChoiceOptionModel.objects.filter(name='default'))
multi_choice_int = models.ManyToManyField(ChoiceOptionModel, blank=False, related_name='multi_choice_int',
default=lambda: [1])
class ChoiceFieldForm(django_forms.ModelForm):
class Meta:
model = ChoiceFieldModel
class FileModel(models.Model): class FileModel(models.Model):
file = models.FileField(storage=temp_storage, upload_to='tests') file = models.FileField(storage=temp_storage, upload_to='tests')
@ -74,6 +91,74 @@ class TestTicket12510(TestCase):
# only one query is required to pull the model from DB # only one query is required to pull the model from DB
self.assertEqual(initial_queries+1, len(connection.queries)) self.assertEqual(initial_queries+1, len(connection.queries))
class ModelFormCallableModelDefault(TestCase):
def test_no_empty_option(self):
"If a model's ForeignKey has blank=False and a default, no empty option is created (Refs #10792)."
option = ChoiceOptionModel.objects.create(name='default')
choices = list(ChoiceFieldForm().fields['choice'].choices)
self.assertEquals(len(choices), 1)
self.assertEquals(choices[0], (option.pk, unicode(option)))
def test_callable_initial_value(self):
"The initial value for a callable default returning a queryset is the pk (refs #13769)"
obj1 = ChoiceOptionModel.objects.create(id=1, name='default')
obj2 = ChoiceOptionModel.objects.create(id=2, name='option 2')
obj3 = ChoiceOptionModel.objects.create(id=3, name='option 3')
self.assertEquals(ChoiceFieldForm().as_p(), """<p><label for="id_choice">Choice:</label> <select name="choice" id="id_choice">
<option value="1" selected="selected">ChoiceOption 1</option>
<option value="2">ChoiceOption 2</option>
<option value="3">ChoiceOption 3</option>
</select><input type="hidden" name="initial-choice" value="1" id="initial-id_choice" /></p>
<p><label for="id_choice_int">Choice int:</label> <select name="choice_int" id="id_choice_int">
<option value="1" selected="selected">ChoiceOption 1</option>
<option value="2">ChoiceOption 2</option>
<option value="3">ChoiceOption 3</option>
</select><input type="hidden" name="initial-choice_int" value="1" id="initial-id_choice_int" /></p>
<p><label for="id_multi_choice">Multi choice:</label> <select multiple="multiple" name="multi_choice" id="id_multi_choice">
<option value="1" selected="selected">ChoiceOption 1</option>
<option value="2">ChoiceOption 2</option>
<option value="3">ChoiceOption 3</option>
</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>
<p><label for="id_multi_choice_int">Multi choice int:</label> <select multiple="multiple" name="multi_choice_int" id="id_multi_choice_int">
<option value="1" selected="selected">ChoiceOption 1</option>
<option value="2">ChoiceOption 2</option>
<option value="3">ChoiceOption 3</option>
</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>""")
def test_initial_instance_value(self):
"Initial instances for model fields may also be instances (refs #7287)"
obj1 = ChoiceOptionModel.objects.create(id=1, name='default')
obj2 = ChoiceOptionModel.objects.create(id=2, name='option 2')
obj3 = ChoiceOptionModel.objects.create(id=3, name='option 3')
self.assertEquals(ChoiceFieldForm(initial={
'choice': obj2,
'choice_int': obj2,
'multi_choice': [obj2,obj3],
'multi_choice_int': ChoiceOptionModel.objects.exclude(name="default"),
}).as_p(), """<p><label for="id_choice">Choice:</label> <select name="choice" id="id_choice">
<option value="1">ChoiceOption 1</option>
<option value="2" selected="selected">ChoiceOption 2</option>
<option value="3">ChoiceOption 3</option>
</select><input type="hidden" name="initial-choice" value="2" id="initial-id_choice" /></p>
<p><label for="id_choice_int">Choice int:</label> <select name="choice_int" id="id_choice_int">
<option value="1">ChoiceOption 1</option>
<option value="2" selected="selected">ChoiceOption 2</option>
<option value="3">ChoiceOption 3</option>
</select><input type="hidden" name="initial-choice_int" value="2" id="initial-id_choice_int" /></p>
<p><label for="id_multi_choice">Multi choice:</label> <select multiple="multiple" name="multi_choice" id="id_multi_choice">
<option value="1">ChoiceOption 1</option>
<option value="2" selected="selected">ChoiceOption 2</option>
<option value="3" selected="selected">ChoiceOption 3</option>
</select><input type="hidden" name="initial-multi_choice" value="2" id="initial-id_multi_choice_0" />
<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>
<p><label for="id_multi_choice_int">Multi choice int:</label> <select multiple="multiple" name="multi_choice_int" id="id_multi_choice_int">
<option value="1">ChoiceOption 1</option>
<option value="2" selected="selected">ChoiceOption 2</option>
<option value="3" selected="selected">ChoiceOption 3</option>
</select><input type="hidden" name="initial-multi_choice_int" value="2" id="initial-id_multi_choice_int_0" />
<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>""")
__test__ = {'API_TESTS': """ __test__ = {'API_TESTS': """
>>> from django.forms.models import ModelForm >>> from django.forms.models import ModelForm
@ -155,18 +240,5 @@ u'class default value'
datetime.date(1999, 3, 2) datetime.date(1999, 3, 2)
>>> shutil.rmtree(temp_storage_location) >>> shutil.rmtree(temp_storage_location)
In a ModelForm with a ModelChoiceField, if the model's ForeignKey has blank=False and a default,
no empty option is created (regression test for #10792).
First we need at least one instance of ChoiceOptionModel:
>>> ChoiceOptionModel.objects.create(name='default')
<ChoiceOptionModel: ChoiceOptionModel object>
>>> class ChoiceFieldForm(ModelForm):
... class Meta:
... model = ChoiceFieldModel
>>> list(ChoiceFieldForm().fields['choice'].choices)
[(1, u'ChoiceOptionModel object')]
"""} """}