PageRenderTime 45ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/tw2/core/validation.py

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