summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Tanner <tanner@gmx.net>2015-03-16 01:33:59 +0100
committerTim Graham <timograham@gmail.com>2015-03-16 09:12:57 -0400
commit28986da4ca167ae257abcaf7caea230eca2bcd80 (patch)
tree7db7c27812e72b860c088d98a7b36d2b1f016023
parent39573a11db694a584de6ca2a858fa05ec4ea13e1 (diff)
Fixed #5986 -- Added ability to customize order of Form fields
-rw-r--r--django/contrib/auth/forms.py9
-rw-r--r--django/forms/forms.py27
-rw-r--r--docs/ref/forms/api.txt25
-rw-r--r--docs/releases/1.9.txt4
-rw-r--r--tests/forms_tests/tests/test_forms.py43
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):
<tr><th>Field13:</th><td><input type="text" name="field13" /></td></tr>
<tr><th>Field14:</th><td><input type="text" name="field14" /></td></tr>""")
+ 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