diff --git a/django/newforms/fields.py b/django/newforms/fields.py index e96cb4a2d1f..bcd30dbc92a 100644 --- a/django/newforms/fields.py +++ b/django/newforms/fields.py @@ -4,7 +4,7 @@ Field classes from django.utils.translation import gettext from util import ErrorList, ValidationError, smart_unicode -from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, CheckboxInput, Select, SelectMultiple +from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple import datetime import re import time @@ -15,7 +15,7 @@ __all__ = ( 'DEFAULT_TIME_INPUT_FORMATS', 'TimeField', 'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField', 'RegexField', 'EmailField', 'URLField', 'BooleanField', - 'ChoiceField', 'MultipleChoiceField', + 'ChoiceField', 'NullBooleanField', 'MultipleChoiceField', 'ComboField', 'MultiValueField', 'SplitDateTimeField', ) @@ -317,6 +317,16 @@ class BooleanField(Field): super(BooleanField, self).clean(value) return bool(value) +class NullBooleanField(BooleanField): + """ + A field whose valid values are None, True and False. Invalid values are + cleaned to None. + """ + widget = NullBooleanSelect + + def clean(self, value): + return {True: True, False: False}.get(value, None) + class ChoiceField(Field): def __init__(self, choices=(), required=True, widget=Select, label=None, initial=None): if isinstance(widget, type): diff --git a/django/newforms/widgets.py b/django/newforms/widgets.py index 2b709d86844..c71810465e7 100644 --- a/django/newforms/widgets.py +++ b/django/newforms/widgets.py @@ -5,13 +5,14 @@ HTML Widget classes __all__ = ( 'Widget', 'TextInput', 'PasswordInput', 'HiddenInput', 'MultipleHiddenInput', 'FileInput', 'Textarea', 'CheckboxInput', - 'Select', 'SelectMultiple', 'RadioSelect', 'CheckboxSelectMultiple', + 'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect', 'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget', ) from util import flatatt, StrAndUnicode, smart_unicode from django.utils.datastructures import MultiValueDict from django.utils.html import escape +from django.utils.translation import gettext from itertools import chain try: @@ -151,6 +152,25 @@ class Select(Widget): output.append(u'') return u'\n'.join(output) +class NullBooleanSelect(Select): + """ + A Select Widget intended to be used with NullBooleanField. + """ + def __init__(self, attrs=None): + choices = ((u'1', gettext('Unknown')), (u'2', gettext('Yes')), (u'3', gettext('No'))) + super(NullBooleanSelect, self).__init__(attrs, choices) + + def render(self, name, value, attrs=None, choices=()): + try: + value = {True: u'2', False: u'3', u'2': u'2', u'3': u'3'}[value] + except KeyError: + value = u'1' + return super(NullBooleanSelect, self).render(name, value, attrs, choices) + + def value_from_datadict(self, data, name): + value = data.get(name, None) + return {u'2': True, u'3': False, True: True, False: False}.get(value, None) + class SelectMultiple(Widget): def __init__(self, attrs=None, choices=()): # choices can be any iterable diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py index 389b076ddd6..95e8b59c019 100644 --- a/tests/regressiontests/forms/tests.py +++ b/tests/regressiontests/forms/tests.py @@ -336,6 +336,40 @@ If 'choices' is passed to both the constructor and render(), then they'll both b >>> w.render('email', 'ŠĐĆŽćžšđ', choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')]) u'' +# NullBooleanSelect Widget #################################################### + +>>> w = NullBooleanSelect() +>>> print w.render('is_cool', True) + +>>> print w.render('is_cool', False) + +>>> print w.render('is_cool', None) + +>>> print w.render('is_cool', '2') + +>>> print w.render('is_cool', '3') + + # SelectMultiple Widget ####################################################### >>> w = SelectMultiple() @@ -1463,6 +1497,20 @@ Traceback (most recent call last): ... ValidationError: [u'Select a valid choice. John is not one of the available choices.'] +# NullBooleanField ############################################################ + +>>> f = NullBooleanField() +>>> f.clean('') +>>> f.clean(True) +True +>>> f.clean(False) +False +>>> f.clean(None) +>>> f.clean('1') +>>> f.clean('2') +>>> f.clean('3') +>>> f.clean('hello') + # MultipleChoiceField ######################################################### >>> f = MultipleChoiceField(choices=[('1', '1'), ('2', '2')]) @@ -2601,6 +2649,57 @@ True >>> p.clean_data {'first_name': u'John', 'last_name': u'Lennon', 'birthday': datetime.date(1940, 10, 9)} +# Forms with NullBooleanFields ################################################ + +NullBooleanField is a bit of a special case because its presentation (widget) +is different than its data. This is handled transparently, though. + +>>> class Person(Form): +... name = CharField() +... is_cool = NullBooleanField() +>>> p = Person({'name': u'Joe'}, auto_id=False) +>>> print p['is_cool'] + +>>> p = Person({'name': u'Joe', 'is_cool': u'1'}, auto_id=False) +>>> print p['is_cool'] + +>>> p = Person({'name': u'Joe', 'is_cool': u'2'}, auto_id=False) +>>> print p['is_cool'] + +>>> p = Person({'name': u'Joe', 'is_cool': u'3'}, auto_id=False) +>>> print p['is_cool'] + +>>> p = Person({'name': u'Joe', 'is_cool': True}, auto_id=False) +>>> print p['is_cool'] + +>>> p = Person({'name': u'Joe', 'is_cool': False}, auto_id=False) +>>> print p['is_cool'] + + # Basic form processing in a view ############################################# >>> from django.template import Template, Context