Ticket #18303: widgets.py

File widgets.py, 33.8 KB (added by blind, 13 years ago)

patch

Line 
1"""
2HTML Widget classes
3"""
4
5from __future__ import absolute_import
6
7import copy
8import datetime
9from itertools import chain
10from urlparse import urljoin
11
12from django.conf import settings
13from django.forms.util import flatatt, to_current_timezone
14from django.utils.datastructures import MultiValueDict, MergeDict
15from django.utils.html import escape, conditional_escape
16from django.utils.translation import ugettext, ugettext_lazy
17from django.utils.encoding import StrAndUnicode, force_unicode
18from django.utils.safestring import mark_safe
19from django.utils import datetime_safe, formats
20
21__all__ = (
22 'Media', 'MediaDefiningClass', 'Widget', 'TextInput', 'PasswordInput',
23 'HiddenInput', 'MultipleHiddenInput', 'ClearableFileInput',
24 'FileInput', 'DateInput', 'DateTimeInput', 'TimeInput', 'Textarea', 'CheckboxInput',
25 'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
26 'CheckboxSelectMultiple', 'MultiWidget',
27 'SplitDateTimeWidget',
28)
29
30MEDIA_TYPES = ('css','js')
31
32class Media(StrAndUnicode):
33 def __init__(self, media=None, **kwargs):
34 if media:
35 media_attrs = media.__dict__
36 else:
37 media_attrs = kwargs
38
39 self._css = {}
40 self._js = []
41
42 for name in MEDIA_TYPES:
43 getattr(self, 'add_' + name)(media_attrs.get(name, None))
44
45 # Any leftover attributes must be invalid.
46 # if media_attrs != {}:
47 # raise TypeError("'class Media' has invalid attribute(s): %s" % ','.join(media_attrs.keys()))
48
49 def __unicode__(self):
50 return self.render()
51
52 def render(self):
53 return mark_safe(u'\n'.join(chain(*[getattr(self, 'render_' + name)() for name in MEDIA_TYPES])))
54
55 def render_js(self):
56 return [u'<script type="text/javascript" src="%s"></script>' % self.absolute_path(path) for path in self._js]
57
58 def render_css(self):
59 # To keep rendering order consistent, we can't just iterate over items().
60 # We need to sort the keys, and iterate over the sorted list.
61 media = self._css.keys()
62 media.sort()
63 return chain(*[
64 [u'<link href="%s" type="text/css" media="%s" rel="stylesheet" />' % (self.absolute_path(path), medium)
65 for path in self._css[medium]]
66 for medium in media])
67
68 def absolute_path(self, path, prefix=None):
69 if path.startswith(u'http://') or path.startswith(u'https://') or path.startswith(u'/'):
70 return path
71 if prefix is None:
72 if settings.STATIC_URL is None:
73 # backwards compatibility
74 prefix = settings.MEDIA_URL
75 else:
76 prefix = settings.STATIC_URL
77 return urljoin(prefix, path)
78
79 def __getitem__(self, name):
80 "Returns a Media object that only contains media of the given type"
81 if name in MEDIA_TYPES:
82 return Media(**{str(name): getattr(self, '_' + name)})
83 raise KeyError('Unknown media type "%s"' % name)
84
85 def add_js(self, data):
86 if data:
87 for path in data:
88 if path not in self._js:
89 self._js.append(path)
90
91 def add_css(self, data):
92 if data:
93 for medium, paths in data.items():
94 for path in paths:
95 if not self._css.get(medium) or path not in self._css[medium]:
96 self._css.setdefault(medium, []).append(path)
97
98 def __add__(self, other):
99 combined = Media()
100 for name in MEDIA_TYPES:
101 getattr(combined, 'add_' + name)(getattr(self, '_' + name, None))
102 getattr(combined, 'add_' + name)(getattr(other, '_' + name, None))
103 return combined
104
105def media_property(cls):
106 def _media(self):
107 # Get the media property of the superclass, if it exists
108 if hasattr(super(cls, self), 'media'):
109 base = super(cls, self).media
110 else:
111 base = Media()
112
113 # Get the media definition for this class
114 definition = getattr(cls, 'Media', None)
115 if definition:
116 extend = getattr(definition, 'extend', True)
117 if extend:
118 if extend == True:
119 m = base
120 else:
121 m = Media()
122 for medium in extend:
123 m = m + base[medium]
124 return m + Media(definition)
125 else:
126 return Media(definition)
127 else:
128 return base
129 return property(_media)
130
131class MediaDefiningClass(type):
132 "Metaclass for classes that can have media definitions"
133 def __new__(cls, name, bases, attrs):
134 new_class = super(MediaDefiningClass, cls).__new__(cls, name, bases,
135 attrs)
136 if 'media' not in attrs:
137 new_class.media = media_property(new_class)
138 return new_class
139
140class SubWidget(StrAndUnicode):
141 """
142 Some widgets are made of multiple HTML elements -- namely, RadioSelect.
143 This is a class that represents the "inner" HTML element of a widget.
144 """
145 def __init__(self, parent_widget, name, value, attrs, choices):
146 self.parent_widget = parent_widget
147 self.name, self.value = name, value
148 self.attrs, self.choices = attrs, choices
149
150 def __unicode__(self):
151 args = [self.name, self.value, self.attrs]
152 if self.choices:
153 args.append(self.choices)
154 return self.parent_widget.render(*args)
155
156class Widget(object):
157 __metaclass__ = MediaDefiningClass
158 is_hidden = False # Determines whether this corresponds to an <input type="hidden">.
159 needs_multipart_form = False # Determines does this widget need multipart form
160 is_localized = False
161 is_required = False
162
163 def __init__(self, attrs=None):
164 if attrs is not None:
165 self.attrs = attrs.copy()
166 else:
167 self.attrs = {}
168
169 def __deepcopy__(self, memo):
170 obj = copy.copy(self)
171 obj.attrs = self.attrs.copy()
172 memo[id(self)] = obj
173 return obj
174
175 def subwidgets(self, name, value, attrs=None, choices=()):
176 """
177 Yields all "subwidgets" of this widget. Used only by RadioSelect to
178 allow template access to individual <input type="radio"> buttons.
179
180 Arguments are the same as for render().
181 """
182 yield SubWidget(self, name, value, attrs, choices)
183
184 def render(self, name, value, attrs=None):
185 """
186 Returns this Widget rendered as HTML, as a Unicode string.
187
188 The 'value' given is not guaranteed to be valid input, so subclass
189 implementations should program defensively.
190 """
191 raise NotImplementedError
192
193 def build_attrs(self, extra_attrs=None, **kwargs):
194 "Helper function for building an attribute dictionary."
195 attrs = dict(self.attrs, **kwargs)
196 if extra_attrs:
197 attrs.update(extra_attrs)
198 return attrs
199
200 def value_from_datadict(self, data, files, name):
201 """
202 Given a dictionary of data and this widget's name, returns the value
203 of this widget. Returns None if it's not provided.
204 """
205 return data.get(name, None)
206
207 def _has_changed(self, initial, data):
208 """
209 Return True if data differs from initial.
210 """
211 # For purposes of seeing whether something has changed, None is
212 # the same as an empty string, if the data or inital value we get
213 # is None, replace it w/ u''.
214 if data is None:
215 data_value = u''
216 else:
217 data_value = data
218 if initial is None:
219 initial_value = u''
220 else:
221 initial_value = initial
222 if force_unicode(initial_value) != force_unicode(data_value):
223 return True
224 return False
225
226 def id_for_label(self, id_):
227 """
228 Returns the HTML ID attribute of this Widget for use by a <label>,
229 given the ID of the field. Returns None if no ID is available.
230
231 This hook is necessary because some widgets have multiple HTML
232 elements and, thus, multiple IDs. In that case, this method should
233 return an ID value that corresponds to the first ID in the widget's
234 tags.
235 """
236 return id_
237
238class Input(Widget):
239 """
240 Base class for all <input> widgets (except type='checkbox' and
241 type='radio', which are special).
242 """
243 input_type = None # Subclasses must define this.
244
245 def _format_value(self, value):
246 if self.is_localized:
247 return formats.localize_input(value)
248 return value
249
250 def render(self, name, value, attrs=None):
251 if value is None:
252 value = ''
253 final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
254 if value != '':
255 # Only add the 'value' attribute if a value is non-empty.
256 final_attrs['value'] = force_unicode(self._format_value(value))
257 return mark_safe(u'<input%s />' % flatatt(final_attrs))
258
259class TextInput(Input):
260 input_type = 'text'
261
262class PasswordInput(Input):
263 input_type = 'password'
264
265 def __init__(self, attrs=None, render_value=False):
266 super(PasswordInput, self).__init__(attrs)
267 self.render_value = render_value
268
269 def render(self, name, value, attrs=None):
270 if not self.render_value: value=None
271 return super(PasswordInput, self).render(name, value, attrs)
272
273class HiddenInput(Input):
274 input_type = 'hidden'
275 is_hidden = True
276
277class MultipleHiddenInput(HiddenInput):
278 """
279 A widget that handles <input type="hidden"> for fields that have a list
280 of values.
281 """
282 def __init__(self, attrs=None, choices=()):
283 super(MultipleHiddenInput, self).__init__(attrs)
284 # choices can be any iterable
285 self.choices = choices
286
287 def render(self, name, value, attrs=None, choices=()):
288 if value is None: value = []
289 final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
290 id_ = final_attrs.get('id', None)
291 inputs = []
292 for i, v in enumerate(value):
293 input_attrs = dict(value=force_unicode(v), **final_attrs)
294 if id_:
295 # An ID attribute was given. Add a numeric index as a suffix
296 # so that the inputs don't all have the same ID attribute.
297 input_attrs['id'] = '%s_%s' % (id_, i)
298 inputs.append(u'<input%s />' % flatatt(input_attrs))
299 return mark_safe(u'\n'.join(inputs))
300
301 def value_from_datadict(self, data, files, name):
302 if isinstance(data, (MultiValueDict, MergeDict)):
303 return data.getlist(name)
304 return data.get(name, None)
305
306class FileInput(Input):
307 input_type = 'file'
308 needs_multipart_form = True
309
310 def render(self, name, value, attrs=None):
311 return super(FileInput, self).render(name, None, attrs=attrs)
312
313 def value_from_datadict(self, data, files, name):
314 "File widgets take data from FILES, not POST"
315 return files.get(name, None)
316
317 def _has_changed(self, initial, data):
318 if data is None:
319 return False
320 return True
321
322FILE_INPUT_CONTRADICTION = object()
323
324class ClearableFileInput(FileInput):
325 initial_text = ugettext_lazy('Currently')
326 input_text = ugettext_lazy('Change')
327 clear_checkbox_label = ugettext_lazy('Clear')
328
329 template_with_initial = u'%(initial_text)s: %(initial)s %(clear_template)s<br />%(input_text)s: %(input)s'
330
331 template_with_clear = u'%(clear)s <label for="%(clear_checkbox_id)s">%(clear_checkbox_label)s</label>'
332
333 def clear_checkbox_name(self, name):
334 """
335 Given the name of the file input, return the name of the clear checkbox
336 input.
337 """
338 return name + '-clear'
339
340 def clear_checkbox_id(self, name):
341 """
342 Given the name of the clear checkbox input, return the HTML id for it.
343 """
344 return name + '_id'
345
346 def render(self, name, value, attrs=None):
347 substitutions = {
348 'initial_text': self.initial_text,
349 'input_text': self.input_text,
350 'clear_template': '',
351 'clear_checkbox_label': self.clear_checkbox_label,
352 }
353 template = u'%(input)s'
354 substitutions['input'] = super(ClearableFileInput, self).render(name, value, attrs)
355
356 if value and hasattr(value, "url"):
357 template = self.template_with_initial
358 substitutions['initial'] = (u'<a href="%s">%s</a>'
359 % (escape(value.url),
360 escape(force_unicode(value))))
361 if not self.is_required:
362 checkbox_name = self.clear_checkbox_name(name)
363 checkbox_id = self.clear_checkbox_id(checkbox_name)
364 substitutions['clear_checkbox_name'] = conditional_escape(checkbox_name)
365 substitutions['clear_checkbox_id'] = conditional_escape(checkbox_id)
366 substitutions['clear'] = CheckboxInput().render(checkbox_name, False, attrs={'id': checkbox_id})
367 substitutions['clear_template'] = self.template_with_clear % substitutions
368
369 return mark_safe(template % substitutions)
370
371 def value_from_datadict(self, data, files, name):
372 upload = super(ClearableFileInput, self).value_from_datadict(data, files, name)
373 if not self.is_required and CheckboxInput().value_from_datadict(
374 data, files, self.clear_checkbox_name(name)):
375 if upload:
376 # If the user contradicts themselves (uploads a new file AND
377 # checks the "clear" checkbox), we return a unique marker
378 # object that FileField will turn into a ValidationError.
379 return FILE_INPUT_CONTRADICTION
380 # False signals to clear any existing value, as opposed to just None
381 return False
382 return upload
383
384class Textarea(Widget):
385 def __init__(self, attrs=None):
386 # The 'rows' and 'cols' attributes are required for HTML correctness.
387 default_attrs = {'cols': '40', 'rows': '10'}
388 if attrs:
389 default_attrs.update(attrs)
390 super(Textarea, self).__init__(default_attrs)
391
392 def render(self, name, value, attrs=None):
393 if value is None: value = ''
394 final_attrs = self.build_attrs(attrs, name=name)
395 return mark_safe(u'<textarea%s>%s</textarea>' % (flatatt(final_attrs),
396 conditional_escape(force_unicode(value))))
397
398class DateInput(Input):
399 input_type = 'text'
400
401 def __init__(self, attrs=None, format=None):
402 super(DateInput, self).__init__(attrs)
403 if format:
404 self.format = format
405 self.manual_format = True
406 else:
407 self.format = formats.get_format('DATE_INPUT_FORMATS')[0]
408 self.manual_format = False
409
410 def _format_value(self, value):
411 if self.is_localized and not self.manual_format:
412 return formats.localize_input(value)
413 elif hasattr(value, 'strftime'):
414 value = datetime_safe.new_date(value)
415 return value.strftime(self.format)
416 return value
417
418 def _has_changed(self, initial, data):
419 # If our field has show_hidden_initial=True, initial will be a string
420 # formatted by HiddenInput using formats.localize_input, which is not
421 # necessarily the format used for this widget. Attempt to convert it.
422 try:
423 input_format = formats.get_format('DATE_INPUT_FORMATS')[0]
424 initial = datetime.datetime.strptime(initial, input_format).date()
425 except (TypeError, ValueError):
426 pass
427 return super(DateInput, self)._has_changed(self._format_value(initial), data)
428
429class DateTimeInput(Input):
430 input_type = 'text'
431
432 def __init__(self, attrs=None, format=None):
433 super(DateTimeInput, self).__init__(attrs)
434 if format:
435 self.format = format
436 self.manual_format = True
437 else:
438 self.format = formats.get_format('DATETIME_INPUT_FORMATS')[0]
439 self.manual_format = False
440
441 def _format_value(self, value):
442 if self.is_localized and not self.manual_format:
443 return formats.localize_input(value)
444 elif hasattr(value, 'strftime'):
445 value = datetime_safe.new_datetime(value)
446 return value.strftime(self.format)
447 return value
448
449 def _has_changed(self, initial, data):
450 # If our field has show_hidden_initial=True, initial will be a string
451 # formatted by HiddenInput using formats.localize_input, which is not
452 # necessarily the format used for this widget. Attempt to convert it.
453 try:
454 input_format = formats.get_format('DATETIME_INPUT_FORMATS')[0]
455 initial = datetime.datetime.strptime(initial, input_format)
456 except (TypeError, ValueError):
457 pass
458 return super(DateTimeInput, self)._has_changed(self._format_value(initial), data)
459
460class TimeInput(Input):
461 input_type = 'text'
462
463 def __init__(self, attrs=None, format=None):
464 super(TimeInput, self).__init__(attrs)
465 if format:
466 self.format = format
467 self.manual_format = True
468 else:
469 self.format = formats.get_format('TIME_INPUT_FORMATS')[0]
470 self.manual_format = False
471
472 def _format_value(self, value):
473 if self.is_localized and not self.manual_format:
474 return formats.localize_input(value)
475 elif hasattr(value, 'strftime'):
476 return value.strftime(self.format)
477 return value
478
479 def _has_changed(self, initial, data):
480 # If our field has show_hidden_initial=True, initial will be a string
481 # formatted by HiddenInput using formats.localize_input, which is not
482 # necessarily the format used for this widget. Attempt to convert it.
483 try:
484 input_format = formats.get_format('TIME_INPUT_FORMATS')[0]
485 initial = datetime.datetime.strptime(initial, input_format).time()
486 except (TypeError, ValueError):
487 pass
488 return super(TimeInput, self)._has_changed(self._format_value(initial), data)
489
490
491# Defined at module level so that CheckboxInput is picklable (#17976)
492def boolean_check(v):
493 return not (v is False or v is None or v == '')
494
495
496class CheckboxInput(Widget):
497 def __init__(self, attrs=None, check_test=None):
498 super(CheckboxInput, self).__init__(attrs)
499 # check_test is a callable that takes a value and returns True
500 # if the checkbox should be checked for that value.
501 self.check_test = boolean_check if check_test is None else check_test
502
503 def render(self, name, value, attrs=None):
504 final_attrs = self.build_attrs(attrs, type='checkbox', name=name)
505 try:
506 result = self.check_test(value)
507 except: # Silently catch exceptions
508 result = False
509 if result:
510 final_attrs['checked'] = 'checked'
511 if not (value == True or value == False or value == None or value == ''):
512 # Only add the 'value' attribute if a value is non-empty.
513 final_attrs['value'] = force_unicode(value)
514 return mark_safe(u'<input%s />' % flatatt(final_attrs))
515
516 def value_from_datadict(self, data, files, name):
517 if name not in data:
518 # A missing value means False because HTML form submission does not
519 # send results for unselected checkboxes.
520 return False
521 value = data.get(name)
522 # Translate true and false strings to boolean values.
523 values = {'true': True, 'false': False}
524 if isinstance(value, basestring):
525 value = values.get(value.lower(), value)
526 return value
527
528 def _has_changed(self, initial, data):
529 # Sometimes data or initial could be None or u'' which should be the
530 # same thing as False.
531 return bool(initial) != bool(data)
532
533class Select(Widget):
534 allow_multiple_selected = False
535
536 def __init__(self, attrs=None, choices=()):
537 super(Select, self).__init__(attrs)
538 # choices can be any iterable, but we may need to render this widget
539 # multiple times. Thus, collapse it into a list so it can be consumed
540 # more than once.
541 self.choices = list(choices)
542
543 def render(self, name, value, attrs=None, choices=()):
544 if value is None: value = ''
545 final_attrs = self.build_attrs(attrs, name=name)
546 output = [u'<select%s>' % flatatt(final_attrs)]
547 options = self.render_options(choices, [value])
548 if options:
549 output.append(options)
550 output.append(u'</select>')
551 return mark_safe(u'\n'.join(output))
552
553 def render_option(self, selected_choices, option_value, option_label):
554 option_value = force_unicode(option_value)
555 if option_value in selected_choices:
556 selected_html = u' selected="selected"'
557 if not self.allow_multiple_selected:
558 # Only allow for a single selection.
559 selected_choices.remove(option_value)
560 else:
561 selected_html = ''
562 return u'<option value="%s"%s>%s</option>' % (
563 escape(option_value), selected_html,
564 conditional_escape(force_unicode(option_label)))
565
566 def render_options(self, choices, selected_choices):
567 # Normalize to strings.
568 selected_choices = set(force_unicode(v) for v in selected_choices)
569 output = []
570 for option_value, option_label in chain(self.choices, choices):
571 if isinstance(option_label, (list, tuple)):
572 output.append(u'<optgroup label="%s">' % escape(force_unicode(option_value)))
573 for option in option_label:
574 output.append(self.render_option(selected_choices, *option))
575 output.append(u'</optgroup>')
576 else:
577 output.append(self.render_option(selected_choices, option_value, option_label))
578 return u'\n'.join(output)
579
580class NullBooleanSelect(Select):
581 """
582 A Select Widget intended to be used with NullBooleanField.
583 """
584 def __init__(self, attrs=None):
585 choices = ((u'1', ugettext_lazy('Unknown')),
586 (u'2', ugettext_lazy('Yes')),
587 (u'3', ugettext_lazy('No')))
588 super(NullBooleanSelect, self).__init__(attrs, choices)
589
590 def render(self, name, value, attrs=None, choices=()):
591 try:
592 value = {True: u'2', False: u'3', u'2': u'2', u'3': u'3'}[value]
593 except KeyError:
594 value = u'1'
595 return super(NullBooleanSelect, self).render(name, value, attrs, choices)
596
597 def value_from_datadict(self, data, files, name):
598 value = data.get(name, None)
599 return {u'2': True,
600 True: True,
601 'True': True,
602 u'3': False,
603 'False': False,
604 False: False}.get(value, None)
605
606 def _has_changed(self, initial, data):
607 # For a NullBooleanSelect, None (unknown) and False (No)
608 # are not the same
609 if initial is not None:
610 initial = bool(initial)
611 if data is not None:
612 data = bool(data)
613 return initial != data
614
615class SelectMultiple(Select):
616 allow_multiple_selected = True
617
618 def render(self, name, value, attrs=None, choices=()):
619 if value is None: value = []
620 final_attrs = self.build_attrs(attrs, name=name)
621 output = [u'<select multiple="multiple"%s>' % flatatt(final_attrs)]
622 options = self.render_options(choices, value)
623 if options:
624 output.append(options)
625 output.append('</select>')
626 return mark_safe(u'\n'.join(output))
627
628 def value_from_datadict(self, data, files, name):
629 if isinstance(data, (MultiValueDict, MergeDict)):
630 return data.getlist(name)
631 return data.get(name, None)
632
633 def _has_changed(self, initial, data):
634 if initial is None:
635 initial = []
636 if data is None:
637 data = []
638 if len(initial) != len(data):
639 return True
640 initial_set = set([force_unicode(value) for value in initial])
641 data_set = set([force_unicode(value) for value in data])
642 return data_set != initial_set
643
644class RadioInput(SubWidget):
645 """
646 An object used by RadioFieldRenderer that represents a single
647 <input type='radio'>.
648 """
649
650 def __init__(self, name, value, attrs, choice, index):
651 self.name, self.value = name, value
652 self.attrs = attrs
653 self.choice_value = force_unicode(choice[0])
654 self.choice_label = force_unicode(choice[1])
655 self.index = index
656
657 def __unicode__(self):
658 return self.render()
659
660 def render(self, name=None, value=None, attrs=None, choices=()):
661 name = name or self.name
662 value = value or self.value
663 attrs = attrs or self.attrs
664 if 'id' in self.attrs:
665 label_for = ' for="%s_%s"' % (self.attrs['id'], self.index)
666 else:
667 label_for = ''
668 choice_label = conditional_escape(force_unicode(self.choice_label))
669 return mark_safe(u'<label%s>%s %s</label>' % (label_for, self.tag(), choice_label))
670
671 def is_checked(self):
672 return self.value == self.choice_value
673
674 def tag(self):
675 if 'id' in self.attrs:
676 self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index)
677 final_attrs = dict(self.attrs, type='radio', name=self.name, value=self.choice_value)
678 if self.is_checked():
679 final_attrs['checked'] = 'checked'
680 return mark_safe(u'<input%s />' % flatatt(final_attrs))
681
682class RadioFieldRenderer(StrAndUnicode):
683 """
684 An object used by RadioSelect to enable customization of radio widgets.
685 """
686
687 def __init__(self, name, value, attrs, choices):
688 self.name, self.value, self.attrs = name, value, attrs
689 self.choices = choices
690
691 def __iter__(self):
692 for i, choice in enumerate(self.choices):
693 yield RadioInput(self.name, self.value, self.attrs.copy(), choice, i)
694
695 def __getitem__(self, idx):
696 choice = self.choices[idx] # Let the IndexError propogate
697 return RadioInput(self.name, self.value, self.attrs.copy(), choice, idx)
698
699 def __unicode__(self):
700 return self.render()
701
702 def render(self):
703 """Outputs a <ul> for this set of radio fields."""
704 return mark_safe(u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>'
705 % force_unicode(w) for w in self]))
706
707class RadioSelect(Select):
708 renderer = RadioFieldRenderer
709
710 def __init__(self, *args, **kwargs):
711 # Override the default renderer if we were passed one.
712 renderer = kwargs.pop('renderer', None)
713 if renderer:
714 self.renderer = renderer
715 super(RadioSelect, self).__init__(*args, **kwargs)
716
717 def subwidgets(self, name, value, attrs=None, choices=()):
718 for widget in self.get_renderer(name, value, attrs, choices):
719 yield widget
720
721 def get_renderer(self, name, value, attrs=None, choices=()):
722 """Returns an instance of the renderer."""
723 if value is None: value = ''
724 str_value = force_unicode(value) # Normalize to string.
725 final_attrs = self.build_attrs(attrs)
726 choices = list(chain(self.choices, choices))
727 return self.renderer(name, str_value, final_attrs, choices)
728
729 def render(self, name, value, attrs=None, choices=()):
730 return self.get_renderer(name, value, attrs, choices).render()
731
732 def id_for_label(self, id_):
733 # RadioSelect is represented by multiple <input type="radio"> fields,
734 # each of which has a distinct ID. The IDs are made distinct by a "_X"
735 # suffix, where X is the zero-based index of the radio field. Thus,
736 # the label for a RadioSelect should reference the first one ('_0').
737 if id_:
738 id_ += '_0'
739 return id_
740
741class CheckboxSelectMultiple(SelectMultiple):
742 def render(self, name, value, attrs=None, choices=()):
743 if value is None: value = []
744 has_id = attrs and 'id' in attrs
745 final_attrs = self.build_attrs(attrs, name=name)
746 output = [u'<ul>']
747 # Normalize to strings
748 str_values = set([force_unicode(v) for v in value])
749 for i, (option_value, option_label) in enumerate(chain(self.choices, choices)):
750 # If an ID attribute was given, add a numeric index as a suffix,
751 # so that the checkboxes don't all have the same ID attribute.
752 if has_id:
753 final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i))
754 label_for = u' for="%s"' % final_attrs['id']
755 else:
756 label_for = ''
757
758 cb = CheckboxInput(final_attrs, check_test=lambda value: value in str_values)
759 option_value = force_unicode(option_value)
760 rendered_cb = cb.render(name, option_value)
761 option_label = conditional_escape(force_unicode(option_label))
762 output.append(u'<li><label%s>%s %s</label></li>' % (label_for, rendered_cb, option_label))
763 output.append(u'</ul>')
764 return mark_safe(u'\n'.join(output))
765
766 def id_for_label(self, id_):
767 # See the comment for RadioSelect.id_for_label()
768 if id_:
769 id_ += '_0'
770 return id_
771
772class MultiWidget(Widget):
773 """
774 A widget that is composed of multiple widgets.
775
776 Its render() method is different than other widgets', because it has to
777 figure out how to split a single value for display in multiple widgets.
778 The ``value`` argument can be one of two things:
779
780 * A list.
781 * A normal value (e.g., a string) that has been "compressed" from
782 a list of values.
783
784 In the second case -- i.e., if the value is NOT a list -- render() will
785 first "decompress" the value into a list before rendering it. It does so by
786 calling the decompress() method, which MultiWidget subclasses must
787 implement. This method takes a single "compressed" value and returns a
788 list.
789
790 When render() does its HTML rendering, each value in the list is rendered
791 with the corresponding widget -- the first value is rendered in the first
792 widget, the second value is rendered in the second widget, etc.
793
794 Subclasses may implement format_output(), which takes the list of rendered
795 widgets and returns a string of HTML that formats them any way you'd like.
796
797 You'll probably want to use this class with MultiValueField.
798 """
799 def __init__(self, widgets, attrs=None):
800 self.widgets = [isinstance(w, type) and w() or w for w in widgets]
801 super(MultiWidget, self).__init__(attrs)
802
803 def render(self, name, value, attrs=None):
804 if self.is_localized:
805 for widget in self.widgets:
806 widget.is_localized = self.is_localized
807 # value is a list of values, each corresponding to a widget
808 # in self.widgets.
809 if not isinstance(value, list):
810 value = self.decompress(value)
811 output = []
812 final_attrs = self.build_attrs(attrs)
813 id_ = final_attrs.get('id', None)
814 for i, widget in enumerate(self.widgets):
815 try:
816 widget_value = value[i]
817 except IndexError:
818 widget_value = None
819 if id_:
820 final_attrs = dict(final_attrs, id='%s_%s' % (id_, i))
821 output.append(widget.render(name + '_%s' % i, widget_value, final_attrs))
822 return mark_safe(self.format_output(output))
823
824 def id_for_label(self, id_):
825 # See the comment for RadioSelect.id_for_label()
826 if id_:
827 id_ += '_0'
828 return id_
829
830 def value_from_datadict(self, data, files, name):
831 return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
832
833 def _has_changed(self, initial, data):
834 if initial is None:
835 initial = [u'' for x in range(0, len(data))]
836 else:
837 if not isinstance(initial, list):
838 initial = self.decompress(initial)
839 for widget, initial, data in zip(self.widgets, initial, data):
840 if widget._has_changed(initial, data):
841 return True
842 return False
843
844 def format_output(self, rendered_widgets):
845 """
846 Given a list of rendered widgets (as strings), returns a Unicode string
847 representing the HTML for the whole lot.
848
849 This hook allows you to format the HTML design of the widgets, if
850 needed.
851 """
852 return u''.join(rendered_widgets)
853
854 def decompress(self, value):
855 """
856 Returns a list of decompressed values for the given compressed value.
857 The given value can be assumed to be valid, but not necessarily
858 non-empty.
859 """
860 raise NotImplementedError('Subclasses must implement this method.')
861
862 def _get_media(self):
863 "Media for a multiwidget is the combination of all media of the subwidgets"
864 media = Media()
865 for w in self.widgets:
866 media = media + w.media
867 return media
868 media = property(_get_media)
869
870 def __deepcopy__(self, memo):
871 obj = super(MultiWidget, self).__deepcopy__(memo)
872 obj.widgets = copy.deepcopy(self.widgets)
873 return obj
874
875class SplitDateTimeWidget(MultiWidget):
876 """
877 A Widget that splits datetime input into two <input type="text"> boxes.
878 """
879
880 def __init__(self, attrs=None, date_format=None, time_format=None):
881 widgets = (DateInput(attrs=attrs, format=date_format),
882 TimeInput(attrs=attrs, format=time_format))
883 super(SplitDateTimeWidget, self).__init__(widgets, attrs)
884
885 def decompress(self, value):
886 if value:
887 value = to_current_timezone(value)
888 return [value.date(), value.time().replace(microsecond=0)]
889 return [None, None]
890
891class SplitHiddenDateTimeWidget(SplitDateTimeWidget):
892 """
893 A Widget that splits datetime input into two <input type="hidden"> inputs.
894 """
895 is_hidden = True
896
897 def __init__(self, attrs=None, date_format=None, time_format=None):
898 super(SplitHiddenDateTimeWidget, self).__init__(attrs, date_format, time_format)
899 for widget in self.widgets:
900 widget.input_type = 'hidden'
901 widget.is_hidden = True
Back to Top