Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Modified [7112] to make things behave more in line with Python subcla…

…ssing when subclassing ModelForms.

Meta can now be subclassed and changes on the child model affect the fields
list. Also removed a block of error checking, since it's harder to mess up in
unexpected ways now (e.g. you can't change the model and get the entirely wrong
fields list), so it was a level of overkill.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@7115 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 1159791cd5b706f01e282d6773a17b66b7cdac9f 1 parent 37962ec
Malcolm Tredinnick authored February 14, 2008
22  django/newforms/forms.py
@@ -22,16 +22,30 @@ def pretty_name(name):
22 22
     name = name[0].upper() + name[1:]
23 23
     return name.replace('_', ' ')
24 24
 
25  
-def get_declared_fields(bases, attrs):
  25
+def get_declared_fields(bases, attrs, with_base_fields=True):
  26
+    """
  27
+    Create a list of form field instances from the passed in 'attrs', plus any
  28
+    similar fields on the base classes (in 'bases'). This is used by both the
  29
+    Form and ModelForm metclasses.
  30
+
  31
+    If 'with_base_fields' is True, all fields from the bases are used.
  32
+    Otherwise, only fields in the 'declared_fields' attribute on the bases are
  33
+    used. The distinction is useful in ModelForm subclassing.
  34
+    """
26 35
     fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
27 36
     fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
28 37
 
29 38
     # If this class is subclassing another Form, add that Form's fields.
30 39
     # Note that we loop over the bases in *reverse*. This is necessary in
31 40
     # order to preserve the correct order of fields.
32  
-    for base in bases[::-1]:
33  
-        if hasattr(base, 'base_fields'):
34  
-            fields = base.base_fields.items() + fields
  41
+    if with_base_fields:
  42
+        for base in bases[::-1]:
  43
+            if hasattr(base, 'base_fields'):
  44
+                fields = base.base_fields.items() + fields
  45
+    else:
  46
+        for base in bases[::-1]:
  47
+            if hasattr(base, 'declared_fields'):
  48
+                fields = base.declared_fields.items() + fields
35 49
 
36 50
     return SortedDict(fields)
37 51
 
23  django/newforms/models.py
@@ -225,7 +225,7 @@ def __new__(cls, name, bases, attrs,
225 225
                     attrs)
226 226
 
227 227
         new_class = type.__new__(cls, name, bases, attrs)
228  
-        declared_fields = get_declared_fields(bases, attrs)
  228
+        declared_fields = get_declared_fields(bases, attrs, False)
229 229
         opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
230 230
         if opts.model:
231 231
             # If a model is defined, extract form fields from it.
