Skip to content

Commit

Permalink
Split django.newforms into forms, fields, widgets, util. Also moved u…
Browse files Browse the repository at this point in the history
…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
adrianholovaty committed Oct 28, 2006
1 parent 4d596a1 commit 88a2f53
Show file tree
Hide file tree
Showing 8 changed files with 869 additions and 843 deletions.
846 changes: 3 additions & 843 deletions django/newforms/__init__.py

Large diffs are not rendered by default.

184 changes: 184 additions & 0 deletions django/newforms/fields.py
@@ -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)
103 changes: 103 additions & 0 deletions django/newforms/forms.py
@@ -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)
55 changes: 55 additions & 0 deletions django/newforms/util.py
@@ -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)
43 changes: 43 additions & 0 deletions django/newforms/widgets.py
@@ -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.

0 comments on commit 88a2f53

Please sign in to comment.