~canonical-django/canonical-django/project-template

« back to all changes in this revision

Viewing changes to trunk/python-packages/django/forms/forms.py

  • Committer: Matthew Nuzum
  • Date: 2008-11-13 05:46:03 UTC
  • Revision ID: matthew.nuzum@canonical.com-20081113054603-v0kvr6z6xyexvqt3
adding to version control

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""
 
2
Form classes
 
3
"""
 
4
 
 
5
from copy import deepcopy
 
6
 
 
7
from django.utils.datastructures import SortedDict
 
8
from django.utils.html import escape
 
9
from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode
 
10
from django.utils.safestring import mark_safe
 
11
 
 
12
from fields import Field, FileField
 
13
from widgets import Media, media_property, TextInput, Textarea
 
14
from util import flatatt, ErrorDict, ErrorList, ValidationError
 
15
 
 
16
__all__ = ('BaseForm', 'Form')
 
17
 
 
18
NON_FIELD_ERRORS = '__all__'
 
19
 
 
20
def pretty_name(name):
 
21
    "Converts 'first_name' to 'First name'"
 
22
    name = name[0].upper() + name[1:]
 
23
    return name.replace('_', ' ')
 
24
 
 
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.
 
30
 
 
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)]
 
37
    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
 
46
    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):
 
54
    """
 
55
    Metaclass that converts Field attributes to a dictionary called
 
56
    'base_fields', taking into account parent class 'base_fields' as well.
 
57
    """
 
58
    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)
 
62
        if 'media' not in attrs:
 
63
            new_class.media = media_property(new_class)
 
64
        return new_class
 
65
 
 
66
class BaseForm(StrAndUnicode):
 
67
    # This is the main implementation of all the Form logic. Note that this
 
68
    # class is different than Form. See the comments by the Form class for more
 
69
    # information. Any improvements to the form API should be made to *this*
 
70
    # class, not to the Form class.
 
71
    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
 
72
                 initial=None, error_class=ErrorList, label_suffix=':',
 
73
                 empty_permitted=False):
 
74
        self.is_bound = data is not None or files is not None
 
75
        self.data = data or {}
 
76
        self.files = files or {}
 
77
        self.auto_id = auto_id
 
78
        self.prefix = prefix
 
79
        self.initial = initial or {}
 
80
        self.error_class = error_class
 
81
        self.label_suffix = label_suffix
 
82
        self.empty_permitted = empty_permitted
 
83
        self._errors = None # Stores the errors after clean() has been called.
 
84
        self._changed_data = None
 
85
 
 
86
        # The base_fields class attribute is the *class-wide* definition of
 
87
        # fields. Because a particular *instance* of the class might want to
 
88
        # alter self.fields, we create self.fields here by copying base_fields.
 
89
        # Instances should always modify self.fields; they should not modify
 
90
        # self.base_fields.
 
91
        self.fields = deepcopy(self.base_fields)
 
92
 
 
93
    def __unicode__(self):
 
94
        return self.as_table()
 
95
 
 
96
    def __iter__(self):
 
97
        for name, field in self.fields.items():
 
98
            yield BoundField(self, field, name)
 
99
 
 
100
    def __getitem__(self, name):
 
101
        "Returns a BoundField with the given name."
 
102
        try:
 
103
            field = self.fields[name]
 
104
        except KeyError:
 
105
            raise KeyError('Key %r not found in Form' % name)
 
106
        return BoundField(self, field, name)
 
107
 
 
108
    def _get_errors(self):
 
109
        "Returns an ErrorDict for the data provided for the form"
 
110
        if self._errors is None:
 
111
            self.full_clean()
 
112
        return self._errors
 
113
    errors = property(_get_errors)
 
114
 
 
115
    def is_valid(self):
 
116
        """
 
117
        Returns True if the form has no errors. Otherwise, False. If errors are
 
118
        being ignored, returns False.
 
119
        """
 
120
        return self.is_bound and not bool(self.errors)
 
121
 
 
122
    def add_prefix(self, field_name):
 
123
        """
 
124
        Returns the field name with a prefix appended, if this Form has a
 
125
        prefix set.
 
126
 
 
127
        Subclasses may wish to override.
 
128
        """
 
129
        return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name
 
130
 
 
131
    def add_initial_prefix(self, field_name):
 
132
        """
 
