summaryrefslogtreecommitdiffstats
path: root/webapp/django/forms
diff options
context:
space:
mode:
Diffstat (limited to 'webapp/django/forms')
-rw-r--r--webapp/django/forms/__init__.py17
-rw-r--r--webapp/django/forms/extras/__init__.py1
-rw-r--r--webapp/django/forms/extras/widgets.py79
-rw-r--r--webapp/django/forms/fields.py837
-rw-r--r--webapp/django/forms/forms.py396
-rw-r--r--webapp/django/forms/formsets.py288
-rw-r--r--webapp/django/forms/models.py558
-rw-r--r--webapp/django/forms/util.py68
-rw-r--r--webapp/django/forms/widgets.py663
9 files changed, 2907 insertions, 0 deletions
diff --git a/webapp/django/forms/__init__.py b/webapp/django/forms/__init__.py
new file mode 100644
index 0000000000..0d9c68f9e0
--- /dev/null
+++ b/webapp/django/forms/__init__.py
@@ -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 *
diff --git a/webapp/django/forms/extras/__init__.py b/webapp/django/forms/extras/__init__.py
new file mode 100644
index 0000000000..a7f6a9b3f6
--- /dev/null
+++ b/webapp/django/forms/extras/__init__.py
@@ -0,0 +1 @@
+from widgets import *
diff --git a/webapp/django/forms/extras/widgets.py b/webapp/django/forms/extras/widgets.py
new file mode 100644
index 0000000000..ffa7ba2de2
--- /dev/null
+++ b/webapp/django/forms/extras/widgets.py
@@ -0,0 +1,79 @@
+"""
+Extra HTML Widget classes
+"""
+
+import datetime
+import re
+
+from django.forms.widgets import Widget, Select
+from django.utils.dates import MONTHS
+from django.utils.safestring import mark_safe
+
+__all__ = ('SelectDateWidget',)
+
+RE_DATE = re.compile(r'(\d{4})-(\d\d?)-(\d\d?)$')
+
+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:
+ year_val, month_val, day_val = value.year, value.month, value.day
+ except AttributeError:
+ year_val = month_val = day_val = None
+ if isinstance(value, basestring):
+ match = RE_DATE.match(value)
+ if match:
+ year_val, month_val, day_val = [int(v) for v in match.groups()]
+
+ output = []
+
+ if 'id' in self.attrs:
+ id_ = self.attrs['id']
+ else:
+ id_ = 'id_%s' % name
+
+ month_choices = MONTHS.items()
+ month_choices.sort()
+ local_attrs = self.build_attrs(id=self.month_field % id_)
+ select_html = Select(choices=month_choices).render(self.month_field % name, month_val, local_attrs)
+ output.append(select_html)
+
+ day_choices = [(i, i) for i in range(1, 32)]
+ local_attrs['id'] = self.day_field % id_
+ select_html = Select(choices=day_choices).render(self.day_field % name, day_val, local_attrs)
+ output.append(select_html)
+
+ year_choices = [(i, i) for i in self.years]
+ local_attrs['id'] = self.year_field % id_
+ select_html = Select(choices=year_choices).render(self.year_field % name, year_val, local_attrs)
+ output.append(select_html)
+
+ return mark_safe(u'\n'.join(output))
+
+ def id_for_label(self, id_):
+ return '%s_month' % id_
+ id_for_label = classmethod(id_for_label)
+
+ def value_from_datadict(self, data, files, 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 data.get(name, None)
diff --git a/webapp/django/forms/fields.py b/webapp/django/forms/fields.py
new file mode 100644
index 0000000000..ee9b8c62f1
--- /dev/null
+++ b/webapp/django/forms/fields.py
@@ -0,0 +1,837 @@
+"""
+Field classes.
+"""
+
+import copy
+import datetime
+import os
+import re
+import time
+import urlparse
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+# Python 2.3 fallbacks
+try:
+ from decimal import Decimal, DecimalException
+except ImportError:
+ from django.utils._decimal import Decimal, DecimalException
+try:
+ set
+except NameError:
+ from sets import Set as set
+
+from django.utils.translation import ugettext_lazy as _
+from django.utils.encoding import smart_unicode, smart_str
+
+from util import ErrorList, ValidationError
+from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateTimeInput, TimeInput
+from django.core.files.uploadedfile import SimpleUploadedFile as UploadedFile
+
+__all__ = (
+ 'Field', 'CharField', 'IntegerField',
+ 'DEFAULT_DATE_INPUT_FORMATS', 'DateField',
+ 'DEFAULT_TIME_INPUT_FORMATS', 'TimeField',
+ 'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField', 'TimeField',
+ 'RegexField', 'EmailField', 'FileField', 'ImageField', 'URLField',
+ 'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField',
+ 'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
+ 'SplitDateTimeField', 'IPAddressField', 'FilePathField',
+)
+
+# These values, if given to to_python(), will trigger the self.required check.
+EMPTY_VALUES = (None, '')
+
+
+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".
+ default_error_messages = {
+ 'required': _(u'This field is required.'),
+ 'invalid': _(u'Enter a valid value.'),
+ }
+
+ # 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,
+ help_text=None, error_messages=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.
+ # help_text -- An optional string to use as "help text" for this Field.
+ if label is not None:
+ label = smart_unicode(label)
+ self.required, self.label, self.initial = required, label, initial
+ if help_text is None:
+ self.help_text = u''
+ else:
+ self.help_text = smart_unicode(help_text)
+ 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 set_class_error_messages(messages, klass):
+ for base_class in klass.__bases__:
+ set_class_error_messages(messages, base_class)
+ messages.update(getattr(klass, 'default_error_messages', {}))
+
+ messages = {}
+ set_class_error_messages(messages, self.__class__)
+ messages.update(error_messages or {})
+ self.error_messages = messages
+
+ 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(self.error_messages['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 {}
+
+ def __deepcopy__(self, memo):
+ result = copy.copy(self)
+ memo[id(self)] = result
+ result.widget = copy.deepcopy(self.widget, memo)
+ return result
+
+class CharField(Field):
+ default_error_messages = {
+ 'max_length': _(u'Ensure this value has at most %(max)d characters (it has %(length)d).'),
+ 'min_length': _(u'Ensure this value has at least %(min)d characters (it has %(length)d).'),
+ }
+
+ def __init__(self, max_length=None, min_length=None, *args, **kwargs):
+ self.max_length, self.min_length = max_length, min_length
+ super(CharField, self).__init__(*args, **kwargs)
+
+ 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)
+ value_length = len(value)
+ if self.max_length is not None and value_length > self.max_length:
+ raise ValidationError(self.error_messages['max_length'] % {'max': self.max_length, 'length': value_length})
+ if self.min_length is not None and value_length < self.min_length:
+ raise ValidationError(self.error_messages['min_length'] % {'min': self.min_length, 'length': value_length})
+ return value
+
+ def widget_attrs(self, widget):
+ if self.max_length is not None and isinstance(widget, (TextInput, PasswordInput)):
+ # The HTML attribute is maxlength, not max_length.
+ return {'maxlength': str(self.max_length)}
+
+class IntegerField(Field):
+ default_error_messages = {
+ 'invalid': _(u'Enter a whole number.'),
+ 'max_value': _(u'Ensure this value is less than or equal to %s.'),
+ 'min_value': _(u'Ensure this value is greater than or equal to %s.'),
+ }
+
+ def __init__(self, max_value=None, min_value=None, *args, **kwargs):
+ self.max_value, self.min_value = max_value, min_value
+ super(IntegerField, self).__init__(*args, **kwargs)
+
+ 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(str(value))
+ except (ValueError, TypeError):
+ raise ValidationError(self.error_messages['invalid'])
+ if self.max_value is not None and value > self.max_value:
+ raise ValidationError(self.error_messages['max_value'] % self.max_value)
+ if self.min_value is not None and value < self.min_value:
+ raise ValidationError(self.error_messages['min_value'] % self.min_value)
+ return value
+
+class FloatField(Field):
+ default_error_messages = {
+ 'invalid': _(u'Enter a number.'),
+ 'max_value': _(u'Ensure this value is less than or equal to %s.'),
+ 'min_value': _(u'Ensure this value is greater than or equal to %s.'),
+ }
+
+ def __init__(self, max_value=None, min_value=None, *args, **kwargs):
+ self.max_value, self.min_value = max_value, min_value
+ Field.__init__(self, *args, **kwargs)
+
+ def clean(self, value):
+ """
+ Validates that float() can be called on the input. Returns a float.
+ Returns None for empty values.
+ """
+ super(FloatField, self).clean(value)
+ if not self.required and value in EMPTY_VALUES:
+ return None
+ try:
+ value = float(value)
+ except (ValueError, TypeError):
+ raise ValidationError(self.error_messages['invalid'])
+ if self.max_value is not None and value > self.max_value:
+ raise ValidationError(self.error_messages['max_value'] % self.max_value)
+ if self.min_value is not None and value < self.min_value:
+ raise ValidationError(self.error_messages['min_value'] % self.min_value)
+ return value
+
+class DecimalField(Field):
+ default_error_messages = {
+ 'invalid': _(u'Enter a number.'),
+ 'max_value': _(u'Ensure this value is less than or equal to %s.'),
+ 'min_value': _(u'Ensure this value is greater than or equal to %s.'),
+ 'max_digits': _('Ensure that there are no more than %s digits in total.'),
+ 'max_decimal_places': _('Ensure that there are no more than %s decimal places.'),
+ 'max_whole_digits': _('Ensure that there are no more than %s digits before the decimal point.')
+ }
+
+ def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs):
+ self.max_value, self.min_value = max_value, min_value
+ self.max_digits, self.decimal_places = max_digits, decimal_places
+ Field.__init__(self, *args, **kwargs)
+
+ def clean(self, value):
+ """
+ Validates that the input is a decimal number. Returns a Decimal
+ instance. Returns None for empty values. Ensures that there are no more
+ than max_digits in the number, and no more than decimal_places digits
+ after the decimal point.
+ """
+ super(DecimalField, self).clean(value)
+ if not self.required and value in EMPTY_VALUES:
+ return None
+ value = smart_str(value).strip()
+ try:
+ value = Decimal(value)
+ except DecimalException:
+ raise ValidationError(self.error_messages['invalid'])
+
+ sign, digittuple, exponent = value.as_tuple()
+ decimals = abs(exponent)
+ # digittuple doesn't include any leading zeros.
+ digits = len(digittuple)
+ if decimals >= digits:
+ # We have leading zeros up to or past the decimal point. Count
+ # everything past the decimal point as a digit. We also add one
+ # for leading zeros before the decimal point (any number of leading
+ # whole zeros collapse to one digit).
+ digits = decimals + 1
+ whole_digits = digits - decimals
+
+ if self.max_value is not None and value > self.max_value:
+ raise ValidationError(self.error_messages['max_value'] % self.max_value)
+ if self.min_value is not None and value < self.min_value:
+ raise ValidationError(self.error_messages['min_value'] % self.min_value)
+ if self.max_digits is not None and digits > self.max_digits:
+ raise ValidationError(self.error_messages['max_digits'] % self.max_digits)
+ if self.decimal_places is not None and decimals > self.decimal_places:
+ raise ValidationError(self.error_messages['max_decimal_places'] % self.decimal_places)
+ if self.max_digits is not None and self.decimal_places is not None and whole_digits > (self.max_digits - self.decimal_places):
+ raise ValidationError(self.error_messages['max_whole_digits'] % (self.max_digits - self.decimal_places))
+ 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):
+ default_error_messages = {
+ 'invalid': _(u'Enter a valid date.'),
+ }
+
+ def __init__(self, input_formats=None, *args, **kwargs):
+ super(DateField, self).__init__(*args, **kwargs)
+ 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(self.error_messages['invalid'])
+
+DEFAULT_TIME_INPUT_FORMATS = (
+ '%H:%M:%S', # '14:30:59'
+ '%H:%M', # '14:30'
+)
+
+class TimeField(Field):
+ widget = TimeInput
+ default_error_messages = {
+ 'invalid': _(u'Enter a valid time.')
+ }
+
+ def __init__(self, input_formats=None, *args, **kwargs):
+ super(TimeField, self).__init__(*args, **kwargs)
+ 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(self.error_messages['invalid'])
+
+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):
+ widget = DateTimeInput
+ default_error_messages = {
+ 'invalid': _(u'Enter a valid date/time.'),
+ }
+
+ def __init__(self, input_formats=None, *args, **kwargs):
+ super(DateTimeField, self).__init__(*args, **kwargs)
+ 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)
+ if isinstance(value, list):
+ # Input comes from a SplitDateTimeWidget, for example. So, it's two
+ # components: date and time.
+ if len(value) != 2:
+ raise ValidationError(self.error_messages['invalid'])
+ value = '%s %s' % tuple(value)
+ for format in self.input_formats:
+ try:
+ return datetime.datetime(*time.strptime(value, format)[:6])
+ except ValueError:
+ continue
+ raise ValidationError(self.error_messages['invalid'])
+
+class RegexField(CharField):
+ def __init__(self, regex, max_length=None, min_length=None, error_message=None, *args, **kwargs):
+ """
+ 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.
+ """
+ # error_message is just kept for backwards compatibility:
+ if error_message:
+ error_messages = kwargs.get('error_messages') or {}
+ error_messages['invalid'] = error_message
+ kwargs['error_messages'] = error_messages
+ super(RegexField, self).__init__(max_length, min_length, *args, **kwargs)
+ if isinstance(regex, basestring):
+ regex = re.compile(regex)
+ self.regex = regex
+
+ def clean(self, value):
+ """
+ Validates that the input matches the regular expression. Returns a
+ Unicode object.
+ """
+ value = super(RegexField, self).clean(value)
+ if value == u'':
+ return value
+ if not self.regex.search(value):
+ raise ValidationError(self.error_messages['invalid'])
+ 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):
+ default_error_messages = {
+ 'invalid': _(u'Enter a valid e-mail address.'),
+ }
+
+ def __init__(self, max_length=None, min_length=None, *args, **kwargs):
+ RegexField.__init__(self, email_re, max_length, min_length, *args,
+ **kwargs)
+
+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 FileField(Field):
+ widget = FileInput
+ default_error_messages = {
+ 'invalid': _(u"No file was submitted. Check the encoding type on the form."),
+ 'missing': _(u"No file was submitted."),
+ 'empty': _(u"The submitted file is empty."),
+ }
+
+ def __init__(self, *args, **kwargs):
+ super(FileField, self).__init__(*args, **kwargs)
+
+ def clean(self, data, initial=None):
+ super(FileField, self).clean(initial or data)
+ if not self.required and data in EMPTY_VALUES:
+ return None
+ elif not data and initial:
+ return initial
+
+ # UploadedFile objects should have name and size attributes.
+ try:
+ file_name = data.name
+ file_size = data.size
+ except AttributeError:
+ raise ValidationError(self.error_messages['invalid'])
+
+ if not file_name:
+ raise ValidationError(self.error_messages['invalid'])
+ if not file_size:
+ raise ValidationError(self.error_messages['empty'])
+
+ return data
+
+class ImageField(FileField):
+ default_error_messages = {
+ 'invalid_image': _(u"Upload a valid image. The file you uploaded was either not an image or a corrupted image."),
+ }
+
+ def clean(self, data, initial=None):
+ """
+ Checks that the file-upload field data contains a valid image (GIF, JPG,
+ PNG, possibly others -- whatever the Python Imaging Library supports).
+ """
+ f = super(ImageField, self).clean(data, initial)
+ if f is None:
+ return None
+ elif not data and initial:
+ return initial
+ from PIL import Image
+
+ # We need to get a file object for PIL. We might have a path or we might
+ # have to read the data into memory.
+ if hasattr(data, 'temporary_file_path'):
+ file = data.temporary_file_path()
+ else:
+ if hasattr(data, 'read'):
+ file = StringIO(data.read())
+ else:
+ file = StringIO(data['content'])
+
+ try:
+ # load() is the only method that can spot a truncated JPEG,
+ # but it cannot be called sanely after verify()
+ trial_image = Image.open(file)
+ trial_image.load()
+
+ # Since we're about to use the file again we have to reset the
+ # file object if possible.
+ if hasattr(file, 'reset'):
+ file.reset()
+
+ # verify() is the only method that can spot a corrupt PNG,
+ # but it must be called immediately after the constructor
+ trial_image = Image.open(file)
+ trial_image.verify()
+ except ImportError:
+ # Under PyPy, it is possible to import PIL. However, the underlying
+ # _imaging C module isn't available, so an ImportError will be
+ # raised. Catch and re-raise.
+ raise
+ except Exception: # Python Imaging Library doesn't recognize it as an image
+ raise ValidationError(self.error_messages['invalid_image'])
+ if hasattr(f, 'seek') and callable(f.seek):
+ f.seek(0)
+ return f
+
+url_re = re.compile(
+ r'^https?://' # http:// or https://
+ r'(?:(?:[A-Z0-9-]+\.)+[A-Z]{2,6}|' #domain...
+ r'localhost|' #localhost...
+ r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
+ r'(?::\d+)?' # optional port
+ r'(?:/?|/\S+)$', re.IGNORECASE)
+
+class URLField(RegexField):
+ default_error_messages = {
+ 'invalid': _(u'Enter a valid URL.'),
+ 'invalid_link': _(u'This URL appears to be a broken link.'),
+ }
+
+ def __init__(self, max_length=None, min_length=None, verify_exists=False,
+ validator_user_agent=URL_VALIDATOR_USER_AGENT, *args, **kwargs):
+ super(URLField, self).__init__(url_re, max_length, min_length, *args,
+ **kwargs)
+ self.verify_exists = verify_exists
+ self.user_agent = validator_user_agent
+
+ def clean(self, value):
+ # If no URL scheme given, assume http://
+ if value and '://' not in value:
+ value = u'http://%s' % value
+ # If no URL path given, assume /
+ if value and not urlparse.urlsplit(value)[2]:
+ value += '/'
+ value = super(URLField, self).clean(value)
+ if value == u'':
+ return value
+ if self.verify_exists:
+ import urllib2
+ 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(self.error_messages['invalid'])
+ except: # urllib2.URLError, httplib.InvalidURL, etc.
+ raise ValidationError(self.error_messages['invalid_link'])
+ return value
+
+class BooleanField(Field):
+ widget = CheckboxInput
+
+ def clean(self, value):
+ """Returns a Python boolean object."""
+ # Explicitly check for the string 'False', which is what a hidden field
+ # will submit for False. Because bool("True") == True, we don't need to
+ # handle that explicitly.
+ if value == 'False':
+ value = False
+ else:
+ value = bool(value)
+ super(BooleanField, self).clean(value)
+ if not value and self.required:
+ raise ValidationError(self.error_messages['required'])
+ return 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):
+ widget = Select
+ default_error_messages = {
+ 'invalid_choice': _(u'Select a valid choice. %(value)s is not one of the available choices.'),
+ }
+
+ def __init__(self, choices=(), required=True, widget=None, label=None,
+ initial=None, help_text=None, *args, **kwargs):
+ super(ChoiceField, self).__init__(required, widget, label, initial,
+ help_text, *args, **kwargs)
+ self.choices = choices
+
+ def _get_choices(self):
+ return self._choices
+
+ def _set_choices(self, value):
+ # Setting choices also sets the choices on the widget.
+ # choices can be any iterable, but we call list() on it because
+ # it will be consumed more than once.
+ self._choices = self.widget.choices = list(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
+ if not self.valid_value(value):
+ raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
+ return value
+
+ def valid_value(self, value):
+ "Check to see if the provided value is a valid choice"
+ for k, v in self.choices:
+ if type(v) in (tuple, list):
+ # This is an optgroup, so look inside the group for options
+ for k2, v2 in v:
+ if value == smart_unicode(k2):
+ return True
+ else:
+ if value == smart_unicode(k):
+ return True
+ return False
+
+class MultipleChoiceField(ChoiceField):
+ hidden_widget = MultipleHiddenInput
+ widget = SelectMultiple
+ default_error_messages = {
+ 'invalid_choice': _(u'Select a valid choice. %(value)s is not one of the available choices.'),
+ 'invalid_list': _(u'Enter a list of values.'),
+ }
+
+ def clean(self, value):
+ """
+ Validates that the input is a list or tuple.
+ """
+ if self.required and not value:
+ raise ValidationError(self.error_messages['required'])
+ elif not self.required and not value:
+ return []
+ if not isinstance(value, (list, tuple)):
+ raise ValidationError(self.error_messages['invalid_list'])
+ new_value = [smart_unicode(val) for val in value]
+ # Validate that each value in the value list is in self.choices.
+ for val in new_value:
+ if not self.valid_value(val):
+ raise ValidationError(self.error_messages['invalid_choice'] % {'value': val})
+ return new_value
+
+class ComboField(Field):
+ """
+ A Field whose clean() method calls multiple Field clean() methods.
+ """
+ def __init__(self, fields=(), *args, **kwargs):
+ super(ComboField, self).__init__(*args, **kwargs)
+ # 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 aggregates the logic of multiple Fields.
+
+ Its clean() method takes a "decompressed" list of values, which are then
+ cleaned into a single value according to self.fields. 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 not have to implement clean(). Instead, they must
+ implement compress(), which takes a list of valid values and returns a
+ "compressed" version of those values -- a single value.
+
+ You'll probably want to use this with MultiWidget.
+ """
+ default_error_messages = {
+ 'invalid': _(u'Enter a list of values.'),
+ }
+
+ def __init__(self, fields=(), *args, **kwargs):
+ super(MultiValueField, self).__init__(*args, **kwargs)
+ # 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 not value or isinstance(value, (list, tuple)):
+ if not value or not [v for v in value if v not in EMPTY_VALUES]:
+ if self.required:
+ raise ValidationError(self.error_messages['required'])
+ else:
+ return self.compress([])
+ else:
+ raise ValidationError(self.error_messages['invalid'])
+ for i, field in enumerate(self.fields):
+ try:
+ field_value = value[i]
+ except IndexError:
+ field_value = None
+ if self.required and field_value in EMPTY_VALUES:
+ raise ValidationError(self.error_messages['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 FilePathField(ChoiceField):
+ def __init__(self, path, match=None, recursive=False, required=True,
+ widget=None, label=None, initial=None, help_text=None,
+ *args, **kwargs):
+ self.path, self.match, self.recursive = path, match, recursive
+ super(FilePathField, self).__init__(choices=(), required=required,
+ widget=widget, label=label, initial=initial, help_text=help_text,
+ *args, **kwargs)
+ self.choices = []
+ if self.match is not None:
+ self.match_re = re.compile(self.match)
+ if recursive:
+ for root, dirs, files in os.walk(self.path):
+ for f in files:
+ if self.match is None or self.match_re.search(f):
+ f = os.path.join(root, f)
+ self.choices.append((f, f.replace(path, "", 1)))
+ else:
+ try:
+ for f in os.listdir(self.path):
+ full_file = os.path.join(self.path, f)
+ if os.path.isfile(full_file) and (self.match is None or self.match_re.search(f)):
+ self.choices.append((full_file, f))
+ except OSError:
+ pass
+ self.widget.choices = self.choices
+
+class SplitDateTimeField(MultiValueField):
+ default_error_messages = {
+ 'invalid_date': _(u'Enter a valid date.'),
+ 'invalid_time': _(u'Enter a valid time.'),
+ }
+
+ def __init__(self, *args, **kwargs):
+ errors = self.default_error_messages.copy()
+ if 'error_messages' in kwargs:
+ errors.update(kwargs['error_messages'])
+ fields = (
+ DateField(error_messages={'invalid': errors['invalid_date']}),
+ TimeField(error_messages={'invalid': errors['invalid_time']}),
+ )
+ super(SplitDateTimeField, self).__init__(fields, *args, **kwargs)
+
+ def compress(self, data_list):
+ if data_list:
+ # Raise a validation error if time or date is empty
+ # (possible if SplitDateTimeField has required=False).
+ if data_list[0] in EMPTY_VALUES:
+ raise ValidationError(self.error_messages['invalid_date'])
+ if data_list[1] in EMPTY_VALUES:
+ raise ValidationError(self.error_messages['invalid_time'])
+ return datetime.datetime.combine(*data_list)
+ return None
+
+ipv4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$')
+
+class IPAddressField(RegexField):
+ default_error_messages = {
+ 'invalid': _(u'Enter a valid IPv4 address.'),
+ }
+
+ def __init__(self, *args, **kwargs):
+ super(IPAddressField, self).__init__(ipv4_re, *args, **kwargs)
diff --git a/webapp/django/forms/forms.py b/webapp/django/forms/forms.py
new file mode 100644
index 0000000000..753ee254bc
--- /dev/null
+++ b/webapp/django/forms/forms.py
@@ -0,0 +1,396 @@
+"""
+Form classes
+"""
+
+from copy import deepcopy
+
+from django.utils.datastructures import SortedDict
+from django.utils.html import escape
+from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode
+from django.utils.safestring import mark_safe
+
+from fields import Field, FileField
+from widgets import Media, media_property, TextInput, Textarea
+from util import flatatt, 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('_', ' ')
+
+def get_declared_fields(bases, attrs, with_base_fields=True):
+ """
+ Create a list of form field instances from the passed in 'attrs', plus any
+ similar fields on the base classes (in 'bases'). This is used by both the
+ Form and ModelForm metclasses.
+
+ If 'with_base_fields' is True, all fields from the bases are used.
+ Otherwise, only fields in the 'declared_fields' attribute on the bases are
+ used. The distinction is useful in ModelForm subclassing.
+ Also integrates any additional media definitions
+ """
+ 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))
+
+ # If this class is subclassing another Form, add that Form's fields.
+ # Note that we loop over the bases in *reverse*. This is necessary in
+ # order to preserve the correct order of fields.
+ if with_base_fields:
+ for base in bases[::-1]:
+ if hasattr(base, 'base_fields'):
+ fields = base.base_fields.items() + fields
+ else:
+ for base in bases[::-1]:
+ if hasattr(base, 'declared_fields'):
+ fields = base.declared_fields.items() + fields
+
+ return SortedDict(fields)
+
+class DeclarativeFieldsMetaclass(type):
+ """
+ Metaclass that converts Field attributes to a dictionary called
+ 'base_fields', taking into account parent class 'base_fields' as well.
+ """
+ def __new__(cls, name, bases, attrs):
+ attrs['base_fields'] = get_declared_fields(bases, attrs)
+ new_class = super(DeclarativeFieldsMetaclass,
+ cls).__new__(cls, name, bases, attrs)
+ if 'media' not in attrs:
+ new_class.media = media_property(new_class)
+ return new_class
+
+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, files=None, auto_id='id_%s', prefix=None,
+ initial=None, error_class=ErrorList, label_suffix=':',
+ empty_permitted=False):
+ self.is_bound = data is not None or files is not None
+ self.data = data or {}
+ self.files = files or {}
+ self.auto_id = auto_id
+ self.prefix = prefix
+ self.initial = initial or {}
+ self.error_class = error_class
+ self.label_suffix = label_suffix
+ self.empty_permitted = empty_permitted
+ self._errors = None # Stores the errors after clean() has been called.
+ self._changed_data = None
+
+ # The base_fields class attribute is the *class-wide* definition of
+ # fields. Because a particular *instance* of the class might want to
+ # alter self.fields, we create self.fields here by copying base_fields.
+ # Instances should always modify self.fields; they should not modify
+ # self.base_fields.
+ self.fields = deepcopy(self.base_fields)
+
+ 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 _get_errors(self):
+ "Returns an ErrorDict for the data provided for the form"
+ if self._errors is None:
+ self.full_clean()
+ return self._errors
+ errors = property(_get_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, help_text_html, 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 = self.error_class([escape(error) for error in bf.errors]) # Escape and cache in local variable.
+ if bf.is_hidden:
+ if bf_errors:
+ top_errors.extend([u'(Hidden field %s) %s' % (name, force_unicode(e)) for e in bf_errors])
+ hidden_fields.append(unicode(bf))
+ else:
+ if errors_on_separate_row and bf_errors:
+ output.append(error_row % force_unicode(bf_errors))
+ if bf.label:
+ label = escape(force_unicode(bf.label))
+ # Only add the suffix if the label does not end in
+ # punctuation.
+ if self.label_suffix:
+ if label[-1] not in ':?.!':
+ label += self.label_suffix
+ label = bf.label_tag(label) or ''
+ else:
+ label = ''
+ if field.help_text:
+ help_text = help_text_html % force_unicode(field.help_text)
+ else:
+ help_text = u''
+ output.append(normal_row % {'errors': force_unicode(bf_errors), 'label': force_unicode(label), 'field': unicode(bf), 'help_text': help_text})
+ if top_errors:
+ output.insert(0, error_row % force_unicode(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 mark_safe(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%(help_text)s</td></tr>', u'<tr><td colspan="2">%s</td></tr>', '</td></tr>', u'<br />%s', 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%(help_text)s</li>', u'<li>%s</li>', '</li>', u' %s', False)
+
+ def as_p(self):
+ "Returns this form rendered as HTML <p>s."
+ return self._html_output(u'<p>%(label)s %(field)s%(help_text)s</p>', u'%s', '</p>', u' %s', 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, self.error_class())
+
+ def full_clean(self):
+ """
+ Cleans all of self.data and populates self._errors and
+ self.cleaned_data.
+ """
+ self._errors = ErrorDict()
+ if not self.is_bound: # Stop further processing.
+ return
+ self.cleaned_data = {}
+ # If the form is permitted to be empty, and none of the form data has
+ # changed from the initial data, short circuit any validation.
+ if self.empty_permitted and not self.has_changed():
+ return
+ for name, field in self.fields.items():
+ # value_from_datadict() gets the data from the data dictionaries.
+ # 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.files, self.add_prefix(name))
+ try:
+ if isinstance(field, FileField):
+ initial = self.initial.get(name, field.initial)
+ value = field.clean(value, initial)
+ else:
+ value = field.clean(value)
+ self.cleaned_data[name] = value
+ if hasattr(self, 'clean_%s' % name):
+ value = getattr(self, 'clean_%s' % name)()
+ self.cleaned_data[name] = value
+ except ValidationError, e:
+ self._errors[name] = e.messages
+ if name in self.cleaned_data:
+ del self.cleaned_data[name]
+ try:
+ self.cleaned_data = self.clean()
+ except ValidationError, e:
+ self._errors[NON_FIELD_ERRORS] = e.messages
+ if self._errors:
+ delattr(self, 'cleaned_data')
+
+ 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.cleaned_data
+
+ def has_changed(self):
+ """
+ Returns True if data differs from initial.
+ """
+ return bool(self.changed_data)
+
+ def _get_changed_data(self):
+ if self._changed_data is None:
+ self._changed_data = []
+ # XXX: For now we're asking the individual widgets whether or not the
+ # data has changed. It would probably be more efficient to hash the
+ # initial data, store it in a hidden field, and compare a hash of the
+ # submitted data, but we'd need a way to easily get the string value
+ # for a given field. Right now, that logic is embedded in the render
+ # method of each widget.
+ for name, field in self.fields.items():
+ prefixed_name = self.add_prefix(name)
+ data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
+ initial_value = self.initial.get(name, field.initial)
+ if field.widget._has_changed(initial_value, data_value):
+ self._changed_data.append(name)
+ return self._changed_data
+ changed_data = property(_get_changed_data)
+
+ def _get_media(self):
+ """
+ Provide a description of all media required to render the widgets on this form
+ """
+ media = Media()
+ for field in self.fields.values():
+ media = media + field.widget.media
+ return media
+ media = property(_get_media)
+
+ def is_multipart(self):
+ """
+ Returns True if the form needs to be multipart-encrypted, i.e. it has
+ FileInput. Otherwise, False.
+ """
+ for field in self.fields.values():
+ if field.widget.needs_multipart_form:
+ return True
+ return False
+
+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
+ self.help_text = field.help_text or ''
+
+ def __unicode__(self):
+ """Renders this field as an HTML widget."""
+ return self.as_widget()
+
+ def _errors(self):
+ """
+ Returns an ErrorList for this field. Returns an empty ErrorList
+ if there are none.
+ """
+ return self.form.errors.get(self.name, self.form.error_class())
+ errors = property(_errors)
+
+ def as_widget(self, widget=None, attrs=None):
+ """
+ Renders the field by rendering the passed widget, adding any HTML
+ attributes passed as attrs. If no widget is specified, then the
+ field's default widget will be used.
+ """
+ if not widget:
+ widget = self.field.widget
+ attrs = attrs or {}
+ auto_id = self.auto_id
+ if auto_id and 'id' not in attrs and 'id' not in widget.attrs:
+ attrs['id'] = auto_id
+ if not self.form.is_bound:
+ data = self.form.initial.get(self.name, self.field.initial)
+ if callable(data):
+ data = data()
+ 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.form.files, 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 mark_safe(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 smart_unicode(auto_id):
+ return smart_unicode(auto_id) % self.html_name
+ elif auto_id:
+ return self.html_name
+ return ''
+ auto_id = property(_auto_id)
diff --git a/webapp/django/forms/formsets.py b/webapp/django/forms/formsets.py
new file mode 100644
index 0000000000..2f13bf5fed
--- /dev/null
+++ b/webapp/django/forms/formsets.py
@@ -0,0 +1,288 @@
+from forms import Form
+from django.utils.encoding import StrAndUnicode
+from django.utils.safestring import mark_safe
+from fields import IntegerField, BooleanField
+from widgets import Media, HiddenInput
+from util import ErrorList, ValidationError
+
+__all__ = ('BaseFormSet', 'all_valid')
+
+# special field names
+TOTAL_FORM_COUNT = 'TOTAL_FORMS'
+INITIAL_FORM_COUNT = 'INITIAL_FORMS'
+ORDERING_FIELD_NAME = 'ORDER'
+DELETION_FIELD_NAME = 'DELETE'
+
+class ManagementForm(Form):
+ """
+ ``ManagementForm`` is used to keep track of how many form instances
+ are displayed on the page. If adding new forms via javascript, you should
+ increment the count field of this form as well.
+ """
+ def __init__(self, *args, **kwargs):
+ self.base_fields[TOTAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
+ self.base_fields[INITIAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
+ super(ManagementForm, self).__init__(*args, **kwargs)
+
+class BaseFormSet(StrAndUnicode):
+ """
+ A collection of instances of the same Form class.
+ """
+ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
+ initial=None, error_class=ErrorList):
+ self.is_bound = data is not None or files is not None
+ self.prefix = prefix or 'form'
+ self.auto_id = auto_id
+ self.data = data
+ self.files = files
+ self.initial = initial
+ self.error_class = error_class
+ self._errors = None
+ self._non_form_errors = None
+ # initialization is different depending on whether we recieved data, initial, or nothing
+ if data or files:
+ self.management_form = ManagementForm(data, auto_id=self.auto_id, prefix=self.prefix)
+ if self.management_form.is_valid():
+ self._total_form_count = self.management_form.cleaned_data[TOTAL_FORM_COUNT]
+ self._initial_form_count = self.management_form.cleaned_data[INITIAL_FORM_COUNT]
+ else:
+ raise ValidationError('ManagementForm data is missing or has been tampered with')
+ else:
+ if initial:
+ self._initial_form_count = len(initial)
+ if self._initial_form_count > self.max_num and self.max_num > 0:
+ self._initial_form_count = self.max_num
+ self._total_form_count = self._initial_form_count + self.extra
+ else:
+ self._initial_form_count = 0
+ self._total_form_count = self.extra
+ if self._total_form_count > self.max_num and self.max_num > 0:
+ self._total_form_count = self.max_num
+ initial = {TOTAL_FORM_COUNT: self._total_form_count,
+ INITIAL_FORM_COUNT: self._initial_form_count}
+ self.management_form = ManagementForm(initial=initial, auto_id=self.auto_id, prefix=self.prefix)
+
+ # construct the forms in the formset
+ self._construct_forms()
+
+ def __unicode__(self):
+ return self.as_table()
+
+ def _construct_forms(self):
+ # instantiate all the forms and put them in self.forms
+ self.forms = []
+ for i in xrange(self._total_form_count):
+ self.forms.append(self._construct_form(i))
+
+ def _construct_form(self, i, **kwargs):
+ """
+ Instantiates and returns the i-th form instance in a formset.
+ """
+ defaults = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
+ if self.data or self.files:
+ defaults['data'] = self.data
+ defaults['files'] = self.files
+ if self.initial:
+ try:
+ defaults['initial'] = self.initial[i]
+ except IndexError:
+ pass
+ # Allow extra forms to be empty.
+ if i >= self._initial_form_count:
+ defaults['empty_permitted'] = True
+ defaults.update(kwargs)
+ form = self.form(**defaults)
+ self.add_fields(form, i)
+ return form
+
+ def _get_initial_forms(self):
+ """Return a list of all the intial forms in this formset."""
+ return self.forms[:self._initial_form_count]
+ initial_forms = property(_get_initial_forms)
+
+ def _get_extra_forms(self):
+ """Return a list of all the extra forms in this formset."""
+ return self.forms[self._initial_form_count:]
+ extra_forms = property(_get_extra_forms)
+
+ # Maybe this should just go away?
+ def _get_cleaned_data(self):
+ """
+ Returns a list of form.cleaned_data dicts for every form in self.forms.
+ """
+ if not self.is_valid():
+ raise AttributeError("'%s' object has no attribute 'cleaned_data'" % self.__class__.__name__)
+ return [form.cleaned_data for form in self.forms]
+ cleaned_data = property(_get_cleaned_data)
+
+ def _get_deleted_forms(self):
+ """
+ Returns a list of forms that have been marked for deletion. Raises an
+ AttributeError is deletion is not allowed.
+ """
+ if not self.is_valid() or not self.can_delete:
+ raise AttributeError("'%s' object has no attribute 'deleted_forms'" % self.__class__.__name__)
+ # construct _deleted_form_indexes which is just a list of form indexes
+ # that have had their deletion widget set to True
+ if not hasattr(self, '_deleted_form_indexes'):
+ self._deleted_form_indexes = []
+ for i in range(0, self._total_form_count):
+ form = self.forms[i]
+ # if this is an extra form and hasn't changed, don't consider it
+ if i >= self._initial_form_count and not form.has_changed():
+ continue
+ if form.cleaned_data[DELETION_FIELD_NAME]:
+ self._deleted_form_indexes.append(i)
+ return [self.forms[i] for i in self._deleted_form_indexes]
+ deleted_forms = property(_get_deleted_forms)
+
+ def _get_ordered_forms(self):
+ """
+ Returns a list of form in the order specified by the incoming data.
+ Raises an AttributeError is deletion is not allowed.
+ """
+ if not self.is_valid() or not self.can_order:
+ raise AttributeError("'%s' object has no attribute 'ordered_forms'" % self.__class__.__name__)
+ # Construct _ordering, which is a list of (form_index, order_field_value)
+ # tuples. After constructing this list, we'll sort it by order_field_value
+ # so we have a way to get to the form indexes in the order specified
+ # by the form data.
+ if not hasattr(self, '_ordering'):
+ self._ordering = []
+ for i in range(0, self._total_form_count):
+ form = self.forms[i]
+ # if this is an extra form and hasn't changed, don't consider it
+ if i >= self._initial_form_count and not form.has_changed():
+ continue
+ # don't add data marked for deletion to self.ordered_data
+ if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
+ continue
+ # A sort function to order things numerically ascending, but
+ # None should be sorted below anything else. Allowing None as
+ # a comparison value makes it so we can leave ordering fields
+ # blamk.
+ def compare_ordering_values(x, y):
+ if x[1] is None:
+ return 1
+ if y[1] is None:
+ return -1
+ return x[1] - y[1]
+ self._ordering.append((i, form.cleaned_data[ORDERING_FIELD_NAME]))
+ # After we're done populating self._ordering, sort it.
+ self._ordering.sort(compare_ordering_values)
+ # Return a list of form.cleaned_data dicts in the order spcified by
+ # the form data.
+ return [self.forms[i[0]] for i in self._ordering]
+ ordered_forms = property(_get_ordered_forms)
+
+ def non_form_errors(self):
+ """
+ Returns an ErrorList of errors that aren't associated with a particular
+ form -- i.e., from formset.clean(). Returns an empty ErrorList if there
+ are none.
+ """
+ if self._non_form_errors is not None:
+ return self._non_form_errors
+ return self.error_class()
+
+ def _get_errors(self):
+ """
+ Returns a list of form.errors for every form in self.forms.
+ """
+ if self._errors is None:
+ self.full_clean()
+ return self._errors
+ errors = property(_get_errors)
+
+ def is_valid(self):
+ """
+ Returns True if form.errors is empty for every form in self.forms.
+ """
+ if not self.is_bound:
+ return False
+ # We loop over every form.errors here rather than short circuiting on the
+ # first failure to make sure validation gets triggered for every form.
+ forms_valid = True
+ for errors in self.errors:
+ if bool(errors):
+ forms_valid = False
+ return forms_valid and not bool(self.non_form_errors())
+
+ def full_clean(self):
+ """
+ Cleans all of self.data and populates self._errors.
+ """
+ self._errors = []
+ if not self.is_bound: # Stop further processing.
+ return
+ for i in range(0, self._total_form_count):
+ form = self.forms[i]
+ self._errors.append(form.errors)
+ # Give self.clean() a chance to do cross-form validation.
+ try:
+ self.clean()
+ except ValidationError, e:
+ self._non_form_errors = e.messages
+
+ def clean(self):
+ """
+ Hook for doing any extra formset-wide cleaning after Form.clean() has
+ been called on every form. Any ValidationError raised by this method
+ will not be associated with a particular form; it will be accesible
+ via formset.non_form_errors()
+ """
+ pass
+
+ def add_fields(self, form, index):
+ """A hook for adding extra fields on to each form instance."""
+ if self.can_order:
+ # Only pre-fill the ordering field for initial forms.
+ if index < self._initial_form_count:
+ form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1, required=False)
+ else:
+ form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', required=False)
+ if self.can_delete:
+ form.fields[DELETION_FIELD_NAME] = BooleanField(label='Delete', required=False)
+
+ def add_prefix(self, index):
+ return '%s-%s' % (self.prefix, index)
+
+ def is_multipart(self):
+ """
+ Returns True if the formset needs to be multipart-encrypted, i.e. it
+ has FileInput. Otherwise, False.
+ """
+ return self.forms[0].is_multipart()
+
+ def _get_media(self):
+ # All the forms on a FormSet are the same, so you only need to
+ # interrogate the first form for media.
+ if self.forms:
+ return self.forms[0].media
+ else:
+ return Media()
+ media = property(_get_media)
+
+ def as_table(self):
+ "Returns this formset rendered as HTML <tr>s -- excluding the <table></table>."
+ # XXX: there is no semantic division between forms here, there
+ # probably should be. It might make sense to render each form as a
+ # table row with each field as a td.
+ forms = u' '.join([form.as_table() for form in self.forms])
+ return mark_safe(u'\n'.join([unicode(self.management_form), forms]))
+
+def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
+ can_delete=False, max_num=0):
+ """Return a FormSet for the given form class."""
+ attrs = {'form': form, 'extra': extra,
+ 'can_order': can_order, 'can_delete': can_delete,
+ 'max_num': max_num}
+ return type(form.__name__ + 'FormSet', (formset,), attrs)
+
+def all_valid(formsets):
+ """Returns true if every formset in formsets is valid."""
+ valid = True
+ for formset in formsets:
+ if not formset.is_valid():
+ valid = False
+ return valid
diff --git a/webapp/django/forms/models.py b/webapp/django/forms/models.py
new file mode 100644
index 0000000000..677556d91b
--- /dev/null
+++ b/webapp/django/forms/models.py
@@ -0,0 +1,558 @@
+"""
+Helper functions for creating Form classes from Django models
+and database field objects.
+"""
+
+from django.utils.translation import ugettext_lazy as _
+from django.utils.encoding import smart_unicode
+from django.utils.datastructures import SortedDict
+
+from util import ValidationError, ErrorList
+from forms import BaseForm, get_declared_fields
+from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES
+from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
+from widgets import media_property
+from formsets import BaseFormSet, formset_factory, DELETION_FIELD_NAME
+
+__all__ = (
+ 'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
+ 'save_instance', 'form_for_fields', 'ModelChoiceField',
+ 'ModelMultipleChoiceField',
+)
+
+def save_instance(form, instance, fields=None, fail_message='saved',
+ commit=True):
+ """
+ Saves bound Form ``form``'s cleaned_data into model instance ``instance``.
+
+ If commit=True, then the changes to ``instance`` will be saved to the
+ database. Returns ``instance``.
+ """
+ from django.db import models
+ opts = instance._meta
+ if form.errors:
+ raise ValueError("The %s could not be %s because the data didn't"
+ " validate." % (opts.object_name, fail_message))
+ cleaned_data = form.cleaned_data
+ for f in opts.fields:
+ if not f.editable or isinstance(f, models.AutoField) \
+ or not f.name in cleaned_data:
+ continue
+ if fields and f.name not in fields:
+ continue
+ f.save_form_data(instance, cleaned_data[f.name])
+ # Wrap up the saving of m2m data as a function.
+ def save_m2m():
+ opts = instance._meta
+ cleaned_data = form.cleaned_data
+ for f in opts.many_to_many:
+ if fields and f.name not in fields:
+ continue
+ if f.name in cleaned_data:
+ f.save_form_data(instance, cleaned_data[f.name])
+ if commit:
+ # If we are committing, save the instance and the m2m data immediately.
+ instance.save()
+ save_m2m()
+ else:
+ # We're not committing. Add a method to the form to allow deferred
+ # saving of m2m data.
+ form.save_m2m = save_m2m
+ return instance
+
+def make_model_save(model, fields, fail_message):
+ """Returns the save() method for a Form."""
+ def save(self, commit=True):
+ return save_instance(self, model(), fields, fail_message, commit)
+ return save
+
+def make_instance_save(instance, fields, fail_message):
+ """Returns the save() method for a Form."""
+ def save(self, commit=True):
+ return save_instance(self, instance, fields, fail_message, commit)
+ return save
+
+def form_for_fields(field_list):
+ """
+ Returns a Form class for the given list of Django database field instances.
+ """
+ fields = SortedDict([(f.name, f.formfield())
+ for f in field_list if f.editable])
+ return type('FormForFields', (BaseForm,), {'base_fields': fields})
+
+
+# ModelForms #################################################################
+
+def model_to_dict(instance, fields=None, exclude=None):
+ """
+ Returns a dict containing the data in ``instance`` suitable for passing as
+ a Form's ``initial`` keyword argument.
+
+ ``fields`` is an optional list of field names. If provided, only the named
+ fields will be included in the returned dict.
+
+ ``exclude`` is an optional list of field names. If provided, the named
+ fields will be excluded from the returned dict, even if they are listed in
+ the ``fields`` argument.
+ """
+ # avoid a circular import
+ from django.db.models.fields.related import ManyToManyField, OneToOneField
+ opts = instance._meta
+ data = {}
+ for f in opts.fields + opts.many_to_many:
+ if not f.editable:
+ continue
+ if fields and not f.name in fields:
+ continue
+ if exclude and f.name in exclude:
+ continue
+ if isinstance(f, ManyToManyField):
+ # If the object doesn't have a primry key yet, just use an empty
+ # list for its m2m fields. Calling f.value_from_object will raise
+ # an exception.
+ if instance.pk is None:
+ data[f.name] = []
+ else:
+ # MultipleChoiceWidget needs a list of pks, not object instances.
+ data[f.name] = [obj.pk for obj in f.value_from_object(instance)]
+ elif isinstance(f, OneToOneField):
+ data[f.attname] = f.value_from_object(instance)
+ else:
+ data[f.name] = f.value_from_object(instance)
+ return data
+
+def fields_for_model(model, fields=None, exclude=None, formfield_callback=lambda f: f.formfield()):
+ """
+ Returns a ``SortedDict`` containing form fields for the given model.
+
+ ``fields`` is an optional list of field names. If provided, only the named
+ fields will be included in the returned fields.
+
+ ``exclude`` is an optional list of field names. If provided, the named
+ fields will be excluded from the returned fields, even if they are listed
+ in the ``fields`` argument.
+ """
+ # TODO: if fields is provided, it would be nice to return fields in that order
+ field_list = []
+ opts = model._meta
+ for f in opts.fields + opts.many_to_many:
+ if not f.editable:
+ continue
+ if fields and not f.name in fields:
+ continue
+ if exclude and f.name in exclude:
+ continue
+ formfield = formfield_callback(f)
+ if formfield:
+ field_list.append((f.name, formfield))
+ return SortedDict(field_list)
+
+class ModelFormOptions(object):
+ def __init__(self, options=None):
+ self.model = getattr(options, 'model', None)
+ self.fields = getattr(options, 'fields', None)
+ self.exclude = getattr(options, 'exclude', None)
+
+
+class ModelFormMetaclass(type):
+ def __new__(cls, name, bases, attrs):
+ formfield_callback = attrs.pop('formfield_callback',
+ lambda f: f.formfield())
+ try:
+ parents = [b for b in bases if issubclass(b, ModelForm)]
+ except NameError:
+ # We are defining ModelForm itself.
+ parents = None
+ new_class = super(ModelFormMetaclass, cls).__new__(cls, name, bases,
+ attrs)
+ if not parents:
+ return new_class
+
+ if 'media' not in attrs:
+ new_class.media = media_property(new_class)
+ declared_fields = get_declared_fields(bases, attrs, False)
+ opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
+ if opts.model:
+ # If a model is defined, extract form fields from it.
+ fields = fields_for_model(opts.model, opts.fields,
+ opts.exclude, formfield_callback)
+ # Override default model fields with any custom declared ones
+ # (plus, include all the other declared fields).
+ fields.update(declared_fields)
+ else:
+ fields = declared_fields
+ new_class.declared_fields = declared_fields
+ new_class.base_fields = fields
+ return new_class
+
+class BaseModelForm(BaseForm):
+ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
+ initial=None, error_class=ErrorList, label_suffix=':',
+ empty_permitted=False, instance=None):
+ opts = self._meta
+ if instance is None:
+ # if we didn't get an instance, instantiate a new one
+ self.instance = opts.model()
+ object_data = {}
+ else:
+ self.instance = instance
+ object_data = model_to_dict(instance, opts.fields, opts.exclude)
+ # if initial was provided, it should override the values from instance
+ if initial is not None:
+ object_data.update(initial)
+ super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
+ error_class, label_suffix, empty_permitted)
+
+ def save(self, commit=True):
+ """
+ Saves this ``form``'s cleaned_data into model instance
+ ``self.instance``.
+
+ If commit=True, then the changes to ``instance`` will be saved to the
+ database. Returns ``instance``.
+ """
+ if self.instance.pk is None:
+ fail_message = 'created'
+ else:
+ fail_message = 'changed'
+ return save_instance(self, self.instance, self._meta.fields, fail_message, commit)
+
+class ModelForm(BaseModelForm):
+ __metaclass__ = ModelFormMetaclass
+
+def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
+ formfield_callback=lambda f: f.formfield()):
+ # HACK: we should be able to construct a ModelForm without creating
+ # and passing in a temporary inner class
+ class Meta:
+ pass
+ setattr(Meta, 'model', model)
+ setattr(Meta, 'fields', fields)
+ setattr(Meta, 'exclude', exclude)
+ class_name = model.__name__ + 'Form'
+ return ModelFormMetaclass(class_name, (form,), {'Meta': Meta,
+ 'formfield_callback': formfield_callback})
+
+
+# ModelFormSets ##############################################################
+
+class BaseModelFormSet(BaseFormSet):
+ """
+ A ``FormSet`` for editing a queryset and/or adding new objects to it.
+ """
+ model = None
+
+ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
+ queryset=None, **kwargs):
+ self.queryset = queryset
+ defaults = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
+ if self.max_num > 0:
+ qs = self.get_queryset()[:self.max_num]
+ else:
+ qs = self.get_queryset()
+ defaults['initial'] = [model_to_dict(obj) for obj in qs]
+ defaults.update(kwargs)
+ super(BaseModelFormSet, self).__init__(**defaults)
+
+ def get_queryset(self):
+ if self.queryset is not None:
+ return self.queryset
+ return self.model._default_manager.get_query_set()
+
+ def save_new(self, form, commit=True):
+ """Saves and returns a new model instance for the given form."""
+ return save_instance(form, self.model(), commit=commit)
+
+ def save_existing(self, form, instance, commit=True):
+ """Saves and returns an existing model instance for the given form."""
+ return save_instance(form, instance, commit=commit)
+
+ def save(self, commit=True):
+ """Saves model instances for every form, adding and changing instances
+ as necessary, and returns the list of instances.
+ """
+ if not commit:
+ self.saved_forms = []
+ def save_m2m():
+ for form in self.saved_forms:
+ form.save_m2m()
+ self.save_m2m = save_m2m
+ return self.save_existing_objects(commit) + self.save_new_objects(commit)
+
+ def save_existing_objects(self, commit=True):
+ self.changed_objects = []
+ self.deleted_objects = []
+ if not self.get_queryset():
+ return []
+
+ # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk
+ existing_objects = {}
+ for obj in self.get_queryset():
+ existing_objects[obj.pk] = obj
+ saved_instances = []
+ for form in self.initial_forms:
+ obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]]
+ if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
+ self.deleted_objects.append(obj)
+ obj.delete()
+ else:
+ if form.changed_data:
+ self.changed_objects.append((obj, form.changed_data))
+ saved_instances.append(self.save_existing(form, obj, commit=commit))
+ if not commit:
+ self.saved_forms.append(form)
+ return saved_instances
+
+ def save_new_objects(self, commit=True):
+ self.new_objects = []
+ for form in self.extra_forms:
+ if not form.has_changed():
+ continue
+ # If someone has marked an add form for deletion, don't save the
+ # object.
+ if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
+ continue
+ self.new_objects.append(self.save_new(form, commit=commit))
+ if not commit:
+ self.saved_forms.append(form)
+ return self.new_objects
+
+ def add_fields(self, form, index):
+ """Add a hidden field for the object's primary key."""
+ if self.model._meta.pk.auto_created:
+ self._pk_field_name = self.model._meta.pk.attname
+ form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
+ super(BaseModelFormSet, self).add_fields(form, index)
+
+def modelformset_factory(model, form=ModelForm, formfield_callback=lambda f: f.formfield(),
+ formset=BaseModelFormSet,
+ extra=1, can_delete=False, can_order=False,
+ max_num=0, fields=None, exclude=None):
+ """
+ Returns a FormSet class for the given Django model class.
+ """
+ form = modelform_factory(model, form=form, fields=fields, exclude=exclude,
+ formfield_callback=formfield_callback)
+ FormSet = formset_factory(form, formset, extra=extra, max_num=max_num,
+ can_order=can_order, can_delete=can_delete)
+ FormSet.model = model
+ return FormSet
+
+
+# InlineFormSets #############################################################
+
+class BaseInlineFormSet(BaseModelFormSet):
+ """A formset for child objects related to a parent."""
+ def __init__(self, data=None, files=None, instance=None,
+ save_as_new=False, prefix=None):
+ from django.db.models.fields.related import RelatedObject
+ self.instance = instance
+ self.save_as_new = save_as_new
+ # is there a better way to get the object descriptor?
+ self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
+ super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix or self.rel_name)
+
+ def _construct_forms(self):
+ if self.save_as_new:
+ self._total_form_count = self._initial_form_count
+ self._initial_form_count = 0
+ super(BaseInlineFormSet, self)._construct_forms()
+
+ def get_queryset(self):
+ """
+ Returns this FormSet's queryset, but restricted to children of
+ self.instance
+ """
+ kwargs = {self.fk.name: self.instance}
+ return self.model._default_manager.filter(**kwargs)
+
+ def save_new(self, form, commit=True):
+ kwargs = {self.fk.get_attname(): self.instance.pk}
+ new_obj = self.model(**kwargs)
+ return save_instance(form, new_obj, commit=commit)
+
+def _get_foreign_key(parent_model, model, fk_name=None):
+ """
+ Finds and returns the ForeignKey from model to parent if there is one.
+ If fk_name is provided, assume it is the name of the ForeignKey field.
+ """
+ # avoid circular import
+ from django.db.models import ForeignKey
+ opts = model._meta
+ if fk_name:
+ fks_to_parent = [f for f in opts.fields if f.name == fk_name]
+ if len(fks_to_parent) == 1:
+ fk = fks_to_parent[0]
+ if not isinstance(fk, ForeignKey) or \
+ (fk.rel.to != parent_model and
+ fk.rel.to not in parent_model._meta.parents.keys()):
+ raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
+ elif len(fks_to_parent) == 0:
+ raise Exception("%s has no field named '%s'" % (model, fk_name))
+ else:
+ # Try to discover what the ForeignKey from model to parent_model is
+ fks_to_parent = [
+ f for f in opts.fields
+ if isinstance(f, ForeignKey)
+ and (f.rel.to == parent_model
+ or f.rel.to in parent_model._meta.parents.keys())
+ ]
+ if len(fks_to_parent) == 1:
+ fk = fks_to_parent[0]
+ elif len(fks_to_parent) == 0:
+ raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
+ else:
+ raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
+ return fk
+
+
+def inlineformset_factory(parent_model, model, form=ModelForm,
+ formset=BaseInlineFormSet, fk_name=None,
+ fields=None, exclude=None,
+ extra=3, can_order=False, can_delete=True, max_num=0,
+ formfield_callback=lambda f: f.formfield()):
+ """
+ Returns an ``InlineFormSet`` for the given kwargs.
+
+ You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
+ to ``parent_model``.
+ """
+ fk = _get_foreign_key(parent_model, model, fk_name=fk_name)
+ # let the formset handle object deletion by default
+
+ if exclude is not None:
+ exclude.append(fk.name)
+ else:
+ exclude = [fk.name]
+ FormSet = modelformset_factory(model, form=form,
+ formfield_callback=formfield_callback,
+ formset=formset,
+ extra=extra, can_delete=can_delete, can_order=can_order,
+ fields=fields, exclude=exclude, max_num=max_num)
+ FormSet.fk = fk
+ return FormSet
+
+
+# Fields #####################################################################
+
+class ModelChoiceIterator(object):
+ def __init__(self, field):
+ self.field = field
+ self.queryset = field.queryset
+
+ def __iter__(self):
+ if self.field.empty_label is not None:
+ yield (u"", self.field.empty_label)
+ if self.field.cache_choices:
+ if self.field.choice_cache is None:
+ self.field.choice_cache = [
+ (obj.pk, self.field.label_from_instance(obj))
+ for obj in self.queryset.all()
+ ]
+ for choice in self.field.choice_cache:
+ yield choice
+ else:
+ for obj in self.queryset.all():
+ yield (obj.pk, self.field.label_from_instance(obj))
+
+class ModelChoiceField(ChoiceField):
+ """A ChoiceField whose choices are a model QuerySet."""
+ # This class is a subclass of ChoiceField for purity, but it doesn't
+ # actually use any of ChoiceField's implementation.
+ default_error_messages = {
+ 'invalid_choice': _(u'Select a valid choice. That choice is not one of'
+ u' the available choices.'),
+ }
+
+ def __init__(self, queryset, empty_label=u"---------", cache_choices=False,
+ required=True, widget=None, label=None, initial=None,
+ help_text=None, *args, **kwargs):
+ self.empty_label = empty_label
+ self.cache_choices = cache_choices
+
+ # Call Field instead of ChoiceField __init__() because we don't need
+ # ChoiceField.__init__().
+ Field.__init__(self, required, widget, label, initial, help_text,
+ *args, **kwargs)
+ self.queryset = queryset
+ self.choice_cache = None
+
+ def _get_queryset(self):
+ return self._queryset
+
+ def _set_queryset(self, queryset):
+ self._queryset = queryset
+ self.widget.choices = self.choices
+
+ queryset = property(_get_queryset, _set_queryset)
+
+ # this method will be used to create object labels by the QuerySetIterator.
+ # Override it to customize the label.
+ def label_from_instance(self, obj):
+ """
+ This method is used to convert objects into strings; it's used to
+ generate the labels for the choices presented by this object. Subclasses
+ can override this method to customize the display of the choices.
+ """
+ return smart_unicode(obj)
+
+ def _get_choices(self):
+ # If self._choices is set, then somebody must have manually set
+ # the property self.choices. In this case, just return self._choices.
+ if hasattr(self, '_choices'):
+ return self._choices
+
+ # Otherwise, execute the QuerySet in self.queryset to determine the
+ # choices dynamically. Return a fresh QuerySetIterator that has not been
+ # consumed. Note that we're instantiating a new QuerySetIterator *each*
+ # time _get_choices() is called (and, thus, each time self.choices is
+ # accessed) so that we can ensure the QuerySet has not been consumed. This
+ # construct might look complicated but it allows for lazy evaluation of
+ # the queryset.
+ return ModelChoiceIterator(self)
+
+ choices = property(_get_choices, ChoiceField._set_choices)
+
+ def clean(self, value):
+ Field.clean(self, value)
+ if value in EMPTY_VALUES:
+ return None
+ try:
+ value = self.queryset.get(pk=value)
+ except self.queryset.model.DoesNotExist:
+ raise ValidationError(self.error_messages['invalid_choice'])
+ return value
+
+class ModelMultipleChoiceField(ModelChoiceField):
+ """A MultipleChoiceField whose choices are a model QuerySet."""
+ widget = SelectMultiple
+ hidden_widget = MultipleHiddenInput
+ default_error_messages = {
+ 'list': _(u'Enter a list of values.'),
+ 'invalid_choice': _(u'Select a valid choice. %s is not one of the'
+ u' available choices.'),
+ }
+
+ def __init__(self, queryset, cache_choices=False, required=True,
+ widget=None, label=None, initial=None,
+ help_text=None, *args, **kwargs):
+ super(ModelMultipleChoiceField, self).__init__(queryset, None,
+ cache_choices, required, widget, label, initial, help_text,
+ *args, **kwargs)
+
+ def clean(self, value):
+ if self.required and not value:
+ raise ValidationError(self.error_messages['required'])
+ elif not self.required and not value:
+ return []
+ if not isinstance(value, (list, tuple)):
+ raise ValidationError(self.error_messages['list'])
+ final_values = []
+ for val in value:
+ try:
+ obj = self.queryset.get(pk=val)
+ except self.queryset.model.DoesNotExist:
+ raise ValidationError(self.error_messages['invalid_choice'] % val)
+ else:
+ final_values.append(obj)
+ return final_values
diff --git a/webapp/django/forms/util.py b/webapp/django/forms/util.py
new file mode 100644
index 0000000000..3d80ad219f
--- /dev/null
+++ b/webapp/django/forms/util.py
@@ -0,0 +1,68 @@
+from django.utils.html import escape
+from django.utils.encoding import smart_unicode, StrAndUnicode, force_unicode
+from django.utils.safestring import mark_safe
+
+def flatatt(attrs):
+ """
+ Convert a dictionary of attributes to a single string.
+ The returned string will contain a leading space followed by key="value",
+ XML-style pairs. It is assumed that the keys do not need to be XML-escaped.
+ If the passed dictionary is empty, then return an empty string.
+ """
+ return u''.join([u' %s="%s"' % (k, escape(v)) for k, v in attrs.items()])
+
+class ErrorDict(dict, StrAndUnicode):
+ """
+ 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 __unicode__(self):
+ return self.as_ul()
+
+ def as_ul(self):
+ if not self: return u''
+ return mark_safe(u'<ul class="errorlist">%s</ul>'
+ % ''.join([u'<li>%s%s</li>' % (k, force_unicode(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' % force_unicode(i) for i in v])) for k, v in self.items()])
+
+class ErrorList(list, StrAndUnicode):
+ """
+ A collection of errors that knows how to display itself in various formats.
+ """
+ def __unicode__(self):
+ return self.as_ul()
+
+ def as_ul(self):
+ if not self: return u''
+ return mark_safe(u'<ul class="errorlist">%s</ul>'
+ % ''.join([u'<li>%s</li>' % force_unicode(e) for e in self]))
+
+ def as_text(self):
+ if not self: return u''
+ return u'\n'.join([u'* %s' % force_unicode(e) for e in self])
+
+ def __repr__(self):
+ return repr([force_unicode(e) for e in self])
+
+class ValidationError(Exception):
+ def __init__(self, message):
+ """
+ ValidationError can be passed any object that can be printed (usually
+ a string) or a list of objects.
+ """
+ if isinstance(message, list):
+ self.messages = ErrorList([smart_unicode(msg) for msg in message])
+ else:
+ 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)
diff --git a/webapp/django/forms/widgets.py b/webapp/django/forms/widgets.py
new file mode 100644
index 0000000000..2e01634571
--- /dev/null
+++ b/webapp/django/forms/widgets.py
@@ -0,0 +1,663 @@
+"""
+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 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 = ''
+ 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 = 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 hasattr(value, 'replace'):
+ 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:
+ 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]
+