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

/tw2/core/validation.py

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