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