Code

Ticket #6630: 02-add-fieldsets.diff

File 02-add-fieldsets.diff, 12.2 KB (added by Petr Marhoun <petr.marhoun@…>, 4 years ago)
Line 
1diff --git a/django/forms/forms.py b/django/forms/forms.py
2--- a/django/forms/forms.py
3+++ b/django/forms/forms.py
4@@ -59,6 +59,20 @@
5     Option ``exclude`` is an optional list of field names. If provided,
6     the named fields will be excluded from the returned fields, even if they
7     are listed in the ``fields`` argument.
8+
9+    Option ``fieldsets`` is a list of two-tuples. The first item in each
10+    two-tuple is a name for the fieldset, and the second is a dictionary
11+    of fieldset options. Valid fieldset options in the dictionary include:
12+
13+    ``fields`` (required): A tuple of field names to display in the fieldset.
14+
15+    ``attrs``: A dictionary of HTML attributes to be used with the fieldset.
16+
17+    ``legend``: A string that should be the content of a ``legend`` tag
18+    to open the fieldset.
19+
20+    ``description``: A string of optional extra text.
21+
22     """
23     selected_fields = SortedDict()
24     for field_name, field in fields.items():
25@@ -66,6 +80,14 @@
26             continue
27         if opts.exclude and field_name in opts.exclude:
28             continue
29+        if opts.fieldsets:
30+            for fieldset_name, fieldset_options in opts.fieldsets:
31+                if field_name in fieldset_options['fields']:
32+                    # Stop iteration of fieldsets so else condition is not used.
33+                    break
34+            else:
35+                # Field is not used in any fieldset.
36+                continue
37         selected_fields[field_name] = field
38     if opts.fields:
39         selected_fields = SortedDict((field_name, selected_fields.get(field_name))
40@@ -76,6 +98,7 @@
41     def __init__(self, options=None):
42         self.fields = getattr(options, 'fields', None)
43         self.exclude = getattr(options, 'exclude', None)
44+        self.fieldsets = getattr(options, 'fieldsets', None)
45 
46 class FormMetaclass(type):
47     """
48@@ -134,6 +157,23 @@
49             raise KeyError('Key %r not found in Form' % name)
50         return BoundField(self, field, name)
51 
52+    def _has_fieldsets(self):
53+        """
54+        Returns True if form has fieldsets. Depends on _meta attribute giving
55+        options which have to define fieldsets. If you do not use FormMetaclass
56+        and want to use this method, you have to define options otherwise.
57+        """
58+        return self._meta.fieldsets is not None
59+    has_fieldsets = property(_has_fieldsets)
60+
61+    def fieldsets(self):
62+        """
63+        Returns collection of FieldSets. Depends on _meta attribute giving
64+        options which have to define fieldsets. If you do not use FormMetaclass
65+        and want to use this method, you have to define options otherwise.
66+        """
67+        return FieldSetCollection(self, self._meta.fieldsets)
68+
69     def _get_errors(self):
70         "Returns an ErrorDict for the data provided for the form"
71         if self._errors is None:
72@@ -414,6 +454,58 @@
73     # BaseForm itself has no way of designating self.fields.
74     __metaclass__ = FormMetaclass
75 
76+class FieldSetCollection(object):
77+    "A collection of FieldSets."
78+    def __init__(self, form, fieldsets):
79+        self.form = form
80+        self.fieldsets = fieldsets
81+
82+    def __iter__(self):
83+        if self.form.has_fieldsets:
84+            for name, options in self.fieldsets:
85+                yield self._construct_fieldset(name, options)
86+        else:
87+            # Construct dummy fieldset for form without any.
88+            yield FieldSet(self.form, None, self.form.fields)
89+
90+    def __getitem__(self, key):
91+        if self.form.has_fieldsets:
92+            for name, options in self.fieldsets:
93+                if name == key:
94+                    return self._construct_fieldset(name, options)
95+        raise KeyError('FieldSet with key %r not found' % key)
96+
97+    def _construct_fieldset(self, name, options):
98+        fields = SortedDict((field_name, self.form.fields[field_name])
99+            for field_name in options['fields'] if field_name in self.form.fields)
100+        return FieldSet(self.form, name, fields, options.get('attrs'),
101+            options.get('legend'), options.get('description'))
102+
103+class FieldSet(object):
104+    "Iterable FieldSet as a collection of BoundFields."
105+    def __init__(self, form, name, fields, attrs=None, legend=None, description=None):
106+        self.form = form
107+        self.name = name
108+        self.fields = fields
109+        self.attrs = attrs
110+        self.legend = legend
111+        self.description = description
112+
113+    def __iter__(self):
114+        for name, field in self.fields.items():
115+            yield BoundField(self.form, field, name)
116+
117+    def __getitem__(self, name):
118+        try:
119+            field = self.fields[name]
120+        except KeyError:
121+            raise KeyError('Key %r not found in FieldSet' % name)
122+        return BoundField(self.form, field, name)
123+
124+    def _errors(self):
125+        return ErrorDict((k, v) for (k, v) in self.form.errors.items() if k in self.fields)
126+    errors = property(_errors)
127+
128 class BoundField(StrAndUnicode):
129     "A Field plus data"
130     def __init__(self, form, field, name):
131diff --git a/django/forms/models.py b/django/forms/models.py
132--- a/django/forms/models.py
133+++ b/django/forms/models.py
134@@ -193,6 +193,7 @@
135         self.model = getattr(options, 'model', None)
136         self.fields = getattr(options, 'fields', None)
137         self.exclude = getattr(options, 'exclude', None)
138+        self.fieldsets = getattr(options, 'fieldsets', None)
139         self.widgets = getattr(options, 'widgets', None)
140 
141 class ModelFormMetaclass(type):
142diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py
143--- a/tests/modeltests/model_forms/models.py
144+++ b/tests/modeltests/model_forms/models.py
145@@ -322,6 +322,49 @@
146 >>> CategoryForm.base_fields.keys()
147 ['name']
148 
149+
150+Using 'fieldsets'
151+
152+>>> class CategoryForm(ModelForm):
153+...
154+...     class Meta:
155+...         model = Category
156+...         fieldsets = (
157+...             ('main', {'fields': ('slug',)}),
158+...             ('other', {'fields': ('name',)}),
159+...         )
160+
161+>>> CategoryForm.base_fields.keys()
162+['name', 'slug']
163+
164+>>> for fieldset in CategoryForm().fieldsets():
165+...     print fieldset.name, fieldset.fields.keys()
166+main ['slug']
167+other ['name']
168+
169+
170+Using 'fields' *and* 'exclude' *and* 'fieldsets'
171+
172+>>> class CategoryForm(ModelForm):
173+...
174+...     class Meta:
175+...         model = Category
176+...         fields = ['name', 'url']
177+...         exclude = ['url']
178+...         fieldsets = (
179+...             ('main', {'fields': ('slug',)}),
180+...             ('other', {'fields': ('name',)}),
181+...         )
182+
183+>>> CategoryForm.base_fields.keys()
184+['name']
185+
186+>>> for fieldset in CategoryForm().fieldsets():
187+...     print fieldset.name, fieldset.fields.keys()
188+main []
189+other ['name']
190+
191+
192 Using 'widgets'
193 
194 >>> class CategoryForm(ModelForm):
195diff --git a/tests/regressiontests/forms/forms.py b/tests/regressiontests/forms/forms.py
196--- a/tests/regressiontests/forms/forms.py
197+++ b/tests/regressiontests/forms/forms.py
198@@ -966,6 +966,142 @@
199 <tr><th>Field2:</th><td><input type="text" name="field2" /></td></tr>
200 <tr><th>Field11:</th><td><input type="text" name="field11" /></td></tr>
201 
202+Or to specify fieldsets.
203+>>> class FieldsetsTestForm(TestForm):
204+...    class Meta:
205+...        fieldsets = (
206+...            ('main', {'fields': ('field3', 'field8', 'field2')}),
207+...            ('other', {'fields': ('field5', 'field11', 'field14')}),
208+...        )
209+>>> p = FieldsetsTestForm(auto_id=False)
210+>>> print p
211+<tr><th>Field2:</th><td><input type="text" name="field2" /></td></tr>
212+<tr><th>Field3:</th><td><input type="text" name="field3" /></td></tr>
213+<tr><th>Field5:</th><td><input type="text" name="field5" /></td></tr>
214+<tr><th>Field8:</th><td><input type="text" name="field8" /></td></tr>
215+<tr><th>Field11:</th><td><input type="text" name="field11" /></td></tr>
216+<tr><th>Field14:</th><td><input type="text" name="field14" /></td></tr>
217+>>> print p.has_fieldsets
218+True
219+>>> for fieldset in p.fieldsets():
220+...     print fieldset.name
221+...     for field in fieldset:
222+...         print field
223+main
224+<input type="text" name="field3" />
225+<input type="text" name="field8" />
226+<input type="text" name="field2" />
227+other
228+<input type="text" name="field5" />
229+<input type="text" name="field11" />
230+<input type="text" name="field14" />
231+
232+Fieldsets and fields in fieldsets can be identified by names.
233+>>> print p.fieldsets()['main'].name
234+main
235+>>> print p.fieldsets()['other']['field5']
236+<input type="text" name="field5" />
237+
238+Each fieldset has attribute errors for current fields.
239+>>> for fieldset in p.fieldsets():
240+...      fieldset.errors.as_ul()
241+u''
242+u''
243+>>> p = FieldsetsTestForm({'field3': '3', 'field2': '2', 'field5': '5', 'field11': '11'})
244+>>> for fieldset in p.fieldsets():
245+...      fieldset.errors.as_ul()
246+u'<ul class="errorlist"><li>field8<ul class="errorlist"><li>This field is required.</li></ul></li></ul>'
247+u'<ul class="errorlist"><li>field14<ul class="errorlist"><li>This field is required.</li></ul></li></ul>'
248+
249+Or to use fields and exlude at once.
250+>>> class CombinedTestForm(TestForm):
251+...    class Meta:
252+...        fields = ('field4', 'field2', 'field11', 'field8')
253+...        exclude = ('field14', 'field3', 'field5', 'field8', 'field1')
254+...        fieldsets = (
255+...            ('main', {'fields': ('field3', 'field8', 'field2')}),
256+...            ('other', {'fields': ('field5', 'field11', 'field14')}),
257+...        )
258+>>> p = CombinedTestForm(auto_id=False)
259+>>> print p
260+<tr><th>Field2:</th><td><input type="text" name="field2" /></td></tr>
261+<tr><th>Field11:</th><td><input type="text" name="field11" /></td></tr>
262+>>> print p.has_fieldsets
263+True
264+>>> for fieldset in p.fieldsets():
265+...     print fieldset.name
266+...     for field in fieldset:
267+...         print field
268+main
269+<input type="text" name="field2" />
270+other
271+<input type="text" name="field11" />
272+
273+You can use fieldsets in your general templates also if you do not define any.
274+>>> class OrderedTestForm(TestForm):
275+...    class Meta:
276+...        fields = ('field4', 'field2', 'field11', 'field8')
277+>>> p = OrderedTestForm(auto_id=False)
278+>>> print p.has_fieldsets
279+False
280+>>> for fieldset in p.fieldsets():
281+...     print fieldset.name
282+...     for field in fieldset:
283+...         print field
284+None
285+<input type="text" name="field4" />
286+<input type="text" name="field2" />
287+<input type="text" name="field11" />
288+<input type="text" name="field8" />
289+
290+Fieldsets can have some additional options - attrs, legend and description.
291+You can use them in your templates.
292+>>> class FieldsetsTestForm(TestForm):
293+...    class Meta:
294+...        fieldsets = (
295+...            ('main', {
296+...                'fields': ('field3', 'field8', 'field2'),
297+...                'legend': 'Main fields',
298+...                'description': 'You should really fill these fields.',
299+...            }),
300+...            ('other', {
301+...                'fields': ('field5', 'field11', 'field14'),
302+...                'legend': 'Other fields',
303+...                'attrs': {'class': 'other'},
304+...            }),
305+...        )
306+>>> p = FieldsetsTestForm(auto_id=False)
307+>>> print p
308+<tr><th>Field2:</th><td><input type="text" name="field2" /></td></tr>
309+<tr><th>Field3:</th><td><input type="text" name="field3" /></td></tr>
310+<tr><th>Field5:</th><td><input type="text" name="field5" /></td></tr>
311+<tr><th>Field8:</th><td><input type="text" name="field8" /></td></tr>
312+<tr><th>Field11:</th><td><input type="text" name="field11" /></td></tr>
313+<tr><th>Field14:</th><td><input type="text" name="field14" /></td></tr>
314+>>> print p.has_fieldsets
315+True
316+>>> for fieldset in p.fieldsets():
317+...     print 'name:', fieldset.name
318+...     print 'attrs:', fieldset.attrs
319+...     print 'description:', fieldset.description
320+...     print 'legend:', fieldset.legend
321+...     for field in fieldset:
322+...         print field 
323+name: main
324+attrs: None
325+description: You should really fill these fields.
326+legend: Main fields
327+<input type="text" name="field3" />
328+<input type="text" name="field8" />
329+<input type="text" name="field2" />
330+name: other
331+attrs: {'class': 'other'}
332+description: None
333+legend: Other fields
334+<input type="text" name="field5" />
335+<input type="text" name="field11" />
336+<input type="text" name="field14" />
337+
338 Some Field classes have an effect on the HTML attributes of their associated
339 Widget. If you set max_length in a CharField and its associated widget is
340 either a TextInput or PasswordInput, then the widget's rendered HTML will