~ubuntu-branches/ubuntu/quantal/python-django/quantal

« back to all changes in this revision

Viewing changes to django/core/validators.py

  • Committer: Bazaar Package Importer
  • Author(s): Scott James Remnant, Eddy Mulyono
  • Date: 2008-09-16 12:18:47 UTC
  • mfrom: (1.1.5 upstream) (4.1.1 lenny)
  • Revision ID: james.westby@ubuntu.com-20080916121847-mg225rg5mnsdqzr0
Tags: 1.0-1ubuntu1
* Merge from Debian (LP: #264191), remaining changes:
  - Run test suite on build.

[Eddy Mulyono]
* Update patch to workaround network test case failures.

Show diffs side-by-side

added added

removed removed

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