@@ -236,27 +236,8 @@ def __new__(cls, name, bases, attrs,
236 236
             fields.update(declared_fields)
237 237
         else:
238 238
             fields = declared_fields
  239
+        new_class.declared_fields = declared_fields
239 240
         new_class.base_fields = fields
240  
-
241  
-        # XXX: The following is a sanity check for the user to avoid
242  
-        # inadvertent attribute hiding.
243  
-
244  
-        # Search base classes, but don't allow more than one Meta model
245  
-        # definition. The fields would be generated correctly, but the save
246  
-        # method won't deal with more than one object. Also, it wouldn't be
247  
-        # clear what to do with multiple fields and exclude lists.
248  
-        first = None
249  
-        current = opts.model
250  
-        for base in parents:
251  
-            base_opts = getattr(base, '_meta', None)
252  
-            base_model = getattr(base_opts, 'model', None)
253  
-            if base_model:
254  
-                if current:
255  
-                    if base_model is not current:
256  
-                        raise ImproperlyConfigured("%s's base classes define more than one model." % name)
257  
-                else:
258  
-                    current = base_model
259  
-
260 241
         return new_class
261 242
 
262 243
 class BaseModelForm(BaseForm):
22  docs/modelforms.txt
@@ -335,14 +335,19 @@ models. For example, using the previous ``ArticleForm`` class::
335 335
 This creates a form that behaves identically to ``ArticleForm``, except there
336 336
 is some extra validation and cleaning for the ``pub_date`` field.
337 337
 
  338
+You can also subclass the parent's ``Meta`` inner class if you want to change
  339
+the ``Meta.fields`` or ``Meta.excludes`` lists::
  340
+
  341
+    >>> class RestrictedArticleForm(EnhancedArticleForm):
  342
+    ...     class Meta(ArticleForm.Meta):
  343
+    ...         exclude = ['body']
  344
+
  345
+This adds in the extra method from the ``EnhancedArticleForm`` and modifies
  346
+the original ``ArticleForm.Meta`` to remove one field.
  347
+
338 348
 There are a couple of things to note, however. Most of these won't normally be
339 349
 of concern unless you are trying to do something tricky with subclassing.
340 350
 
341  
- * All the fields from the parent classes will appear in the child
342  
-   ``ModelForm``. This means you cannot change a parent's ``Meta.exclude``
343  
-   attribute, for example, and except it to have an effect, since the field is
344  
-   already part of the field list in the parent class.
345  
-
346 351
  * Normal Python name resolution rules apply. If you have multiple base
347 352
    classes that declare a ``Meta`` inner class, only the first one will be
348 353
    used.  This means the child's ``Meta``, if it exists, otherwise the
@@ -351,10 +356,3 @@ of concern unless you are trying to do something tricky with subclassing.
351 356
  * For technical reasons, you cannot have a subclass that is inherited from
352 357
    both a ``ModelForm`` and a ``Form`` simultaneously.
353 358
 
354  
-Because of the "child inherits all fields from parents" behaviour, you
355  
-shouldn't try to declare model fields in multiple classes (parent and child).
356  
-Instead, declare all the model-related stuff in one class and use inheritance
357  
-to add "extra" non-model fields and methods to the final result. Whether you
358  
-put the "extra" functions in the parent class or the child class will depend
359  
-on how you intend to reuse them.
360  
-
37  tests/modeltests/model_forms/models.py
@@ -155,43 +155,52 @@ def __unicode__(self):
155 155
 ...     class Meta:
156 156
 ...         model = Category
157 157
 
158  
->>> class BadForm(CategoryForm):
  158
+>>> class OddForm(CategoryForm):
159 159
 ...     class Meta:
160 160
 ...         model = Article
161  
-Traceback (most recent call last):
162  
-...
163  
-ImproperlyConfigured: BadForm's base classes define more than one model.
  161
+
  162
+OddForm is now an Article-related thing, because BadForm.Meta overrides
  163
+CategoryForm.Meta.
  164
+>>> OddForm.base_fields.keys()
  165
+['headline', 'slug', 'pub_date', 'writer', 'article', 'status', 'categories']
164 166
 
165 167
 >>> class ArticleForm(ModelForm):
166 168
 ...     class Meta:
167 169
 ...         model = Article
168 170
 
  171
+First class with a Meta class wins.
  172
+
169 173
 >>> class BadForm(ArticleForm, CategoryForm):
170 174
 ...     pass
171  
-Traceback (most recent call last):
172  
-...
173  
-ImproperlyConfigured: BadForm's base classes define more than one model.
  175
+>>> OddForm.base_fields.keys()
  176
+['headline', 'slug', 'pub_date', 'writer', 'article', 'status', 'categories']
174 177
 
175  
-This one is OK since the subclass specifies the same model as the parent.
  178
+Subclassing without specifying a Meta on the class will use the parent's Meta
  179
+(or the first parent in the MRO if there are multiple parent classes).
176 180
 
177  
->>> class SubCategoryForm(CategoryForm):
  181
+>>> class CategoryForm(ModelForm):
178 182
 ...     class Meta:
179 183
 ...         model = Category
  184
+>>> class SubCategoryForm(CategoryForm):
  185
+...     pass
  186
+>>> SubCategoryForm.base_fields.keys()
  187
+['name', 'slug', 'url']
180 188
 
181  
-
182  
-Subclassing without specifying a Meta on the class will use the parent's Meta
183  
-(or the first parent in the MRO if there are multiple parent classes).
  189
+We can also subclass the Meta inner class to change the fields list.
184 190
 
185 191
 >>> class CategoryForm(ModelForm):
  192
+...     checkbox = forms.BooleanField()
  193
+...
186 194
 ...     class Meta:
187 195
 ...         model = Category
188  
-...         exclude = ['url']
189 196
 >>> class SubCategoryForm(CategoryForm):
190  
-...     pass
  197
+...     class Meta(CategoryForm.Meta):
  198
+...         exclude = ['url']
191 199
 
192 200
 >>> print SubCategoryForm()
193 201
 <tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" maxlength="20" /></td></tr>
194 202
 <tr><th><label for="id_slug">Slug:</label></th><td><input id="id_slug" type="text" name="slug" maxlength="20" /></td></tr>
  203
+<tr><th><label for="id_checkbox">Checkbox:</label></th><td><input type="checkbox" name="checkbox" id="id_checkbox" /></td></tr>
195 204
 
196 205
 # Old form_for_x tests #######################################################
197 206
 

0 notes on commit 1159791

Please sign in to comment.
Something went wrong with that request. Please try again.