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