[cig-commits] r5916 - in cs/buildbot/trunk/buildbot: . forms forms/extras

leif at geodynamics.org leif at geodynamics.org
Fri Jan 26 13:36:21 PST 2007


Author: leif
Date: 2007-01-26 13:36:20 -0800 (Fri, 26 Jan 2007)
New Revision: 5916

Added:
   cs/buildbot/trunk/buildbot/forms/
   cs/buildbot/trunk/buildbot/forms/__init__.py
   cs/buildbot/trunk/buildbot/forms/extras/
   cs/buildbot/trunk/buildbot/forms/extras/__init__.py
   cs/buildbot/trunk/buildbot/forms/extras/widgets.py
   cs/buildbot/trunk/buildbot/forms/fields.py
   cs/buildbot/trunk/buildbot/forms/forms.py
   cs/buildbot/trunk/buildbot/forms/models.py
   cs/buildbot/trunk/buildbot/forms/util.py
   cs/buildbot/trunk/buildbot/forms/widgets.py
Log:
Exported django.newforms from the development version of Django
(r4431) and incorporated it as buildbot.forms.


Added: cs/buildbot/trunk/buildbot/forms/__init__.py
===================================================================
--- cs/buildbot/trunk/buildbot/forms/__init__.py	2007-01-26 21:32:04 UTC (rev 5915)
+++ cs/buildbot/trunk/buildbot/forms/__init__.py	2007-01-26 21:36:20 UTC (rev 5916)
@@ -0,0 +1,17 @@
+"""
+Django validation and HTML form handling.
+
+TODO:
+    Default value for field
+    Field labels
+    Nestable Forms
+    FatalValidationError -- short-circuits all other validators on a form
+    ValidationWarning
+    "This form field requires foo.js" and form.js_includes()
+"""
+
+from util import ValidationError
+from widgets import *
+from fields import *
+from forms import *
+from models import *

Added: cs/buildbot/trunk/buildbot/forms/extras/__init__.py
===================================================================
--- cs/buildbot/trunk/buildbot/forms/extras/__init__.py	2007-01-26 21:32:04 UTC (rev 5915)
+++ cs/buildbot/trunk/buildbot/forms/extras/__init__.py	2007-01-26 21:36:20 UTC (rev 5916)
@@ -0,0 +1 @@
+from widgets import *

Added: cs/buildbot/trunk/buildbot/forms/extras/widgets.py
===================================================================
--- cs/buildbot/trunk/buildbot/forms/extras/widgets.py	2007-01-26 21:32:04 UTC (rev 5915)
+++ cs/buildbot/trunk/buildbot/forms/extras/widgets.py	2007-01-26 21:36:20 UTC (rev 5916)
@@ -0,0 +1,59 @@
+"""
+Extra HTML Widget classes
+"""
+
+from django.newforms.widgets import Widget, Select
+from django.utils.dates import MONTHS
+import datetime
+
+__all__ = ('SelectDateWidget',)
+
+class SelectDateWidget(Widget):
+    """
+    A Widget that splits date input into three <select> boxes.
+
+    This also serves as an example of a Widget that has more than one HTML
+    element and hence implements value_from_datadict.
+    """
+    month_field = '%s_month'
+    day_field = '%s_day'
+    year_field = '%s_year'
+
+    def __init__(self, attrs=None, years=None):
+        # years is an optional list/tuple of years to use in the "year" select box.
+        self.attrs = attrs or {}
+        if years:
+            self.years = years
+        else:
+            this_year = datetime.date.today().year
+            self.years = range(this_year, this_year+10)
+
+    def render(self, name, value, attrs=None):
+        try:
+            value = datetime.date(*map(int, value.split('-')))
+            year_val, month_val, day_val = value.year, value.month, value.day
+        except (AttributeError, TypeError, ValueError):
+            year_val = month_val = day_val = None
+
+        output = []
+
+        month_choices = MONTHS.items()
+        month_choices.sort()
+        select_html = Select(choices=month_choices).render(self.month_field % name, month_val)
+        output.append(select_html)
+
+        day_choices = [(i, i) for i in range(1, 32)]
+        select_html = Select(choices=day_choices).render(self.day_field % name, day_val)
+        output.append(select_html)
+
+        year_choices = [(i, i) for i in self.years]
+        select_html = Select(choices=year_choices).render(self.year_field % name, year_val)
+        output.append(select_html)
+
+        return u'\n'.join(output)
+
+    def value_from_datadict(self, data, name):
+        y, m, d = data.get(self.year_field % name), data.get(self.month_field % name), data.get(self.day_field % name)
+        if y and m and d:
+            return '%s-%s-%s' % (y, m, d)
+        return None

