Ticket #4511: forms.py

File forms.py, 13.1 KB (added by gustavo@…, 17 years ago)

newforms/forms.py changed to have ID attribute in <p>, <ul> and <tr>

Line 
1"""
2Form classes
3"""
4
5import copy
6
7from django.utils.datastructures import SortedDict
8from django.utils.html import escape
9from django.utils.encoding import StrAndUnicode
10
11from fields import Field
12from widgets import TextInput, Textarea
13from util import flatatt, ErrorDict, ErrorList, ValidationError
14
15__all__ = ('BaseForm', 'Form')
16
17NON_FIELD_ERRORS = '__all__'
18
19def pretty_name(name):
20 "Converts 'first_name' to 'First name'"
21 name = name[0].upper() + name[1:]
22 return name.replace('_', ' ')
23
24class 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
36class 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
55class 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
212class 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
221class 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)
Back to Top