PageRenderTime 33ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/tw2/core/validation.py

https://bitbucket.org/sanjiv/tw2.core
Python | 454 lines | 440 code | 5 blank | 9 comment | 7 complexity | 288acd9b9247f0fd8ed25618fecae035 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 isinstance(value, basestring):
  125. try:
  126. if self.encoding:
  127. value = value.decode(self.encoding)
  128. except (UnicodeDecodeError, UnicodeEncodeError), e:
  129. raise ValidationError('decode', self)
  130. if self.strip:
  131. value = value.strip()
  132. return value
  133. def validate_python(self, value, state=None):
  134. if self.required and not value:
  135. raise ValidationError('required', self)
  136. def from_python(self, value):
  137. return value
  138. def __repr__(self):
  139. _bool = ['False', 'True']
  140. return ("Validator(required=%s, strip=%s, encoding='%s')" %
  141. (_bool[int(self.required)], _bool[int(self.strip)], self.encoding))
  142. def clone(self, **kw):
  143. nself = copy.copy(self)
  144. for k in kw:
  145. setattr(nself, k, kw[k])
  146. return nself
  147. class LengthValidator(Validator):
  148. """
  149. Confirm a value is of a suitable length. Usually you'll use
  150. :class:`StringLengthValidator` or :class:`ListLengthValidator` instead.
  151. `min`
  152. Minimum length (default: None)
  153. `max`
  154. Maximum length (default: None)
  155. """
  156. msgs = {
  157. 'tooshort': 'Value is too short',
  158. 'toolong': 'Value is too long',
  159. }
  160. min = None
  161. max = None
  162. def validate_python(self, value, state=None):
  163. super(LengthValidator, self).validate_python
  164. if self.min and len(value) < self.min:
  165. raise ValidationError('tooshort', self)
  166. if self.max and len(value) > self.max:
  167. raise ValidationError('toolong', self)
  168. class StringLengthValidator(LengthValidator):
  169. """
  170. Check a string is a suitable length. The only difference to LengthValidator
  171. is that the messages are worded differently.
  172. """
  173. msgs = {
  174. 'tooshort': ('string_tooshort', 'Must be at least $min characters'),
  175. 'toolong': ('string_toolong', 'Cannot be longer than $max characters'),
  176. }
  177. class ListLengthValidator(LengthValidator):
  178. """
  179. Check a list is a suitable length. The only difference to LengthValidator
  180. is that the messages are worded differently.
  181. """
  182. msgs = {
  183. 'tooshort': ('list_tooshort', 'Select at least $min'),
  184. 'toolong': ('list_toolong', 'Select no more than $max'),
  185. }
  186. class RangeValidator(Validator):
  187. """
  188. Confirm a value is within an appropriate range. This is not usually used
  189. directly, but other validators are derived from this.
  190. `min`
  191. Minimum value (default: None)
  192. `max`
  193. Maximum value (default: None)
  194. """
  195. msgs = {
  196. 'toosmall': 'Must be at least $min',
  197. 'toobig': 'Cannot be more than $max',
  198. }
  199. min = None
  200. max = None
  201. def validate_python(self, value, state=None):
  202. super(RangeValidator, self).validate_python(value)
  203. if self.min and value < self.min:
  204. raise ValidationError('toosmall', self)
  205. if self.max and value > self.max:
  206. raise ValidationError('toobig', self)
  207. class IntValidator(RangeValidator):
  208. """
  209. Confirm the value is an integer. This is derived from :class:`RangeValidator`
  210. so `min` and `max` can be specified.
  211. """
  212. msgs = {
  213. 'notint': 'Must be an integer',
  214. }
  215. def to_python(self, value):
  216. value = super(IntValidator, self).to_python(value)
  217. try:
  218. if value is None or str(value) == '':
  219. return None
  220. else:
  221. return int(value)
  222. except ValueError:
  223. raise ValidationError('notint', self)
  224. def validate_python(self, value, state=None):
  225. # avoid super().validate_python, as it sees int(0) as missing
  226. value = self.to_python(value) # TBD: I wanted to avoid this; needed to make unit tests pass
  227. if self.required and value is None:
  228. raise ValidationError('required', self)
  229. if value is not None:
  230. if self.min and value < self.min:
  231. raise ValidationError('toosmall', self)
  232. if self.max and value > self.max:
  233. raise ValidationError('toobig', self)
  234. def from_python(self, value):
  235. return str(value)
  236. class BoolValidator(RangeValidator):
  237. """
  238. Convert a value to a boolean. This is particularly intended to handle
  239. check boxes.
  240. """
  241. msgs = {
  242. 'required': ('bool_required', 'You must select this')
  243. }
  244. def to_python(self, value):
  245. value = super(BoolValidator, self).to_python(value)
  246. return str(value).lower() in ('on', 'yes', 'true', '1')
  247. class OneOfValidator(Validator):
  248. """
  249. Confirm the value is one of a list of acceptable values. This is useful for
  250. confirming that select fields have not been tampered with by a user.
  251. `values`
  252. Acceptable values
  253. """
  254. msgs = {
  255. 'notinlist': 'Invalid value',
  256. }
  257. values = []
  258. def validate_python(self, value, state=None):
  259. super(OneOfValidator, self).validate_python(value)
  260. if value not in self.values:
  261. raise ValidationError('notinlist', self)
  262. class DateValidator(RangeValidator):
  263. """
  264. Confirm the value is a valid date. This is derived from :class:`RangeValidator`
  265. so `min` and `max` can be specified.
  266. `format`
  267. The expected date format. The format must be specified using the same
  268. syntax as the Python strftime function.
  269. """
  270. msgs = {
  271. 'baddate': 'Must follow date format $format_str',
  272. 'toosmall': ('date_toosmall', 'Cannot be earlier than $min_str'),
  273. 'toobig': ('date_toobig', 'Cannot be later than $max_str'),
  274. }
  275. format = '%d/%m/%Y'
  276. format_tbl = {'d':'day', 'H':'hour', 'I':'hour', 'm':'month', 'M':'minute',
  277. 'S':'second', 'y':'year', 'Y':'year'}
  278. @property
  279. def format_str(self):
  280. return re.sub('%(.)', lambda m: self.format_tbl.get(m.group(1), ''), self.format)
  281. @property
  282. def min_str(self):
  283. return self.min.strftime(self.format)
  284. @property
  285. def max_str(self):
  286. return self.max.strftime(self.format)
  287. def to_python(self, value):
  288. value = super(DateValidator, self).to_python(value)
  289. try:
  290. date = time.strptime(value, self.format)
  291. return datetime.date(date.tm_year, date.tm_mon, date.tm_mday)
  292. except ValueError:
  293. raise ValidationError('baddate', self)
  294. def from_python(self, value):
  295. return value.strftime(self.format)
  296. class DateTimeValidator(DateValidator):
  297. """
  298. Confirm the value is a valid date and time; otherwise just like :class:`DateValidator`.
  299. """
  300. msgs = {
  301. 'baddate': ('baddatetime', 'Must follow date/time format $format_str'),
  302. }
  303. format = '%d/%m/%Y %H:%M'
  304. def to_python(self, value):
  305. if value is None:
  306. return value
  307. try:
  308. return datetime.datetime.strptime(value, self.format)
  309. except ValueError:
  310. raise ValidationError('baddate', self)
  311. def from_python(self, value):
  312. return value.strftime(self.format)
  313. class RegexValidator(Validator):
  314. """
  315. Confirm the value matches a regular expression.
  316. `regex`
  317. A Python regular expression object, generated like ``re.compile('^\w+$')``
  318. """
  319. msgs = {
  320. 'badregex': 'Invalid value',
  321. }
  322. regex = None
  323. def validate_python(self, value, state=None):
  324. super(RegexValidator, self).validate_python(value)
  325. if value and not self.regex.search(value):
  326. raise ValidationError('badregex', self)
  327. class EmailValidator(RegexValidator):
  328. """
  329. Confirm the value is a valid email address.
  330. """
  331. msgs = {
  332. 'badregex': ('bademail', 'Must be a valid email address'),
  333. }
  334. regex = re.compile('^[\w\-.]+@[\w\-.]+$')
  335. class UrlValidator(RegexValidator):
  336. """
  337. Confirm the value is a valid URL.
  338. """
  339. msgs = {
  340. 'regex': ('badurl', 'Must be a valid URL'),
  341. }
  342. regex = re.compile('^https?://', re.IGNORECASE)
  343. class IpAddressValidator(Validator):
  344. """
  345. Confirm the value is a valid IP4 address.
  346. """
  347. msgs = {
  348. 'badipaddress': 'Must be a valid IP address',
  349. }
  350. regex = re.compile('^(\d+)\.(\d+)\.(\d+)\.(\d+)$', re.IGNORECASE)
  351. def validate_python(self, value, state=None):
  352. m = self.regex.search(value)
  353. if not m or any(not(0 <= int(g) <= 255) for g in m.groups()):
  354. raise ValidationError('badipaddress', self)
  355. class MatchValidator(Validator):
  356. """
  357. Confirm two fields on a form match.
  358. """
  359. msgs = {
  360. 'mismatch': "$field1_str doesn't match $field2_str"
  361. }
  362. def __init__(self, field1, field2, **kw):
  363. super(MatchValidator, self).__init__(**kw)
  364. self.field1 = field1
  365. self.field2 = field2
  366. @property
  367. def field1_str(self):
  368. return string.capitalize(util.name2label(self.field1).lower())
  369. @property
  370. def field2_str(self):
  371. return util.name2label(self.field2).lower()
  372. def validate_python(self, value, state=None):
  373. v1 = value.get(self.field1, None)
  374. v2 = value.get(self.field2, None)
  375. if v1 is not Invalid and v2 is not Invalid and v1 != v2:
  376. raise ValidationError('mismatch', self)