1 | """
|
---|
2 | Form classes
|
---|
3 | """
|
---|
4 |
|
---|
5 | import copy
|
---|
6 |
|
---|
7 | from django.utils.datastructures import SortedDict
|
---|
8 | from django.utils.html import escape
|
---|
9 | from django.utils.encoding import StrAndUnicode
|
---|
10 |
|
---|
11 | from fields import Field
|
---|
12 | from widgets import TextInput, Textarea
|
---|
13 | from util import flatatt, ErrorDict, ErrorList, ValidationError
|
---|
14 |
|
---|
15 | __all__ = ('BaseForm', 'Form')
|
---|
16 |
|
---|
17 | NON_FIELD_ERRORS = '__all__'
|
---|
18 |
|
---|
19 | def pretty_name(name):
|
---|
20 | "Converts 'first_name' to 'First name'"
|
---|
21 | name = name[0].upper() + name[1:]
|
---|
22 | return name.replace('_', ' ')
|
---|
23 |
|
---|
24 | class SortedDictFromList(SortedDict):
|
---|
25 | "A dictionary that keeps its keys in the order in which they're inserted."
|
---|
26 | # This is different than django.utils.datastructures.SortedDict, because
|
---|
27 | # this takes a list/tuple as the argument to __init__().
|
---|
28 | def __init__(self, data=None):
|
---|
29 | if data is None: data = []
|
---|
30 | self.keyOrder = [d[0] for d in data]
|
---|
31 | dict.__init__(self, dict(data))
|
---|
32 |
|
---|
33 | def copy(self):
|
---|
34 | return SortedDictFromList([(k, copy.copy(v)) for k, v in self.items()])
|
---|
35 |
|
---|
36 | class DeclarativeFieldsMetaclass(type):
|
---|
37 | """
|
---|
38 | Metaclass that converts Field attributes to a dictionary called
|
---|
39 | 'base_fields', taking into account parent class 'base_fields' as well.
|
---|
40 | """
|
---|
41 | def __new__(cls, name, bases, attrs):
|
---|
42 | fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
|
---|
43 | fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
|
---|
44 |
|
---|
45 | # If this class is subclassing another Form, add that Form's fields.
|
---|
46 | # Note that we loop over the bases in *reverse*. This is necessary in
|
---|
47 | # order to preserve the correct order of fields.
|
---|
48 | for base in bases[::-1]:
|
---|
49 | if hasattr(base, 'base_fields'):
|
---|
50 | fields = base.base_fields.items() + fields
|
---|
51 |
|
---|
52 | attrs['base_fields'] = SortedDictFromList(fields)
|
---|
53 | return type.__new__(cls, name, bases, attrs)
|
---|
54 |
|
---|
55 | class BaseForm(StrAndUnicode):
|
---|
56 | # This is the main implementation of all the Form logic. Note that this
|
---|
57 | # class is different than Form. See the comments by the Form class for more
|
---|
58 | # information. Any improvements to the form API should be made to *this*
|
---|
59 | # class, not to the Form class.
|
---|
60 | def __init__(self, data=None, auto_id='id_%s', prefix=None, initial=None):
|
---|
61 | self.is_bound = data is not None
|
---|
62 | self.data = data or {}
|
---|
63 | self.auto_id = auto_id
|
---|
64 | self.prefix = prefix
|
---|
65 | self.initial = initial or {}
|
---|
66 | self._errors = None # Stores the errors after clean() has been called.
|
---|
67 |
|
---|
68 | # The base_fields class attribute is the *class-wide* definition of
|
---|
69 | # fields. Because a particular *instance* of the class might want to
|
---|
70 | # alter self.fields, we create self.fields here by copying base_fields.
|
---|
71 | # Instances should always modify self.fields; they should not modify
|
---|
72 | # self.base_fields.
|
---|
73 | self.fields = self.base_fields.copy()
|
---|
74 |
|
---|
75 | def __unicode__(self):
|
---|
76 | return self.as_table()
|
---|
77 |
|
---|
78 | def __iter__(self):
|
---|
79 | for name, field in self.fields.items():
|
---|
80 | yield BoundField(self, field, name)
|
---|
81 |
|
---|
82 | def __getitem__(self, name):
|
---|
83 | "Returns a BoundField with the given name."
|
---|
84 | try:
|
---|
85 | field = self.fields[name]
|
---|
86 | except KeyError:
|
---|
87 | raise KeyError('Key %r not found in Form' % name)
|
---|
88 | return BoundField(self, field, name)
|
---|
89 |
|
---|
90 | def _get_errors(self):
|
---|
91 | "Returns an ErrorDict for self.data"
|
---|
92 | if self._errors is None:
|
---|
93 | self.full_clean()
|
---|
94 | return self._errors
|
---|
95 | errors = property(_get_errors)
|
---|
96 |
|
---|
97 | def is_valid(self):
|
---|
98 | """
|
---|
99 | Returns True if the form has no errors. Otherwise, False. If errors are
|
---|
100 | being ignored, returns False.
|
---|
101 | """
|
---|
102 | return self.is_bound and not bool(self.errors)
|
---|
103 |
|
---|
104 | def add_prefix(self, field_name):
|
---|
105 | """
|
---|
106 | Returns the field name with a prefix appended, if this Form has a
|
---|
107 | prefix set.
|
---|
108 |
|
---|
109 | Subclasses may wish to override.
|
---|
110 | """
|
---|
111 | return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name
|
---|
112 |
|
---|
113 | def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row):
|
---|
114 | "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()."
|
---|
115 | top_errors = self.non_field_errors() # Errors that should be displayed above all fields.
|
---|
116 | output, hidden_fields = [], []
|
---|
117 | for name, field in self.fields.items():
|
---|
118 | bf = BoundField(self, field, name)
|
---|
119 | bf_errors = ErrorList([escape(error) for error in bf.errors]) # Escape and cache in local variable.
|
---|
120 | if bf.is_hidden:
|
---|
121 | if bf_errors:
|
---|
122 | top_errors.extend(['(Hidden field %s) %s' % (name, e) for e in bf_errors])
|
---|
123 | hidden_fields.append(unicode(bf))
|
---|
124 | else:
|
---|
125 | if errors_on_separate_row and bf_errors:
|
---|
126 | output.append(error_row % bf_errors)
|
---|
127 | if bf.label:
|
---|
128 | label = escape(bf.label)
|
---|
129 | # Only add a colon if the label does not end in punctuation.
|
---|
130 | if label[-1] not in ':?.!':
|
---|
131 | label += ':'
|
---|
132 | label = bf.label_tag(label) or ''
|
---|
133 | else:
|
---|
134 | label = ''
|
---|
135 | if field.help_text:
|
---|
136 | help_text = help_text_html % field.help_text
|
---|
137 | else:
|
---|
138 | help_text = u''
|
---|
139 | output.append(normal_row % {'name': name, 'errors': bf_errors, 'label': label, 'field': unicode(bf), 'help_text': help_text})
|
---|
140 | if top_errors:
|
---|
141 | output.insert(0, error_row % top_errors)
|
---|
142 | if hidden_fields: # Insert any hidden fields in the last row.
|
---|
143 | str_hidden = u''.join(hidden_fields)
|
---|
144 | if output:
|
---|
145 | last_row = output[-1]
|
---|
146 | # Chop off the trailing row_ender (e.g. '</td></tr>') and insert the hidden fields.
|
---|
147 | output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender
|
---|
148 | else: # If there aren't any rows in the output, just append the hidden fields.
|
---|
149 | output.append(str_hidden)
|
---|
150 | return u'\n'.join(output)
|
---|
151 |
|
---|
152 | def as_table(self):
|
---|
153 | "Returns this form rendered as HTML <tr>s -- excluding the <table></table>."
|
---|
154 | return self._html_output(u'<tr id="%(name)s><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)
|
---|
155 |
|
---|
156 | def as_ul(self):
|
---|
157 | "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."
|
---|
158 | return self._html_output(u'<li id="%(name)s">%(errors)s%(label)s %(field)s%(help_text)s</li>', u'<li>%s</li>', '</li>', u' %s', False)
|
---|
159 |
|
---|
160 | def as_p(self):
|
---|
161 | "Returns this form rendered as HTML <p>s."
|
---|
162 | return self._html_output(u'<p id="%(name)s>%(label)s %(field)s%(help_text)s</p>', u'<p>%s</p>', '</p>', u' %s', True)
|
---|
163 |
|
---|
164 | def non_field_errors(self):
|
---|
165 | """
|
---|
166 | Returns an ErrorList of errors that aren't associated with a particular
|
---|
167 | field -- i.e., from Form.clean(). Returns an empty ErrorList if there
|
---|
168 | are none.
|
---|
169 | """
|
---|
170 | return self.errors.get(NON_FIELD_ERRORS, ErrorList())
|
---|
171 |
|
---|
172 | def full_clean(self):
|
---|
173 | """
|
---|
174 | Cleans all of self.data and populates self._errors and
|
---|
175 | self.cleaned_data.
|
---|
176 | """
|
---|
177 | self._errors = ErrorDict()
|
---|
178 | if not self.is_bound: # Stop further processing.
|
---|
179 | return
|
---|
180 | self.cleaned_data = {}
|
---|
181 | for name, field in self.fields.items():
|
---|
182 | # value_from_datadict() gets the data from the dictionary.
|
---|
183 | # Each widget type knows how to retrieve its own data, because some
|
---|
184 | # widgets split data over several HTML fields.
|
---|
185 | value = field.widget.value_from_datadict(self.data, self.add_prefix(name))
|
---|
186 | try:
|
---|
187 | value = field.clean(value)
|
---|
188 | self.cleaned_data[name] = value
|
---|
189 | if hasattr(self, 'clean_%s' % name):
|
---|
190 | value = getattr(self, 'clean_%s' % name)()
|
---|
191 | self.cleaned_data[name] = value
|
---|
192 | except ValidationError, e:
|
---|
193 | self._errors[name] = e.messages
|
---|
194 | if name in self.cleaned_data:
|
---|
195 | del self.cleaned_data[name]
|
---|
196 | try:
|
---|
197 | self.cleaned_data = self.clean()
|
---|
198 | except ValidationError, e:
|
---|
199 | self._errors[NON_FIELD_ERRORS] = e.messages
|
---|
200 | if self._errors:
|
---|
201 | delattr(self, 'cleaned_data')
|
---|
202 |
|
---|
203 | def clean(self):
|
---|
204 | """
|
---|
205 | Hook for doing any extra form-wide cleaning after Field.clean() been
|
---|
206 | called on every field. Any ValidationError raised by this method will
|
---|
207 | not be associated with a particular field; it will have a special-case
|
---|
208 | association with the field named '__all__'.
|
---|
209 | """
|
---|
210 | return self.cleaned_data
|
---|
211 |
|
---|
212 | class Form(BaseForm):
|
---|
213 | "A collection of Fields, plus their associated data."
|
---|
214 | # This is a separate class from BaseForm in order to abstract the way
|
---|
215 | # self.fields is specified. This class (Form) is the one that does the
|
---|
216 | # fancy metaclass stuff purely for the semantic sugar -- it allows one
|
---|
217 | # to define a form using declarative syntax.
|
---|
218 | # BaseForm itself has no way of designating self.fields.
|
---|
219 | __metaclass__ = DeclarativeFieldsMetaclass
|
---|
220 |
|
---|
221 | class BoundField(StrAndUnicode):
|
---|
222 | "A Field plus data"
|
---|
223 | def __init__(self, form, field, name):
|
---|
224 | self.form = form
|
---|
225 | self.field = field
|
---|
226 | self.name = name
|
---|
227 | self.html_name = form.add_prefix(name)
|
---|
228 | if self.field.label is None:
|
---|
229 | self.label = pretty_name(name)
|
---|
230 | else:
|
---|
231 | self.label = self.field.label
|
---|
232 | self.help_text = field.help_text or ''
|
---|
233 |
|
---|
234 | def __unicode__(self):
|
---|
235 | "Renders this field as an HTML widget."
|
---|
236 | # Use the 'widget' attribute on the field to determine which type
|
---|
237 | # of HTML widget to use.
|
---|
238 | value = self.as_widget(self.field.widget)
|
---|
239 | if not isinstance(value, basestring):
|
---|
240 | # Some Widget render() methods -- notably RadioSelect -- return a
|
---|
241 | # "special" object rather than a string. Call __unicode__() on that
|
---|
242 | # object to get its rendered value.
|
---|
243 | value = unicode(value)
|
---|
244 | return value
|
---|
245 |
|
---|
246 | def _errors(self):
|
---|
247 | """
|
---|
248 | Returns an ErrorList for this field. Returns an empty ErrorList
|
---|
249 | if there are none.
|
---|
250 | """
|
---|
251 | return self.form.errors.get(self.name, ErrorList())
|
---|
252 | errors = property(_errors)
|
---|
253 |
|
---|
254 | def as_widget(self, widget, attrs=None):
|
---|
255 | attrs = attrs or {}
|
---|
256 | auto_id = self.auto_id
|
---|
257 | if auto_id and 'id' not in attrs and 'id' not in widget.attrs:
|
---|
258 | attrs['id'] = auto_id
|
---|
259 | if not self.form.is_bound:
|
---|
260 | data = self.form.initial.get(self.name, self.field.initial)
|
---|
261 | if callable(data):
|
---|
262 | data = data()
|
---|
263 | else:
|
---|
264 | data = self.data
|
---|
265 | return widget.render(self.html_name, data, attrs=attrs)
|
---|
266 |
|
---|
267 | def as_text(self, attrs=None):
|
---|
268 | """
|
---|
269 | Returns a string of HTML for representing this as an <input type="text">.
|
---|
270 | """
|
---|
271 | return self.as_widget(TextInput(), attrs)
|
---|
272 |
|
---|
273 | def as_textarea(self, attrs=None):
|
---|
274 | "Returns a string of HTML for representing this as a <textarea>."
|
---|
275 | return self.as_widget(Textarea(), attrs)
|
---|
276 |
|
---|
277 | def as_hidden(self, attrs=None):
|
---|
278 | """
|
---|
279 | Returns a string of HTML for representing this as an <input type="hidden">.
|
---|
280 | """
|
---|
281 | return self.as_widget(self.field.hidden_widget(), attrs)
|
---|
282 |
|
---|
283 | def _data(self):
|
---|
284 | """
|
---|
285 | Returns the data for this BoundField, or None if it wasn't given.
|
---|
286 | """
|
---|
287 | return self.field.widget.value_from_datadict(self.form.data, self.html_name)
|
---|
288 | data = property(_data)
|
---|
289 |
|
---|
290 | def label_tag(self, contents=None, attrs=None):
|
---|
291 | """
|
---|
292 | Wraps the given contents in a <label>, if the field has an ID attribute.
|
---|
293 | Does not HTML-escape the contents. If contents aren't given, uses the
|
---|
294 | field's HTML-escaped label.
|
---|
295 |
|
---|
296 | If attrs are given, they're used as HTML attributes on the <label> tag.
|
---|
297 | """
|
---|
298 | contents = contents or escape(self.label)
|
---|
299 | widget = self.field.widget
|
---|
300 | id_ = widget.attrs.get('id') or self.auto_id
|
---|
301 | if id_:
|
---|
302 | attrs = attrs and flatatt(attrs) or ''
|
---|
303 | contents = '<label for="%s"%s>%s</label>' % (widget.id_for_label(id_), attrs, contents)
|
---|
304 | return contents
|
---|
305 |
|
---|
306 | def _is_hidden(self):
|
---|
307 | "Returns True if this BoundField's widget is hidden."
|
---|
308 | return self.field.widget.is_hidden
|
---|
309 | is_hidden = property(_is_hidden)
|
---|
310 |
|
---|
311 | def _auto_id(self):
|
---|
312 | """
|
---|
313 | Calculates and returns the ID attribute for this BoundField, if the
|
---|
314 | associated Form has specified auto_id. Returns an empty string otherwise.
|
---|
315 | """
|
---|
316 | auto_id = self.form.auto_id
|
---|
317 | if auto_id and '%s' in str(auto_id):
|
---|
318 | return str(auto_id) % self.html_name
|
---|
319 | elif auto_id:
|
---|
320 | return self.html_name
|
---|
321 | return ''
|
---|
322 | auto_id = property(_auto_id)
|
---|