Skip to content

Commit

Permalink
Fixed #5986 -- Added ability to customize order of Form fields
Browse files Browse the repository at this point in the history
  • Loading branch information
ttanner authored and timgraham committed Mar 16, 2015
1 parent 39573a1 commit 28986da
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 8 deletions.
9 changes: 2 additions & 7 deletions 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 (
Expand Down Expand Up @@ -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.
Expand All @@ -315,11 +315,6 @@ def clean_old_password(self):
)
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):
"""
Expand Down
27 changes: 26 additions & 1 deletion django/forms/forms.py
Expand Up @@ -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 {}
Expand All @@ -96,6 +98,29 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
# 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):

This comment has been minimized.

Copy link
@apollo13

apollo13 Mar 16, 2015

Member

@timgraham, @ttanner What about a ordered_fields property instead? Then you'd have

self.fields = self.ordered_fields

or similar. Just came to my mind, since we don't really have a pattern like that in Django anywhere, the admin usually has getter methods for everything, so just from a naming perspective I'd at least name it get_ordered_fields and return the fields…

This comment has been minimized.

Copy link
@timgraham

timgraham Mar 16, 2015

Member

Seems like a good suggestion to me.

This comment has been minimized.

Copy link
@ttanner

ttanner Mar 16, 2015

Author Contributor

replacing it with a simple getter method would make it impossible to change the order later on, e.g., in a constructor as in the test line 1075 or #3652 (comment)

This comment has been minimized.

Copy link
@apollo13

apollo13 Mar 16, 2015

Member

True that, although we could still do something like self.fields = self.get_ordered_fields(field_order=None). That said I don't care too much -- it is just that the API strikes me as somewhat odd.

"""
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()
Expand Down
25 changes: 25 additions & 0 deletions docs/ref/forms/api.txt
Expand Up @@ -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
~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
4 changes: 4 additions & 0 deletions docs/releases/1.9.txt
Expand Up @@ -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
^^^^^^^^^^^^^

Expand Down
43 changes: 43 additions & 0 deletions tests/forms_tests/tests/test_forms.py
Expand Up @@ -1046,6 +1046,49 @@ class TestForm(Form):
<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
Expand Down

0 comments on commit 28986da

Please sign in to comment.