Skip to content

Commit 28986da

Browse files
ttannertimgraham
authored andcommitted
Fixed django#5986 -- Added ability to customize order of Form fields
1 parent 39573a1 commit 28986da

File tree

5 files changed

+100
-8
lines changed

5 files changed

+100
-8
lines changed

django/contrib/auth/forms.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
from __future__ import unicode_literals
22

3-
from collections import OrderedDict
4-
53
from django import forms
64
from django.contrib.auth import authenticate, get_user_model
75
from django.contrib.auth.hashers import (
@@ -303,6 +301,8 @@ class PasswordChangeForm(SetPasswordForm):
303301
old_password = forms.CharField(label=_("Old password"),
304302
widget=forms.PasswordInput)
305303

304+
field_order = ['old_password', 'new_password1', 'new_password2']
305+
306306
def clean_old_password(self):
307307
"""
308308
Validates that the old_password field is correct.
@@ -315,11 +315,6 @@ def clean_old_password(self):
315315
)
316316
return old_password
317317

318-
PasswordChangeForm.base_fields = OrderedDict(
319-
(k, PasswordChangeForm.base_fields[k])
320-
for k in ['old_password', 'new_password1', 'new_password2']
321-
)
322-
323318

324319
class AdminPasswordChangeForm(forms.Form):
325320
"""

django/forms/forms.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,11 @@ class BaseForm(object):
7373
# class is different than Form. See the comments by the Form class for more
7474
# information. Any improvements to the form API should be made to *this*
7575
# class, not to the Form class.
76+
field_order = None
77+
7678
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
7779
initial=None, error_class=ErrorList, label_suffix=None,
78-
empty_permitted=False):
80+
empty_permitted=False, field_order=None):
7981
self.is_bound = data is not None or files is not None
8082
self.data = data or {}
8183
self.files = files or {}
@@ -96,6 +98,29 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
9698
# self.base_fields.
9799
self.fields = copy.deepcopy(self.base_fields)
98100
self._bound_fields_cache = {}
101+
self.order_fields(self.field_order if field_order is None else field_order)
102+
103+
def order_fields(self, field_order):
104+
"""
105+
Rearranges the fields according to field_order.
106+
107+
field_order is a list of field names specifying the order. Fields not
108+
included in the list are appended in the default order for backward
109+
compatibility with subclasses not overriding field_order. If field_order
110+
is None, all fields are kept in the order defined in the class.
111+
Unknown fields in field_order are ignored to allow disabling fields in
112+
form subclasses without redefining ordering.
113+
"""
114+
if field_order is None:
115+
return
116+
fields = OrderedDict()
117+
for key in field_order:
118+
try:
119+
fields[key] = self.fields.pop(key)
120+
except KeyError: # ignore unknown fields
121+
pass
122+
fields.update(self.fields) # add remaining fields in original order
123+
self.fields = fields
99124

100125
def __str__(self):
101126
return self.as_table()

docs/ref/forms/api.txt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,31 @@ example, in the ``ContactForm`` example, the fields are defined in the order
700700
``subject``, ``message``, ``sender``, ``cc_myself``. To reorder the HTML
701701
output, just change the order in which those fields are listed in the class.
702702

703+
There are several other ways to customize the order:
704+
705+
.. attribute:: Form.field_order
706+
707+
.. versionadded:: 1.9
708+
709+
By default ``Form.field_order=None``, which retains the order in which you
710+
define the fields in your form class. If ``field_order`` is a list of field
711+
names, the fields are ordered as specified by the list and remaining fields are
712+
appended according to the default order. Unknown field names in the list are
713+
ignored. This makes it possible to disable a field in a subclass by setting it
714+
to ``None`` without having to redefine ordering.
715+
716+
You can also use the ``Form.field_order`` argument to a :class:`Form` to
717+
override the field order. If a :class:`~django.forms.Form` defines
718+
:attr:`~Form.field_order` *and* you include ``field_order`` when instantiating
719+
the ``Form``, then the latter ``field_order`` will have precedence.
720+
721+
.. method:: Form.order_fields(field_order)
722+
723+
.. versionadded:: 1.9
724+
725+
You may rearrange the fields any time using ``order_fields()`` with a list of
726+
field names as in :attr:`~django.forms.Form.field_order`.
727+
703728
How errors are displayed
704729
~~~~~~~~~~~~~~~~~~~~~~~~
705730

docs/releases/1.9.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ Forms
119119
``field_classes`` to customize the type of the fields. See
120120
:ref:`modelforms-overriding-default-fields` for details.
121121

122+
* You can now specify the order in which form fields are rendered with the
123+
:attr:`~django.forms.Form.field_order` attribute, the ``field_order``
124+
constructor argument , or the :meth:`~django.forms.Form.order_fields` method.
125+
122126
Generic Views
123127
^^^^^^^^^^^^^
124128

tests/forms_tests/tests/test_forms.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,49 @@ class TestForm(Form):
10461046
<tr><th>Field13:</th><td><input type="text" name="field13" /></td></tr>
10471047
<tr><th>Field14:</th><td><input type="text" name="field14" /></td></tr>""")
10481048

1049+
def test_explicit_field_order(self):
1050+
class TestFormParent(Form):
1051+
field1 = CharField()
1052+
field2 = CharField()
1053+
field4 = CharField()
1054+
field5 = CharField()
1055+
field6 = CharField()
1056+
field_order = ['field6', 'field5', 'field4', 'field2', 'field1']
1057+
1058+
class TestForm(TestFormParent):
1059+
field3 = CharField()
1060+
field_order = ['field2', 'field4', 'field3', 'field5', 'field6']
1061+
1062+
class TestFormRemove(TestForm):
1063+
field1 = None
1064+
1065+
class TestFormMissing(TestForm):
1066+
field_order = ['field2', 'field4', 'field3', 'field5', 'field6', 'field1']
1067+
field1 = None
1068+
1069+
class TestFormInit(TestFormParent):
1070+
field3 = CharField()
1071+
field_order = None
1072+
1073+
def __init__(self, **kwargs):
1074+
super(TestFormInit, self).__init__(**kwargs)
1075+
self.order_fields(field_order=TestForm.field_order)
1076+
1077+
p = TestFormParent()
1078+
self.assertEqual(list(p.fields.keys()), TestFormParent.field_order)
1079+
p = TestFormRemove()
1080+
self.assertEqual(list(p.fields.keys()), TestForm.field_order)
1081+
p = TestFormMissing()
1082+
self.assertEqual(list(p.fields.keys()), TestForm.field_order)
1083+
p = TestForm()
1084+
self.assertEqual(list(p.fields.keys()), TestFormMissing.field_order)
1085+
p = TestFormInit()
1086+
order = list(TestForm.field_order) + ['field1']
1087+
self.assertEqual(list(p.fields.keys()), order)
1088+
TestForm.field_order = ['unknown']
1089+
p = TestForm()
1090+
self.assertEqual(list(p.fields.keys()), ['field1', 'field2', 'field4', 'field5', 'field6', 'field3'])
1091+
10491092
def test_form_html_attributes(self):
10501093
# Some Field classes have an effect on the HTML attributes of their associated
10511094
# Widget. If you set max_length in a CharField and its associated widget is

0 commit comments

Comments
 (0)