Author: russellm Date: 2010-08-14 07:05:41 -0500 (Sat, 14 Aug 2010) New Revision: 13577
Modified: django/trunk/django/forms/fields.py django/trunk/django/forms/forms.py django/trunk/django/forms/models.py django/trunk/django/forms/widgets.py django/trunk/tests/regressiontests/forms/models.py Log: 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. Modified: django/trunk/django/forms/fields.py =================================================================== --- django/trunk/django/forms/fields.py 2010-08-14 00:52:33 UTC (rev 13576) +++ django/trunk/django/forms/fields.py 2010-08-14 12:05:41 UTC (rev 13577) @@ -127,6 +127,9 @@ self.validators = self.default_validators + validators + def prepare_value(self, value): + return value + def to_python(self, value): return value Modified: django/trunk/django/forms/forms.py =================================================================== --- django/trunk/django/forms/forms.py 2010-08-14 00:52:33 UTC (rev 13576) +++ django/trunk/django/forms/forms.py 2010-08-14 12:05:41 UTC (rev 13577) @@ -18,10 +18,10 @@ NON_FIELD_ERRORS = '__all__' def pretty_name(name): - """Converts 'first_name' to 'First name'""" - if not name: - return u'' - return name.replace('_', ' ').capitalize() + """Converts 'first_name' to 'First name'""" + if not name: + return u'' + return name.replace('_', ' ').capitalize() def get_declared_fields(bases, attrs, with_base_fields=True): """ @@ -423,6 +423,7 @@ """ if not widget: widget = self.field.widget + attrs = attrs or {} auto_id = self.auto_id if auto_id and 'id' not in attrs and 'id' not in widget.attrs: @@ -430,6 +431,7 @@ attrs['id'] = auto_id else: attrs['id'] = self.html_initial_id + if not self.form.is_bound: data = self.form.initial.get(self.name, self.field.initial) if callable(data): @@ -439,6 +441,8 @@ data = self.form.initial.get(self.name, self.field.initial) else: data = self.data + data = self.field.prepare_value(data) + if not only_initial: name = self.html_name else: Modified: django/trunk/django/forms/models.py =================================================================== --- django/trunk/django/forms/models.py 2010-08-14 00:52:33 UTC (rev 13576) +++ django/trunk/django/forms/models.py 2010-08-14 12:05:41 UTC (rev 13577) @@ -906,13 +906,8 @@ return len(self.queryset) def choice(self, obj): - if self.field.to_field_name: - key = obj.serializable_value(self.field.to_field_name) - else: - key = obj.pk - return (key, self.field.label_from_instance(obj)) + return (self.field.prepare_value(obj), self.field.label_from_instance(obj)) - class ModelChoiceField(ChoiceField): """A ChoiceField whose choices are a model QuerySet.""" # This class is a subclass of ChoiceField for purity, but it doesn't @@ -971,8 +966,8 @@ return self._choices # Otherwise, execute the QuerySet in self.queryset to determine the - # choices dynamically. Return a fresh QuerySetIterator that has not been - # consumed. Note that we're instantiating a new QuerySetIterator *each* + # choices dynamically. Return a fresh ModelChoiceIterator that has not been + # consumed. Note that we're instantiating a new ModelChoiceIterator *each* # 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 # construct might look complicated but it allows for lazy evaluation of @@ -981,6 +976,14 @@ 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): if value in EMPTY_VALUES: return None @@ -1030,3 +1033,8 @@ if force_unicode(val) not in pks: raise ValidationError(self.error_messages['invalid_choice'] % val) 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) Modified: django/trunk/django/forms/widgets.py =================================================================== --- django/trunk/django/forms/widgets.py 2010-08-14 00:52:33 UTC (rev 13576) +++ django/trunk/django/forms/widgets.py 2010-08-14 12:05:41 UTC (rev 13577) @@ -450,13 +450,14 @@ output.append(u'</select>') return mark_safe(u'\n'.join(output)) + def render_option(self, selected_choices, option_value, option_label): + option_value = force_unicode(option_value) + selected_html = (option_value in selected_choices) and u' selected="selected"' or '' + return u'<option value="%s"%s>%s</option>' % ( + escape(option_value), selected_html, + conditional_escape(force_unicode(option_label))) + def render_options(self, choices, selected_choices): - def render_option(option_value, option_label): - option_value = force_unicode(option_value) - selected_html = (option_value in selected_choices) and u' selected="selected"' or '' - return u'<option value="%s"%s>%s</option>' % ( - escape(option_value), selected_html, - conditional_escape(force_unicode(option_label))) # Normalize to strings. selected_choices = set([force_unicode(v) for v in selected_choices]) output = [] @@ -464,10 +465,10 @@ if isinstance(option_label, (list, tuple)): output.append(u'<optgroup label="%s">' % escape(force_unicode(option_value))) for option in option_label: - output.append(render_option(*option)) + output.append(self.render_option(selected_choices, *option)) output.append(u'</optgroup>') 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) class NullBooleanSelect(Select): Modified: django/trunk/tests/regressiontests/forms/models.py =================================================================== --- django/trunk/tests/regressiontests/forms/models.py 2010-08-14 00:52:33 UTC (rev 13576) +++ django/trunk/tests/regressiontests/forms/models.py 2010-08-14 12:05:41 UTC (rev 13577) @@ -38,12 +38,29 @@ Can't reuse ChoiceModel because error_message tests require that it have no instances.""" name = models.CharField(max_length=10) + class Meta: + ordering = ('name',) + + def __unicode__(self): + return u'ChoiceOption %d' % self.pk + class ChoiceFieldModel(models.Model): """Model with ForeignKey to another model, for testing ModelForm generation with ModelChoiceField.""" 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): file = models.FileField(storage=temp_storage, upload_to='tests') @@ -74,7 +91,75 @@ # only one query is required to pull the model from DB 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': """ >>> from django.forms.models import ModelForm >>> from django.core.files.uploadedfile import SimpleUploadedFile @@ -155,18 +240,5 @@ datetime.date(1999, 3, 2) >>> 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')] - """} -- You received this message because you are subscribed to the Google Groups "Django updates" group. To post to this group, send email to django-upda...@googlegroups.com. To unsubscribe from this group, send email to django-updates+unsubscr...@googlegroups.com. For more options, visit this group at http://groups.google.com/group/django-updates?hl=en.