from . import core
import re
from . import util
import string
import datetime
import copy
import functools
import webob
import warnings
import uuid
from .i18n import _
import six
# Compat
if six.PY3:
capitalize = str.capitalize
else:
capitalize = string.capitalize
# This hack helps work with different versions of WebOb
if not hasattr(webob, 'MultiDict'):
# Check for webob versions with UnicodeMultiDict
if hasattr(webob.multidict, 'UnicodeMultiDict'):
webob.MultiDict = webob.multidict.UnicodeMultiDict
else:
webob.MultiDict = webob.multidict.MultiDict
try:
import formencode
except ImportError:
formencode = None
class Invalid(object):
pass
class EmptyField(object):
pass
if formencode:
class BaseValidationError(core.WidgetError, formencode.Invalid):
def __init__(self, msg):
formencode.Invalid.__init__(self, msg, None, None)
else:
class BaseValidationError(core.WidgetError):
def __init__(self, msg):
core.WidgetError.__init__(self, msg)
self.msg = msg
[docs]class ValidationError(BaseValidationError):
"""Invalid data was encountered during validation.
The constructor can be passed a short message name, which is looked up in
a validator's :attr:`msgs` dictionary. Any values in this, like
``$val``` are substituted with that attribute from the validator. An
explicit validator instance can be passed to the constructor, or this
defaults to :class:`Validator` otherwise.
"""
def __init__(self, msg, validator=None, widget=None):
self.widget = widget
validator = validator or Validator
mw = core.request_local().get('middleware')
if isinstance(validator, Validator):
msg = validator.msg_rewrites.get(msg, msg)
if mw and msg in mw.config.validator_msgs:
msg = mw.config.validator_msgs[msg]
elif hasattr(validator, 'msgs') and msg in validator.msgs:
msg = validator.msgs.get(msg, msg)
# In the event that the user specified a form-wide validator but
# they did not specify a childerror message, show no error.
if msg == 'childerror':
msg = ''
msg = re.sub('\$(\w+)',
lambda m: str(getattr(validator, m.group(1))), six.text_type(msg))
super(ValidationError, self).__init__(msg)
@property
def message(self):
"""Added for backwards compatibility. Synonymous with `msg`."""
return self.msg
catch = ValidationError
if formencode:
catch = (catch, formencode.Invalid)
def safe_validate(validator, value, state=None):
try:
return validator.to_python(value, state=state)
except catch:
return Invalid
def catch_errors(fn):
@functools.wraps(fn)
def wrapper(self, *args, **kw):
try:
d = fn(self, *args, **kw)
return d
except catch as e:
e_msg = six.text_type(e)
if self:
self.error_msg = e_msg
raise ValidationError(e_msg, widget=self)
return wrapper
[docs]def unflatten_params(params):
"""This performs the first stage of validation. It takes a dictionary where
some keys will be compound names, such as "form:subform:field" and converts
this into a nested dict/list structure. It also performs unicode decoding,
with the encoding specified in the middleware config.
"""
if isinstance(params, webob.MultiDict):
params = params.mixed()
mw = core.request_local().get('middleware')
enc = mw.config.encoding if mw else 'utf-8'
try:
for p in params:
if isinstance(params[p], six.binary_type):
params[p] = params[p].decode(enc)
except UnicodeDecodeError:
raise ValidationError('decode', Validator(encoding=enc))
out = {}
for pname in params:
dct = out
elements = pname.split(':')
for e in elements[:-1]:
dct = dct.setdefault(e, {})
dct[elements[-1]] = params[pname]
numdict_to_list(out)
return out
number_re = re.compile('^\d+$')
def numdict_to_list(dct):
for k, v in dct.items():
if isinstance(v, dict):
numdict_to_list(v)
if all(number_re.match(k) for k in v):
dct[k] = [v[x] for x in sorted(v, key=int)]
class ValidatorMeta(type):
"""Metaclass for :class:`Validator`.
This makes the :attr:`msgs` dict copy from its base class.
"""
def __new__(meta, name, bases, dct):
if 'msgs' in dct:
msgs = {}
rewrites = {}
for b in bases:
try:
msgs.update(b.msgs)
rewrites.update(b.msgs_rewrites)
except AttributeError:
pass
msgs.update(dct['msgs'])
add_to_msgs = {}
del_from_msgs = []
for m, d in msgs.items():
if isinstance(d, tuple):
add_to_msgs[d[0]] = d[1]
rewrites[m] = d[0]
del_from_msgs.append(m)
msgs.update(add_to_msgs)
for m in del_from_msgs:
del msgs[m]
dct['msgs'] = msgs
dct['msg_rewrites'] = rewrites
if 'validate_python' in dct and '_validate_python' not in dct:
dct['_validate_python'] = dct.pop('validate_python')
warnings.warn('validate_python() is deprecated;'
' use _validate_python() instead',
DeprecationWarning, stacklevel=2)
return type.__new__(meta, name, bases, dct)
[docs]class Validator(six.with_metaclass(ValidatorMeta, object)):
"""Base class for validators
`required`
Whether empty values are forbidden in this field. (default: False)
`strip`
Whether to strip leading and trailing space from the input, before
any other validation. (default: True)
To convert and validate a value to Python, use the :meth:`to_python`
method, to convert back from Python, use :meth:`from_python`.
To create your own validators, sublass this class, and override any of
:meth:`_validate_python`, :meth:`_convert_to_python`,
or :meth:`_convert_from_python`. Note that these methods are not
meant to be used externally. All of them may raise ValidationErrors.
"""
msgs = {
'required': _('Enter a value'),
'decode': _('Received in wrong character set; should be $encoding'),
'corrupt': _('Form submission received corrupted; please try again'),
'childerror': '', # Children of this widget have errors
}
required = False
strip = True
if_empty = None
def __init__(self, **kw):
for k in kw:
setattr(self, k, kw[k])
[docs] def to_python(self, value, state=None):
"""Convert an external value to Python and validate it."""
if self._is_empty(value):
if self.required:
raise ValidationError('required', self)
return self.if_empty
if self.strip and isinstance(value, six.string_types):
value = value.strip()
value = self._convert_to_python(value, state)
self._validate_python(value, state)
return value
[docs] def from_python(self, value, state=None):
"""Convert from a Python object to an external value."""
if self._is_empty(value):
return ''
if isinstance(value, six.string_types) and self.strip:
value = value.strip()
value = self._convert_from_python(value, state)
return value
def __repr__(self):
_bool = ['False', 'True']
return ("Validator(required=%s, strip=%s)" %
(_bool[int(self.required)], _bool[int(self.strip)]))
def _validate_python(self, value, state=None):
""""Overridable internal method for validation of Python values."""
pass
def _convert_to_python(self, value, state=None):
""""Overridable internal method for conversion to Python values."""
return value
def _convert_from_python(self, value, state=None):
""""Overridable internal method for conversion from Python values."""
return value
@staticmethod
def _is_empty(value):
"""Check whether the given value should be considered "empty"."""
return value is None or value == '' or (
isinstance(value, (list, tuple, dict)) and not value)
[docs] def validate_python(self, value, state=None):
""""Deprecated, use :meth:`_validate_python` instead.
This method has been renamed in FormEncode 1.3 and ToscaWidgets 2.2
in order to clarify that is an internal method that is meant to be
overridden only; you must call meth:`to_python` to validate values.
"""
warnings.warn('validate_python() is deprecated;'
' use _validate_python() instead',
DeprecationWarning, stacklevel=2)
return self._validate_python(value, state)
def clone(self, **kw):
nself = copy.copy(self)
for k in kw:
setattr(nself, k, kw[k])
return nself
[docs]class BlankValidator(Validator):
"""
Always returns EmptyField. This is the default for hidden fields,
so their values are not included in validated data.
"""
[docs] def to_python(self, value, state=None):
return EmptyField
[docs]class LengthValidator(Validator):
"""
Confirm a value is of a suitable length. Usually you'll use
:class:`StringLengthValidator` or :class:`ListLengthValidator` instead.
`min`
Minimum length (default: None)
`max`
Maximum length (default: None)
"""
msgs = {
'tooshort': _('Value is too short'),
'toolong': _('Value is too long'),
}
min = None
max = None
def __init__(self, **kw):
super(LengthValidator, self).__init__(**kw)
if self.min:
self.required = True
def _validate_python(self, value, state=None):
if self.min and len(value) < self.min:
raise ValidationError('tooshort', self)
if self.max and len(value) > self.max:
raise ValidationError('toolong', self)
[docs]class StringLengthValidator(LengthValidator):
"""
Check a string is a suitable length. The only difference to LengthValidator
is that the messages are worded differently.
"""
msgs = {
'tooshort': (
'string_tooshort', _('Must be at least $min characters')),
'toolong': (
'string_toolong', _('Cannot be longer than $max characters')),
}
[docs]class ListLengthValidator(LengthValidator):
"""
Check a list is a suitable length. The only difference to LengthValidator
is that the messages are worded differently.
"""
msgs = {
'tooshort': ('list_tooshort', _('Select at least $min')),
'toolong': ('list_toolong', _('Select no more than $max')),
}
[docs]class RangeValidator(Validator):
"""
Confirm a value is within an appropriate range. This is not usually used
directly, but other validators are derived from this.
`min`
Minimum value (default: None)
`max`
Maximum value (default: None)
"""
msgs = {
'toosmall': _('Must be at least $min'),
'toobig': _('Cannot be more than $max'),
}
min = None
max = None
def _validate_python(self, value, state=None):
if self.min is not None and value < self.min:
raise ValidationError('toosmall', self)
if self.max is not None and value > self.max:
raise ValidationError('toobig', self)
[docs]class IntValidator(RangeValidator):
"""
Confirm the value is an integer. This is derived from
:class:`RangeValidator` so `min` and `max` can be specified.
"""
msgs = {
'notint': _('Must be an integer'),
}
def _convert_to_python(self, value, state=None):
try:
return int(value)
except ValueError:
raise ValidationError('notint', self)
def _convert_from_python(self, value, state=None):
return str(value)
[docs]class BoolValidator(RangeValidator):
"""
Convert a value to a boolean. This is particularly intended to handle
check boxes.
"""
msgs = {
'required': ('bool_required', _('You must select this'))
}
if_empty = False
def _convert_to_python(self, value, state=None):
return str(value).lower() in ('on', 'yes', 'true', '1', 'y', 't')
def _convert_from_python(self, value, state=None):
return value and 'true' or 'false'
[docs]class OneOfValidator(Validator):
"""
Confirm the value is one of a list of acceptable values. This is useful for
confirming that select fields have not been tampered with by a user.
`values`
Acceptable values
"""
msgs = {
'notinlist': _('Invalid value'),
}
values = []
def _validate_python(self, value, state=None):
if value not in self.values:
raise ValidationError('notinlist', self)
[docs]class DateTimeValidator(RangeValidator):
"""
Confirm the value is a valid date and time. This is derived from
:class:`RangeValidator` so `min` and `max` can be specified.
`format`
The expected date/time format. The format must be specified using
the same syntax as the Python strftime function.
"""
msgs = {
'baddatetime': _('Must follow date/time format $format_str'),
'toosmall': ('date_toosmall', _('Cannot be earlier than $min_str')),
'toobig': ('date_toobig', _('Cannot be later than $max_str')),
}
format = '%Y-%m-%d %H:%M'
format_tbl = {
'd': 'day',
'H': 'hour',
'I': 'hour',
'm': 'month',
'M': 'minute',
'S': 'second',
'y': 'year',
'Y': 'year',
}
@property
def format_str(self):
f = lambda m: self.format_tbl.get(m.group(1), '')
return re.sub('%(.)', f, self.format)
@property
def min_str(self):
return self.min.strftime(self.format)
@property
def max_str(self):
return self.max.strftime(self.format)
def _convert_to_python(self, value, state=None):
if isinstance(value, datetime.datetime):
return value
if isinstance(value, datetime.date):
return datetime.datetime(value.year, value.month, value.day)
try:
return datetime.datetime.strptime(value, self.format)
except ValueError:
raise ValidationError('baddatetime', self)
def _validate_python(self, value, state=None):
super(DateTimeValidator, self)._validate_python(value, state)
def _convert_from_python(self, value, state=None):
return value.strftime(self.format)
[docs]class DateValidator(DateTimeValidator):
"""
Confirm the value is a valid date.
Just like :class:`DateTimeValidator`, but without the time component.
"""
msgs = {
'baddatetime': (
'baddate', _('Must follow date format $format_str')),
}
format = '%Y-%m-%d'
def _convert_to_python(self, value, state=None):
value = super(DateValidator, self)._convert_to_python(value)
return value.date()
[docs]class RegexValidator(Validator):
"""
Confirm the value matches a regular expression.
`regex`
A Python regular expression object, generated like
``re.compile('^\w+$')``
"""
msgs = {
'badregex': _('Invalid value'),
}
regex = None
def _validate_python(self, value, state=None):
if not self.regex.search(value):
raise ValidationError('badregex', self)
[docs]class EmailValidator(RegexValidator):
"""
Confirm the value is a valid email address.
"""
msgs = {
'badregex': ('bademail', _('Must be a valid email address')),
}
regex = re.compile('^[\w\-.]+@[\w\-.]+$')
[docs]class UrlValidator(RegexValidator):
"""
Confirm the value is a valid URL.
"""
msgs = {
'regex': ('badurl', _('Must be a valid URL')),
}
regex = re.compile('^https?://', re.IGNORECASE)
[docs]class IpAddressValidator(Validator):
"""
Confirm the value is a valid IP4 address, or network block.
`allow_netblock`
Allow the IP address to include a network block (default: False)
`require_netblock`
Require the IP address to include a network block (default: False)
"""
allow_netblock = False
require_netblock = False
msgs = {
'badipaddress': _('Must be a valid IP address'),
'badnetblock': _('Must be a valid IP network block'),
}
regex = re.compile('^(\d+)\.(\d+)\.(\d+)\.(\d+)(/(\d+))?$')
def _validate_python(self, value, state=None):
m = self.regex.search(value)
if not m or any(not(0 <= int(g) <= 255) for g in m.groups()[:4]):
raise ValidationError('badipaddress', self)
if m.group(6):
if not self.allow_netblock:
raise ValidationError('badipaddress', self)
if not (0 <= int(m.group(6)) <= 32):
raise ValidationError('badnetblock', self)
elif self.require_netblock:
raise ValidationError('badnetblock', self)
[docs]class UUIDValidator(Validator):
"""
Confirm the value is a valid uuid and convert to uuid.UUID.
"""
msgs = {
'badregex': ('baduuid', _('Value not recognised as a UUID')),
}
regex = re.compile(\
'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
def _validate_python(self, value, state=None):
if not self.regex.search(str(value)):
raise ValidationError('badregex', self)
def _convert_to_python(self, value, state=None):
try:
return uuid.UUID( "{%s}" % value )
except ValueError:
raise ValidationError('baduuid', self)
def _convert_from_python(self, value, state=None):
return str(value)
[docs]class MatchValidator(Validator):
"""Confirm a field matches another field
`other_field`
Name of the sibling field this must match
`pass_on_invalid`
Pass validation if sibling field is Invalid
"""
msgs = {
'mismatch': _("Must match $other_field_str"),
'notfound': _("$other_field_str field is not found"),
'invalid': _("$other_field_str field is invalid"),
}
def __init__(self, other_field, pass_on_invalid=False, **kw):
super(MatchValidator, self).__init__(**kw)
self.other_field = other_field
self.pass_on_invalid = pass_on_invalid
@property
def other_field_str(self):
return capitalize(util.name2label(self.other_field).lower())
def _validate_python(self, value, state):
if isinstance(state, dict):
# Backward compatibility
values = state
else:
values = state.full_dict
if self.other_field not in values:
raise ValidationError('notfound', self)
other_value = values[self.other_field]
if other_value is Invalid:
if not self.pass_on_invalid:
raise ValidationError('invalid', self)
elif value != other_value:
raise ValidationError('mismatch', self)
def _is_empty(self, value):
return self.required and super(MatchValidator, self)._is_empty(value)
[docs]class CompoundValidator(Validator):
""" Base class for compound validators.
Child classes :class:`Any` and :class:`All` take validators as arguments
and use them to validate "value". In case the validation fails, they
raise a ValidationError with a compound message.
>>> v = All(StringLengthValidator(max=50), EmailValidator, required=True)
"""
def __init__(self, *args, **kw):
super(CompoundValidator, self).__init__(**kw)
self.validators = []
for arg in args:
if isinstance(arg, Validator):
self.validators.append(arg)
elif issubclass(arg, Validator):
self.validators.append(arg())
if getattr(arg, 'required', False):
self.required = True
[docs]class All(CompoundValidator):
"""
Confirm all validators passed as arguments are valid.
"""
def _validate_python(self, value, state=None):
msg = []
for validator in self.validators:
try:
validator._validate_python(value, state)
except ValidationError as e:
msg.append(six.text_type(e))
if msg:
msgset = set()
msg = ', '.join(m for m in msg
if m not in msgset and not msgset.add(m))
raise ValidationError(msg, self)
[docs]class Any(CompoundValidator):
"""
Confirm at least one of the validators passed as arguments is valid.
"""
def _validate_python(self, value, state=None):
msg = []
for validator in self.validators:
try:
validator._validate_python(value, state)
except ValidationError as e:
msg.append(six.text_type(e))
if len(msg) == len(self.validators):
msgset = set()
msg = ', '.join(m for m in msg
if m not in msgset and not msgset.add(m))
raise ValidationError(msg, self)