Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Split django.newforms into forms, fields, widgets, util. Also moved u…
…nit tests from docstrings to a standalone module in tests/regressiontests/forms, to save docstring memory overhead, keep code readable and fit our exisitng convention git-svn-id: http://code.djangoproject.com/svn/django/trunk@3945 bcc190cf-cafb-0310-a4f2-bffc1f526a37
- Loading branch information
1 parent
4d596a1
commit 88a2f53
Showing
8 changed files
with
869 additions
and
843 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,184 @@ | |||
""" | |||
Field classes | |||
""" | |||
|
|||
from util import ValidationError, DEFAULT_ENCODING | |||
from widgets import TextInput, CheckboxInput | |||
import datetime | |||
import re | |||
import time | |||
|
|||
__all__ = ( | |||
'Field', 'CharField', 'IntegerField', | |||
'DEFAULT_DATE_INPUT_FORMATS', 'DateField', | |||
'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField', | |||
'RegexField', 'EmailField', 'BooleanField', | |||
) | |||
|
|||
# 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. | |||
|
|||
def __init__(self, required=True, widget=None): | |||
self.required = required | |||
widget = widget or self.widget | |||
if isinstance(widget, type): | |||
widget = widget() | |||
self.widget = widget | |||
|
|||
def to_python(self, value): | |||
""" | |||
Validates the given value and returns its "normalized" value as an | |||
appropriate Python object. | |||
Raises ValidationError for any errors. | |||
""" | |||
if self.required and value in EMPTY_VALUES: | |||
raise ValidationError(u'This field is required.') | |||
return value | |||
|
|||
class CharField(Field): | |||
def __init__(self, max_length=None, min_length=None, required=True, widget=None): | |||
Field.__init__(self, required, widget) | |||
self.max_length, self.min_length = max_length, min_length | |||
|
|||
def to_python(self, value): | |||
"Validates max_length and min_length. Returns a Unicode object." | |||
Field.to_python(self, value) | |||
if value in EMPTY_VALUES: value = u'' | |||
if not isinstance(value, basestring): | |||
value = unicode(str(value), DEFAULT_ENCODING) | |||
elif not isinstance(value, unicode): | |||
value = unicode(value, DEFAULT_ENCODING) | |||
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) | |||
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) | |||
return value | |||
|
|||
class IntegerField(Field): | |||
def to_python(self, value): | |||
""" | |||
Validates that int() can be called on the input. Returns the result | |||
of int(). | |||
""" | |||
super(IntegerField, self).to_python(value) | |||
try: | |||
return int(value) | |||
except (ValueError, TypeError): | |||
raise ValidationError(u'Enter a whole number.') | |||
|
|||
DEFAULT_DATE_INPUT_FORMATS = ( | |||
'%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06' | |||
'%b %d %Y', '%b %d, %Y', # 'Oct 25 2006', 'Oct 25, 2006' | |||
'%d %b %Y', '%d %b, %Y', # '25 Oct 2006', '25 Oct, 2006' | |||
'%B %d %Y', '%B %d, %Y', # 'October 25 2006', 'October 25, 2006' | |||
'%d %B %Y', '%d %B, %Y', # '25 October 2006', '25 October, 2006' | |||
) | |||
|
|||
class DateField(Field): | |||
def __init__(self, input_formats=None, required=True, widget=None): | |||
Field.__init__(self, required, widget) | |||
self.input_formats = input_formats or DEFAULT_DATE_INPUT_FORMATS | |||
|
|||
def to_python(self, value): | |||
""" | |||
Validates that the input can be converted to a date. Returns a Python | |||
datetime.date object. | |||
""" | |||
Field.to_python(self, 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(u'Enter a valid date.') | |||
|
|||
DEFAULT_DATETIME_INPUT_FORMATS = ( | |||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59' | |||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30' | |||
'%Y-%m-%d', # '2006-10-25' | |||
'%m/%d/%Y %H:%M:%S', # '10/25/2006 14:30:59' | |||
'%m/%d/%Y %H:%M', # '10/25/2006 14:30' | |||
'%m/%d/%Y', # '10/25/2006' | |||
'%m/%d/%y %H:%M:%S', # '10/25/06 14:30:59' | |||
'%m/%d/%y %H:%M', # '10/25/06 14:30' | |||
'%m/%d/%y', # '10/25/06' | |||
) | |||
|
|||
class DateTimeField(Field): | |||
def __init__(self, input_formats=None, required=True, widget=None): | |||
Field.__init__(self, required, widget) | |||
self.input_formats = input_formats or DEFAULT_DATETIME_INPUT_FORMATS | |||
|
|||
def to_python(self, value): | |||
""" | |||
Validates that the input can be converted to a datetime. Returns a | |||
Python datetime.datetime object. | |||
""" | |||
Field.to_python(self, value) | |||
if value in EMPTY_VALUES: | |||
return None | |||
if isinstance(value, datetime.datetime): | |||
return value | |||
if isinstance(value, datetime.date): | |||
return datetime.datetime(value.year, value.month, value.day) | |||
for format in self.input_formats: | |||
try: | |||
return datetime.datetime(*time.strptime(value, format)[:6]) | |||
except ValueError: | |||
continue | |||
raise ValidationError(u'Enter a valid date/time.') | |||
|
|||
class RegexField(Field): | |||
def __init__(self, regex, error_message=None, required=True, widget=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) | |||
if isinstance(regex, basestring): | |||
regex = re.compile(regex) | |||
self.regex = regex | |||
self.error_message = error_message or u'Enter a valid value.' | |||
|
|||
def to_python(self, value): | |||
""" | |||
Validates that the input matches the regular expression. Returns a | |||
Unicode object. | |||
""" | |||
Field.to_python(self, value) | |||
if value in EMPTY_VALUES: value = u'' | |||
if not isinstance(value, basestring): | |||
value = unicode(str(value), DEFAULT_ENCODING) | |||
elif not isinstance(value, unicode): | |||
value = unicode(value, DEFAULT_ENCODING) | |||
if not self.regex.search(value): | |||
raise ValidationError(self.error_message) | |||
return value | |||
|
|||
email_re = re.compile( | |||
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom | |||
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string | |||
r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain | |||
|
|||
class EmailField(RegexField): | |||
def __init__(self, required=True, widget=None): | |||
RegexField.__init__(self, email_re, u'Enter a valid e-mail address.', required, widget) | |||
|
|||
class BooleanField(Field): | |||
widget = CheckboxInput | |||
|
|||
def to_python(self, value): | |||
"Returns a Python boolean object." | |||
Field.to_python(self, value) | |||
return bool(value) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,103 @@ | |||
""" | |||
Form classes | |||
""" | |||
|
|||
from fields import Field | |||
from widgets import TextInput, Textarea | |||
from util import ErrorDict, ErrorList, ValidationError | |||
|
|||
class DeclarativeFieldsMetaclass(type): | |||
"Metaclass that converts Field attributes to a dictionary called 'fields'." | |||
def __new__(cls, name, bases, attrs): | |||
attrs['fields'] = dict([(name, attrs.pop(name)) for name, obj in attrs.items() if isinstance(obj, Field)]) | |||
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): # TODO: prefix stuff | |||
self.data = data or {} | |||
self.__data_python = None # Stores the data after to_python() has been called. | |||
self.__errors = None # Stores the errors after to_python() has been called. | |||
|
|||
def __iter__(self): | |||
for name, field in self.fields.items(): | |||
yield BoundField(self, field, name) | |||
|
|||
def to_python(self): | |||
if self.__errors is None: | |||
self._validate() | |||
return self.__data_python | |||
|
|||
def errors(self): | |||
"Returns an ErrorDict for self.data" | |||
if self.__errors is None: | |||
self._validate() | |||
return self.__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()"). | |||
""" | |||
return not bool(self.errors()) | |||
|
|||
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 _validate(self): | |||
data_python = {} | |||
errors = ErrorDict() | |||
for name, field in self.fields.items(): | |||
try: | |||
value = field.to_python(self.data.get(name, None)) | |||
data_python[name] = value | |||
except ValidationError, e: | |||
errors[name] = e.messages | |||
if not errors: # Only set self.data_python if there weren't errors. | |||
self.__data_python = data_python | |||
self.__errors = errors | |||
|
|||
class BoundField(object): | |||
"A Field plus data" | |||
def __init__(self, form, field, name): | |||
self._form = form | |||
self._field = field | |||
self._name = name | |||
|
|||
def __str__(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) | |||
|
|||
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() | |||
errors = property(_errors) | |||
|
|||
def as_widget(self, widget, attrs=None): | |||
return widget.render(self._name, self._form.data.get(self._name, None), 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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,55 @@ | |||
# Default encoding for input byte strings. | |||
DEFAULT_ENCODING = 'utf-8' # TODO: First look at django.conf.settings, then fall back to this. | |||
|
|||
def smart_unicode(s): | |||
if not isinstance(s, unicode): | |||
s = unicode(s, DEFAULT_ENCODING) | |||
return s | |||
|
|||
class ErrorDict(dict): | |||
""" | |||
A collection of errors that knows how to display itself in various formats. | |||
The dictionary keys are the field names, and the values are the errors. | |||
""" | |||
def __str__(self): | |||
return self.as_ul() | |||
|
|||
def as_ul(self): | |||
if not self: return u'' | |||
return u'<ul class="errorlist">%s</ul>' % ''.join([u'<li>%s%s</li>' % (k, v) for k, v in self.items()]) | |||
|
|||
def as_text(self): | |||
return u'\n'.join([u'* %s\n%s' % (k, u'\n'.join([u' * %s' % i for i in v])) for k, v in self.items()]) | |||
|
|||
class ErrorList(list): | |||
""" | |||
A collection of errors that knows how to display itself in various formats. | |||
""" | |||
def __str__(self): | |||
return self.as_ul() | |||
|
|||
def as_ul(self): | |||
if not self: return u'' | |||
return u'<ul class="errorlist">%s</ul>' % ''.join([u'<li>%s</li>' % e for e in self]) | |||
|
|||
def as_text(self): | |||
if not self: return u'' | |||
return u'\n'.join([u'* %s' % e for e in self]) | |||
|
|||
class ValidationError(Exception): | |||
def __init__(self, message): | |||
"ValidationError can be passed a string or a list." | |||
if isinstance(message, list): | |||
self.messages = ErrorList([smart_unicode(msg) for msg in message]) | |||
else: | |||
assert isinstance(message, basestring), ("%s should be a basestring" % repr(message)) | |||
message = smart_unicode(message) | |||
self.messages = ErrorList([message]) | |||
|
|||
def __str__(self): | |||
# This is needed because, without a __str__(), printing an exception | |||
# instance would result in this: | |||
# AttributeError: ValidationError instance has no attribute 'args' | |||
# See http://www.python.org/doc/current/tut/node10.html#handling | |||
return repr(self.messages) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,43 @@ | |||
""" | |||
HTML Widget classes | |||
""" | |||
|
|||
__all__ = ('Widget', 'TextInput', 'Textarea', 'CheckboxInput') | |||
|
|||
from django.utils.html import escape | |||
|
|||
# Converts a dictionary to a single string with key="value", XML-style. | |||
# Assumes keys do not need to be XML-escaped. | |||
flatatt = lambda attrs: ' '.join(['%s="%s"' % (k, escape(v)) for k, v in attrs.items()]) | |||
|
|||
class Widget(object): | |||
def __init__(self, attrs=None): | |||
self.attrs = attrs or {} | |||
|
|||
def render(self, name, value): | |||
raise NotImplementedError | |||
|
|||
class TextInput(Widget): | |||
def render(self, name, value, attrs=None): | |||
if value is None: value = '' | |||
final_attrs = dict(self.attrs, type='text', name=name) | |||
if attrs: | |||
final_attrs.update(attrs) | |||
if value != '': final_attrs['value'] = value # Only add the 'value' attribute if a value is non-empty. | |||
return u'<input %s />' % flatatt(final_attrs) | |||
|
|||
class Textarea(Widget): | |||
def render(self, name, value, attrs=None): | |||
if value is None: value = '' | |||
final_attrs = dict(self.attrs, name=name) | |||
if attrs: | |||
final_attrs.update(attrs) | |||
return u'<textarea %s>%s</textarea>' % (flatatt(final_attrs), escape(value)) | |||
|
|||
class CheckboxInput(Widget): | |||
def render(self, name, value, attrs=None): | |||
final_attrs = dict(self.attrs, type='checkbox', name=name) | |||
if attrs: | |||
final_attrs.update(attrs) | |||
if value: final_attrs['checked'] = 'checked' | |||
return u'<input %s />' % flatatt(final_attrs) |
Empty file.
Empty file.
Oops, something went wrong.