133
        Add a 'initial' prefix for checking dynamic initial values
 
134
        """
 
135
        return u'initial-%s' % self.add_prefix(field_name)
 
136
 
 
137
    def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row):
 
138
        "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()."
 
139
        top_errors = self.non_field_errors() # Errors that should be displayed above all fields.
 
140
        output, hidden_fields = [], []
 
141
        for name, field in self.fields.items():
 
142
            bf = BoundField(self, field, name)
 
143
            bf_errors = self.error_class([escape(error) for error in bf.errors]) # Escape and cache in local variable.
 
144
            if bf.is_hidden:
 
145
                if bf_errors:
 
146
                    top_errors.extend([u'(Hidden field %s) %s' % (name, force_unicode(e)) for e in bf_errors])
 
147
                hidden_fields.append(unicode(bf))
 
148
            else:
 
149
                if errors_on_separate_row and bf_errors:
 
150
                    output.append(error_row % force_unicode(bf_errors))
 
151
                if bf.label:
 
152
                    label = escape(force_unicode(bf.label))
 
153
                    # Only add the suffix if the label does not end in
 
154
                    # punctuation.
 
155
                    if self.label_suffix:
 
156
                        if label[-1] not in ':?.!':
 
157
                            label += self.label_suffix
 
158
                    label = bf.label_tag(label) or ''
 
159
                else:
 
160
                    label = ''
 
161
                if field.help_text:
 
162
                    help_text = help_text_html % force_unicode(field.help_text)
 
163
                else:
 
164
                    help_text = u''
 
165
                output.append(normal_row % {'errors': force_unicode(bf_errors), 'label': force_unicode(label), 'field': unicode(bf), 'help_text': help_text})
 
166
        if top_errors:
 
167
            output.insert(0, error_row % force_unicode(top_errors))
 
168
        if hidden_fields: # Insert any hidden fields in the last row.
 
169
            str_hidden = u''.join(hidden_fields)
 
170
            if output:
 
171
                last_row = output[-1]
 
172
                # Chop off the trailing row_ender (e.g. '</td></tr>') and
 
173
                # insert the hidden fields.
 
174
                output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender
 
175
            else:
 
176
                # If there aren't any rows in the output, just append the
 
177
                # hidden fields.
 
178
                output.append(str_hidden)
 
179
        return mark_safe(u'\n'.join(output))
 
180
 
 
181
    def as_table(self):
 
182
        "Returns this form rendered as HTML <tr>s -- excluding the <table></table>."
 
183
        return self._html_output(u'<tr><th>%(label)s</th><td>%(errors)s%(field)s%(help_text)s</td></tr>', u'<tr><td colspan="2">%s</td></tr>', '</td></tr>', u'<br />%s', False)
 
184
 
 
185
    def as_ul(self):
 
186
        "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."
 
187
        return self._html_output(u'<li>%(errors)s%(label)s %(field)s%(help_text)s</li>', u'<li>%s</li>', '</li>', u' %s', False)
 
188
 
 
189
    def as_p(self):
 
190
        "Returns this form rendered as HTML <p>s."
 
191
        return self._html_output(u'<p>%(label)s %(field)s%(help_text)s</p>', u'%s', '</p>', u' %s', True)
 
192
 
 
193
    def non_field_errors(self):
 
194
        """
 
195
        Returns an ErrorList of errors that aren't associated with a particular
 
196
        field -- i.e., from Form.clean(). Returns an empty ErrorList if there
 
197
        are none.
 
198
        """
 
199
        return self.errors.get(NON_FIELD_ERRORS, self.error_class())
 
200
 
 
201
    def full_clean(self):
 
202
        """
 
203
        Cleans all of self.data and populates self._errors and
 
204
        self.cleaned_data.
 
