Django

Code

root/django/branches/newforms-admin/django/newforms/forms.py

Revision 7853, 16.0 kB (checked in by brosner, 5 months ago)

newforms-admin: Merged from trunk up to [7852].

  • Property svn:eol-style set to native
Line 
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 _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row):
132         "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()."
133         top_errors = self.non_field_errors() # Errors that should be displayed above all fields.
134         output, hidden_fields = [], []
135         for name, field in self.fields.items():
136             bf = BoundField(self, field, name)
137             bf_errors = self.error_class([escape(error) for error in bf.errors]) # Escape and cache in local variable.
138             if bf.is_hidden:
139                 if bf_errors:
140                     top_errors.extend([u'(Hidden field %s) %s' % (name, force_unicode(e)) for e in bf_errors])
141                 hidden_fields.append(unicode(bf))
142             else:
143                 if errors_on_separate_row and bf_errors:
144                     output.append(error_row % force_unicode(bf_errors))
145                 if bf.label:
146                     label = escape(force_unicode(bf.label))
147                     # Only add the suffix if the label does not end in
148                     # punctuation.
149                     if self.label_suffix:
150                         if label[-1] not in ':?.!':
151                             label += self.label_suffix
152                     label = bf.label_tag(label) or ''
153                 else:
154                     label = ''
155                 if field.help_text:
156                     help_text = help_text_html % force_unicode(field.help_text)
157                 else:
158                     help_text = u''
159                 output.append(normal_row % {'errors': force_unicode(bf_errors), 'label': force_unicode(label), 'field': unicode(bf), 'help_text': help_text})
160         if top_errors:
161             output.insert(0, error_row % force_unicode(top_errors))
162         if hidden_fields: # Insert any hidden fields in the last row.
163             str_hidden = u''.join(hidden_fields)
164             if output:
165                 last_row = output[-1]
166                 # Chop off the trailing row_ender (e.g. '</td></tr>') and
167                 # insert the hidden fields.
168                 output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender
169             else:
170                 # If there aren't any rows in the output, just append the
171                 # hidden fields.
172                 output.append(str_hidden)
173         return mark_safe(u'\n'.join(output))
174
175     def as_table(self):
176         "Returns this form rendered as HTML <tr>s -- excluding the <table></table>."
177         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)
178
179     def as_ul(self):
180         "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."
181         return self._html_output(u'<li>%(errors)s%(label)s %(field)s%(help_text)s</li>', u'<li>%s</li>', '</li>', u' %s', False)
182
183     def as_p(self):
184         "Returns this form rendered as HTML <p>s."
185         return self._html_output(u'<p>%(label)s %(field)s%(help_text)s</p>', u'%s', '</p>', u' %s', True)
186
187     def non_field_errors(self):
188         """
189         Returns an ErrorList of errors that aren't associated with a particular
190         field -- i.e., from Form.clean(). Returns an empty ErrorList if there
191         are none.
192         """
193         return self.errors.get(NON_FIELD_ERRORS, self.error_class())
194
195     def full_clean(self):
196         """
197         Cleans all of self.data and populates self._errors and
198         self.cleaned_data.
199         """
200         self._errors = ErrorDict()
201         if not self.is_bound: # Stop further processing.
202             return
203         self.cleaned_data = {}
204         # If the form is permitted to be empty, and none of the form data has
205         # changed from the initial data, short circuit any validation.
206         if self.empty_permitted and not self.has_changed():
207             return
208         for name, field in self.fields.items():
209             # value_from_datadict() gets the data from the data dictionaries.
210             # Each widget type knows how to retrieve its own data, because some
211             # widgets split data over several HTML fields.
212             value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
213             try:
214                 if isinstance(field, FileField):
215                     initial = self.initial.get(name, field.initial)
216                     value = field.clean(value, initial)
217                 else:
218                     value = field.clean(value)
219                 self.cleaned_data[name] = value
220                 if hasattr(self, 'clean_%s' % name):
221                     value = getattr(self, 'clean_%s' % name)()
222                     self.cleaned_data[name] = value
223             except ValidationError, e:
224                 self._errors[name] = e.messages
225                 if name in self.cleaned_data:
226                     del self.cleaned_data[name]
227         try:
228             self.cleaned_data = self.clean()
229         except ValidationError, e:
230             self._errors[NON_FIELD_ERRORS] = e.messages
231         if self._errors:
232             delattr(self, 'cleaned_data')
233
234     def clean(self):
235         """
236         Hook for doing any extra form-wide cleaning after Field.clean() been
237         called on every field. Any ValidationError raised by this method will
238         not be associated with a particular field; it will have a special-case
239         association with the field named '__all__'.
240         """
241         return self.cleaned_data
242
243     def has_changed(self):
244         """
245         Returns True if data differs from initial.
246         """
247         return bool(self.changed_data)
248    
249     def _get_changed_data(self):
250         if self._changed_data is None:
251             self._changed_data = []
252             # XXX: For now we're asking the individual widgets whether or not the
253             # data has changed. It would probably be more efficient to hash the
254             # initial data, store it in a hidden field, and compare a hash of the
255             # submitted data, but we'd need a way to easily get the string value
256             # for a given field. Right now, that logic is embedded in the render
257             # method of each widget.
258             for name, field in self.fields.items():
259                 prefixed_name = self.add_prefix(name)
260                 data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
261                 initial_value = self.initial.get(name, field.initial)
262                 if field.widget._has_changed(initial_value, data_value):
263                     self._changed_data.append(name)
264         return self._changed_data
265     changed_data = property(_get_changed_data)
266
267     def _get_media(self):
268         """
269         Provide a description of all media required to render the widgets on this form
270         """
271         media = Media()
272         for field in self.fields.values():
273             media = media + field.widget.media
274         return media
275     media = property(_get_media)
276
277     def is_multipart(self):
278         """
279         Returns True if the form needs to be multipart-encrypted, i.e. it has
280         FileInput. Otherwise, False.
281         """
282         for field in self.fields.values():
283             if field.widget.needs_multipart_form:
284                 return True
285         return False
286
287 class Form(BaseForm):
288     "A collection of Fields, plus their associated data."
289     # This is a separate class from BaseForm in order to abstract the way
290     # self.fields is specified. This class (Form) is the one that does the
291     # fancy metaclass stuff purely for the semantic sugar -- it allows one
292     # to define a form using declarative syntax.
293     # BaseForm itself has no way of designating self.fields.
294     __metaclass__ = DeclarativeFieldsMetaclass
295
296 class BoundField(StrAndUnicode):
297     "A Field plus data"
298     def __init__(self, form, field, name):
299         self.form = form
300         self.field = field
301         self.name = name
302         self.html_name = form.add_prefix(name)
303         if self.field.label is None:
304             self.label = pretty_name(name)
305         else:
306             self.label = self.field.label
307         self.help_text = field.help_text or ''
308
309     def __unicode__(self):
310         """Renders this field as an HTML widget."""
311         return self.as_widget()
312
313     def _errors(self):
314         """
315         Returns an ErrorList for this field. Returns an empty ErrorList
316         if there are none.
317         """
318         return self.form.errors.get(self.name, self.form.error_class())
319     errors = property(_errors)
320
321     def as_widget(self, widget=None, attrs=None):
322         """
323         Renders the field by rendering the passed widget, adding any HTML
324         attributes passed as attrs.  If no widget is specified, then the
325         field's default widget will be used.
326         """
327         if not widget:
328             widget = self.field.widget
329         attrs = attrs or {}
330         auto_id = self.auto_id
331         if auto_id and 'id' not in attrs and 'id' not in widget.attrs:
332             attrs['id'] = auto_id
333         if not self.form.is_bound:
334             data = self.form.initial.get(self.name, self.field.initial)
335             if callable(data):
336                 data = data()
337         else:
338             data = self.data
339         return widget.render(self.html_name, data, attrs=attrs)
340
341     def as_text(self, attrs=None):
342         """
343         Returns a string of HTML for representing this as an <input type="text">.
344         """
345         return self.as_widget(TextInput(), attrs)
346
347     def as_textarea(self, attrs=None):
348         "Returns a string of HTML for representing this as a <textarea>."
349         return self.as_widget(Textarea(), attrs)
350
351     def as_hidden(self, attrs=None):
352         """
353         Returns a string of HTML for representing this as an <input type="hidden">.
354         """
355         return self.as_widget(self.field.hidden_widget(), attrs)
356
357     def _data(self):
358         """
359         Returns the data for this BoundField, or None if it wasn't given.
360         """
361         return self.field.widget.value_from_datadict(self.form.data, self.form.files, self.html_name)
362     data = property(_data)
363
364     def label_tag(self, contents=None, attrs=None):
365         """
366         Wraps the given contents in a <label>, if the field has an ID attribute.
367         Does not HTML-escape the contents. If contents aren't given, uses the
368         field's HTML-escaped label.
369
370         If attrs are given, they're used as HTML attributes on the <label> tag.
371         """
372         contents = contents or escape(self.label)
373         widget = self.field.widget
374         id_ = widget.attrs.get('id') or self.auto_id
375         if id_:
376             attrs = attrs and flatatt(attrs) or ''
377             contents = '<label for="%s"%s>%s</label>' % (widget.id_for_label(id_), attrs, contents)
378         return mark_safe(contents)
379
380     def _is_hidden(self):
381         "Returns True if this BoundField's widget is hidden."
382         return self.field.widget.is_hidden
383     is_hidden = property(_is_hidden)
384
385     def _auto_id(self):
386         """
387         Calculates and returns the ID attribute for this BoundField, if the
388         associated Form has specified auto_id. Returns an empty string otherwise.
389         """
390         auto_id = self.form.auto_id
391         if auto_id and '%s' in smart_unicode(auto_id):
392             return smart_unicode(auto_id) % self.html_name
393         elif auto_id:
394             return self.html_name
395         return ''
396     auto_id = property(_auto_id)
Note: See TracBrowser for help on using the browser.