PageRenderTime 33ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

/tw2/core/validation.py

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