205
        """
 
206
        self._errors = ErrorDict()
 
207
        if not self.is_bound: # Stop further processing.
 
208
            return
 
209
        self.cleaned_data = {}
 
210
        # If the form is permitted to be empty, and none of the form data has
 
211
        # changed from the initial data, short circuit any validation.
 
212
        if self.empty_permitted and not self.has_changed():
 
213
            return
 
214
        for name, field in self.fields.items():
 
215
            # value_from_datadict() gets the data from the data dictionaries.
 
216
            # Each widget type knows how to retrieve its own data, because some
 
217
            # widgets split data over several HTML fields.
 
218
            value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
 
219
            try:
 
220
                if isinstance(field, FileField):
 
221
                    initial = self.initial.get(name, field.initial)
 
222
                    value = field.clean(value, initial)
 
223
                else:
 
224
                    value = field.clean(value)
 
225
                self.cleaned_data[name] = value
 
226
                if hasattr(self, 'clean_%s' % name):
 
227
                    value = getattr(self, 'clean_%s' % name)()
 
228
                    self.cleaned_data[name] = value
 
229
            except ValidationError, e:
 
230
                self._errors[name] = e.messages
 
231
                if name in self.cleaned_data:
 
232
                    del self.cleaned_data[name]
 
233
        try:
 
234
            self.cleaned_data = self.clean()
 
235
        except ValidationError, e:
 
236
            self._errors[NON_FIELD_ERRORS] = e.messages
 
237
        if self._errors:
 
238
            delattr(self, 'cleaned_data')
 
239
 
 
240
    def clean(self):
 
241
        """
 
242
        Hook for doing any extra form-wide cleaning after Field.clean() been
 
243
        called on every field. Any ValidationError raised by this method will
 
244
        not be associated with a particular field; it will have a special-case
 
245
        association with the field named '__all__'.
 
246
        """
 
247
        return self.cleaned_data
 
248
 
 
249
    def has_changed(self):
 
250
        """
 
251
        Returns True if data differs from initial.
 
252
        """
 
253
        return bool(self.changed_data)
 
254
 
 
255
    def _get_changed_data(self):
 
256
        if self._changed_data is None:
 
257
            self._changed_data = []
 
258
            # XXX: For now we're asking the individual widgets whether or not the
 
259
            # data has changed. It would probably be more efficient to hash the
 
260
            # initial data, store it in a hidden field, and compare a hash of the
 
261
            # submitted data, but we'd need a way to easily get the string value
 
262
            # for a given field. Right now, that logic is embedded in the render
 
263
            # method of each widget.
 
264
            for name, field in self.fields.items():
 
265
                prefixed_name = self.add_prefix(name)
 
266
                data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
 
267
                if not field.show_hidden_initial:
 
268
                    initial_value = self.initial.get(name, field.initial)
 
269
                else:
 
270
                    initial_prefixed_name = self.add_initial_prefix(name)
 
271
                    hidden_widget = field.hidden_widget()
 
272
                    initial_value = hidden_widget.value_from_datadict(
 
273
                        self.data, self.files, initial_prefixed_name)
 
274
                if field.widget._has_changed(initial_value, data_value):
 
275
                    self._changed_data.append(name)
 
276
        return self._changed_data
 
277
    changed_data = property(_get_changed_data)
 
278
 
 
279
    def _get_media(self):
 
280
        """
 
281
        Provide a description of all media required to render the widgets on this form
 
282
        """
 
283
        media = Media()
 
284
        for field in self.fields.values():
 
285
            media = media + field.widget.media
 
286
        return media
 
287
    media = property(_get_media)
 
288
 
 
289
    def is_multipart(self):
 
290
        """
 
291
        Returns True if the form needs to be multipart-encrypted, i.e. it has
 
292
        FileInput. Otherwise, False.
 
293
        """
 
294
        for field in self.fields.values():
 
295
            if field.widget.needs_multipart_form:
 
296
                return True
 
297
        return False
 
298
 
 
299
class Form(BaseForm):
 
300
    "A collection of Fields, plus their associated data."
 
301
    # This is a separate class from BaseForm in order to abstract the way
 
302
    # self.fields is specified. This class (Form) is the one that does the
 
303
    # fancy metaclass stuff purely for the semantic sugar -- it allows one
 
304
    # to define a form using declarative syntax.
 
305
    # BaseForm itself has no way of designating self.fields.
 
306
    __metaclass__ = DeclarativeFieldsMetaclass
 
307
 
 
308
class BoundField(StrAndUnicode):
 
309
    "A Field plus data"
 
310
    def __init__(self, form, field, name):
 
311
        self.form = form
 
312
        self.field = field
 
313
        self.name = name
 
314
        self.html_name = form.add_prefix(name)
 
315
        self.html_initial_name = form.add_initial_prefix(name)
 
316
        if self.field.label is None:
 
317
            self.label = pretty_name(name)
 
318
        else:
 
319
            self.label = self.field.label
 
320
        self.help_text = field.help_text or ''
 
321
 
 
322
    def __unicode__(self):
 
323
        """Renders this field as an HTML widget."""
 
324
        if self.field.show_hidden_initial:
 
325
            return self.as_widget() + self.as_hidden(only_initial=True)
 
326
        return self.as_widget()
 
327
 
 
328
    def _errors(self):
 
329
        """
 
