mirror of
				https://github.com/django/django.git
				synced 2025-11-03 05:13:23 +00:00 
			
		
		
		
	Fixed #20649 -- Allowed blank field display to be defined in the initial list of choices.
This commit is contained in:
		
							parent
							
								
									a1889397a9
								
							
						
					
					
						commit
						1123f45511
					
				
					 8 changed files with 152 additions and 8 deletions
				
			
		
							
								
								
									
										1
									
								
								AUTHORS
									
										
									
									
									
								
							
							
						
						
									
										1
									
								
								AUTHORS
									
										
									
									
									
								
							| 
						 | 
					@ -161,6 +161,7 @@ answer newbie questions, and generally made Django that much better:
 | 
				
			||||||
    Paul Collier <paul@paul-collier.com>
 | 
					    Paul Collier <paul@paul-collier.com>
 | 
				
			||||||
    Paul Collins <paul.collins.iii@gmail.com>
 | 
					    Paul Collins <paul.collins.iii@gmail.com>
 | 
				
			||||||
    Robert Coup
 | 
					    Robert Coup
 | 
				
			||||||
 | 
					    Alex Couper <http://alexcouper.com/>
 | 
				
			||||||
    Deric Crago <deric.crago@gmail.com>
 | 
					    Deric Crago <deric.crago@gmail.com>
 | 
				
			||||||
    Brian Fabian Crain <http://www.bfc.do/>
 | 
					    Brian Fabian Crain <http://www.bfc.do/>
 | 
				
			||||||
    David Cramer <dcramer@gmail.com>
 | 
					    David Cramer <dcramer@gmail.com>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -544,7 +544,14 @@ class Field(object):
 | 
				
			||||||
    def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH):
 | 
					    def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH):
 | 
				
			||||||
        """Returns choices with a default blank choices included, for use
 | 
					        """Returns choices with a default blank choices included, for use
 | 
				
			||||||
        as SelectField choices for this field."""
 | 
					        as SelectField choices for this field."""
 | 
				
			||||||
        first_choice = blank_choice if include_blank else []
 | 
					        blank_defined = False
 | 
				
			||||||
 | 
					        for choice, _ in self.choices:
 | 
				
			||||||
 | 
					            if choice in ('', None):
 | 
				
			||||||
 | 
					                blank_defined = True
 | 
				
			||||||
 | 
					                break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        first_choice = (blank_choice if include_blank and
 | 
				
			||||||
 | 
					                        not blank_defined else [])
 | 
				
			||||||
        if self.choices:
 | 
					        if self.choices:
 | 
				
			||||||
            return first_choice + list(self.choices)
 | 
					            return first_choice + list(self.choices)
 | 
				
			||||||
        rel_model = self.rel.to
 | 
					        rel_model = self.rel.to
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -511,6 +511,8 @@ class Select(Widget):
 | 
				
			||||||
        return mark_safe('\n'.join(output))
 | 
					        return mark_safe('\n'.join(output))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def render_option(self, selected_choices, option_value, option_label):
 | 
					    def render_option(self, selected_choices, option_value, option_label):
 | 
				
			||||||
 | 
					        if option_value == None:
 | 
				
			||||||
 | 
					            option_value = ''
 | 
				
			||||||
        option_value = force_text(option_value)
 | 
					        option_value = force_text(option_value)
 | 
				
			||||||
        if option_value in selected_choices:
 | 
					        if option_value in selected_choices:
 | 
				
			||||||
            selected_html = mark_safe(' selected="selected"')
 | 
					            selected_html = mark_safe(' selected="selected"')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -152,11 +152,20 @@ method to retrieve the human-readable name for the field's current value. See
 | 
				
			||||||
:meth:`~django.db.models.Model.get_FOO_display` in the database API
 | 
					:meth:`~django.db.models.Model.get_FOO_display` in the database API
 | 
				
			||||||
documentation.
 | 
					documentation.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Finally, note that choices can be any iterable object -- not necessarily a list
 | 
					Note that choices can be any iterable object -- not necessarily a list or tuple.
 | 
				
			||||||
or tuple. This lets you construct choices dynamically. But if you find yourself
 | 
					This lets you construct choices dynamically. But if you find yourself hacking
 | 
				
			||||||
hacking :attr:`~Field.choices` to be dynamic, you're probably better off using a
 | 
					:attr:`~Field.choices` to be dynamic, you're probably better off using a proper
 | 
				
			||||||
