Ticket #6630: 00-fieldsets.diff

File 00-fieldsets.diff, 21.3 KB (added by Petr Marhoun <petr.marhoun@…>, 7 years ago)
  • django/contrib/admin/options.py

    === modified file 'django/contrib/admin/options.py'
     
    267267            exclude = []
    268268        else:
    269269            exclude = list(self.exclude)
     270        exclude += kwargs.get("exclude", [])
     271        if not exclude:
     272            exclude = None
    270273        defaults = {
    271274            "form": self.form,
    272275            "fields": fields,
    273             "exclude": exclude + kwargs.get("exclude", []),
     276            "exclude": exclude,
    274277            "formfield_callback": self.formfield_for_dbfield,
    275278        }
    276279        defaults.update(kwargs)
  • django/contrib/auth/forms.py

    === modified file 'django/contrib/auth/forms.py'
     
    1919
    2020    class Meta:
    2121        model = User
    22         fields = ("username",)
     22        fields = ("username", "password1", "password2")
    2323
    2424    def clean_username(self):
    2525        username = self.cleaned_data["username"]
  • django/forms/forms.py

    === modified file 'django/forms/forms.py'
     
    44
    55from copy import deepcopy
    66
     7from django.core.exceptions import ImproperlyConfigured
    78from django.utils.datastructures import SortedDict
    89from django.utils.html import escape
    910from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode
     
    2223    name = name[0].upper() + name[1:]
    2324    return name.replace('_', ' ')
    2425
    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.
     26class FormOptions(object):
     27    def __init__(self, options=None):
     28        self.fieldsets = getattr(options, 'fieldsets', None)
     29        self.fields = getattr(options, 'fields', None)
     30        self.exclude = getattr(options, 'exclude', None)
    3031
    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     Also integrates any additional media definitions
    35     """
    36     fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
     32def create_declared_fields(cls, attrs):
     33    """
     34    Create a list of form field instances from the passed in 'attrs'.
     35    This is used by both the Form and ModelForm metaclasses.
     36    """
     37    fields = []
     38    for name, possible_field in attrs.items():
     39        if isinstance(possible_field, Field):
     40            fields.append((name, possible_field))
     41            delattr(cls, name)
    3742    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
     43    cls.declared_fields = SortedDict(fields)
     44
     45def create_base_fields_pool_from_declared_fields(cls, attrs):
     46    """
     47    Create a list of form field instances which are declared in the form and
     48    its superclasses (from 'csl.__mro__'). This is used by the Form metaclass.
     49
     50    Note that we loop over the bases in *reverse*. This is necessary in
     51    order to preserve the correct order of fields.
     52    """
     53    fields = []
     54    for base in cls.__mro__[::-1]:
     55        try:
     56            fields += base.declared_fields.items() # Raise AttributeError if the base is not a form.
     57        except AttributeError:
     58            pass
     59    cls.base_fields_pool = SortedDict(fields)
     60
     61def create_base_fields_from_base_fields_pool(cls, attrs):
     62    """
     63    Create a list of form field instances from the base fields pool. Select
     64    only the fields which are defined in one of the options 'fieldsets',
     65    'fields' and 'exclude'. If no option is set, select all fields.
     66    This is used by both the Form and ModelForm metaclasses.
     67
     68    Also check that only one option is used.
     69    """
     70    if (cls._meta.fieldsets is None) + (cls._meta.fields is None) + (cls._meta.exclude is None) < 2:
     71        raise ImproperlyConfigured("%s cannot have more than one option from fieldsets, fields and exclude." % cls.__name__)
     72    if cls._meta.fieldsets:
     73        names = []
     74        for fieldset in cls._meta.fieldsets:
     75            names.extend(fieldset['fields'])
     76    elif cls._meta.fields:
     77        names = cls._meta.fields
     78    elif cls._meta.exclude:
     79        names = [name for name in cls.base_fields_pool if name not in cls._meta.exclude]
    4680    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):
     81        names = cls.base_fields_pool.keys()
     82    cls.base_fields = SortedDict([(name, cls.base_fields_pool[name]) for name in names])
     83
     84class FormMetaclass(type):
    5485    """
    5586    Metaclass that converts Field attributes to a dictionary called
    5687    'base_fields', taking into account parent class 'base_fields' as well.
     88
     89    Also integrates any additional media definitions
    5790    """
    5891    def __new__(cls, name, bases, attrs):
    59         attrs['base_fields'] = get_declared_fields(bases, attrs)
    60         new_class = super(DeclarativeFieldsMetaclass,
    61                      cls).__new__(cls, name, bases, attrs)
     92        new_class = type.__new__(cls, name, bases, attrs)
     93        new_class._meta = FormOptions(getattr(new_class, 'Meta', None))
     94        create_declared_fields(new_class, attrs)
     95        create_base_fields_pool_from_declared_fields(new_class, attrs)
     96        create_base_fields_from_base_fields_pool(new_class, attrs)
    6297        if 'media' not in attrs:
    6398            new_class.media = media_property(new_class)
    6499        return new_class
     
    105140            raise KeyError('Key %r not found in Form' % name)
    106141        return BoundField(self, field, name)
    107142
     143    def has_fieldsets(self):
     144        return self._meta.fieldsets is not None
     145   
     146    def fieldsets(self):
     147        if self.has_fieldsets():
     148            for fieldset in self._meta.fieldsets:
     149                yield {
     150                    'attrs': fieldset.get('attrs', {}),
     151                    'legend': fieldset.get('legend', u''),
     152                    'fields': [self[name] for name in fieldset['fields']],
     153                }
     154   
    108155    def _get_errors(self):
    109156        "Returns an ErrorDict for the data provided for the form"
    110157        if self._errors is None:
     
    310357    # fancy metaclass stuff purely for the semantic sugar -- it allows one
    311358    # to define a form using declarative syntax.
    312359    # BaseForm itself has no way of designating self.fields.
    313     __metaclass__ = DeclarativeFieldsMetaclass
     360    __metaclass__ = FormMetaclass
    314361
    315362class BoundField(StrAndUnicode):
    316363    "A Field plus data"
  • django/forms/models.py

    === modified file 'django/forms/models.py'
     
    99from django.utils.translation import ugettext_lazy as _
    1010
    1111from util import ValidationError, ErrorList
    12 from forms import BaseForm, get_declared_fields
     12from forms import FormOptions, BaseForm, create_declared_fields
     13from forms import create_base_fields_from_base_fields_pool
    1314from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES
    1415from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
    1516from widgets import media_property
     
    154155            field_list.append((f.name, formfield))
    155156    return SortedDict(field_list)
    156157
    157 class ModelFormOptions(object):
     158class ModelFormOptions(FormOptions):
    158159    def __init__(self, options=None):
     160        super(ModelFormOptions, self).__init__(options)
    159161        self.model = getattr(options, 'model', None)
    160         self.fields = getattr(options, 'fields', None)
    161         self.exclude = getattr(options, 'exclude', None)
    162 
     162
     163def create_model_fields(cls, attrs):
     164    """
     165    Create a list of form field instances from the option 'model'.
     166    This is used by the ModelForm metaclass.
     167    """
     168    formfield_callback = attrs.pop('formfield_callback', lambda f: f.formfield())
     169    fields = []
     170    if cls._meta.model:
     171        for dbfield in cls._meta.model._meta.fields + cls._meta.model._meta.many_to_many:
     172            if dbfield.editable:
     173                formfield = formfield_callback(dbfield)
     174                if formfield:
     175                    fields.append((dbfield.name, formfield))
     176    cls.model_fields = SortedDict(fields)
     177
     178def create_base_fields_pool_from_model_fields_and_declared_fields(cls, attrs):
     179    """
     180    Create a list of form field instances which are declared in the form and
     181    its superclasses (from 'csl.__mro__'). Add fields from the last form
     182    with a model. This is used by the MetaclasForm metaclass.
     183
     184    Note that we loop over the bases in *reverse*. This is necessary in
     185    order to preserve the correct order of fields.
     186    """
     187    model_fields, declared_fields = [], []
     188    for base in cls.__mro__[::-1]:
     189        try:
     190            declared_fields += base.declared_fields.items() # Raise AttributeError if the base is not a form.
     191            if base._meta.model: # Raise AttributeError if the base is not a model form.
     192                model_fields = base.model_fields.items()
     193        except AttributeError:
     194            pass
     195    cls.base_fields_pool = SortedDict(model_fields + declared_fields)
    163196
    164197class ModelFormMetaclass(type):
     198    """
     199    Metaclass that converts Field attributes to a dictionary called
     200    'base_fields', taking into account parent class 'base_fields' as well.
     201    Add fields from the class' model.
     202
     203    Also integrates any additional media definitions
     204    """
    165205    def __new__(cls, name, bases, attrs):
    166         formfield_callback = attrs.pop('formfield_callback',
    167                 lambda f: f.formfield())
    168         try:
    169             parents = [b for b in bases if issubclass(b, ModelForm)]
    170         except NameError:
    171             # We are defining ModelForm itself.
    172             parents = None
    173         declared_fields = get_declared_fields(bases, attrs, False)
    174         new_class = super(ModelFormMetaclass, cls).__new__(cls, name, bases,
    175                 attrs)
    176         if not parents:
    177             return new_class
    178 
     206        new_class = type.__new__(cls, name, bases, attrs)
     207        new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
     208        create_model_fields(new_class, attrs)
     209        create_declared_fields(new_class, attrs)
     210        create_base_fields_pool_from_model_fields_and_declared_fields(new_class, attrs)
     211        create_base_fields_from_base_fields_pool(new_class, attrs)
    179212        if 'media' not in attrs:
    180213            new_class.media = media_property(new_class)
    181         opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
    182         if opts.model:
    183             # If a model is defined, extract form fields from it.
    184             fields = fields_for_model(opts.model, opts.fields,
    185                                       opts.exclude, formfield_callback)
    186             # Override default model fields with any custom declared ones
    187             # (plus, include all the other declared fields).
    188             fields.update(declared_fields)
    189         else:
    190             fields = declared_fields
    191         new_class.declared_fields = declared_fields
    192         new_class.base_fields = fields
    193214        return new_class
    194215
    195216class BaseModelForm(BaseForm):
    196217    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
    197218                 initial=None, error_class=ErrorList, label_suffix=':',
    198219                 empty_permitted=False, instance=None):
    199         opts = self._meta
    200220        if instance is None:
    201221            # if we didn't get an instance, instantiate a new one
    202             self.instance = opts.model()
     222            self.instance = self._meta.model()
    203223            object_data = {}
    204224        else:
    205225            self.instance = instance
    206             object_data = model_to_dict(instance, opts.fields, opts.exclude)
     226            object_data = model_to_dict(instance)
    207227        # if initial was provided, it should override the values from instance
    208228        if initial is not None:
    209229            object_data.update(initial)
     
    309329            fail_message = 'created'
    310330        else:
    311331            fail_message = 'changed'
    312         return save_instance(self, self.instance, self._meta.fields, fail_message, commit)
     332        return save_instance(self, self.instance, self.fields.keys(), fail_message, commit)
    313333
    314334class ModelForm(BaseModelForm):
    315335    __metaclass__ = ModelFormMetaclass
    316336
    317 def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
     337def modelform_factory(model, form=ModelForm, fields=None, exclude=None, fieldsets=None,
    318338                       formfield_callback=lambda f: f.formfield()):
    319339    # HACK: we should be able to construct a ModelForm without creating
    320340    # and passing in a temporary inner class
    321341    class Meta:
    322342        pass
    323343    setattr(Meta, 'model', model)
     344    setattr(Meta, 'fieldsets', fieldsets)
    324345    setattr(Meta, 'fields', fields)
    325346    setattr(Meta, 'exclude', exclude)
    326347    class_name = model.__name__ + 'Form'
     
    430451def modelformset_factory(model, form=ModelForm, formfield_callback=lambda f: f.formfield(),
    431452                         formset=BaseModelFormSet,
    432453                         extra=1, can_delete=False, can_order=False,
    433                          max_num=0, fields=None, exclude=None):
     454                         max_num=0, fields=None, exclude=None, fieldsets=None):
    434455    """
    435456    Returns a FormSet class for the given Django model class.
    436457    """
    437458    form = modelform_factory(model, form=form, fields=fields, exclude=exclude,
     459                             fieldsets=fieldsets,
    438460                             formfield_callback=formfield_callback)
    439461    FormSet = formset_factory(form, formset, extra=extra, max_num=max_num,
    440462                              can_order=can_order, can_delete=can_delete)
     
    524546
    525547def inlineformset_factory(parent_model, model, form=ModelForm,
    526548                          formset=BaseInlineFormSet, fk_name=None,
    527                           fields=None, exclude=None,
     549                          fields=None, exclude=None, fieldsets=None,
    528550                          extra=3, can_order=False, can_delete=True, max_num=0,
    529551                          formfield_callback=lambda f: f.formfield()):
    530552    """
     
    549571        'extra': extra,
    550572        'can_delete': can_delete,
    551573        'can_order': can_order,
     574        'fieldsets': fieldsets,
    552575        'fields': fields,
    553576        'exclude': exclude,
    554577        'max_num': max_num,
  • tests/modeltests/model_forms/models.py

    === modified file 'tests/modeltests/model_forms/models.py'
     
    217217...         model = Category
    218218...         fields = ['name', 'url']
    219219...         exclude = ['url']
    220 
    221 >>> CategoryForm.base_fields.keys()
    222 ['name']
     220Traceback (most recent call last):
     221  File "/home/petr/django/local2/00-forms-fieldsets/django/test/_doctest.py", line 1267, in __run
     222    compileflags, 1) in test.globs
     223  File "<doctest modeltests.model_forms.models.__test__.API_TESTS[12]>", line 1, in ?
     224    class CategoryForm(ModelForm):
     225  File "/home/petr/django/local2/00-forms-fieldsets/django/forms/models.py", line 220, in __new__
     226    metaclassing.create_base_fields_from_base_fields_pool(new_class)
     227  File "/home/petr/django/local2/00-forms-fieldsets/django/forms/metaclassing.py", line 50, in create_base_fields_from_base_fields_pool
     228    raise ImproperlyConfigured("%s cannot have more than one option from fieldsets, fields and exclude." % cls.__name__)
     229ImproperlyConfigured: CategoryForm cannot have more than one option from fieldsets, fields and exclude.
    223230
    224231Don't allow more than one 'model' definition in the inheritance hierarchy.
    225232Technically, it would generate a valid form, but the fact that the resulting
  • tests/regressiontests/forms/forms.py

    === modified file 'tests/regressiontests/forms/forms.py'
     
    12651265...     haircut_type = CharField()
    12661266>>> b = Beatle(auto_id=False)
    12671267>>> print b.as_ul()
     1268<li>Instrument: <input type="text" name="instrument" /></li>
    12681269<li>First name: <input type="text" name="first_name" /></li>
    12691270<li>Last name: <input type="text" name="last_name" /></li>
    12701271<li>Birthday: <input type="text" name="birthday" /></li>
    1271 <li>Instrument: <input type="text" name="instrument" /></li>
    12721272<li>Haircut type: <input type="text" name="haircut_type" /></li>
    12731273
    12741274# Forms with prefixes #########################################################
     
    17491749>>> form.is_valid()
    17501750True
    17511751
     1752# Forms with meta attributes fields, exclude and fielsets #####################
     1753 
     1754>>> class UserForm(Form):
     1755...     username = CharField()
     1756...     email = CharField(widget=PasswordInput)
     1757...     first_name = CharField()
     1758...     last_name = CharField()
     1759 
     1760>>> t = Template('''
     1761... <form action="">
     1762... {% if form.has_fieldsets %}
     1763... {% for fieldset in form.fieldsets %}
     1764...     <fieldset>
     1765...     {% if fieldset.legend %}
     1766...         <legend>{{ fieldset.legend }}</legend>
     1767...     {% endif %}
     1768...     {% for field in fieldset.fields %}
     1769...         <p><label>{{ field.label }}: {{ field }}</label></p>
     1770...     {% endfor %}
     1771...     </fieldset>
     1772... {% endfor %}
     1773... {% else %}
     1774... {% for field in form %}
     1775...     <p><label>{{ field.label }}: {{ field }}</label></p>
     1776... {% endfor %}
     1777... {% endif %}
     1778...     <input type="submit" />
     1779... </form>''')
     1780 
     1781>>> clean_re = re.compile(r'\n( *\n)+')
     1782>>> clean = lambda text: clean_re.sub('\n', text).strip()
     1783 
     1784>>> print clean(t.render(Context({'form': UserForm()})))
     1785<form action="">
     1786    <p><label>Username: <input type="text" name="username" id="id_username" /></label></p>
     1787    <p><label>Email: <input type="password" name="email" id="id_email" /></label></p>
     1788    <p><label>First name: <input type="text" name="first_name" id="id_first_name" /></label></p>
     1789    <p><label>Last name: <input type="text" name="last_name" id="id_last_name" /></label></p>
     1790    <input type="submit" />
     1791</form>
     1792 
     1793>>> class OrderingUserForm(UserForm):
     1794...     class Meta:
     1795...         fields = ('first_name', 'last_name', 'username', 'email')
     1796
     1797>>> print clean(t.render(Context({'form': OrderingUserForm()})))
     1798<form action="">
     1799    <p><label>First name: <input type="text" name="first_name" id="id_first_name" /></label></p>
     1800    <p><label>Last name: <input type="text" name="last_name" id="id_last_name" /></label></p>
     1801    <p><label>Username: <input type="text" name="username" id="id_username" /></label></p>
     1802    <p><label>Email: <input type="password" name="email" id="id_email" /></label></p>
     1803    <input type="submit" />
     1804</form>
     1805
     1806>>> class FilteringUserForm(UserForm):
     1807...     class Meta:
     1808...         fields = ('first_name', 'last_name')
     1809
     1810>>> print clean(t.render(Context({'form': FilteringUserForm()})))
     1811<form action="">
     1812    <p><label>First name: <input type="text" name="first_name" id="id_first_name" /></label></p>
     1813    <p><label>Last name: <input type="text" name="last_name" id="id_last_name" /></label></p>
     1814    <input type="submit" />
     1815</form>
     1816
     1817>>> class ExcludingUserForm(UserForm):
     1818...     class Meta:
     1819...         exclude = ('first_name', 'last_name')
     1820
     1821>>> print clean(t.render(Context({'form': ExcludingUserForm()})))
     1822<form action="">
     1823    <p><label>Username: <input type="text" name="username" id="id_username" /></label></p>
     1824    <p><label>Email: <input type="password" name="email" id="id_email" /></label></p>
     1825    <input type="submit" />
     1826</form>
     1827
     1828>>> class FieldsetUserForm(UserForm):
     1829...     class Meta:
     1830...         fieldsets = (
     1831...             {'fields': ('username', 'email')},
     1832...             {'fields': ('first_name', 'last_name'), 'legend': 'Name'},
     1833...         )
     1834
     1835>>> print clean(t.render(Context({'form': FieldsetUserForm()})))
     1836<form action="">
     1837    <fieldset>
     1838        <p><label>Username: <input type="text" name="username" id="id_username" /></label></p>
     1839        <p><label>Email: <input type="password" name="email" id="id_email" /></label></p>
     1840    </fieldset>
     1841    <fieldset>
     1842        <legend>Name</legend>
     1843        <p><label>First name: <input type="text" name="first_name" id="id_first_name" /></label></p>
     1844        <p><label>Last name: <input type="text" name="last_name" id="id_last_name" /></label></p>
     1845    </fieldset>
     1846    <input type="submit" />
     1847</form>
     1848
    17521849"""
  • tests/regressiontests/modeladmin/models.py

    === modified file 'tests/regressiontests/modeladmin/models.py'
     
    132132>>> ma.get_form(request).base_fields.keys()
    133133['name', 'sign_date']
    134134 
    135 # Using `fields` and `exclude`.
    136 
    137 >>> class BandAdmin(ModelAdmin):
    138 ...     fields = ['name', 'bio']
    139 ...     exclude = ['bio']
    140 >>> ma = BandAdmin(Band, site)
    141 >>> ma.get_form(request).base_fields.keys()
    142 ['name']
    143 
    144135If we specify a form, it should use it allowing custom validation to work
    145136properly. This won't, however, break any of the admin widgets or media.
    146137
Back to Top