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