1 | from django.core.validators import ValidationError
|
---|
2 | from lxml import etree
|
---|
3 | from StringIO import StringIO
|
---|
4 | from django.utils.translation import gettext_lazy as _
|
---|
5 | from django.template.defaultfilters import escape
|
---|
6 | import re
|
---|
7 |
|
---|
8 | class RelaxNG(object):
|
---|
9 | "Validate against a Relax NG schema"
|
---|
10 | def __init__(self, schema_path, additional_root_element=None):
|
---|
11 | self.schema_path = schema_path
|
---|
12 | self.additional_root_element = additional_root_element
|
---|
13 |
|
---|
14 | def raiseValidationError(self, field_data, error_log):
|
---|
15 | display_errors = []
|
---|
16 | lines = field_data.split('\n')
|
---|
17 | for error in error_log:
|
---|
18 | # Scrape the lxml error messages to reword them more nicely.
|
---|
19 | m = re.search(r'Opening and ending tag mismatch: (.+?) line (\d+?) and (.+?)', error.message)
|
---|
20 | if m:
|
---|
21 | display_errors.append(_('Please close the unclosed %(tag)s tag from line %(line)s. (Line starts with "%(start)s".)') % \
|
---|
22 | {'tag':m.group(1).replace('/', ''), 'line':m.group(2), 'start':lines[int(m.group(2)) - 1][:30]})
|
---|
23 | continue
|
---|
24 | m = re.search(r'Did not expect text in element (.+?) content', error.message)
|
---|
25 | if m:
|
---|
26 | display_errors.append(_('Some text starting on line %(line)s is not allowed in that context. (Line starts with "%(start)s".)') % \
|
---|
27 | {'line':error.line, 'start':lines[int(error.line) - 1][:30]})
|
---|
28 | continue
|
---|
29 | m = re.search(r'Invalid attribute (.+?) for element (.+?)', error.message)
|
---|
30 | if m:
|
---|
31 | display_errors.append(_('"%(attr)s" on line %(line)s is an invalid attribute. (Line starts with "%(start)s".)') % \
|
---|
32 | {'attr':m.group(1), 'line':error.line, 'start':lines[int(error.line) - 1][:30]})
|
---|
33 | continue
|
---|
34 | m = re.search(r'Did not expect element (.+?) there', error.message)
|
---|
35 | if m:
|
---|
36 | display_errors.append(_('"<%(tag)s>" on line %(line)s is an invalid tag. (Line starts with "%(start)s".)') % \
|
---|
37 | {'tag':m.group(1), 'line':error.line, 'start':lines[int(error.line) - 1][:30]})
|
---|
38 | continue
|
---|
39 | m = re.search(r'Element (.+?) failed to validate attributes', error.message)
|
---|
40 | if m:
|
---|
41 | display_errors.append(_('A tag on line %(line)s is missing one or more required attributes. (Line starts with "%(start)s".)') % \
|
---|
42 | {'line':error.line, 'start':lines[int(error.line) - 1][:30]})
|
---|
43 | continue
|
---|
44 | m = re.search(r'Invalid attribute (.+?) for element (.+?)', error.message)
|
---|
45 | if m:
|
---|
46 | display_errors.append(_('The "%(attr)s" attribute on line %(line)s has an invalid value. (Line starts with "%(start)s".)') % \
|
---|
47 | {'attr':m.group(1), 'line':error.line, 'start':lines[int(error.line) - 1][:30]})
|
---|
48 | continue
|
---|
49 | # Failing all those checks, use the default error message.
|
---|
50 | display_errors.append('Line %s: %s [%s]' % (error.line, error.message, error.level_name))
|
---|
51 | raise ValidationError, display_errors
|
---|
52 |
|
---|
53 | def __call__(self, field_data, all_data):
|
---|
54 | self.errors = []
|
---|
55 | if self.additional_root_element:
|
---|
56 | field_data = '<%(are)s>%(data)s\n</%(are)s>' % {
|
---|
57 | 'are': self.additional_root_element,
|
---|
58 | 'data': field_data
|
---|
59 | }
|
---|
60 |
|
---|
61 | etree.clearErrorLog()
|
---|
62 | try:
|
---|
63 | doc = etree.parse(StringIO(field_data))
|
---|
64 | except etree.XMLSyntaxError, e:
|
---|
65 | self.raiseValidationError(field_data, e.error_log)
|
---|
66 | etree.clearErrorLog()
|
---|
67 | try:
|
---|
68 | rng_doc = etree.parse(self.schema_path)
|
---|
69 | except etree.XMLSyntaxError, e:
|
---|
70 | self.raiseValidationError(field_data, e.error_log)
|
---|
71 | rng = etree.RelaxNG(rng_doc)
|
---|
72 | if not rng(doc):
|
---|
73 | self.raiseValidationError(field_data, rng.error_log)
|
---|
74 |
|
---|