diff options
| author | Adrian Holovaty <adrian@holovaty.com> | 2006-12-27 02:49:28 +0000 |
|---|---|---|
| committer | Adrian Holovaty <adrian@holovaty.com> | 2006-12-27 02:49:28 +0000 |
| commit | 0cf7bc439129c66df8d64601e885f83b256b4f25 (patch) | |
| tree | a7fd3cd4df5e862578544778d1b720ded59b7274 /django/newforms | |
| parent | c4673e4fb68237f96652d7372bec0283c5446c27 (diff) | |
per-object-permissions: Merged to trunk [4241]
git-svn-id: http://code.djangoproject.com/svn/django/branches/per-object-permissions@4242 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django/newforms')
| -rw-r--r-- | django/newforms/__init__.py | 15 | ||||
| -rw-r--r-- | django/newforms/extras/__init__.py | 1 | ||||
| -rw-r--r-- | django/newforms/extras/widgets.py | 59 | ||||
| -rw-r--r-- | django/newforms/fields.py | 158 | ||||
| -rw-r--r-- | django/newforms/forms.py | 204 | ||||
| -rw-r--r-- | django/newforms/models.py | 38 | ||||
| -rw-r--r-- | django/newforms/util.py | 15 | ||||
| -rw-r--r-- | django/newforms/widgets.py | 116 |
8 files changed, 471 insertions, 135 deletions
diff --git a/django/newforms/__init__.py b/django/newforms/__init__.py index 2a472d7b39..0d9c68f9e0 100644 --- a/django/newforms/__init__.py +++ b/django/newforms/__init__.py @@ -13,16 +13,5 @@ TODO: from util import ValidationError from widgets import * from fields import * -from forms import Form - -########################## -# DATABASE API SHORTCUTS # -########################## - -def form_for_model(model): - "Returns a Form instance for the given Django model class." - raise NotImplementedError - -def form_for_fields(field_list): - "Returns a Form instance for the given list of Django database field instances." - raise NotImplementedError +from forms import * +from models import * diff --git a/django/newforms/extras/__init__.py b/django/newforms/extras/__init__.py new file mode 100644 index 0000000000..a7f6a9b3f6 --- /dev/null +++ b/django/newforms/extras/__init__.py @@ -0,0 +1 @@ +from widgets import * diff --git a/django/newforms/extras/widgets.py b/django/newforms/extras/widgets.py new file mode 100644 index 0000000000..1011934fb8 --- /dev/null +++ b/django/newforms/extras/widgets.py @@ -0,0 +1,59 @@ +""" +Extra HTML Widget classes +""" + +from django.newforms.widgets import Widget, Select +from django.utils.dates import MONTHS +import datetime + +__all__ = ('SelectDateWidget',) + +class SelectDateWidget(Widget): + """ + A Widget that splits date input into three <select> boxes. + + This also serves as an example of a Widget that has more than one HTML + element and hence implements value_from_datadict. + """ + month_field = '%s_month' + day_field = '%s_day' + year_field = '%s_year' + + def __init__(self, attrs=None, years=None): + # years is an optional list/tuple of years to use in the "year" select box. + self.attrs = attrs or {} + if years: + self.years = years + else: + this_year = datetime.date.today().year + self.years = range(this_year, this_year+10) + + def render(self, name, value, attrs=None): + try: + value = datetime.date(*map(int, value.split('-'))) + year_val, month_val, day_val = value.year, value.month, value.day + except (AttributeError, TypeError, ValueError): + year_val = month_val = day_val = None + + output = [] + + month_choices = MONTHS.items() + month_choices.sort() + select_html = Select(choices=month_choices).render(self.month_field % name, month_val) + output.append(select_html) + + day_choices = [(i, i) for i in range(1, 32)] + select_html = Select(choices=day_choices).render(self.day_field % name, day_val) + output.append(select_html) + + year_choices = [(i, i) for i in self.years] + select_html = Select(choices=year_choices).render(self.year_field % name, year_val) + output.append(select_html) + + return u'\n'.join(output) + + def value_from_datadict(self, data, name): + y, m, d = data.get(self.year_field % name), data.get(self.month_field % name), data.get(self.day_field % name) + if y and m and d: + return '%s-%s-%s' % (y, m, d) + return None diff --git a/django/newforms/fields.py b/django/newforms/fields.py index 40fc18bd3e..5c6d46ddac 100644 --- a/django/newforms/fields.py +++ b/django/newforms/fields.py @@ -2,8 +2,9 @@ Field classes """ -from util import ValidationError, DEFAULT_ENCODING, smart_unicode -from widgets import TextInput, CheckboxInput, Select, SelectMultiple +from django.utils.translation import gettext +from util import ValidationError, smart_unicode +from widgets import TextInput, PasswordInput, CheckboxInput, Select, SelectMultiple import datetime import re import time @@ -11,6 +12,7 @@ import time __all__ = ( 'Field', 'CharField', 'IntegerField', 'DEFAULT_DATE_INPUT_FORMATS', 'DateField', + 'DEFAULT_TIME_INPUT_FORMATS', 'TimeField', 'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField', 'RegexField', 'EmailField', 'URLField', 'BooleanField', 'ChoiceField', 'MultipleChoiceField', @@ -31,11 +33,19 @@ class Field(object): # Tracks each time a Field instance is created. Used to retain order. creation_counter = 0 - def __init__(self, required=True, widget=None): - self.required = required + def __init__(self, required=True, widget=None, label=None): + if label is not None: + label = smart_unicode(label) + self.required, self.label = required, label 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. @@ -50,36 +60,62 @@ class Field(object): Raises ValidationError for any errors. """ if self.required and value in EMPTY_VALUES: - raise ValidationError(u'This field is required.') + raise ValidationError(gettext(u'This field is required.')) return value + def widget_attrs(self, widget): + """ + Given a Widget instance (*not* a Widget class), returns a dictionary of + any HTML attributes that should be added to the Widget, based on this + Field. + """ + return {} + class CharField(Field): - def __init__(self, max_length=None, min_length=None, required=True, widget=None): - Field.__init__(self, required, widget) + def __init__(self, max_length=None, min_length=None, required=True, widget=None, label=None): self.max_length, self.min_length = max_length, min_length + Field.__init__(self, required, widget, label) def clean(self, value): "Validates max_length and min_length. Returns a Unicode object." Field.clean(self, value) - if value in EMPTY_VALUES: value = u'' + if value in EMPTY_VALUES: + value = u'' + if not self.required: + return value value = smart_unicode(value) if self.max_length is not None and len(value) > self.max_length: - raise ValidationError(u'Ensure this value has at most %d characters.' % self.max_length) + raise ValidationError(gettext(u'Ensure this value has at most %d characters.') % self.max_length) if self.min_length is not None and len(value) < self.min_length: - raise ValidationError(u'Ensure this value has at least %d characters.' % self.min_length) + raise ValidationError(gettext(u'Ensure this value has at least %d characters.') % self.min_length) return value + def widget_attrs(self, widget): + if self.max_length is not None and isinstance(widget, (TextInput, PasswordInput)): + return {'maxlength': str(self.max_length)} + class IntegerField(Field): + def __init__(self, max_value=None, min_value=None, required=True, widget=None, label=None): + self.max_value, self.min_value = max_value, min_value + Field.__init__(self, required, widget, label) + def clean(self, value): """ Validates that int() can be called on the input. Returns the result of int(). """ super(IntegerField, self).clean(value) + if not self.required and value in EMPTY_VALUES: + return u'' try: - return int(value) + value = int(value) except (ValueError, TypeError): - raise ValidationError(u'Enter a whole number.') + raise ValidationError(gettext(u'Enter a whole number.')) + if self.max_value is not None and value > self.max_value: + raise ValidationError(gettext(u'Ensure this value is less than or equal to %s.') % self.max_value) + if self.min_value is not None and value < self.min_value: + raise ValidationError(gettext(u'Ensure this value is greater than or equal to %s.') % self.min_value) + return value DEFAULT_DATE_INPUT_FORMATS = ( '%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06' @@ -90,8 +126,8 @@ DEFAULT_DATE_INPUT_FORMATS = ( ) class DateField(Field): - def __init__(self, input_formats=None, required=True, widget=None): - Field.__init__(self, required, widget) + def __init__(self, input_formats=None, required=True, widget=None, label=None): + Field.__init__(self, required, widget, label) self.input_formats = input_formats or DEFAULT_DATE_INPUT_FORMATS def clean(self, value): @@ -111,7 +147,34 @@ class DateField(Field): return datetime.date(*time.strptime(value, format)[:3]) except ValueError: continue - raise ValidationError(u'Enter a valid date.') + raise ValidationError(gettext(u'Enter a valid date.')) + +DEFAULT_TIME_INPUT_FORMATS = ( + '%H:%M:%S', # '14:30:59' + '%H:%M', # '14:30' +) + +class TimeField(Field): + def __init__(self, input_formats=None, required=True, widget=None, label=None): + Field.__init__(self, required, widget, label) + 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. + """ + Field.clean(self, value) + if value in EMPTY_VALUES: + return None + if isinstance(value, datetime.time): + return value + for format in self.input_formats: + try: + return datetime.time(*time.strptime(value, format)[3:6]) + except ValueError: + continue + raise ValidationError(gettext(u'Enter a valid time.')) DEFAULT_DATETIME_INPUT_FORMATS = ( '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59' @@ -126,8 +189,8 @@ DEFAULT_DATETIME_INPUT_FORMATS = ( ) class DateTimeField(Field): - def __init__(self, input_formats=None, required=True, widget=None): - Field.__init__(self, required, widget) + def __init__(self, input_formats=None, required=True, widget=None, label=None): + Field.__init__(self, required, widget, label) self.input_formats = input_formats or DEFAULT_DATETIME_INPUT_FORMATS def clean(self, value): @@ -147,20 +210,22 @@ class DateTimeField(Field): return datetime.datetime(*time.strptime(value, format)[:6]) except ValueError: continue - raise ValidationError(u'Enter a valid date/time.') + raise ValidationError(gettext(u'Enter a valid date/time.')) class RegexField(Field): - def __init__(self, regex, error_message=None, required=True, widget=None): + def __init__(self, regex, max_length=None, min_length=None, error_message=None, + required=True, widget=None, label=None): """ regex can be either a string or a compiled regular expression object. error_message is an optional error message to use, if 'Enter a valid value' is too generic for you. """ - Field.__init__(self, required, widget) + Field.__init__(self, required, widget, label) if isinstance(regex, basestring): regex = re.compile(regex) self.regex = regex - self.error_message = error_message or u'Enter a valid value.' + self.max_length, self.min_length = max_length, min_length + self.error_message = error_message or gettext(u'Enter a valid value.') def clean(self, value): """ @@ -170,6 +235,12 @@ class RegexField(Field): Field.clean(self, value) if value in EMPTY_VALUES: value = u'' value = smart_unicode(value) + if not self.required and value == u'': + return value + if self.max_length is not None and len(value) > self.max_length: + raise ValidationError(gettext(u'Ensure this value has at most %d characters.') % self.max_length) + if self.min_length is not None and len(value) < self.min_length: + raise ValidationError(gettext(u'Ensure this value has at least %d characters.') % self.min_length) if not self.regex.search(value): raise ValidationError(self.error_message) return value @@ -180,8 +251,8 @@ email_re = re.compile( r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain class EmailField(RegexField): - def __init__(self, required=True, widget=None): - RegexField.__init__(self, email_re, u'Enter a valid e-mail address.', required, widget) + def __init__(self, max_length=None, min_length=None, required=True, widget=None, label=None): + RegexField.__init__(self, email_re, max_length, min_length, gettext(u'Enter a valid e-mail address.'), required, widget, label) url_re = re.compile( r'^https?://' # http:// or https:// @@ -197,9 +268,9 @@ except ImportError: URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)' class URLField(RegexField): - def __init__(self, required=True, verify_exists=False, widget=None, + def __init__(self, max_length=None, min_length=None, required=True, verify_exists=False, widget=None, label=None, validator_user_agent=URL_VALIDATOR_USER_AGENT): - RegexField.__init__(self, url_re, u'Enter a valid URL.', required, widget) + RegexField.__init__(self, url_re, max_length, min_length, gettext(u'Enter a valid URL.'), required, widget, label) self.verify_exists = verify_exists self.user_agent = validator_user_agent @@ -219,9 +290,9 @@ class URLField(RegexField): req = urllib2.Request(value, None, headers) u = urllib2.urlopen(req) except ValueError: - raise ValidationError(u'Enter a valid URL.') + raise ValidationError(gettext(u'Enter a valid URL.')) except: # urllib2.URLError, httplib.InvalidURL, etc. - raise ValidationError(u'This URL appears to be a broken link.') + raise ValidationError(gettext(u'This URL appears to be a broken link.')) return value class BooleanField(Field): @@ -233,10 +304,10 @@ class BooleanField(Field): return bool(value) class ChoiceField(Field): - def __init__(self, choices=(), required=True, widget=Select): + def __init__(self, choices=(), required=True, widget=Select, label=None): if isinstance(widget, type): widget = widget(choices=choices) - Field.__init__(self, required, widget) + Field.__init__(self, required, widget, label) self.choices = choices def clean(self, value): @@ -246,37 +317,46 @@ class ChoiceField(Field): value = Field.clean(self, value) if value in EMPTY_VALUES: value = u'' value = smart_unicode(value) + if not self.required and value == u'': + return value valid_values = set([str(k) for k, v in self.choices]) if value not in valid_values: - raise ValidationError(u'Select a valid choice. %s is not one of the available choices.' % value) + raise ValidationError(gettext(u'Select a valid choice. %s is not one of the available choices.') % value) return value class MultipleChoiceField(ChoiceField): - def __init__(self, choices=(), required=True, widget=SelectMultiple): - ChoiceField.__init__(self, choices, required, widget) + def __init__(self, choices=(), required=True, widget=SelectMultiple, label=None): + ChoiceField.__init__(self, choices, required, widget, label) def clean(self, value): """ Validates that the input is a list or tuple. """ - if not isinstance(value, (list, tuple)): - raise ValidationError(u'Enter a list of values.') if self.required and not value: - raise ValidationError(u'This field is required.') + raise ValidationError(gettext(u'This field is required.')) + elif not self.required and not value: + return [] + if not isinstance(value, (list, tuple)): + raise ValidationError(gettext(u'Enter a list of values.')) new_value = [] for val in value: val = smart_unicode(val) new_value.append(val) # Validate that each value in the value list is in self.choices. - valid_values = set([k for k, v in self.choices]) + valid_values = set([smart_unicode(k) for k, v in self.choices]) for val in new_value: if val not in valid_values: - raise ValidationError(u'Select a valid choice. %s is not one of the available choices.' % val) + raise ValidationError(gettext(u'Select a valid choice. %s is not one of the available choices.') % val) return new_value class ComboField(Field): - def __init__(self, fields=(), required=True, widget=None): - Field.__init__(self, required, widget) + def __init__(self, fields=(), required=True, widget=None, label=None): + Field.__init__(self, required, widget, label) + # 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): diff --git a/django/newforms/forms.py b/django/newforms/forms.py index b8264fb691..201cce3868 100644 --- a/django/newforms/forms.py +++ b/django/newforms/forms.py @@ -2,10 +2,13 @@ Form classes """ -from django.utils.datastructures import SortedDict +from django.utils.datastructures import SortedDict, MultiValueDict +from django.utils.html import escape from fields import Field -from widgets import TextInput, Textarea -from util import ErrorDict, ErrorList, ValidationError +from widgets import TextInput, Textarea, HiddenInput +from util import StrAndUnicode, ErrorDict, ErrorList, ValidationError + +__all__ = ('BaseForm', 'Form') NON_FIELD_ERRORS = '__all__' @@ -31,17 +34,20 @@ class DeclarativeFieldsMetaclass(type): attrs['fields'] = SortedDictFromList(fields) return type.__new__(cls, name, bases, attrs) -class Form(object): - "A collection of Fields, plus their associated data." - __metaclass__ = DeclarativeFieldsMetaclass - - def __init__(self, data=None, auto_id=False): # TODO: prefix stuff +class BaseForm(StrAndUnicode): + # This is the main implementation of all the Form logic. Note that this + # class is different than Form. See the comments by the Form class for more + # information. Any improvements to the form API should be made to *this* + # class, not to the Form class. + def __init__(self, data=None, auto_id='id_%s', prefix=None): + self.ignore_errors = data is None self.data = data or {} self.auto_id = auto_id + self.prefix = prefix self.clean_data = None # Stores the data after clean() has been called. self.__errors = None # Stores the errors after clean() has been called. - def __str__(self): + def __unicode__(self): return self.as_table() def __iter__(self): @@ -56,60 +62,76 @@ class Form(object): raise KeyError('Key %r not found in Form' % name) return BoundField(self, field, name) - def clean(self): - if self.__errors is None: - self.full_clean() - return self.clean_data - - def errors(self): + def _errors(self): "Returns an ErrorDict for self.data" if self.__errors is None: self.full_clean() return self.__errors + errors = property(_errors) def is_valid(self): """ - Returns True if the form has no errors. Otherwise, False. This exists - solely for convenience, so client code can use positive logic rather - than confusing negative logic ("if not form.errors()"). + Returns True if the form has no errors. Otherwise, False. If errors are + being ignored, returns False. + """ + return not self.ignore_errors 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 not bool(self.errors()) + return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name + + def _html_output(self, normal_row, error_row, row_ender, errors_on_separate_row): + "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()." + top_errors = self.non_field_errors() # Errors that should be displayed above all fields. + output, hidden_fields = [], [] + for name, field in self.fields.items(): + bf = BoundField(self, field, name) + bf_errors = bf.errors # Cache in local variable. + if bf.is_hidden: + if bf_errors: + top_errors.extend(['(Hidden field %s) %s' % (name, e) for e in bf_errors]) + hidden_fields.append(unicode(bf)) + else: + if errors_on_separate_row and bf_errors: + output.append(error_row % bf_errors) + label = bf.label and bf.label_tag(escape(bf.label + ':')) or '' + output.append(normal_row % {'errors': bf_errors, 'label': label, 'field': bf}) + if top_errors: + output.insert(0, error_row % top_errors) + if hidden_fields: # Insert any hidden fields in the last row. + str_hidden = u''.join(hidden_fields) + if output: + last_row = output[-1] + # Chop off the trailing row_ender (e.g. '</td></tr>') and insert the hidden fields. + output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender + else: # If there aren't any rows in the output, just append the hidden fields. + output.append(str_hidden) + return u'\n'.join(output) def as_table(self): "Returns this form rendered as HTML <tr>s -- excluding the <table></table>." - return u'\n'.join(['<tr><td>%s:</td><td>%s</td></tr>' % (pretty_name(name), BoundField(self, field, name)) for name, field in self.fields.items()]) + return self._html_output(u'<tr><th>%(label)s</th><td>%(errors)s%(field)s</td></tr>', u'<tr><td colspan="2">%s</td></tr>', '</td></tr>', False) def as_ul(self): "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>." - return u'\n'.join(['<li>%s: %s</li>' % (pretty_name(name), BoundField(self, field, name)) for name, field in self.fields.items()]) + return self._html_output(u'<li>%(errors)s%(label)s %(field)s</li>', u'<li>%s</li>', '</li>', False) - def as_table_with_errors(self): - "Returns this form rendered as HTML <tr>s, with errors." - output = [] - if self.errors().get(NON_FIELD_ERRORS): - # Errors not corresponding to a particular field are displayed at the top. - output.append('<tr><td colspan="2"><ul>%s</ul></td></tr>' % '\n'.join(['<li>%s</li>' % e for e in self.errors()[NON_FIELD_ERRORS]])) - for name, field in self.fields.items(): - bf = BoundField(self, field, name) - if bf.errors: - output.append('<tr><td colspan="2"><ul>%s</ul></td></tr>' % '\n'.join(['<li>%s</li>' % e for e in bf.errors])) - output.append('<tr><td>%s:</td><td>%s</td></tr>' % (pretty_name(name), bf)) - return u'\n'.join(output) + def as_p(self): + "Returns this form rendered as HTML <p>s." + return self._html_output(u'<p>%(label)s %(field)s</p>', u'<p>%s</p>', '</p>', True) - def as_ul_with_errors(self): - "Returns this form rendered as HTML <li>s, with errors." - output = [] - if self.errors().get(NON_FIELD_ERRORS): - # Errors not corresponding to a particular field are displayed at the top. - output.append('<li><ul>%s</ul></li>' % '\n'.join(['<li>%s</li>' % e for e in self.errors()[NON_FIELD_ERRORS]])) - for name, field in self.fields.items(): - bf = BoundField(self, field, name) - line = '<li>' - if bf.errors: - line += '<ul>%s</ul>' % '\n'.join(['<li>%s</li>' % e for e in bf.errors]) - line += '%s: %s</li>' % (pretty_name(name), bf) - output.append(line) - return u'\n'.join(output) + def non_field_errors(self): + """ + Returns an ErrorList of errors that aren't associated with a particular + field -- i.e., from Form.clean(). Returns an empty ErrorList if there + are none. + """ + return self.errors.get(NON_FIELD_ERRORS, ErrorList()) def full_clean(self): """ @@ -117,8 +139,14 @@ class Form(object): """ self.clean_data = {} errors = ErrorDict() + if self.ignore_errors: # Stop further processing. + self.__errors = errors + return for name, field in self.fields.items(): - value = self.data.get(name, None) + # value_from_datadict() gets the data from the dictionary. + # Each widget type knows how to retrieve its own data, because some + # widgets split data over several HTML fields. + value = field.widget.value_from_datadict(self.data, self.add_prefix(name)) try: value = field.clean(value) self.clean_data[name] = value @@ -138,40 +166,59 @@ class Form(object): def clean(self): """ Hook for doing any extra form-wide cleaning after Field.clean() been - called on every field. + called on every field. Any ValidationError raised by this method will + not be associated with a particular field; it will have a special-case + association with the field named '__all__'. """ return self.clean_data -class BoundField(object): +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.form = form + self.field = field + self.name = name + self.html_name = form.add_prefix(name) + if self.field.label is None: + self.label = pretty_name(name) + else: + self.label = self.field.label - def __str__(self): + def __unicode__(self): "Renders this field as an HTML widget." # Use the 'widget' attribute on the field to determine which type # of HTML widget to use. - return self.as_widget(self._field.widget) + value = self.as_widget(self.field.widget) + if not isinstance(value, basestring): + # Some Widget render() methods -- notably RadioSelect -- return a + # "special" object rather than a string. Call the __str__() on that + # object to get its rendered value. + value = value.__str__() + return value def _errors(self): """ Returns an ErrorList for this field. Returns an empty ErrorList if there are none. """ - try: - return self._form.errors()[self._name] - except KeyError: - return ErrorList() + return self.form.errors.get(self.name, ErrorList()) errors = property(_errors) def as_widget(self, widget, attrs=None): attrs = attrs or {} auto_id = self.auto_id - if not attrs.has_key('id') and not widget.attrs.has_key('id') and auto_id: + if auto_id and not attrs.has_key('id') and not widget.attrs.has_key('id'): attrs['id'] = auto_id - return widget.render(self._name, self._form.data.get(self._name, None), attrs=attrs) + return widget.render(self.html_name, self.data, attrs=attrs) def as_text(self, attrs=None): """ @@ -183,15 +230,44 @@ class BoundField(object): "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(HiddenInput(), attrs) + + def _data(self): + "Returns the data for this BoundField, or None if it wasn't given." + return self.field.widget.value_from_datadict(self.form.data, self.html_name) + data = property(_data) + + def label_tag(self, contents=None): + """ + 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. + """ + contents = contents or escape(self.label) + widget = self.field.widget + id_ = widget.attrs.get('id') or self.auto_id + if id_: + contents = '<label for="%s">%s</label>' % (widget.id_for_label(id_), contents) + return contents + + def _is_hidden(self): + "Returns True if this BoundField's widget is hidden." + return self.field.widget.is_hidden + is_hidden = property(_is_hidden) + def _auto_id(self): """ Calculates and returns the ID attribute for this BoundField, if the associated Form has specified auto_id. Returns an empty string otherwise. """ - auto_id = self._form.auto_id + auto_id = self.form.auto_id if auto_id and '%s' in str(auto_id): - return str(auto_id) % self._name + return str(auto_id) % self.html_name elif auto_id: - return self._name + return self.html_name return '' auto_id = property(_auto_id) diff --git a/django/newforms/models.py b/django/newforms/models.py new file mode 100644 index 0000000000..6b111d7ee1 --- /dev/null +++ b/django/newforms/models.py @@ -0,0 +1,38 @@ +""" +Helper functions for creating Form classes from Django models +and database field objects. +""" + +from forms import BaseForm, DeclarativeFieldsMetaclass, SortedDictFromList + +__all__ = ('form_for_model', 'form_for_fields') + +def create(self, save=True): + "Creates and returns model instance according to self.clean_data." + if self.errors: + raise ValueError("The %s could not be created because the data didn't validate." % self._model._meta.object_name) + obj = self._model(**self.clean_data) + if save: + obj.save() + return obj + +def form_for_model(model, form=None): + """ + Returns a Form class for the given Django model class. + + Provide 'form' if you want to use a custom BaseForm subclass. + """ + opts = model._meta + field_list = [] + for f in opts.fields + opts.many_to_many: + formfield = f.formfield() + if formfield: + field_list.append((f.name, formfield)) + fields = SortedDictFromList(field_list) + form = form or BaseForm + return type(opts.object_name + 'Form', (form,), {'fields': fields, '_model': model, 'create': create}) + +def form_for_fields(field_list): + "Returns a Form class for the given list of Django database field instances." + fields = SortedDictFromList([(f.name, f.formfield()) for f in field_list]) + return type('FormForFields', (BaseForm,), {'fields': fields}) diff --git a/django/newforms/util.py b/django/newforms/util.py index a5cc4932ea..a78623a17b 100644 --- a/django/newforms/util.py +++ b/django/newforms/util.py @@ -1,13 +1,22 @@ -# Default encoding for input byte strings. -DEFAULT_ENCODING = 'utf-8' # TODO: First look at django.conf.settings, then fall back to this. +from django.conf import settings def smart_unicode(s): if not isinstance(s, basestring): s = unicode(str(s)) elif not isinstance(s, unicode): - s = unicode(s, DEFAULT_ENCODING) + s = unicode(s, settings.DEFAULT_CHARSET) return s +class StrAndUnicode(object): + """ + A class whose __str__ returns its __unicode__ as a bytestring + according to settings.DEFAULT_CHARSET. + + Useful as a mix-in. + """ + def __str__(self): + return self.__unicode__().encode(settings.DEFAULT_CHARSET) + class ErrorDict(dict): """ A collection of errors that knows how to display itself in various formats. diff --git a/django/newforms/widgets.py b/django/newforms/widgets.py index 318c76e55d..996e353775 100644 --- a/django/newforms/widgets.py +++ b/django/newforms/widgets.py @@ -5,10 +5,11 @@ HTML Widget classes __all__ = ( 'Widget', 'TextInput', 'PasswordInput', 'HiddenInput', 'FileInput', 'Textarea', 'CheckboxInput', - 'Select', 'SelectMultiple', 'RadioSelect', + 'Select', 'SelectMultiple', 'RadioSelect', 'CheckboxSelectMultiple', ) -from util import smart_unicode +from util import StrAndUnicode, smart_unicode +from django.utils.datastructures import MultiValueDict from django.utils.html import escape from itertools import chain @@ -22,25 +23,54 @@ except NameError: flatatt = lambda attrs: u''.join([u' %s="%s"' % (k, escape(v)) for k, v in attrs.items()]) class Widget(object): - requires_data_list = False # Determines whether render()'s 'value' argument should be a list. + is_hidden = False # Determines whether this corresponds to an <input type="hidden">. + def __init__(self, attrs=None): self.attrs = attrs or {} - def render(self, name, value): + def render(self, name, value, attrs=None): + """ + Returns this Widget rendered as HTML, as a Unicode string. + + The 'value' given is not guaranteed to be valid input, so subclass + implementations should program defensively. + """ raise NotImplementedError def build_attrs(self, extra_attrs=None, **kwargs): + "Helper function for building an attribute dictionary." attrs = dict(self.attrs, **kwargs) if extra_attrs: attrs.update(extra_attrs) return attrs + def value_from_datadict(self, data, name): + """ + Given a dictionary of data and this widget's name, returns the value + of this widget. Returns None if it's not provided. + """ + return data.get(name, None) + + def id_for_label(self, id_): + """ + Returns the HTML ID attribute of this Widget for use by a <label>, + given the ID of the field. Returns None if no ID is available. + + This hook is necessary because some widgets have multiple HTML + elements and, thus, multiple IDs. In that case, this method should + return an ID value that corresponds to the first ID in the widget's + tags. + """ + return id_ + id_for_label = classmethod(id_for_label) + class Input(Widget): """ Base class for all <input> widgets (except type='checkbox' and type='radio', which are special). """ input_type = None # Subclasses must define this. + def render(self, name, value, attrs=None): if value is None: value = '' final_attrs = self.build_attrs(attrs, type=self.input_type, name=name) @@ -55,6 +85,7 @@ class PasswordInput(Input): class HiddenInput(Input): input_type = 'hidden' + is_hidden = True class FileInput(Input): input_type = 'file' @@ -67,9 +98,22 @@ class Textarea(Widget): return u'<textarea%s>%s</textarea>' % (flatatt(final_attrs), escape(value)) class CheckboxInput(Widget): + def __init__(self, attrs=None, check_test=bool): + # check_test is a callable that takes a value and returns True + # if the checkbox should be checked for that value. + self.attrs = attrs or {} + self.check_test = check_test + def render(self, name, value, attrs=None): final_attrs = self.build_attrs(attrs, type='checkbox', name=name) - if value: final_attrs['checked'] = 'checked' + try: + result = self.check_test(value) + except: # Silently catch exceptions + result = False + if result: + final_attrs['checked'] = 'checked' + if value not in ('', True, False, None): + final_attrs['value'] = smart_unicode(value) # Only add the 'value' attribute if a value is non-empty. return u'<input%s />' % flatatt(final_attrs) class Select(Widget): @@ -91,7 +135,6 @@ class Select(Widget): return u'\n'.join(output) class SelectMultiple(Widget): - requires_data_list = True def __init__(self, attrs=None, choices=()): # choices can be any iterable self.attrs = attrs or {} @@ -109,36 +152,48 @@ class SelectMultiple(Widget): output.append(u'</select>') return u'\n'.join(output) -class RadioInput(object): + def value_from_datadict(self, data, name): + if isinstance(data, MultiValueDict): + return data.getlist(name) + return data.get(name, None) + +class RadioInput(StrAndUnicode): "An object used by RadioFieldRenderer that represents a single <input type='radio'>." - def __init__(self, name, value, attrs, choice): + def __init__(self, name, value, attrs, choice, index): self.name, self.value = name, value - self.attrs = attrs or {} + self.attrs = attrs self.choice_value, self.choice_label = choice + self.index = index - def __str__(self): + def __unicode__(self): return u'<label>%s %s</label>' % (self.tag(), self.choice_label) def is_checked(self): return self.value == smart_unicode(self.choice_value) def tag(self): + if self.attrs.has_key('id'): + self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index) final_attrs = dict(self.attrs, type='radio', name=self.name, value=self.choice_value) if self.is_checked(): final_attrs['checked'] = 'checked' return u'<input%s />' % flatatt(final_attrs) -class RadioFieldRenderer(object): +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 choice in self.choices: - yield RadioInput(self.name, self.value, self.attrs, choice) + 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 __str__(self): + def __unicode__(self): "Outputs a <ul> for this set of radio fields." return u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>' % w for w in self]) @@ -147,7 +202,36 @@ class RadioSelect(Select): "Returns a RadioFieldRenderer instance rather than a Unicode string." if value is None: value = '' str_value = smart_unicode(value) # Normalize to string. + attrs = attrs or {} return RadioFieldRenderer(name, str_value, attrs, list(chain(self.choices, choices))) -class CheckboxSelectMultiple(Widget): - pass + def id_for_label(self, id_): + # RadioSelect is represented by multiple <input type="radio"> fields, + # each of which has a distinct ID. The IDs are made distinct by a "_X" + # suffix, where X is the zero-based index of the radio field. Thus, + # the label for a RadioSelect should reference the first one ('_0'). + if id_: + id_ += '_0' + return id_ + id_for_label = classmethod(id_for_label) + +class CheckboxSelectMultiple(SelectMultiple): + def render(self, name, value, attrs=None, choices=()): + if value is None: value = [] + final_attrs = self.build_attrs(attrs, name=name) + output = [u'<ul>'] + str_values = set([smart_unicode(v) for v in value]) # Normalize to strings. + cb = CheckboxInput(final_attrs, check_test=lambda value: value in str_values) + for option_value, option_label in chain(self.choices, choices): + option_value = smart_unicode(option_value) + rendered_cb = cb.render(name, option_value) + output.append(u'<li><label>%s %s</label></li>' % (rendered_cb, escape(smart_unicode(option_label)))) + output.append(u'</ul>') + return u'\n'.join(output) + + def id_for_label(self, id_): + # See the comment for RadioSelect.id_for_label() + if id_: + id_ += '_0' + return id_ + id_for_label = classmethod(id_for_label) |
