app/django/forms/widgets.py
changeset 323 ff1a9aa48cfd
equal deleted inserted replaced
322:6641e941ef1e 323:ff1a9aa48cfd
       
     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', '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(**{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         self.attrs = {'cols': '40', 'rows': '10'}
       
   279         if attrs:
       
   280             self.attrs.update(attrs)
       
   281 
       
   282     def render(self, name, value, attrs=None):
       
   283         if value is None: value = ''
       
   284         final_attrs = self.build_attrs(attrs, name=name)
       
   285         return mark_safe(u'<textarea%s>%s</textarea>' % (flatatt(final_attrs),
       
   286                 conditional_escape(force_unicode(value))))
       
   287 
       
   288 class DateTimeInput(Input):
       
   289     input_type = 'text'
       
   290     format = '%Y-%m-%d %H:%M:%S'     # '2006-10-25 14:30:59'
       
   291 
       
   292     def __init__(self, attrs=None, format=None):
       
   293         super(DateTimeInput, self).__init__(attrs)
       
   294         if format:
       
   295             self.format = format
       
   296 
       
   297     def render(self, name, value, attrs=None):
       
   298         if value is None:
       
   299             value = ''
       
   300         elif hasattr(value, 'strftime'):
       
   301             value = datetime_safe.new_datetime(value)
       
   302             value = value.strftime(self.format)
       
   303         return super(DateTimeInput, self).render(name, value, attrs)
       
   304 
       
   305 class TimeInput(Input):
       
   306     input_type = 'text'
       
   307 
       
   308     def render(self, name, value, attrs=None):
       
   309         if value is None:
       
   310             value = ''
       
   311         elif isinstance(value, time):
       
   312             value = value.replace(microsecond=0)
       
   313         return super(TimeInput, self).render(name, value, attrs)
       
   314 
       
   315 class CheckboxInput(Widget):
       
   316     def __init__(self, attrs=None, check_test=bool):
       
   317         super(CheckboxInput, self).__init__(attrs)
       
   318         # check_test is a callable that takes a value and returns True
       
   319         # if the checkbox should be checked for that value.
       
   320         self.check_test = check_test
       
   321 
       
   322     def render(self, name, value, attrs=None):
       
   323         final_attrs = self.build_attrs(attrs, type='checkbox', name=name)
       
   324         try:
       
   325             result = self.check_test(value)
       
   326         except: # Silently catch exceptions
       
   327             result = False
       
   328         if result:
       
   329             final_attrs['checked'] = 'checked'
       
   330         if value not in ('', True, False, None):
       
   331             # Only add the 'value' attribute if a value is non-empty.
       
   332             final_attrs['value'] = force_unicode(value)
       
   333         return mark_safe(u'<input%s />' % flatatt(final_attrs))
       
   334 
       
   335     def value_from_datadict(self, data, files, name):
       
   336         if name not in data:
       
   337             # A missing value means False because HTML form submission does not
       
   338             # send results for unselected checkboxes.
       
   339             return False
       
   340         return super(CheckboxInput, self).value_from_datadict(data, files, name)
       
   341 
       
   342     def _has_changed(self, initial, data):
       
   343         # Sometimes data or initial could be None or u'' which should be the
       
   344         # same thing as False.
       
   345         return bool(initial) != bool(data)
       
   346 
       
   347 class Select(Widget):
       
   348     def __init__(self, attrs=None, choices=()):
       
   349         super(Select, self).__init__(attrs)
       
   350         # choices can be any iterable, but we may need to render this widget
       
   351         # multiple times. Thus, collapse it into a list so it can be consumed
       
   352         # more than once.
       
   353         self.choices = list(choices)
       
   354 
       
   355     def render(self, name, value, attrs=None, choices=()):
       
   356         if value is None: value = ''
       
   357         final_attrs = self.build_attrs(attrs, name=name)
       
   358         output = [u'<select%s>' % flatatt(final_attrs)]
       
   359         options = self.render_options(choices, [value])
       
   360         if options:
       
   361             output.append(options)
       
   362         output.append('</select>')
       
   363         return mark_safe(u'\n'.join(output))
       
   364 
       
   365     def render_options(self, choices, selected_choices):
       
   366         def render_option(option_value, option_label):
       
   367             option_value = force_unicode(option_value)
       
   368             selected_html = (option_value in selected_choices) and u' selected="selected"' or ''
       
   369             return u'<option value="%s"%s>%s</option>' % (
       
   370                 escape(option_value), selected_html,
       
   371                 conditional_escape(force_unicode(option_label)))
       
   372         # Normalize to strings.
       
   373         selected_choices = set([force_unicode(v) for v in selected_choices])
       
   374         output = []
       
   375         for option_value, option_label in chain(self.choices, choices):
       
   376             if isinstance(option_label, (list, tuple)):
       
   377                 output.append(u'<optgroup label="%s">' % escape(force_unicode(option_value)))
       
   378                 for option in option_label:
       
   379                     output.append(render_option(*option))
       
   380                 output.append(u'</optgroup>')
       
   381             else:
       
   382                 output.append(render_option(option_value, option_label))
       
   383         return u'\n'.join(output)
       
   384 
       
   385 class NullBooleanSelect(Select):
       
   386     """
       
   387     A Select Widget intended to be used with NullBooleanField.
       
   388     """
       
   389     def __init__(self, attrs=None):
       
   390         choices = ((u'1', ugettext('Unknown')), (u'2', ugettext('Yes')), (u'3', ugettext('No')))
       
   391         super(NullBooleanSelect, self).__init__(attrs, choices)
       
   392 
       
   393     def render(self, name, value, attrs=None, choices=()):
       
   394         try:
       
   395             value = {True: u'2', False: u'3', u'2': u'2', u'3': u'3'}[value]
       
   396         except KeyError:
       
   397             value = u'1'
       
   398         return super(NullBooleanSelect, self).render(name, value, attrs, choices)
       
   399 
       
   400     def value_from_datadict(self, data, files, name):
       
   401         value = data.get(name, None)
       
   402         return {u'2': True, u'3': False, True: True, False: False}.get(value, None)
       
   403 
       
   404     def _has_changed(self, initial, data):
       
   405         # Sometimes data or initial could be None or u'' which should be the
       
   406         # same thing as False.
       
   407         return bool(initial) != bool(data)
       
   408 
       
   409 class SelectMultiple(Select):
       
   410     def render(self, name, value, attrs=None, choices=()):
       
   411         if value is None: value = []
       
   412         final_attrs = self.build_attrs(attrs, name=name)
       
   413         output = [u'<select multiple="multiple"%s>' % flatatt(final_attrs)]
       
   414         options = self.render_options(choices, value)
       
   415         if options:
       
   416             output.append(options)
       
   417         output.append('</select>')
       
   418         return mark_safe(u'\n'.join(output))
       
   419 
       
   420     def value_from_datadict(self, data, files, name):
       
   421         if isinstance(data, (MultiValueDict, MergeDict)):
       
   422             return data.getlist(name)
       
   423         return data.get(name, None)
       
   424 
       
   425     def _has_changed(self, initial, data):
       
   426         if initial is None:
       
   427             initial = []
       
   428         if data is None:
       
   429             data = []
       
   430         if len(initial) != len(data):
       
   431             return True
       
   432         for value1, value2 in zip(initial, data):
       
   433             if force_unicode(value1) != force_unicode(value2):
       
   434                 return True
       
   435         return False
       
   436 
       
   437 class RadioInput(StrAndUnicode):
       
   438     """
       
   439     An object used by RadioFieldRenderer that represents a single
       
   440     <input type='radio'>.
       
   441     """
       
   442 
       
   443     def __init__(self, name, value, attrs, choice, index):
       
   444         self.name, self.value = name, value
       
   445         self.attrs = attrs
       
   446         self.choice_value = force_unicode(choice[0])
       
   447         self.choice_label = force_unicode(choice[1])
       
   448         self.index = index
       
   449 
       
   450     def __unicode__(self):
       
   451         if 'id' in self.attrs:
       
   452             label_for = ' for="%s_%s"' % (self.attrs['id'], self.index)
       
   453         else:
       
   454             label_for = ''
       
   455         choice_label = conditional_escape(force_unicode(self.choice_label))
       
   456         return mark_safe(u'<label%s>%s %s</label>' % (label_for, self.tag(), choice_label))
       
   457 
       
   458     def is_checked(self):
       
   459         return self.value == self.choice_value
       
   460 
       
   461     def tag(self):
       
   462         if 'id' in self.attrs:
       
   463             self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index)
       
   464         final_attrs = dict(self.attrs, type='radio', name=self.name, value=self.choice_value)
       
   465         if self.is_checked():
       
   466             final_attrs['checked'] = 'checked'
       
   467         return mark_safe(u'<input%s />' % flatatt(final_attrs))
       
   468 
       
   469 class RadioFieldRenderer(StrAndUnicode):
       
   470     """
       
   471     An object used by RadioSelect to enable customization of radio widgets.
       
   472     """
       
   473 
       
   474     def __init__(self, name, value, attrs, choices):
       
   475         self.name, self.value, self.attrs = name, value, attrs
       
   476         self.choices = choices
       
   477 
       
   478     def __iter__(self):
       
   479         for i, choice in enumerate(self.choices):
       
   480             yield RadioInput(self.name, self.value, self.attrs.copy(), choice, i)
       
   481 
       
   482     def __getitem__(self, idx):
       
   483         choice = self.choices[idx] # Let the IndexError propogate
       
   484         return RadioInput(self.name, self.value, self.attrs.copy(), choice, idx)
       
   485 
       
   486     def __unicode__(self):
       
   487         return self.render()
       
   488 
       
   489     def render(self):
       
   490         """Outputs a <ul> for this set of radio fields."""
       
   491         return mark_safe(u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>'
       
   492                 % force_unicode(w) for w in self]))
       
   493 
       
   494 class RadioSelect(Select):
       
   495     renderer = RadioFieldRenderer
       
   496 
       
   497     def __init__(self, *args, **kwargs):
       
   498         # Override the default renderer if we were passed one.
       
   499         renderer = kwargs.pop('renderer', None)
       
   500         if renderer:
       
   501             self.renderer = renderer
       
   502         super(RadioSelect, self).__init__(*args, **kwargs)
       
   503 
       
   504     def get_renderer(self, name, value, attrs=None, choices=()):
       
   505         """Returns an instance of the renderer."""
       
   506         if value is None: value = ''
       
   507         str_value = force_unicode(value) # Normalize to string.
       
   508         final_attrs = self.build_attrs(attrs)
       
   509         choices = list(chain(self.choices, choices))
       
   510         return self.renderer(name, str_value, final_attrs, choices)
       
   511 
       
   512     def render(self, name, value, attrs=None, choices=()):
       
   513         return self.get_renderer(name, value, attrs, choices).render()
       
   514 
       
   515     def id_for_label(self, id_):
       
   516         # RadioSelect is represented by multiple <input type="radio"> fields,
       
   517         # each of which has a distinct ID. The IDs are made distinct by a "_X"
       
   518         # suffix, where X is the zero-based index of the radio field. Thus,
       
   519         # the label for a RadioSelect should reference the first one ('_0').
       
   520         if id_:
       
   521             id_ += '_0'
       
   522         return id_
       
   523     id_for_label = classmethod(id_for_label)
       
   524 
       
   525 class CheckboxSelectMultiple(SelectMultiple):
       
   526     def render(self, name, value, attrs=None, choices=()):
       
   527         if value is None: value = []
       
   528         has_id = attrs and 'id' in attrs
       
   529         final_attrs = self.build_attrs(attrs, name=name)
       
   530         output = [u'<ul>']
       
   531         # Normalize to strings
       
   532         str_values = set([force_unicode(v) for v in value])
       
   533         for i, (option_value, option_label) in enumerate(chain(self.choices, choices)):
       
   534             # If an ID attribute was given, add a numeric index as a suffix,
       
   535             # so that the checkboxes don't all have the same ID attribute.
       
   536             if has_id:
       
   537                 final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i))
       
   538                 label_for = u' for="%s"' % final_attrs['id']
       
   539             else:
       
   540                 label_for = ''
       
   541 
       
   542             cb = CheckboxInput(final_attrs, check_test=lambda value: value in str_values)
       
   543             option_value = force_unicode(option_value)
       
   544             rendered_cb = cb.render(name, option_value)
       
   545             option_label = conditional_escape(force_unicode(option_label))
       
   546             output.append(u'<li><label%s>%s %s</label></li>' % (label_for, rendered_cb, option_label))
       
   547         output.append(u'</ul>')
       
   548         return mark_safe(u'\n'.join(output))
       
   549 
       
   550     def id_for_label(self, id_):
       
   551         # See the comment for RadioSelect.id_for_label()
       
   552         if id_:
       
   553             id_ += '_0'
       
   554         return id_
       
   555     id_for_label = classmethod(id_for_label)
       
   556 
       
   557 class MultiWidget(Widget):
       
   558     """
       
   559     A widget that is composed of multiple widgets.
       
   560 
       
   561     Its render() method is different than other widgets', because it has to
       
   562     figure out how to split a single value for display in multiple widgets.
       
   563     The ``value`` argument can be one of two things:
       
   564 
       
   565         * A list.
       
   566         * A normal value (e.g., a string) that has been "compressed" from
       
   567           a list of values.
       
   568 
       
   569     In the second case -- i.e., if the value is NOT a list -- render() will
       
   570     first "decompress" the value into a list before rendering it. It does so by
       
   571     calling the decompress() method, which MultiWidget subclasses must
       
   572     implement. This method takes a single "compressed" value and returns a
       
   573     list.
       
   574 
       
   575     When render() does its HTML rendering, each value in the list is rendered
       
   576     with the corresponding widget -- the first value is rendered in the first
       
   577     widget, the second value is rendered in the second widget, etc.
       
   578 
       
   579     Subclasses may implement format_output(), which takes the list of rendered
       
   580     widgets and returns a string of HTML that formats them any way you'd like.
       
   581 
       
   582     You'll probably want to use this class with MultiValueField.
       
   583     """
       
   584     def __init__(self, widgets, attrs=None):
       
   585         self.widgets = [isinstance(w, type) and w() or w for w in widgets]
       
   586         super(MultiWidget, self).__init__(attrs)
       
   587 
       
   588     def render(self, name, value, attrs=None):
       
   589         # value is a list of values, each corresponding to a widget
       
   590         # in self.widgets.
       
   591         if not isinstance(value, list):
       
   592             value = self.decompress(value)
       
   593         output = []
       
   594         final_attrs = self.build_attrs(attrs)
       
   595         id_ = final_attrs.get('id', None)
       
   596         for i, widget in enumerate(self.widgets):
       
   597             try:
       
   598                 widget_value = value[i]
       
   599             except IndexError:
       
   600                 widget_value = None
       
   601             if id_:
       
   602                 final_attrs = dict(final_attrs, id='%s_%s' % (id_, i))
       
   603             output.append(widget.render(name + '_%s' % i, widget_value, final_attrs))
       
   604         return mark_safe(self.format_output(output))
       
   605 
       
   606     def id_for_label(self, id_):
       
   607         # See the comment for RadioSelect.id_for_label()
       
   608         if id_:
       
   609             id_ += '_0'
       
   610         return id_
       
   611     id_for_label = classmethod(id_for_label)
       
   612 
       
   613     def value_from_datadict(self, data, files, name):
       
   614         return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
       
   615 
       
   616     def _has_changed(self, initial, data):
       
   617         if initial is None:
       
   618             initial = [u'' for x in range(0, len(data))]
       
   619         else:
       
   620             if not isinstance(initial, list):
       
   621                 initial = self.decompress(initial)
       
   622         for widget, initial, data in zip(self.widgets, initial, data):
       
   623             if widget._has_changed(initial, data):
       
   624                 return True
       
   625         return False
       
   626 
       
   627     def format_output(self, rendered_widgets):
       
   628         """
       
   629         Given a list of rendered widgets (as strings), returns a Unicode string
       
   630         representing the HTML for the whole lot.
       
   631 
       
   632         This hook allows you to format the HTML design of the widgets, if
       
   633         needed.
       
   634         """
       
   635         return u''.join(rendered_widgets)
       
   636 
       
   637     def decompress(self, value):
       
   638         """
       
   639         Returns a list of decompressed values for the given compressed value.
       
   640         The given value can be assumed to be valid, but not necessarily
       
   641         non-empty.
       
   642         """
       
   643         raise NotImplementedError('Subclasses must implement this method.')
       
   644 
       
   645     def _get_media(self):
       
   646         "Media for a multiwidget is the combination of all media of the subwidgets"
       
   647         media = Media()
       
   648         for w in self.widgets:
       
   649             media = media + w.media
       
   650         return media
       
   651     media = property(_get_media)
       
   652 
       
   653 class SplitDateTimeWidget(MultiWidget):
       
   654     """
       
   655     A Widget that splits datetime input into two <input type="text"> boxes.
       
   656     """
       
   657     def __init__(self, attrs=None):
       
   658         widgets = (TextInput(attrs=attrs), TextInput(attrs=attrs))
       
   659         super(SplitDateTimeWidget, self).__init__(widgets, attrs)
       
   660 
       
   661     def decompress(self, value):
       
   662         if value:
       
   663             return [value.date(), value.time().replace(microsecond=0)]
       
   664         return [None, None]
       
   665 
       
   666 class SplitHiddenDateTimeWidget(SplitDateTimeWidget):
       
   667     """
       
   668     A Widget that splits datetime input into two <input type="hidden"> inputs.
       
   669     """
       
   670     def __init__(self, attrs=None):
       
   671         widgets = (HiddenInput(attrs=attrs), HiddenInput(attrs=attrs))
       
   672         super(SplitDateTimeWidget, self).__init__(widgets, attrs)
       
   673