From 28986da4ca167ae257abcaf7caea230eca2bcd80 Mon Sep 17 00:00:00 2001 From: Thomas Tanner Date: Mon, 16 Mar 2015 01:33:59 +0100 Subject: [PATCH] Fixed #5986 -- Added ability to customize order of Form fields --- django/contrib/auth/forms.py | 9 ++---- django/forms/forms.py | 27 ++++++++++++++++- docs/ref/forms/api.txt | 25 ++++++++++++++++ docs/releases/1.9.txt | 4 +++ tests/forms_tests/tests/test_forms.py | 43 +++++++++++++++++++++++++++ 5 files changed, 100 insertions(+), 8 deletions(-) diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index cc3be5ba0b..928c4c7988 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -from collections import OrderedDict - from django import forms from django.contrib.auth import authenticate, get_user_model from django.contrib.auth.hashers import ( @@ -303,6 +301,8 @@ class PasswordChangeForm(SetPasswordForm): old_password = forms.CharField(label=_("Old password"), widget=forms.PasswordInput) + field_order = ['old_password', 'new_password1', 'new_password2'] + def clean_old_password(self): """ Validates that the old_password field is correct. @@ -315,11 +315,6 @@ class PasswordChangeForm(SetPasswordForm): ) return old_password -PasswordChangeForm.base_fields = OrderedDict( - (k, PasswordChangeForm.base_fields[k]) - for k in ['old_password', 'new_password1', 'new_password2'] -) - class AdminPasswordChangeForm(forms.Form): """ diff --git a/django/forms/forms.py b/django/forms/forms.py index 761dd93afa..1845494c34 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -73,9 +73,11 @@ class BaseForm(object): # class is different than Form. See the comments by the Form class for more # information. Any improvements to the form API should be made to *this* # class, not to the Form class. + field_order = None + def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=ErrorList, label_suffix=None, - empty_permitted=False): + empty_permitted=False, field_order=None): self.is_bound = data is not None or files is not None self.data = data or {} self.files = files or {} @@ -96,6 +98,29 @@ class BaseForm(object): # self.base_fields. self.fields = copy.deepcopy(self.base_fields) self._bound_fields_cache = {} + self.order_fields(self.field_order if field_order is None else field_order) + + def order_fields(self, field_order): + """ + Rearranges the fields according to field_order. + + field_order is a list of field names specifying the order. Fields not + included in the list are appended in the default order for backward + compatibility with subclasses not overriding field_order. If field_order + is None, all fields are kept in the order defined in the class. + Unknown fields in field_order are ignored to allow disabling fields in + form subclasses without redefining ordering. + """ + if field_order is None: + return + fields = OrderedDict() + for key in field_order: + try: + fields[key] = self.fields.pop(key) + except KeyError: # ignore unknown fields + pass + fields.update(self.fields) # add remaining fields in original order + self.fields = fields def __str__(self): return self.as_table() diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index ec50f02d42..3bc39cdc0f 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -700,6 +700,31 @@ example, in the ``ContactForm`` example, the fields are defined in the order ``subject``, ``message``, ``sender``, ``cc_myself``. To reorder the HTML output, just change the order in which those fields are listed in the class. +There are several other ways to customize the order: + +.. attribute:: Form.field_order + +.. versionadded:: 1.9 + +By default ``Form.field_order=None``, which retains the order in which you +define the fields in your form class. If ``field_order`` is a list of field +names, the fields are ordered as specified by the list and remaining fields are +appended according to the default order. Unknown field names in the list are +ignored. This makes it possible to disable a field in a subclass by setting it +to ``None`` without having to redefine ordering. + +You can also use the ``Form.field_order`` argument to a :class:`Form` to +override the field order. If a :class:`~django.forms.Form` defines +:attr:`~Form.field_order` *and* you include ``field_order`` when instantiating +the ``Form``, then the latter ``field_order`` will have precedence. + +.. method:: Form.order_fields(field_order) + +.. versionadded:: 1.9 + +You may rearrange the fields any time using ``order_fields()`` with a list of +field names as in :attr:`~django.forms.Form.field_order`. + How errors are displayed ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index a6544a6030..08d79cb59a 100644 --- a/docs/releases/1.9.txt +++ b/docs/releases/1.9.txt @@ -119,6 +119,10 @@ Forms ``field_classes`` to customize the type of the fields. See :ref:`modelforms-overriding-default-fields` for details. +* You can now specify the order in which form fields are rendered with the + :attr:`~django.forms.Form.field_order` attribute, the ``field_order`` + constructor argument , or the :meth:`~django.forms.Form.order_fields` method. + Generic Views ^^^^^^^^^^^^^ diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index 589a9cfc84..ef447a94e1 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -1046,6 +1046,49 @@ class FormsTestCase(TestCase): Field13: Field14:""") + def test_explicit_field_order(self): + class TestFormParent(Form): + field1 = CharField() + field2 = CharField() + field4 = CharField() + field5 = CharField() + field6 = CharField() + field_order = ['field6', 'field5', 'field4', 'field2', 'field1'] + + class TestForm(TestFormParent): + field3 = CharField() + field_order = ['field2', 'field4', 'field3', 'field5', 'field6'] + + class TestFormRemove(TestForm): + field1 = None + + class TestFormMissing(TestForm): + field_order = ['field2', 'field4', 'field3', 'field5', 'field6', 'field1'] + field1 = None + + class TestFormInit(TestFormParent): + field3 = CharField() + field_order = None + + def __init__(self, **kwargs): + super(TestFormInit, self).__init__(**kwargs) + self.order_fields(field_order=TestForm.field_order) + + p = TestFormParent() + self.assertEqual(list(p.fields.keys()), TestFormParent.field_order) + p = TestFormRemove() + self.assertEqual(list(p.fields.keys()), TestForm.field_order) + p = TestFormMissing() + self.assertEqual(list(p.fields.keys()), TestForm.field_order) + p = TestForm() + self.assertEqual(list(p.fields.keys()), TestFormMissing.field_order) + p = TestFormInit() + order = list(TestForm.field_order) + ['field1'] + self.assertEqual(list(p.fields.keys()), order) + TestForm.field_order = ['unknown'] + p = TestForm() + self.assertEqual(list(p.fields.keys()), ['field1', 'field2', 'field4', 'field5', 'field6', 'field3']) + def test_form_html_attributes(self): # Some Field classes have an effect on the HTML attributes of their associated # Widget. If you set max_length in a CharField and its associated widget is