Django

Code

Changeset 7112

Show
Ignore:
Timestamp:
02/14/08 06:56:49 (1 year ago)
Author:
mtredinnick
Message:

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.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/trunk/django/newforms/forms.py

    r7021 r7112  
    2222    name = name[0].upper() + name[1:] 
    2323    return name.replace('_', ' ') 
     24 
     25def get_declared_fields(bases, attrs): 
     26    fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] 
     27    fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) 
     28 
     29    # If this class is subclassing another Form, add that Form's fields. 
     30    # Note that we loop over the bases in *reverse*. This is necessary in 
     31    # 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 
     35 
     36    return SortedDict(fields) 
    2437 
    2538class DeclarativeFieldsMetaclass(type): 
     
    2942    """ 
    3043    def __new__(cls, name, bases, attrs): 
    31         fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] 
    32         fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) 
    33  
    34         # If this class is subclassing another Form, add that Form's fields. 
    35         # Note that we loop over the bases in *reverse*. This is necessary in 
    36         # order to preserve the correct order of fields. 
    37         for base in bases[::-1]: 
    38             if hasattr(base, 'base_fields'): 
    39                 fields = base.base_fields.items() + fields 
    40  
    41         attrs['base_fields'] = SortedDict(fields) 
     44        attrs['base_fields'] = get_declared_fields(bases, attrs) 
    4245        return type.__new__(cls, name, bases, attrs) 
    4346 
  • django/trunk/django/newforms/models.py

    r7067 r7112  
    1212 
    1313from util import ValidationError, ErrorList 
    14 from forms import BaseForm 
     14from forms import BaseForm, get_declared_fields 
    1515from fields import Field, ChoiceField, EMPTY_VALUES 
    1616from widgets import Select, SelectMultiple, MultipleHiddenInput 
     
    212212        self.exclude = getattr(options, 'exclude', None) 
    213213 
     214 
    214215class ModelFormMetaclass(type): 
    215216    def __new__(cls, name, bases, attrs, 
    216217                formfield_callback=lambda f: f.formfield()): 
    217         fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] 
    218         fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) 
    219  
    220         # If this class is subclassing another Form, add that Form's fields. 
    221         # Note that we loop over the bases in *reverse*. This is necessary in 
    222         # order to preserve the correct order of fields. 
    223         for base in bases[::-1]: 
    224             if hasattr(base, 'base_fields'): 
    225                 fields = base.base_fields.items() + fields 
    226         declared_fields = SortedDict(fields) 
    227  
    228         opts = ModelFormOptions(attrs.get('Meta', None)) 
    229         attrs['_meta'] = opts 
    230  
    231         # Don't allow more than one Meta model definition in bases. The fields 
    232         # would be generated correctly, but the save method won't deal with 
    233         # more than one object. 
    234         base_models = [] 
    235         for base in bases: 
     218        try: 
     219            parents = [b for b in bases if issubclass(b, ModelForm)] 
     220        except NameError: 
     221            # We are defining ModelForm itself. 
     222            parents = None 
     223        if not parents: 
     224            return super(ModelFormMetaclass, cls).__new__(cls, name, bases, 
     225                    attrs) 
     226 
     227        new_class = type.__new__(cls, name, bases, attrs) 
     228        declared_fields = get_declared_fields(bases, attrs) 
     229        opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None)) 
     230        if opts.model: 
     231            # If a model is defined, extract form fields from it. 
     232            fields = fields_for_model(opts.model, opts.fields, 
     233                                      opts.exclude, formfield_callback) 
     234            # Fields defined on the base classes override local fields and are 
     235            # always included. 
     236            fields.update(declared_fields) 
     237        else: 
     238            fields = declared_fields 
     239        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: 
    236251            base_opts = getattr(base, '_meta', None) 
    237252            base_model = getattr(base_opts, 'model', None) 
    238             if base_model is not None: 
    239                 base_models.append(base_model) 
    240         if len(base_models) > 1: 
    241             raise ImproperlyConfigured("%s's base classes define more than one model." % name) 
    242  
    243         # If a model is defined, extract form fields from it and add them to base_fields 
    244         if attrs['_meta'].model is not None: 
    245             # Don't allow a subclass to define a different Meta model than a 
    246             # parent class has. Technically the right fields would be generated, 
    247             # but the save method will not deal with more than one model. 
    248             for base in bases: 
    249                 base_opts = getattr(base, '_meta', None) 
    250                 base_model = getattr(base_opts, 'model', None) 
    251                 if base_model and base_model is not opts.model: 
    252                     raise ImproperlyConfigured('%s defines a different model than its parent.' % name) 
    253             model_fields = fields_for_model(opts.model, opts.fields, 
    254                     opts.exclude, formfield_callback) 
    255             # fields declared in base classes override fields from the model 
    256             model_fields.update(declared_fields) 
    257             attrs['base_fields'] = model_fields 
    258         else: 
    259             attrs['base_fields'] = declared_fields 
    260         return type.__new__(cls, name, bases, attrs) 
     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        return new_class 
    261261 
    262262class BaseModelForm(BaseForm): 
    263263    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 
    264                  initial=None, error_class=ErrorList, label_suffix=':', instance=None): 
     264                 initial=None, error_class=ErrorList, label_suffix=':', 
     265                 instance=None): 
    265266        opts = self._meta 
    266267        if instance is None: 
     
    278279    def save(self, commit=True): 
    279280        """ 
    280         Saves this ``form``'s cleaned_data into model instance ``self.instance``. 
     281        Saves this ``form``'s cleaned_data into model instance 
     282        ``self.instance``. 
    281283 
    282284        If commit=True, then the changes to ``instance`` will be saved to the 
  • django/trunk/docs/modelforms.txt

    r7036 r7112  
    321321   ...     class Meta: 
    322322   ...         model = Article 
     323 
     324Form inheritance 
     325---------------- 
     326As with the basic forms, you can extend and reuse ``ModelForms`` by inheriting 
     327them. Normally, this will be useful if you need to declare some extra fields 
     328or extra methods on a parent class for use in a number of forms derived from 
     329models. For example, using the previous ``ArticleForm`` class:: 
     330 
     331    >>> class EnhancedArticleForm(ArticleForm): 
     332    ...     def clean_pub_date(self): 
     333    ...         ... 
     334 
     335This creates a form that behaves identically to ``ArticleForm``, except there 
     336is some extra validation and cleaning for the ``pub_date`` field. 
     337 
     338There are a couple of things to note, however. Most of these won't normally be 
     339of concern unless you are trying to do something tricky with subclassing. 
     340 
     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 * Normal Python name resolution rules apply. If you have multiple base 
     347   classes that declare a ``Meta`` inner class, only the first one will be 
     348   used.  This means the child's ``Meta``, if it exists, otherwise the 
     349   ``Meta`` of the first parent, etc. 
     350 
     351 * For technical reasons, you cannot have a subclass that is inherited from 
     352   both a ``ModelForm`` and a ``Form`` simultaneously. 
     353 
     354Because of the "child inherits all fields from parents" behaviour, you 
     355shouldn't try to declare model fields in multiple classes (parent and child). 
     356Instead, declare all the model-related stuff in one class and use inheritance 
     357to add "extra" non-model fields and methods to the final result. Whether you 
     358put the "extra" functions in the parent class or the child class will depend 
     359on how you intend to reuse them. 
     360 
  • django/trunk/tests/modeltests/model_forms/models.py

    r7025 r7112  
    6565    def __unicode__(self): 
    6666        return self.description 
    67          
     67 
    6868class ImageFile(models.Model): 
    6969    description = models.CharField(max_length=20) 
    7070    image = models.FileField(upload_to=tempfile.gettempdir()) 
    71      
     71 
    7272    def __unicode__(self): 
    7373        return self.description 
     
    161161Traceback (most recent call last): 
    162162... 
    163 ImproperlyConfigured: BadForm defines a different model than its parent
     163ImproperlyConfigured: BadForm's base classes define more than one model
    164164 
    165165>>> class ArticleForm(ModelForm): 
     
    179179...         model = Category 
    180180 
     181 
     182Subclassing 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). 
     184 
     185>>> class CategoryForm(ModelForm): 
     186...     class Meta: 
     187...         model = Category 
     188...         exclude = ['url'] 
     189>>> class SubCategoryForm(CategoryForm): 
     190...     pass 
     191 
     192>>> print SubCategoryForm() 
     193<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" maxlength="20" /></td></tr> 
     194<tr><th><label for="id_slug">Slug:</label></th><td><input id="id_slug" type="text" name="slug" maxlength="20" /></td></tr> 
    181195 
    182196# Old form_for_x tests #######################################################