Coverage for tw2/core/validation.py : 97%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from . import core
2import re
3from . import util
4import string
5import datetime
6import copy
7import functools
8import webob
9import warnings
10import uuid
12from .i18n import _
13import six
15# Compat
16if six.PY3:
17 capitalize = str.capitalize
18else:
19 capitalize = string.capitalize
21# This hack helps work with different versions of WebOb
22if not hasattr(webob, 'MultiDict'):
23 # Check for webob versions with UnicodeMultiDict
24 if hasattr(webob.multidict, 'UnicodeMultiDict'):
25 webob.MultiDict = webob.multidict.UnicodeMultiDict
26 else:
27 webob.MultiDict = webob.multidict.MultiDict
29try:
30 import formencode
31except ImportError:
32 formencode = None
35class Invalid(object):
36 pass
39class EmptyField(object):
40 pass
43if formencode:
44 class BaseValidationError(core.WidgetError, formencode.Invalid):
45 def __init__(self, msg):
46 formencode.Invalid.__init__(self, msg, None, None)
47else:
48 class BaseValidationError(core.WidgetError):
49 def __init__(self, msg):
50 core.WidgetError.__init__(self, msg)
51 self.msg = msg
54class ValidationError(BaseValidationError):
55 """Invalid data was encountered during validation.
57 The constructor can be passed a short message name, which is looked up in
58 a validator's :attr:`msgs` dictionary. Any values in this, like
59 ``$val``` are substituted with that attribute from the validator. An
60 explicit validator instance can be passed to the constructor, or this
61 defaults to :class:`Validator` otherwise.
62 """
63 def __init__(self, msg, validator=None, widget=None):
64 self.widget = widget
65 validator = validator or Validator
66 mw = core.request_local().get('middleware')
67 if isinstance(validator, Validator):
68 msg = validator.msg_rewrites.get(msg, msg)
70 if mw and msg in mw.config.validator_msgs:
71 msg = mw.config.validator_msgs[msg]
72 elif hasattr(validator, 'msgs') and msg in validator.msgs:
73 msg = validator.msgs.get(msg, msg)
75 # In the event that the user specified a form-wide validator but
76 # they did not specify a childerror message, show no error.
77 if msg == 'childerror':
78 msg = ''
80 msg = re.sub('\$(\w+)',
81 lambda m: str(getattr(validator, m.group(1))), six.text_type(msg))
82 super(ValidationError, self).__init__(msg)
84 @property
85 def message(self):
86 """Added for backwards compatibility. Synonymous with `msg`."""
87 return self.msg
90catch = ValidationError
91if formencode:
92 catch = (catch, formencode.Invalid)
95def safe_validate(validator, value, state=None):
96 try:
97 return validator.to_python(value, state=state)
98 except catch:
99 return Invalid
102def catch_errors(fn):
103 @functools.wraps(fn)
104 def wrapper(self, *args, **kw):
105 try:
106 d = fn(self, *args, **kw)
107 return d
108 except catch as e:
109 e_msg = six.text_type(e)
110 if self:
111 self.error_msg = e_msg
112 raise ValidationError(e_msg, widget=self)
113 return wrapper
116def unflatten_params(params):
117 """This performs the first stage of validation. It takes a dictionary where
118 some keys will be compound names, such as "form:subform:field" and converts
119 this into a nested dict/list structure. It also performs unicode decoding,
120 with the encoding specified in the middleware config.
121 """
122 if isinstance(params, webob.MultiDict):
123 params = params.mixed()
125 mw = core.request_local().get('middleware')
126 enc = mw.config.encoding if mw else 'utf-8'
128 try:
129 for p in params:
130 if isinstance(params[p], six.binary_type):
131 params[p] = params[p].decode(enc)
132 except UnicodeDecodeError:
133 raise ValidationError('decode', Validator(encoding=enc))
135 out = {}
136 for pname in params:
137 dct = out
138 elements = pname.split(':')
139 for e in elements[:-1]:
140 dct = dct.setdefault(e, {})
141 dct[elements[-1]] = params[pname]
143 numdict_to_list(out)
144 return out
146number_re = re.compile('^\d+$')
149def numdict_to_list(dct):
150 for k, v in dct.items():
151 if isinstance(v, dict):
152 numdict_to_list(v)
153 if all(number_re.match(k) for k in v):
154 dct[k] = [v[x] for x in sorted(v, key=int)]
157class ValidatorMeta(type):
158 """Metaclass for :class:`Validator`.
160 This makes the :attr:`msgs` dict copy from its base class.
161 """
162 def __new__(meta, name, bases, dct):
163 if 'msgs' in dct:
164 msgs = {}
165 rewrites = {}
166 for b in bases:
167 try:
168 msgs.update(b.msgs)
169 rewrites.update(b.msgs_rewrites)
170 except AttributeError:
171 pass
172 msgs.update(dct['msgs'])
173 add_to_msgs = {}
174 del_from_msgs = []
175 for m, d in msgs.items():
176 if isinstance(d, tuple):
177 add_to_msgs[d[0]] = d[1]
178 rewrites[m] = d[0]
179 del_from_msgs.append(m)
180 msgs.update(add_to_msgs)
181 for m in del_from_msgs:
182 del msgs[m]
183 dct['msgs'] = msgs
184 dct['msg_rewrites'] = rewrites
185 if 'validate_python' in dct and '_validate_python' not in dct:
186 dct['_validate_python'] = dct.pop('validate_python')
187 warnings.warn('validate_python() is deprecated;'
188 ' use _validate_python() instead',
189 DeprecationWarning, stacklevel=2)
190 return type.__new__(meta, name, bases, dct)
193class Validator(six.with_metaclass(ValidatorMeta, object)):
194 """Base class for validators
196 `required`
197 Whether empty values are forbidden in this field. (default: False)
199 `strip`
200 Whether to strip leading and trailing space from the input, before
201 any other validation. (default: True)
203 To convert and validate a value to Python, use the :meth:`to_python`
204 method, to convert back from Python, use :meth:`from_python`.
206 To create your own validators, sublass this class, and override any of
207 :meth:`_validate_python`, :meth:`_convert_to_python`,
208 or :meth:`_convert_from_python`. Note that these methods are not
209 meant to be used externally. All of them may raise ValidationErrors.
211 """
213 msgs = {
214 'required': _('Enter a value'),
215 'decode': _('Received in wrong character set; should be $encoding'),
216 'corrupt': _('Form submission received corrupted; please try again'),
217 'childerror': '', # Children of this widget have errors
218 }
219 required = False
220 strip = True
221 if_empty = None
223 def __init__(self, **kw):
224 for k in kw:
225 setattr(self, k, kw[k])
227 def to_python(self, value, state=None):
228 """Convert an external value to Python and validate it."""
229 if self._is_empty(value):
230 if self.required:
231 raise ValidationError('required', self)
232 return self.if_empty
233 if self.strip and isinstance(value, six.string_types):
234 value = value.strip()
235 value = self._convert_to_python(value, state)
236 self._validate_python(value, state)
237 return value
239 def from_python(self, value, state=None):
240 """Convert from a Python object to an external value."""
241 if self._is_empty(value):
242 return ''
243 if isinstance(value, six.string_types) and self.strip:
244 value = value.strip()
245 value = self._convert_from_python(value, state)
246 return value
248 def __repr__(self):
249 _bool = ['False', 'True']
250 return ("Validator(required=%s, strip=%s)" %
251 (_bool[int(self.required)], _bool[int(self.strip)]))
253 def _validate_python(self, value, state=None):
254 """"Overridable internal method for validation of Python values."""
255 pass
257 def _convert_to_python(self, value, state=None):
258 """"Overridable internal method for conversion to Python values."""
259 return value
261 def _convert_from_python(self, value, state=None):
262 """"Overridable internal method for conversion from Python values."""
263 return value
265 @staticmethod
266 def _is_empty(value):
267 """Check whether the given value should be considered "empty"."""
268 return value is None or value == '' or (
269 isinstance(value, (list, tuple, dict)) and not value)
271 def validate_python(self, value, state=None):
272 """"Deprecated, use :meth:`_validate_python` instead.
274 This method has been renamed in FormEncode 1.3 and ToscaWidgets 2.2
275 in order to clarify that is an internal method that is meant to be
276 overridden only; you must call meth:`to_python` to validate values.
278 """
279 warnings.warn('validate_python() is deprecated;'
280 ' use _validate_python() instead',
281 DeprecationWarning, stacklevel=2)
282 return self._validate_python(value, state)
284 def clone(self, **kw):
285 nself = copy.copy(self)
286 for k in kw:
287 setattr(nself, k, kw[k])
288 return nself
290if formencode:
291 validator_classes = (Validator, formencode.Validator)
292else:
293 validator_classes = (Validator, )
296class BlankValidator(Validator):
297 """
298 Always returns EmptyField. This is the default for hidden fields,
299 so their values are not included in validated data.
300 """
301 def to_python(self, value, state=None):
302 return EmptyField
305class LengthValidator(Validator):
306 """
307 Confirm a value is of a suitable length. Usually you'll use
308 :class:`StringLengthValidator` or :class:`ListLengthValidator` instead.
310 `min`
311 Minimum length (default: None)
313 `max`
314 Maximum length (default: None)
315 """
316 msgs = {
317 'tooshort': _('Value is too short'),
318 'toolong': _('Value is too long'),
319 }
320 min = None
321 max = None
323 def __init__(self, **kw):
324 super(LengthValidator, self).__init__(**kw)
325 if self.min:
326 self.required = True
328 def _validate_python(self, value, state=None):
329 if self.min and len(value) < self.min:
330 raise ValidationError('tooshort', self)
331 if self.max and len(value) > self.max:
332 raise ValidationError('toolong', self)
335class StringLengthValidator(LengthValidator):
336 """
337 Check a string is a suitable length. The only difference to LengthValidator
338 is that the messages are worded differently.
339 """
341 msgs = {
342 'tooshort': (
343 'string_tooshort', _('Must be at least $min characters')),
344 'toolong': (
345 'string_toolong', _('Cannot be longer than $max characters')),
346 }
349class ListLengthValidator(LengthValidator):
350 """
351 Check a list is a suitable length. The only difference to LengthValidator
352 is that the messages are worded differently.
353 """
355 msgs = {
356 'tooshort': ('list_tooshort', _('Select at least $min')),
357 'toolong': ('list_toolong', _('Select no more than $max')),
358 }
361class RangeValidator(Validator):
362 """
363 Confirm a value is within an appropriate range. This is not usually used
364 directly, but other validators are derived from this.
366 `min`
367 Minimum value (default: None)
369 `max`
370 Maximum value (default: None)
371 """
372 msgs = {
373 'toosmall': _('Must be at least $min'),
374 'toobig': _('Cannot be more than $max'),
375 }
376 min = None
377 max = None
379 def _validate_python(self, value, state=None):
380 if self.min is not None and value < self.min:
381 raise ValidationError('toosmall', self)
382 if self.max is not None and value > self.max:
383 raise ValidationError('toobig', self)
386class IntValidator(RangeValidator):
387 """
388 Confirm the value is an integer. This is derived from
389 :class:`RangeValidator` so `min` and `max` can be specified.
390 """
391 msgs = {
392 'notint': _('Must be an integer'),
393 }
395 def _convert_to_python(self, value, state=None):
396 try:
397 return int(value)
398 except ValueError:
399 raise ValidationError('notint', self)
401 def _convert_from_python(self, value, state=None):
402 return str(value)
405class BoolValidator(RangeValidator):
406 """
407 Convert a value to a boolean. This is particularly intended to handle
408 check boxes.
409 """
410 msgs = {
411 'required': ('bool_required', _('You must select this'))
412 }
413 if_empty = False
415 def _convert_to_python(self, value, state=None):
416 return str(value).lower() in ('on', 'yes', 'true', '1', 'y', 't')
418 def _convert_from_python(self, value, state=None):
419 return value and 'true' or 'false'
422class OneOfValidator(Validator):
423 """
424 Confirm the value is one of a list of acceptable values. This is useful for
425 confirming that select fields have not been tampered with by a user.
427 `values`
428 Acceptable values
429 """
430 msgs = {
431 'notinlist': _('Invalid value'),
432 }
433 values = []
435 def _validate_python(self, value, state=None):
436 if value not in self.values:
437 raise ValidationError('notinlist', self)
440class DateTimeValidator(RangeValidator):
441 """
442 Confirm the value is a valid date and time. This is derived from
443 :class:`RangeValidator` so `min` and `max` can be specified.
445 `format`
446 The expected date/time format. The format must be specified using
447 the same syntax as the Python strftime function.
448 """
449 msgs = {
450 'baddatetime': _('Must follow date/time format $format_str'),
451 'toosmall': ('date_toosmall', _('Cannot be earlier than $min_str')),
452 'toobig': ('date_toobig', _('Cannot be later than $max_str')),
453 }
454 format = '%Y-%m-%d %H:%M'
456 format_tbl = {
457 'd': 'day',
458 'H': 'hour',
459 'I': 'hour',
460 'm': 'month',
461 'M': 'minute',
462 'S': 'second',
463 'y': 'year',
464 'Y': 'year',
465 }
467 @property
468 def format_str(self):
469 f = lambda m: self.format_tbl.get(m.group(1), '')
470 return re.sub('%(.)', f, self.format)
472 @property
473 def min_str(self):
474 return self.min.strftime(self.format)
476 @property
477 def max_str(self):
478 return self.max.strftime(self.format)
480 def _convert_to_python(self, value, state=None):
481 if isinstance(value, datetime.datetime):
482 return value
483 if isinstance(value, datetime.date):
484 return datetime.datetime(value.year, value.month, value.day)
485 try:
486 return datetime.datetime.strptime(value, self.format)
487 except ValueError:
488 raise ValidationError('baddatetime', self)
490 def _validate_python(self, value, state=None):
491 super(DateTimeValidator, self)._validate_python(value, state)
493 def _convert_from_python(self, value, state=None):
494 return value.strftime(self.format)
497class DateValidator(DateTimeValidator):
498 """
499 Confirm the value is a valid date.
501 Just like :class:`DateTimeValidator`, but without the time component.
502 """
503 msgs = {
504 'baddatetime': (
505 'baddate', _('Must follow date format $format_str')),
506 }
507 format = '%Y-%m-%d'
509 def _convert_to_python(self, value, state=None):
510 value = super(DateValidator, self)._convert_to_python(value)
511 return value.date()
514class RegexValidator(Validator):
515 """
516 Confirm the value matches a regular expression.
518 `regex`
519 A Python regular expression object, generated like
520 ``re.compile('^\w+$')``
521 """
522 msgs = {
523 'badregex': _('Invalid value'),
524 }
525 regex = None
527 def _validate_python(self, value, state=None):
528 if not self.regex.search(value):
529 raise ValidationError('badregex', self)
532class EmailValidator(RegexValidator):
533 """
534 Confirm the value is a valid email address.
535 """
536 msgs = {
537 'badregex': ('bademail', _('Must be a valid email address')),
538 }
539 regex = re.compile('^[\w\-.]+@[\w\-.]+$')
542class UrlValidator(RegexValidator):
543 """
544 Confirm the value is a valid URL.
545 """
546 msgs = {
547 'regex': ('badurl', _('Must be a valid URL')),
548 }
549 regex = re.compile('^https?://', re.IGNORECASE)
552class IpAddressValidator(Validator):
553 """
554 Confirm the value is a valid IP4 address, or network block.
556 `allow_netblock`
557 Allow the IP address to include a network block (default: False)
559 `require_netblock`
560 Require the IP address to include a network block (default: False)
561 """
562 allow_netblock = False
563 require_netblock = False
565 msgs = {
566 'badipaddress': _('Must be a valid IP address'),
567 'badnetblock': _('Must be a valid IP network block'),
568 }
569 regex = re.compile('^(\d+)\.(\d+)\.(\d+)\.(\d+)(/(\d+))?$')
571 def _validate_python(self, value, state=None):
572 m = self.regex.search(value)
573 if not m or any(not(0 <= int(g) <= 255) for g in m.groups()[:4]):
574 raise ValidationError('badipaddress', self)
575 if m.group(6):
576 if not self.allow_netblock:
577 raise ValidationError('badipaddress', self)
578 if not (0 <= int(m.group(6)) <= 32):
579 raise ValidationError('badnetblock', self)
580 elif self.require_netblock:
581 raise ValidationError('badnetblock', self)
584class UUIDValidator(Validator):
585 """
586 Confirm the value is a valid uuid and convert to uuid.UUID.
587 """
588 msgs = {
589 'badregex': ('baduuid', _('Value not recognised as a UUID')),
590 }
592 regex = re.compile(\
593 '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
595 def _validate_python(self, value, state=None):
596 if not self.regex.search(str(value)):
597 raise ValidationError('badregex', self)
599 def _convert_to_python(self, value, state=None):
600 try:
601 return uuid.UUID( "{%s}" % value )
603 except ValueError:
604 raise ValidationError('baduuid', self)
606 def _convert_from_python(self, value, state=None):
607 return str(value)
610class MatchValidator(Validator):
611 """Confirm a field matches another field
613 `other_field`
614 Name of the sibling field this must match
615 `pass_on_invalid`
616 Pass validation if sibling field is Invalid
617 """
618 msgs = {
619 'mismatch': _("Must match $other_field_str"),
620 'notfound': _("$other_field_str field is not found"),
621 'invalid': _("$other_field_str field is invalid"),
622 }
624 def __init__(self, other_field, pass_on_invalid=False, **kw):
625 super(MatchValidator, self).__init__(**kw)
626 self.other_field = other_field
627 self.pass_on_invalid = pass_on_invalid
629 @property
630 def other_field_str(self):
631 return capitalize(util.name2label(self.other_field).lower())
633 def _validate_python(self, value, state):
634 if isinstance(state, dict):
635 # Backward compatibility
636 values = state
637 else:
638 values = state.full_dict
640 if self.other_field not in values:
641 raise ValidationError('notfound', self)
643 other_value = values[self.other_field]
645 if other_value is Invalid:
646 if not self.pass_on_invalid:
647 raise ValidationError('invalid', self)
648 elif value != other_value:
649 raise ValidationError('mismatch', self)
651 def _is_empty(self, value):
652 return self.required and super(MatchValidator, self)._is_empty(value)
655class CompoundValidator(Validator):
656 """ Base class for compound validators.
658 Child classes :class:`Any` and :class:`All` take validators as arguments
659 and use them to validate "value". In case the validation fails, they
660 raise a ValidationError with a compound message.
662 >>> v = All(StringLengthValidator(max=50), EmailValidator, required=True)
663 """
665 def __init__(self, *args, **kw):
666 super(CompoundValidator, self).__init__(**kw)
668 self.validators = []
669 for arg in args:
670 if isinstance(arg, validator_classes):
671 self.validators.append(arg)
672 elif issubclass(arg, validator_classes):
673 self.validators.append(arg())
674 if getattr(arg, 'required', False):
675 self.required = True
678class All(CompoundValidator):
679 """
680 Confirm all validators passed as arguments are valid.
681 """
683 def _validate_python(self, value, state=None):
684 msg = []
685 for validator in self.validators:
686 try:
687 validator._validate_python(value, state)
688 except ValidationError as e:
689 msg.append(six.text_type(e))
690 if msg:
691 msgset = set()
692 msg = ', '.join(m for m in msg
693 if m not in msgset and not msgset.add(m))
694 raise ValidationError(msg, self)
697class Any(CompoundValidator):
698 """
699 Confirm at least one of the validators passed as arguments is valid.
700 """
702 def _validate_python(self, value, state=None):
703 msg = []
704 for validator in self.validators:
705 try:
706 validator._validate_python(value, state)
707 except ValidationError as e:
708 msg.append(six.text_type(e))
709 if len(msg) == len(self.validators):
710 msgset = set()
711 msg = ', '.join(m for m in msg
712 if m not in msgset and not msgset.add(m))
713 raise ValidationError(msg, self)