|
| 1 | +""" |
| 2 | +forms for django-form-utils |
| 3 | +
|
| 4 | +Time-stamp: <2010-04-28 02:57:16 carljm forms.py> |
| 5 | +
|
| 6 | +""" |
| 7 | +from copy import deepcopy |
| 8 | + |
| 9 | +from django import forms |
| 10 | +from django.forms.util import flatatt, ErrorDict |
| 11 | +from django.utils.safestring import mark_safe |
| 12 | + |
| 13 | +class Fieldset(object): |
| 14 | + """ |
| 15 | + An iterable Fieldset with a legend and a set of BoundFields. |
| 16 | +
|
| 17 | + """ |
| 18 | + def __init__(self, form, name, boundfields, legend='', classes='', description=''): |
| 19 | + self.form = form |
| 20 | + self.boundfields = boundfields |
| 21 | + if legend is None: legend = name |
| 22 | + self.legend = legend and mark_safe(legend) |
| 23 | + self.classes = classes |
| 24 | + self.description = mark_safe(description) |
| 25 | + self.name = name |
| 26 | + |
| 27 | + |
| 28 | + def _errors(self): |
| 29 | + return ErrorDict(((k, v) for (k, v) in self.form.errors.iteritems() |
| 30 | + if k in [f.name for f in self.boundfields])) |
| 31 | + errors = property(_errors) |
| 32 | + |
| 33 | + def __iter__(self): |
| 34 | + for bf in self.boundfields: |
| 35 | + yield _mark_row_attrs(bf, self.form) |
| 36 | + |
| 37 | + def __repr__(self): |
| 38 | + return "%s('%s', %s, legend='%s', classes='%s', description='%s')" % ( |
| 39 | + self.__class__.__name__, self.name, |
| 40 | + [f.name for f in self.boundfields], self.legend, self.classes, self.description) |
| 41 | + |
| 42 | +class FieldsetCollection(object): |
| 43 | + def __init__(self, form, fieldsets): |
| 44 | + self.form = form |
| 45 | + self.fieldsets = fieldsets |
| 46 | + self._cached_fieldsets = [] |
| 47 | + |
| 48 | + def __len__(self): |
| 49 | + return len(self.fieldsets) or 1 |
| 50 | + |
| 51 | + def __iter__(self): |
| 52 | + if not self._cached_fieldsets: |
| 53 | + self._gather_fieldsets() |
| 54 | + for field in self._cached_fieldsets: |
| 55 | + yield field |
| 56 | + |
| 57 | + def __getitem__(self, key): |
| 58 | + if not self._cached_fieldsets: |
| 59 | + self._gather_fieldsets() |
| 60 | + for field in self._cached_fieldsets: |
| 61 | + if field.name == key: |
| 62 | + return field |
| 63 | + raise KeyError |
| 64 | + |
| 65 | + def _gather_fieldsets(self): |
| 66 | + if not self.fieldsets: |
| 67 | + self.fieldsets = (('main', {'fields': self.form.fields.keys(), |
| 68 | + 'legend': ''}),) |
| 69 | + for name, options in self.fieldsets: |
| 70 | + try: |
| 71 | + field_names = [n for n in options['fields'] |
| 72 | + if n in self.form.fields] |
| 73 | + except KeyError: |
| 74 | + raise ValueError("Fieldset definition must include 'fields' option." ) |
| 75 | + boundfields = [forms.forms.BoundField(self.form, self.form.fields[n], n) |
| 76 | + for n in field_names] |
| 77 | + self._cached_fieldsets.append(Fieldset(self.form, name, |
| 78 | + boundfields, options.get('legend', None), |
| 79 | + ' '.join(options.get('classes', ())), |
| 80 | + options.get('description', ''))) |
| 81 | + |
| 82 | +def _get_meta_attr(attrs, attr, default): |
| 83 | + try: |
| 84 | + ret = getattr(attrs['Meta'], attr) |
| 85 | + except (KeyError, AttributeError): |
| 86 | + ret = default |
| 87 | + return ret |
| 88 | + |
| 89 | +def _set_meta_attr(attrs, attr, value): |
| 90 | + try: |
| 91 | + setattr(attrs['Meta'], attr, value) |
| 92 | + return True |
| 93 | + except KeyError: |
| 94 | + return False |
| 95 | + |
| 96 | +def get_fieldsets(bases, attrs): |
| 97 | + """ |
| 98 | + Get the fieldsets definition from the inner Meta class. |
| 99 | +
|
| 100 | + """ |
| 101 | + fieldsets = _get_meta_attr(attrs, 'fieldsets', None) |
| 102 | + if fieldsets is None: |
| 103 | + #grab the fieldsets from the first base class that has them |
| 104 | + for base in bases: |
| 105 | + fieldsets = getattr(base, 'base_fieldsets', None) |
| 106 | + if fieldsets is not None: |
| 107 | + break |
| 108 | + fieldsets = fieldsets or [] |
| 109 | + return fieldsets |
| 110 | + |
| 111 | +def get_fields_from_fieldsets(fieldsets): |
| 112 | + """ |
| 113 | + Get a list of all fields included in a fieldsets definition. |
| 114 | +
|
| 115 | + """ |
| 116 | + fields = [] |
| 117 | + try: |
| 118 | + for name, options in fieldsets: |
| 119 | + fields.extend(options['fields']) |
| 120 | + except (TypeError, KeyError): |
| 121 | + raise ValueError('"fieldsets" must be an iterable of two-tuples, ' |
| 122 | + 'and the second tuple must be a dictionary ' |
| 123 | + 'with a "fields" key') |
| 124 | + return fields |
| 125 | + |
| 126 | +def get_row_attrs(bases, attrs): |
| 127 | + """ |
| 128 | + Get the row_attrs definition from the inner Meta class. |
| 129 | +
|
| 130 | + """ |
| 131 | + return _get_meta_attr(attrs, 'row_attrs', {}) |
| 132 | + |
| 133 | +def _mark_row_attrs(bf, form): |
| 134 | + row_attrs = deepcopy(form._row_attrs.get(bf.name, {})) |
| 135 | + if bf.field.required: |
| 136 | + req_class = 'required' |
| 137 | + else: |
| 138 | + req_class = 'optional' |
| 139 | + if 'class' in row_attrs: |
| 140 | + row_attrs['class'] = row_attrs['class'] + ' ' + req_class |
| 141 | + else: |
| 142 | + row_attrs['class'] = req_class |
| 143 | + bf.row_attrs = mark_safe(flatatt(row_attrs)) |
| 144 | + return bf |
| 145 | + |
| 146 | +class BetterFormBaseMetaclass(type): |
| 147 | + def __new__(cls, name, bases, attrs): |
| 148 | + attrs['base_fieldsets'] = get_fieldsets(bases, attrs) |
| 149 | + fields = get_fields_from_fieldsets(attrs['base_fieldsets']) |
| 150 | + if (_get_meta_attr(attrs, 'fields', None) is None and |
| 151 | + _get_meta_attr(attrs, 'exclude', None) is None): |
| 152 | + _set_meta_attr(attrs, 'fields', fields) |
| 153 | + attrs['base_row_attrs'] = get_row_attrs(bases, attrs) |
| 154 | + new_class = super(BetterFormBaseMetaclass, |
| 155 | + cls).__new__(cls, name, bases, attrs) |
| 156 | + return new_class |
| 157 | + |
| 158 | +class BetterFormMetaclass(BetterFormBaseMetaclass, |
| 159 | + forms.forms.DeclarativeFieldsMetaclass): |
| 160 | + pass |
| 161 | + |
| 162 | +class BetterModelFormMetaclass(BetterFormBaseMetaclass, |
| 163 | + forms.models.ModelFormMetaclass): |
| 164 | + pass |
| 165 | + |
| 166 | +class BetterBaseForm(object): |
| 167 | + """ |
| 168 | + ``BetterForm`` and ``BetterModelForm`` are subclasses of Form |
| 169 | + and ModelForm that allow for declarative definition of fieldsets |
| 170 | + and row_attrs in an inner Meta class. |
| 171 | +
|
| 172 | + The row_attrs declaration is a dictionary mapping field names to |
| 173 | + dictionaries of attribute/value pairs. The attribute/value |
| 174 | + dictionaries will be flattened into HTML-style attribute/values |
| 175 | + (i.e. {'style': 'display: none'} will become ``style="display: |
| 176 | + none"``), and will be available as the ``row_attrs`` attribute of |
| 177 | + the ``BoundField``. Also, a CSS class of "required" or "optional" |
| 178 | + will automatically be added to the row_attrs of each |
| 179 | + ``BoundField``, depending on whether the field is required. |
| 180 | +
|
| 181 | + There is no automatic inheritance of ``row_attrs``. |
| 182 | + |
| 183 | + The fieldsets declaration is a list of two-tuples very similar to |
| 184 | + the ``fieldsets`` option on a ModelAdmin class in |
| 185 | + ``django.contrib.admin``. |
| 186 | +
|
| 187 | + The first item in each two-tuple is a name for the fieldset, and |
| 188 | + the second is a dictionary of fieldset options. |
| 189 | +
|
| 190 | + Valid fieldset options in the dictionary include: |
| 191 | +
|
| 192 | + ``fields`` (required): A tuple of field names to display in this |
| 193 | + fieldset. |
| 194 | +
|
| 195 | + ``classes``: A list of extra CSS classes to apply to the fieldset. |
| 196 | +
|
| 197 | + ``legend``: This value, if present, will be the contents of a ``legend`` |
| 198 | + tag to open the fieldset. |
| 199 | +
|
| 200 | + ``description``: A string of optional extra text to be displayed |
| 201 | + under the ``legend`` of the fieldset. |
| 202 | +
|
| 203 | + When iterated over, the ``fieldsets`` attribute of a |
| 204 | + ``BetterForm`` (or ``BetterModelForm``) yields ``Fieldset``s. |
| 205 | + Each ``Fieldset`` has a ``name`` attribute, a ``legend`` |
| 206 | + attribute, , a ``classes`` attribute (the ``classes`` tuple |
| 207 | + collapsed into a space-separated string), and a description |
| 208 | + attribute, and when iterated over yields its ``BoundField``s. |
| 209 | +
|
| 210 | + Subclasses of a ``BetterForm`` will inherit their parent's |
| 211 | + fieldsets unless they define their own. |
| 212 | +
|
| 213 | + A ``BetterForm`` or ``BetterModelForm`` can still be iterated over |
| 214 | + directly to yield all of its ``BoundField``s, regardless of |
| 215 | + fieldsets. |
| 216 | +
|
| 217 | + """ |
| 218 | + def __init__(self, *args, **kwargs): |
| 219 | + self._fieldsets = deepcopy(self.base_fieldsets) |
| 220 | + self._row_attrs = deepcopy(self.base_row_attrs) |
| 221 | + self._fieldset_collection = None |
| 222 | + super(BetterBaseForm, self).__init__(*args, **kwargs) |
| 223 | + |
| 224 | + @property |
| 225 | + def fieldsets(self): |
| 226 | + if not self._fieldset_collection: |
| 227 | + self._fieldset_collection = FieldsetCollection(self, |
| 228 | + self._fieldsets) |
| 229 | + return self._fieldset_collection |
| 230 | + |
| 231 | + def __iter__(self): |
| 232 | + for bf in super(BetterBaseForm, self).__iter__(): |
| 233 | + yield _mark_row_attrs(bf, self) |
| 234 | + |
| 235 | + def __getitem__(self, name): |
| 236 | + bf = super(BetterBaseForm, self).__getitem__(name) |
| 237 | + return _mark_row_attrs(bf, self) |
| 238 | + |
| 239 | +class BetterForm(BetterBaseForm, forms.Form): |
| 240 | + __metaclass__ = BetterFormMetaclass |
| 241 | + __doc__ = BetterBaseForm.__doc__ |
| 242 | + |
| 243 | +class BetterModelForm(BetterBaseForm, forms.ModelForm): |
| 244 | + __metaclass__ = BetterModelFormMetaclass |
| 245 | + __doc__ = BetterBaseForm.__doc__ |
| 246 | + |
| 247 | + |
| 248 | +class BasePreviewForm (object): |
| 249 | + """ |
| 250 | + Mixin to add preview functionality to a form. If the form is submitted with |
| 251 | + the following k/v pair in its ``data`` dictionary: |
| 252 | + |
| 253 | + 'submit': 'preview' (value string is case insensitive) |
| 254 | + |
| 255 | + Then ``PreviewForm.preview`` will be marked ``True`` and the form will |
| 256 | + be marked invalid (though this invalidation will not put an error in |
| 257 | + its ``errors`` dictionary). |
| 258 | + |
| 259 | + """ |
| 260 | + def __init__(self, *args, **kwargs): |
| 261 | + super(BasePreviewForm, self).__init__(*args, **kwargs) |
| 262 | + self.preview = self.check_preview(kwargs.get('data', None)) |
| 263 | + |
| 264 | + def check_preview(self, data): |
| 265 | + if data and data.get('submit', '').lower() == u'preview': |
| 266 | + return True |
| 267 | + return False |
| 268 | + |
| 269 | + def is_valid(self, *args, **kwargs): |
| 270 | + if self.preview: |
| 271 | + return False |
| 272 | + return super(BasePreviewForm, self).is_valid() |
| 273 | + |
| 274 | +class PreviewModelForm(BasePreviewForm, BetterModelForm): |
| 275 | + pass |
| 276 | + |
| 277 | +class PreviewForm(BasePreviewForm, BetterForm): |
| 278 | + pass |
0 commit comments