proper database table with a :class:`ForeignKey`. :attr:`~Field.choices` is
 | 
					database table with a :class:`ForeignKey`. :attr:`~Field.choices` is meant for
 | 
				
			||||||
meant for static data that doesn't change much, if ever.
 | 
					static data that doesn't change much, if ever.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. versionadded:: 1.7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Unless :attr:`blank=False<Field.blank>` is set on the field along with a
 | 
				
			||||||
 | 
					:attr:`~Field.default` then a label containing ``"---------"`` will be rendered
 | 
				
			||||||
 | 
					with the select box. To override this behavior, add a tuple to ``choices``
 | 
				
			||||||
 | 
					containing ``None``; e.g. ``(None, 'Your String For Display')``.
 | 
				
			||||||
 | 
					Alternatively, you can use an empty string instead of ``None`` where this makes
 | 
				
			||||||
 | 
					sense - such as on a :class:`~django.db.models.CharField`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
``db_column``
 | 
					``db_column``
 | 
				
			||||||
-------------
 | 
					-------------
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -105,6 +105,11 @@ Minor features
 | 
				
			||||||
  <django.contrib.auth.forms.AuthenticationForm.confirm_login_allowed>` method
 | 
					  <django.contrib.auth.forms.AuthenticationForm.confirm_login_allowed>` method
 | 
				
			||||||
  to more easily customize the login policy.
 | 
					  to more easily customize the login policy.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* :attr:`Field.choices<django.db.models.Field.choices>` now allows you to
 | 
				
			||||||
 | 
					  customize the "empty choice" label by including a tuple with an empty string
 | 
				
			||||||
 | 
					  or ``None`` for the key and the custom label as the value. The default blank
 | 
				
			||||||
 | 
					  option ``"----------"`` will be omitted in this case.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Backwards incompatible changes in 1.7
 | 
					Backwards incompatible changes in 1.7
 | 
				
			||||||
