PageRenderTime 60ms CodeModel.GetById 32ms RepoModel.GetById 1ms app.codeStats 0ms

/tw2/core/validation.py

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