Modified [7112] to make things behave more in line with Python subclassing 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
This commit is contained in:
parent
37962ecea7
commit
1159791cd5
|
@ -22,16 +22,30 @@ def pretty_name(name):
|
||||||
name = name[0].upper() + name[1:]
|
name = name[0].upper() + name[1:]
|
||||||
return name.replace('_', ' ')
|
return name.replace('_', ' ')
|
||||||
|
|
||||||
def get_declared_fields(bases, attrs):
|
def get_declared_fields(bases, attrs, with_base_fields=True):
|
||||||
|
"""
|
||||||
|
Create a list of form field instances from the passed in 'attrs', plus any
|
||||||
|
similar fields on the base classes (in 'bases'). This is used by both the
|
||||||
|
Form and ModelForm metclasses.
|
||||||
|
|
||||||
|
If 'with_base_fields' is True, all fields from the bases are used.
|
||||||
|
Otherwise, only fields in the 'declared_fields' attribute on the bases are
|
||||||
|
used. The distinction is useful in ModelForm subclassing.
|
||||||
|
"""
|
||||||
fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
|
fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
|
||||||
fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
|
fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
|
||||||
|
|
||||||
# If this class is subclassing another Form, add that Form's fields.
|
# If this class is subclassing another Form, add that Form's fields.
|
||||||
# Note that we loop over the bases in *reverse*. This is necessary in
|
# Note that we loop over the bases in *reverse*. This is necessary in
|
||||||
# order to preserve the correct order of fields.
|
# order to preserve the correct order of fields.
|
||||||
for base in bases[::-1]:
|
if with_base_fields:
|
||||||
if hasattr(base, 'base_fields'):
|
for base in bases[::-1]:
|
||||||
fields = base.base_fields.items() + fields
|
if hasattr(base, 'base_fields'):
|
||||||
|
fields = base.base_fields.items() + fields
|
||||||
|
else:
|
||||||
|
for base in bases[::-1]:
|
||||||
|
if hasattr(base, 'declared_fields'):
|
||||||
|
fields = base.declared_fields.items() + fields
|
||||||
|
|
||||||
return SortedDict(fields)
|
return SortedDict(fields)
|
||||||
|
|
||||||
|
|
|
@ -225,7 +225,7 @@ class ModelFormMetaclass(type):
|
||||||
attrs)
|
attrs)
|
||||||
|
|
||||||
new_class = type.__new__(cls, name, bases, attrs)
|
new_class = type.__new__(cls, name, bases, attrs)
|
||||||
declared_fields = get_declared_fields(bases, attrs)
|
declared_fields = get_declared_fields(bases, attrs, False)
|
||||||
opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
|
opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
|
||||||
if opts.model:
|
if opts.model:
|
||||||
# If a model is defined, extract form fields from it.
|
# If a model is defined, extract form fields from it.
|
||||||
|
@ -236,27 +236,8 @@ class ModelFormMetaclass(type):
|
||||||
fields.update(declared_fields)
|
fields.update(declared_fields)
|
||||||
else:
|
else:
|
||||||
fields = declared_fields
|
fields = declared_fields
|
||||||
|
new_class.declared_fields = declared_fields
|
||||||
new_class.base_fields = fields
|
new_class.base_fields = fields
|
||||||
|
|
||||||
# XXX: The following is a sanity check for the user to avoid
|
|
||||||
# inadvertent attribute hiding.
|
|
||||||
|
|
||||||
# Search base classes, but don't allow more than one Meta model
|
|
||||||
# definition. The fields would be generated correctly, but the save
|
|
||||||
# method won't deal with more than one object. Also, it wouldn't be
|
|
||||||
# clear what to do with multiple fields and exclude lists.
|
|
||||||
first = None
|
|
||||||
current = opts.model
|
|
||||||
for base in parents:
|
|
||||||
base_opts = getattr(base, '_meta', None)
|
|
||||||
base_model = getattr(base_opts, 'model', None)
|
|
||||||
if base_model:
|
|
||||||
if current:
|
|
||||||
if base_model is not current:
|
|
||||||
raise ImproperlyConfigured("%s's base classes define more than one model." % name)
|
|
||||||
else:
|
|
||||||
current = base_model
|
|
||||||
|
|
||||||
return new_class
|
return new_class
|
||||||
|
|
||||||
class BaseModelForm(BaseForm):
|
class BaseModelForm(BaseForm):
|
||||||
|
|
|
@ -335,14 +335,19 @@ models. For example, using the previous ``ArticleForm`` class::
|
||||||
This creates a form that behaves identically to ``ArticleForm``, except there
|
This creates a form that behaves identically to ``ArticleForm``, except there
|
||||||
is some extra validation and cleaning for the ``pub_date`` field.
|
is some extra validation and cleaning for the ``pub_date`` field.
|
||||||
|
|
||||||
|
You can also subclass the parent's ``Meta`` inner class if you want to change
|
||||||
|
the ``Meta.fields`` or ``Meta.excludes`` lists::
|
||||||
|
|
||||||
|
>>> class RestrictedArticleForm(EnhancedArticleForm):
|
||||||
|
... class Meta(ArticleForm.Meta):
|
||||||
|
... exclude = ['body']
|
||||||
|
|
||||||
|
This adds in the extra method from the ``EnhancedArticleForm`` and modifies
|
||||||
|
the original ``ArticleForm.Meta`` to remove one field.
|
||||||
|
|
||||||
There are a couple of things to note, however. Most of these won't normally be
|
There are a couple of things to note, however. Most of these won't normally be
|
||||||
of concern unless you are trying to do something tricky with subclassing.
|
of concern unless you are trying to do something tricky with subclassing.
|
||||||
|
|
||||||
* All the fields from the parent classes will appear in the child
|
|
||||||
``ModelForm``. This means you cannot change a parent's ``Meta.exclude``
|
|
||||||
attribute, for example, and except it to have an effect, since the field is
|
|
||||||
already part of the field list in the parent class.
|
|
||||||
|
|
||||||
* Normal Python name resolution rules apply. If you have multiple base
|
* Normal Python name resolution rules apply. If you have multiple base
|
||||||
classes that declare a ``Meta`` inner class, only the first one will be
|
classes that declare a ``Meta`` inner class, only the first one will be
|
||||||
used. This means the child's ``Meta``, if it exists, otherwise the
|
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.
|
||||||
* For technical reasons, you cannot have a subclass that is inherited from
|
* For technical reasons, you cannot have a subclass that is inherited from
|
||||||
both a ``ModelForm`` and a ``Form`` simultaneously.
|
both a ``ModelForm`` and a ``Form`` simultaneously.
|
||||||
|
|
||||||
Because of the "child inherits all fields from parents" behaviour, you
|
|
||||||
shouldn't try to declare model fields in multiple classes (parent and child).
|
|
||||||
Instead, declare all the model-related stuff in one class and use inheritance
|
|
||||||
to add "extra" non-model fields and methods to the final result. Whether you
|
|
||||||
put the "extra" functions in the parent class or the child class will depend
|
|
||||||
on how you intend to reuse them.
|
|
||||||
|
|
||||||
|
|
|
@ -155,29 +155,25 @@ familiar with the mechanics.
|
||||||
... class Meta:
|
... class Meta:
|
||||||
... model = Category
|
... model = Category
|
||||||
|
|
||||||
>>> class BadForm(CategoryForm):
|
>>> class OddForm(CategoryForm):
|
||||||
... class Meta:
|
... class Meta:
|
||||||
... model = Article
|
... model = Article
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
OddForm is now an Article-related thing, because BadForm.Meta overrides
|
||||||
ImproperlyConfigured: BadForm's base classes define more than one model.
|
CategoryForm.Meta.
|
||||||
|
>>> OddForm.base_fields.keys()
|
||||||
|
['headline', 'slug', 'pub_date', 'writer', 'article', 'status', 'categories']
|
||||||
|
|
||||||
>>> class ArticleForm(ModelForm):
|
>>> class ArticleForm(ModelForm):
|
||||||
... class Meta:
|
... class Meta:
|
||||||
... model = Article
|
... model = Article
|
||||||
|
|
||||||
|
First class with a Meta class wins.
|
||||||
|
|
||||||
>>> class BadForm(ArticleForm, CategoryForm):
|
>>> class BadForm(ArticleForm, CategoryForm):
|
||||||
... pass
|
... pass
|
||||||
Traceback (most recent call last):
|
>>> OddForm.base_fields.keys()
|
||||||
...
|
['headline', 'slug', 'pub_date', 'writer', 'article', 'status', 'categories']
|
||||||
ImproperlyConfigured: BadForm's base classes define more than one model.
|
|
||||||
|
|
||||||
This one is OK since the subclass specifies the same model as the parent.
|
|
||||||
|
|
||||||
>>> class SubCategoryForm(CategoryForm):
|
|
||||||
... class Meta:
|
|
||||||
... model = Category
|
|
||||||
|
|
||||||
|
|
||||||
Subclassing without specifying a Meta on the class will use the parent's Meta
|
Subclassing without specifying a Meta on the class will use the parent's Meta
|
||||||
(or the first parent in the MRO if there are multiple parent classes).
|
(or the first parent in the MRO if there are multiple parent classes).
|
||||||
|
@ -185,13 +181,26 @@ Subclassing without specifying a Meta on the class will use the parent's Meta
|
||||||
>>> class CategoryForm(ModelForm):
|
>>> class CategoryForm(ModelForm):
|
||||||
... class Meta:
|
... class Meta:
|
||||||
... model = Category
|
... model = Category
|
||||||
... exclude = ['url']
|
|
||||||
>>> class SubCategoryForm(CategoryForm):
|
>>> class SubCategoryForm(CategoryForm):
|
||||||
... pass
|
... pass
|
||||||
|
>>> SubCategoryForm.base_fields.keys()
|
||||||
|
['name', 'slug', 'url']
|
||||||
|
|
||||||
|
We can also subclass the Meta inner class to change the fields list.
|
||||||
|
|
||||||
|
>>> class CategoryForm(ModelForm):
|
||||||
|
... checkbox = forms.BooleanField()
|
||||||
|
...
|
||||||
|
... class Meta:
|
||||||
|
... model = Category
|
||||||
|
>>> class SubCategoryForm(CategoryForm):
|
||||||
|
... class Meta(CategoryForm.Meta):
|
||||||
|
... exclude = ['url']
|
||||||
|
|
||||||
>>> print SubCategoryForm()
|
>>> print SubCategoryForm()
|
||||||
<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" maxlength="20" /></td></tr>
|
<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" maxlength="20" /></td></tr>
|
||||||
<tr><th><label for="id_slug">Slug:</label></th><td><input id="id_slug" type="text" name="slug" maxlength="20" /></td></tr>
|
<tr><th><label for="id_slug">Slug:</label></th><td><input id="id_slug" type="text" name="slug" maxlength="20" /></td></tr>
|
||||||
|
<tr><th><label for="id_checkbox">Checkbox:</label></th><td><input type="checkbox" name="checkbox" id="id_checkbox" /></td></tr>
|
||||||
|
|
||||||
# Old form_for_x tests #######################################################
|
# Old form_for_x tests #######################################################
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue