| 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)
|
|---|
| 131 |
|
|---|