| 1 |
from StringIO import StringIO |
|---|
| 2 |
from django.utils.translation import ugettext as _ |
|---|
| 3 |
from lxml import etree |
|---|
| 4 |
import re |
|---|
| 5 |
|
|---|
| 6 |
# example XmlField: |
|---|
| 7 |
#class XmlField(forms.CharField): |
|---|
| 8 |
# widget = forms.Textarea |
|---|
| 9 |
# |
|---|
| 10 |
# def __init__(self, schema_path=None, additional_root_element=None, *args, **kwargs): |
|---|
| 11 |
# super(XmlField, self).__init__(*args, **kwargs) |
|---|
| 12 |
# self.additional_root_element = additional_root_element |
|---|
| 13 |
# self.schema_path = schema_path |
|---|
| 14 |
# |
|---|
| 15 |
# def clean(self, value): |
|---|
| 16 |
# from ....validators import RelaxNGValidator |
|---|
| 17 |
# super(XmlField, self).clean(value) |
|---|
| 18 |
# schema_path = self.schema_path |
|---|
| 19 |
# xml_validator = RelaxNGValidator(schema_path, self.additional_root_element) |
|---|
| 20 |
# return xml_validator.forms_validate(value) |
|---|
| 21 |
|
|---|
| 22 |
# copy of django.core.validators.ValidationError, cut most of the code out |
|---|
| 23 |
# needed to throw different exceptions for new/old-forms |
|---|
| 24 |
class ValidationError(Exception): |
|---|
| 25 |
def __init__(self, messages): # messages must be a list |
|---|
| 26 |
self.messages = messages |
|---|
| 27 |
|
|---|
| 28 |
# for rnc-files see: |
|---|
| 29 |
# rnc2rng: http://www.gnosis.cx/download/relax/ |
|---|
| 30 |
class RelaxNGValidator(object): |
|---|
| 31 |
"Validate against a Relax NG schema" |
|---|
| 32 |
def __init__(self, schema_path, additional_root_element=None): |
|---|
| 33 |
self.schema_path = schema_path |
|---|
| 34 |
self.additional_root_element = additional_root_element |
|---|
| 35 |
|
|---|
| 36 |
def raiseValidationError(self, xml_data, error_log): |
|---|
| 37 |
display_errors = [] |
|---|
| 38 |
if self.additional_root_element: |
|---|
| 39 |
adjust_line = -1 |
|---|
| 40 |
else: |
|---|
| 41 |
adjust_line = 0 |
|---|
| 42 |
lines = xml_data.split('\n') |
|---|
| 43 |
for error in error_log: |
|---|
| 44 |
# Scrape the lxml error messages to reword them more nicely. |
|---|
| 45 |
m = re.search(r'Opening and ending tag mismatch: (.+?) line (\d+?) and (.+?)$', error.message) |
|---|
| 46 |
if m: |
|---|
| 47 |
display_errors.append(_(u'Please close the unclosed %(tag)s tag from line %(line)s. (Line starts with "%(start)s".)') % \ |
|---|
| 48 |
{'tag':m.group(1).replace('/', ''), 'line':int(m.group(2)) + adjust_line, 'start':lines[int(m.group(2)) - 1][:30]}) |
|---|
| 49 |
continue |
|---|
| 50 |
m = re.search(r'Did not expect text in element (.+?) content', error.message) |
|---|
| 51 |
if m: |
|---|
| 52 |
display_errors.append(_(u'Some text starting on line %(line)s is not allowed in that context. (Line starts with "%(start)s".)') % \ |
|---|
| 53 |
{'line':error.line + adjust_line, 'start':lines[int(error.line) - 1][:30]}) |
|---|
| 54 |
continue |
|---|
| 55 |
m = re.search(r'Specification mandate value for attribute (.+?)$', error.message) |
|---|
| 56 |
if m: |
|---|
| 57 |
display_errors.append(_(u'"%(attr)s" on line %(line)s is an invalid attribute. (Line starts with "%(start)s".)') % \ |
|---|
| 58 |
{'attr':m.group(1), 'line':error.line + adjust_line, 'start':lines[int(error.line) - 1][:30]}) |
|---|
| 59 |
continue |
|---|
| 60 |
m = re.search(r'Invalid attribute (.+?) for element (.+?)$', error.message) |
|---|
| 61 |
if m: |
|---|
| 62 |
display_errors.append(_(u'"%(attr)s" on line %(line)s is an invalid attribute. (Line starts with "%(start)s".)') % \ |
|---|
| 63 |
{'attr':m.group(1), 'line':error.line + adjust_line, 'start':lines[int(error.line) - 1][:30]}) |
|---|
| 64 |
continue |
|---|
| 65 |
m = re.search(r'Did not expect element (.+?) there', error.message) |
|---|
| 66 |
if m: |
|---|
| 67 |
display_errors.append(_(u'"<%(tag)s>" on line %(line)s is an invalid tag. (Line starts with "%(start)s".)') % \ |
|---|
| 68 |
{'tag':m.group(1), 'line':error.line + adjust_line, 'start':lines[int(error.line) - 1][:30]}) |
|---|
| 69 |
continue |
|---|
| 70 |
m = re.search(r'Element (.+?) failed to validate attributes', error.message) |
|---|
| 71 |
if m: |
|---|
| 72 |
display_errors.append(_(u'A tag on line %(line)s is missing one or more required attributes. (Line starts with "%(start)s".)') % \ |
|---|
| 73 |
{'line':error.line + adjust_line, 'start':lines[int(error.line) - 1][:30]}) |
|---|
| 74 |
continue |
|---|
| 75 |
m = re.search(r'Invalid attribute (.+?) for element (.+?)$', error.message) |
|---|
| 76 |
if m: |
|---|
| 77 |
display_errors.append(_(u'The "%(attr)s" attribute on line %(line)s has an invalid value. (Line starts with "%(start)s".)') % \ |
|---|
| 78 |
{'attr':m.group(1), 'line':error.line + adjust_line, 'start':lines[int(error.line) - 1][:30]}) |
|---|
| 79 |
continue |
|---|
| 80 |
# Failing all those checks, use the default error message. |
|---|
| 81 |
display_errors.append(u'Line %s: %s [%s]' % (error.line + adjust_line, error.message, error.level_name)) |
|---|
| 82 |
raise ValidationError, display_errors |
|---|
| 83 |
|
|---|
| 84 |
def validate(self, xml_data): |
|---|
| 85 |
self.errors = [] |
|---|
| 86 |
if self.additional_root_element: |
|---|
| 87 |
xml_data = '<%(are)s>\n%(data)s\n</%(are)s>' % { |
|---|
| 88 |
'are': self.additional_root_element, |
|---|
| 89 |
'data': xml_data |
|---|
| 90 |
} |
|---|
| 91 |
|
|---|
| 92 |
etree.clearErrorLog() |
|---|
| 93 |
try: |
|---|
| 94 |
doc = etree.parse(StringIO(xml_data)) |
|---|
| 95 |
except etree.XMLSyntaxError, e: |
|---|
| 96 |
self.raiseValidationError(xml_data, e.error_log) |
|---|
| 97 |
etree.clearErrorLog() |
|---|
| 98 |
|
|---|
| 99 |
schema_path = self.schema_path |
|---|
| 100 |
if schema_path: |
|---|
| 101 |
if not schema_path[0] == '/': |
|---|
| 102 |
import os.path |
|---|
| 103 |
schema_path = os.path.join(os.path.dirname(__file__), "rng/%s" % schema_path) |
|---|
| 104 |
|
|---|
| 105 |
try: |
|---|
| 106 |
rng_doc = etree.parse(schema_path) |
|---|
| 107 |
rng = etree.RelaxNG(rng_doc) |
|---|
| 108 |
except (etree.XMLSyntaxError, etree.RelaxNGParseError), e: |
|---|
| 109 |
import os.path |
|---|
| 110 |
raise ValidationError, [_(u"Could not load %s for validation, please contact the admin") % os.path.basename(self.schema_path)] |
|---|
| 111 |
if not rng(doc): |
|---|
| 112 |
self.raiseValidationError(xml_data, rng.error_log) |
|---|
| 113 |
|
|---|
| 114 |
# oldforms-way of using this |
|---|
| 115 |
def __call__(self, field_data, all_data): |
|---|
| 116 |
from django.core.validators import ValidationError as CoreValidationError |
|---|
| 117 |
try: |
|---|
| 118 |
self.validate(field_data) |
|---|
| 119 |
except ValidationError, e: |
|---|
| 120 |
raise CoreValidationError(e.messages) |
|---|
| 121 |
|
|---|
| 122 |
# newforms-way of using this |
|---|
| 123 |
def forms_validate(self, value): |
|---|
| 124 |
from django.newforms.util import ValidationError as FormsValidationError |
|---|
| 125 |
from django.utils.encoding import smart_unicode |
|---|
| 126 |
try: |
|---|
| 127 |
self.validate(value) |
|---|
| 128 |
except ValidationError, e: |
|---|
| 129 |
raise FormsValidationError(e.messages) |
|---|
| 130 |
return smart_unicode(value) |
|---|