PageRenderTime 56ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/tw2/core/validation.py

https://github.com/lukasgraf/tw2.core
Python | 604 lines | 581 code | 13 blank | 10 comment | 11 complexity | ebca0ebb8e7f3e469f96bf7d8819076e MD5 | raw file
  1. import core
  2. import re
  3. import util
  4. import string
  5. import time
  6. import datetime
  7. import copy
  8. import functools
  9. import webob
  10. from i18n import _
  11. # This hack helps work with different versions of WebOb
  12. if not hasattr(webob, 'MultiDict'):
  13. webob.MultiDict = webob.multidict.MultiDict
  14. try:
  15. import formencode
  16. except ImportError:
  17. formencode = None
  18. class Invalid(object):
  19. pass
  20. class EmptyField(object):
  21. pass
  22. if formencode:
  23. class BaseValidationError(core.WidgetError, formencode.Invalid):
  24. def __init__(self, msg):
  25. formencode.Invalid.__init__(self, msg, None, None)
  26. else:
  27. class BaseValidationError(core.WidgetError):
  28. pass
  29. class ValidationError(BaseValidationError):
  30. """Invalid data was encountered during validation.
  31. The constructor can be passed a short message name, which is looked up in
  32. a validator's :attr:`msgs` dictionary. Any values in this, like
  33. ``$val``` are substituted with that attribute from the validator. An
  34. explicit validator instance can be passed to the constructor, or this
  35. defaults to :class:`Validator` otherwise.
  36. """
  37. def __init__(self, msg, validator=None, widget=None):
  38. self.widget = widget
  39. validator = validator or Validator
  40. mw = core.request_local().get('middleware')
  41. if isinstance(validator, Validator):
  42. msg = validator.msg_rewrites.get(msg, msg)
  43. if mw and msg in mw.config.validator_msgs:
  44. msg = mw.config.validator_msgs[msg]
  45. elif hasattr(validator, 'msgs') and msg in validator.msgs:
  46. msg = validator.msgs.get(msg, msg)
  47. # In the event that the user specified a form-wide validator but
  48. # they did not specify a childerror message, show no error.
  49. if msg == 'childerror':
  50. msg = ''
  51. msg = re.sub('\$(\w+)',
  52. lambda m: str(getattr(validator, m.group(1))), unicode(msg))
  53. super(ValidationError, self).__init__(msg)
  54. @property
  55. def message(self):
  56. """ Added for backwards compatibility. Synonymous with `msg` """
  57. return self.msg
  58. def safe_validate(validator, value, state=None):
  59. try:
  60. value = validator.to_python(value, state=None)
  61. validator.validate_python(value)
  62. return value
  63. except ValidationError:
  64. return Invalid
  65. catch = ValidationError
  66. if formencode:
  67. catch = formencode.Invalid
  68. def catch_errors(fn):
  69. @functools.wraps(fn)
  70. def wrapper(self, *args, **kw):
  71. try:
  72. d = fn(self, *args, **kw)
  73. return d
  74. except catch, e:
  75. e_msg = unicode(e)
  76. if self:
  77. self.error_msg = e_msg
  78. raise ValidationError(e_msg, widget=self)
  79. return wrapper
  80. def unflatten_params(params):
  81. """This performs the first stage of validation. It takes a dictionary where
  82. some keys will be compound names, such as "form:subform:field" and converts
  83. this into a nested dict/list structure. It also performs unicode decoding,
  84. with the encoding specified in the middleware config.
  85. """
  86. if isinstance(params, webob.MultiDict):
  87. params = params.mixed()
  88. mw = core.request_local().get('middleware')
  89. enc = mw.config.encoding if mw else 'utf-8'
  90. try:
  91. for p in params:
  92. if isinstance(params[p], str):
  93. params[p] = params[p].decode(enc)
  94. except UnicodeDecodeError:
  95. raise ValidationError('decode', Validator(encoding=enc))
  96. out = {}
  97. for pname in params:
  98. dct = out
  99. elements = pname.split(':')
  100. for e in elements[:-1]:
  101. dct = dct.setdefault(e, {})
  102. dct[elements[-1]] = params[pname]
  103. numdict_to_list(out)
  104. return out
  105. number_re = re.compile('^\d+$')
  106. def numdict_to_list(dct):
  107. for k, v in dct.items():
  108. if isinstance(v, dict):
  109. numdict_to_list(v)
  110. if all(number_re.match(k) for k in v):
  111. dct[k] = [v[x] for x in sorted(v, key=int)]
  112. class ValidatorMeta(type):
  113. """Metaclass for :class:`Validator`.
  114. This makes the :attr:`msgs` dict copy from its base class.
  115. """
  116. def __new__(meta, name, bases, dct):
  117. if 'msgs' in dct:
  118. msgs = {}
  119. rewrites = {}
  120. for b in bases:
  121. if hasattr(b, 'msgs'):
  122. msgs.update(b.msgs)
  123. msgs.update(dct['msgs'])
  124. for m, d in msgs.items():
  125. if isinstance(d, tuple):
  126. msgs[d[0]] = d[1]
  127. rewrites[m] = d[0]
  128. del msgs[m]
  129. dct['msgs'] = msgs
  130. dct['msg_rewrites'] = rewrites
  131. return type.__new__(meta, name, bases, dct)
  132. class Validator(object):
  133. """Base class for validators
  134. `required`
  135. Whether empty values are forbidden in this field. (default: False)
  136. `strip`
  137. Whether to strip leading and trailing space from the input, before
  138. any other validation. (default: True)
  139. To create your own validators, sublass this class, and override any of:
  140. :meth:`to_python`, :meth:`validate_python`, or :meth:`from_python`.
  141. """
  142. __metaclass__ = ValidatorMeta
  143. msgs = {
  144. 'required': _('Enter a value'),
  145. 'decode': _('Received in wrong character set; should be $encoding'),
  146. 'corrupt': _('Form submission received corrupted; please try again'),
  147. 'childerror': '', # Children of this widget have errors
  148. }
  149. required = False
  150. strip = True
  151. def __init__(self, **kw):
  152. for k in kw:
  153. setattr(self, k, kw[k])
  154. def to_python(self, value, state=None):
  155. if self.required and value is None:
  156. raise ValidationError('required', self)
  157. if isinstance(value, basestring) and self.strip:
  158. value = value.strip()
  159. return value
  160. def validate_python(self, value, state=None):
  161. if self.required and not value:
  162. raise ValidationError('required', self)
  163. def from_python(self, value, state=None):
  164. return value
  165. def __repr__(self):
  166. _bool = ['False', 'True']
  167. return ("Validator(required=%s, strip=%s)" %
  168. (_bool[int(self.required)], _bool[int(self.strip)]))
  169. def clone(self, **kw):
  170. nself = copy.copy(self)
  171. for k in kw:
  172. setattr(nself, k, kw[k])
  173. return nself
  174. class BlankValidator(Validator):
  175. """
  176. Always returns EmptyField. This is the default for hidden fields,
  177. so their values are not included in validated data.
  178. """
  179. def to_python(self, value, state=None):
  180. return EmptyField
  181. class LengthValidator(Validator):
  182. """
  183. Confirm a value is of a suitable length. Usually you'll use
  184. :class:`StringLengthValidator` or :class:`ListLengthValidator` instead.
  185. `min`
  186. Minimum length (default: None)
  187. `max`
  188. Maximum length (default: None)
  189. """
  190. msgs = {
  191. 'tooshort': _('Value is too short'),
  192. 'toolong': _('Value is too long'),
  193. }
  194. min = None
  195. max = None
  196. def validate_python(self, value, state=None):
  197. super(LengthValidator, self).validate_python(value, state)
  198. if self.min and len(value) < self.min:
  199. raise ValidationError('tooshort', self)
  200. if self.max and len(value) > self.max:
  201. raise ValidationError('toolong', self)
  202. class StringLengthValidator(LengthValidator):
  203. """
  204. Check a string is a suitable length. The only difference to LengthValidator
  205. is that the messages are worded differently.
  206. """
  207. msgs = {
  208. 'tooshort': (
  209. 'string_tooshort', _('Must be at least $min characters')),
  210. 'toolong': (
  211. 'string_toolong', _('Cannot be longer than $max characters')),
  212. }
  213. class ListLengthValidator(LengthValidator):
  214. """
  215. Check a list is a suitable length. The only difference to LengthValidator
  216. is that the messages are worded differently.
  217. """
  218. msgs = {
  219. 'tooshort': ('list_tooshort', _('Select at least $min')),
  220. 'toolong': ('list_toolong', _('Select no more than $max')),
  221. }
  222. class RangeValidator(Validator):
  223. """
  224. Confirm a value is within an appropriate range. This is not usually used
  225. directly, but other validators are derived from this.
  226. `min`
  227. Minimum value (default: None)
  228. `max`
  229. Maximum value (default: None)
  230. """
  231. msgs = {
  232. 'toosmall': _('Must be at least $min'),
  233. 'toobig': _('Cannot be more than $max'),
  234. }
  235. min = None
  236. max = None
  237. def validate_python(self, value, state=None):
  238. super(RangeValidator, self).validate_python(value, state)
  239. if self.min is not None and value < self.min:
  240. raise ValidationError('toosmall', self)
  241. if self.max is not None and value > self.max:
  242. raise ValidationError('toobig', self)
  243. class IntValidator(RangeValidator):
  244. """
  245. Confirm the value is an integer. This is derived from
  246. :class:`RangeValidator` so `min` and `max` can be specified.
  247. """
  248. msgs = {
  249. 'notint': _('Must be an integer'),
  250. }
  251. def to_python(self, value, state=None):
  252. value = super(IntValidator, self).to_python(value, state)
  253. try:
  254. if value is None or str(value) == '':
  255. return None
  256. else:
  257. return int(value)
  258. except ValueError:
  259. raise ValidationError('notint', self)
  260. def validate_python(self, value, state=None):
  261. if self.required and value is None:
  262. raise ValidationError('required', self)
  263. if value is not None:
  264. if self.min and value < self.min:
  265. raise ValidationError('toosmall', self)
  266. if self.max and value > self.max:
  267. raise ValidationError('toobig', self)
  268. def from_python(self, value, state=None):
  269. if value is None:
  270. return None
  271. else:
  272. return str(value)
  273. class BoolValidator(RangeValidator):
  274. """
  275. Convert a value to a boolean. This is particularly intended to handle
  276. check boxes.
  277. """
  278. msgs = {
  279. 'required': ('bool_required', _('You must select this'))
  280. }
  281. def to_python(self, value, state=None):
  282. value = super(BoolValidator, self).to_python(value, state)
  283. return str(value).lower() in ('on', 'yes', 'true', '1')
  284. class OneOfValidator(Validator):
  285. """
  286. Confirm the value is one of a list of acceptable values. This is useful for
  287. confirming that select fields have not been tampered with by a user.
  288. `values`
  289. Acceptable values
  290. """
  291. msgs = {
  292. 'notinlist': _('Invalid value'),
  293. }
  294. values = []
  295. def validate_python(self, value, state=None):
  296. super(OneOfValidator, self).validate_python(value, state)
  297. if value not in self.values:
  298. raise ValidationError('notinlist', self)
  299. class DateValidator(RangeValidator):
  300. """
  301. Confirm the value is a valid date. This is derived from
  302. :class:`RangeValidator` so `min` and `max` can be specified.
  303. `format`
  304. The expected date format. The format must be specified using the same
  305. syntax as the Python strftime function.
  306. """
  307. msgs = {
  308. 'baddate': _('Must follow date format $format_str'),
  309. 'toosmall': ('date_toosmall', _('Cannot be earlier than $min_str')),
  310. 'toobig': ('date_toobig', _('Cannot be later than $max_str')),
  311. }
  312. format = '%d/%m/%Y'
  313. format_tbl = {
  314. 'd': 'day',
  315. 'H': 'hour',
  316. 'I': 'hour',
  317. 'm': 'month',
  318. 'M': 'minute',
  319. 'S': 'second',
  320. 'y': 'year',
  321. 'Y': 'year',
  322. }
  323. @property
  324. def format_str(self):
  325. f = lambda m: self.format_tbl.get(m.group(1), '')
  326. return re.sub('%(.)', f, self.format)
  327. @property
  328. def min_str(self):
  329. return self.min.strftime(self.format)
  330. @property
  331. def max_str(self):
  332. return self.max.strftime(self.format)
  333. def to_python(self, value, state=None):
  334. value = super(DateValidator, self).to_python(value, state)
  335. if not value:
  336. return None
  337. try:
  338. date = time.strptime(value, self.format)
  339. return datetime.date(date.tm_year, date.tm_mon, date.tm_mday)
  340. except ValueError:
  341. raise ValidationError('baddate', self)
  342. def validate_python(self, value, state=None):
  343. super(DateValidator, self).validate_python(value, state)
  344. if self.required and not value:
  345. raise ValidationError('required', self)
  346. def from_python(self, value, state=None):
  347. return value and value.strftime(self.format) or ''
  348. class DateTimeValidator(DateValidator):
  349. """
  350. Confirm the value is a valid date and time; otherwise just like
  351. :class:`DateValidator`.
  352. """
  353. msgs = {
  354. 'baddate': (
  355. 'baddatetime', _('Must follow date/time format $format_str')),
  356. }
  357. format = '%d/%m/%Y %H:%M'
  358. def to_python(self, value, state=None):
  359. if not value:
  360. return None
  361. try:
  362. return datetime.datetime.strptime(value, self.format)
  363. except ValueError:
  364. raise ValidationError('baddate', self)
  365. class RegexValidator(Validator):
  366. """
  367. Confirm the value matches a regular expression.
  368. `regex`
  369. A Python regular expression object, generated like
  370. ``re.compile('^\w+$')``
  371. """
  372. msgs = {
  373. 'badregex': _('Invalid value'),
  374. }
  375. regex = None
  376. def validate_python(self, value, state=None):
  377. super(RegexValidator, self).validate_python(value, state)
  378. if value and not self.regex.search(value):
  379. raise ValidationError('badregex', self)
  380. class EmailValidator(RegexValidator):
  381. """
  382. Confirm the value is a valid email address.
  383. """
  384. msgs = {
  385. 'badregex': ('bademail', _('Must be a valid email address')),
  386. }
  387. regex = re.compile('^[\w\-.]+@[\w\-.]+$')
  388. class UrlValidator(RegexValidator):
  389. """
  390. Confirm the value is a valid URL.
  391. """
  392. msgs = {
  393. 'regex': ('badurl', _('Must be a valid URL')),
  394. }
  395. regex = re.compile('^https?://', re.IGNORECASE)
  396. class IpAddressValidator(Validator):
  397. """
  398. Confirm the value is a valid IP4 address, or network block.
  399. `allow_netblock`
  400. Allow the IP address to include a network block (default: False)
  401. `require_netblock`
  402. Require the IP address to include a network block (default: False)
  403. """
  404. allow_netblock = False
  405. require_netblock = False
  406. msgs = {
  407. 'badipaddress': _('Must be a valid IP address'),
  408. 'badnetblock': _('Must be a valid IP network block'),
  409. }
  410. regex = re.compile('^(\d+)\.(\d+)\.(\d+)\.(\d+)(/(\d+))?$')
  411. def validate_python(self, value, state=None):
  412. if value:
  413. m = self.regex.search(value)
  414. if not m or any(not(0 <= int(g) <= 255) for g in m.groups()[:4]):
  415. raise ValidationError('badipaddress', self)
  416. if m.group(6):
  417. if not self.allow_netblock:
  418. raise ValidationError('badipaddress', self)
  419. if not (0 <= int(m.group(6)) <= 32):
  420. raise ValidationError('badnetblock', self)
  421. elif self.require_netblock:
  422. raise ValidationError('badnetblock', self)
  423. class MatchValidator(Validator):
  424. """Confirm a field matches another field
  425. `other_field`
  426. Name of the sibling field this must match
  427. """
  428. msgs = {
  429. 'mismatch': _("Must match $other_field_str"),
  430. }
  431. def __init__(self, other_field, **kw):
  432. super(MatchValidator, self).__init__(**kw)
  433. self.other_field = other_field
  434. @property
  435. def other_field_str(self):
  436. return string.capitalize(util.name2label(self.other_field).lower())
  437. def validate_python(self, value, state):
  438. super(MatchValidator, self).validate_python(value, state)
  439. if value != state[self.other_field]:
  440. raise ValidationError('mismatch', self)
  441. class CompoundValidator(Validator):
  442. """ Base class for compound validators.
  443. Child classes :class:`Any` and :class`All` take validators as arguments
  444. and use them to validate "value". In case the validation fails, they
  445. raise a ValidationError with a compound message.
  446. >>> v = All(StringLengthValidator(max=50), EmailValidator, required=True)
  447. """
  448. def __init__(self, *args, **kw):
  449. super(CompoundValidator, self).__init__(**kw)
  450. self.validators = []
  451. for arg in args:
  452. if isinstance(arg, Validator):
  453. self.validators.append(arg)
  454. elif issubclass(arg, Validator):
  455. self.validators.append(arg())
  456. if getattr(arg, 'required', False):
  457. self.required = True
  458. class All(CompoundValidator):
  459. """
  460. Confirm all validators passed as arguments are valid.
  461. """
  462. def validate_python(self, value, state=None):
  463. super(All, self).validate_python(value)
  464. msg = []
  465. for validator in self.validators:
  466. try:
  467. validator.validate_python(value, state)
  468. except ValidationError, e:
  469. msg.append(str(e))
  470. if msg:
  471. raise ValidationError(' and '.join(set(msg)), self)
  472. class Any(CompoundValidator):
  473. """
  474. Confirm at least one of the validators passed as arguments is valid.
  475. """
  476. def validate_python(self, value, state=None):
  477. super(Any, self).validate_python(value)
  478. msg = []
  479. for validator in self.validators:
  480. try:
  481. validator.validate_python(value, state)
  482. except ValidationError, e:
  483. msg.append(str(e))
  484. if len(msg) == len(self.validators):
  485. raise ValidationError(' or '.join(set(msg)), self)