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