Added: cs/buildbot/trunk/buildbot/forms/fields.py
===================================================================
--- cs/buildbot/trunk/buildbot/forms/fields.py	2007-01-26 21:32:04 UTC (rev 5915)
+++ cs/buildbot/trunk/buildbot/forms/fields.py	2007-01-26 21:36:20 UTC (rev 5916)
@@ -0,0 +1,491 @@
+"""
+Field classes
+"""
+
+from django.utils.translation import gettext
+from util import ErrorList, ValidationError, smart_unicode
+from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple
+import datetime
+import re
+import time
+
+__all__ = (
+    'Field', 'CharField', 'IntegerField',
+    'DEFAULT_DATE_INPUT_FORMATS', 'DateField',
+    'DEFAULT_TIME_INPUT_FORMATS', 'TimeField',
+    'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField',
+    'RegexField', 'EmailField', 'URLField', 'BooleanField',
+    'ChoiceField', 'NullBooleanField', 'MultipleChoiceField',
+    'ComboField', 'MultiValueField',
+    'SplitDateTimeField',
+)
+
+# These values, if given to to_python(), will trigger the self.required check.
+EMPTY_VALUES = (None, '')
+
+try:
+    set # Only available in Python 2.4+
+except NameError:
+    from sets import Set as set # Python 2.3 fallback
+
+class Field(object):
+    widget = TextInput # Default widget to use when rendering this type of Field.
+    hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden".
+
+    # Tracks each time a Field instance is created. Used to retain order.
+    creation_counter = 0
+
+    def __init__(self, required=True, widget=None, label=None, initial=None):
+        # required -- Boolean that specifies whether the field is required.
+        #             True by default.
+        # widget -- A Widget class, or instance of a Widget class, that should be
+        #         used for this Field when displaying it. Each Field has a default
+        #         Widget that it'll use if you don't specify this. In most cases,
+        #         the default widget is TextInput.
+        # label -- A verbose name for this field, for use in displaying this field in
+        #         a form. By default, Django will use a "pretty" version of the form
+        #         field name, if the Field is part of a Form.
+        # initial -- A value to use in this Field's initial display. This value is
+        #            *not* used as a fallback if data isn't given.
+        if label is not None:
+            label = smart_unicode(label)
+        self.required, self.label, self.initial = required, label, initial
+        widget = widget or self.widget
+        if isinstance(widget, type):
+            widget = widget()
+
+        # Hook into self.widget_attrs() for any Field-specific HTML attributes.
+        extra_attrs = self.widget_attrs(widget)
+        if extra_attrs:
+            widget.attrs.update(extra_attrs)
+
+        self.widget = widget
+
+        # Increase the creation counter, and save our local copy.
+        self.creation_counter = Field.creation_counter
+        Field.creation_counter += 1
+
+    def clean(self, value):
+        """
+        Validates the given value and returns its "cleaned" value as an
+        appropriate Python object.
+
+        Raises ValidationError for any errors.
+        """
+        if self.required and value in EMPTY_VALUES:
+            raise ValidationError(gettext(u'This field is required.'))
+        return value
+
+    def widget_attrs(self, widget):
+        """
+        Given a Widget instance (*not* a Widget class), returns a dictionary of
+        any HTML attributes that should be added to the Widget, based on this
+        Field.
+        """
+        return {}
+
+class CharField(Field):
+    def __init__(self, max_length=None, min_length=None, required=True, widget=None, label=None, initial=None):
+        self.max_length, self.min_length = max_length, min_length
+        super(CharField, self).__init__(required, widget, label, initial)
+
+    def clean(self, value):
+        "Validates max_length and min_length. Returns a Unicode object."
+        super(CharField, self).clean(value)
+        if value in EMPTY_VALUES:
+            return u''
+        value = smart_unicode(value)
+        if self.max_length is not None and len(value) > self.max_length:
+            raise ValidationError(gettext(u'Ensure this value has at most %d characters.') % self.max_length)
+        if self.min_length is not None and len(value) < self.min_length:
+            raise ValidationError(gettext(u'Ensure this value has at least %d characters.') % self.min_length)
+        return value
+
+    def widget_attrs(self, widget):
+        if self.max_length is not None and isinstance(widget, (TextInput, PasswordInput)):
+            return {'maxlength': str(self.max_length)}
+
+class IntegerField(Field):
+    def __init__(self, max_value=None, min_value=None, required=True, widget=None, label=None, initial=None):
+        self.max_value, self.min_value = max_value, min_value
+        super(IntegerField, self).__init__(required, widget, label, initial)
+
+    def clean(self, value):
+        """
+        Validates that int() can be called on the input. Returns the result
+        of int(). Returns None for empty values.
+        """
+        super(IntegerField, self).clean(value)
+        if value in EMPTY_VALUES:
+            return None
+        try:
+            value = int(value)
+        except (ValueError, TypeError):
+            raise ValidationError(gettext(u'Enter a whole number.'))
+        if self.max_value is not None and value > self.max_value:
+            raise ValidationError(gettext(u'Ensure this value is less than or equal to %s.') % self.max_value)
+        if self.min_value is not None and value < self.min_value:
+            raise ValidationError(gettext(u'Ensure this value is greater than or equal to %s.') % self.min_value)
+        return value
+
+DEFAULT_DATE_INPUT_FORMATS = (
+    '%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06'
+    '%b %d %Y', '%b %d, %Y',            # 'Oct 25 2006', 'Oct 25, 2006'
+    '%d %b %Y', '%d %b, %Y',            # '25 Oct 2006', '25 Oct, 2006'
+    '%B %d %Y', '%B %d, %Y',            # 'October 25 2006', 'October 25, 2006'
+    '%d %B %Y', '%d %B, %Y',            # '25 October 2006', '25 October, 2006'
+)
+
+class DateField(Field):
+    def __init__(self, input_formats=None, required=True, widget=None, label=None, initial=None):
+        super(DateField, self).__init__(required, widget, label, initial)
+        self.input_formats = input_formats or DEFAULT_DATE_INPUT_FORMATS
+
+    def clean(self, value):
+        """
+        Validates that the input can be converted to a date. Returns a Python
+        datetime.date object.
+        """
+        super(DateField, self).clean(value)
+        if value in EMPTY_VALUES:
+            return None
+        if isinstance(value, datetime.datetime):
+            return value.date()
+        if isinstance(value, datetime.date):
+            return value
+        for format in self.input_formats:
+            try:
+                return datetime.date(*time.strptime(value, format)[:3])
+            except ValueError:
+                continue
+        raise ValidationError(gettext(u'Enter a valid date.'))
+
+DEFAULT_TIME_INPUT_FORMATS = (
+    '%H:%M:%S',     # '14:30:59'
+    '%H:%M',        # '14:30'
+)
+
+class TimeField(Field):
+    def __init__(self, input_formats=None, required=True, widget=None, label=None, initial=None):
+        super(TimeField, self).__init__(required, widget, label, initial)
+        self.input_formats = input_formats or DEFAULT_TIME_INPUT_FORMATS
+
+    def clean(self, value):
+        """
+        Validates that the input can be converted to a time. Returns a Python
+        datetime.time object.
+        """
+        super(TimeField, self).clean(value)
+        if value in EMPTY_VALUES:
+            return None
+        if isinstance(value, datetime.time):
+            return value
+        for format in self.input_formats:
+            try:
+                return datetime.time(*time.strptime(value, format)[3:6])
+            except ValueError:
+                continue
+        raise ValidationError(gettext(u'Enter a valid time.'))
+
+DEFAULT_DATETIME_INPUT_FORMATS = (
+    '%Y-%m-%d %H:%M:%S',     # '2006-10-25 14:30:59'
+    '%Y-%m-%d %H:%M',        # '2006-10-25 14:30'
+    '%Y-%m-%d',              # '2006-10-25'
+    '%m/%d/%Y %H:%M:%S',     # '10/25/2006 14:30:59'
+    '%m/%d/%Y %H:%M',        # '10/25/2006 14:30'
+    '%m/%d/%Y',              # '10/25/2006'
+    '%m/%d/%y %H:%M:%S',     # '10/25/06 14:30:59'
+    '%m/%d/%y %H:%M',        # '10/25/06 14:30'
+    '%m/%d/%y',              # '10/25/06'
+)
+
+class DateTimeField(Field):
+    def __init__(self, input_formats=None, required=True, widget=None, label=None, initial=None):
+        super(DateTimeField, self).__init__(required, widget, label, initial)
+        self.input_formats = input_formats or DEFAULT_DATETIME_INPUT_FORMATS
+
+    def clean(self, value):
+        """
+        Validates that the input can be converted to a datetime. Returns a
+        Python datetime.datetime object.
+        """
+        super(DateTimeField, self).clean(value)
+        if value in EMPTY_VALUES:
+            return None
+        if isinstance(value, datetime.datetime):
+            return value
+        if isinstance(value, datetime.date):
+            return datetime.datetime(value.year, value.month, value.day)
+        for format in self.input_formats:
+            try:
+                return datetime.datetime(*time.strptime(value, format)[:6])
+            except ValueError:
+                continue
+        raise ValidationError(gettext(u'Enter a valid date/time.'))
+
+class RegexField(Field):
+    def __init__(self, regex, max_length=None, min_length=None, error_message=None,
+            required=True, widget=None, label=None, initial=None):
+        """
+        regex can be either a string or a compiled regular expression object.
+        error_message is an optional error message to use, if
+        'Enter a valid value' is too generic for you.
+        """
+        super(RegexField, self).__init__(required, widget, label, initial)
+        if isinstance(regex, basestring):
+            regex = re.compile(regex)
+        self.regex = regex
+        self.max_length, self.min_length = max_length, min_length
+        self.error_message = error_message or gettext(u'Enter a valid value.')
+
+    def clean(self, value):
+        """
+        Validates that the input matches the regular expression. Returns a
+        Unicode object.
+        """
+        super(RegexField, self).clean(value)
+        if value in EMPTY_VALUES:
+            value = u''
+        value = smart_unicode(value)
+        if value == u'':
+            return value
+        if self.max_length is not None and len(value) > self.max_length:
+            raise ValidationError(gettext(u'Ensure this value has at most %d characters.') % self.max_length)
+        if self.min_length is not None and len(value) < self.min_length:
+            raise ValidationError(gettext(u'Ensure this value has at least %d characters.') % self.min_length)
+        if not self.regex.search(value):
+            raise ValidationError(self.error_message)
+        return value
+
+email_re = re.compile(
+    r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*"  # dot-atom
+    r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string
+    r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE)  # domain
+
+class EmailField(RegexField):
+    def __init__(self, max_length=None, min_length=None, required=True, widget=None, label=None, initial=None):
+        RegexField.__init__(self, email_re, max_length, min_length, gettext(u'Enter a valid e-mail address.'), required, widget, label, initial)
+
+url_re = re.compile(
+    r'^https?://' # http:// or https://
+    r'(?:[A-Z0-9-]+\.)+[A-Z]{2,6}' # domain
+    r'(?::\d+)?' # optional port
+    r'(?:/?|/\S+)$', re.IGNORECASE)
+
+try:
+    from django.conf import settings
+    URL_VALIDATOR_USER_AGENT = settings.URL_VALIDATOR_USER_AGENT
+except ImportError:
+    # It's OK if Django settings aren't configured.
+    URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)'
+
+class URLField(RegexField):
+    def __init__(self, max_length=None, min_length=None, required=True, verify_exists=False, widget=None, label=None,
+            initial=None, validator_user_agent=URL_VALIDATOR_USER_AGENT):
+        super(URLField, self).__init__(url_re, max_length, min_length, gettext(u'Enter a valid URL.'), required, widget, label, initial)
+        self.verify_exists = verify_exists
+        self.user_agent = validator_user_agent
+
+    def clean(self, value):
+        value = super(URLField, self).clean(value)
+        if value == u'':
+            return value
+        if self.verify_exists:
+            import urllib2
+            from django.conf import settings
+            headers = {
+                "Accept": "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
+                "Accept-Language": "en-us,en;q=0.5",
+                "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7",
+                "Connection": "close",
+                "User-Agent": self.user_agent,
+            }
+            try:
+                req = urllib2.Request(value, None, headers)
+                u = urllib2.urlopen(req)
+            except ValueError:
+                raise ValidationError(gettext(u'Enter a valid URL.'))
+            except: # urllib2.URLError, httplib.InvalidURL, etc.
+                raise ValidationError(gettext(u'This URL appears to be a broken link.'))
+        return value
+
+class BooleanField(Field):
+    widget = CheckboxInput
+
+    def clean(self, value):
+        "Returns a Python boolean object."
+        super(BooleanField, self).clean(value)
+        return bool(value)
+
+class NullBooleanField(BooleanField):
+    """
+    A field whose valid values are None, True and False. Invalid values are
+    cleaned to None.
+    """
+    widget = NullBooleanSelect
+
+    def clean(self, value):
+        return {True: True, False: False}.get(value, None)
+
+class ChoiceField(Field):
+    def __init__(self, choices=(), required=True, widget=Select, label=None, initial=None):
+        if isinstance(widget, type):
+            widget = widget()
+        super(ChoiceField, self).__init__(required, widget, label, initial)
+        self.choices = choices
+
+    def _get_choices(self):
+        return self._choices
+
+    def _set_choices(self, value):
+        # Setting choices also sets the choices on the widget.
+        self._choices = value
+        self.widget.choices = value
+
+    choices = property(_get_choices, _set_choices)
+
+    def clean(self, value):
+        """
+        Validates that the input is in self.choices.
+        """
+        value = super(ChoiceField, self).clean(value)
+        if value in EMPTY_VALUES:
+            value = u''
+        value = smart_unicode(value)
+        if value == u'':
+            return value
+        valid_values = set([str(k) for k, v in self.choices])
+        if value not in valid_values:
+            raise ValidationError(gettext(u'Select a valid choice. %s is not one of the available choices.') % value)
+        return value
+
+class MultipleChoiceField(ChoiceField):
+    hidden_widget = MultipleHiddenInput
+
+    def __init__(self, choices=(), required=True, widget=SelectMultiple, label=None, initial=None):
+        super(MultipleChoiceField, self).__init__(choices, required, widget, label, initial)
+
+    def clean(self, value):
+        """
+        Validates that the input is a list or tuple.
+        """
+        if self.required and not value:
+            raise ValidationError(gettext(u'This field is required.'))
+        elif not self.required and not value:
+            return []
+        if not isinstance(value, (list, tuple)):
+            raise ValidationError(gettext(u'Enter a list of values.'))
+        new_value = []
+        for val in value:
+            val = smart_unicode(val)
+            new_value.append(val)
+        # Validate that each value in the value list is in self.choices.
+        valid_values = set([smart_unicode(k) for k, v in self.choices])
+        for val in new_value:
+            if val not in valid_values:
+                raise ValidationError(gettext(u'Select a valid choice. %s is not one of the available choices.') % val)
+        return new_value
+
+class ComboField(Field):
+    """
+    A Field whose clean() method calls multiple Field clean() methods.
+    """
+    def __init__(self, fields=(), required=True, widget=None, label=None, initial=None):
+        super(ComboField, self).__init__(required, widget, label, initial)
+        # Set 'required' to False on the individual fields, because the
+        # required validation will be handled by ComboField, not by those
+        # individual fields.
+        for f in fields:
+            f.required = False
+        self.fields = fields
+
+    def clean(self, value):
+        """
+        Validates the given value against all of self.fields, which is a
+        list of Field instances.
+        """
+        super(ComboField, self).clean(value)
+        for field in self.fields:
+            value = field.clean(value)
+        return value
+
+class MultiValueField(Field):
+    """
+    A Field that is composed of multiple Fields.
+
+    Its clean() method takes a "decompressed" list of values. Each value in
+    this list is cleaned by the corresponding field -- the first value is
+    cleaned by the first field, the second value is cleaned by the second
+    field, etc. Once all fields are cleaned, the list of clean values is
+    "compressed" into a single value.
+
+    Subclasses should implement compress(), which specifies how a list of
+    valid values should be converted to a single value. Subclasses should not
+    have to implement clean().
+
+    You'll probably want to use this with MultiWidget.
+    """
+    def __init__(self, fields=(), required=True, widget=None, label=None, initial=None):
+        super(MultiValueField, self).__init__(required, widget, label, initial)
+        # Set 'required' to False on the individual fields, because the
+        # required validation will be handled by MultiValueField, not by those
+        # individual fields.
+        for f in fields:
+            f.required = False
+        self.fields = fields
+
+    def clean(self, value):
+        """
+        Validates every value in the given list. A value is validated against
+        the corresponding Field in self.fields.
+
+        For example, if this MultiValueField was instantiated with
+        fields=(DateField(), TimeField()), clean() would call
+        DateField.clean(value[0]) and TimeField.clean(value[1]).
+        """
+        clean_data = []
+        errors = ErrorList()
+        if self.required and not value:
+            raise ValidationError(gettext(u'This field is required.'))
+        elif not self.required and not value:
+            return self.compress([])
+        if not isinstance(value, (list, tuple)):
+            raise ValidationError(gettext(u'Enter a list of values.'))
+        for i, field in enumerate(self.fields):
+            try:
+                field_value = value[i]
+            except KeyError:
+                field_value = None
+            if self.required and field_value in EMPTY_VALUES:
+                raise ValidationError(gettext(u'This field is required.'))
+            try:
+                clean_data.append(field.clean(field_value))
+            except ValidationError, e:
+                # Collect all validation errors in a single list, which we'll
+                # raise at the end of clean(), rather than raising a single
+                # exception for the first error we encounter.
+                errors.extend(e.messages)
+        if errors:
+            raise ValidationError(errors)
+        return self.compress(clean_data)
+
+    def compress(self, data_list):
+        """
+        Returns a single value for the given list of values. The values can be
+        assumed to be valid.
+
+        For example, if this MultiValueField was instantiated with
+        fields=(DateField(), TimeField()), this might return a datetime
+        object created by combining the date and time in data_list.
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+class SplitDateTimeField(MultiValueField):
+    def __init__(self, required=True, widget=None, label=None, initial=None):
+        fields = (DateField(), TimeField())
+        super(SplitDateTimeField, self).__init__(fields, required, widget, label, initial)
+
+    def compress(self, data_list):
+        if data_list:
+            return datetime.datetime.combine(*data_list)
+        return None

Added: cs/buildbot/trunk/buildbot/forms/forms.py
===================================================================
--- cs/buildbot/trunk/buildbot/forms/forms.py	2007-01-26 21:32:04 UTC (rev 5915)
+++ cs/buildbot/trunk/buildbot/forms/forms.py	2007-01-26 21:36:20 UTC (rev 5916)
@@ -0,0 +1,282 @@
+"""
+Form classes
+"""
+
+from django.utils.datastructures import SortedDict, MultiValueDict
+from django.utils.html import escape
+from fields import Field
+from widgets import TextInput, Textarea, HiddenInput, MultipleHiddenInput
+from util import flatatt, StrAndUnicode, ErrorDict, ErrorList, ValidationError
+
+__all__ = ('BaseForm', 'Form')
+
+NON_FIELD_ERRORS = '__all__'
+
+def pretty_name(name):
+    "Converts 'first_name' to 'First name'"
+    name = name[0].upper() + name[1:]
+    return name.replace('_', ' ')
+
+class SortedDictFromList(SortedDict):
+    "A dictionary that keeps its keys in the order in which they're inserted."
+    # This is different than django.utils.datastructures.SortedDict, because
+    # this takes a list/tuple as the argument to __init__().
+    def __init__(self, data=None):
+        if data is None: data = []
+        self.keyOrder = [d[0] for d in data]
+        dict.__init__(self, dict(data))
+
+class DeclarativeFieldsMetaclass(type):
+    "Metaclass that converts Field attributes to a dictionary called 'fields'."
+    def __new__(cls, name, bases, attrs):
+        fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
+        fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
+        attrs['fields'] = SortedDictFromList(fields)
+        return type.__new__(cls, name, bases, attrs)
+
+class BaseForm(StrAndUnicode):
+    # This is the main implementation of all the Form logic. Note that this
+    # class is different than Form. See the comments by the Form class for more
+    # information. Any improvements to the form API should be made to *this*
+    # class, not to the Form class.
+    def __init__(self, data=None, auto_id='id_%s', prefix=None, initial=None):
+        self.is_bound = data is not None
+        self.data = data or {}
+        self.auto_id = auto_id
+        self.prefix = prefix
+        self.initial = initial or {}
+        self.__errors = None # Stores the errors after clean() has been called.
+
+    def __unicode__(self):
+        return self.as_table()
+
+    def __iter__(self):
+        for name, field in self.fields.items():
+            yield BoundField(self, field, name)
+
+    def __getitem__(self, name):
+        "Returns a BoundField with the given name."
+        try:
+            field = self.fields[name]
+        except KeyError:
+            raise KeyError('Key %r not found in Form' % name)
+        return BoundField(self, field, name)
+
+    def _errors(self):
+        "Returns an ErrorDict for self.data"
+        if self.__errors is None:
+            self.full_clean()
+        return self.__errors
+    errors = property(_errors)
+
+    def is_valid(self):
+        """
+        Returns True if the form has no errors. Otherwise, False. If errors are
+        being ignored, returns False.
+        """
+        return self.is_bound and not bool(self.errors)
+
+    def add_prefix(self, field_name):
+        """
+        Returns the field name with a prefix appended, if this Form has a
+        prefix set.
+
+        Subclasses may wish to override.
+        """
+        return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name
+
+    def _html_output(self, normal_row, error_row, row_ender, errors_on_separate_row):
+        "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()."
+        top_errors = self.non_field_errors() # Errors that should be displayed above all fields.
+        output, hidden_fields = [], []
+        for name, field in self.fields.items():
+            bf = BoundField(self, field, name)
+            bf_errors = bf.errors # Cache in local variable.
+            if bf.is_hidden:
+                if bf_errors:
+                    top_errors.extend(['(Hidden field %s) %s' % (name, e) for e in bf_errors])
+                hidden_fields.append(unicode(bf))
+            else:
+                if errors_on_separate_row and bf_errors:
+                    output.append(error_row % bf_errors)
+                label = bf.label and bf.label_tag(escape(bf.label + ':')) or ''
+                output.append(normal_row % {'errors': bf_errors, 'label': label, 'field': unicode(bf)})
+        if top_errors:
+            output.insert(0, error_row % top_errors)
+        if hidden_fields: # Insert any hidden fields in the last row.
+            str_hidden = u''.join(hidden_fields)
+            if output:
+                last_row = output[-1]
+                # Chop off the trailing row_ender (e.g. '</td></tr>') and insert the hidden fields.
+                output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender
+            else: # If there aren't any rows in the output, just append the hidden fields.
+                output.append(str_hidden)
+        return u'\n'.join(output)
+
+    def as_table(self):
+        "Returns this form rendered as HTML <tr>s -- excluding the <table></table>."
+        return self._html_output(u'<tr><th>%(label)s</th><td>%(errors)s%(field)s</td></tr>', u'<tr><td colspan="2">%s</td></tr>', '</td></tr>', False)
+
+    def as_ul(self):
+        "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."
+        return self._html_output(u'<li>%(errors)s%(label)s %(field)s</li>', u'<li>%s</li>', '</li>', False)
+
+    def as_p(self):
+        "Returns this form rendered as HTML <p>s."
+        return self._html_output(u'<p>%(label)s %(field)s</p>', u'<p>%s</p>', '</p>', True)
+
+    def non_field_errors(self):
+        """
+        Returns an ErrorList of errors that aren't associated with a particular
+        field -- i.e., from Form.clean(). Returns an empty ErrorList if there
+        are none.
+        """
+        return self.errors.get(NON_FIELD_ERRORS, ErrorList())
+
+    def full_clean(self):
+        """
+        Cleans all of self.data and populates self.__errors and self.clean_data.
+        """
+        errors = ErrorDict()
+        if not self.is_bound: # Stop further processing.
+            self.__errors = errors
+            return
+        self.clean_data = {}
+        for name, field in self.fields.items():
+            # value_from_datadict() gets the data from the dictionary.
+            # Each widget type knows how to retrieve its own data, because some
+            # widgets split data over several HTML fields.
+            value = field.widget.value_from_datadict(self.data, self.add_prefix(name))
+            try:
+                value = field.clean(value)
+                self.clean_data[name] = value
+                if hasattr(self, 'clean_%s' % name):
+                    value = getattr(self, 'clean_%s' % name)()
+                self.clean_data[name] = value
+            except ValidationError, e:
+                errors[name] = e.messages
+        try:
+            self.clean_data = self.clean()
+        except ValidationError, e:
+            errors[NON_FIELD_ERRORS] = e.messages
+        if errors:
+            delattr(self, 'clean_data')
+        self.__errors = errors
+
+    def clean(self):
+        """
+        Hook for doing any extra form-wide cleaning after Field.clean() been
+        called on every field. Any ValidationError raised by this method will
+        not be associated with a particular field; it will have a special-case
+        association with the field named '__all__'.
+        """
+        return self.clean_data
+
+class Form(BaseForm):
+    "A collection of Fields, plus their associated data."
+    # This is a separate class from BaseForm in order to abstract the way
+    # self.fields is specified. This class (Form) is the one that does the
+    # fancy metaclass stuff purely for the semantic sugar -- it allows one
+    # to define a form using declarative syntax.
+    # BaseForm itself has no way of designating self.fields.
+    __metaclass__ = DeclarativeFieldsMetaclass
+
+class BoundField(StrAndUnicode):
+    "A Field plus data"
+    def __init__(self, form, field, name):
+        self.form = form
+        self.field = field
+        self.name = name
+        self.html_name = form.add_prefix(name)
+        if self.field.label is None:
+            self.label = pretty_name(name)
+        else:
+            self.label = self.field.label
+
+    def __unicode__(self):
+        "Renders this field as an HTML widget."
+        # Use the 'widget' attribute on the field to determine which type
+        # of HTML widget to use.
+        value = self.as_widget(self.field.widget)
+        if not isinstance(value, basestring):
+            # Some Widget render() methods -- notably RadioSelect -- return a
+            # "special" object rather than a string. Call the __str__() on that
+            # object to get its rendered value.
+            value = value.__str__()
+        return value
+
+    def _errors(self):
+        """
+        Returns an ErrorList for this field. Returns an empty ErrorList
+        if there are none.
+        """
+        return self.form.errors.get(self.name, ErrorList())
+    errors = property(_errors)
+
+    def as_widget(self, widget, attrs=None):
+        attrs = attrs or {}
+        auto_id = self.auto_id
+        if auto_id and not attrs.has_key('id') and not widget.attrs.has_key('id'):
+            attrs['id'] = auto_id
+        if not self.form.is_bound:
+            data = self.form.initial.get(self.name, self.field.initial)
+        else:
+            data = self.data
+        return widget.render(self.html_name, data, attrs=attrs)
+
+    def as_text(self, attrs=None):
+        """
+        Returns a string of HTML for representing this as an <input type="text">.
+        """
+        return self.as_widget(TextInput(), attrs)
+
+    def as_textarea(self, attrs=None):
+        "Returns a string of HTML for representing this as a <textarea>."
+        return self.as_widget(Textarea(), attrs)
+
+    def as_hidden(self, attrs=None):
+        """
+        Returns a string of HTML for representing this as an <input type="hidden">.
+        """
+        return self.as_widget(self.field.hidden_widget(), attrs)
+
+    def _data(self):
+        """
+        Returns the data for this BoundField, or None if it wasn't given.
+        """
+        return self.field.widget.value_from_datadict(self.form.data, self.html_name)
+    data = property(_data)
+
+    def label_tag(self, contents=None, attrs=None):
+        """
+        Wraps the given contents in a <label>, if the field has an ID attribute.
+        Does not HTML-escape the contents. If contents aren't given, uses the
+        field's HTML-escaped label.
+
+        If attrs are given, they're used as HTML attributes on the <label> tag.
+        """
+        contents = contents or escape(self.label)
+        widget = self.field.widget
+        id_ = widget.attrs.get('id') or self.auto_id
+        if id_:
+            attrs = attrs and flatatt(attrs) or ''
+            contents = '<label for="%s"%s>%s</label>' % (widget.id_for_label(id_), attrs, contents)
+        return contents
+
+    def _is_hidden(self):
+        "Returns True if this BoundField's widget is hidden."
+        return self.field.widget.is_hidden
+    is_hidden = property(_is_hidden)
+
+    def _auto_id(self):
+        """
+        Calculates and returns the ID attribute for this BoundField, if the
+        associated Form has specified auto_id. Returns an empty string otherwise.
+        """
+        auto_id = self.form.auto_id
+        if auto_id and '%s' in str(auto_id):
+            return str(auto_id) % self.html_name
+        elif auto_id:
+            return self.html_name
+        return ''
+    auto_id = property(_auto_id)

Added: cs/buildbot/trunk/buildbot/forms/models.py
===================================================================
--- cs/buildbot/trunk/buildbot/forms/models.py	2007-01-26 21:32:04 UTC (rev 5915)
+++ cs/buildbot/trunk/buildbot/forms/models.py	2007-01-26 21:36:20 UTC (rev 5916)
@@ -0,0 +1,95 @@
+"""
+Helper functions for creating Form classes from Django models
+and database field objects.
+"""
+
+from forms import BaseForm, DeclarativeFieldsMetaclass, SortedDictFromList
+
+__all__ = ('save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields')
+
+def model_save(self, commit=True):
+    """
+    Creates and returns model instance according to self.clean_data.
+
+    This method is created for any form_for_model Form.
+    """
+    if self.errors:
+        raise ValueError("The %s could not be created because the data didn't validate." % self._model._meta.object_name)
+    obj = self._model(**self.clean_data)
+    if commit:
+        obj.save()
+    return obj
+
+def save_instance(form, instance, commit=True):
+    """
+    Saves bound Form ``form``'s clean_data into model instance ``instance``.
+
+    Assumes ``form`` has a field for every non-AutoField database field in
+    ``instance``. If commit=True, then the changes to ``instance`` will be
+    saved to the database. Returns ``instance``.
+    """
+    from django.db import models
+    opts = instance.__class__._meta
+    if form.errors:
+        raise ValueError("The %s could not be changed because the data didn't validate." % opts.object_name)
+    clean_data = form.clean_data
+    for f in opts.fields + opts.many_to_many:
+        if isinstance(f, models.AutoField):
+            continue
+        setattr(instance, f.attname, clean_data[f.name])
+    if commit:
+        instance.save()
+    return instance
+
+def make_instance_save(instance):
+    "Returns the save() method for a form_for_instance Form."
+    def save(self, commit=True):
+        return save_instance(self, instance, commit)
+    return save
+
+def form_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield()):
+    """
+    Returns a Form class for the given Django model class.
+
+    Provide ``form`` if you want to use a custom BaseForm subclass.
+
+    Provide ``formfield_callback`` if you want to define different logic for
+    determining the formfield for a given database field. It's a callable that
+    takes a database Field instance and returns a form Field instance.
+    """
+    opts = model._meta
+    field_list = []
+    for f in opts.fields + opts.many_to_many:
+        formfield = formfield_callback(f)
+        if formfield:
+            field_list.append((f.name, formfield))
+    fields = SortedDictFromList(field_list)
+    return type(opts.object_name + 'Form', (form,), {'fields': fields, '_model': model, 'save': model_save})
+
+def form_for_instance(instance, form=BaseForm, formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)):
+    """
+    Returns a Form class for the given Django model instance.
+
+    Provide ``form`` if you want to use a custom BaseForm subclass.
+
+    Provide ``formfield_callback`` if you want to define different logic for
+    determining the formfield for a given database field. It's a callable that
+    takes a database Field instance, plus **kwargs, and returns a form Field
+    instance with the given kwargs (i.e. 'initial').
+    """
+    model = instance.__class__
+    opts = model._meta
+    field_list = []
+    for f in opts.fields + opts.many_to_many:
+        current_value = f.value_from_object(instance)
+        formfield = formfield_callback(f, initial=current_value)
+        if formfield:
+            field_list.append((f.name, formfield))
+    fields = SortedDictFromList(field_list)
+    return type(opts.object_name + 'InstanceForm', (form,),
+        {'fields': fields, '_model': model, 'save': make_instance_save(instance)})
+
+def form_for_fields(field_list):
+    "Returns a Form class for the given list of Django database field instances."
+    fields = SortedDictFromList([(f.name, f.formfield()) for f in field_list])
+    return type('FormForFields', (BaseForm,), {'fields': fields})

Added: cs/buildbot/trunk/buildbot/forms/util.py
===================================================================
--- cs/buildbot/trunk/buildbot/forms/util.py	2007-01-26 21:32:04 UTC (rev 5915)
+++ cs/buildbot/trunk/buildbot/forms/util.py	2007-01-26 21:36:20 UTC (rev 5916)
@@ -0,0 +1,71 @@
+from django.conf import settings
+from django.utils.html import escape
+
+# Converts a dictionary to a single string with key="value", XML-style with
+# a leading space. Assumes keys do not need to be XML-escaped.
+flatatt = lambda attrs: u''.join([u' %s="%s"' % (k, escape(v)) for k, v in attrs.items()])
+
+def smart_unicode(s):
+    if not isinstance(s, basestring):
+        s = unicode(str(s))
+    elif not isinstance(s, unicode):
+        s = unicode(s, settings.DEFAULT_CHARSET)
+    return s
+
+class StrAndUnicode(object):
+    """
+    A class whose __str__ returns its __unicode__ as a bytestring
+    according to settings.DEFAULT_CHARSET.
+
+    Useful as a mix-in.
+    """
+    def __str__(self):
+        return self.__unicode__().encode(settings.DEFAULT_CHARSET)
+
+class ErrorDict(dict):
+    """
+    A collection of errors that knows how to display itself in various formats.
+
+    The dictionary keys are the field names, and the values are the errors.
+    """
+    def __str__(self):
+        return self.as_ul()
+
+    def as_ul(self):
+        if not self: return u''
+        return u'<ul class="errorlist">%s</ul>' % ''.join([u'<li>%s%s</li>' % (k, v) for k, v in self.items()])
+
+    def as_text(self):
+        return u'\n'.join([u'* %s\n%s' % (k, u'\n'.join([u'  * %s' % i for i in v])) for k, v in self.items()])
+
+class ErrorList(list):
+    """
+    A collection of errors that knows how to display itself in various formats.
+    """
+    def __str__(self):
+        return self.as_ul()
+
+    def as_ul(self):
+        if not self: return u''
+        return u'<ul class="errorlist">%s</ul>' % ''.join([u'<li>%s</li>' % e for e in self])
+
+    def as_text(self):
+        if not self: return u''
+        return u'\n'.join([u'* %s' % e for e in self])
+
+class ValidationError(Exception):
+    def __init__(self, message):
+        "ValidationError can be passed a string or a list."
+        if isinstance(message, list):
+            self.messages = ErrorList([smart_unicode(msg) for msg in message])
+        else:
+            assert isinstance(message, basestring), ("%s should be a basestring" % repr(message))
+            message = smart_unicode(message)
+            self.messages = ErrorList([message])
+
+    def __str__(self):
+        # This is needed because, without a __str__(), printing an exception
+        # instance would result in this:
+        # AttributeError: ValidationError instance has no attribute 'args'
+        # See http://www.python.org/doc/current/tut/node10.html#handling
+        return repr(self.messages)

Added: cs/buildbot/trunk/buildbot/forms/widgets.py
===================================================================
--- cs/buildbot/trunk/buildbot/forms/widgets.py	2007-01-26 21:32:04 UTC (rev 5915)
+++ cs/buildbot/trunk/buildbot/forms/widgets.py	2007-01-26 21:36:20 UTC (rev 5916)
@@ -0,0 +1,338 @@
+"""
+HTML Widget classes
+"""
+
+__all__ = (
+    'Widget', 'TextInput', 'PasswordInput', 'HiddenInput', 'MultipleHiddenInput',
+    'FileInput', 'Textarea', 'CheckboxInput',
+    'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect', 'CheckboxSelectMultiple',
+    'MultiWidget', 'SplitDateTimeWidget',
+)
+
+from util import flatatt, StrAndUnicode, smart_unicode
+from django.utils.datastructures import MultiValueDict
+from django.utils.html import escape
+from django.utils.translation import gettext
+from itertools import chain
+
+try:
+    set # Only available in Python 2.4+
+except NameError:
+    from sets import Set as set # Python 2.3 fallback
+
+class Widget(object):
+    is_hidden = False          # Determines whether this corresponds to an <input type="hidden">.
+
+    def __init__(self, attrs=None):
+        self.attrs = attrs or {}
+
+    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, 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 != '': final_attrs['value'] = smart_unicode(value) # Only add the 'value' attribute if a value is non-empty.
+        return u'<input%s />' % flatatt(final_attrs)
+
+class TextInput(Input):
+    input_type = 'text'
+
+class PasswordInput(Input):
+    input_type = 'password'
+
+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=()):
+        # choices can be any iterable
+        self.attrs = attrs or {}
+        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 u'\n'.join([(u'<input%s />' % flatatt(dict(value=smart_unicode(v), **final_attrs))) for v in value])
+
+    def value_from_datadict(self, data, name):
+        if isinstance(data, MultiValueDict):
+            return data.getlist(name)
+        return data.get(name, None)
+
+class FileInput(Input):
+    input_type = 'file'
+
+class Textarea(Widget):
+    def render(self, name, value, attrs=None):
+        if value is None: value = ''
+        value = smart_unicode(value)
+        final_attrs = self.build_attrs(attrs, name=name)
+        return u'<textarea%s>%s</textarea>' % (flatatt(final_attrs), escape(value))
+
+class CheckboxInput(Widget):
+    def __init__(self, attrs=None, check_test=bool):
+        # check_test is a callable that takes a value and returns True
+        # if the checkbox should be checked for that value.
+        self.attrs = attrs or {}
+        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):
+            final_attrs['value'] = smart_unicode(value) # Only add the 'value' attribute if a value is non-empty.
+        return u'<input%s />' % flatatt(final_attrs)
+
+class Select(Widget):
+    def __init__(self, attrs=None, choices=()):
+        # choices can be any iterable
+        self.attrs = attrs or {}
+        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%s>' % flatatt(final_attrs)]
+        str_value = smart_unicode(value) # Normalize to string.
+        for option_value, option_label in chain(self.choices, choices):
+            option_value = smart_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, escape(smart_unicode(option_label))))
+        output.append(u'</select>')
+        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', gettext('Unknown')), (u'2', gettext('Yes')), (u'3', gettext('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, 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=()):
+        # choices can be any iterable
+        self.attrs = attrs or {}
+        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([smart_unicode(v) for v in value]) # Normalize to strings.
+        for option_value, option_label in chain(self.choices, choices):
+            option_value = smart_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, escape(smart_unicode(option_label))))
+        output.append(u'</select>')
+        return u'\n'.join(output)
+
+    def value_from_datadict(self, data, 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 = smart_unicode(choice[0])
+        self.choice_label = smart_unicode(choice[1])
+        self.index = index
+
+    def __unicode__(self):
+        return u'<label>%s %s</label>' % (self.tag(), self.choice_label)
+
+    def is_checked(self):
+        return self.value == self.choice_value
+
+    def tag(self):
+        if self.attrs.has_key('id'):
+            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 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):
+        "Outputs a <ul> for this set of radio fields."
+        return u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>' % w for w in self])
+
+class RadioSelect(Select):
+    def render(self, name, value, attrs=None, choices=()):
+        "Returns a RadioFieldRenderer instance rather than a Unicode string."
+        if value is None: value = ''
+        str_value = smart_unicode(value) # Normalize to string.
+        attrs = attrs or {}
+        return RadioFieldRenderer(name, str_value, attrs, list(chain(self.choices, choices)))
+
+    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 = []
+        final_attrs = self.build_attrs(attrs, name=name)
+        output = [u'<ul>']
+        str_values = set([smart_unicode(v) for v in value]) # Normalize to strings.
+        cb = CheckboxInput(final_attrs, check_test=lambda value: value in str_values)
+        for option_value, option_label in chain(self.choices, choices):
+            option_value = smart_unicode(option_value)
+            rendered_cb = cb.render(name, option_value)
+            output.append(u'<li><label>%s %s</label></li>' % (rendered_cb, escape(smart_unicode(option_label))))
+        output.append(u'</ul>')
+        return 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 takes a "decompressed" list of values, not a single
+    value. Each value in this list is rendered in the corresponding widget --
+    the first value is rendered in the first widget, the second value is
+    rendered in the second widget, etc.
+
+    Subclasses should implement decompress(), which specifies how a single
+    value should be converted to a list of values. Subclasses should not
+    have to implement clean().
+
+    Subclasses may implement format_output(), which takes the list of rendered
+    widgets and returns HTML that formats them any way you'd like.
+
+    You'll probably want to use this 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 = []
+        for i, widget in enumerate(self.widgets):
+            try:
+                widget_value = value[i]
+            except KeyError:
+                widget_value = None
+            output.append(widget.render(name + '_%s' % i, widget_value, attrs))
+        return self.format_output(output)
+
+    def value_from_datadict(self, data, name):
+        return [data.get(name + '_%s' % i) for i in range(len(self.widgets))]
+
+    def format_output(self, rendered_widgets):
+        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()]
+        return [None, None]



More information about the cig-commits mailing list