=====================================
 | 
					=====================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,7 +34,30 @@ class Defaults(models.Model):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ChoiceModel(models.Model):
 | 
					class ChoiceModel(models.Model):
 | 
				
			||||||
    """For ModelChoiceField and ModelMultipleChoiceField tests."""
 | 
					    """For ModelChoiceField and ModelMultipleChoiceField tests."""
 | 
				
			||||||
 | 
					    CHOICES = [
 | 
				
			||||||
 | 
					        ('', 'No Preference'),
 | 
				
			||||||
 | 
					        ('f', 'Foo'),
 | 
				
			||||||
 | 
					        ('b', 'Bar'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    INTEGER_CHOICES = [
 | 
				
			||||||
 | 
					        (None, 'No Preference'),
 | 
				
			||||||
 | 
					        (1, 'Foo'),
 | 
				
			||||||
 | 
					        (2, 'Bar'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    STRING_CHOICES_WITH_NONE = [
 | 
				
			||||||
 | 
					        (None, 'No Preference'),
 | 
				
			||||||
 | 
					        ('f', 'Foo'),
 | 
				
			||||||
 | 
					        ('b', 'Bar'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    name = models.CharField(max_length=10)
 | 
					    name = models.CharField(max_length=10)
 | 
				
			||||||
 | 
					    choice = models.CharField(max_length=2, blank=True, choices=CHOICES)
 | 
				
			||||||
 | 
					    choice_string_w_none = models.CharField(
 | 
				
			||||||
 | 
					        max_length=2, blank=True, null=True, choices=STRING_CHOICES_WITH_NONE)
 | 
				
			||||||
 | 
					    choice_integer = models.IntegerField(choices=INTEGER_CHOICES, blank=True,
 | 
				
			||||||
 | 
					                                         null=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@python_2_unicode_compatible
 | 
					@python_2_unicode_compatible
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,8 +10,8 @@ from django.forms.models import ModelFormMetaclass
 | 
				
			||||||
from django.test import TestCase
 | 
					from django.test import TestCase
 | 
				
			||||||
from django.utils import six
 | 
					from django.utils import six
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..models import (ChoiceOptionModel, ChoiceFieldModel, FileModel, Group,
 | 
					from ..models import (ChoiceModel, ChoiceOptionModel, ChoiceFieldModel,
 | 
				
			||||||
    BoundaryModel, Defaults, OptionalMultiChoiceModel)
 | 
					    FileModel, Group, BoundaryModel, Defaults, OptionalMultiChoiceModel)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ChoiceFieldForm(ModelForm):
 | 
					class ChoiceFieldForm(ModelForm):
 | 
				
			||||||
| 
						 | 
					@ -34,6 +34,24 @@ class ChoiceFieldExclusionForm(ModelForm):
 | 
				
			||||||
        model = ChoiceFieldModel
 | 
					        model = ChoiceFieldModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EmptyCharLabelChoiceForm(ModelForm):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = ChoiceModel
 | 
				
			||||||
 | 
					        fields = ['name', 'choice']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EmptyIntegerLabelChoiceForm(ModelForm):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = ChoiceModel
 | 
				
			||||||
 | 
					        fields = ['name', 'choice_integer']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EmptyCharLabelNoneChoiceForm(ModelForm):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = ChoiceModel
 | 
				
			||||||
 | 
					        fields = ['name', 'choice_string_w_none']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FileForm(Form):
 | 
					class FileForm(Form):
 | 
				
			||||||
    file1 = FileField()
 | 
					    file1 = FileField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -259,3 +277,78 @@ class ManyToManyExclusionTestCase(TestCase):
 | 
				
			||||||
        self.assertEqual(form.instance.choice_int.pk, data['choice_int'])
 | 
					        self.assertEqual(form.instance.choice_int.pk, data['choice_int'])
 | 
				
			||||||
        self.assertEqual(list(form.instance.multi_choice.all()), [opt2, opt3])
 | 
					        self.assertEqual(list(form.instance.multi_choice.all()), [opt2, opt3])
 | 
				
			||||||
        self.assertEqual([obj.pk for obj in form.instance.multi_choice_int.all()], data['multi_choice_int'])
 | 
					        self.assertEqual([obj.pk for obj in form.instance.multi_choice_int.all()], data['multi_choice_int'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EmptyLabelTestCase(TestCase):
 | 
				
			||||||
 | 
					    def test_empty_field_char(self):
 | 
				
			||||||
 | 
					        f = EmptyCharLabelChoiceForm()
 | 
				
			||||||
 | 
					        self.assertHTMLEqual(f.as_p(),
 | 
				
			||||||
 | 
					            """<p><label for="id_name">Name:</label> <input id="id_name" maxlength="10" name="name" type="text" /></p>
 | 
				
			||||||
 | 
					<p><label for="id_choice">Choice:</label> <select id="id_choice" name="choice">
 | 
				
			||||||
 | 
					<option value="" selected="selected">No Preference</option>
 | 
				
			||||||
 | 
					<option value="f">Foo</option>
 | 
				
			||||||
 | 
					<option value="b">Bar</option>
 | 
				
			||||||
 | 
					</select></p>""")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_empty_field_char_none(self):
 | 
				
			||||||
 | 
					        f = EmptyCharLabelNoneChoiceForm()
 | 
				
			||||||
 | 
					        self.assertHTMLEqual(f.as_p(),
 | 
				
			||||||
 | 
					            """<p><label for="id_name">Name:</label> <input id="id_name" maxlength="10" name="name" type="text" /></p>
 | 
				
			||||||
 | 
					<p><label for="id_choice_string_w_none">Choice string w none:</label> <select id="id_choice_string_w_none" name="choice_string_w_none">
 | 
				
			||||||
 | 
					<option value="" selected="selected">No Preference</option>
 | 
				
			||||||
 | 
					<option value="f">Foo</option>
 | 
				
			||||||
 | 
					<option value="b">Bar</option>
 | 
				
			||||||
 | 
					</select></p>""")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_save_empty_label_forms(self):
 | 
				
			||||||
 | 
					        # Test that saving a form with a blank choice results in the expected
 | 
				
			||||||
 | 
					        # value being stored in the database.
 | 
				
			||||||
 | 
					        tests = [
 | 
				
			||||||
 | 
					            (EmptyCharLabelNoneChoiceForm, 'choice_string_w_none', None),
 | 
				
			||||||
 | 
					            (EmptyIntegerLabelChoiceForm, 'choice_integer', None),
 | 
				
			||||||
 | 
					            (EmptyCharLabelChoiceForm, 'choice', ''),
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for form, key, expected in tests:
 | 
				
			||||||
 | 
					            f = form({'name': 'some-key', key: ''})
 | 
				
			||||||
 | 
					            self.assertTrue(f.is_valid())
 | 
				
			||||||
 | 
					            m = f.save()
 | 
				
			||||||
 | 
					            self.assertEqual(expected, getattr(m, key))
 | 
				
			||||||
 | 
					            self.assertEqual('No Preference',
 | 
				
			||||||
 | 
					                             getattr(m, 'get_{0}_display'.format(key))())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_empty_field_integer(self):
 | 
				
			||||||
 | 
					        f = EmptyIntegerLabelChoiceForm()
 | 
				
			||||||
 | 
					        self.assertHTMLEqual(f.as_p(),
 | 
				
			||||||
 | 
					            """<p><label for="id_name">Name:</label> <input id="id_name" maxlength="10" name="name" type="text" /></p>
 | 
				
			||||||
 | 
					<p><label for="id_choice_integer">Choice integer:</label> <select id="id_choice_integer" name="choice_integer">
 | 
				
			||||||
 | 
					<option value="" selected="selected">No Preference</option>
 | 
				
			||||||
 | 
					<option value="1">Foo</option>
 | 
				
			||||||
 | 
					<option value="2">Bar</option>
 | 
				
			||||||
 | 
					</select></p>""")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_get_display_value_on_none(self):
 | 
				
			||||||
 | 
					        m = ChoiceModel.objects.create(name='test', choice='', choice_integer=None)
 | 
				
			||||||
 | 
					        self.assertEqual(None, m.choice_integer)
 | 
				
			||||||
 | 
					        self.assertEqual('No Preference', m.get_choice_integer_display())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_html_rendering_of_prepopulated_models(self):
 | 
				
			||||||
 | 
					        none_model = ChoiceModel(name='none-test', choice_integer=None)
 | 
				
			||||||
 | 
					        f = EmptyIntegerLabelChoiceForm(instance=none_model)
 | 
				
			||||||
 | 
					        self.assertHTMLEqual(f.as_p(),
 | 
				
			||||||
 | 
					            """<p><label for="id_name">Name:</label> <input id="id_name" maxlength="10" name="name" type="text" value="none-test"/></p>
 | 
				
			||||||
 | 
					<p><label for="id_choice_integer">Choice integer:</label> <select id="id_choice_integer" name="choice_integer">
 | 
				
			||||||
 | 
					<option value="" selected="selected">No Preference</option>
 | 
				
			||||||
 | 
					<option value="1">Foo</option>
 | 
				
			||||||
 | 
					<option value="2">Bar</option>
 | 
				
			||||||
 | 
					</select></p>""")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foo_model = ChoiceModel(name='foo-test', choice_integer=1)
 | 
				
			||||||
 | 
					        f = EmptyIntegerLabelChoiceForm(instance=foo_model)
 | 
				
			||||||
 | 
					        self.assertHTMLEqual(f.as_p(),
 | 
				
			||||||
 | 
					            """<p><label for="id_name">Name:</label> <input id="id_name" maxlength="10" name="name" type="text" value="foo-test"/></p>
 | 
				
			||||||
 | 
					<p><label for="id_choice_integer">Choice integer:</label> <select id="id_choice_integer" name="choice_integer">
 | 
				
			||||||
 | 
					<option value="">No Preference</option>
 | 
				
			||||||
 | 
					<option value="1" selected="selected">Foo</option>
 | 
				
			||||||
 | 
					<option value="2">Bar</option>
 | 
				
			||||||
 | 
					</select></p>""")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -331,6 +331,10 @@ class ValidationTest(test.TestCase):
 | 
				
			||||||
        f = models.CharField(choices=[('a', 'A'), ('b', 'B')])
 | 
					        f = models.CharField(choices=[('a', 'A'), ('b', 'B')])
 | 
				
			||||||
        self.assertRaises(ValidationError, f.clean, "not a", None)
 | 
					        self.assertRaises(ValidationError, f.clean, "not a", None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_charfield_get_choices_with_blank_defined(self):
 | 
				
			||||||
 | 
					        f = models.CharField(choices=[('', '<><>'), ('a', 'A')])
 | 
				
			||||||
 | 
					        self.assertEqual(f.get_choices(True), [('', '<><>'), ('a', 'A')])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_choices_validation_supports_named_groups(self):
 | 
					    def test_choices_validation_supports_named_groups(self):
 | 
				
			||||||
        f = models.IntegerField(
 | 
					        f = models.IntegerField(
 | 
				
			||||||
            choices=(('group', ((10, 'A'), (20, 'B'))), (30, 'C')))
 | 
					            choices=(('group', ((10, 'A'), (20, 'B'))), (30, 'C')))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue