app/django/core/validators.py
changeset 54 03e267d67478
equal deleted inserted replaced
53:57b4279d8c4e 54:03e267d67478
       
     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