app/django/newforms/widgets.py
changeset 54 03e267d67478
equal deleted inserted replaced
53:57b4279d8c4e 54:03e267d67478
       
     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 
       
    13 from django.utils.datastructures import MultiValueDict
       
    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 util import flatatt
       
    19 
       
    20 __all__ = (
       
    21     'Widget', 'TextInput', 'PasswordInput',
       
    22     'HiddenInput', 'MultipleHiddenInput',
       
    23     'FileInput', 'DateTimeInput', 'Textarea', 'CheckboxInput',
       
    24     'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
       
    25     'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget',
       
    26 )
       
    27 
       
    28 class Widget(object):
       
    29     is_hidden = False          # Determines whether this corresponds to an <input type="hidden">.
       
    30     needs_multipart_form = False # Determines does this widget need multipart-encrypted form
       
    31 
       
    32     def __init__(self, attrs=None):
       
    33         if attrs is not None:
       
    34             self.attrs = attrs.copy()
       
    35         else:
       
    36             self.attrs = {}
       
    37 
       
    38     def __deepcopy__(self, memo):
       
    39         obj = copy.copy(self)
       
    40         obj.attrs = self.attrs.copy()
       
    41         memo[id(self)] = obj
       
    42         return obj
       
    43 
       
    44     def render(self, name, value, attrs=None):
       
    45         """
       
    46         Returns this Widget rendered as HTML, as a Unicode string.
       
    47 
       
    48         The 'value' given is not guaranteed to be valid input, so subclass
       
    49         implementations should program defensively.
       
    50         """
       
    51         raise NotImplementedError
       
    52 
       
    53     def build_attrs(self, extra_attrs=None, **kwargs):
       
    54         "Helper function for building an attribute dictionary."
       
    55         attrs = dict(self.attrs, **kwargs)
       
    56         if extra_attrs:
       
    57             attrs.update(extra_attrs)
       
    58         return attrs
       
    59 
       
    60     def value_from_datadict(self, data, files, name):
       
    61         """
       
    62         Given a dictionary of data and this widget's name, returns the value
       
    63         of this widget. Returns None if it's not provided.
       
    64         """
       
    65         return data.get(name, None)
       
    66 
       
    67     def id_for_label(self, id_):
       
    68         """
       
    69         Returns the HTML ID attribute of this Widget for use by a <label>,
       
    70         given the ID of the field. Returns None if no ID is available.
       
    71 
       
    72         This hook is necessary because some widgets have multiple HTML
       
    73         elements and, thus, multiple IDs. In that case, this method should
       
    74         return an ID value that corresponds to the first ID in the widget's
       
    75         tags.
       
    76         """
       
    77         return id_
       
    78     id_for_label = classmethod(id_for_label)
       
    79 
       
    80 class Input(Widget):
       
    81     """
       
    82     Base class for all <input> widgets (except type='checkbox' and
       
    83     type='radio', which are special).
       
    84     """
       
    85     input_type = None # Subclasses must define this.
       
    86 
       
    87     def render(self, name, value, attrs=None):
       
    88         if value is None: value = ''
       
    89         final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
       
    90         if value != '':
       
    91             # Only add the 'value' attribute if a value is non-empty.
       
    92             final_attrs['value'] = force_unicode(value)
       
    93         return mark_safe(u'<input%s />' % flatatt(final_attrs))
       
    94 
       
    95 class TextInput(Input):
       
    96     input_type = 'text'
       
    97 
       
    98 class PasswordInput(Input):
       
    99     input_type = 'password'
       
   100 
       
   101     def __init__(self, attrs=None, render_value=True):
       
   102         super(PasswordInput, self).__init__(attrs)
       
   103         self.render_value = render_value
       
   104 
       
   105     def render(self, name, value, attrs=None):
       
   106         if not self.render_value: value=None
       
   107         return super(PasswordInput, self).render(name, value, attrs)
       
   108 
       
   109 class HiddenInput(Input):
       
   110     input_type = 'hidden'
       
   111     is_hidden = True
       
   112 
       
   113 class MultipleHiddenInput(HiddenInput):
       
   114     """
       
   115     A widget that handles <input type="hidden"> for fields that have a list
       
   116     of values.
       
   117     """
       
   118     def __init__(self, attrs=None, choices=()):
       
   119         super(MultipleHiddenInput, self).__init__(attrs)
       
   120         # choices can be any iterable
       
   121         self.choices = choices
       
   122 
       
   123     def render(self, name, value, attrs=None, choices=()):
       
   124         if value is None: value = []
       
   125         final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
       
   126         return mark_safe(u'\n'.join([(u'<input%s />' %
       
   127             flatatt(dict(value=force_unicode(v), **final_attrs)))
       
   128             for v in value]))
       
   129 
       
   130     def value_from_datadict(self, data, files, name):
       
   131         if isinstance(data, MultiValueDict):
       
   132             return data.getlist(name)
       
   133         return data.get(name, None)
       
   134 
       
   135 class FileInput(Input):
       
   136     input_type = 'file'
       
   137     needs_multipart_form = True
       
   138 
       
   139     def render(self, name, value, attrs=None):
       
   140         return super(FileInput, self).render(name, None, attrs=attrs)
       
   141 
       
   142     def value_from_datadict(self, data, files, name):
       
   143         "File widgets take data from FILES, not POST"
       
   144         return files.get(name, None)
       
   145 
       
   146 class Textarea(Widget):
       
   147     def __init__(self, attrs=None):
       
   148         # The 'rows' and 'cols' attributes are required for HTML correctness.
       
   149         self.attrs = {'cols': '40', 'rows': '10'}
       
   150         if attrs:
       
   151             self.attrs.update(attrs)
       
   152 
       
   153     def render(self, name, value, attrs=None):
       
   154         if value is None: value = ''
       
   155         value = force_unicode(value)
       
   156         final_attrs = self.build_attrs(attrs, name=name)
       
   157         return mark_safe(u'<textarea%s>%s</textarea>' % (flatatt(final_attrs),
       
   158                 conditional_escape(force_unicode(value))))
       
   159 
       
   160 class DateTimeInput(Input):
       
   161     input_type = 'text'
       
   162     format = '%Y-%m-%d %H:%M:%S'     # '2006-10-25 14:30:59'
       
   163 
       
   164     def __init__(self, attrs=None, format=None):
       
   165         super(DateTimeInput, self).__init__(attrs)
       
   166         if format:
       
   167             self.format = format
       
   168 
       
   169     def render(self, name, value, attrs=None):
       
   170         if value is None:
       
   171             value = ''
       
   172         elif hasattr(value, 'strftime'):
       
   173             value = value.strftime(self.format)
       
   174         return super(DateTimeInput, self).render(name, value, attrs)
       
   175 
       
   176 class CheckboxInput(Widget):
       
   177     def __init__(self, attrs=None, check_test=bool):
       
   178         super(CheckboxInput, self).__init__(attrs)
       
   179         # check_test is a callable that takes a value and returns True
       
   180         # if the checkbox should be checked for that value.
       
   181         self.check_test = check_test
       
   182 
       
   183     def render(self, name, value, attrs=None):
       
   184         final_attrs = self.build_attrs(attrs, type='checkbox', name=name)
       
   185         try:
       
   186             result = self.check_test(value)
       
   187         except: # Silently catch exceptions
       
   188             result = False
       
   189         if result:
       
   190             final_attrs['checked'] = 'checked'
       
   191         if value not in ('', True, False, None):
       
   192             # Only add the 'value' attribute if a value is non-empty.
       
   193             final_attrs['value'] = force_unicode(value)
       
   194         return mark_safe(u'<input%s />' % flatatt(final_attrs))
       
   195 
       
   196     def value_from_datadict(self, data, files, name):
       
   197         if name not in data:
       
   198             # A missing value means False because HTML form submission does not
       
   199             # send results for unselected checkboxes.
       
   200             return False
       
   201         return super(CheckboxInput, self).value_from_datadict(data, files, name)
       
   202 
       
   203 class Select(Widget):
       
   204     def __init__(self, attrs=None, choices=()):
       
   205         super(Select, self).__init__(attrs)
       
   206         # choices can be any iterable, but we may need to render this widget
       
   207         # multiple times. Thus, collapse it into a list so it can be consumed
       
   208         # more than once.
       
   209         self.choices = list(choices)
       
   210 
       
   211     def render(self, name, value, attrs=None, choices=()):
       
   212         if value is None: value = ''
       
   213         final_attrs = self.build_attrs(attrs, name=name)
       
   214         output = [u'<select%s>' % flatatt(final_attrs)]
       
   215         # Normalize to string.
       
   216         str_value = force_unicode(value)
       
   217         for option_value, option_label in chain(self.choices, choices):
       
   218             option_value = force_unicode(option_value)
       
   219             selected_html = (option_value == str_value) and u' selected="selected"' or ''
       
   220             output.append(u'<option value="%s"%s>%s</option>' % (
       
   221                     escape(option_value), selected_html,
       
   222                     conditional_escape(force_unicode(option_label))))
       
   223         output.append(u'</select>')
       
   224         return mark_safe(u'\n'.join(output))
       
   225 
       
   226 class NullBooleanSelect(Select):
       
   227     """
       
   228     A Select Widget intended to be used with NullBooleanField.
       
   229     """
       
   230     def __init__(self, attrs=None):
       
   231         choices = ((u'1', ugettext('Unknown')), (u'2', ugettext('Yes')), (u'3', ugettext('No')))
       
   232         super(NullBooleanSelect, self).__init__(attrs, choices)
       
   233 
       
   234     def render(self, name, value, attrs=None, choices=()):
       
   235         try:
       
   236             value = {True: u'2', False: u'3', u'2': u'2', u'3': u'3'}[value]
       
   237         except KeyError:
       
   238             value = u'1'
       
   239         return super(NullBooleanSelect, self).render(name, value, attrs, choices)
       
   240 
       
   241     def value_from_datadict(self, data, files, name):
       
   242         value = data.get(name, None)
       
   243         return {u'2': True, u'3': False, True: True, False: False}.get(value, None)
       
   244 
       
   245 class SelectMultiple(Widget):
       
   246     def __init__(self, attrs=None, choices=()):
       
   247         super(SelectMultiple, self).__init__(attrs)
       
   248         # choices can be any iterable
       
   249         self.choices = choices
       
   250 
       
   251     def render(self, name, value, attrs=None, choices=()):
       
   252         if value is None: value = []
       
   253         final_attrs = self.build_attrs(attrs, name=name)
       
   254         output = [u'<select multiple="multiple"%s>' % flatatt(final_attrs)]
       
   255         str_values = set([force_unicode(v) for v in value]) # Normalize to strings.
       
   256         for option_value, option_label in chain(self.choices, choices):
       
   257             option_value = force_unicode(option_value)
       
   258             selected_html = (option_value in str_values) and ' selected="selected"' or ''
       
   259             output.append(u'<option value="%s"%s>%s</option>' % (
       
   260                     escape(option_value), selected_html,
       
   261                     conditional_escape(force_unicode(option_label))))
       
   262         output.append(u'</select>')
       
   263         return mark_safe(u'\n'.join(output))
       
   264 
       
   265     def value_from_datadict(self, data, files, name):
       
   266         if isinstance(data, MultiValueDict):
       
   267             return data.getlist(name)
       
   268         return data.get(name, None)
       
   269 
       
   270 class RadioInput(StrAndUnicode):
       
   271     """
       
   272     An object used by RadioFieldRenderer that represents a single
       
   273     <input type='radio'>.
       
   274     """
       
   275 
       
   276     def __init__(self, name, value, attrs, choice, index):
       
   277         self.name, self.value = name, value
       
   278         self.attrs = attrs
       
   279         self.choice_value = force_unicode(choice[0])
       
   280         self.choice_label = force_unicode(choice[1])
       
   281         self.index = index
       
   282 
       
   283     def __unicode__(self):
       
   284         return mark_safe(u'<label>%s %s</label>' % (self.tag(),
       
   285                 conditional_escape(force_unicode(self.choice_label))))
       
   286 
       
   287     def is_checked(self):
       
   288         return self.value == self.choice_value
       
   289 
       
   290     def tag(self):
       
   291         if 'id' in self.attrs:
       
   292             self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index)
       
   293         final_attrs = dict(self.attrs, type='radio', name=self.name, value=self.choice_value)
       
   294         if self.is_checked():
       
   295             final_attrs['checked'] = 'checked'
       
   296         return mark_safe(u'<input%s />' % flatatt(final_attrs))
       
   297 
       
   298 class RadioFieldRenderer(StrAndUnicode):
       
   299     """
       
   300     An object used by RadioSelect to enable customization of radio widgets.
       
   301     """
       
   302 
       
   303     def __init__(self, name, value, attrs, choices):
       
   304         self.name, self.value, self.attrs = name, value, attrs
       
   305         self.choices = choices
       
   306 
       
   307     def __iter__(self):
       
   308         for i, choice in enumerate(self.choices):
       
   309             yield RadioInput(self.name, self.value, self.attrs.copy(), choice, i)
       
   310 
       
   311     def __getitem__(self, idx):
       
   312         choice = self.choices[idx] # Let the IndexError propogate
       
   313         return RadioInput(self.name, self.value, self.attrs.copy(), choice, idx)
       
   314 
       
   315     def __unicode__(self):
       
   316         return self.render()
       
   317 
       
   318     def render(self):
       
   319         """Outputs a <ul> for this set of radio fields."""
       
   320         return mark_safe(u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>'
       
   321                 % force_unicode(w) for w in self]))
       
   322 
       
   323 class RadioSelect(Select):
       
   324     renderer = RadioFieldRenderer
       
   325 
       
   326     def __init__(self, *args, **kwargs):
       
   327         # Override the default renderer if we were passed one.
       
   328         renderer = kwargs.pop('renderer', None)
       
   329         if renderer:
       
   330             self.renderer = renderer
       
   331         super(RadioSelect, self).__init__(*args, **kwargs)
       
   332 
       
   333     def get_renderer(self, name, value, attrs=None, choices=()):
       
   334         """Returns an instance of the renderer."""
       
   335         if value is None: value = ''
       
   336         str_value = force_unicode(value) # Normalize to string.
       
   337         final_attrs = self.build_attrs(attrs)
       
   338         choices = list(chain(self.choices, choices))
       
   339         return self.renderer(name, str_value, final_attrs, choices)
       
   340 
       
   341     def render(self, name, value, attrs=None, choices=()):
       
   342         return self.get_renderer(name, value, attrs, choices).render()
       
   343 
       
   344     def id_for_label(self, id_):
       
   345         # RadioSelect is represented by multiple <input type="radio"> fields,
       
   346         # each of which has a distinct ID. The IDs are made distinct by a "_X"
       
   347         # suffix, where X is the zero-based index of the radio field. Thus,
       
   348         # the label for a RadioSelect should reference the first one ('_0').
       
   349         if id_:
       
   350             id_ += '_0'
       
   351         return id_
       
   352     id_for_label = classmethod(id_for_label)
       
   353 
       
   354 class CheckboxSelectMultiple(SelectMultiple):
       
   355     def render(self, name, value, attrs=None, choices=()):
       
   356         if value is None: value = []
       
   357         has_id = attrs and 'id' in attrs
       
   358         final_attrs = self.build_attrs(attrs, name=name)
       
   359         output = [u'<ul>']
       
   360         # Normalize to strings
       
   361         str_values = set([force_unicode(v) for v in value])
       
   362         for i, (option_value, option_label) in enumerate(chain(self.choices, choices)):
       
   363             # If an ID attribute was given, add a numeric index as a suffix,
       
   364             # so that the checkboxes don't all have the same ID attribute.
       
   365             if has_id:
       
   366                 final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i))
       
   367             cb = CheckboxInput(final_attrs, check_test=lambda value: value in str_values)
       
   368             option_value = force_unicode(option_value)
       
   369             rendered_cb = cb.render(name, option_value)
       
   370             output.append(u'<li><label>%s %s</label></li>' % (rendered_cb,
       
   371                     conditional_escape(force_unicode(option_label))))
       
   372         output.append(u'</ul>')
       
   373         return mark_safe(u'\n'.join(output))
       
   374 
       
   375     def id_for_label(self, id_):
       
   376         # See the comment for RadioSelect.id_for_label()
       
   377         if id_:
       
   378             id_ += '_0'
       
   379         return id_
       
   380     id_for_label = classmethod(id_for_label)
       
   381 
       
   382 class MultiWidget(Widget):
       
   383     """
       
   384     A widget that is composed of multiple widgets.
       
   385 
       
   386     Its render() method is different than other widgets', because it has to
       
   387     figure out how to split a single value for display in multiple widgets.
       
   388     The ``value`` argument can be one of two things:
       
   389 
       
   390         * A list.
       
   391         * A normal value (e.g., a string) that has been "compressed" from
       
   392           a list of values.
       
   393 
       
   394     In the second case -- i.e., if the value is NOT a list -- render() will
       
   395     first "decompress" the value into a list before rendering it. It does so by
       
   396     calling the decompress() method, which MultiWidget subclasses must
       
   397     implement. This method takes a single "compressed" value and returns a
       
   398     list.
       
   399 
       
   400     When render() does its HTML rendering, each value in the list is rendered
       
   401     with the corresponding widget -- the first value is rendered in the first
       
   402     widget, the second value is rendered in the second widget, etc.
       
   403 
       
   404     Subclasses may implement format_output(), which takes the list of rendered
       
   405     widgets and returns a string of HTML that formats them any way you'd like.
       
   406 
       
   407     You'll probably want to use this class with MultiValueField.
       
   408     """
       
   409     def __init__(self, widgets, attrs=None):
       
   410         self.widgets = [isinstance(w, type) and w() or w for w in widgets]
       
   411         super(MultiWidget, self).__init__(attrs)
       
   412 
       
   413     def render(self, name, value, attrs=None):
       
   414         # value is a list of values, each corresponding to a widget
       
   415         # in self.widgets.
       
   416         if not isinstance(value, list):
       
   417             value = self.decompress(value)
       
   418         output = []
       
   419         final_attrs = self.build_attrs(attrs)
       
   420         id_ = final_attrs.get('id', None)
       
   421         for i, widget in enumerate(self.widgets):
       
   422             try:
       
   423                 widget_value = value[i]
       
   424             except IndexError:
       
   425                 widget_value = None
       
   426             if id_:
       
   427                 final_attrs = dict(final_attrs, id='%s_%s' % (id_, i))
       
   428             output.append(widget.render(name + '_%s' % i, widget_value, final_attrs))
       
   429         return mark_safe(self.format_output(output))
       
   430 
       
   431     def id_for_label(self, id_):
       
   432         # See the comment for RadioSelect.id_for_label()
       
   433         if id_:
       
   434             id_ += '_0'
       
   435         return id_
       
   436     id_for_label = classmethod(id_for_label)
       
   437 
       
   438     def value_from_datadict(self, data, files, name):
       
   439         return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
       
   440 
       
   441     def format_output(self, rendered_widgets):
       
   442         """
       
   443         Given a list of rendered widgets (as strings), returns a Unicode string
       
   444         representing the HTML for the whole lot.
       
   445 
       
   446         This hook allows you to format the HTML design of the widgets, if
       
   447         needed.
       
   448         """
       
   449         return u''.join(rendered_widgets)
       
   450 
       
   451     def decompress(self, value):
       
   452         """
       
   453         Returns a list of decompressed values for the given compressed value.
       
   454         The given value can be assumed to be valid, but not necessarily
       
   455         non-empty.
       
   456         """
       
   457         raise NotImplementedError('Subclasses must implement this method.')
       
   458 
       
   459 class SplitDateTimeWidget(MultiWidget):
       
   460     """
       
   461     A Widget that splits datetime input into two <input type="text"> boxes.
       
   462     """
       
   463     def __init__(self, attrs=None):
       
   464         widgets = (TextInput(attrs=attrs), TextInput(attrs=attrs))
       
   465         super(SplitDateTimeWidget, self).__init__(widgets, attrs)
       
   466 
       
   467     def decompress(self, value):
       
   468         if value:
       
   469             return [value.date(), value.time().replace(microsecond=0)]
       
   470         return [None, None]
       
   471