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

/tw2/core/validation.py

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