330
        Returns an ErrorList for this field. Returns an empty ErrorList
 
331
        if there are none.
 
332
        """
 
333
        return self.form.errors.get(self.name, self.form.error_class())
 
334
    errors = property(_errors)
 
335
 
 
336
    def as_widget(self, widget=None, attrs=None, only_initial=False):
 
337
        """
 
338
        Renders the field by rendering the passed widget, adding any HTML
 
339
        attributes passed as attrs.  If no widget is specified, then the
 
340
        field's default widget will be used.
 
341
        """
 
342
        if not widget:
 
343
            widget = self.field.widget
 
344
        attrs = attrs or {}
 
345
        auto_id = self.auto_id
 
346
        if auto_id and 'id' not in attrs and 'id' not in widget.attrs:
 
347
            attrs['id'] = auto_id
 
348
        if not self.form.is_bound:
 
349
            data = self.form.initial.get(self.name, self.field.initial)
 
350
            if callable(data):
 
351
                data = data()
 
352
        else:
 
353
            data = self.data
 
354
        if not only_initial:
 
355
            name = self.html_name
 
356
        else:
 
357
            name = self.html_initial_name
 
358
        return widget.render(name, data, attrs=attrs)
 
359
        
 
360
    def as_text(self, attrs=None, **kwargs):
 
361
        """
 
362
        Returns a string of HTML for representing this as an <input type="text">.
 
363
        """
 
364
        return self.as_widget(TextInput(), attrs, **kwargs)
 
365
 
 
366
    def as_textarea(self, attrs=None, **kwargs):
 
367
        "Returns a string of HTML for representing this as a <textarea>."
 
368
        return self.as_widget(Textarea(), attrs, **kwargs)
 
369
 
 
370
    def as_hidden(self, attrs=None, **kwargs):
 
371
        """
 
372
        Returns a string of HTML for representing this as an <input type="hidden">.
 
373
        """
 
374
        return self.as_widget(self.field.hidden_widget(), attrs, **kwargs)
 
375
 
 
376
    def _data(self):
 
377
        """
 
378
        Returns the data for this BoundField, or None if it wasn't given.
 
379
        """
 
380
        return self.field.widget.value_from_datadict(self.form.data, self.form.files, self.html_name)
 
381
    data = property(_data)
 
382
 
 
383
    def label_tag(self, contents=None, attrs=None):
 
384
        """
 
385
        Wraps the given contents in a <label>, if the field has an ID attribute.
 
386
        Does not HTML-escape the contents. If contents aren't given, uses the
 
387
        field's HTML-escaped label.
 
388
 
 
389
        If attrs are given, they're used as HTML attributes on the <label> tag.
 
390
        """
 
391
        contents = contents or escape(self.label)
 
392
        widget = self.field.widget
 
393
        id_ = widget.attrs.get('id') or self.auto_id
 
394
        if id_:
 
395
            attrs = attrs and flatatt(attrs) or ''
 
396
            contents = u'<label for="%s"%s>%s</label>' % (widget.id_for_label(id_), attrs, unicode(contents))
 
397
        return mark_safe(contents)
 
398
 
 
399
    def _is_hidden(self):
 
400
        "Returns True if this BoundField's widget is hidden."
 
401
        return self.field.widget.is_hidden
 
402
    is_hidden = property(_is_hidden)
 
403
 
 
404
    def _auto_id(self):
 
405
        """
 
406
        Calculates and returns the ID attribute for this BoundField, if the
 
407
        associated Form has specified auto_id. Returns an empty string otherwise.
 
408
        """
 
409
        auto_id = self.form.auto_id
 
410
        if auto_id and '%s' in smart_unicode(auto_id):
 
411
            return smart_unicode(auto_id) % self.html_name
 
412
        elif auto_id:
 
413
            return self.html_name
 
414
        return ''
 
415
    auto_id = property(_auto_id)