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