mirror of
				https://github.com/django/django.git
				synced 2025-11-04 05:35:37 +00:00 
			
		
		
		
	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:
		
							parent
							
								
									d69cdc6d70
								
							
						
					
					
						commit
						b3dc3a0106
					
				
					 5 changed files with 122 additions and 34 deletions
				
			
		| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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')]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""}
 | 
					"""}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue