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

/tw2/core/validation.py

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