Kaydet (Commit) 37962ece authored tarafından Malcolm Tredinnick's avatar Malcolm Tredinnick

Fixed #6337. Refs #3632 -- Fixed ModelForms subclassing, to the extent that it can be made to work.

This ended up being an almost complete rewrite of ModelForms.__new__, but
should be backwards compatible (although the text of one error message has
changed, which is only user visible and only if you pass in invalid code).

Documentation updated, also.

This started out as a patch from semenov (many thanks!), but by the time all
the problems were hammered out, little of the original was left. Still, it was
a good starting point.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@7112 bcc190cf-cafb-0310-a4f2-bffc1f526a37
üst 5078010a
...@@ -22,23 +22,26 @@ def pretty_name(name): ...@@ -22,23 +22,26 @@ 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):
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))
# 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
# order to preserve the correct order of fields.
for base in bases[::-1]:
if hasattr(base, 'base_fields'):
fields = base.base_fields.items() + fields
return SortedDict(fields)
class DeclarativeFieldsMetaclass(type): class DeclarativeFieldsMetaclass(type):
""" """
Metaclass that converts Field attributes to a dictionary called Metaclass that converts Field attributes to a dictionary called
'base_fields', taking into account parent class 'base_fields' as well. 'base_fields', taking into account parent class 'base_fields' as well.
""" """
def __new__(cls, name, bases, attrs): def __new__(cls, name, bases, attrs):
fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] attrs['base_fields'] = get_declared_fields(bases, attrs)
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.
# Note that we loop over the bases in *reverse*. This is necessary in
# order to preserve the correct order of fields.
for base in bases[::-1]:
if hasattr(base, 'base_fields'):
fields = base.base_fields.items() + fields
attrs['base_fields'] = SortedDict(fields)
return type.__new__(cls, name, bases, attrs) return type.__new__(cls, name, bases, attrs)
class BaseForm(StrAndUnicode): class BaseForm(StrAndUnicode):
......
...@@ -11,7 +11,7 @@ from django.utils.datastructures import SortedDict ...@@ -11,7 +11,7 @@ from django.utils.datastructures import SortedDict
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from util import ValidationError, ErrorList from util import ValidationError, ErrorList
from forms import BaseForm from forms import BaseForm, get_declared_fields
from fields import Field, ChoiceField, EMPTY_VALUES from fields import Field, ChoiceField, EMPTY_VALUES
from widgets import Select, SelectMultiple, MultipleHiddenInput from widgets import Select, SelectMultiple, MultipleHiddenInput
...@@ -211,57 +211,58 @@ class ModelFormOptions(object): ...@@ -211,57 +211,58 @@ class ModelFormOptions(object):
self.fields = getattr(options, 'fields', None) self.fields = getattr(options, 'fields', None)
self.exclude = getattr(options, 'exclude', None) self.exclude = getattr(options, 'exclude', None)
class ModelFormMetaclass(type): class ModelFormMetaclass(type):
def __new__(cls, name, bases, attrs, def __new__(cls, name, bases, attrs,
formfield_callback=lambda f: f.formfield()): formfield_callback=lambda f: f.formfield()):
fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] try:
fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) parents = [b for b in bases if issubclass(b, ModelForm)]
except NameError:
# If this class is subclassing another Form, add that Form's fields. # We are defining ModelForm itself.
# Note that we loop over the bases in *reverse*. This is necessary in parents = None
# order to preserve the correct order of fields. if not parents:
for base in bases[::-1]: return super(ModelFormMetaclass, cls).__new__(cls, name, bases,
if hasattr(base, 'base_fields'): attrs)
fields = base.base_fields.items() + fields
declared_fields = SortedDict(fields) new_class = type.__new__(cls, name, bases, attrs)
declared_fields = get_declared_fields(bases, attrs)
opts = ModelFormOptions(attrs.get('Meta', None)) opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
attrs['_meta'] = opts if opts.model:
# If a model is defined, extract form fields from it.
# Don't allow more than one Meta model definition in bases. The fields fields = fields_for_model(opts.model, opts.fields,
# would be generated correctly, but the save method won't deal with opts.exclude, formfield_callback)
# more than one object. # Fields defined on the base classes override local fields and are
base_models = [] # always included.
for base in bases: fields.update(declared_fields)
else:
fields = declared_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_opts = getattr(base, '_meta', None)
base_model = getattr(base_opts, 'model', None) base_model = getattr(base_opts, 'model', None)
if base_model is not None: if base_model:
base_models.append(base_model) if current:
if len(base_models) > 1: if base_model is not current:
raise ImproperlyConfigured("%s's base classes define more than one model." % name) raise ImproperlyConfigured("%s's base classes define more than one model." % name)
else:
# If a model is defined, extract form fields from it and add them to base_fields current = base_model
if attrs['_meta'].model is not None:
# Don't allow a subclass to define a different Meta model than a return new_class
# parent class has. Technically the right fields would be generated,
# but the save method will not deal with more than one model.
for base in bases:
base_opts = getattr(base, '_meta', None)
base_model = getattr(base_opts, 'model', None)
if base_model and base_model is not opts.model:
raise ImproperlyConfigured('%s defines a different model than its parent.' % name)
model_fields = fields_for_model(opts.model, opts.fields,
opts.exclude, formfield_callback)
# fields declared in base classes override fields from the model
model_fields.update(declared_fields)
attrs['base_fields'] = model_fields
else:
attrs['base_fields'] = declared_fields
return type.__new__(cls, name, bases, attrs)
class BaseModelForm(BaseForm): class BaseModelForm(BaseForm):
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=':', instance=None): initial=None, error_class=ErrorList, label_suffix=':',
instance=None):
opts = self._meta opts = self._meta
if instance is None: if instance is None:
# if we didn't get an instance, instantiate a new one # if we didn't get an instance, instantiate a new one
...@@ -277,7 +278,8 @@ class BaseModelForm(BaseForm): ...@@ -277,7 +278,8 @@ class BaseModelForm(BaseForm):
def save(self, commit=True): def save(self, commit=True):
""" """
Saves this ``form``'s cleaned_data into model instance ``self.instance``. Saves this ``form``'s cleaned_data into model instance
``self.instance``.
If commit=True, then the changes to ``instance`` will be saved to the If commit=True, then the changes to ``instance`` will be saved to the
database. Returns ``instance``. database. Returns ``instance``.
......
...@@ -320,3 +320,41 @@ parameter when declaring the form field:: ...@@ -320,3 +320,41 @@ parameter when declaring the form field::
... ...
... class Meta: ... class Meta:
... model = Article ... model = Article
Form inheritance
----------------
As with the basic forms, you can extend and reuse ``ModelForms`` by inheriting
them. Normally, this will be useful if you need to declare some extra fields
or extra methods on a parent class for use in a number of forms derived from
models. For example, using the previous ``ArticleForm`` class::
>>> class EnhancedArticleForm(ArticleForm):
... def clean_pub_date(self):
... ...
This creates a form that behaves identically to ``ArticleForm``, except there
is some extra validation and cleaning for the ``pub_date`` field.
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.
* 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
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
``Meta`` of the first parent, etc.
* For technical reasons, you cannot have a subclass that is inherited from
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.
...@@ -64,11 +64,11 @@ class TextFile(models.Model): ...@@ -64,11 +64,11 @@ class TextFile(models.Model):
def __unicode__(self): def __unicode__(self):
return self.description return self.description
class ImageFile(models.Model): class ImageFile(models.Model):
description = models.CharField(max_length=20) description = models.CharField(max_length=20)
image = models.FileField(upload_to=tempfile.gettempdir()) image = models.FileField(upload_to=tempfile.gettempdir())
def __unicode__(self): def __unicode__(self):
return self.description return self.description
...@@ -160,7 +160,7 @@ familiar with the mechanics. ...@@ -160,7 +160,7 @@ familiar with the mechanics.
... model = Article ... model = Article
Traceback (most recent call last): Traceback (most recent call last):
... ...
ImproperlyConfigured: BadForm defines a different model than its parent. ImproperlyConfigured: BadForm's base classes define more than one model.
>>> class ArticleForm(ModelForm): >>> class ArticleForm(ModelForm):
... class Meta: ... class Meta:
...@@ -179,6 +179,20 @@ This one is OK since the subclass specifies the same model as the parent. ...@@ -179,6 +179,20 @@ This one is OK since the subclass specifies the same model as the parent.
... model = Category ... model = Category
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).
>>> class CategoryForm(ModelForm):
... class Meta:
... model = Category
... exclude = ['url']
>>> class SubCategoryForm(CategoryForm):
... pass
>>> 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_slug">Slug:</label></th><td><input id="id_slug" type="text" name="slug" maxlength="20" /></td></tr>
# Old form_for_x tests ####################################################### # Old form_for_x tests #######################################################
>>> from django.newforms import ModelForm, CharField >>> from django.newforms import ModelForm, CharField
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment