PageRenderTime 56ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/tw2/core/validation.py

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