Ticket #17301: ticket17301_v2.diff

File ticket17301_v2.diff, 37.4 KB (added by Łukasz Rekucki, 13 years ago)

Patch with better code reuse in metaclass.

  • django/forms/forms.py

    diff --git a/django/forms/forms.py b/django/forms/forms.py
    index 94eb22d..1ea35f6 100644
    a b def get_declared_fields(bases, attrs, with_base_fields=True):  
    5454
    5555    return SortedDict(fields)
    5656
    57 class DeclarativeFieldsMetaclass(type):
    58     """
    59     Metaclass that converts Field attributes to a dictionary called
    60     'base_fields', taking into account parent class 'base_fields' as well.
    61     """
     57class BaseFormOptions(object):
     58    def __init__(self, options=None):
     59        self.fieldsets = getattr(options, 'fieldsets', None)
     60
     61class BaseFormMetaclass(type):
    6262    def __new__(cls, name, bases, attrs):
    63         attrs['base_fields'] = get_declared_fields(bases, attrs)
    64         new_class = super(DeclarativeFieldsMetaclass,
    65                      cls).__new__(cls, name, bases, attrs)
     63        parents = [b for b in bases if isinstance(b, cls)]
     64        new_class = super(BaseFormMetaclass, cls).__new__(cls, name, bases, attrs)
     65        if not parents:
     66            return new_class
    6667        if 'media' not in attrs:
    6768            new_class.media = media_property(new_class)
     69        new_class._meta = cls.make_options(getattr(new_class, 'Meta', None))
    6870        return new_class
    6971
     72    @classmethod
     73    def make_options(cls, meta):
     74        return BaseFormOptions(meta)
     75
     76
     77class DeclarativeFieldsMetaclass(BaseFormMetaclass):
     78    def __new__(cls, name, bases, attrs):
     79        attrs['base_fields'] = get_declared_fields(bases, attrs)
     80        return super(DeclarativeFieldsMetaclass, cls).__new__(cls, name, bases, attrs)
     81
     82
    7083class BaseForm(StrAndUnicode):
    7184    # This is the main implementation of all the Form logic. Note that this
    7285    # class is different than Form. See the comments by the Form class for more
    class BaseForm(StrAndUnicode):  
    109122            raise KeyError('Key %r not found in Form' % name)
    110123        return BoundField(self, field, name)
    111124
    112     def _get_errors(self):
     125    @property
     126    def errors(self):
    113127        "Returns an ErrorDict for the data provided for the form"
    114128        if self._errors is None:
    115129            self.full_clean()
    116130        return self._errors
    117     errors = property(_get_errors)
    118131
    119132    def is_valid(self):
    120133        """
    class BaseForm(StrAndUnicode):  
    138151        """
    139152        return u'initial-%s' % self.add_prefix(field_name)
    140153
    141     def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row):
     154    def _html_output(self, fieldset_method, error_row, before_fieldset=u'', after_fieldset=u''):
    142155        "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()."
    143156        top_errors = self.non_field_errors() # Errors that should be displayed above all fields.
    144         output, hidden_fields = [], []
    145 
    146         for name, field in self.fields.items():
    147             html_class_attr = ''
    148             bf = self[name]
    149             bf_errors = self.error_class([conditional_escape(error) for error in bf.errors]) # Escape and cache in local variable.
    150             if bf.is_hidden:
    151                 if bf_errors:
    152                     top_errors.extend([u'(Hidden field %s) %s' % (name, force_unicode(e)) for e in bf_errors])
    153                 hidden_fields.append(unicode(bf))
    154             else:
    155                 # Create a 'class="..."' atribute if the row should have any
    156                 # CSS classes applied.
    157                 css_classes = bf.css_classes()
    158                 if css_classes:
    159                     html_class_attr = ' class="%s"' % css_classes
    160 
    161                 if errors_on_separate_row and bf_errors:
    162                     output.append(error_row % force_unicode(bf_errors))
    163 
    164                 if bf.label:
    165                     label = conditional_escape(force_unicode(bf.label))
    166                     # Only add the suffix if the label does not end in
    167                     # punctuation.
    168                     if self.label_suffix:
    169                         if label[-1] not in ':?.!':
    170                             label += self.label_suffix
    171                     label = bf.label_tag(label) or ''
    172                 else:
    173                     label = ''
    174 
    175                 if field.help_text:
    176                     help_text = help_text_html % force_unicode(field.help_text)
    177                 else:
    178                     help_text = u''
    179 
    180                 output.append(normal_row % {
    181                     'errors': force_unicode(bf_errors),
    182                     'label': force_unicode(label),
    183                     'field': unicode(bf),
    184                     'help_text': help_text,
    185                     'html_class_attr': html_class_attr
    186                 })
     157        output = []
     158
     159        for fieldset in self.fieldsets:
     160            fieldset_html = [getattr(fieldset, fieldset_method)()]
     161            if not fieldset.dummy:
     162                fieldset_html.insert(0, u'<fieldset>')
     163                fieldset_html.insert(1, before_fieldset)
     164                fieldset_html.append(after_fieldset)
     165                fieldset_html.append(u'</fieldset>')
     166                if fieldset.legend:
     167                    fieldset_html.insert(1, fieldset.legend_tag())
     168                if top_errors:
     169                    output.insert(0, force_unicode(top_errors))
     170            output.extend(fieldset_html)
    187171
    188172        if top_errors:
    189173            output.insert(0, error_row % force_unicode(top_errors))
    190174
    191         if hidden_fields: # Insert any hidden fields in the last row.
    192             str_hidden = u''.join(hidden_fields)
    193             if output:
    194                 last_row = output[-1]
    195                 # Chop off the trailing row_ender (e.g. '</td></tr>') and
    196                 # insert the hidden fields.
    197                 if not last_row.endswith(row_ender):
    198                     # This can happen in the as_p() case (and possibly others
    199                     # that users write): if there are only top errors, we may
    200                     # not be able to conscript the last row for our purposes,
    201                     # so insert a new, empty row.
    202                     last_row = (normal_row % {'errors': '', 'label': '',
    203                                               'field': '', 'help_text':'',
    204                                               'html_class_attr': html_class_attr})
    205                     output.append(last_row)
    206                 output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender
    207             else:
    208                 # If there aren't any rows in the output, just append the
    209                 # hidden fields.
    210                 output.append(str_hidden)
    211175        return mark_safe(u'\n'.join(output))
    212176
    213177    def as_table(self):
    214178        "Returns this form rendered as HTML <tr>s -- excluding the <table></table>."
    215179        return self._html_output(
    216             normal_row = u'<tr%(html_class_attr)s><th>%(label)s</th><td>%(errors)s%(field)s%(help_text)s</td></tr>',
     180            fieldset_method='as_table',
    217181            error_row = u'<tr><td colspan="2">%s</td></tr>',
    218             row_ender = u'</td></tr>',
    219             help_text_html = u'<br /><span class="helptext">%s</span>',
    220             errors_on_separate_row = False)
     182            before_fieldset=u'<table>',
     183            after_fieldset=u'</table>')
    221184
    222185    def as_ul(self):
    223186        "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."
    224187        return self._html_output(
    225             normal_row = u'<li%(html_class_attr)s>%(errors)s%(label)s %(field)s%(help_text)s</li>',
     188            fieldset_method='as_ul',
    226189            error_row = u'<li>%s</li>',
    227             row_ender = '</li>',
    228             help_text_html = u' <span class="helptext">%s</span>',
    229             errors_on_separate_row = False)
     190            before_fieldset=u'<ul>',
     191            after_fieldset=u'</ul>')
    230192
    231193    def as_p(self):
    232194        "Returns this form rendered as HTML <p>s."
    233195        return self._html_output(
    234             normal_row = u'<p%(html_class_attr)s>%(label)s %(field)s%(help_text)s</p>',
    235             error_row = u'%s',
    236             row_ender = '</p>',
    237             help_text_html = u' <span class="helptext">%s</span>',
    238             errors_on_separate_row = True)
     196            fieldset_method='as_p',
     197            error_row = u'%s')
    239198
    240199    def non_field_errors(self):
    241200        """
    class BaseForm(StrAndUnicode):  
    322281        """
    323282        return bool(self.changed_data)
    324283
    325     def _get_changed_data(self):
     284    @property
     285    def changed_data(self):
    326286        if self._changed_data is None:
    327287            self._changed_data = []
    328288            # XXX: For now we're asking the individual widgets whether or not the
    class BaseForm(StrAndUnicode):  
    344304                if field.widget._has_changed(initial_value, data_value):
    345305                    self._changed_data.append(name)
    346306        return self._changed_data
    347     changed_data = property(_get_changed_data)
    348307
    349     def _get_media(self):
     308    @property
     309    def media(self):
    350310        """
    351311        Provide a description of all media required to render the widgets on this form
    352312        """
    class BaseForm(StrAndUnicode):  
    354314        for field in self.fields.values():
    355315            media = media + field.widget.media
    356316        return media
    357     media = property(_get_media)
    358317
    359318    def is_multipart(self):
    360319        """
    class BaseForm(StrAndUnicode):  
    380339        """
    381340        return [field for field in self if not field.is_hidden]
    382341
     342    @property
     343    def fieldsets(self):
     344        """
     345        Returns a list of Fieldset objects for each fieldset
     346        defined in Form's Meta options. If no fieldsets were defined,
     347        returns a list containing single, 'dummy' Fieldset with
     348        all form fields.
     349        """
     350        if self._meta.fieldsets:
     351            return [Fieldset(self, legend, attrs.get('fields', tuple()))
     352                    for legend, attrs in self._meta.fieldsets]
     353        return [Fieldset(self, None, self.fields.keys(), dummy=True)]
     354
    383355class Form(BaseForm):
    384356    "A collection of Fields, plus their associated data."
    385357    # This is a separate class from BaseForm in order to abstract the way
    class Form(BaseForm):  
    389361    # BaseForm itself has no way of designating self.fields.
    390362    __metaclass__ = DeclarativeFieldsMetaclass
    391363
     364class Fieldset(StrAndUnicode):
     365
     366    def __init__(self, form, legend, fields, dummy=False):
     367        """
     368        Arguments:
     369        form   -- form this fieldset belongs to
     370        legend -- fieldset's legend (used in <legend> tag)
     371        fields -- list containing names of fields in this fieldset
     372
     373        Keyword arguments:
     374        dummy  -- flag informing that the fieldset was created automatically
     375                  from all fields of form, because user has not defined
     376                  custom fieldsets
     377        """
     378        self.form = form
     379        self.legend = legend
     380        self.fields = fields
     381        self.dummy = dummy
     382
     383    def __unicode__(self):
     384        return self.as_table()
     385
     386    def __iter__(self):
     387        for name in self.fields:
     388            yield BoundField(self.form, self.form.fields[name], name)
     389
     390    def __getitem__(self, name):
     391        "Returns a BoundField with the given name."
     392        if name not in self.fields:
     393            raise KeyError('Key %r not found in Fieldset' % name)
     394        return self.form[name]
     395
     396    def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row):
     397        "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()."
     398        output, hidden_fields = [], []
     399        top_errors = self.form.error_class()
     400
     401        for name in self.fields:
     402            field = self.form.fields[name]
     403            html_class_attr = ''
     404            bf = self.form[name]
     405            bf_errors = self.form.error_class([conditional_escape(error) for error in bf.errors]) # Escape and cache in local variable.
     406            if bf.is_hidden:
     407                if bf_errors:
     408                    top_errors.extend([u'(Hidden field %s) %s' % (name, force_unicode(e)) for e in bf_errors])
     409                hidden_fields.append(unicode(bf))
     410            else:
     411                # Create a 'class="..."' atribute if the row should have any
     412                # CSS classes applied.
     413                css_classes = bf.css_classes()
     414                if css_classes:
     415                    html_class_attr = ' class="%s"' % css_classes
     416
     417                if errors_on_separate_row and bf_errors:
     418                    output.append(error_row % force_unicode(bf_errors))
     419
     420                if bf.label:
     421                    label = conditional_escape(force_unicode(bf.label))
     422                    # Only add the suffix if the label does not end in
     423                    # punctuation.
     424                    if self.form.label_suffix:
     425                        if label[-1] not in ':?.!':
     426                            label += self.form.label_suffix
     427                    label = bf.label_tag(label) or ''
     428                else:
     429                    label = ''
     430
     431                if field.help_text:
     432                    help_text = help_text_html % force_unicode(field.help_text)
     433                else:
     434                    help_text = u''
     435
     436                output.append(normal_row % {
     437                    'errors': force_unicode(bf_errors),
     438                    'label': force_unicode(label),
     439                    'field': unicode(bf),
     440                    'help_text': help_text,
     441                    'html_class_attr': html_class_attr
     442                })
     443
     444        if top_errors:
     445            output.insert(0, error_row % force_unicode(top_errors))
     446
     447        if hidden_fields: # Insert any hidden fields in the last row.
     448            str_hidden = u''.join(hidden_fields)
     449            if output:
     450                last_row = output[-1]
     451                # Chop off the trailing row_ender (e.g. '</td></tr>') and
     452                # insert the hidden fields.
     453                if not last_row.endswith(row_ender):
     454                    # This can happen in the as_p() case (and possibly others
     455                    # that users write): if there are only top errors, we may
     456                    # not be able to conscript the last row for our purposes,
     457                    # so insert a new, empty row.
     458                    last_row = (normal_row % {'errors': '', 'label': '',
     459                                              'field': '', 'help_text':'',
     460                                              'html_class_attr': html_class_attr})
     461                    output.append(last_row)
     462                output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender
     463            else:
     464                # If there aren't any rows in the output, just append the
     465                # hidden fields.
     466                output.append(str_hidden)
     467
     468        return mark_safe(u'\n'.join(output))
     469
     470    def as_table(self):
     471        "Returns this fieldset rendered as HTML <tr>s -- excluding the <table>, <fieldset> and <legend> tags."
     472        return self._html_output(
     473            normal_row = u'<tr%(html_class_attr)s><th>%(label)s</th><td>%(errors)s%(field)s%(help_text)s</td></tr>',
     474            error_row = u'<tr><td colspan="2">%s</td></tr>',
     475            row_ender = u'</td></tr>',
     476            help_text_html = u'<br /><span class="helptext">%s</span>',
     477            errors_on_separate_row = False)
     478
     479    def as_ul(self):
     480        "Returns this fieldset rendered as HTML <li>s -- excluding the <ul>, <fieldset> and <legend> tags."
     481        return self._html_output(
     482            normal_row = u'<li%(html_class_attr)s>%(errors)s%(label)s %(field)s%(help_text)s</li>',
     483            error_row = u'<li>%s</li>',
     484            row_ender = '</li>',
     485            help_text_html = u' <span class="helptext">%s</span>',
     486            errors_on_separate_row = False)
     487
     488    def as_p(self):
     489        "Returns this fieldset rendered as HTML <p>s -- excluding the <fieldset> and <legend> tags."
     490        return self._html_output(
     491            normal_row = u'<p%(html_class_attr)s>%(label)s %(field)s%(help_text)s</p>',
     492            error_row = u'%s',
     493            row_ender = '</p>',
     494            help_text_html = u' <span class="helptext">%s</span>',
     495            errors_on_separate_row = True)
     496
     497    def legend_tag(self, contents=None, attrs=None):
     498        """
     499        Wraps the given contents in a <legend>. Does not HTML-escape the contents.
     500        If contents aren't given, uses the fieldset's HTML-escaped legend.
     501
     502        If attrs are given, they're used as HTML attributes on the <legend> tag.
     503        """
     504        if contents is None and self.legend is not None:
     505            contents = conditional_escape(self.legend)
     506        attrs = attrs and flatatt(attrs) or ''
     507        if contents is not None:
     508            return mark_safe(u'<legend%s>%s</legend>' % (attrs, force_unicode(self.legend)))
     509        return None
     510
     511    def hidden_fields(self):
     512        """
     513        Returns a list of all the BoundField objects that are hidden fields.
     514        Useful for manual form layout in templates.
     515        """
     516        return [field for field in self if field.is_hidden]
     517
     518    def visible_fields(self):
     519        """
     520        Returns a list of BoundField objects that aren't hidden fields.
     521        The opposite of the hidden_fields() method.
     522        """
     523        return [field for field in self if not field.is_hidden]
     524
    392525class BoundField(StrAndUnicode):
    393526    "A Field plus data"
    394527    def __init__(self, form, field, name):
    class BoundField(StrAndUnicode):  
    426559    def __getitem__(self, idx):
    427560        return list(self.__iter__())[idx]
    428561
    429     def _errors(self):
     562    @property
     563    def errors(self):
    430564        """
    431565        Returns an ErrorList for this field. Returns an empty ErrorList
    432566        if there are none.
    433567        """
    434568        return self.form.errors.get(self.name, self.form.error_class())
    435     errors = property(_errors)
    436569
    437570    def as_widget(self, widget=None, attrs=None, only_initial=False):
    438571        """
    class BoundField(StrAndUnicode):  
    473606        """
    474607        return self.as_widget(self.field.hidden_widget(), attrs, **kwargs)
    475608
    476     def _data(self):
     609    @property
     610    def data(self):
    477611        """
    478612        Returns the data for this BoundField, or None if it wasn't given.
    479613        """
    480614        return self.field.widget.value_from_datadict(self.form.data, self.form.files, self.html_name)
    481     data = property(_data)
    482615
    483616    def value(self):
    484617        """
    class BoundField(StrAndUnicode):  
    524657            extra_classes.add(self.form.required_css_class)
    525658        return ' '.join(extra_classes)
    526659
    527     def _is_hidden(self):
     660    @property
     661    def is_hidden(self):
    528662        "Returns True if this BoundField's widget is hidden."
    529663        return self.field.widget.is_hidden
    530     is_hidden = property(_is_hidden)
    531664
    532     def _auto_id(self):
     665    @property
     666    def auto_id(self):
    533667        """
    534668        Calculates and returns the ID attribute for this BoundField, if the
    535669        associated Form has specified auto_id. Returns an empty string otherwise.
    class BoundField(StrAndUnicode):  
    540674        elif auto_id:
    541675            return self.html_name
    542676        return ''
    543     auto_id = property(_auto_id)
    544677
    545     def _id_for_label(self):
     678    @property
     679    def id_for_label(self):
    546680        """
    547681        Wrapper around the field widget's `id_for_label` method.
    548682        Useful, for example, for focusing on this field regardless of whether
    class BoundField(StrAndUnicode):  
    551685        widget = self.field.widget
    552686        id_ = widget.attrs.get('id') or self.auto_id
    553687        return widget.id_for_label(id_)
    554     id_for_label = property(_id_for_label)
  • django/forms/models.py

    diff --git a/django/forms/models.py b/django/forms/models.py
    index cd8f027..ccec2e5 100644
    a b from __future__ import absolute_import  
    88from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, FieldError
    99from django.core.validators import EMPTY_VALUES
    1010from django.forms.fields import Field, ChoiceField
    11 from django.forms.forms import BaseForm, get_declared_fields
     11from django.forms.forms import (BaseForm, BaseFormOptions, BaseFormMetaclass,
     12    get_declared_fields)
    1213from django.forms.formsets import BaseFormSet, formset_factory
    1314from django.forms.util import ErrorList
    1415from django.forms.widgets import (SelectMultiple, HiddenInput,
    15     MultipleHiddenInput, media_property)
     16    MultipleHiddenInput)
    1617from django.utils.encoding import smart_unicode, force_unicode
    1718from django.utils.datastructures import SortedDict
    1819from django.utils.text import get_text_list, capfirst
    def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_c  
    175176        )
    176177    return field_dict
    177178
    178 class ModelFormOptions(object):
     179class ModelFormOptions(BaseFormOptions):
    179180    def __init__(self, options=None):
     181        super(ModelFormOptions, self).__init__(options)
    180182        self.model = getattr(options, 'model', None)
    181183        self.fields = getattr(options, 'fields', None)
    182184        self.exclude = getattr(options, 'exclude', None)
    183185        self.widgets = getattr(options, 'widgets', None)
    184186
     187class ModelFormMetaclass(BaseFormMetaclass):
    185188
    186 class ModelFormMetaclass(type):
    187189    def __new__(cls, name, bases, attrs):
    188190        formfield_callback = attrs.pop('formfield_callback', None)
    189         try:
    190             parents = [b for b in bases if issubclass(b, ModelForm)]
    191         except NameError:
    192             # We are defining ModelForm itself.
    193             parents = None
    194         declared_fields = get_declared_fields(bases, attrs, False)
    195         new_class = super(ModelFormMetaclass, cls).__new__(cls, name, bases,
    196                 attrs)
    197         if not parents:
     191
     192        new_class = super(ModelFormMetaclass, cls).__new__(cls, name, bases, attrs)
     193        opts = getattr(new_class, "_meta", None)
     194        if not opts:  # no parents
    198195            return new_class
     196       
     197        declared_fields = get_declared_fields(bases, attrs, False)
    199198
    200         if 'media' not in attrs:
    201             new_class.media = media_property(new_class)
    202         opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
    203199        if opts.model:
    204200            # If a model is defined, extract form fields from it.
    205201            fields = fields_for_model(opts.model, opts.fields,
    class ModelFormMetaclass(type):  
    222218        new_class.base_fields = fields
    223219        return new_class
    224220
     221    @classmethod
     222    def make_options(cls, meta):
     223        return ModelFormOptions(meta)
     224
     225
    225226class BaseModelForm(BaseForm):
    226227    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
    227228                 initial=None, error_class=ErrorList, label_suffix=':',
    class BaseModelForm(BaseForm):  
    305306
    306307    def _post_clean(self):
    307308        opts = self._meta
    308         # Update the model instance with self.cleaned_data.
     309        # Update the model instance with self.cleaned_data.ModelForm
    309310        self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude)
    310311
    311312        exclude = self._get_validation_exclusions()
  • django/forms/widgets.py

    diff --git a/django/forms/widgets.py b/django/forms/widgets.py
    index 1fbef98..f95a16d 100644
    a b class Media(StrAndUnicode):  
    7070            return path
    7171        if prefix is None:
    7272            if settings.STATIC_URL is None:
    73                  # backwards compatibility
     73                # backwards compatibility
    7474                prefix = settings.MEDIA_URL
    7575            else:
    7676                prefix = settings.STATIC_URL
  • docs/topics/forms/index.txt

    diff --git a/docs/topics/forms/index.txt b/docs/topics/forms/index.txt
    index 18e55f5..bd47732 100644
    a b tag::  
    388388If you find yourself doing this often, you might consider creating a custom
    389389:ref:`inclusion tag<howto-custom-template-tags-inclusion-tags>`.
    390390
     391Fieldsets
     392---------
     393
     394.. versionadded:: development
     395
     396Having a complex form you may want to organize its fields in logical groups.
     397In Django you can do that using fieldsets. Fieldsets allow you to iterate
     398through their fields and are autmatically rendered by form using ``<fieldset>``
     399HTML tag and its subtag ``<legend>``.
     400
     401
     402Fieldsets are defined using ``Meta`` options class. If you're familiar
     403with :attr:`~django.contrib.admin.ModelAdmin.fieldsets` from Django admin
     404options, you alreadyknow the syntax:
     405
     406.. code-block:: python
     407
     408    class PersonForm(forms.Form):
     409        home_phone = CharField()
     410        cell_phone = CharField()
     411        first_name = CharField()
     412        last_name = CharField()
     413
     414        class Meta:
     415            fieldsets = (
     416                (None, {
     417                    'fields': ('first_name', 'last_name',),
     418                }),
     419                ("Phone numbers", {
     420                    'fields': ('cell_phone', 'home_phone',),
     421                }),
     422            )
     423
     424Having above example form you may render it in a template just like a normal form::
     425
     426    <form action="" method="post">
     427        {{ form.as_table }}
     428        <input type="submit" value="Send" />
     429    </form>
     430
     431Except now instead of one ``<table>`` element, ``as_table`` method will
     432print two tables wrapped up in ``<fieldset>`` tags::
     433
     434    <form action="" method="post">
     435        <fieldset>
     436            <table>
     437                <tr>
     438                    <th><label for="id_first_name">First name:</label></th>
     439                    <td><input type="text" name="first_name" id="id_first_name" /></td>
     440                </tr>
     441                <tr>
     442                    <th><label for="id_last_name">Last name:</label></th>
     443                    <td><input type="text" name="last_name" id="id_last_name" /></td>
     444                </tr>
     445            </table>
     446        </fieldset>
     447        <fieldset>
     448            <legend>Phone numbers</legend>
     449            <table>
     450                <tr>
     451                    <th><label for="id_cell_phone">Cell phone:</label></th>
     452                    <td><input type="text" name="cell_phone" id="id_cell_phone" /></td>
     453                </tr>
     454                <tr>
     455                    <th><label for="id_home_phone">Home phone:</label></th>
     456                    <td><input type="text" name="home_phone" id="id_home_phone" /></td>
     457                </tr>
     458            </table>
     459        </fieldset>
     460        <input type="submit" value="Send" />
     461    </form>
     462
     463You can also customize your output looping through form's fieldsets and using
     464their methods -- ``as_table``, ``as_ul`` and ``as_p`` -- which behave just like
     465their equivalents from ``Form`` class and using a ``legend_tag`` method::
     466
     467    <form action="" method="post">
     468        {% for fieldset in form.fieldsets %}
     469            <fieldset>
     470                {{ fieldset.legend_tag }}
     471                <table>
     472                    {{ fieldset.as_table }}
     473                </table>
     474            </fieldset>
     475        {% endfor %}
     476        <input type="submit" value="Send" />
     477    </form>
     478
     479You can be even more specific and loop through all fields of all fieldsets::
     480
     481    <form action="" method="post">
     482        {% for fieldset in form.fieldsets %}
     483            <fieldset>
     484                {{ fieldset.legend_tag }}
     485                <ul>
     486                    {% for field in fieldset %}
     487                        <li>
     488                            {{ field.label_tag }}
     489                            {{ field }}
     490                        </li>
     491                    {% endfor %}
     492                </ul>
     493            </fieldset>
     494        {% endfor %}
     495        <input type="submit" value="Send" />
     496    </form>
     497
     498You can also loop though fieldset ``hidden_fields`` and ``visible_fields`` just
     499line in a form class.
     500
    391501Further topics
    392502==============
    393503
  • tests/regressiontests/forms/tests/__init__.py

    diff --git a/tests/regressiontests/forms/tests/__init__.py b/tests/regressiontests/forms/tests/__init__.py
    index 8e2150c..8c3d0fb 100644
    a b from .util import FormsUtilTestCase  
    1919from .validators import TestFieldWithValidators
    2020from .widgets import (FormsWidgetTestCase, FormsI18NWidgetsTestCase,
    2121    WidgetTests, ClearableFileInputTests)
     22from .fieldsets import FieldsetsTestCase
  • new file tests/regressiontests/forms/tests/fieldsets.py

    diff --git a/tests/regressiontests/forms/tests/fieldsets.py b/tests/regressiontests/forms/tests/fieldsets.py
    new file mode 100644
    index 0000000..79ccabb
    - +  
     1# -*- coding: utf-8 -*-
     2import datetime
     3
     4from django.core.files.uploadedfile import SimpleUploadedFile
     5from django.forms import *
     6from django import forms
     7from django.http import QueryDict
     8from django.template import Template, Context
     9from django.utils.datastructures import MultiValueDict, MergeDict
     10from django.utils.safestring import mark_safe
     11from django.utils.unittest import TestCase
     12
     13
     14class PersonWithoutFormfields(Form):
     15    first_name = CharField()
     16    last_name = CharField()
     17    birthday = DateField()
     18    band = CharField()
     19    secret = CharField(widget=HiddenInput)
     20
     21class Person(PersonWithoutFormfields):
     22    class Meta:
     23        fieldsets = (
     24            (None, {
     25                'fields': ('first_name', 'last_name', 'birthday'),
     26            }),
     27            ("Additional fields", {
     28                'fields': ('band', 'secret'),
     29            }),
     30        )
     31
     32class FieldsetsTestCase(TestCase):
     33
     34    some_data = {
     35        'first_name': u'John',
     36        'last_name': u'Lennon',
     37        'birthday': u'1940-10-9',
     38        'band': u'The Beatles',
     39        'secret': u'he didnt say',
     40    }
     41
     42    def test_simple_rendering(self):
     43        # Pass a dictionary to a Form's __init__().
     44        p = Person(self.some_data)
     45        # as_table
     46        self.assertEqual(str(p), """<fieldset>
     47<table>
     48<tr><th><label for="id_first_name">First name:</label></th><td><input type="text" name="first_name" value="John" id="id_first_name" /></td></tr>
     49<tr><th><label for="id_last_name">Last name:</label></th><td><input type="text" name="last_name" value="Lennon" id="id_last_name" /></td></tr>
     50<tr><th><label for="id_birthday">Birthday:</label></th><td><input type="text" name="birthday" value="1940-10-9" id="id_birthday" /></td></tr>
     51</table>
     52</fieldset>
     53<fieldset>
     54<legend>Additional fields</legend>
     55<table>
     56<tr><th><label for="id_band">Band:</label></th><td><input type="text" name="band" value="The Beatles" id="id_band" /><input type="hidden" name="secret" value="he didnt say" id="id_secret" /></td></tr>
     57</table>
     58</fieldset>""")
     59        self.assertEqual(str(p), unicode(p))
     60        self.assertEqual(str(p), p.as_table())
     61        # as_ul
     62        self.assertEqual(p.as_ul(), """<fieldset>
     63<ul>
     64<li><label for="id_first_name">First name:</label> <input type="text" name="first_name" value="John" id="id_first_name" /></li>
     65<li><label for="id_last_name">Last name:</label> <input type="text" name="last_name" value="Lennon" id="id_last_name" /></li>
     66<li><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" value="1940-10-9" id="id_birthday" /></li>
     67</ul>
     68</fieldset>
     69<fieldset>
     70<legend>Additional fields</legend>
     71<ul>
     72<li><label for="id_band">Band:</label> <input type="text" name="band" value="The Beatles" id="id_band" /><input type="hidden" name="secret" value="he didnt say" id="id_secret" /></li>
     73</ul>
     74</fieldset>""")
     75        # as_p
     76        self.assertEqual(p.as_p(), """<fieldset>
     77
     78<p><label for="id_first_name">First name:</label> <input type="text" name="first_name" value="John" id="id_first_name" /></p>
     79<p><label for="id_last_name">Last name:</label> <input type="text" name="last_name" value="Lennon" id="id_last_name" /></p>
     80<p><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" value="1940-10-9" id="id_birthday" /></p>
     81
     82</fieldset>
     83<fieldset>
     84<legend>Additional fields</legend>
     85
     86<p><label for="id_band">Band:</label> <input type="text" name="band" value="The Beatles" id="id_band" /><input type="hidden" name="secret" value="he didnt say" id="id_secret" /></p>
     87
     88</fieldset>""") # Additional blank lines are ok
     89
     90    def test_single_fieldset_rendering(self):
     91        # Pass a dictionary to a Form's __init__().
     92        p = Person(self.some_data)
     93        # as_table
     94        self.assertEqual(str(p.fieldsets[0]), """<tr><th><label for="id_first_name">First name:</label></th><td><input type="text" name="first_name" value="John" id="id_first_name" /></td></tr>
     95<tr><th><label for="id_last_name">Last name:</label></th><td><input type="text" name="last_name" value="Lennon" id="id_last_name" /></td></tr>
     96<tr><th><label for="id_birthday">Birthday:</label></th><td><input type="text" name="birthday" value="1940-10-9" id="id_birthday" /></td></tr>""")
     97        self.assertEqual(str(p.fieldsets[0]), unicode(p.fieldsets[0]))
     98        self.assertEqual(str(p.fieldsets[0]), p.fieldsets[0].as_table())
     99        self.assertEqual(str(p.fieldsets[1]), """<tr><th><label for="id_band">Band:</label></th><td><input type="text" name="band" value="The Beatles" id="id_band" /><input type="hidden" name="secret" value="he didnt say" id="id_secret" /></td></tr>""")
     100        self.assertEqual(str(p.fieldsets[1]), p.fieldsets[1].as_table())
     101        # as_ul
     102        self.assertEqual(p.fieldsets[0].as_ul(), """<li><label for="id_first_name">First name:</label> <input type="text" name="first_name" value="John" id="id_first_name" /></li>
     103<li><label for="id_last_name">Last name:</label> <input type="text" name="last_name" value="Lennon" id="id_last_name" /></li>
     104<li><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" value="1940-10-9" id="id_birthday" /></li>""")
     105        self.assertEqual(p.fieldsets[1].as_ul(), """<li><label for="id_band">Band:</label> <input type="text" name="band" value="The Beatles" id="id_band" /><input type="hidden" name="secret" value="he didnt say" id="id_secret" /></li>""")
     106        # as_p
     107        self.assertEqual(p.fieldsets[0].as_p(), """<p><label for="id_first_name">First name:</label> <input type="text" name="first_name" value="John" id="id_first_name" /></p>
     108<p><label for="id_last_name">Last name:</label> <input type="text" name="last_name" value="Lennon" id="id_last_name" /></p>
     109<p><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" value="1940-10-9" id="id_birthday" /></p>""")
     110        self.assertEqual(p.fieldsets[1].as_p(), """<p><label for="id_band">Band:</label> <input type="text" name="band" value="The Beatles" id="id_band" /><input type="hidden" name="secret" value="he didnt say" id="id_secret" /></p>""")
     111
     112    def test_fieldset_fields_iteration(self):
     113        # Pass a dictionary to a Form's __init__().
     114        p = Person(self.some_data)
     115        for fieldset in p.fieldsets:
     116            for field in fieldset:
     117                pass
     118        self.assertEqual(set([field.name for field in p.fieldsets[0].visible_fields()]), set(['first_name', 'last_name', 'birthday']))
     119        self.assertEqual(len(p.fieldsets[0].hidden_fields()), 0)
     120        self.assertEqual(set([field.name for field in p.fieldsets[1].visible_fields()]), set(['band']))
     121        self.assertEqual(set([field.name for field in p.fieldsets[1].hidden_fields()]), set(['secret']))
     122
     123    def test_legend_tag(self):
     124        # Pass a dictionary to a Form's __init__().
     125        p = Person(self.some_data)
     126        self.assertIsNone(p.fieldsets[0].legend_tag())
     127        self.assertEqual(p.fieldsets[1].legend_tag(), """<legend>Additional fields</legend>""")
     128
  • tests/regressiontests/forms/tests/forms.py

    diff --git a/tests/regressiontests/forms/tests/forms.py b/tests/regressiontests/forms/tests/forms.py
    index 3f529f2..4b0faa6 100644
    a b class FormsTestCase(TestCase):  
    18071807        form = NameForm(data={'name' : ['fname', 'lname']})
    18081808        self.assertTrue(form.is_valid())
    18091809        self.assertEqual(form.cleaned_data, {'name' : 'fname lname'})
     1810
     1811    def test_meta_options(self):
     1812        class MetaOptionsForm(Form):
     1813            class Meta:
     1814                fieldsets = 0xDEADBEEF
     1815                some_nonexising_option = True
     1816        class MetaOptionsDerivantForm(MetaOptionsForm):
     1817            pass
     1818        # Test classes
     1819        self.assertEqual(MetaOptionsForm._meta.fieldsets, 0xDEADBEEF)
     1820        self.assertEqual(MetaOptionsDerivantForm._meta.fieldsets, 0xDEADBEEF)
     1821        self.assertFalse(hasattr(MetaOptionsForm._meta, 'some_nonexising_option'))
     1822        self.assertFalse(hasattr(MetaOptionsDerivantForm._meta, 'some_nonexising_option'))
     1823        # Test instances
     1824        meta_options_form = MetaOptionsForm()
     1825        meta_options_derivant_form = MetaOptionsDerivantForm()
     1826        self.assertEqual(meta_options_form._meta.fieldsets, 0xDEADBEEF)
     1827        self.assertEqual(meta_options_derivant_form._meta.fieldsets, 0xDEADBEEF)
     1828        self.assertFalse(hasattr(meta_options_form._meta, 'some_nonexising_option'))
     1829        self.assertFalse(hasattr(meta_options_derivant_form._meta, 'some_nonexising_option'))
     1830
     1831    def test_meta_options_override(self):
     1832        class MetaOptionsForm(Form):
     1833            class Meta:
     1834                fieldsets = 0xDEADBEEF
     1835        class MetaOptionsDerivantForm(MetaOptionsForm):
     1836            class Meta:
     1837                fieldsets = 0xCAFEBABE
     1838        # Test classes
     1839        self.assertEqual(MetaOptionsForm._meta.fieldsets, 0xDEADBEEF)
     1840        self.assertEqual(MetaOptionsDerivantForm._meta.fieldsets, 0xCAFEBABE)
     1841        # Test instances
     1842        meta_options_form = MetaOptionsForm()
     1843        meta_options_derivant_form = MetaOptionsDerivantForm()
     1844        self.assertEqual(meta_options_form._meta.fieldsets, 0xDEADBEEF)
     1845        self.assertEqual(meta_options_derivant_form._meta.fieldsets, 0xCAFEBABE)
     1846
     1847    def test_meta_option_defaults(self):
     1848        class MetaOptionsForm(Form):
     1849            pass
     1850        # Test classes
     1851        self.assertIsNone(MetaOptionsForm._meta.fieldsets)
     1852        # Test instance
     1853        meta_options_form = MetaOptionsForm()
     1854        self.assertIsNone(meta_options_form._meta.fieldsets)
Back to Top