import cgi
import re
import datetime as datetimemod
import decimal
import copy
class field(object):
""" base class for former fields
instantiate with an id, title, optional default value, optional
mandatory (defaulting to True), and an optional list of dicts with
attributes (which will be placed on the tag on rendering -
XXX not sure about that one yet)
"""
# the following list is used to determine what scripts should be included
# for widgets that use JS, the form will provide functionality to generate
# a list of script tags, with each script appearing only once
scripts = []
# allows overriding the form's enctype
enctype = 'application/x-www-form-urlencoded'
# the following bits allow hooking into certain events, note that multiple
# instances of the same string will appear only once
onload = ''
onunload = ''
onsubmit = ''
messages = {
'mandatory': 'field is mandatory',
'wrong_type': 'could not convert to %(type)s',
'unexpected_format': 'unexpected %(type)s format',
'positive_int': 'can not be less than 0',
'decimal_dot': ',',
'no_default_provided':
'this field is mandatory, but a default value is not provided',
}
type = unicode
def __init__(self, id, title, value=None, validators=None, mandatory=True,
attributes=None, encoding='utf-8'):
self.id = id
self.title = title
if value is None:
value = ''
self.value = value
self.validators = validators or []
self.mandatory = mandatory
self.attributes = attributes or {}
self.encoding = encoding
def get_value(self, data):
""" returns the best value available after validation
this checks if there's data available, if not returns the default
value (can be empty) for this field, if there is data it tries to
validate and returns either the validated/normalized data, or the
value as provided by the user
"""
value = self.value
error = None
if data is not None:
try:
value = self.value_from_data(data)
value = self.validate(value)
except ValueError, e:
error = e.args[0]
return value, error
def value_from_data(self, data):
""" retrieve the (single) value from data dict or FieldStorage
this should extract the value for this field from 'data', which
can be a cgi.FieldStorage or cgi.MiniFieldStorage instance or
a dictionary (not None, this has been dealt with already), and
return it
for more complex widgets, it may be that the value should be
built-up from multiple keys in the dict/fs, for instance a date
field could render itself as seperate day, month and year fields,
in that case this should be able to both deal with that three field
situation, and with a single key/value pair
"""
if (isinstance(data, cgi.FieldStorage) or
isinstance(data, cgi.MiniFieldStorage)):
value = data.getvalue(self.id)
else:
value = data.get(self.id)
if value is None:
# simple cases will want a string, obviously this depends on the
# type of field (may be date, or list for multi fields, etc.)
value = u''
elif not isinstance(value, unicode):
value = unicode(str(value), self.encoding)
return value
def validate(self, value):
if value is None or (hasattr(value, 'strip') and value.strip() == ''):
if self.mandatory:
raise ValueError(str(self.messages['mandatory']))
return
if not isinstance(value, self.type):
raise ValueError(
str(self.messages['wrong_type'] % {
'type': self.type.__name__}))
for validator in self.validators:
# allow validators to normalize value
value = validator.validate(value)
return value
def render(self, data):
""" render the whole widget, including label etc.
this method extracts the value from data (FieldStorage or dict),
validates it, and renders the field all at once, and returns a
tuple (html, error) with the generated html and the error
that were generated (string, None if nothing went wrong)
call with None as data argument to skip validation and use the
default value of this field, or None, as value
"""
value, error = self.get_value(data)
return (self.render_validated(value, error), error)
def render_validated(self, value, error):
""" render the tag including the label, error message, etc.
you can override this in subclasses, but usually it makes more
sense to just override render_tag below
"""
cls = 'former-field'
if self.mandatory:
cls += '-mandatory'
ret = ['
' % (cls,)]
if error:
if not isinstance(error, unicode):
error = unicode(str(error), self.encoding)
ret.append('
%s
' % (
cgi.escape(error),))
title = self.title
if not isinstance(title, unicode):
title = unicode(str(title), 'utf-8')
ret += ['
',
'' % (self.id, cgi.escape(title)),
'
']
ret.append(self.render_tag(value, error))
ret += [
'
',
]
return '\n'.join(ret)
def render_tag(self, value, error):
""" render just the field tag without label etc.
override this in subclasses
"""
attrs = self.render_attributes()
return u'' % (attrs,)
def render_attributes(self):
attrs = ''
if self.attributes:
for key, value in self.attributes.iteritems():
if not isinstance(value, unicode):
value = unicode(str(value), self.encoding)
attrs += ' %s="%s"' % (key, cgi.escape(value))
return attrs
def copy(self):
return self.__class__(self.id, self.title, self.value,
self.validators[:], self.mandatory,
self.attributes.copy())
class line(field):
def render_tag(self, value, error):
if value is None:
value = ''
else:
if not isinstance(value, unicode):
value = unicode(str(value), self.encoding)
attrs = self.render_attributes()
return u'' % (
self.id, self.id, cgi.escape(value), attrs)
class hidden(field):
def __init__(self, id, value=None, validators=None, attributes=None,
encoding='utf-8'):
super(hidden, self).__init__(
id, '', value, validators, True, attributes, encoding)
def render_validated(self, value, error):
if value is None:
value = ''
if not isinstance(value, unicode):
value = unicode(str(value), self.encoding)
return '' % (
self.id, self.id, cgi.escape(value))
class password(field):
def render_tag(self, value, error):
# note that we never render the actual value into the field
attrs = self.render_attributes()
return '' % (
self.id, self.id, attrs)
class integer(line):
type = int
def __init__(self, *args, **kwargs):
super(integer, self).__init__(*args, **kwargs)
if self.value not in (u'', None):
self.value = int(self.value)
self.mandatory = False
def value_from_data(self, data):
value = super(integer, self).value_from_data(data)
if value in (None, '', u'') and not self.mandatory:
return self.value # XXX ?!?
try:
value = int(value)
except ValueError:
raise ValueError(
str(self.messages['wrong_type'] % {'type': 'int'}))
return value
def validate(self, value):
value = super(integer, self).validate(value)
if value in (None, u''):
value = self.value
if value in (None, u''):
# this should not happen - the user should provide either a
# default value, or make the field mandatory (check in
# __init__)
raise ValueError(str(self.messages['no_default_provided']))
return value
def render_tag(self, value, error):
if value is None:
value = ''
else:
if not isinstance(value, unicode):
value = unicode(str(value), self.encoding)
if not isinstance(error, unicode):
error = unicode(str(error), self.encoding)
attrs = self.render_attributes()
return '' % (
self.id, self.id, cgi.escape(str(value)), attrs)
# XXX a bit nasty
_orgfloat = float
class float(line):
type = float
def __init__(self, *args, **kwargs):
super(integer, self).__init__(*args, **kwargs)
if self.value:
self.value = int(self.value)
def value_from_data(self, data):
value = super(float, self).value_from_data(data)
if value in (None, '') and not self.mandatory:
return
try:
value = _orgfloat(value)
except ValueError:
raise ValueError(
str(self.messages['wrong_type'] % {'type': 'float'}))
return value
def render_tag(self, value, error):
if value is None:
value = ''
if not isinstance(value, unicode):
value = unicode(str(value), self.encoding)
if not isinstance(error, unicode):
error = unicode(str(error), self.encoding)
attrs = self.render_attributes()
return '' % (
self.id, self.id, cgi.escape(str(value)), attrs)
class date(line):
type = datetimemod.date
def value_from_data(self, data):
from dateutil import parser
if (isinstance(data, cgi.FieldStorage) or
isinstance(data, cgi.MiniFieldStorage)):
value = data.getvalue(self.id)
else:
value = data.get(self.id)
if value is None:
return None
if isinstance(value, datetimemod.date):
return value
if not isinstance(value, unicode):
value = unicode(str(value), self.encoding)
value = value.strip()
p = parser.parser()
p.info = parser.parserinfo()
p.info.dayfirst = True
try:
value = p.parse(value).date()
except ValueError:
raise ValueError(
str(self.messages['unexpected_format'] % {'type': 'date'}))
return value
def render_tag(self, value, error):
if not value:
value = ''
else:
if not isinstance(value, str) and not isinstance(value, unicode):
value = '%s-%s-%s' % (value.day, value.month, value.year)
return super(date, self).render_tag(value, error)
class datetime(date):
type = datetimemod.datetime
def value_from_data(self, data):
from dateutil import parser
if (isinstance(data, cgi.FieldStorage) or
isinstance(data, cgi.MiniFieldStorage)):
value = data.getvalue(self.id)
else:
value = data.get(self.id)
if value is None:
return None
if isinstance(value, datetimemod.datetime):
return value
if not isinstance(value, unicode):
value = unicode(str(value), self.encoding)
value = value.strip()
p = parser.parser()
p.info = parser.parserinfo()
p.info.dayfirst = True
try:
value = p.parse(value)
except ValueError:
raise ValueError(
str(self.messages['unexpected_format'] % {
'type': 'date/time'}))
return value
def render_tag(self, value, error):
if not value:
value = ''
else:
if not isinstance(value, str) and not isinstance(value, unicode):
value = '%s-%s-%s %s:%s' % (
value.day, value.month, value.year,
value.hour, value.minute)
return super(date, self).render_tag(value, error)
class money(line):
""" handle money data
allow using a comma instead of a dot for decimals, don't allow
seperators for thousands, convert to plain decimal
"""
type = decimal.Decimal
def value_from_data(self, data):
reg_comma = re.compile(
'^\d*\[%s]\d{1,2}$' % (str(self.messages['decimal_dot']),))
if (isinstance(data, cgi.FieldStorage) or
isinstance(data, cgi.MiniFieldStorage)):
value = data.getvalue(self.id)
else:
value = data.get(self.id)
if value is None or isinstance(value, decimal.Decimal):
return value
if not isinstance(value, unicode):
value = unicode(str(value), self.encoding)
value = value.strip().replace(str(self.messages['decimal_dot']), '.')
try:
value = decimal.Decimal(value)
except decimal.InvalidOperation:
raise ValueError(
str(self.messages['unexpected_format'] % {'type': 'decimal'}))
if value < 0:
raise ValueError(str(self.messages['positive_int']))
return value
def render_tag(self, value, error):
if not value:
value = ''
else:
if not isinstance(value, str) and not isinstance(value, unicode):
value = ('%0.2f' % (value,)).replace(
'.', str(self.messages['decimal_dot']))
return super(money, self).render_tag(value, error)
class text(field):
""" a textarea
"""
def render_tag(self, value, error):
if value is None:
value = u''
elif not isinstance(value, unicode):
value = unicode(str(value), self.encoding)
attrs = self.render_attributes()
return '' % (
self.id, self.id, attrs, cgi.escape(value))
class singlecheckbox(field):
def __init__(self, id, title, value=None, label=None, default=False,
validators=None, attributes=None, encoding='utf-8'):
super(singlecheckbox, self).__init__(
id, title, value, validators=validators,
mandatory=False, attributes=attributes, encoding=encoding)
self.label = label
self.default = default
def render_tag(self, value, error):
attrs = self.render_attributes()
if not isinstance(value, unicode):
value = unicode(str(value), self.encoding)
checked = (value and self.value == value)
checkedstr = checked and 'checked="checked" ' or ''
return (
' %s' % (
self.id, self.id, self.value, checkedstr, attrs, self.label))
def copy(self):
return self.__class__(self.id, self.title, self.value, self.label,
self.default, self.validators, self.attributes)
class selectfield(field):
""" multiple choices, one result
requires an additional argument 'values', a list (or tuple) of tuples
(value, labeltext)
"""
def __init__(self, id, title, values, value=None, validators=None,
mandatory=True, attributes=None, encoding='utf-8'):
super(selectfield, self).__init__(id, title, value, validators,
mandatory, attributes, encoding)
self.values = values
def value_from_data(self, data):
value = super(selectfield, self).value_from_data(data)
# the value in the provided list may not have been a string (though
# we do assume they all convert to a string in a meaningful way),
# here we check each item to see if its string representation is
# the same as the value provided, and return it, in order to make
# sure the returned value is the same as provided
return value
for v, label in self.values:
if str(v) == value:
return v
return value
def copy(self):
return self.__class__(self.id, self.title, self.values, self.value,
self.validators, self.mandatory, self.attributes)
class radio(selectfield):
def render_tag(self, value, error):
if value is None:
value = ''
attrs = self.render_attributes()
ret = ['
']
for i, (rvalue, labeltext) in enumerate(self.values):
# check this radio if a) it has been selected, or b) this is
# the first radio of a mandatory field and there is no value
# (effectively selecting the first element by default)
currentattrs = attrs
checkvalue = value or self.value
if not isinstance(checkvalue, unicode):
checkvalue = unicode(str(checkvalue), self.encoding)
if not isinstance(rvalue, unicode):
rvalue = unicode(str(rvalue), self.encoding)
if not isinstance(labeltext, unicode):
labeltext = unicode(str(labeltext), self.encoding)
if ((not value and i == 0 and self.mandatory) or
rvalue == checkvalue):
currentattrs += ' checked="checked"'
ret += [
'' % (
self.id, self.id, i, cgi.escape(rvalue), currentattrs),
'' % (self.id, i,
cgi.escape(labeltext)),
]
ret.append('
')
return '\n'.join(ret)
class singleselect(selectfield):
def render_tag(self, value, error):
if value is None:
value = ''
attrs = self.render_attributes()
ret = ['')
return '\n'.join(ret)
class multifield(field):
""" multiple choices, multiple results
requires an additional argument 'values', a list (or tuple) of tuples
(value, labeltext)
self.value is not a single value, but a list
"""
type = list
def __init__(self, id, title, values, value=None, validators=None,
mandatory=True, attributes=None, encoding='utf-8'):
super(multifield, self).__init__(id, title, value, validators,
mandatory, attributes, encoding)
self.values = values
def value_from_data(self, data):
if (isinstance(data, cgi.FieldStorage) or
isinstance(data, cgi.MiniFieldStorage)):
value = data.getlist(self.id)
else:
value = data.get(self.id)
if not isinstance(value, list):
value = [value]
if value is None:
return []
fixed = []
for v in value:
if not isinstance(v, unicode):
v = unicode(str(v), self.encoding)
fixed.append(v)
return fixed
def copy(self):
return self.__class__(self.id, self.title, self.values, self.value,
self.validators, self.mandatory, self.attributes)
class checkbox(multifield):
""" a set of checkboxes
"""
def render_tag(self, value, error):
if value is None:
value = []
checkvalue = value or self.value
fixed = []
for v in checkvalue:
if not isinstance(v, unicode):
v = unicode(str(v), self.encoding)
fixed.append(v)
checkvalue = fixed
attrs = self.render_attributes()
ret = ['
']
for i, (rvalue, labeltext) in enumerate(self.values):
if not isinstance(rvalue, unicode):
rvalue = unicode(str(rvalue), self.encoding)
if not isinstance(labeltext, unicode):
labeltext = unicode(str(labeltext), self.encoding)
# check this checkbox if a) it has been selected, or b) this is
# the first checkbox of a mandatory field and there is no value
# (effectively selecting the first element by default)
currentattrs = attrs
if ((not value and i == 0 and self.mandatory) or
rvalue in checkvalue):
currentattrs += ' checked="checked"'
ret += [
('') % (
self.id, self.id, i, cgi.escape(rvalue), currentattrs),
'' % (self.id, i,
cgi.escape(labeltext)),
]
ret.append('