app/django/forms/widgets.py
changeset 323 ff1a9aa48cfd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/django/forms/widgets.py	Tue Oct 14 16:00:59 2008 +0000
@@ -0,0 +1,673 @@
+"""
+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.conf import settings
+from django.utils.datastructures import MultiValueDict, MergeDict
+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 django.utils import datetime_safe
+from datetime import time
+from util import flatatt
+from urlparse import urljoin
+
+__all__ = (
+    'Media', 'MediaDefiningClass', 'Widget', 'TextInput', 'PasswordInput',
+    'HiddenInput', 'MultipleHiddenInput',
+    'FileInput', 'DateTimeInput', 'TimeInput', 'Textarea', 'CheckboxInput',
+    'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
+    'CheckboxSelectMultiple', 'MultiWidget',
+    'SplitDateTimeWidget',
+)
+
+MEDIA_TYPES = ('css','js')
+
+class Media(StrAndUnicode):
+    def __init__(self, media=None, **kwargs):
+        if media:
+            media_attrs = media.__dict__
+        else:
+            media_attrs = kwargs
+
+        self._css = {}
+        self._js = []
+
+        for name in MEDIA_TYPES:
+            getattr(self, 'add_' + name)(media_attrs.get(name, None))
+
+        # Any leftover attributes must be invalid.
+        # if media_attrs != {}:
+        #     raise TypeError, "'class Media' has invalid attribute(s): %s" % ','.join(media_attrs.keys())
+
+    def __unicode__(self):
+        return self.render()
+
+    def render(self):
+        return mark_safe(u'\n'.join(chain(*[getattr(self, 'render_' + name)() for name in MEDIA_TYPES])))
+
+    def render_js(self):
+        return [u'<script type="text/javascript" src="%s"></script>' % self.absolute_path(path) for path in self._js]
+
+    def render_css(self):
+        # To keep rendering order consistent, we can't just iterate over items().
+        # We need to sort the keys, and iterate over the sorted list.
+        media = self._css.keys()
+        media.sort()
+        return chain(*[
+            [u'<link href="%s" type="text/css" media="%s" rel="stylesheet" />' % (self.absolute_path(path), medium)
+                    for path in self._css[medium]]
+                for medium in media])
+
+    def absolute_path(self, path):
+        if path.startswith(u'http://') or path.startswith(u'https://') or path.startswith(u'/'):
+            return path
+        return urljoin(settings.MEDIA_URL,path)
+
+    def __getitem__(self, name):
+        "Returns a Media object that only contains media of the given type"
+        if name in MEDIA_TYPES:
+            return Media(**{name: getattr(self, '_' + name)})
+        raise KeyError('Unknown media type "%s"' % name)
+
+    def add_js(self, data):
+        if data:
+            self._js.extend([path for path in data if path not in self._js])
+
+    def add_css(self, data):
+        if data:
+            for medium, paths in data.items():
+                self._css.setdefault(medium, []).extend([path for path in paths if path not in self._css[medium]])
+
+    def __add__(self, other):
+        combined = Media()
+        for name in MEDIA_TYPES:
+            getattr(combined, 'add_' + name)(getattr(self, '_' + name, None))
+            getattr(combined, 'add_' + name)(getattr(other, '_' + name, None))
+        return combined
+
+def media_property(cls):
+    def _media(self):
+        # Get the media property of the superclass, if it exists
+        if hasattr(super(cls, self), 'media'):
+            base = super(cls, self).media
+        else:
+            base = Media()
+
+        # Get the media definition for this class
+        definition = getattr(cls, 'Media', None)
+        if definition:
+            extend = getattr(definition, 'extend', True)
+            if extend:
+                if extend == True:
+                    m = base
+                else:
+                    m = Media()
+                    for medium in extend:
+                        m = m + base[medium]
+                return m + Media(definition)
+            else:
+                return Media(definition)
+        else:
+            return base
+    return property(_media)
+
+class MediaDefiningClass(type):
+    "Metaclass for classes that can have media definitions"
+    def __new__(cls, name, bases, attrs):
+        new_class = super(MediaDefiningClass, cls).__new__(cls, name, bases,
+                                                           attrs)
+        if 'media' not in attrs:
+            new_class.media = media_property(new_class)
+        return new_class
+
+class Widget(object):
+    __metaclass__ = MediaDefiningClass
+    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 _has_changed(self, initial, data):
+        """
+        Return True if data differs from initial.
+        """
+        # For purposes of seeing whether something has changed, None is
+        # the same as an empty string, if the data or inital value we get
+        # is None, replace it w/ u''.
+        if data is None:
+            data_value = u''
+        else:
+            data_value = data
+        if initial is None:
+            initial_value = u''
+        else:
+            initial_value = initial
+        if force_unicode(initial_value) != force_unicode(data_value):
+            return True
+        return False
+
+    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, MergeDict)):
+            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)
+
+    def _has_changed(self, initial, data):
+        if data is None:
+            return False
+        return True
+
+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 = ''
+        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 = datetime_safe.new_datetime(value)
+            value = value.strftime(self.format)
+        return super(DateTimeInput, self).render(name, value, attrs)
+
+class TimeInput(Input):
+    input_type = 'text'
+
+    def render(self, name, value, attrs=None):
+        if value is None:
+            value = ''
+        elif isinstance(value, time):
+            value = value.replace(microsecond=0)
+        return super(TimeInput, 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)
+
+    def _has_changed(self, initial, data):
+        # Sometimes data or initial could be None or u'' which should be the
+        # same thing as False.
+        return bool(initial) != bool(data)
+
+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)]
+        options = self.render_options(choices, [value])
+        if options:
+            output.append(options)
+        output.append('</select>')
+        return mark_safe(u'\n'.join(output))
+
+    def render_options(self, choices, selected_choices):
+        def render_option(option_value, option_label):
+            option_value = force_unicode(option_value)
+            selected_html = (option_value in selected_choices) and u' selected="selected"' or ''
+            return u'<option value="%s"%s>%s</option>' % (
+                escape(option_value), selected_html,
+                conditional_escape(force_unicode(option_label)))
+        # Normalize to strings.
+        selected_choices = set([force_unicode(v) for v in selected_choices])
+        output = []
+        for option_value, option_label in chain(self.choices, choices):
+            if isinstance(option_label, (list, tuple)):
+                output.append(u'<optgroup label="%s">' % escape(force_unicode(option_value)))
+                for option in option_label:
+                    output.append(render_option(*option))
+                output.append(u'</optgroup>')
+            else:
+                output.append(render_option(option_value, option_label))
+        return 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)
+
+    def _has_changed(self, initial, data):
+        # Sometimes data or initial could be None or u'' which should be the
+        # same thing as False.
+        return bool(initial) != bool(data)
+
+class SelectMultiple(Select):
+    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)]
+        options = self.render_options(choices, value)
+        if options:
+            output.append(options)
+        output.append('</select>')
+        return mark_safe(u'\n'.join(output))
+
+    def value_from_datadict(self, data, files, name):
+        if isinstance(data, (MultiValueDict, MergeDict)):
+            return data.getlist(name)
+        return data.get(name, None)
+
+    def _has_changed(self, initial, data):
+        if initial is None:
+            initial = []
+        if data is None:
+            data = []
+        if len(initial) != len(data):
+            return True
+        for value1, value2 in zip(initial, data):
+            if force_unicode(value1) != force_unicode(value2):
+                return True
+        return False
+
+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):
+        if 'id' in self.attrs:
+            label_for = ' for="%s_%s"' % (self.attrs['id'], self.index)
+        else:
+            label_for = ''
+        choice_label = conditional_escape(force_unicode(self.choice_label))
+        return mark_safe(u'<label%s>%s %s</label>' % (label_for, self.tag(), 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))
+                label_for = u' for="%s"' % final_attrs['id']
+            else:
+                label_for = ''
+
+            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)
+            option_label = conditional_escape(force_unicode(option_label))
+            output.append(u'<li><label%s>%s %s</label></li>' % (label_for, rendered_cb, 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 _has_changed(self, initial, data):
+        if initial is None:
+            initial = [u'' for x in range(0, len(data))]
+        else:
+            if not isinstance(initial, list):
+                initial = self.decompress(initial)
+        for widget, initial, data in zip(self.widgets, initial, data):
+            if widget._has_changed(initial, data):
+                return True
+        return False
+
+    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.')
+
+    def _get_media(self):
+        "Media for a multiwidget is the combination of all media of the subwidgets"
+        media = Media()
+        for w in self.widgets:
+            media = media + w.media
+        return media
+    media = property(_get_media)
+
+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]
+
+class SplitHiddenDateTimeWidget(SplitDateTimeWidget):
+    """
+    A Widget that splits datetime input into two <input type="hidden"> inputs.
+    """
+    def __init__(self, attrs=None):
+        widgets = (HiddenInput(attrs=attrs), HiddenInput(attrs=attrs))
+        super(SplitDateTimeWidget, self).__init__(widgets, attrs)
+