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('
') return '\n'.join(ret)