Django

Code

root/django/branches/gis/django/core/validators.py

Revision 7979, 27.1 kB (checked in by jbronn, 4 months ago)

gis: Merged revisions 7921,7926-7928,7938-7941,7945-7947,7949-7950,7952,7955-7956,7961,7964-7968,7970-7978 via svnmerge from trunk.

This includes the newforms-admin branch, and thus is backwards-incompatible. The geographic admin is _not_ in this changeset, and is forthcoming.

  • Property svn:eol-style set to native
  • Property svn:keywords set to LastChangedRevision
Line 
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     try:
145         date(year, month, day)
146     except ValueError, e:
147         msg = _('Invalid date: %s') % _(str(e))
148         raise ValidationError, msg
149
150 def isValidANSIDate(field_data, all_data):
151     if not ansi_date_re.search(field_data):
152         raise ValidationError, _('Enter a valid date in YYYY-MM-DD format.')
153     _isValidDate(field_data)
154
155 def isValidANSITime(field_data, all_data):
156     if not ansi_time_re.search(field_data):
157         raise ValidationError, _('Enter a valid time in HH:MM format.')
158
159 def isValidANSIDatetime(field_data, all_data):
160     if not ansi_datetime_re.search(field_data):
161         raise ValidationError, _('Enter a valid date/time in YYYY-MM-DD HH:MM format.')
162     _isValidDate(field_data.split()[0])
163
164 def isValidEmail(field_data, all_data):
165     if not email_re.search(field_data):
166         raise ValidationError, _('Enter a valid e-mail address.')
167
168 def isValidImage(field_data, all_data):
169     """
170     Checks that the file-upload field data contains a valid image (GIF, JPG,
171     PNG, possibly others -- whatever the Python Imaging Library supports).
172     """
173     from PIL import Image
174     from cStringIO import StringIO
175     try:
176         content = field_data.read()
177     except TypeError:
178         raise ValidationError, _("No file was submitted. Check the encoding type on the form.")
179     try:
180         # load() is the only method that can spot a truncated JPEG,
181         #  but it cannot be called sanely after verify()
182         trial_image = Image.open(StringIO(content))
183         trial_image.load()
184         # verify() is the only method that can spot a corrupt PNG,
185         #  but it must be called immediately after the constructor
186         trial_image = Image.open(StringIO(content))
187         trial_image.verify()
188     except Exception: # Python Imaging Library doesn't recognize it as an image
189         raise ValidationError, _("Upload a valid image. The file you uploaded was either not an image or a corrupted image.")
190
191 def isValidImageURL(field_data, all_data):
192     uc = URLMimeTypeCheck(('image/jpeg', 'image/gif', 'image/png'))
193     try:
194         uc(field_data, all_data)
195     except URLMimeTypeCheck.InvalidContentType:
196         raise ValidationError, _("The URL %s does not point to a valid image.") % field_data
197
198 def isValidPhone(field_data, all_data):
199     if not phone_re.search(field_data):
200         raise ValidationError, _('Phone numbers must be in XXX-XXX-XXXX format. "%s" is invalid.') % field_data
201
202 def isValidQuicktimeVideoURL(field_data, all_data):
203     "Checks that the given URL is a video that can be played by QuickTime (qt, mpeg)"
204     uc = URLMimeTypeCheck(('video/quicktime', 'video/mpeg',))
205     try:
206         uc(field_data, all_data)
207     except URLMimeTypeCheck.InvalidContentType:
208         raise ValidationError, _("The URL %s does not point to a valid QuickTime video.") % field_data
209
210 def isValidURL(field_data, all_data):
211     if not url_re.search(field_data):
212         raise ValidationError, _("A valid URL is required.")
213
214 def isValidHTML(field_data, all_data):
215     import urllib, urllib2
216     try:
217         u = urllib2.urlopen('http://validator.w3.org/check', urllib.urlencode({'fragment': field_data, 'output': 'xml'}))
218     except:
219         # Validator or Internet connection is unavailable. Fail silently.
220         return
221     html_is_valid = (u.headers.get('x-w3c-validator-status', 'Invalid') == 'Valid')
222     if html_is_valid:
223         return
224     from xml.dom.minidom import parseString
225     error_messages = [e.firstChild.wholeText for e in parseString(u.read()).getElementsByTagName('messages')[0].getElementsByTagName('msg')]
226     raise ValidationError, _("Valid HTML is required. Specific errors are:\n%s") % "\n".join(error_messages)
227
228 def isWellFormedXml(field_data, all_data):
229     from xml.dom.minidom import parseString
230     try:
231         parseString(field_data)
232     except Exception, e: # Naked except because we're not sure what will be thrown
233         raise ValidationError, _("Badly formed XML: %s") % str(e)
234
235 def isWellFormedXmlFragment(field_data, all_data):
236     isWellFormedXml('<root>%s</root>' % field_data, all_data)
237
238 def isExistingURL(field_data, all_data):
239     try:
240         headers = {
241             "Accept" : "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
242             "Accept-Language" : "en-us,en;q=0.5",
243             "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7",
244             "Connection" : "close",
245             "User-Agent": settings.URL_VALIDATOR_USER_AGENT
246             }
247         req = urllib2.Request(field_data,None, headers)
248         u = urllib2.urlopen(req)
249     except ValueError:
250         raise ValidationError, _("Invalid URL: %s") % field_data
251     except urllib2.HTTPError, e:
252         # 401s are valid; they just mean authorization is required.
253         # 301 and 302 are redirects; they just mean look somewhere else.
254         if str(e.code) not in ('401','301','302'):
255             raise ValidationError, _("The URL %s is a broken link.") % field_data
256     except: # urllib2.URLError, httplib.InvalidURL, etc.
257         raise ValidationError, _("The URL %s is a broken link.") % field_data
258
259 def isValidUSState(field_data, all_data):
260     "Checks that the given string is a valid two-letter U.S. state abbreviation"
261     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']
262     if field_data.upper() not in states:
263         raise ValidationError, _("Enter a valid U.S. state abbreviation.")
264
265 def hasNoProfanities(field_data, all_data):
266     """
267     Checks that the given string has no profanities in it. This does a simple
268     check for whether each profanity exists within the string, so 'fuck' will
269     catch 'motherfucker' as well. Raises a ValidationError such as:
270         Watch your mouth! The words "f--k" and "s--t" are not allowed here.
271     """
272     field_data = field_data.lower() # normalize
273     words_seen = [w for w in settings.PROFANITIES_LIST if w in field_data]
274     if words_seen:
275         from django.utils.text import get_text_list
276         plural = len(words_seen)
277         raise ValidationError, ungettext("Watch your mouth! The word %s is not allowed here.",
278             "Watch your mouth! The words %s are not allowed here.", plural) % \
279             get_text_list(['"%s%s%s"' % (i[0], '-'*(len(i)-2), i[-1]) for i in words_seen], _('and'))
280
281 class AlwaysMatchesOtherField(object):
282     def __init__(self, other_field_name, error_message=None):
283         self.other = other_field_name
284         self.error_message = error_message or lazy_inter(ugettext_lazy("This field must match the '%s' field."), self.other)
285         self.always_test = True
286
287     def __call__(self, field_data, all_data):
288         if field_data != all_data[self.other]:
289             raise ValidationError, self.error_message
290
291 class ValidateIfOtherFieldEquals(object):
292     def __init__(self, other_field, other_value, validator_list):
293         self.other_field, self.other_value = other_field, other_value
294         self.validator_list = validator_list
295         self.always_test = True
296
297     def __call__(self, field_data, all_data):
298         if self.other_field in all_data and all_data[self.other_field] == self.other_value:
299             for v in self.validator_list:
300                 v(field_data, all_data)
301
302 class RequiredIfOtherFieldNotGiven(object):
303     def __init__(self, other_field_name, error_message=ugettext_lazy("Please enter something for at least one field.")):
304         self.other, self.error_message = other_field_name, error_message
305         self.always_test = True
306
307     def __call__(self, field_data, all_data):
308         if not all_data.get(self.other, False) and not field_data:
309             raise ValidationError, self.error_message
310
311 class RequiredIfOtherFieldsGiven(object):
312     def __init__(self, other_field_names, error_message=ugettext_lazy("Please enter both fields or leave them both empty.")):
313         self.other, self.error_message = other_field_names, error_message
314         self.always_test = True
315
316     def __call__(self, field_data, all_data):
317         for field in self.other:
318             if all_data.get(field, False) and not field_data:
319                 raise ValidationError, self.error_message
320
321 class RequiredIfOtherFieldGiven(RequiredIfOtherFieldsGiven):
322     "Like RequiredIfOtherFieldsGiven, but takes a single field name instead of a list."
323     def __init__(self, other_field_name, error_message=ugettext_lazy("Please enter both fields or leave them both empty.")):
324         RequiredIfOtherFieldsGiven.__init__(self, [other_field_name], error_message)
325
326 class RequiredIfOtherFieldEquals(object):
327     def __init__(self, other_field, other_value, error_message=None, other_label=None):
328         self.other_field = other_field
329         self.other_value = other_value
330         other_label = other_label or other_value
331         self.error_message = error_message or lazy_inter(ugettext_lazy("This field must be given if %(field)s is %(value)s"), {
332             'field': other_field, 'value': other_label})
333         self.always_test = True
334
335     def __call__(self, field_data, all_data):
336         if self.other_field in all_data and all_data[self.other_field] == self.other_value and not field_data:
337             raise ValidationError(self.error_message)
338
339 class RequiredIfOtherFieldDoesNotEqual(object):
340     def __init__(self, other_field, other_value, other_label=None, error_message=None):
341         self.other_field = other_field
342         self.other_value = other_value
343         other_label = other_label or other_value
344         self.error_message = error_message or lazy_inter(ugettext_lazy("This field must be given if %(field)s is not %(value)s"), {
345             'field': other_field, 'value': other_label})
346         self.always_test = True
347
348     def __call__(self, field_data, all_data):
349         if self.other_field in all_data and all_data[self.other_field] != self.other_value and not field_data:
350             raise ValidationError(self.error_message)
351
352 class IsLessThanOtherField(object):
353     def __init__(self, other_field_name, error_message):
354         self.other, self.error_message = other_field_name, error_message
355
356     def __call__(self, field_data, all_data):
357         if field_data > all_data[self.other]:
358             raise ValidationError, self.error_message
359
360 class UniqueAmongstFieldsWithPrefix(object):
361     def __init__(self, field_name, prefix, error_message):
362         self.field_name, self.prefix = field_name, prefix
363         self.error_message = error_message or ugettext_lazy("Duplicate values are not allowed.")
364
365     def __call__(self, field_data, all_data):
366         for field_name, value in all_data.items():
367             if field_name != self.field_name and value == field_data:
368                 raise ValidationError, self.error_message
369
370 class NumberIsInRange(object):
371     """
372     Validator that tests if a value is in a range (inclusive).
373     """
374     def __init__(self, lower=None, upper=None, error_message=''):
375         self.lower, self.upper = lower, upper
376         if not error_message:
377             if lower and upper:
378                  self.error_message = _("This value must be between %(lower)s and %(upper)s.") % {'lower': lower, 'upper': upper}
379             elif lower:
380                 self.error_message = _("This value must be at least %s.") % lower
381             elif upper:
382                 self.error_message = _("This value must be no more than %s.") % upper
383         else:
384             self.error_message = error_message
385
386     def __call__(self, field_data, all_data):
387         # Try to make the value numeric. If this fails, we assume another
388         # validator will catch the problem.
389         try:
390             val = float(field_data)
391         except ValueError:
392             return
393
394         # Now validate
395         if self.lower and self.upper and (val < self.lower or val > self.upper):
396             raise ValidationError(self.error_message)
397         elif self.lower and val < self.lower:
398             raise ValidationError(self.error_message)
399         elif self.upper and val > self.upper:
400             raise ValidationError(self.error_message)
401
402 class IsAPowerOf(object):
403     """
404     Usage: If you create an instance of the IsPowerOf validator:
405         v = IsAPowerOf(2)
406
407     The following calls will succeed:
408         v(4, None)
409         v(8, None)
410         v(16, None)
411
412     But this call:
413         v(17, None)
414     will raise "django.core.validators.ValidationError: ['This value must be a power of 2.']"
415     """
416     def __init__(self, power_of):
417         self.power_of = power_of
418
419     def __call__(self, field_data, all_data):
420         from math import log
421         val = log(int(field_data)) / log(self.power_of)
422         if val != int(val):
423             raise ValidationError, _("This value must be a power of %s.") % self.power_of
424
425 class IsValidDecimal(object):
426     def __init__(self, max_digits, decimal_places):
427         self.max_digits, self.decimal_places = max_digits, decimal_places
428
429     def __call__(self, field_data, all_data):
430         try:
431             val = Decimal(field_data)
432         except DecimalException:
433             raise ValidationError, _("Please enter a valid decimal number.")
434
435         pieces = str(val).lstrip("-").split('.')
436         decimals = (len(pieces) == 2) and len(pieces[1]) or 0
437         digits = len(pieces[0])
438
439         if digits + decimals > self.max_digits:
440             raise ValidationError, ungettext("Please enter a valid decimal number with at most %s total digit.",
441                 "Please enter a valid decimal number with at most %s total digits.", self.max_digits) % self.max_digits
442         if digits > (self.max_digits - self.decimal_places):
443             raise ValidationError, ungettext( "Please enter a valid decimal number with a whole part of at most %s digit.",
444                 "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)
445         if decimals > self.decimal_places:
446             raise ValidationError, ungettext("Please enter a valid decimal number with at most %s decimal place.",
447                 "Please enter a valid decimal number with at most %s decimal places.", self.decimal_places) % self.decimal_places
448
449 def isValidFloat(field_data, all_data):
450     data = smart_str(field_data)
451     try:
452         float(data)
453     except ValueError:
454         raise ValidationError, _("Please enter a valid floating point number.")
455
456 class HasAllowableSize(object):
457     """
458     Checks that the file-upload field data is a certain size. min_size and
459     max_size are measurements in bytes.
460     """
461     def __init__(self, min_size=None, max_size=None, min_error_message=None, max_error_message=None):
462         self.min_size, self.max_size = min_size, max