PageRenderTime 55ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/tw2/core/validation.py

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