|
1 from django.core import validators |
|
2 from django.core.exceptions import PermissionDenied |
|
3 from django.utils.html import escape |
|
4 from django.utils.safestring import mark_safe |
|
5 from django.conf import settings |
|
6 from django.utils.translation import ugettext, ungettext |
|
7 from django.utils.encoding import smart_unicode, force_unicode |
|
8 from django.utils.maxlength import LegacyMaxlength |
|
9 |
|
10 FORM_FIELD_ID_PREFIX = 'id_' |
|
11 |
|
12 class EmptyValue(Exception): |
|
13 "This is raised when empty data is provided" |
|
14 pass |
|
15 |
|
16 class Manipulator(object): |
|
17 # List of permission strings. User must have at least one to manipulate. |
|
18 # None means everybody has permission. |
|
19 required_permission = '' |
|
20 |
|
21 def __init__(self): |
|
22 # List of FormField objects |
|
23 self.fields = [] |
|
24 |
|
25 def __getitem__(self, field_name): |
|
26 "Looks up field by field name; raises KeyError on failure" |
|
27 for field in self.fields: |
|
28 if field.field_name == field_name: |
|
29 return field |
|
30 raise KeyError, "Field %s not found\n%s" % (field_name, repr(self.fields)) |
|
31 |
|
32 def __delitem__(self, field_name): |
|
33 "Deletes the field with the given field name; raises KeyError on failure" |
|
34 for i, field in enumerate(self.fields): |
|
35 if field.field_name == field_name: |
|
36 del self.fields[i] |
|
37 return |
|
38 raise KeyError, "Field %s not found" % field_name |
|
39 |
|
40 def check_permissions(self, user): |
|
41 """Confirms user has required permissions to use this manipulator; raises |
|
42 PermissionDenied on failure.""" |
|
43 if self.required_permission is None: |
|
44 return |
|
45 if user.has_perm(self.required_permission): |
|
46 return |
|
47 raise PermissionDenied |
|
48 |
|
49 def prepare(self, new_data): |
|
50 """ |
|
51 Makes any necessary preparations to new_data, in place, before data has |
|
52 been validated. |
|
53 """ |
|
54 for field in self.fields: |
|
55 field.prepare(new_data) |
|
56 |
|
57 def get_validation_errors(self, new_data): |
|
58 "Returns dictionary mapping field_names to error-message lists" |
|
59 errors = {} |
|
60 self.prepare(new_data) |
|
61 for field in self.fields: |
|
62 errors.update(field.get_validation_errors(new_data)) |
|
63 val_name = 'validate_%s' % field.field_name |
|
64 if hasattr(self, val_name): |
|
65 val = getattr(self, val_name) |
|
66 try: |
|
67 field.run_validator(new_data, val) |
|
68 except (validators.ValidationError, validators.CriticalValidationError), e: |
|
69 errors.setdefault(field.field_name, []).extend(e.messages) |
|
70 |
|
71 # if field.is_required and not new_data.get(field.field_name, False): |
|
72 # errors.setdefault(field.field_name, []).append(ugettext_lazy('This field is required.')) |
|
73 # continue |
|
74 # try: |
|
75 # validator_list = field.validator_list |
|
76 # if hasattr(self, 'validate_%s' % field.field_name): |
|
77 # validator_list.append(getattr(self, 'validate_%s' % field.field_name)) |
|
78 # for validator in validator_list: |
|
79 # if field.is_required or new_data.get(field.field_name, False) or hasattr(validator, 'always_test'): |
|
80 # try: |
|
81 # if hasattr(field, 'requires_data_list'): |
|
82 # validator(new_data.getlist(field.field_name), new_data) |
|
83 # else: |
|
84 # validator(new_data.get(field.field_name, ''), new_data) |
|
85 # except validators.ValidationError, e: |
|
86 # errors.setdefault(field.field_name, []).extend(e.messages) |
|
87 # # If a CriticalValidationError is raised, ignore any other ValidationErrors |
|
88 # # for this particular field |
|
89 # except validators.CriticalValidationError, e: |
|
90 # errors.setdefault(field.field_name, []).extend(e.messages) |
|
91 return errors |
|
92 |
|
93 def save(self, new_data): |
|
94 "Saves the changes and returns the new object" |
|
95 # changes is a dictionary-like object keyed by field_name |
|
96 raise NotImplementedError |
|
97 |
|
98 def do_html2python(self, new_data): |
|
99 """ |
|
100 Convert the data from HTML data types to Python datatypes, changing the |
|
101 object in place. This happens after validation but before storage. This |
|
102 must happen after validation because html2python functions aren't |
|
103 expected to deal with invalid input. |
|
104 """ |
|
105 for field in self.fields: |
|
106 field.convert_post_data(new_data) |
|
107 |
|
108 class FormWrapper(object): |
|
109 """ |
|
110 A wrapper linking a Manipulator to the template system. |
|
111 This allows dictionary-style lookups of formfields. It also handles feeding |
|
112 prepopulated data and validation error messages to the formfield objects. |
|
113 """ |
|
114 def __init__(self, manipulator, data=None, error_dict=None, edit_inline=True): |
|
115 self.manipulator = manipulator |
|
116 if data is None: |
|
117 data = {} |
|
118 if error_dict is None: |
|
119 error_dict = {} |
|
120 self.data = data |
|
121 self.error_dict = error_dict |
|
122 self._inline_collections = None |
|
123 self.edit_inline = edit_inline |
|
124 |
|
125 def __repr__(self): |
|
126 return repr(self.__dict__) |
|
127 |
|
128 def __getitem__(self, key): |
|
129 for field in self.manipulator.fields: |
|
130 if field.field_name == key: |
|
131 data = field.extract_data(self.data) |
|
132 return FormFieldWrapper(field, data, self.error_dict.get(field.field_name, [])) |
|
133 if self.edit_inline: |
|
134 self.fill_inline_collections() |
|
135 for inline_collection in self._inline_collections: |
|
136 # The 'orig_name' comparison is for backwards compatibility |
|
137 # with hand-crafted forms. |
|
138 if inline_collection.name == key or (':' not in key and inline_collection.orig_name == key): |
|
139 return inline_collection |
|
140 raise KeyError, "Could not find Formfield or InlineObjectCollection named %r" % key |
|
141 |
|
142 def fill_inline_collections(self): |
|
143 if not self._inline_collections: |
|
144 ic = [] |
|
145 related_objects = self.manipulator.get_related_objects() |
|
146 for rel_obj in related_objects: |
|
147 data = rel_obj.extract_data(self.data) |
|
148 inline_collection = InlineObjectCollection(self.manipulator, rel_obj, data, self.error_dict) |
|
149 ic.append(inline_collection) |
|
150 self._inline_collections = ic |
|
151 |
|
152 def has_errors(self): |
|
153 return self.error_dict != {} |
|
154 |
|
155 def _get_fields(self): |
|
156 try: |
|
157 return self._fields |
|
158 except AttributeError: |
|
159 self._fields = [self.__getitem__(field.field_name) for field in self.manipulator.fields] |
|
160 return self._fields |
|
161 |
|
162 fields = property(_get_fields) |
|
163 |
|
164 class FormFieldWrapper(object): |
|
165 "A bridge between the template system and an individual form field. Used by FormWrapper." |
|
166 def __init__(self, formfield, data, error_list): |
|
167 self.formfield, self.data, self.error_list = formfield, data, error_list |
|
168 self.field_name = self.formfield.field_name # for convenience in templates |
|
169 |
|
170 def __str__(self): |
|
171 "Renders the field" |
|
172 return unicode(self).encode('utf-8') |
|
173 |
|
174 def __unicode__(self): |
|
175 "Renders the field" |
|
176 return force_unicode(self.formfield.render(self.data)) |
|
177 |
|
178 def __repr__(self): |
|
179 return '<FormFieldWrapper for "%s">' % self.formfield.field_name |
|
180 |
|
181 def field_list(self): |
|
182 """ |
|
183 Like __str__(), but returns a list. Use this when the field's render() |
|
184 method returns a list. |
|
185 """ |
|
186 return self.formfield.render(self.data) |
|
187 |
|
188 def errors(self): |
|
189 return self.error_list |
|
190 |
|
191 def html_error_list(self): |
|
192 if self.errors(): |
|
193 return mark_safe('<ul class="errorlist"><li>%s</li></ul>' % '</li><li>'.join([escape(e) for e in self.errors()])) |
|
194 else: |
|
195 return mark_safe('') |
|
196 |
|
197 def get_id(self): |
|
198 return self.formfield.get_id() |
|
199 |
|
200 class FormFieldCollection(FormFieldWrapper): |
|
201 "A utility class that gives the template access to a dict of FormFieldWrappers" |
|
202 def __init__(self, formfield_dict): |
|
203 self.formfield_dict = formfield_dict |
|
204 |
|
205 def __str__(self): |
|
206 return unicode(self).encode('utf-8') |
|
207 |
|
208 def __unicode__(self): |
|
209 return unicode(self.formfield_dict) |
|
210 |
|
211 def __getitem__(self, template_key): |
|
212 "Look up field by template key; raise KeyError on failure" |
|
213 return self.formfield_dict[template_key] |
|
214 |
|
215 def __repr__(self): |
|
216 return "<FormFieldCollection: %s>" % self.formfield_dict |
|
217 |
|
218 def errors(self): |
|
219 "Returns list of all errors in this collection's formfields" |
|
220 errors = [] |
|
221 for field in self.formfield_dict.values(): |
|
222 if hasattr(field, 'errors'): |
|
223 errors.extend(field.errors()) |
|
224 return errors |
|
225 |
|
226 def has_errors(self): |
|
227 return bool(len(self.errors())) |
|
228 |
|
229 def html_combined_error_list(self): |
|
230 return mark_safe(''.join([field.html_error_list() for field in self.formfield_dict.values() if hasattr(field, 'errors')])) |
|
231 |
|
232 class InlineObjectCollection(object): |
|
233 "An object that acts like a sparse list of form field collections." |
|
234 def __init__(self, parent_manipulator, rel_obj, data, errors): |
|
235 self.parent_manipulator = parent_manipulator |
|
236 self.rel_obj = rel_obj |
|
237 self.data = data |
|
238 self.errors = errors |
|
239 self._collections = None |
|
240 self.name = rel_obj.name |
|
241 # This is the name used prior to fixing #1839. Needs for backwards |
|
242 # compatibility. |
|
243 self.orig_name = rel_obj.opts.module_name |
|
244 |
|
245 def __len__(self): |
|
246 self.fill() |
|
247 return self._collections.__len__() |
|
248 |
|
249 def __getitem__(self, k): |
|
250 self.fill() |
|
251 return self._collections.__getitem__(k) |
|
252 |
|
253 def __setitem__(self, k, v): |
|
254 self.fill() |
|
255 return self._collections.__setitem__(k,v) |
|
256 |
|
257 def __delitem__(self, k): |
|
258 self.fill() |
|
259 return self._collections.__delitem__(k) |
|
260 |
|
261 def __iter__(self): |
|
262 self.fill() |
|
263 return iter(self._collections.values()) |
|
264 |
|
265 def items(self): |
|
266 self.fill() |
|
267 return self._collections.items() |
|
268 |
|
269 def fill(self): |
|
270 if self._collections: |
|
271 return |
|
272 else: |
|
273 var_name = self.rel_obj.opts.object_name.lower() |
|
274 collections = {} |
|
275 orig = None |
|
276 if hasattr(self.parent_manipulator, 'original_object'): |
|
277 orig = self.parent_manipulator.original_object |
|
278 orig_list = self.rel_obj.get_list(orig) |
|
279 |
|
280 for i, instance in enumerate(orig_list): |
|
281 collection = {'original': instance} |
|
282 for f in self.rel_obj.editable_fields(): |
|
283 for field_name in f.get_manipulator_field_names(''): |
|
284 full_field_name = '%s.%d.%s' % (var_name, i, field_name) |
|
285 field = self.parent_manipulator[full_field_name] |
|
286 data = field.extract_data(self.data) |
|
287 errors = self.errors.get(full_field_name, []) |
|
288 collection[field_name] = FormFieldWrapper(field, data, errors) |
|
289 collections[i] = FormFieldCollection(collection) |
|
290 self._collections = collections |
|
291 |
|
292 |
|
293 class FormField(object): |
|
294 """Abstract class representing a form field. |
|
295 |
|
296 Classes that extend FormField should define the following attributes: |
|
297 field_name |
|
298 The field's name for use by programs. |
|
299 validator_list |
|
300 A list of validation tests (callback functions) that the data for |
|
301 this field must pass in order to be added or changed. |
|
302 is_required |
|
303 A Boolean. Is it a required field? |
|
304 Subclasses should also implement a render(data) method, which is responsible |
|
305 for rending the form field in XHTML. |
|
306 """ |
|
307 # Provide backwards compatibility for the maxlength attribute and |
|
308 # argument for this class and all subclasses. |
|
309 __metaclass__ = LegacyMaxlength |
|
310 |
|
311 def __str__(self): |
|
312 return unicode(self).encode('utf-8') |
|
313 |
|
314 def __unicode__(self): |
|
315 return self.render(u'') |
|
316 |
|
317 def __repr__(self): |
|
318 return 'FormField "%s"' % self.field_name |
|
319 |
|
320 def prepare(self, new_data): |
|
321 "Hook for doing something to new_data (in place) before validation." |
|
322 pass |
|
323 |
|
324 def html2python(data): |
|
325 "Hook for converting an HTML datatype (e.g. 'on' for checkboxes) to a Python type" |
|
326 return data |
|
327 html2python = staticmethod(html2python) |
|
328 |
|
329 def render(self, data): |
|
330 raise NotImplementedError |
|
331 |
|
332 def get_member_name(self): |
|
333 if hasattr(self, 'member_name'): |
|
334 return self.member_name |
|
335 else: |
|
336 return self.field_name |
|
337 |
|
338 def extract_data(self, data_dict): |
|
339 if hasattr(self, 'requires_data_list') and hasattr(data_dict, 'getlist'): |
|
340 data = data_dict.getlist(self.get_member_name()) |
|
341 else: |
|
342 data = data_dict.get(self.get_member_name(), None) |
|
343 if data is None: |
|
344 data = '' |
|
345 return data |
|
346 |
|
347 def convert_post_data(self, new_data): |
|
348 name = self.get_member_name() |
|
349 if self.field_name in new_data: |
|
350 d = new_data.getlist(self.field_name) |
|
351 try: |
|
352 converted_data = [self.__class__.html2python(data) for data in d] |
|
353 except ValueError: |
|
354 converted_data = d |
|
355 new_data.setlist(name, converted_data) |
|
356 else: |
|
357 try: |
|
358 #individual fields deal with None values themselves |
|
359 new_data.setlist(name, [self.__class__.html2python(None)]) |
|
360 except EmptyValue: |
|
361 new_data.setlist(name, []) |
|
362 |
|
363 |
|
364 def run_validator(self, new_data, validator): |
|
365 if self.is_required or new_data.get(self.field_name, False) or hasattr(validator, 'always_test'): |
|
366 if hasattr(self, 'requires_data_list'): |
|
367 validator(new_data.getlist(self.field_name), new_data) |
|
368 else: |
|
369 validator(new_data.get(self.field_name, ''), new_data) |
|
370 |
|
371 def get_validation_errors(self, new_data): |
|
372 errors = {} |
|
373 if self.is_required and not new_data.get(self.field_name, False): |
|
374 errors.setdefault(self.field_name, []).append(ugettext('This field is required.')) |
|
375 return errors |
|
376 try: |
|
377 for validator in self.validator_list: |
|
378 try: |
|
379 self.run_validator(new_data, validator) |
|
380 except validators.ValidationError, e: |
|
381 errors.setdefault(self.field_name, []).extend(e.messages) |
|
382 # If a CriticalValidationError is raised, ignore any other ValidationErrors |
|
383 # for this particular field |
|
384 except validators.CriticalValidationError, e: |
|
385 errors.setdefault(self.field_name, []).extend(e.messages) |
|
386 return errors |
|
387 |
|
388 def get_id(self): |
|
389 "Returns the HTML 'id' attribute for this form field." |
|
390 return FORM_FIELD_ID_PREFIX + self.field_name |
|
391 |
|
392 #################### |
|
393 # GENERIC WIDGETS # |
|
394 #################### |
|
395 |
|
396 class TextField(FormField): |
|
397 input_type = "text" |
|
398 def __init__(self, field_name, length=30, max_length=None, is_required=False, validator_list=None, member_name=None): |
|
399 if validator_list is None: validator_list = [] |
|
400 self.field_name = field_name |
|
401 self.length, self.max_length = length, max_length |
|
402 self.is_required = is_required |
|
403 self.validator_list = [self.isValidLength, self.hasNoNewlines] + validator_list |
|
404 if member_name != None: |
|
405 self.member_name = member_name |
|
406 |
|
407 def isValidLength(self, data, form): |
|
408 if data and self.max_length and len(smart_unicode(data)) > self.max_length: |
|
409 raise validators.ValidationError, ungettext("Ensure your text is less than %s character.", |
|
410 "Ensure your text is less than %s characters.", self.max_length) % self.max_length |
|
411 |
|
412 def hasNoNewlines(self, data, form): |
|
413 if data and '\n' in data: |
|
414 raise validators.ValidationError, ugettext("Line breaks are not allowed here.") |
|
415 |
|
416 def render(self, data): |
|
417 if data is None: |
|
418 data = u'' |
|
419 max_length = u'' |
|
420 if self.max_length: |
|
421 max_length = u'maxlength="%s" ' % self.max_length |
|
422 return mark_safe(u'<input type="%s" id="%s" class="v%s%s" name="%s" size="%s" value="%s" %s/>' % \ |
|
423 (self.input_type, self.get_id(), self.__class__.__name__, self.is_required and u' required' or '', |
|
424 self.field_name, self.length, escape(data), max_length)) |
|
425 |
|
426 def html2python(data): |
|
427 return data |
|
428 html2python = staticmethod(html2python) |
|
429 |
|
430 class PasswordField(TextField): |
|
431 input_type = "password" |
|
432 |
|
433 class LargeTextField(TextField): |
|
434 def __init__(self, field_name, rows=10, cols=40, is_required=False, validator_list=None, max_length=None): |
|
435 if validator_list is None: validator_list = [] |
|
436 self.field_name = field_name |
|
437 self.rows, self.cols, self.is_required = rows, cols, is_required |
|
438 self.validator_list = validator_list[:] |
|
439 if max_length: |
|
440 self.validator_list.append(self.isValidLength) |
|
441 self.max_length = max_length |
|
442 |
|
443 def render(self, data): |
|
444 if data is None: |
|
445 data = '' |
|
446 return mark_safe(u'<textarea id="%s" class="v%s%s" name="%s" rows="%s" cols="%s">%s</textarea>' % \ |
|
447 (self.get_id(), self.__class__.__name__, self.is_required and u' required' or u'', |
|
448 self.field_name, self.rows, self.cols, escape(data))) |
|
449 |
|
450 class HiddenField(FormField): |
|
451 def __init__(self, field_name, is_required=False, validator_list=None, max_length=None): |
|
452 if validator_list is None: validator_list = [] |
|
453 self.field_name, self.is_required = field_name, is_required |
|
454 self.validator_list = validator_list[:] |
|
455 |
|
456 def render(self, data): |
|
457 return mark_safe(u'<input type="hidden" id="%s" name="%s" value="%s" />' % \ |
|
458 (self.get_id(), self.field_name, escape(data))) |
|
459 |
|
460 class CheckboxField(FormField): |
|
461 def __init__(self, field_name, checked_by_default=False, validator_list=None, is_required=False): |
|
462 if validator_list is None: validator_list = [] |
|
463 self.field_name = field_name |
|
464 self.checked_by_default = checked_by_default |
|
465 self.is_required = is_required |
|
466 self.validator_list = validator_list[:] |
|
467 |
|
468 def render(self, data): |
|
469 checked_html = '' |
|
470 if data or (data is '' and self.checked_by_default): |
|
471 checked_html = ' checked="checked"' |
|
472 return mark_safe(u'<input type="checkbox" id="%s" class="v%s" name="%s"%s />' % \ |
|
473 (self.get_id(), self.__class__.__name__, |
|
474 self.field_name, checked_html)) |
|
475 |
|
476 def html2python(data): |
|
477 "Convert value from browser ('on' or '') to a Python boolean" |
|
478 if data == 'on': |
|
479 return True |
|
480 return False |
|
481 html2python = staticmethod(html2python) |
|
482 |
|
483 class SelectField(FormField): |
|
484 def __init__(self, field_name, choices=None, size=1, is_required=False, validator_list=None, member_name=None): |
|
485 if validator_list is None: validator_list = [] |
|
486 if choices is None: choices = [] |
|
487 choices = [(k, smart_unicode(v, strings_only=True)) for k, v in choices] |
|
488 self.field_name = field_name |
|
489 # choices is a list of (value, human-readable key) tuples because order matters |
|
490 self.choices, self.size, self.is_required = choices, size, is_required |
|
491 self.validator_list = [self.isValidChoice] + validator_list |
|
492 if member_name != None: |
|
493 self.member_name = member_name |
|
494 |
|
495 def render(self, data): |
|
496 output = [u'<select id="%s" class="v%s%s" name="%s" size="%s">' % \ |
|
497 (self.get_id(), self.__class__.__name__, |
|
498 self.is_required and u' required' or u'', self.field_name, self.size)] |
|
499 str_data = smart_unicode(data) # normalize to string |
|
500 for value, display_name in self.choices: |
|
501 selected_html = u'' |
|
502 if smart_unicode(value) == str_data: |
|
503 selected_html = u' selected="selected"' |
|
504 output.append(u' <option value="%s"%s>%s</option>' % (escape(value), selected_html, force_unicode(escape(display_name)))) |
|
505 output.append(u' </select>') |
|
506 return mark_safe(u'\n'.join(output)) |
|
507 |
|
508 def isValidChoice(self, data, form): |
|
509 str_data = smart_unicode(data) |
|
510 str_choices = [smart_unicode(item[0]) for item in self.choices] |
|
511 if str_data not in str_choices: |
|
512 raise validators.ValidationError, ugettext("Select a valid choice; '%(data)s' is not in %(choices)s.") % {'data': str_data, 'choices': str_choices} |
|
513 |
|
514 class NullSelectField(SelectField): |
|
515 "This SelectField converts blank fields to None" |
|
516 def html2python(data): |
|
517 if not data: |
|
518 return None |
|
519 return data |
|
520 html2python = staticmethod(html2python) |
|
521 |
|
522 class RadioSelectField(FormField): |
|
523 def __init__(self, field_name, choices=None, ul_class='', is_required=False, validator_list=None, member_name=None): |
|
524 if validator_list is None: validator_list = [] |
|
525 if choices is None: choices = [] |
|
526 choices = [(k, smart_unicode(v)) for k, v in choices] |
|
527 self.field_name = field_name |
|
528 # choices is a list of (value, human-readable key) tuples because order matters |
|
529 self.choices, self.is_required = choices, is_required |
|
530 self.validator_list = [self.isValidChoice] + validator_list |
|
531 self.ul_class = ul_class |
|
532 if member_name != None: |
|
533 self.member_name = member_name |
|
534 |
|
535 def render(self, data): |
|
536 """ |
|
537 Returns a special object, RadioFieldRenderer, that is iterable *and* |
|
538 has a default unicode() rendered output. |
|
539 |
|
540 This allows for flexible use in templates. You can just use the default |
|
541 rendering: |
|
542 |
|
543 {{ field_name }} |
|
544 |
|
545 ...which will output the radio buttons in an unordered list. |
|
546 Or, you can manually traverse each radio option for special layout: |
|
547 |
|
548 {% for option in field_name.field_list %} |
|
549 {{ option.field }} {{ option.label }}<br /> |
|
550 {% endfor %} |
|
551 """ |
|
552 class RadioFieldRenderer: |
|
553 def __init__(self, datalist, ul_class): |
|
554 self.datalist, self.ul_class = datalist, ul_class |
|
555 def __unicode__(self): |
|
556 "Default unicode() output for this radio field -- a <ul>" |
|
557 output = [u'<ul%s>' % (self.ul_class and u' class="%s"' % self.ul_class or u'')] |
|
558 output.extend([u'<li>%s %s</li>' % (d['field'], d['label']) for d in self.datalist]) |
|
559 output.append(u'</ul>') |
|
560 return mark_safe(u''.join(output)) |
|
561 def __iter__(self): |
|
562 for d in self.datalist: |
|
563 yield d |
|
564 def __len__(self): |
|
565 return len(self.datalist) |
|
566 datalist = [] |
|
567 str_data = smart_unicode(data) # normalize to string |
|
568 for i, (value, display_name) in enumerate(self.choices): |
|
569 selected_html = '' |
|
570 if smart_unicode(value) == str_data: |
|
571 selected_html = u' checked="checked"' |
|
572 datalist.append({ |
|
573 'value': value, |
|
574 'name': display_name, |
|
575 'field': mark_safe(u'<input type="radio" id="%s" name="%s" value="%s"%s/>' % \ |
|
576 (self.get_id() + u'_' + unicode(i), self.field_name, value, selected_html)), |
|
577 'label': mark_safe(u'<label for="%s">%s</label>' % \ |
|
578 (self.get_id() + u'_' + unicode(i), display_name), |
|
579 )}) |
|
580 return RadioFieldRenderer(datalist, self.ul_class) |
|
581 |
|
582 def isValidChoice(self, data, form): |
|
583 str_data = smart_unicode(data) |
|
584 str_choices = [smart_unicode(item[0]) for item in self.choices] |
|
585 if str_data not in str_choices: |
|
586 raise validators.ValidationError, ugettext("Select a valid choice; '%(data)s' is not in %(choices)s.") % {'data':str_data, 'choices':str_choices} |
|
587 |
|
588 class NullBooleanField(SelectField): |
|
589 "This SelectField provides 'Yes', 'No' and 'Unknown', mapping results to True, False or None" |
|
590 def __init__(self, field_name, is_required=False, validator_list=None): |
|
591 if validator_list is None: validator_list = [] |
|
592 SelectField.__init__(self, field_name, choices=[('1', ugettext('Unknown')), ('2', ugettext('Yes')), ('3', ugettext('No'))], |
|
593 is_required=is_required, validator_list=validator_list) |
|
594 |
|
595 def render(self, data): |
|
596 if data is None: data = '1' |
|
597 elif data == True: data = '2' |
|
598 elif data == False: data = '3' |
|
599 return SelectField.render(self, data) |
|
600 |
|
601 def html2python(data): |
|
602 return {None: None, '1': None, '2': True, '3': False}[data] |
|
603 html2python = staticmethod(html2python) |
|
604 |
|
605 class SelectMultipleField(SelectField): |
|
606 requires_data_list = True |
|
607 def render(self, data): |
|
608 output = [u'<select id="%s" class="v%s%s" name="%s" size="%s" multiple="multiple">' % \ |
|
609 (self.get_id(), self.__class__.__name__, self.is_required and u' required' or u'', |
|
610 self.field_name, self.size)] |
|
611 str_data_list = map(smart_unicode, data) # normalize to strings |
|
612 for value, choice in self.choices: |
|
613 selected_html = u'' |
|
614 if smart_unicode(value) in str_data_list: |
|
615 selected_html = u' selected="selected"' |
|
616 output.append(u' <option value="%s"%s>%s</option>' % (escape(value), selected_html, force_unicode(escape(choice)))) |
|
617 output.append(u' </select>') |
|
618 return mark_safe(u'\n'.join(output)) |
|
619 |
|
620 def isValidChoice(self, field_data, all_data): |
|
621 # data is something like ['1', '2', '3'] |
|
622 str_choices = [smart_unicode(item[0]) for item in self.choices] |
|
623 for val in map(smart_unicode, field_data): |
|
624 if val not in str_choices: |
|
625 raise validators.ValidationError, ugettext("Select a valid choice; '%(data)s' is not in %(choices)s.") % {'data':val, 'choices':str_choices} |
|
626 |
|
627 def html2python(data): |
|
628 if data is None: |
|
629 raise EmptyValue |
|
630 return data |
|
631 html2python = staticmethod(html2python) |
|
632 |
|
633 class CheckboxSelectMultipleField(SelectMultipleField): |
|
634 """ |
|
635 This has an identical interface to SelectMultipleField, except the rendered |
|
636 widget is different. Instead of a <select multiple>, this widget outputs a |
|
637 <ul> of <input type="checkbox">es. |
|
638 |
|
639 Of course, that results in multiple form elements for the same "single" |
|
640 field, so this class's prepare() method flattens the split data elements |
|
641 back into the single list that validators, renderers and save() expect. |
|
642 """ |
|
643 requires_data_list = True |
|
644 def __init__(self, field_name, choices=None, ul_class='', validator_list=None): |
|
645 if validator_list is None: validator_list = [] |
|
646 if choices is None: choices = [] |
|
647 self.ul_class = ul_class |
|
648 SelectMultipleField.__init__(self, field_name, choices, size=1, is_required=False, validator_list=validator_list) |
|
649 |
|
650 def prepare(self, new_data): |
|
651 # new_data has "split" this field into several fields, so flatten it |
|
652 # back into a single list. |
|
653 data_list = [] |
|
654 for value, readable_value in self.choices: |
|
655 if new_data.get('%s%s' % (self.field_name, value), '') == 'on': |
|
656 data_list.append(value) |
|
657 new_data.setlist(self.field_name, data_list) |
|
658 |
|
659 def render(self, data): |
|
660 output = [u'<ul%s>' % (self.ul_class and u' class="%s"' % self.ul_class or u'')] |
|
661 str_data_list = map(smart_unicode, data) # normalize to strings |
|
662 for value, choice in self.choices: |
|
663 checked_html = u'' |
|
664 if smart_unicode(value) in str_data_list: |
|
665 checked_html = u' checked="checked"' |
|
666 field_name = u'%s%s' % (self.field_name, value) |
|
667 output.append(u'<li><input type="checkbox" id="%s" class="v%s" name="%s"%s value="on" /> <label for="%s">%s</label></li>' % \ |
|
668 (self.get_id() + escape(value), self.__class__.__name__, field_name, checked_html, |
|
669 self.get_id() + escape(value), choice)) |
|
670 output.append(u'</ul>') |
|
671 return mark_safe(u'\n'.join(output)) |
|
672 |
|
673 #################### |
|
674 # FILE UPLOADS # |
|
675 #################### |
|
676 |
|
677 class FileUploadField(FormField): |
|
678 def __init__(self, field_name, is_required=False, validator_list=None, max_length=None): |
|
679 if validator_list is None: validator_list = [] |
|
680 self.field_name, self.is_required = field_name, is_required |
|
681 self.validator_list = [self.isNonEmptyFile] + validator_list |
|
682 |
|
683 def isNonEmptyFile(self, field_data, all_data): |
|
684 try: |
|
685 content = field_data['content'] |
|
686 except TypeError: |
|
687 raise validators.CriticalValidationError, ugettext("No file was submitted. Check the encoding type on the form.") |
|
688 if not content: |
|
689 raise validators.CriticalValidationError, ugettext("The submitted file is empty.") |
|
690 |
|
691 def render(self, data): |
|
692 return mark_safe(u'<input type="file" id="%s" class="v%s" name="%s" />' % \ |
|
693 (self.get_id(), self.__class__.__name__, self.field_name)) |
|
694 |
|
695 def html2python(data): |
|
696 if data is None: |
|
697 raise EmptyValue |
|
698 return data |
|
699 html2python = staticmethod(html2python) |
|
700 |
|
701 class ImageUploadField(FileUploadField): |
|
702 "A FileUploadField that raises CriticalValidationError if the uploaded file isn't an image." |
|
703 def __init__(self, *args, **kwargs): |
|
704 FileUploadField.__init__(self, *args, **kwargs) |
|
705 self.validator_list.insert(0, self.isValidImage) |
|
706 |
|
707 def isValidImage(self, field_data, all_data): |
|
708 try: |
|
709 validators.isValidImage(field_data, all_data) |
|
710 except validators.ValidationError, e: |
|
711 raise validators.CriticalValidationError, e.messages |
|
712 |
|
713 #################### |
|
714 # INTEGERS/FLOATS # |
|
715 #################### |
|
716 |
|
717 class IntegerField(TextField): |
|
718 def __init__(self, field_name, length=10, max_length=None, is_required=False, validator_list=None, member_name=None): |
|
719 if validator_list is None: validator_list = [] |
|
720 validator_list = [self.isInteger] + validator_list |
|
721 if member_name is not None: |
|
722 self.member_name = member_name |
|
723 TextField.__init__(self, field_name, length, max_length, is_required, validator_list) |
|
724 |
|
725 def isInteger(self, field_data, all_data): |
|
726 try: |
|
727 validators.isInteger(field_data, all_data) |
|
728 except validators.ValidationError, e: |
|
729 raise validators.CriticalValidationError, e.messages |
|
730 |
|
731 def html2python(data): |
|
732 if data == '' or data is None: |
|
733 return None |
|
734 return int(data) |
|
735 html2python = staticmethod(html2python) |
|
736 |
|
737 class SmallIntegerField(IntegerField): |
|
738 def __init__(self, field_name, length=5, max_length=5, is_required=False, validator_list=None): |
|
739 if validator_list is None: validator_list = [] |
|
740 validator_list = [self.isSmallInteger] + validator_list |
|
741 IntegerField.__init__(self, field_name, length, max_length, is_required, validator_list) |
|
742 |
|
743 def isSmallInteger(self, field_data, all_data): |
|
744 if not -32768 <= int(field_data) <= 32767: |
|
745 raise validators.CriticalValidationError, ugettext("Enter a whole number between -32,768 and 32,767.") |
|
746 |
|
747 class PositiveIntegerField(IntegerField): |
|
748 def __init__(self, field_name, length=10, max_length=None, is_required=False, validator_list=None): |
|
749 if validator_list is None: validator_list = [] |
|
750 validator_list = [self.isPositive] + validator_list |
|
751 IntegerField.__init__(self, field_name, length, max_length, is_required, validator_list) |
|
752 |
|
753 def isPositive(self, field_data, all_data): |
|
754 if int(field_data) < 0: |
|
755 raise validators.CriticalValidationError, ugettext("Enter a positive number.") |
|
756 |
|
757 class PositiveSmallIntegerField(IntegerField): |
|
758 def __init__(self, field_name, length=5, max_length=None, is_required=False, validator_list=None): |
|
759 if validator_list is None: validator_list = [] |
|
760 validator_list = [self.isPositiveSmall] + validator_list |
|
761 IntegerField.__init__(self, field_name, length, max_length, is_required, validator_list) |
|
762 |
|
763 def isPositiveSmall(self, field_data, all_data): |
|
764 if not 0 <= int(field_data) <= 32767: |
|
765 raise validators.CriticalValidationError, ugettext("Enter a whole number between 0 and 32,767.") |
|
766 |
|
767 class FloatField(TextField): |
|
768 def __init__(self, field_name, is_required=False, validator_list=None): |
|
769 if validator_list is None: validator_list = [] |
|
770 validator_list = [validators.isValidFloat] + validator_list |
|
771 TextField.__init__(self, field_name, is_required=is_required, validator_list=validator_list) |
|
772 |
|
773 def html2python(data): |
|
774 if data == '' or data is None: |
|
775 return None |
|
776 return float(data) |
|
777 html2python = staticmethod(html2python) |
|
778 |
|
779 class DecimalField(TextField): |
|
780 def __init__(self, field_name, max_digits, decimal_places, is_required=False, validator_list=None): |
|
781 if validator_list is None: validator_list = [] |
|
782 self.max_digits, self.decimal_places = max_digits, decimal_places |
|
783 validator_list = [self.isValidDecimal] + validator_list |
|
784 # Initialise the TextField, making sure it's large enough to fit the number with a - sign and a decimal point. |
|
785 super(DecimalField, self).__init__(field_name, max_digits+2, max_digits+2, is_required, validator_list) |
|
786 |
|
787 def isValidDecimal(self, field_data, all_data): |
|
788 v = validators.IsValidDecimal(self.max_digits, self.decimal_places) |
|
789 try: |
|
790 v(field_data, all_data) |
|
791 except validators.ValidationError, e: |
|
792 raise validators.CriticalValidationError, e.messages |
|
793 |
|
794 def html2python(data): |
|
795 if data == '' or data is None: |
|
796 return None |
|
797 try: |
|
798 import decimal |
|
799 except ImportError: |
|
800 from django.utils import _decimal as decimal |
|
801 try: |
|
802 return decimal.Decimal(data) |
|
803 except decimal.InvalidOperation, e: |
|
804 raise ValueError, e |
|
805 html2python = staticmethod(html2python) |
|
806 |
|
807 #################### |
|
808 # DATES AND TIMES # |
|
809 #################### |
|
810 |
|
811 class DatetimeField(TextField): |
|
812 """A FormField that automatically converts its data to a datetime.datetime object. |
|
813 The data should be in the format YYYY-MM-DD HH:MM:SS.""" |
|
814 def __init__(self, field_name, length=30, max_length=None, is_required=False, validator_list=None): |
|
815 if validator_list is None: validator_list = [] |
|
816 self.field_name = field_name |
|
817 self.length, self.max_length = length, max_length |
|
818 self.is_required = is_required |
|
819 self.validator_list = [validators.isValidANSIDatetime] + validator_list |
|
820 |
|
821 def html2python(data): |
|
822 "Converts the field into a datetime.datetime object" |
|
823 import datetime |
|
824 try: |
|
825 date, time = data.split() |
|
826 y, m, d = date.split('-') |
|
827 timebits = time.split(':') |
|
828 h, mn = timebits[:2] |
|
829 if len(timebits) > 2: |
|
830 s = int(timebits[2]) |
|
831 else: |
|
832 s = 0 |
|
833 return datetime.datetime(int(y), int(m), int(d), int(h), int(mn), s) |
|
834 except ValueError: |
|
835 return None |
|
836 html2python = staticmethod(html2python) |
|
837 |
|
838 class DateField(TextField): |
|
839 """A FormField that automatically converts its data to a datetime.date object. |
|
840 The data should be in the format YYYY-MM-DD.""" |
|
841 def __init__(self, field_name, is_required=False, validator_list=None): |
|
842 if validator_list is None: validator_list = [] |
|
843 validator_list = [self.isValidDate] + validator_list |
|
844 TextField.__init__(self, field_name, length=10, max_length=10, |
|
845 is_required=is_required, validator_list=validator_list) |
|
846 |
|
847 def isValidDate(self, field_data, all_data): |
|
848 try: |
|
849 validators.isValidANSIDate(field_data, all_data) |
|
850 except validators.ValidationError, e: |
|
851 raise validators.CriticalValidationError, e.messages |
|
852 |
|
853 def html2python(data): |
|
854 "Converts the field into a datetime.date object" |
|
855 import time, datetime |
|
856 try: |
|
857 time_tuple = time.strptime(data, '%Y-%m-%d') |
|
858 return datetime.date(*time_tuple[0:3]) |
|
859 except (ValueError, TypeError): |
|
860 return None |
|
861 html2python = staticmethod(html2python) |
|
862 |
|
863 class TimeField(TextField): |
|
864 """A FormField that automatically converts its data to a datetime.time object. |
|
865 The data should be in the format HH:MM:SS or HH:MM:SS.mmmmmm.""" |
|
866 def __init__(self, field_name, is_required=False, validator_list=None): |
|
867 if validator_list is None: validator_list = [] |
|
868 validator_list = [self.isValidTime] + validator_list |
|
869 TextField.__init__(self, field_name, length=8, max_length=8, |
|
870 is_required=is_required, validator_list=validator_list) |
|
871 |
|
872 def isValidTime(self, field_data, all_data): |
|
873 try: |
|
874 validators.isValidANSITime(field_data, all_data) |
|
875 except validators.ValidationError, e: |
|
876 raise validators.CriticalValidationError, e.messages |
|
877 |
|
878 def html2python(data): |
|
879 "Converts the field into a datetime.time object" |
|
880 import time, datetime |
|
881 try: |
|
882 part_list = data.split('.') |
|
883 try: |
|
884 time_tuple = time.strptime(part_list[0], '%H:%M:%S') |
|
885 except ValueError: # seconds weren't provided |
|
886 time_tuple = time.strptime(part_list[0], '%H:%M') |
|
887 t = datetime.time(*time_tuple[3:6]) |
|
888 if (len(part_list) == 2): |
|
889 t = t.replace(microsecond=int(part_list[1])) |
|
890 return t |
|
891 except (ValueError, TypeError, AttributeError): |
|
892 return None |
|
893 html2python = staticmethod(html2python) |
|
894 |
|
895 #################### |
|
896 # INTERNET-RELATED # |
|
897 #################### |
|
898 |
|
899 class EmailField(TextField): |
|
900 "A convenience FormField for validating e-mail addresses" |
|
901 def __init__(self, field_name, length=50, max_length=75, is_required=False, validator_list=None): |
|
902 if validator_list is None: validator_list = [] |
|
903 validator_list = [self.isValidEmail] + validator_list |
|
904 TextField.__init__(self, field_name, length, max_length=max_length, |
|
905 is_required=is_required, validator_list=validator_list) |
|
906 |
|
907 def isValidEmail(self, field_data, all_data): |
|
908 try: |
|
909 validators.isValidEmail(field_data, all_data) |
|
910 except validators.ValidationError, e: |
|
911 raise validators.CriticalValidationError, e.messages |
|
912 |
|
913 class URLField(TextField): |
|
914 "A convenience FormField for validating URLs" |
|
915 def __init__(self, field_name, length=50, max_length=200, is_required=False, validator_list=None): |
|
916 if validator_list is None: validator_list = [] |
|
917 validator_list = [self.isValidURL] + validator_list |
|
918 TextField.__init__(self, field_name, length=length, max_length=max_length, |
|
919 is_required=is_required, validator_list=validator_list) |
|
920 |
|
921 def isValidURL(self, field_data, all_data): |
|
922 try: |
|
923 validators.isValidURL(field_data, all_data) |
|
924 except validators.ValidationError, e: |
|
925 raise validators.CriticalValidationError, e.messages |
|
926 |
|
927 class IPAddressField(TextField): |
|
928 def __init__(self, field_name, length=15, max_length=15, is_required=False, validator_list=None): |
|
929 if validator_list is None: validator_list = [] |
|
930 validator_list = [self.isValidIPAddress] + validator_list |
|
931 TextField.__init__(self, field_name, length=length, max_length=max_length, |
|
932 is_required=is_required, validator_list=validator_list) |
|
933 |
|
934 def isValidIPAddress(self, field_data, all_data): |
|
935 try: |
|
936 validators.isValidIPAddress4(field_data, all_data) |
|
937 except validators.ValidationError, e: |
|
938 raise validators.CriticalValidationError, e.messages |
|
939 |
|
940 def html2python(data): |
|
941 return data or None |
|
942 html2python = staticmethod(html2python) |
|
943 |
|
944 #################### |
|
945 # MISCELLANEOUS # |
|
946 #################### |
|
947 |
|
948 class FilePathField(SelectField): |
|
949 "A SelectField whose choices are the files in a given directory." |
|
950 def __init__(self, field_name, path, match=None, recursive=False, is_required=False, validator_list=None, max_length=None): |
|
951 import os |
|
952 from django.db.models import BLANK_CHOICE_DASH |
|
953 if match is not None: |
|
954 import re |
|
955 match_re = re.compile(match) |
|
956 choices = not is_required and BLANK_CHOICE_DASH[:] or [] |
|
957 if recursive: |
|
958 for root, dirs, files in os.walk(path): |
|
959 for f in files: |
|
960 if match is None or match_re.search(f): |
|
961 f = os.path.join(root, f) |
|
962 choices.append((f, f.replace(path, "", 1))) |
|
963 else: |
|
964 try: |
|
965 for f in os.listdir(path): |
|
966 full_file = os.path.join(path, f) |
|
967 if os.path.isfile(full_file) and (match is None or match_re.search(f)): |
|
968 choices.append((full_file, f)) |
|
969 except OSError: |
|
970 pass |
|
971 SelectField.__init__(self, field_name, choices, 1, is_required, validator_list) |
|
972 |
|
973 class PhoneNumberField(TextField): |
|
974 "A convenience FormField for validating phone numbers (e.g. '630-555-1234')" |
|
975 def __init__(self, field_name, is_required=False, validator_list=None): |
|
976 if validator_list is None: validator_list = [] |
|
977 validator_list = [self.isValidPhone] + validator_list |
|
978 TextField.__init__(self, field_name, length=12, max_length=12, |
|
979 is_required=is_required, validator_list=validator_list) |
|
980 |
|
981 def isValidPhone(self, field_data, all_data): |
|
982 try: |
|
983 validators.isValidPhone(field_data, all_data) |
|
984 except validators.ValidationError, e: |
|
985 raise validators.CriticalValidationError, e.messages |
|
986 |
|
987 class USStateField(TextField): |
|
988 "A convenience FormField for validating U.S. states (e.g. 'IL')" |
|
989 def __init__(self, field_name, is_required=False, validator_list=None): |
|
990 if validator_list is None: validator_list = [] |
|
991 validator_list = [self.isValidUSState] + validator_list |
|
992 TextField.__init__(self, field_name, length=2, max_length=2, |
|
993 is_required=is_required, validator_list=validator_list) |
|
994 |
|
995 def isValidUSState(self, field_data, all_data): |
|
996 try: |
|
997 validators.isValidUSState(field_data, all_data) |
|
998 except validators.ValidationError, e: |
|
999 raise validators.CriticalValidationError, e.messages |
|
1000 |
|
1001 def html2python(data): |
|
1002 if data: |
|
1003 return data.upper() # Should always be stored in upper case |
|
1004 return data |
|
1005 html2python = staticmethod(html2python) |
|
1006 |
|
1007 class CommaSeparatedIntegerField(TextField): |
|
1008 "A convenience FormField for validating comma-separated integer fields" |
|
1009 def __init__(self, field_name, max_length=None, is_required=False, validator_list=None): |
|
1010 if validator_list is None: validator_list = [] |
|
1011 validator_list = [self.isCommaSeparatedIntegerList] + validator_list |
|
1012 TextField.__init__(self, field_name, length=20, max_length=max_length, |
|
1013 is_required=is_required, validator_list=validator_list) |
|
1014 |
|
1015 def isCommaSeparatedIntegerList(self, field_data, all_data): |
|
1016 try: |
|
1017 validators.isCommaSeparatedIntegerList(field_data, all_data) |
|
1018 except validators.ValidationError, e: |
|
1019 raise validators.CriticalValidationError, e.messages |
|
1020 |
|
1021 def render(self, data): |
|
1022 if data is None: |
|
1023 data = u'' |
|
1024 elif isinstance(data, (list, tuple)): |
|
1025 data = u','.join(data) |
|
1026 return super(CommaSeparatedIntegerField, self).render(data) |
|
1027 |
|
1028 class RawIdAdminField(CommaSeparatedIntegerField): |
|
1029 def html2python(data): |
|
1030 if data: |
|
1031 return data.split(',') |
|
1032 else: |
|
1033 return [] |
|
1034 html2python = staticmethod(html2python) |
|
1035 |
|
1036 class XMLLargeTextField(LargeTextField): |
|
1037 """ |
|
1038 A LargeTextField with an XML validator. The schema_path argument is the |
|
1039 full path to a Relax NG compact schema to validate against. |
|
1040 """ |
|
1041 def __init__(self, field_name, schema_path, **kwargs): |
|
1042 self.schema_path = schema_path |
|
1043 kwargs.setdefault('validator_list', []).insert(0, self.isValidXML) |
|
1044 LargeTextField.__init__(self, field_name, **kwargs) |
|
1045 |
|
1046 def isValidXML(self, field_data, all_data): |
|
1047 v = validators.RelaxNGCompact(self.schema_path) |
|
1048 try: |
|
1049 v(field_data, all_data) |
|
1050 except validators.ValidationError, e: |
|
1051 raise validators.CriticalValidationError, e.messages |