--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/django/newforms/widgets.py Fri Jul 18 18:22:23 2008 +0000
@@ -0,0 +1,471 @@
+"""
+HTML Widget classes
+"""
+
+try:
+ set
+except NameError:
+ from sets import Set as set # Python 2.3 fallback
+
+import copy
+from itertools import chain
+
+from django.utils.datastructures import MultiValueDict
+from django.utils.html import escape, conditional_escape
+from django.utils.translation import ugettext
+from django.utils.encoding import StrAndUnicode, force_unicode
+from django.utils.safestring import mark_safe
+from util import flatatt
+
+__all__ = (
+ 'Widget', 'TextInput', 'PasswordInput',
+ 'HiddenInput', 'MultipleHiddenInput',
+ 'FileInput', 'DateTimeInput', 'Textarea', 'CheckboxInput',
+ 'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
+ 'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget',
+)
+
+class Widget(object):
+ is_hidden = False # Determines whether this corresponds to an <input type="hidden">.
+ needs_multipart_form = False # Determines does this widget need multipart-encrypted form
+
+ def __init__(self, attrs=None):
+ if attrs is not None:
+ self.attrs = attrs.copy()
+ else:
+ self.attrs = {}
+
+ def __deepcopy__(self, memo):
+ obj = copy.copy(self)
+ obj.attrs = self.attrs.copy()
+ memo[id(self)] = obj
+ return obj
+
+ def render(self, name, value, attrs=None):
+ """
+ Returns this Widget rendered as HTML, as a Unicode string.
+
+ The 'value' given is not guaranteed to be valid input, so subclass
+ implementations should program defensively.
+ """
+ raise NotImplementedError
+
+ def build_attrs(self, extra_attrs=None, **kwargs):
+ "Helper function for building an attribute dictionary."
+ attrs = dict(self.attrs, **kwargs)
+ if extra_attrs:
+ attrs.update(extra_attrs)
+ return attrs
+
+ def value_from_datadict(self, data, files, name):
+ """
+ Given a dictionary of data and this widget's name, returns the value
+ of this widget. Returns None if it's not provided.
+ """
+ return data.get(name, None)
+
+ def id_for_label(self, id_):
+ """
+ Returns the HTML ID attribute of this Widget for use by a <label>,
+ given the ID of the field. Returns None if no ID is available.
+
+ This hook is necessary because some widgets have multiple HTML
+ elements and, thus, multiple IDs. In that case, this method should
+ return an ID value that corresponds to the first ID in the widget's
+ tags.
+ """
+ return id_
+ id_for_label = classmethod(id_for_label)
+
+class Input(Widget):
+ """
+ Base class for all <input> widgets (except type='checkbox' and
+ type='radio', which are special).
+ """
+ input_type = None # Subclasses must define this.
+
+ def render(self, name, value, attrs=None):
+ if value is None: value = ''
+ final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
+ if value != '':
+ # Only add the 'value' attribute if a value is non-empty.
+ final_attrs['value'] = force_unicode(value)
+ return mark_safe(u'<input%s />' % flatatt(final_attrs))
+
+class TextInput(Input):
+ input_type = 'text'
+
+class PasswordInput(Input):
+ input_type = 'password'
+
+ def __init__(self, attrs=None, render_value=True):
+ super(PasswordInput, self).__init__(attrs)
+ self.render_value = render_value
+
+ def render(self, name, value, attrs=None):
+ if not self.render_value: value=None
+ return super(PasswordInput, self).render(name, value, attrs)
+
+class HiddenInput(Input):
+ input_type = 'hidden'
+ is_hidden = True
+
+class MultipleHiddenInput(HiddenInput):
+ """
+ A widget that handles <input type="hidden"> for fields that have a list
+ of values.
+ """
+ def __init__(self, attrs=None, choices=()):
+ super(MultipleHiddenInput, self).__init__(attrs)
+ # choices can be any iterable
+ self.choices = choices
+
+ def render(self, name, value, attrs=None, choices=()):
+ if value is None: value = []
+ final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
+ return mark_safe(u'\n'.join([(u'<input%s />' %
+ flatatt(dict(value=force_unicode(v), **final_attrs)))
+ for v in value]))
+
+ def value_from_datadict(self, data, files, name):
+ if isinstance(data, MultiValueDict):
+ return data.getlist(name)
+ return data.get(name, None)
+
+class FileInput(Input):
+ input_type = 'file'
+ needs_multipart_form = True
+
+ def render(self, name, value, attrs=None):
+ return super(FileInput, self).render(name, None, attrs=attrs)
+
+ def value_from_datadict(self, data, files, name):
+ "File widgets take data from FILES, not POST"
+ return files.get(name, None)
+
+class Textarea(Widget):
+ def __init__(self, attrs=None):
+ # The 'rows' and 'cols' attributes are required for HTML correctness.
+ self.attrs = {'cols': '40', 'rows': '10'}
+ if attrs:
+ self.attrs.update(attrs)
+
+ def render(self, name, value, attrs=None):
+ if value is None: value = ''
+ value = force_unicode(value)
+ final_attrs = self.build_attrs(attrs, name=name)
+ return mark_safe(u'<textarea%s>%s</textarea>' % (flatatt(final_attrs),
+ conditional_escape(force_unicode(value))))
+
+class DateTimeInput(Input):
+ input_type = 'text'
+ format = '%Y-%m-%d %H:%M:%S' # '2006-10-25 14:30:59'
+
+ def __init__(self, attrs=None, format=None):
+ super(DateTimeInput, self).__init__(attrs)
+ if format:
+ self.format = format
+
+ def render(self, name, value, attrs=None):
+ if value is None:
+ value = ''
+ elif hasattr(value, 'strftime'):
+ value = value.strftime(self.format)
+ return super(DateTimeInput, self).render(name, value, attrs)
+
+class CheckboxInput(Widget):
+ def __init__(self, attrs=None, check_test=bool):
+ super(CheckboxInput, self).__init__(attrs)
+ # check_test is a callable that takes a value and returns True
+ # if the checkbox should be checked for that value.
+ self.check_test = check_test
+
+ def render(self, name, value, attrs=None):
+ final_attrs = self.build_attrs(attrs, type='checkbox', name=name)
+ try:
+ result = self.check_test(value)
+ except: # Silently catch exceptions
+ result = False
+ if result:
+ final_attrs['checked'] = 'checked'
+ if value not in ('', True, False, None):
+ # Only add the 'value' attribute if a value is non-empty.
+ final_attrs['value'] = force_unicode(value)
+ return mark_safe(u'<input%s />' % flatatt(final_attrs))
+
+ def value_from_datadict(self, data, files, name):
+ if name not in data:
+ # A missing value means False because HTML form submission does not
+ # send results for unselected checkboxes.
+ return False
+ return super(CheckboxInput, self).value_from_datadict(data, files, name)
+
+class Select(Widget):
+ def __init__(self, attrs=None, choices=()):
+ super(Select, self).__init__(attrs)
+ # choices can be any iterable, but we may need to render this widget
+ # multiple times. Thus, collapse it into a list so it can be consumed
+ # more than once.
+ self.choices = list(choices)
+
+ def render(self, name, value, attrs=None, choices=()):
+ if value is None: value = ''
+ final_attrs = self.build_attrs(attrs, name=name)
+ output = [u'<select%s>' % flatatt(final_attrs)]
+ # Normalize to string.
+ str_value = force_unicode(value)
+ for option_value, option_label in chain(self.choices, choices):
+ option_value = force_unicode(option_value)
+ selected_html = (option_value == str_value) and u' selected="selected"' or ''
+ output.append(u'<option value="%s"%s>%s</option>' % (
+ escape(option_value), selected_html,
+ conditional_escape(force_unicode(option_label))))
+ output.append(u'</select>')
+ return mark_safe(u'\n'.join(output))
+
+class NullBooleanSelect(Select):
+ """
+ A Select Widget intended to be used with NullBooleanField.
+ """
+ def __init__(self, attrs=None):
+ choices = ((u'1', ugettext('Unknown')), (u'2', ugettext('Yes')), (u'3', ugettext('No')))
+ super(NullBooleanSelect, self).__init__(attrs, choices)
+
+ def render(self, name, value, attrs=None, choices=()):
+ try:
+ value = {True: u'2', False: u'3', u'2': u'2', u'3': u'3'}[value]
+ except KeyError:
+ value = u'1'
+ return super(NullBooleanSelect, self).render(name, value, attrs, choices)
+
+ def value_from_datadict(self, data, files, name):
+ value = data.get(name, None)
+ return {u'2': True, u'3': False, True: True, False: False}.get(value, None)
+
+class SelectMultiple(Widget):
+ def __init__(self, attrs=None, choices=()):
+ super(SelectMultiple, self).__init__(attrs)
+ # choices can be any iterable
+ self.choices = choices
+
+ def render(self, name, value, attrs=None, choices=()):
+ if value is None: value = []
+ final_attrs = self.build_attrs(attrs, name=name)
+ output = [u'<select multiple="multiple"%s>' % flatatt(final_attrs)]
+ str_values = set([force_unicode(v) for v in value]) # Normalize to strings.
+ for option_value, option_label in chain(self.choices, choices):
+ option_value = force_unicode(option_value)
+ selected_html = (option_value in str_values) and ' selected="selected"' or ''
+ output.append(u'<option value="%s"%s>%s</option>' % (
+ escape(option_value), selected_html,
+ conditional_escape(force_unicode(option_label))))
+ output.append(u'</select>')
+ return mark_safe(u'\n'.join(output))
+
+ def value_from_datadict(self, data, files, name):
+ if isinstance(data, MultiValueDict):
+ return data.getlist(name)
+ return data.get(name, None)
+
+class RadioInput(StrAndUnicode):
+ """
+ An object used by RadioFieldRenderer that represents a single
+ <input type='radio'>.
+ """
+
+ def __init__(self, name, value, attrs, choice, index):
+ self.name, self.value = name, value
+ self.attrs = attrs
+ self.choice_value = force_unicode(choice[0])
+ self.choice_label = force_unicode(choice[1])
+ self.index = index
+
+ def __unicode__(self):
+ return mark_safe(u'<label>%s %s</label>' % (self.tag(),
+ conditional_escape(force_unicode(self.choice_label))))
+
+ def is_checked(self):
+ return self.value == self.choice_value
+
+ def tag(self):
+ if 'id' in self.attrs:
+ self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index)
+ final_attrs = dict(self.attrs, type='radio', name=self.name, value=self.choice_value)
+ if self.is_checked():
+ final_attrs['checked'] = 'checked'
+ return mark_safe(u'<input%s />' % flatatt(final_attrs))
+
+class RadioFieldRenderer(StrAndUnicode):
+ """
+ An object used by RadioSelect to enable customization of radio widgets.
+ """
+
+ def __init__(self, name, value, attrs, choices):
+ self.name, self.value, self.attrs = name, value, attrs
+ self.choices = choices
+
+ def __iter__(self):
+ for i, choice in enumerate(self.choices):
+ yield RadioInput(self.name, self.value, self.attrs.copy(), choice, i)
+
+ def __getitem__(self, idx):
+ choice = self.choices[idx] # Let the IndexError propogate
+ return RadioInput(self.name, self.value, self.attrs.copy(), choice, idx)
+
+ def __unicode__(self):
+ return self.render()
+
+ def render(self):
+ """Outputs a <ul> for this set of radio fields."""
+ return mark_safe(u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>'
+ % force_unicode(w) for w in self]))
+
+class RadioSelect(Select):
+ renderer = RadioFieldRenderer
+
+ def __init__(self, *args, **kwargs):
+ # Override the default renderer if we were passed one.
+ renderer = kwargs.pop('renderer', None)
+ if renderer:
+ self.renderer = renderer
+ super(RadioSelect, self).__init__(*args, **kwargs)
+
+ def get_renderer(self, name, value, attrs=None, choices=()):
+ """Returns an instance of the renderer."""
+ if value is None: value = ''
+ str_value = force_unicode(value) # Normalize to string.
+ final_attrs = self.build_attrs(attrs)
+ choices = list(chain(self.choices, choices))
+ return self.renderer(name, str_value, final_attrs, choices)
+
+ def render(self, name, value, attrs=None, choices=()):
+ return self.get_renderer(name, value, attrs, choices).render()
+
+ def id_for_label(self, id_):
+ # RadioSelect is represented by multiple <input type="radio"> fields,
+ # each of which has a distinct ID. The IDs are made distinct by a "_X"
+ # suffix, where X is the zero-based index of the radio field. Thus,
+ # the label for a RadioSelect should reference the first one ('_0').
+ if id_:
+ id_ += '_0'
+ return id_
+ id_for_label = classmethod(id_for_label)
+
+class CheckboxSelectMultiple(SelectMultiple):
+ def render(self, name, value, attrs=None, choices=()):
+ if value is None: value = []
+ has_id = attrs and 'id' in attrs
+ final_attrs = self.build_attrs(attrs, name=name)
+ output = [u'<ul>']
+ # Normalize to strings
+ str_values = set([force_unicode(v) for v in value])
+ for i, (option_value, option_label) in enumerate(chain(self.choices, choices)):
+ # If an ID attribute was given, add a numeric index as a suffix,
+ # so that the checkboxes don't all have the same ID attribute.
+ if has_id:
+ final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i))
+ cb = CheckboxInput(final_attrs, check_test=lambda value: value in str_values)
+ option_value = force_unicode(option_value)
+ rendered_cb = cb.render(name, option_value)
+ output.append(u'<li><label>%s %s</label></li>' % (rendered_cb,
+ conditional_escape(force_unicode(option_label))))
+ output.append(u'</ul>')
+ return mark_safe(u'\n'.join(output))
+
+ def id_for_label(self, id_):
+ # See the comment for RadioSelect.id_for_label()
+ if id_:
+ id_ += '_0'
+ return id_
+ id_for_label = classmethod(id_for_label)
+
+class MultiWidget(Widget):
+ """
+ A widget that is composed of multiple widgets.
+
+ Its render() method is different than other widgets', because it has to
+ figure out how to split a single value for display in multiple widgets.
+ The ``value`` argument can be one of two things:
+
+ * A list.
+ * A normal value (e.g., a string) that has been "compressed" from
+ a list of values.
+
+ In the second case -- i.e., if the value is NOT a list -- render() will
+ first "decompress" the value into a list before rendering it. It does so by
+ calling the decompress() method, which MultiWidget subclasses must
+ implement. This method takes a single "compressed" value and returns a
+ list.
+
+ When render() does its HTML rendering, each value in the list is rendered
+ with the corresponding widget -- the first value is rendered in the first
+ widget, the second value is rendered in the second widget, etc.
+
+ Subclasses may implement format_output(), which takes the list of rendered
+ widgets and returns a string of HTML that formats them any way you'd like.
+
+ You'll probably want to use this class with MultiValueField.
+ """
+ def __init__(self, widgets, attrs=None):
+ self.widgets = [isinstance(w, type) and w() or w for w in widgets]
+ super(MultiWidget, self).__init__(attrs)
+
+ def render(self, name, value, attrs=None):
+ # value is a list of values, each corresponding to a widget
+ # in self.widgets.
+ if not isinstance(value, list):
+ value = self.decompress(value)
+ output = []
+ final_attrs = self.build_attrs(attrs)
+ id_ = final_attrs.get('id', None)
+ for i, widget in enumerate(self.widgets):
+ try:
+ widget_value = value[i]
+ except IndexError:
+ widget_value = None
+ if id_:
+ final_attrs = dict(final_attrs, id='%s_%s' % (id_, i))
+ output.append(widget.render(name + '_%s' % i, widget_value, final_attrs))
+ return mark_safe(self.format_output(output))
+
+ def id_for_label(self, id_):
+ # See the comment for RadioSelect.id_for_label()
+ if id_:
+ id_ += '_0'
+ return id_
+ id_for_label = classmethod(id_for_label)
+
+ def value_from_datadict(self, data, files, name):
+ return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
+
+ def format_output(self, rendered_widgets):
+ """
+ Given a list of rendered widgets (as strings), returns a Unicode string
+ representing the HTML for the whole lot.
+
+ This hook allows you to format the HTML design of the widgets, if
+ needed.
+ """
+ return u''.join(rendered_widgets)
+
+ def decompress(self, value):
+ """
+ Returns a list of decompressed values for the given compressed value.
+ The given value can be assumed to be valid, but not necessarily
+ non-empty.
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+class SplitDateTimeWidget(MultiWidget):
+ """
+ A Widget that splits datetime input into two <input type="text"> boxes.
+ """
+ def __init__(self, attrs=None):
+ widgets = (TextInput(attrs=attrs), TextInput(attrs=attrs))
+ super(SplitDateTimeWidget, self).__init__(widgets, attrs)
+
+ def decompress(self, value):
+ if value:
+ return [value.date(), value.time().replace(microsecond=0)]
+ return [None, None]
+