Ticket #6630: 02-add-fieldsets.diff

File 02-add-fieldsets.diff, 12.2 KB (added by Petr Marhoun <petr.marhoun@…>, 14 years ago)
  • django/forms/forms.py

    diff --git a/django/forms/forms.py b/django/forms/forms.py
    a b  
    5959    Option ``exclude`` is an optional list of field names. If provided,
    6060    the named fields will be excluded from the returned fields, even if they
    6161    are listed in the ``fields`` argument.
     62
     63    Option ``fieldsets`` is a list of two-tuples. The first item in each
     64    two-tuple is a name for the fieldset, and the second is a dictionary
     65    of fieldset options. Valid fieldset options in the dictionary include:
     66
     67    ``fields`` (required): A tuple of field names to display in the fieldset.
     68
     69    ``attrs``: A dictionary of HTML attributes to be used with the fieldset.
     70
     71    ``legend``: A string that should be the content of a ``legend`` tag
     72    to open the fieldset.
     73
     74    ``description``: A string of optional extra text.
     75
    6276    """
    6377    selected_fields = SortedDict()
    6478    for field_name, field in fields.items():
     
    6680            continue
    6781        if opts.exclude and field_name in opts.exclude:
    6882            continue
     83        if opts.fieldsets:
     84            for fieldset_name, fieldset_options in opts.fieldsets:
     85                if field_name in fieldset_options['fields']:
     86                    # Stop iteration of fieldsets so else condition is not used.
     87                    break
     88            else:
     89                # Field is not used in any fieldset.
     90                continue
    6991        selected_fields[field_name] = field
    7092    if opts.fields:
    7193        selected_fields = SortedDict((field_name, selected_fields.get(field_name))
     
    7698    def __init__(self, options=None):
    7799        self.fields = getattr(options, 'fields', None)
    78100        self.exclude = getattr(options, 'exclude', None)
     101        self.fieldsets = getattr(options, 'fieldsets', None)
    79102
    80103class FormMetaclass(type):
    81104    """
     
    134157            raise KeyError('Key %r not found in Form' % name)
    135158        return BoundField(self, field, name)
    136159
     160    def _has_fieldsets(self):
     161        """
     162        Returns True if form has fieldsets. Depends on _meta attribute giving
     163        options which have to define fieldsets. If you do not use FormMetaclass
     164        and want to use this method, you have to define options otherwise.
     165        """
     166        return self._meta.fieldsets is not None
     167    has_fieldsets = property(_has_fieldsets)
     168
     169    def fieldsets(self):
     170        """
     171        Returns collection of FieldSets. Depends on _meta attribute giving
     172        options which have to define fieldsets. If you do not use FormMetaclass
     173        and want to use this method, you have to define options otherwise.
     174        """
     175        return FieldSetCollection(self, self._meta.fieldsets)
     176
    137177    def _get_errors(self):
    138178        "Returns an ErrorDict for the data provided for the form"
    139179        if self._errors is None:
     
    414454    # BaseForm itself has no way of designating self.fields.
    415455    __metaclass__ = FormMetaclass
    416456
     457class FieldSetCollection(object):
     458    "A collection of FieldSets."
     459    def __init__(self, form, fieldsets):
     460        self.form = form
     461        self.fieldsets = fieldsets
     462
     463    def __iter__(self):
     464        if self.form.has_fieldsets:
     465            for name, options in self.fieldsets:
     466                yield self._construct_fieldset(name, options)
     467        else:
     468            # Construct dummy fieldset for form without any.
     469            yield FieldSet(self.form, None, self.form.fields)
     470
     471    def __getitem__(self, key):
     472        if self.form.has_fieldsets:
     473            for name, options in self.fieldsets:
     474                if name == key:
     475                    return self._construct_fieldset(name, options)
     476        raise KeyError('FieldSet with key %r not found' % key)
     477
     478    def _construct_fieldset(self, name, options):
     479        fields = SortedDict((field_name, self.form.fields[field_name])
     480            for field_name in options['fields'] if field_name in self.form.fields)
     481        return FieldSet(self.form, name, fields, options.get('attrs'),
     482            options.get('legend'), options.get('description'))
     483
     484class FieldSet(object):
     485    "Iterable FieldSet as a collection of BoundFields."
     486    def __init__(self, form, name, fields, attrs=None, legend=None, description=None):
     487        self.form = form
     488        self.name = name
     489        self.fields = fields
     490        self.attrs = attrs
     491        self.legend = legend
     492        self.description = description
     493
     494    def __iter__(self):
     495        for name, field in self.fields.items():
     496            yield BoundField(self.form, field, name)
     497
     498    def __getitem__(self, name):
     499        try:
     500            field = self.fields[name]
     501        except KeyError:
     502            raise KeyError('Key %r not found in FieldSet' % name)
     503        return BoundField(self.form, field, name)
     504
     505    def _errors(self):
     506        return ErrorDict((k, v) for (k, v) in self.form.errors.items() if k in self.fields)
     507    errors = property(_errors)
     508
    417509class BoundField(StrAndUnicode):
    418510    "A Field plus data"
    419511    def __init__(self, form, field, name):
  • django/forms/models.py

    diff --git a/django/forms/models.py b/django/forms/models.py
    a b  
    193193        self.model = getattr(options, 'model', None)
    194194        self.fields = getattr(options, 'fields', None)
    195195        self.exclude = getattr(options, 'exclude', None)
     196        self.fieldsets = getattr(options, 'fieldsets', None)
    196197        self.widgets = getattr(options, 'widgets', None)
    197198
    198199class ModelFormMetaclass(type):
  • tests/modeltests/model_forms/models.py

    diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py
    a b  
    322322>>> CategoryForm.base_fields.keys()
    323323['name']
    324324
     325
     326Using 'fieldsets'
     327
     328>>> class CategoryForm(ModelForm):
     329...
     330...     class Meta:
     331...         model = Category
     332...         fieldsets = (
     333...             ('main', {'fields': ('slug',)}),
     334...             ('other', {'fields': ('name',)}),
     335...         )
     336
     337>>> CategoryForm.base_fields.keys()
     338['name', 'slug']
     339
     340>>> for fieldset in CategoryForm().fieldsets():
     341...     print fieldset.name, fieldset.fields.keys()
     342main ['slug']
     343other ['name']
     344
     345
     346Using 'fields' *and* 'exclude' *and* 'fieldsets'
     347
     348>>> class CategoryForm(ModelForm):
     349...
     350...     class Meta:
     351...         model = Category
     352...         fields = ['name', 'url']
     353...         exclude = ['url']
     354...         fieldsets = (
     355...             ('main', {'fields': ('slug',)}),
     356...             ('other', {'fields': ('name',)}),
     357...         )
     358
     359>>> CategoryForm.base_fields.keys()
     360['name']
     361
     362>>> for fieldset in CategoryForm().fieldsets():
     363...     print fieldset.name, fieldset.fields.keys()
     364main []
     365other ['name']
     366
     367
    325368Using 'widgets'
    326369
    327370>>> class CategoryForm(ModelForm):
  • tests/regressiontests/forms/forms.py

    diff --git a/tests/regressiontests/forms/forms.py b/tests/regressiontests/forms/forms.py
    a b  
    966966<tr><th>Field2:</th><td><input type="text" name="field2" /></td></tr>
    967967<tr><th>Field11:</th><td><input type="text" name="field11" /></td></tr>
    968968
     969Or to specify fieldsets.
     970>>> class FieldsetsTestForm(TestForm):
     971...    class Meta:
     972...        fieldsets = (
     973...            ('main', {'fields': ('field3', 'field8', 'field2')}),
     974...            ('other', {'fields': ('field5', 'field11', 'field14')}),
     975...        )
     976>>> p = FieldsetsTestForm(auto_id=False)
     977>>> print p
     978<tr><th>Field2:</th><td><input type="text" name="field2" /></td></tr>
     979<tr><th>Field3:</th><td><input type="text" name="field3" /></td></tr>
     980<tr><th>Field5:</th><td><input type="text" name="field5" /></td></tr>
     981<tr><th>Field8:</th><td><input type="text" name="field8" /></td></tr>
     982<tr><th>Field11:</th><td><input type="text" name="field11" /></td></tr>
     983<tr><th>Field14:</th><td><input type="text" name="field14" /></td></tr>
     984>>> print p.has_fieldsets
     985True
     986>>> for fieldset in p.fieldsets():
     987...     print fieldset.name
     988...     for field in fieldset:
     989...         print field
     990main
     991<input type="text" name="field3" />
     992<input type="text" name="field8" />
     993<input type="text" name="field2" />
     994other
     995<input type="text" name="field5" />
     996<input type="text" name="field11" />
     997<input type="text" name="field14" />
     998
     999Fieldsets and fields in fieldsets can be identified by names.
     1000>>> print p.fieldsets()['main'].name
     1001main
     1002>>> print p.fieldsets()['other']['field5']
     1003<input type="text" name="field5" />
     1004
     1005Each fieldset has attribute errors for current fields.
     1006>>> for fieldset in p.fieldsets():
     1007...      fieldset.errors.as_ul()
     1008u''
     1009u''
     1010>>> p = FieldsetsTestForm({'field3': '3', 'field2': '2', 'field5': '5', 'field11': '11'})
     1011>>> for fieldset in p.fieldsets():
     1012...      fieldset.errors.as_ul()
     1013u'<ul class="errorlist"><li>field8<ul class="errorlist"><li>This field is required.</li></ul></li></ul>'
     1014u'<ul class="errorlist"><li>field14<ul class="errorlist"><li>This field is required.</li></ul></li></ul>'
     1015
     1016Or to use fields and exlude at once.
     1017>>> class CombinedTestForm(TestForm):
     1018...    class Meta:
     1019...        fields = ('field4', 'field2', 'field11', 'field8')
     1020...        exclude = ('field14', 'field3', 'field5', 'field8', 'field1')
     1021...        fieldsets = (
     1022...            ('main', {'fields': ('field3', 'field8', 'field2')}),
     1023...            ('other', {'fields': ('field5', 'field11', 'field14')}),
     1024...        )
     1025>>> p = CombinedTestForm(auto_id=False)
     1026>>> print p
     1027<tr><th>Field2:</th><td><input type="text" name="field2" /></td></tr>
     1028<tr><th>Field11:</th><td><input type="text" name="field11" /></td></tr>
     1029>>> print p.has_fieldsets
     1030True
     1031>>> for fieldset in p.fieldsets():
     1032...     print fieldset.name
     1033...     for field in fieldset:
     1034...         print field
     1035main
     1036<input type="text" name="field2" />
     1037other
     1038<input type="text" name="field11" />
     1039
     1040You can use fieldsets in your general templates also if you do not define any.
     1041>>> class OrderedTestForm(TestForm):
     1042...    class Meta:
     1043...        fields = ('field4', 'field2', 'field11', 'field8')
     1044>>> p = OrderedTestForm(auto_id=False)
     1045>>> print p.has_fieldsets
     1046False
     1047>>> for fieldset in p.fieldsets():
     1048...     print fieldset.name
     1049...     for field in fieldset:
     1050...         print field
     1051None
     1052<input type="text" name="field4" />
     1053<input type="text" name="field2" />
     1054<input type="text" name="field11" />
     1055<input type="text" name="field8" />
     1056
     1057Fieldsets can have some additional options - attrs, legend and description.
     1058You can use them in your templates.
     1059>>> class FieldsetsTestForm(TestForm):
     1060...    class Meta:
     1061...        fieldsets = (
     1062...            ('main', {
     1063...                'fields': ('field3', 'field8', 'field2'),
     1064...                'legend': 'Main fields',
     1065...                'description': 'You should really fill these fields.',
     1066...            }),
     1067...            ('other', {
     1068...                'fields': ('field5', 'field11', 'field14'),
     1069...                'legend': 'Other fields',
     1070...                'attrs': {'class': 'other'},
     1071...            }),
     1072...        )
     1073>>> p = FieldsetsTestForm(auto_id=False)
     1074>>> print p
     1075<tr><th>Field2:</th><td><input type="text" name="field2" /></td></tr>
     1076<tr><th>Field3:</th><td><input type="text" name="field3" /></td></tr>
     1077<tr><th>Field5:</th><td><input type="text" name="field5" /></td></tr>
     1078<tr><th>Field8:</th><td><input type="text" name="field8" /></td></tr>
     1079<tr><th>Field11:</th><td><input type="text" name="field11" /></td></tr>
     1080<tr><th>Field14:</th><td><input type="text" name="field14" /></td></tr>
     1081>>> print p.has_fieldsets
     1082True
     1083>>> for fieldset in p.fieldsets():
     1084...     print 'name:', fieldset.name
     1085...     print 'attrs:', fieldset.attrs
     1086...     print 'description:', fieldset.description
     1087...     print 'legend:', fieldset.legend
     1088...     for field in fieldset:
     1089...         print field 
     1090name: main
     1091attrs: None
     1092description: You should really fill these fields.
     1093legend: Main fields
     1094<input type="text" name="field3" />
     1095<input type="text" name="field8" />
     1096<input type="text" name="field2" />
     1097name: other
     1098attrs: {'class': 'other'}
     1099description: None
     1100legend: Other fields
     1101<input type="text" name="field5" />
     1102<input type="text" name="field11" />
     1103<input type="text" name="field14" />
     1104
    9691105Some Field classes have an effect on the HTML attributes of their associated
    9701106Widget. If you set max_length in a CharField and its associated widget is
    9711107either a TextInput or PasswordInput, then the widget's rendered HTML will
Back to Top