diff --git a/django/forms/fields.py b/django/forms/fields.py index afbefb64c01..128b6bcbf1d 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -70,7 +70,7 @@ class Field(six.with_metaclass(RenameFieldMethods, object)): def __init__(self, required=True, widget=None, label=None, initial=None, help_text='', error_messages=None, show_hidden_initial=False, - validators=[], localize=False, label_suffix=None): + validators=[], localize=False, disabled=False, label_suffix=None): # required -- Boolean that specifies whether the field is required. # True by default. # widget -- A Widget class, or instance of a Widget class, that should @@ -90,11 +90,14 @@ class Field(six.with_metaclass(RenameFieldMethods, object)): # hidden widget with initial value after widget. # validators -- List of additional validators to use # localize -- Boolean that specifies if the field should be localized. + # disabled -- Boolean that specifies whether the field is disabled, that + # is its widget is shown in the form but not editable. # label_suffix -- Suffix to be added to the label. Overrides # form's label_suffix. self.required, self.label, self.initial = required, label, initial self.show_hidden_initial = show_hidden_initial self.help_text = help_text + self.disabled = disabled self.label_suffix = label_suffix widget = widget or self.widget if isinstance(widget, type): diff --git a/django/forms/forms.py b/django/forms/forms.py index 4ee79d13ce3..ad9e87cae43 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -386,7 +386,10 @@ class BaseForm(object): # value_from_datadict() gets the data from the data dictionaries. # Each widget type knows how to retrieve its own data, because some # widgets split data over several HTML fields. - value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name)) + if field.disabled: + value = self.initial.get(name, field.initial) + else: + value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name)) try: if isinstance(field, FileField): initial = self.initial.get(name, field.initial) @@ -567,6 +570,8 @@ class BoundField(object): widget.is_localized = True attrs = attrs or {} + if self.field.disabled: + attrs['disabled'] = True auto_id = self.auto_id if auto_id and 'id' not in attrs and 'id' not in widget.attrs: if not only_initial: diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index c4a40021d0a..776ef873add 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -299,6 +299,17 @@ as the rendered output. See the :ref:`format localization ` documentation for more information. +``disabled`` +~~~~~~~~~~~~ + +.. attribute:: Field.disabled + +.. versionadded:: 1.9 + +The ``disabled`` boolean argument, when set to ``True``, disables a form field +using the ``disabled`` HTML attribute so that it won't be editable by users. +Even if a user tampers with the field's value submitted to the server, it will +be ignored in favor of the value from the form's initial data. Checking if the field data has changed -------------------------------------- diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index 783009db07a..6dc0e8473f7 100644 --- a/docs/releases/1.9.txt +++ b/docs/releases/1.9.txt @@ -313,6 +313,9 @@ Forms and trailing whitespace. As this defaults to ``True`` this is different behavior from previous releases. +* Form fields now support the :attr:`~django.forms.Field.disabled` argument, + allowing the field widget to be displayed disabled by browsers. + Generic Views ^^^^^^^^^^^^^ diff --git a/tests/forms_tests/tests/test_fields.py b/tests/forms_tests/tests/test_fields.py index 9c341ca7a3b..1644bda4e2d 100644 --- a/tests/forms_tests/tests/test_fields.py +++ b/tests/forms_tests/tests/test_fields.py @@ -176,6 +176,10 @@ class FieldsTests(SimpleTestCase): self.assertEqual(f.clean(' 1'), ' 1') self.assertEqual(f.clean('1 '), '1 ') + def test_charfield_disabled(self): + f = CharField(disabled=True) + self.assertWidgetRendersTo(f, '') + # IntegerField ################################################################ def test_integerfield_1(self): @@ -1076,6 +1080,12 @@ class FieldsTests(SimpleTestCase): form = ChoiceFieldForm() self.assertEqual([('P', 'Paul')], list(form.fields['choicefield'].choices)) + def test_choicefield_disabled(self): + f = ChoiceField(choices=[('J', 'John'), ('P', 'Paul')], disabled=True) + self.assertWidgetRendersTo(f, + '') + # TypedChoiceField ############################################################ # TypedChoiceField is just like ChoiceField, except that coerced types will # be returned: diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index 07de8283346..acd1b6840ad 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -501,6 +501,37 @@ class FormsTestCase(SimpleTestCase): """) + def test_form_with_disabled_fields(self): + class PersonForm(Form): + name = CharField() + birthday = DateField(disabled=True) + + class PersonFormFieldInitial(Form): + name = CharField() + birthday = DateField(disabled=True, initial=datetime.date(1974, 8, 16)) + + # Disabled fields are generally not transmitted by user agents. + # The value from the form's initial data is used. + f1 = PersonForm({'name': 'John Doe'}, initial={'birthday': datetime.date(1974, 8, 16)}) + f2 = PersonFormFieldInitial({'name': 'John Doe'}) + for form in (f1, f2): + self.assertTrue(form.is_valid()) + self.assertEqual( + form.cleaned_data, + {'birthday': datetime.date(1974, 8, 16), 'name': 'John Doe'} + ) + + # Values provided in the form's data are ignored. + data = {'name': 'John Doe', 'birthday': '1984-11-10'} + f1 = PersonForm(data, initial={'birthday': datetime.date(1974, 8, 16)}) + f2 = PersonFormFieldInitial(data) + for form in (f1, f2): + self.assertTrue(form.is_valid()) + self.assertEqual( + form.cleaned_data, + {'birthday': datetime.date(1974, 8, 16), 'name': 'John Doe'} + ) + def test_hidden_data(self): class SongForm(Form): name = CharField()