Fixed #20649 -- Allowed blank field display to be defined in the initial list of choices.

This commit is contained in:
Alex Couper 2013-07-20 21:49:33 +00:00 committed by Tim Graham
parent a1889397a9
commit 1123f45511
8 changed files with 152 additions and 8 deletions

View File

@ -161,6 +161,7 @@ answer newbie questions, and generally made Django that much better:
Paul Collier <paul@paul-collier.com>
Paul Collins <paul.collins.iii@gmail.com>
Robert Coup
Alex Couper <http://alexcouper.com/>
Deric Crago <deric.crago@gmail.com>
Brian Fabian Crain <http://www.bfc.do/>
David Cramer <dcramer@gmail.com>

View File

@ -544,7 +544,14 @@ class Field(object):
def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH):
"""Returns choices with a default blank choices included, for use
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:
return first_choice + list(self.choices)
rel_model = self.rel.to

View File

@ -511,6 +511,8 @@ class Select(Widget):
return mark_safe('\n'.join(output))
def render_option(self, selected_choices, option_value, option_label):
if option_value == None:
option_value = ''
option_value = force_text(option_value)
if option_value in selected_choices:
selected_html = mark_safe(' selected="selected"')

View File

@ -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
documentation.
Finally, note that choices can be any iterable object -- not necessarily a list
or tuple. This lets you construct choices dynamically. But if you find yourself
hacking :attr:`~Field.choices` to be dynamic, you're probably better off using a
proper database table with a :class:`ForeignKey`. :attr:`~Field.choices` is
meant for static data that doesn't change much, if ever.
Note that choices can be any iterable object -- not necessarily a list or tuple.
This lets you construct choices dynamically. But if you find yourself hacking
:attr:`~Field.choices` to be dynamic, you're probably better off using a proper
database table with a :class:`ForeignKey`. :attr:`~Field.choices` is meant for
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``
-------------

View File

@ -105,6 +105,11 @@ Minor features
<django.contrib.auth.forms.AuthenticationForm.confirm_login_allowed>` method
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
=====================================

View File

@ -34,7 +34,30 @@ class Defaults(models.Model):
class ChoiceModel(models.Model):
"""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)
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

View File

@ -10,8 +10,8 @@ from django.forms.models import ModelFormMetaclass
from django.test import TestCase
from django.utils import six
from ..models import (ChoiceOptionModel, ChoiceFieldModel, FileModel, Group,
BoundaryModel, Defaults, OptionalMultiChoiceModel)
from ..models import (ChoiceModel, ChoiceOptionModel, ChoiceFieldModel,
FileModel, Group, BoundaryModel, Defaults, OptionalMultiChoiceModel)
class ChoiceFieldForm(ModelForm):
@ -34,6 +34,24 @@ class ChoiceFieldExclusionForm(ModelForm):
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):
file1 = FileField()
@ -259,3 +277,78 @@ class ManyToManyExclusionTestCase(TestCase):
self.assertEqual(form.instance.choice_int.pk, data['choice_int'])
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'])
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>""")

View File

@ -331,6 +331,10 @@ class ValidationTest(test.TestCase):
f = models.CharField(choices=[('a', 'A'), ('b', 'B')])
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):
f = models.IntegerField(
choices=(('group', ((10, 'A'), (20, 'B'))), (30, 'C')))