Code

Ticket #6630: 01-move-fields-and-exclude.diff

File 01-move-fields-and-exclude.diff, 12.8 KB (added by Petr Marhoun <petr.marhoun@…>, 4 years ago)
Line 
1diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
2--- a/django/contrib/auth/forms.py
3+++ b/django/contrib/auth/forms.py
4@@ -20,7 +20,7 @@
5 
6     class Meta:
7         model = User
8-        fields = ("username",)
9+        fields = ("username", "password1", "password2")
10 
11     def clean_username(self):
12         username = self.cleaned_data["username"]
13diff --git a/django/forms/forms.py b/django/forms/forms.py
14--- a/django/forms/forms.py
15+++ b/django/forms/forms.py
16@@ -23,43 +23,71 @@
17         return u''
18     return name.replace('_', ' ').capitalize()
19 
20-def get_declared_fields(bases, attrs, with_base_fields=True):
21+def get_declared_fields(attrs):
22     """
23-    Create a list of form field instances from the passed in 'attrs', plus any
24-    similar fields on the base classes (in 'bases'). This is used by both the
25-    Form and ModelForm metclasses.
26-
27-    If 'with_base_fields' is True, all fields from the bases are used.
28-    Otherwise, only fields in the 'declared_fields' attribute on the bases are
29-    used. The distinction is useful in ModelForm subclassing.
30-    Also integrates any additional media definitions
31+    Create a list of form field instances from the passed in 'attrs'. This is
32+    used by both the Form and ModelForm metaclasses.
33     """
34-    fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
35+    fields = [(field_name, attrs.pop(field_name)) for field_name, obj
36+        in attrs.items() if isinstance(obj, Field)]
37     fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
38-
39-    # If this class is subclassing another Form, add that Form's fields.
40-    # Note that we loop over the bases in *reverse*. This is necessary in
41-    # order to preserve the correct order of fields.
42-    if with_base_fields:
43-        for base in bases[::-1]:
44-            if hasattr(base, 'base_fields'):
45-                fields = base.base_fields.items() + fields
46-    else:
47-        for base in bases[::-1]:
48-            if hasattr(base, 'declared_fields'):
49-                fields = base.declared_fields.items() + fields
50-
51     return SortedDict(fields)
52 
53-class DeclarativeFieldsMetaclass(type):
54+def get_base_fields(new_class):
55+    """
56+    Create a list of form field instances declared on the form and its base
57+    classes. This is used by both the Form and ModelForm metaclasses.
58+
59+    """
60+    # Note that we loop over the bases in *reverse*. This is necessary in order
61+    # to preserve the correct order of fields.
62+    fields = SortedDict()
63+    for base in reversed(new_class.mro()):
64+        if hasattr(base, 'declared_fields'):
65+            fields.update(base.declared_fields)
66+    return fields
67+
68+def select_fields(fields, opts):
69+    """
70+    Select some fields based on options.  This is used by both the Form and
71+    ModelForm metaclasses.
72+
73+    Option ``fields`` is an optional list of field names. If provided, only
74+    the named fields will be included in the returned fields and fields are
75+    sorted by it.
76+
77+    Option ``exclude`` is an optional list of field names. If provided,
78+    the named fields will be excluded from the returned fields, even if they
79+    are listed in the ``fields`` argument.
80+    """
81+    selected_fields = SortedDict()
82+    for field_name, field in fields.items():
83+        if opts.fields and not field_name in opts.fields:
84+            continue
85+        if opts.exclude and field_name in opts.exclude:
86+            continue
87+        selected_fields[field_name] = field
88+    if opts.fields:
89+        selected_fields = SortedDict((field_name, selected_fields.get(field_name))
90+            for field_name in opts.fields if field_name in selected_fields)
91+    return selected_fields
92+
93+class FormOptions(object):
94+    def __init__(self, options=None):
95+        self.fields = getattr(options, 'fields', None)
96+        self.exclude = getattr(options, 'exclude', None)
97+
98+class FormMetaclass(type):
99     """
100     Metaclass that converts Field attributes to a dictionary called
101-    'base_fields', taking into account parent class 'base_fields' as well.
102+    'base_fields', taking into account parent class fields as well.
103+    Also integrates any additional media definitions.
104     """
105     def __new__(cls, name, bases, attrs):
106-        attrs['base_fields'] = get_declared_fields(bases, attrs)
107-        new_class = super(DeclarativeFieldsMetaclass,
108-                     cls).__new__(cls, name, bases, attrs)
109+        attrs['declared_fields'] = get_declared_fields(attrs)
110+        new_class = super(FormMetaclass, cls).__new__(cls, name, bases, attrs)
111+        opts = new_class._meta = FormOptions(getattr(new_class, 'Meta', None))
112+        new_class.base_fields = select_fields(get_base_fields(new_class), opts)
113         if 'media' not in attrs:
114             new_class.media = media_property(new_class)
115         return new_class
116@@ -384,7 +412,7 @@
117     # fancy metaclass stuff purely for the semantic sugar -- it allows one
118     # to define a form using declarative syntax.
119     # BaseForm itself has no way of designating self.fields.
120-    __metaclass__ = DeclarativeFieldsMetaclass
121+    __metaclass__ = FormMetaclass
122 
123 class BoundField(StrAndUnicode):
124     "A Field plus data"
125diff --git a/django/forms/models.py b/django/forms/models.py
126--- a/django/forms/models.py
127+++ b/django/forms/models.py
128@@ -12,7 +12,7 @@
129 from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
130 from django.core.validators import EMPTY_VALUES
131 from util import ErrorList
132-from forms import BaseForm, get_declared_fields
133+from forms import BaseForm, get_declared_fields, get_base_fields, select_fields
134 from fields import Field, ChoiceField
135 from widgets import SelectMultiple, HiddenInput, MultipleHiddenInput
136 from widgets import media_property
137@@ -195,36 +195,25 @@
138         self.exclude = getattr(options, 'exclude', None)
139         self.widgets = getattr(options, 'widgets', None)
140 
141-
142 class ModelFormMetaclass(type):
143     def __new__(cls, name, bases, attrs):
144         formfield_callback = attrs.pop('formfield_callback',
145                 lambda f, **kwargs: f.formfield(**kwargs))
146-        try:
147-            parents = [b for b in bases if issubclass(b, ModelForm)]
148-        except NameError:
149-            # We are defining ModelForm itself.
150-            parents = None
151-        declared_fields = get_declared_fields(bases, attrs, False)
152-        new_class = super(ModelFormMetaclass, cls).__new__(cls, name, bases,
153-                attrs)
154-        if not parents:
155-            return new_class
156-
157-        if 'media' not in attrs:
158-            new_class.media = media_property(new_class)
159+        attrs['declared_fields'] = get_declared_fields(attrs)
160+        new_class = super(ModelFormMetaclass, cls).__new__(cls, name, bases, attrs)
161         opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
162         if opts.model:
163             # If a model is defined, extract form fields from it.
164-            fields = fields_for_model(opts.model, opts.fields,
165-                                      opts.exclude, opts.widgets, formfield_callback)
166+            fields = fields_for_model(opts.model, widgets=opts.widgets,
167+                formfield_callback=formfield_callback)
168             # Override default model fields with any custom declared ones
169             # (plus, include all the other declared fields).
170-            fields.update(declared_fields)
171+            fields.update(get_base_fields(new_class))
172         else:
173-            fields = declared_fields
174-        new_class.declared_fields = declared_fields
175-        new_class.base_fields = fields
176+            fields = get_base_fields(new_class)
177+        new_class.base_fields = select_fields(fields, opts)
178+        if 'media' not in attrs:
179+            new_class.media = media_property(new_class)
180         return new_class
181 
182 class BaseModelForm(BaseForm):
183diff --git a/tests/regressiontests/forms/forms.py b/tests/regressiontests/forms/forms.py
184--- a/tests/regressiontests/forms/forms.py
185+++ b/tests/regressiontests/forms/forms.py
186@@ -928,6 +928,44 @@
187 <tr><th>Field13:</th><td><input type="text" name="field13" /></td></tr>
188 <tr><th>Field14:</th><td><input type="text" name="field14" /></td></tr>
189 
190+It is possible to select some fields and reorder them.
191+>>> class OrderedTestForm(TestForm):
192+...    class Meta:
193+...        fields = ('field4', 'field2', 'field11', 'field8')
194+>>> p = OrderedTestForm(auto_id=False)
195+>>> print p
196+<tr><th>Field4:</th><td><input type="text" name="field4" /></td></tr>
197+<tr><th>Field2:</th><td><input type="text" name="field2" /></td></tr>
198+<tr><th>Field11:</th><td><input type="text" name="field11" /></td></tr>
199+<tr><th>Field8:</th><td><input type="text" name="field8" /></td></tr>
200+
201+Or to exlude some fields.
202+>>> class ExcludingTestForm(TestForm):
203+...    class Meta:
204+...        exclude = ('field14', 'field3', 'field5', 'field8', 'field1')
205+>>> p = ExcludingTestForm(auto_id=False)
206+>>> print p
207+<tr><th>Field2:</th><td><input type="text" name="field2" /></td></tr>
208+<tr><th>Field4:</th><td><input type="text" name="field4" /></td></tr>
209+<tr><th>Field6:</th><td><input type="text" name="field6" /></td></tr>
210+<tr><th>Field7:</th><td><input type="text" name="field7" /></td></tr>
211+<tr><th>Field9:</th><td><input type="text" name="field9" /></td></tr>
212+<tr><th>Field10:</th><td><input type="text" name="field10" /></td></tr>
213+<tr><th>Field11:</th><td><input type="text" name="field11" /></td></tr>
214+<tr><th>Field12:</th><td><input type="text" name="field12" /></td></tr>
215+<tr><th>Field13:</th><td><input type="text" name="field13" /></td></tr>
216+
217+Or to use fields and exlude at once.
218+>>> class CombinedTestForm(TestForm):
219+...    class Meta:
220+...        fields = ('field4', 'field2', 'field11', 'field8')
221+...        exclude = ('field14', 'field3', 'field5', 'field8', 'field1')
222+>>> p = CombinedTestForm(auto_id=False)
223+>>> print p
224+<tr><th>Field4:</th><td><input type="text" name="field4" /></td></tr>
225+<tr><th>Field2:</th><td><input type="text" name="field2" /></td></tr>
226+<tr><th>Field11:</th><td><input type="text" name="field11" /></td></tr>
227+
228 Some Field classes have an effect on the HTML attributes of their associated
229 Widget. If you set max_length in a CharField and its associated widget is
230 either a TextInput or PasswordInput, then the widget's rendered HTML will
231@@ -1314,15 +1352,15 @@
232 <li>Birthday: <input type="text" name="birthday" /></li>
233 <li>Instrument: <input type="text" name="instrument" /></li>
234 
235-Yes, you can subclass multiple forms. The fields are added in the order in
236-which the parent classes are listed.
237+Yes, you can subclass multiple forms. The fields are added in the order given
238+by the method resolution order.
239 >>> class Person(Form):
240 ...     first_name = CharField()
241 ...     last_name = CharField()
242 ...     birthday = DateField()
243 >>> class Instrument(Form):
244 ...     instrument = CharField()
245->>> class Beatle(Person, Instrument):
246+>>> class Beatle(Instrument, Person):
247 ...     haircut_type = CharField()
248 >>> b = Beatle(auto_id=False)
249 >>> print b.as_ul()
250@@ -1332,6 +1370,39 @@
251 <li>Instrument: <input type="text" name="instrument" /></li>
252 <li>Haircut type: <input type="text" name="haircut_type" /></li>
253 
254+You can use also more complex inheritence.
255+>>> class FormA(Form):
256+...    field_a = CharField()
257+>>> class FormB(Form):
258+...    field_b = CharField()
259+>>> class FormC(Form):
260+...    field_c = CharField()
261+>>> class FormD(FormB, FormC):
262+...    field_d = CharField()
263+>>> class FormE(FormC, FormA):
264+...    field_e = CharField()
265+>>> class FormF(FormD, FormE):
266+...    field_f = CharField()
267+>>> print FormF(auto_id=False).as_ul()
268+<li>Field a: <input type="text" name="field_a" /></li>
269+<li>Field c: <input type="text" name="field_c" /></li>
270+<li>Field e: <input type="text" name="field_e" /></li>
271+<li>Field b: <input type="text" name="field_b" /></li>
272+<li>Field d: <input type="text" name="field_d" /></li>
273+<li>Field f: <input type="text" name="field_f" /></li>
274+
275+But you can reorder fields by meta attributes.
276+>>> class FormG(FormF):
277+...    field_g = CharField()
278+...    class Meta:
279+...        fields = ('field_c', 'field_e', 'field_a', 'field_b', 'field_g')
280+...        exclude = ('field_b', 'field_d')
281+>>> print FormG(auto_id=False).as_ul()
282+<li>Field c: <input type="text" name="field_c" /></li>
283+<li>Field e: <input type="text" name="field_e" /></li>
284+<li>Field a: <input type="text" name="field_a" /></li>
285+<li>Field g: <input type="text" name="field_g" /></li>
286+
287 # Forms with prefixes #########################################################
288 
289 Sometimes it's necessary to have multiple forms display on the same HTML page,
290diff --git a/tests/regressiontests/forms/models.py b/tests/regressiontests/forms/models.py
291--- a/tests/regressiontests/forms/models.py
292+++ b/tests/regressiontests/forms/models.py
293@@ -145,7 +145,9 @@
294 >>> f.is_valid()
295 True
296 >>> f.cleaned_data['name']
297-u'Hello'
298+Traceback (most recent call last):
299+...
300+KeyError: 'name'
301 >>> obj = f.save()
302 >>> obj.name
303 u'class default value'