PageRenderTime 67ms CodeModel.GetById 28ms RepoModel.GetById 1ms app.codeStats 0ms

/unladen_swallow/lib/google_appengine/lib/django/django/core/validators.py

https://bitbucket.org/csenger/benchmarks
Python | 573 lines | 527 code | 21 blank | 25 comment | 33 complexity | 232f2e78e1c0220385c34f52b308f3f7 MD5 | raw file
Possible License(s): BSD-3-Clause, Apache-2.0, GPL-2.0
  1. """
  2. A library of validators that return None and raise ValidationError when the
  3. provided data isn't valid.
  4. Validators may be callable classes, and they may have an 'always_test'
  5. attribute. If an 'always_test' attribute exists (regardless of value), the
  6. validator will *always* be run, regardless of whether its associated
  7. form field is required.
  8. """
  9. import urllib2
  10. from django.conf import settings
  11. from django.utils.translation import gettext, gettext_lazy, ngettext
  12. from django.utils.functional import Promise, lazy
  13. import re
  14. _datere = r'\d{4}-\d{1,2}-\d{1,2}'
  15. _timere = r'(?:[01]?[0-9]|2[0-3]):[0-5][0-9](?::[0-5][0-9])?'
  16. alnum_re = re.compile(r'^\w+$')
  17. alnumurl_re = re.compile(r'^[-\w/]+$')
  18. ansi_date_re = re.compile('^%s$' % _datere)
  19. ansi_time_re = re.compile('^%s$' % _timere)
  20. ansi_datetime_re = re.compile('^%s %s$' % (_datere, _timere))
  21. email_re = re.compile(
  22. r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom
  23. r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string
  24. r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain
  25. integer_re = re.compile(r'^-?\d+$')
  26. ip4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$')
  27. phone_re = re.compile(r'^[A-PR-Y0-9]{3}-[A-PR-Y0-9]{3}-[A-PR-Y0-9]{4}$', re.IGNORECASE)
  28. slug_re = re.compile(r'^[-\w]+$')
  29. url_re = re.compile(r'^https?://\S+$')
  30. lazy_inter = lazy(lambda a,b: str(a) % b, str)
  31. class ValidationError(Exception):
  32. def __init__(self, message):
  33. "ValidationError can be passed a string or a list."
  34. if isinstance(message, list):
  35. self.messages = message
  36. else:
  37. assert isinstance(message, (basestring, Promise)), ("%s should be a string" % repr(message))
  38. self.messages = [message]
  39. def __str__(self):
  40. # This is needed because, without a __str__(), printing an exception
  41. # instance would result in this:
  42. # AttributeError: ValidationError instance has no attribute 'args'
  43. # See http://www.python.org/doc/current/tut/node10.html#handling
  44. return str(self.messages)
  45. class CriticalValidationError(Exception):
  46. def __init__(self, message):
  47. "ValidationError can be passed a string or a list."
  48. if isinstance(message, list):
  49. self.messages = message
  50. else:
  51. assert isinstance(message, (basestring, Promise)), ("'%s' should be a string" % message)
  52. self.messages = [message]
  53. def __str__(self):
  54. return str(self.messages)
  55. def isAlphaNumeric(field_data, all_data):
  56. if not alnum_re.search(field_data):
  57. raise ValidationError, gettext("This value must contain only letters, numbers and underscores.")
  58. def isAlphaNumericURL(field_data, all_data):
  59. if not alnumurl_re.search(field_data):
  60. raise ValidationError, gettext("This value must contain only letters, numbers, underscores, dashes or slashes.")
  61. def isSlug(field_data, all_data):
  62. if not slug_re.search(field_data):
  63. raise ValidationError, gettext("This value must contain only letters, numbers, underscores or hyphens.")
  64. def isLowerCase(field_data, all_data):
  65. if field_data.lower() != field_data:
  66. raise ValidationError, gettext("Uppercase letters are not allowed here.")
  67. def isUpperCase(field_data, all_data):
  68. if field_data.upper() != field_data:
  69. raise ValidationError, gettext("Lowercase letters are not allowed here.")
  70. def isCommaSeparatedIntegerList(field_data, all_data):
  71. for supposed_int in field_data.split(','):
  72. try:
  73. int(supposed_int)
  74. except ValueError:
  75. raise ValidationError, gettext("Enter only digits separated by commas.")
  76. def isCommaSeparatedEmailList(field_data, all_data):
  77. """
  78. Checks that field_data is a string of e-mail addresses separated by commas.
  79. Blank field_data values will not throw a validation error, and whitespace
  80. is allowed around the commas.
  81. """
  82. for supposed_email in field_data.split(','):
  83. try:
  84. isValidEmail(supposed_email.strip(), '')
  85. except ValidationError:
  86. raise ValidationError, gettext("Enter valid e-mail addresses separated by commas.")
  87. def isValidIPAddress4(field_data, all_data):
  88. if not ip4_re.search(field_data):
  89. raise ValidationError, gettext("Please enter a valid IP address.")
  90. def isNotEmpty(field_data, all_data):
  91. if field_data.strip() == '':
  92. raise ValidationError, gettext("Empty values are not allowed here.")
  93. def isOnlyDigits(field_data, all_data):
  94. if not field_data.isdigit():
  95. raise ValidationError, gettext("Non-numeric characters aren't allowed here.")
  96. def isNotOnlyDigits(field_data, all_data):
  97. if field_data.isdigit():
  98. raise ValidationError, gettext("This value can't be comprised solely of digits.")
  99. def isInteger(field_data, all_data):
  100. # This differs from isOnlyDigits because this accepts the negative sign
  101. if not integer_re.search(field_data):
  102. raise ValidationError, gettext("Enter a whole number.")
  103. def isOnlyLetters(field_data, all_data):
  104. if not field_data.isalpha():
  105. raise ValidationError, gettext("Only alphabetical characters are allowed here.")
  106. def _isValidDate(date_string):
  107. """
  108. A helper function used by isValidANSIDate and isValidANSIDatetime to
  109. check if the date is valid. The date string is assumed to already be in
  110. YYYY-MM-DD format.
  111. """
  112. from datetime import date
  113. # Could use time.strptime here and catch errors, but datetime.date below
  114. # produces much friendlier error messages.
  115. year, month, day = map(int, date_string.split('-'))
  116. # This check is needed because strftime is used when saving the date
  117. # value to the database, and strftime requires that the year be >=1900.
  118. if year < 1900:
  119. raise ValidationError, gettext('Year must be 1900 or later.')
  120. try:
  121. date(year, month, day)
  122. except ValueError, e:
  123. msg = gettext('Invalid date: %s') % gettext(str(e))
  124. raise ValidationError, msg
  125. def isValidANSIDate(field_data, all_data):
  126. if not ansi_date_re.search(field_data):
  127. raise ValidationError, gettext('Enter a valid date in YYYY-MM-DD format.')
  128. _isValidDate(field_data)
  129. def isValidANSITime(field_data, all_data):
  130. if not ansi_time_re.search(field_data):
  131. raise ValidationError, gettext('Enter a valid time in HH:MM format.')
  132. def isValidANSIDatetime(field_data, all_data):
  133. if not ansi_datetime_re.search(field_data):
  134. raise ValidationError, gettext('Enter a valid date/time in YYYY-MM-DD HH:MM format.')
  135. _isValidDate(field_data.split()[0])
  136. def isValidEmail(field_data, all_data):
  137. if not email_re.search(field_data):
  138. raise ValidationError, gettext('Enter a valid e-mail address.')
  139. def isValidImage(field_data, all_data):
  140. """
  141. Checks that the file-upload field data contains a valid image (GIF, JPG,
  142. PNG, possibly others -- whatever the Python Imaging Library supports).
  143. """
  144. from PIL import Image
  145. from cStringIO import StringIO
  146. try:
  147. content = field_data['content']
  148. except TypeError:
  149. raise ValidationError, gettext("No file was submitted. Check the encoding type on the form.")
  150. try:
  151. Image.open(StringIO(content))
  152. except IOError: # Python Imaging Library doesn't recognize it as an image
  153. raise ValidationError, gettext("Upload a valid image. The file you uploaded was either not an image or a corrupted image.")
  154. def isValidImageURL(field_data, all_data):
  155. uc = URLMimeTypeCheck(('image/jpeg', 'image/gif', 'image/png'))
  156. try:
  157. uc(field_data, all_data)
  158. except URLMimeTypeCheck.InvalidContentType:
  159. raise ValidationError, gettext("The URL %s does not point to a valid image.") % field_data
  160. def isValidPhone(field_data, all_data):
  161. if not phone_re.search(field_data):
  162. raise ValidationError, gettext('Phone numbers must be in XXX-XXX-XXXX format. "%s" is invalid.') % field_data
  163. def isValidQuicktimeVideoURL(field_data, all_data):
  164. "Checks that the given URL is a video that can be played by QuickTime (qt, mpeg)"
  165. uc = URLMimeTypeCheck(('video/quicktime', 'video/mpeg',))
  166. try:
  167. uc(field_data, all_data)
  168. except URLMimeTypeCheck.InvalidContentType:
  169. raise ValidationError, gettext("The URL %s does not point to a valid QuickTime video.") % field_data
  170. def isValidURL(field_data, all_data):
  171. if not url_re.search(field_data):
  172. raise ValidationError, gettext("A valid URL is required.")
  173. def isValidHTML(field_data, all_data):
  174. import urllib, urllib2
  175. try:
  176. u = urllib2.urlopen('http://validator.w3.org/check', urllib.urlencode({'fragment': field_data, 'output': 'xml'}))
  177. except:
  178. # Validator or Internet connection is unavailable. Fail silently.
  179. return
  180. html_is_valid = (u.headers.get('x-w3c-validator-status', 'Invalid') == 'Valid')
  181. if html_is_valid:
  182. return
  183. from xml.dom.minidom import parseString
  184. error_messages = [e.firstChild.wholeText for e in parseString(u.read()).getElementsByTagName('messages')[0].getElementsByTagName('msg')]
  185. raise ValidationError, gettext("Valid HTML is required. Specific errors are:\n%s") % "\n".join(error_messages)
  186. def isWellFormedXml(field_data, all_data):
  187. from xml.dom.minidom import parseString
  188. try:
  189. parseString(field_data)
  190. except Exception, e: # Naked except because we're not sure what will be thrown
  191. raise ValidationError, gettext("Badly formed XML: %s") % str(e)
  192. def isWellFormedXmlFragment(field_data, all_data):
  193. isWellFormedXml('<root>%s</root>' % field_data, all_data)
  194. def isExistingURL(field_data, all_data):
  195. try:
  196. headers = {
  197. "Accept" : "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
  198. "Accept-Language" : "en-us,en;q=0.5",
  199. "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7",
  200. "Connection" : "close",
  201. "User-Agent": settings.URL_VALIDATOR_USER_AGENT
  202. }
  203. req = urllib2.Request(field_data,None, headers)
  204. u = urllib2.urlopen(req)
  205. except ValueError:
  206. raise ValidationError, _("Invalid URL: %s") % field_data
  207. except urllib2.HTTPError, e:
  208. # 401s are valid; they just mean authorization is required.
  209. # 301 and 302 are redirects; they just mean look somewhere else.
  210. if str(e.code) not in ('401','301','302'):
  211. raise ValidationError, _("The URL %s is a broken link.") % field_data
  212. except: # urllib2.URLError, httplib.InvalidURL, etc.
  213. raise ValidationError, _("The URL %s is a broken link.") % field_data
  214. def isValidUSState(field_data, all_data):
  215. "Checks that the given string is a valid two-letter U.S. state abbreviation"
  216. states = ['AA', 'AE', 'AK', 'AL', 'AP', 'AR', 'AS', 'AZ', 'CA', 'CO', 'CT', 'DC', 'DE', 'FL', 'FM', 'GA', 'GU', 'HI', 'IA', 'ID', 'IL', 'IN', 'KS', 'KY', 'LA', 'MA', 'MD', 'ME', 'MH', 'MI', 'MN', 'MO', 'MP', 'MS', 'MT', 'NC', 'ND', 'NE', 'NH', 'NJ', 'NM', 'NV', 'NY', 'OH', 'OK', 'OR', 'PA', 'PR', 'PW', 'RI', 'SC', 'SD', 'TN', 'TX', 'UT', 'VA', 'VI', 'VT', 'WA', 'WI', 'WV', 'WY']
  217. if field_data.upper() not in states:
  218. raise ValidationError, gettext("Enter a valid U.S. state abbreviation.")
  219. def hasNoProfanities(field_data, all_data):
  220. """
  221. Checks that the given string has no profanities in it. This does a simple
  222. check for whether each profanity exists within the string, so 'fuck' will
  223. catch 'motherfucker' as well. Raises a ValidationError such as:
  224. Watch your mouth! The words "f--k" and "s--t" are not allowed here.
  225. """
  226. field_data = field_data.lower() # normalize
  227. words_seen = [w for w in settings.PROFANITIES_LIST if w in field_data]
  228. if words_seen:
  229. from django.utils.text import get_text_list
  230. plural = len(words_seen) > 1
  231. raise ValidationError, ngettext("Watch your mouth! The word %s is not allowed here.",
  232. "Watch your mouth! The words %s are not allowed here.", plural) % \
  233. get_text_list(['"%s%s%s"' % (i[0], '-'*(len(i)-2), i[-1]) for i in words_seen], 'and')
  234. class AlwaysMatchesOtherField(object):
  235. def __init__(self, other_field_name, error_message=None):
  236. self.other = other_field_name
  237. self.error_message = error_message or lazy_inter(gettext_lazy("This field must match the '%s' field."), self.other)
  238. self.always_test = True
  239. def __call__(self, field_data, all_data):
  240. if field_data != all_data[self.other]:
  241. raise ValidationError, self.error_message
  242. class ValidateIfOtherFieldEquals(object):
  243. def __init__(self, other_field, other_value, validator_list):
  244. self.other_field, self.other_value = other_field, other_value
  245. self.validator_list = validator_list
  246. self.always_test = True
  247. def __call__(self, field_data, all_data):
  248. if all_data.has_key(self.other_field) and all_data[self.other_field] == self.other_value:
  249. for v in self.validator_list:
  250. v(field_data, all_data)
  251. class RequiredIfOtherFieldNotGiven(object):
  252. def __init__(self, other_field_name, error_message=gettext_lazy("Please enter something for at least one field.")):
  253. self.other, self.error_message = other_field_name, error_message
  254. self.always_test = True
  255. def __call__(self, field_data, all_data):
  256. if not all_data.get(self.other, False) and not field_data:
  257. raise ValidationError, self.error_message
  258. class RequiredIfOtherFieldsGiven(object):
  259. def __init__(self, other_field_names, error_message=gettext_lazy("Please enter both fields or leave them both empty.")):
  260. self.other, self.error_message = other_field_names, error_message
  261. self.always_test = True
  262. def __call__(self, field_data, all_data):
  263. for field in self.other:
  264. if all_data.get(field, False) and not field_data:
  265. raise ValidationError, self.error_message
  266. class RequiredIfOtherFieldGiven(RequiredIfOtherFieldsGiven):
  267. "Like RequiredIfOtherFieldsGiven, but takes a single field name instead of a list."
  268. def __init__(self, other_field_name, error_message=gettext_lazy("Please enter both fields or leave them both empty.")):
  269. RequiredIfOtherFieldsGiven.__init__(self, [other_field_name], error_message)
  270. class RequiredIfOtherFieldEquals(object):
  271. def __init__(self, other_field, other_value, error_message=None, other_label=None):
  272. self.other_field = other_field
  273. self.other_value = other_value
  274. other_label = other_label or other_value
  275. self.error_message = error_message or lazy_inter(gettext_lazy("This field must be given if %(field)s is %(value)s"), {
  276. 'field': other_field, 'value': other_label})
  277. self.always_test = True
  278. def __call__(self, field_data, all_data):
  279. if all_data.has_key(self.other_field) and all_data[self.other_field] == self.other_value and not field_data:
  280. raise ValidationError(self.error_message)
  281. class RequiredIfOtherFieldDoesNotEqual(object):
  282. def __init__(self, other_field, other_value, other_label=None, error_message=None):
  283. self.other_field = other_field
  284. self.other_value = other_value
  285. other_label = other_label or other_value
  286. self.error_message = error_message or lazy_inter(gettext_lazy("This field must be given if %(field)s is not %(value)s"), {
  287. 'field': other_field, 'value': other_label})
  288. self.always_test = True
  289. def __call__(self, field_data, all_data):
  290. if all_data.has_key(self.other_field) and all_data[self.other_field] != self.other_value and not field_data:
  291. raise ValidationError(self.error_message)
  292. class IsLessThanOtherField(object):
  293. def __init__(self, other_field_name, error_message):
  294. self.other, self.error_message = other_field_name, error_message
  295. def __call__(self, field_data, all_data):
  296. if field_data > all_data[self.other]:
  297. raise ValidationError, self.error_message
  298. class UniqueAmongstFieldsWithPrefix(object):
  299. def __init__(self, field_name, prefix, error_message):
  300. self.field_name, self.prefix = field_name, prefix
  301. self.error_message = error_message or gettext_lazy("Duplicate values are not allowed.")
  302. def __call__(self, field_data, all_data):
  303. for field_name, value in all_data.items():
  304. if field_name != self.field_name and value == field_data:
  305. raise ValidationError, self.error_message
  306. class NumberIsInRange(object):
  307. """
  308. Validator that tests if a value is in a range (inclusive).
  309. """
  310. def __init__(self, lower=None, upper=None, error_message=''):
  311. self.lower, self.upper = lower, upper
  312. if not error_message:
  313. if lower and upper:
  314. self.error_message = gettext("This value must be between %(lower)s and %(upper)s.") % {'lower': lower, 'upper': upper}
  315. elif lower:
  316. self.error_message = gettext("This value must be at least %s.") % lower
  317. elif upper:
  318. self.error_message = gettext("This value must be no more than %s.") % upper
  319. else:
  320. self.error_message = error_message
  321. def __call__(self, field_data, all_data):
  322. # Try to make the value numeric. If this fails, we assume another
  323. # validator will catch the problem.
  324. try:
  325. val = float(field_data)
  326. except ValueError:
  327. return
  328. # Now validate
  329. if self.lower and self.upper and (val < self.lower or val > self.upper):
  330. raise ValidationError(self.error_message)
  331. elif self.lower and val < self.lower:
  332. raise ValidationError(self.error_message)
  333. elif self.upper and val > self.upper:
  334. raise ValidationError(self.error_message)
  335. class IsAPowerOf(object):
  336. """
  337. >>> v = IsAPowerOf(2)
  338. >>> v(4, None)
  339. >>> v(8, None)
  340. >>> v(16, None)
  341. >>> v(17, None)
  342. django.core.validators.ValidationError: ['This value must be a power of 2.']
  343. """
  344. def __init__(self, power_of):
  345. self.power_of = power_of
  346. def __call__(self, field_data, all_data):
  347. from math import log
  348. val = log(int(field_data)) / log(self.power_of)
  349. if val != int(val):
  350. raise ValidationError, gettext("This value must be a power of %s.") % self.power_of
  351. class IsValidFloat(object):
  352. def __init__(self, max_digits, decimal_places):
  353. self.max_digits, self.decimal_places = max_digits, decimal_places
  354. def __call__(self, field_data, all_data):
  355. data = str(field_data)
  356. try:
  357. float(data)
  358. except ValueError:
  359. raise ValidationError, gettext("Please enter a valid decimal number.")
  360. # Negative floats require more space to input.
  361. max_allowed_length = data.startswith('-') and (self.max_digits + 2) or (self.max_digits + 1)
  362. if len(data) > max_allowed_length:
  363. raise ValidationError, ngettext("Please enter a valid decimal number with at most %s total digit.",
  364. "Please enter a valid decimal number with at most %s total digits.", self.max_digits) % self.max_digits
  365. if (not '.' in data and len(data) > (max_allowed_length - self.decimal_places - 1)) or ('.' in data and len(data) > (max_allowed_length - (self.decimal_places - len(data.split('.')[1])))):
  366. raise ValidationError, ngettext( "Please enter a valid decimal number with a whole part of at most %s digit.",
  367. "Please enter a valid decimal number with a whole part of at most %s digits.", str(self.max_digits-self.decimal_places)) % str(self.max_digits-self.decimal_places)
  368. if '.' in data and len(data.split('.')[1]) > self.decimal_places:
  369. raise ValidationError, ngettext("Please enter a valid decimal number with at most %s decimal place.",
  370. "Please enter a valid decimal number with at most %s decimal places.", self.decimal_places) % self.decimal_places
  371. class HasAllowableSize(object):
  372. """
  373. Checks that the file-upload field data is a certain size. min_size and
  374. max_size are measurements in bytes.
  375. """
  376. def __init__(self, min_size=None, max_size=None, min_error_message=None, max_error_message=None):
  377. self.min_size, self.max_size = min_size, max_size
  378. self.min_error_message = min_error_message or lazy_inter(gettext_lazy("Make sure your uploaded file is at least %s bytes big."), min_size)
  379. self.max_error_message = max_error_message or lazy_inter(gettext_lazy("Make sure your uploaded file is at most %s bytes big."), max_size)
  380. def __call__(self, field_data, all_data):
  381. try:
  382. content = field_data['content']
  383. except TypeError:
  384. raise ValidationError, gettext_lazy("No file was submitted. Check the encoding type on the form.")
  385. if self.min_size is not None and len(content) < self.min_size:
  386. raise ValidationError, self.min_error_message
  387. if self.max_size is not None and len(content) > self.max_size:
  388. raise ValidationError, self.max_error_message
  389. class MatchesRegularExpression(object):
  390. """
  391. Checks that the field matches the given regular-expression. The regex
  392. should be in string format, not already compiled.
  393. """
  394. def __init__(self, regexp, error_message=gettext_lazy("The format for this field is wrong.")):
  395. self.regexp = re.compile(regexp)
  396. self.error_message = error_message
  397. def __call__(self, field_data, all_data):
  398. if not self.regexp.search(field_data):
  399. raise ValidationError(self.error_message)
  400. class AnyValidator(object):
  401. """
  402. This validator tries all given validators. If any one of them succeeds,
  403. validation passes. If none of them succeeds, the given message is thrown
  404. as a validation error. The message is rather unspecific, so it's best to
  405. specify one on instantiation.
  406. """
  407. def __init__(self, validator_list=None, error_message=gettext_lazy("This field is invalid.")):
  408. if validator_list is None: validator_list = []
  409. self.validator_list = validator_list
  410. self.error_message = error_message
  411. for v in validator_list:
  412. if hasattr(v, 'always_test'):
  413. self.always_test = True
  414. def __call__(self, field_data, all_data):
  415. for v in self.validator_list:
  416. try:
  417. v(field_data, all_data)
  418. return
  419. except ValidationError, e:
  420. pass
  421. raise ValidationError(self.error_message)
  422. class URLMimeTypeCheck(object):
  423. "Checks that the provided URL points to a document with a listed mime type"
  424. class CouldNotRetrieve(ValidationError):
  425. pass
  426. class InvalidContentType(ValidationError):
  427. pass
  428. def __init__(self, mime_type_list):
  429. self.mime_type_list = mime_type_list
  430. def __call__(self, field_data, all_data):
  431. import urllib2
  432. try:
  433. isValidURL(field_data, all_data)
  434. except ValidationError:
  435. raise
  436. try:
  437. info = urllib2.urlopen(field_data).info()
  438. except (urllib2.HTTPError, urllib2.URLError):
  439. raise URLMimeTypeCheck.CouldNotRetrieve, gettext("Could not retrieve anything from %s.") % field_data
  440. content_type = info['content-type']
  441. if content_type not in self.mime_type_list:
  442. raise URLMimeTypeCheck.InvalidContentType, gettext("The URL %(url)s returned the invalid Content-Type header '%(contenttype)s'.") % {
  443. 'url': field_data, 'contenttype': content_type}
  444. class RelaxNGCompact(object):
  445. "Validate against a Relax NG compact schema"
  446. def __init__(self, schema_path, additional_root_element=None):
  447. self.schema_path = schema_path
  448. self.additional_root_element = additional_root_element
  449. def __call__(self, field_data, all_data):
  450. import os, tempfile
  451. if self.additional_root_element:
  452. field_data = '<%(are)s>%(data)s\n</%(are)s>' % {
  453. 'are': self.additional_root_element,
  454. 'data': field_data
  455. }
  456. filename = tempfile.mktemp() # Insecure, but nothing else worked
  457. fp = open(filename, 'w')
  458. fp.write(field_data)
  459. fp.close()
  460. if not os.path.exists(settings.JING_PATH):
  461. raise Exception, "%s not found!" % settings.JING_PATH
  462. p = os.popen('%s -c %s %s' % (settings.JING_PATH, self.schema_path, filename))
  463. errors = [line.strip() for line in p.readlines()]
  464. p.close()
  465. os.unlink(filename)
  466. display_errors = []
  467. lines = field_data.split('\n')
  468. for error in errors:
  469. ignored, line, level, message = error.split(':', 3)
  470. # Scrape the Jing error messages to reword them more nicely.
  471. m = re.search(r'Expected "(.*?)" to terminate element starting on line (\d+)', message)
  472. if m:
  473. display_errors.append(_('Please close the unclosed %(tag)s tag from line %(line)s. (Line starts with "%(start)s".)') % \
  474. {'tag':m.group(1).replace('/', ''), 'line':m.group(2), 'start':lines[int(m.group(2)) - 1][:30]})
  475. continue
  476. if message.strip() == 'text not allowed here':
  477. display_errors.append(_('Some text starting on line %(line)s is not allowed in that context. (Line starts with "%(start)s".)') % \
  478. {'line':line, 'start':lines[int(line) - 1][:30]})
  479. continue
  480. m = re.search(r'\s*attribute "(.*?)" not allowed at this point; ignored', message)
  481. if m:
  482. display_errors.append(_('"%(attr)s" on line %(line)s is an invalid attribute. (Line starts with "%(start)s".)') % \
  483. {'attr':m.group(1), 'line':line, 'start':lines[int(line) - 1][:30]})
  484. continue
  485. m = re.search(r'\s*unknown element "(.*?)"', message)
  486. if m:
  487. display_errors.append(_('"<%(tag)s>" on line %(line)s is an invalid tag. (Line starts with "%(start)s".)') % \
  488. {'tag':m.group(1), 'line':line, 'start':lines[int(line) - 1][:30]})
  489. continue
  490. if message.strip() == 'required attributes missing':
  491. display_errors.append(_('A tag on line %(line)s is missing one or more required attributes. (Line starts with "%(start)s".)') % \
  492. {'line':line, 'start':lines[int(line) - 1][:30]})
  493. continue
  494. m = re.search(r'\s*bad value for attribute "(.*?)"', message)
  495. if m:
  496. display_errors.append(_('The "%(attr)s" attribute on line %(line)s has an invalid value. (Line starts with "%(start)s".)') % \
  497. {'attr':m.group(1), 'line':line, 'start':lines[int(line) - 1][:30]})
  498. continue
  499. # Failing all those checks, use the default error message.
  500. display_error = 'Line %s: %s [%s]' % (line, message, level.strip())
  501. display_errors.append(display_error)
  502. if len(display_errors) > 0:
  503. raise ValidationError, display_errors