diff --git a/gluon/html.py b/gluon/html.py
index 44f4f3eb..6642cfc5 100644
--- a/gluon/html.py
+++ b/gluon/html.py
@@ -25,9 +25,9 @@ import marshal
from gluon import decoder
from gluon.storage import Storage
-from gluon.validators import simple_hash
from gluon.utils import web2py_uuid, compare
from gluon.highlight import highlight
+from gluon.validators import simple_hash
def local_html_escape(data, quote=False):
diff --git a/gluon/packages/dal b/gluon/packages/dal
index 48adca66..e28ad146 160000
--- a/gluon/packages/dal
+++ b/gluon/packages/dal
@@ -1 +1 @@
-Subproject commit 48adca66cf3a26531e3ef7e999919533963be4ab
+Subproject commit e28ad14614e075a6d8022abffffbd00da05b52be
diff --git a/gluon/tests/__init__.py b/gluon/tests/__init__.py
index 60c0b91c..29df8993 100644
--- a/gluon/tests/__init__.py
+++ b/gluon/tests/__init__.py
@@ -10,7 +10,6 @@ from .test_html import *
from .test_contribs import *
from .test_routes import *
from .test_router import *
-from .test_validators import *
from .test_authapi import *
from .test_tools import *
from .test_utils import *
@@ -21,8 +20,7 @@ from .test_appadmin import *
from .test_web import *
from .test_sqlhtml import *
from .test_cron import *
-from .test_is_url import *
-from .test_scheduler import *
+# from .test_scheduler import *
if sys.version[:3] == '2.7':
from .test_old_doctests import *
diff --git a/gluon/tests/test_is_url.py b/gluon/tests/test_is_url.py
deleted file mode 100644
index babe4edf..00000000
--- a/gluon/tests/test_is_url.py
+++ /dev/null
@@ -1,691 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-Unit tests for IS_URL()
-"""
-
-import unittest
-
-from gluon.validators import IS_URL, IS_HTTP_URL, IS_GENERIC_URL
-from gluon.validators import unicode_to_ascii_authority
-
-
-class TestIsUrl(unittest.TestCase):
-
- def testModeHttp(self):
-
- # defaults to mode='http'
-
- x = IS_URL()
- self.assertEqual(x('http://google.ca'), ('http://google.ca',
- None))
- self.assertEqual(x('google.ca'), ('http://google.ca', None))
- self.assertEqual(x('google.ca:80'), ('http://google.ca:80',
- None))
- self.assertEqual(x('unreal.blargg'), ('unreal.blargg',
- 'Enter a valid URL'))
- self.assertEqual(x('google..ca'), ('google..ca', 'Enter a valid URL'))
- self.assertEqual(
- x('google.ca..'), ('google.ca..', 'Enter a valid URL'))
-
- # explicit use of 'http' mode
-
- x = IS_URL(mode='http')
- self.assertEqual(x('http://google.ca'), ('http://google.ca',
- None))
- self.assertEqual(x('google.ca'), ('http://google.ca', None))
- self.assertEqual(x('google.ca:80'), ('http://google.ca:80',
- None))
- self.assertEqual(x('unreal.blargg'), ('unreal.blargg',
- 'Enter a valid URL'))
-
- # prepends 'https' instead of 'http'
-
- x = IS_URL(mode='http', prepend_scheme='https')
- self.assertEqual(x('http://google.ca'), ('http://google.ca',
- None))
- self.assertEqual(x('google.ca'), ('https://google.ca', None))
- self.assertEqual(x('google.ca:80'), ('https://google.ca:80',
- None))
- self.assertEqual(x('unreal.blargg'), ('unreal.blargg',
- 'Enter a valid URL'))
-
- # prepending disabled
-
- x = IS_URL(prepend_scheme=None)
- self.assertEqual(x('http://google.ca'), ('http://google.ca',
- None))
- self.assertEqual(x('google.ca'), ('google.ca', None))
- self.assertEqual(x('google.ca:80'), ('google.ca:80', None))
- self.assertEqual(x('unreal.blargg'), ('unreal.blargg',
- 'Enter a valid URL'))
-
- # custom allowed_schemes
-
- x = IS_URL(mode='http', allowed_schemes=[None, 'http'])
- self.assertEqual(x('http://google.ca'), ('http://google.ca',
- None))
- self.assertEqual(x('https://google.ca'), ('https://google.ca',
- 'Enter a valid URL'))
- self.assertEqual(x('google.ca'), ('http://google.ca', None))
- self.assertEqual(x('google.ca:80'), ('http://google.ca:80',
- None))
- self.assertEqual(x('unreal.blargg'), ('unreal.blargg',
- 'Enter a valid URL'))
-
- # custom allowed_schemes, excluding None
-
- x = IS_URL(allowed_schemes=['http'])
- self.assertEqual(x('http://google.ca'), ('http://google.ca',
- None))
- self.assertEqual(x('https://google.ca'), ('https://google.ca',
- 'Enter a valid URL'))
- self.assertEqual(x('google.ca'), ('google.ca', 'Enter a valid URL'))
- self.assertEqual(x('google.ca:80'), ('google.ca:80',
- 'Enter a valid URL'))
- self.assertEqual(x('unreal.blargg'), ('unreal.blargg',
- 'Enter a valid URL'))
-
- # custom allowed_schemes and prepend_scheme
-
- x = IS_URL(allowed_schemes=[None, 'https'],
- prepend_scheme='https')
- self.assertEqual(x('http://google.ca'), ('http://google.ca',
- 'Enter a valid URL'))
- self.assertEqual(x('https://google.ca'), ('https://google.ca',
- None))
- self.assertEqual(x('google.ca'), ('https://google.ca', None))
- self.assertEqual(x('google.ca:80'), ('https://google.ca:80',
- None))
- self.assertEqual(x('unreal.blargg'), ('unreal.blargg',
- 'Enter a valid URL'))
-
- # Now any URL requiring prepending will fail, but prepending is still
- # enabled!
-
- x = IS_URL(allowed_schemes=['http'])
- self.assertEqual(x('google.ca'), ('google.ca', 'Enter a valid URL'))
-
- def testModeGeneric(self):
-
- # 'generic' mode
-
- x = IS_URL(mode='generic')
- self.assertEqual(x('http://google.ca'), ('http://google.ca', None))
- self.assertEqual(x('google.ca'), ('google.ca', None))
- self.assertEqual(x('google.ca:80'), ('http://google.ca:80', None))
- self.assertEqual(x('blargg://unreal'), ('blargg://unreal',
- 'Enter a valid URL'))
-
- # 'generic' mode with custom allowed_schemes that still includes
- # 'http' (the default for prepend_scheme)
-
- x = IS_URL(mode='generic', allowed_schemes=['http', 'blargg'])
- self.assertEqual(x('http://google.ca'), ('http://google.ca',
- None))
- self.assertEqual(x('ftp://google.ca'), ('ftp://google.ca',
- 'Enter a valid URL'))
- self.assertEqual(x('google.ca'), ('google.ca', 'Enter a valid URL'))
- self.assertEqual(x('google.ca:80'), ('google.ca:80',
- 'Enter a valid URL'))
- self.assertEqual(x('blargg://unreal'), ('blargg://unreal',
- None))
-
- # 'generic' mode with overriden prepend_scheme
-
- x = IS_URL(mode='generic', prepend_scheme='ftp')
- self.assertEqual(x('http://google.ca'), ('http://google.ca',
- None))
- self.assertEqual(x('ftp://google.ca'), ('ftp://google.ca',
- None))
- self.assertEqual(x('google.ca'), ('google.ca', None))
- self.assertEqual(x('google.ca:80'), ('ftp://google.ca:80',
- None))
- self.assertEqual(x('blargg://unreal'), ('blargg://unreal',
- 'Enter a valid URL'))
-
- # 'generic' mode with overriden allowed_schemes and prepend_scheme
-
- x = IS_URL(mode='generic', allowed_schemes=[None, 'ftp', 'ftps'
- ], prepend_scheme='ftp')
- self.assertEqual(x('http://google.ca'), ('http://google.ca',
- 'Enter a valid URL'))
- self.assertEqual(x('google.ca'), ('google.ca', None))
- self.assertEqual(x('ftp://google.ca'), ('ftp://google.ca',
- None))
- self.assertEqual(x('google.ca:80'), ('ftp://google.ca:80',
- None))
- self.assertEqual(x('blargg://unreal'), ('blargg://unreal',
- 'Enter a valid URL'))
-
- # Now any URL requiring prepending will fail, but prepending is still
- # enabled!
-
- x = IS_URL(mode='generic', allowed_schemes=['http'])
- self.assertEqual(x('google.ca'), ('google.ca', 'Enter a valid URL'))
-
- def testExceptionalUse(self):
-
- # mode must be in set ['http', 'generic']
-
- try:
- x = IS_URL(mode='ftp')
- x('http://www.google.ca')
- except Exception as e:
- if str(e) != "invalid mode 'ftp' in IS_URL":
- self.fail('Wrong exception: ' + str(e))
- else:
- self.fail("Accepted invalid mode: 'ftp'")
-
- # allowed_schemes in 'http' mode must be in set [None, 'http', 'https']
-
- try:
- x = IS_URL(allowed_schemes=[None, 'ftp', 'ftps'],
- prepend_scheme='ftp')
- x('http://www.benn.ca') # we can only reasonably know about the
- # error at calling time
- except Exception as e:
- if str(e)\
- != "allowed_scheme value 'ftp' is not in [None, 'http', 'https']":
- self.fail('Wrong exception: ' + str(e))
- else:
- self.fail(
- "Accepted invalid allowed_schemes: [None, 'ftp', 'ftps']")
-
- # prepend_scheme's value must be in allowed_schemes (default for 'http'
- # mode is [None, 'http', 'https'])
-
- try:
- x = IS_URL(prepend_scheme='ftp')
- x('http://www.benn.ca') # we can only reasonably know about the
- # error at calling time
- except Exception as e:
- if str(e)\
- != "prepend_scheme='ftp' is not in allowed_schemes=[None, 'http', 'https']":
- self.fail('Wrong exception: ' + str(e))
- else:
- self.fail("Accepted invalid prepend_scheme: 'ftp'")
-
- # custom allowed_schemes that excludes 'http', so prepend_scheme must be
- # specified!
-
- try:
- x = IS_URL(allowed_schemes=[None, 'https'])
- except Exception as e:
- if str(e)\
- != "prepend_scheme='http' is not in allowed_schemes=[None, 'https']":
- self.fail('Wrong exception: ' + str(e))
- else:
- self.fail("Accepted invalid prepend_scheme: 'http'")
-
- # prepend_scheme must be in allowed_schemes
-
- try:
- x = IS_URL(allowed_schemes=[None, 'http'],
- prepend_scheme='https')
- except Exception as e:
- if str(e)\
- != "prepend_scheme='https' is not in allowed_schemes=[None, 'http']":
- self.fail('Wrong exception: ' + str(e))
- else:
- self.fail("Accepted invalid prepend_scheme: 'https'")
-
- # prepend_scheme's value (default is 'http') must be in allowed_schemes
-
- try:
- x = IS_URL(mode='generic', allowed_schemes=[None, 'ftp',
- 'ftps'])
- except Exception as e:
- if str(e)\
- != "prepend_scheme='http' is not in allowed_schemes=[None, 'ftp', 'ftps']":
- self.fail('Wrong exception: ' + str(e))
- else:
- self.fail("Accepted invalid prepend_scheme: 'http'")
-
- # prepend_scheme's value must be in allowed_schemes, which by default
- # is all schemes that really exist
-
- try:
- x = IS_URL(mode='generic', prepend_scheme='blargg')
- x('http://www.google.ca')
- # we can only reasonably know about the error at calling time
- except Exception as e:
- if not str(e).startswith(
- "prepend_scheme='blargg' is not in allowed_schemes="):
- self.fail('Wrong exception: ' + str(e))
- else:
- self.fail("Accepted invalid prepend_scheme: 'blargg'")
-
- # prepend_scheme's value must be in allowed_schemes
-
- try:
- x = IS_URL(mode='generic', allowed_schemes=[None, 'http'],
- prepend_scheme='blargg')
- except Exception as e:
- if str(e)\
- != "prepend_scheme='blargg' is not in allowed_schemes=[None, 'http']":
- self.fail('Wrong exception: ' + str(e))
- else:
- self.fail("Accepted invalid prepend_scheme: 'blargg'")
-
- # Not inluding None in the allowed_schemes essentially disabled
- # prepending, so even though
- # prepend_scheme has the invalid value 'http', we don't care!
-
- x = IS_URL(allowed_schemes=['https'], prepend_scheme='https')
- self.assertEqual(x('google.ca'), ('google.ca', 'Enter a valid URL'))
-
- # Not inluding None in the allowed_schemes essentially disabled prepending, so even though
- # prepend_scheme has the invalid value 'http', we don't care!
-
- x = IS_URL(mode='generic', allowed_schemes=['https'],
- prepend_scheme='https')
- self.assertEqual(x('google.ca'), ('google.ca', 'Enter a valid URL'))
-
-
-# ##############################################################################
-
-
-class TestIsGenericUrl(unittest.TestCase):
-
- x = IS_GENERIC_URL()
-
- def testInvalidUrls(self):
- urlsToCheckA = []
- for i in list(range(0, 32)) + [127]:
-
- # Control characters are disallowed in any part of a URL
-
- urlsToCheckA.append('http://www.benn' + chr(i) + '.ca')
-
- urlsToCheckB = [
- None,
- '',
- 'http://www.no spaces allowed.com',
- 'http://www.benn.ca/no spaces allowed/',
- 'http://www.benn.ca/angle_bracket/',
- 'http://www.benn.ca/invalid%character',
- 'http://www.benn.ca/illegal%%20use',
- 'http://www.benn.ca/illegaluse%',
- 'http://www.benn.ca/illegaluse%0',
- 'http://www.benn.ca/illegaluse%x',
- 'http://www.benn.ca/ill%egaluse%x',
- 'http://www.benn.ca/double"quote/',
- 'http://www.curly{brace.com',
- 'http://www.benn.ca/curly}brace/',
- 'http://www.benn.ca/or|symbol/',
- 'http://www.benn.ca/back\slash',
- 'http://www.benn.ca/the^carat',
- 'http://left[bracket.me',
- 'http://www.benn.ca/right]bracket',
- 'http://www.benn.ca/angle`quote',
- '-ttp://www.benn.ca',
- '+ttp://www.benn.ca',
- '.ttp://www.benn.ca',
- '9ttp://www.benn.ca',
- 'ht;tp://www.benn.ca',
- 'ht@tp://www.benn.ca',
- 'ht&tp://www.benn.ca',
- 'ht=tp://www.benn.ca',
- 'ht$tp://www.benn.ca',
- 'ht,tp://www.benn.ca',
- 'ht:tp://www.benn.ca',
- 'htp://invalid_scheme.com',
- ]
-
- failures = []
-
- for url in urlsToCheckA + urlsToCheckB:
- if self.x(url)[1] is None:
- failures.append('Incorrectly accepted: ' + str(url))
-
- if len(failures) > 0:
- self.fail(failures)
-
- def testValidUrls(self):
- urlsToCheck = [
- 'ftp://ftp.is.co.za/rfc/rfc1808.txt',
- 'gopher://spinaltap.micro.umn.edu/00/Weather/California/Los%20Angeles',
- 'http://www.math.uio.no/faq/compression-faq/part1.html',
- 'mailto:mduerst@ifi.unizh.ch',
- 'news:comp.infosystems.www.servers.unix',
- 'telnet://melvyl.ucop.edu/',
- 'hTTp://www.benn.ca',
- '%66%74%70://ftp.is.co.za/rfc/rfc1808.txt',
- '%46%74%70://ftp.is.co.za/rfc/rfc1808.txt',
- '/faq/compression-faq/part1.html',
- 'google.com',
- 'www.google.com:8080',
- '128.127.123.250:8080',
- 'blargg:ping',
- 'http://www.benn.ca',
- 'http://benn.ca',
- 'http://amazon.com/books/',
- 'https://amazon.com/movies',
- 'rtsp://idontknowthisprotocol',
- 'HTTP://allcaps.com',
- 'http://localhost',
- 'http://localhost#fragment',
- 'http://localhost/hello',
- 'http://localhost/hello?query=True',
- 'http://localhost/hello/',
- 'http://localhost:8080',
- 'http://localhost:8080/',
- 'http://localhost:8080/hello',
- 'http://localhost:8080/hello/',
- 'file:///C:/Documents%20and%20Settings/Jonathan/Desktop/view.py',
- ]
-
- failures = []
-
- for url in urlsToCheck:
- if self.x(url)[1] is not None:
- failures.append('Incorrectly rejected: ' + str(url))
-
- if len(failures) > 0:
- self.fail(failures)
-
- def testPrepending(self):
- # Does not prepend scheme for abbreviated domains
- self.assertEqual(self.x('google.ca'), ('google.ca', None))
-
- # Does not prepend scheme for abbreviated domains
- self.assertEqual(self.x('google.ca:8080'), ('google.ca:8080', None))
-
- # Does not prepend when scheme already exists
- self.assertEqual(self.x('https://google.ca'),
- ('https://google.ca', None))
-
- # Does not prepend if None type is not specified in allowed_scheme,
- # because a scheme is required
-
- y = IS_GENERIC_URL(allowed_schemes=['http', 'blargg'],
- prepend_scheme='http')
- self.assertEqual(y('google.ca'), ('google.ca', 'Enter a valid URL'))
-
-
-# ##############################################################################
-
-
-class TestIsHttpUrl(unittest.TestCase):
-
- x = IS_HTTP_URL()
-
- def testInvalidUrls(self):
- urlsToCheck = [
- None,
- '',
- 'http://invalid' + chr(2) + '.com',
- 'htp://invalid_scheme.com',
- 'blargg://invalid_scheme.com',
- 'http://-123.com',
- 'http://abcd-.ca',
- 'http://-abc123-.me',
- 'http://www.dom&ain.com/',
- 'http://www.dom=ain.com/',
- 'http://www.benn.ca&',
- 'http://%62%65%6E%6E%2E%63%61/path',
- 'http://.domain.com',
- 'http://.domain.com./path',
- 'http://domain..com',
- 'http://domain...at..com',
- 'http://domain.com..',
- 'http://domain.com../path',
- 'http://domain.3m',
- 'http://domain.-3m',
- 'http://domain.3m-',
- 'http://domain.-3m-',
- 'http://domain.co&m',
- 'http://domain.m3456',
- 'http://domain.m-3/path#fragment',
- 'http://domain.m---k/path?query=value',
- 'http://23.32..',
- 'http://23..32.56.0',
- 'http://38997.222.999',
- 'http://23.32.56.99.',
- 'http://.23.32.56.99',
- 'http://.23.32.56.99.',
- 'http://w127.123.0.256:8080',
- 'http://23.32.56.99:abcd',
- 'http://23.32.56.99:23cd',
- 'http://google.com:cd22',
- 'http://23.32:1300.56.99',
- 'http://www.yahoo:1600.com',
- 'path/segment/without/starting/slash',
- 'http://www.math.uio.no;param=3',
- '://ABC.com:/%7esmith/home.html',
- ]
-
- failures = []
-
- for url in urlsToCheck:
- if self.x(url)[1] is None:
- failures.append('Incorrectly accepted: ' + str(url))
-
- if len(failures) > 0:
- self.fail(failures)
-
- def testValidUrls(self):
-
- urlsToCheck = [
- 'http://abc.com:80/~smith/home.html',
- 'http://ABC.com/%7Esmith/home.html',
- 'http://ABC.com:/%7esmith/home.html',
- 'http://www.math.uio.no/faq/compression-faq/part1.html',
- '//google.ca/faq/compression-faq/part1.html',
- '//google.ca/faq;param=3',
- '//google.ca/faq/index.html?query=5',
- '//google.ca/faq/index.html;param=value?query=5',
- '/faq/compression-faq/part1.html',
- '/faq;param=3',
- '/faq/index.html?query=5',
- '/faq/index.html;param=value?query=5',
- 'google.com',
- 'benn.ca/init/default',
- 'benn.ca/init;param=value/default?query=value',
- 'http://host-name---with-dashes.me',
- 'http://www.host-name---with-dashes.me',
- 'http://a.com',
- 'http://a.3.com',
- 'http://a.bl-ck.com',
- 'http://bl-e.b.com',
- 'http://host123with456numbers.ca',
- 'http://1234567890.com.',
- 'http://1234567890.com./path',
- 'http://google.com./path',
- 'http://domain.xn--d1acj3b',
- 'http://127.123.0.256',
- 'http://127.123.0.256/document/drawer',
- '127.123.0.256/document/',
- '156.212.123.100',
- 'http://www.google.com:180200',
- 'http://www.google.com:8080/path',
- 'http://www.google.com:8080',
- '//www.google.com:8080',
- 'www.google.com:8080',
- 'http://127.123.0.256:8080/path',
- '//127.123.0.256:8080',
- '127.123.0.256:8080',
- 'http://example.me??query=value?',
- 'http://a.com',
- 'http://3.com',
- 'http://www.benn.ca',
- 'http://benn.ca',
- 'http://amazon.com/books/',
- 'https://amazon.com/movies',
- 'hTTp://allcaps.com',
- 'http://localhost',
- 'HTTPS://localhost.',
- 'http://localhost#fragment',
- 'http://localhost/hello;param=value',
- 'http://localhost/hello;param=value/hi;param2=value2;param3=value3',
- 'http://localhost/hello?query=True',
- 'http://www.benn.ca/hello;param=value/hi;param2=value2;param3=value3/index.html?query=3',
- 'http://localhost/hello/?query=1500&five=6',
- 'http://localhost:8080',
- 'http://localhost:8080/',
- 'http://localhost:8080/hello',
- 'http://localhost:8080/hello%20world/',
- 'http://www.a.3.be-nn.5.ca',
- 'http://www.amazon.COM',
- ]
-
- failures = []
-
- for url in urlsToCheck:
- if self.x(url)[1] is not None:
- failures.append('Incorrectly rejected: ' + str(url))
-
- if len(failures) > 0:
- self.fail(failures)
-
- def testPrepending(self):
- # prepends scheme for abbreviated domains
- self.assertEqual(self.x('google.ca'), ('http://google.ca', None))
-
- # prepends scheme for abbreviated domains
- self.assertEqual(self.x('google.ca:8080'),
- ('http://google.ca:8080', None))
-
- # does not prepend when scheme already exists
- self.assertEqual(self.x('https://google.ca'),
- ('https://google.ca', None))
-
- y = IS_HTTP_URL(
- prepend_scheme='https', allowed_schemes=[None, 'https'])
- self.assertEqual(y('google.ca'), (
- 'https://google.ca', None)) # prepends https if asked
-
- z = IS_HTTP_URL(prepend_scheme=None)
- self.assertEqual(z('google.ca:8080'), ('google.ca:8080', None)) # prepending disabled
-
- try:
- IS_HTTP_URL(prepend_scheme='mailto')
- except Exception as e:
- if str(e)\
- != "prepend_scheme='mailto' is not in allowed_schemes=[None, 'http', 'https']":
- self.fail('Wrong exception: ' + str(e))
- else:
- self.fail("Got invalid prepend_scheme: 'mailto'")
-
- # Does not prepend if None type is not specified in allowed_scheme, because a scheme is required
-
- a = IS_HTTP_URL(allowed_schemes=['http'])
- self.assertEqual(a('google.ca'), ('google.ca', 'Enter a valid URL'))
- self.assertEqual(a('google.ca:80'), ('google.ca:80',
- 'Enter a valid URL'))
-
-
-class TestUnicode(unittest.TestCase):
- x = IS_URL()
- y = IS_URL(allowed_schemes=['https'], prepend_scheme='https')
- #excludes the option for abbreviated URLs with no scheme
- z = IS_URL(prepend_scheme=None)
- # disables prepending the scheme in the return value
-
- def testUnicodeToAsciiUrl(self):
- self.assertEqual(unicode_to_ascii_authority(u'www.Alliancefran\xe7aise.nu'), 'www.xn--alliancefranaise-npb.nu')
- self.assertEqual(
- unicode_to_ascii_authority(u'www.benn.ca'), 'www.benn.ca')
- self.assertRaises(UnicodeError, unicode_to_ascii_authority,
- u'\u4e2d' * 1000) # label is too long
-
- def testValidUrls(self):
- self.assertEqual(self.x(u'www.Alliancefrancaise.nu'), (
- 'http://www.Alliancefrancaise.nu', None))
- self.assertEqual(self.x(u'www.Alliancefran\xe7aise.nu'), (
- 'http://www.xn--alliancefranaise-npb.nu', None))
- self.assertEqual(self.x(u'www.Alliancefran\xe7aise.nu:8080'), (
- 'http://www.xn--alliancefranaise-npb.nu:8080', None))
- self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu'),
- ('http://www.xn--alliancefranaise-npb.nu', None))
- self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue', None))
- self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue#fragment', None))
- self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue?query=value#fragment', None))
- self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu:8080/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu:8080/parnaise/blue?query=value#fragment', None))
- self.assertEqual(self.x(u'www.Alliancefran\xe7aise.nu/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue?query=value#fragment', None))
- self.assertEqual(self.x(
- u'http://\u4e2d\u4fd4.com'), ('http://xn--fiq13b.com', None))
- self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com/\u4e86'),
- ('http://xn--fiq13b.com/%4e%86', None))
- self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com/\u4e86?query=\u4e86'), ('http://xn--fiq13b.com/%4e%86?query=%4e%86', None))
- self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com/\u4e86?query=\u4e86#fragment'), ('http://xn--fiq13b.com/%4e%86?query=%4e%86#fragment', None))
- self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com?query=\u4e86#fragment'), ('http://xn--fiq13b.com?query=%4e%86#fragment', None))
- self.assertEqual(
- self.x(u'http://B\xfccher.ch'), ('http://xn--bcher-kva.ch', None))
- self.assertEqual(self.x(u'http://\xe4\xf6\xfc\xdf.com'), (
- 'http://xn--ss-uia6e4a.com', None))
- self.assertEqual(self.x(
- u'http://visegr\xe1d.com'), ('http://xn--visegrd-mwa.com', None))
- self.assertEqual(self.x(u'http://h\xe1zipatika.com'), (
- 'http://xn--hzipatika-01a.com', None))
- self.assertEqual(self.x(u'http://www.\xe7ukurova.com'), (
- 'http://www.xn--ukurova-txa.com', None))
- self.assertEqual(self.x(u'http://nixier\xf6hre.nixieclock-tube.com'), ('http://xn--nixierhre-57a.nixieclock-tube.com', None))
- self.assertEqual(self.x(u'google.ca.'), ('http://google.ca.', None))
-
- self.assertEqual(
- self.y(u'https://google.ca'), ('https://google.ca', None))
- self.assertEqual(self.y(
- u'https://\u4e2d\u4fd4.com'), ('https://xn--fiq13b.com', None))
-
- self.assertEqual(self.z(u'google.ca'), ('google.ca', None))
-
- def testInvalidUrls(self):
- self.assertEqual(
- self.x(u'://ABC.com'), (u'://ABC.com', 'Enter a valid URL'))
- self.assertEqual(self.x(u'http://\u4e2d\u4fd4.dne'), (
- u'http://\u4e2d\u4fd4.dne', 'Enter a valid URL'))
- self.assertEqual(self.x(u'https://google.dne'), (
- u'https://google.dne', 'Enter a valid URL'))
- self.assertEqual(self.x(u'https://google..ca'), (
- u'https://google..ca', 'Enter a valid URL'))
- self.assertEqual(
- self.x(u'google..ca'), (u'google..ca', 'Enter a valid URL'))
- self.assertEqual(self.x(u'http://' + u'\u4e2d' * 1000 + u'.com'), (
- u'http://' + u'\u4e2d' * 1000 + u'.com', 'Enter a valid URL'))
-
- self.assertEqual(self.x(u'http://google.com#fragment_\u4e86'), (
- u'http://google.com#fragment_\u4e86', 'Enter a valid URL'))
- self.assertEqual(self.x(u'http\u4e86://google.com'), (
- u'http\u4e86://google.com', 'Enter a valid URL'))
- self.assertEqual(self.x(u'http\u4e86://google.com#fragment_\u4e86'), (
- u'http\u4e86://google.com#fragment_\u4e86', 'Enter a valid URL'))
-
- self.assertEqual(self.y(u'http://\u4e2d\u4fd4.com/\u4e86'), (
- u'http://\u4e2d\u4fd4.com/\u4e86', 'Enter a valid URL'))
- #self.assertEqual(self.y(u'google.ca'), (u'google.ca', 'Enter a valid URL'))
-
- self.assertEqual(self.z(u'invalid.domain..com'), (
- u'invalid.domain..com', 'Enter a valid URL'))
- self.assertEqual(self.z(u'invalid.\u4e2d\u4fd4.blargg'), (
- u'invalid.\u4e2d\u4fd4.blargg', 'Enter a valid URL'))
-
-# ##############################################################################
-
-
-class TestSimple(unittest.TestCase):
-
- def test_IS_URL(self):
- rtn = IS_URL()('abc.com')
- self.assertEqual(rtn, ('http://abc.com', None))
- rtn = IS_URL(mode='generic')('abc.com')
- self.assertEqual(rtn, ('abc.com', None))
- rtn = IS_URL(allowed_schemes=['https'], prepend_scheme='https')('https://abc.com')
- self.assertEqual(rtn, ('https://abc.com', None))
- rtn = IS_URL(prepend_scheme='https')('abc.com')
- self.assertEqual(rtn, ('https://abc.com', None))
- rtn = IS_URL(mode='generic', allowed_schemes=['ftps', 'https'], prepend_scheme='https')('https://abc.com')
- self.assertEqual(rtn, ('https://abc.com', None))
- rtn = IS_URL(mode='generic', allowed_schemes=['ftps', 'https', None], prepend_scheme='https')('abc.com')
- self.assertEqual(rtn, ('abc.com', None))
- # regression test for issue 773
- rtn = IS_URL()('domain.ninja')
- self.assertEqual(rtn, ('http://domain.ninja', None))
- # addition of allowed_tlds
- rtn = IS_URL(allowed_tlds=['com', 'net', 'org'])('domain.ninja')
- self.assertEqual(rtn, ('domain.ninja', 'Enter a valid URL'))
- # mode = 'generic' doesn't consider allowed_tlds
- rtn = IS_URL(mode='generic', allowed_tlds=['com', 'net', 'org'])('domain.ninja')
- self.assertEqual(rtn, ('domain.ninja', None))
diff --git a/gluon/tests/test_validators.py b/gluon/tests/test_validators.py
deleted file mode 100644
index 90f62c95..00000000
--- a/gluon/tests/test_validators.py
+++ /dev/null
@@ -1,1273 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""Unit tests for http.py """
-
-import unittest
-import datetime
-import decimal
-import re
-
-from gluon.validators import *
-from gluon._compat import PY2, to_bytes
-
-class TestValidators(unittest.TestCase):
-
- def myassertRegex(self, *args, **kwargs):
- if PY2:
- return getattr(self, 'assertRegexpMatches')(*args, **kwargs)
- return getattr(self, 'assertRegex')(*args, **kwargs)
-
- def test_MISC(self):
- """ Test miscelaneous utility functions and some general behavior guarantees """
- from gluon.validators import options_sorter, Validator, UTC
- self.assertEqual(options_sorter(('a', 'a'), ('a', 'a')), -1)
- self.assertEqual(options_sorter(('A', 'A'), ('a', 'a')), -1)
- self.assertEqual(options_sorter(('b', 'b'), ('a', 'a')), 1)
- self.assertRaises(NotImplementedError, Validator(), 1)
- utc = UTC()
- dt = datetime.datetime.now()
- self.assertEqual(utc.utcoffset(dt), UTC.ZERO)
- self.assertEqual(utc.dst(dt), UTC.ZERO)
- self.assertEqual(utc.tzname(dt), 'UTC')
-
- def test_IS_MATCH(self):
- rtn = IS_MATCH('.+')('hello')
- self.assertEqual(rtn, ('hello', None))
- rtn = IS_MATCH('hell')('hello')
- self.assertEqual(rtn, ('hello', None))
- rtn = IS_MATCH('hell.*', strict=False)('hello')
- self.assertEqual(rtn, ('hello', None))
- rtn = IS_MATCH('hello')('shello')
- self.assertEqual(rtn, ('shello', 'Invalid expression'))
- rtn = IS_MATCH('hello', search=True)('shello')
- self.assertEqual(rtn, ('shello', None))
- rtn = IS_MATCH('hello', search=True, strict=False)('shellox')
- self.assertEqual(rtn, ('shellox', None))
- rtn = IS_MATCH('.*hello.*', search=True, strict=False)('shellox')
- self.assertEqual(rtn, ('shellox', None))
- rtn = IS_MATCH('.+')('')
- self.assertEqual(rtn, ('', 'Invalid expression'))
- rtn = IS_MATCH('hell', strict=True)('hellas')
- self.assertEqual(rtn, ('hellas', 'Invalid expression'))
- rtn = IS_MATCH('hell$', strict=True)('hellas')
- self.assertEqual(rtn, ('hellas', 'Invalid expression'))
- rtn = IS_MATCH('^.hell$', strict=True)('shell')
- self.assertEqual(rtn, ('shell', None))
- rtn = IS_MATCH(u'hell', is_unicode=True)('àòè')
- if PY2:
- self.assertEqual(rtn, ('\xc3\xa0\xc3\xb2\xc3\xa8', 'Invalid expression'))
- else:
- self.assertEqual(rtn, ('àòè', 'Invalid expression'))
- rtn = IS_MATCH(u'hell', is_unicode=True)(u'hell')
- self.assertEqual(rtn, (u'hell', None))
- rtn = IS_MATCH('hell', is_unicode=True)(u'hell')
- self.assertEqual(rtn, (u'hell', None))
- # regr test for #1044
- rtn = IS_MATCH('hello')(u'\xff')
- self.assertEqual(rtn, (u'\xff', 'Invalid expression'))
-
- def test_IS_EQUAL_TO(self):
- rtn = IS_EQUAL_TO('aaa')('aaa')
- self.assertEqual(rtn, ('aaa', None))
- rtn = IS_EQUAL_TO('aaa')('aab')
- self.assertEqual(rtn, ('aab', 'No match'))
-
- def test_IS_EXPR(self):
- rtn = IS_EXPR('int(value) < 2')('1')
- self.assertEqual(rtn, ('1', None))
- rtn = IS_EXPR('int(value) < 2')('2')
- self.assertEqual(rtn, ('2', 'Invalid expression'))
- rtn = IS_EXPR(lambda value: int(value))('1')
- self.assertEqual(rtn, ('1', 1))
- rtn = IS_EXPR(lambda value: int(value) < 2 and 'invalid' or None)('2')
- self.assertEqual(rtn, ('2', None))
-
- def test_IS_LENGTH(self):
- rtn = IS_LENGTH()('')
- self.assertEqual(rtn, ('', None))
- rtn = IS_LENGTH()('1234567890')
- self.assertEqual(rtn, ('1234567890', None))
- rtn = IS_LENGTH(maxsize=5, minsize=0)('1234567890') # too long
- self.assertEqual(rtn, ('1234567890', 'Enter from 0 to 5 characters'))
- rtn = IS_LENGTH(maxsize=50, minsize=20)('1234567890') # too short
- self.assertEqual(rtn, ('1234567890', 'Enter from 20 to 50 characters'))
- rtn = IS_LENGTH()(None)
- self.assertEqual(rtn, (None, None))
- rtn = IS_LENGTH(minsize=0)(None)
- self.assertEqual(rtn, (None, None))
- rtn = IS_LENGTH(minsize=1)(None)
- self.assertEqual(rtn, (None, 'Enter from 1 to 255 characters'))
- rtn = IS_LENGTH(minsize=1)([])
- self.assertEqual(rtn, ([], 'Enter from 1 to 255 characters'))
- rtn = IS_LENGTH(minsize=1)([1, 2])
- self.assertEqual(rtn, ([1, 2], None))
- rtn = IS_LENGTH(minsize=1)([1])
- self.assertEqual(rtn, ([1], None))
- # test non utf-8 str
- cpstr = u'lálá'.encode('cp1252')
- rtn = IS_LENGTH(minsize=4)(cpstr)
- self.assertEqual(rtn, (cpstr, None))
- rtn = IS_LENGTH(maxsize=4)(cpstr)
- self.assertEqual(rtn, (cpstr, None))
- rtn = IS_LENGTH(minsize=0, maxsize=3)(cpstr)
- self.assertEqual(rtn, (cpstr, 'Enter from 0 to 3 characters'))
- # test unicode
- rtn = IS_LENGTH(2)(u'°2')
- if PY2:
- self.assertEqual(rtn, ('\xc2\xb02', None))
- else:
- self.assertEqual(rtn, (u'°2', None))
- rtn = IS_LENGTH(2)(u'°12')
- if PY2:
- self.assertEqual(rtn, (u'\xb012', 'Enter from 0 to 2 characters'))
- else:
- self.assertEqual(rtn, (u'°12', 'Enter from 0 to 2 characters'))
- # test automatic str()
- rtn = IS_LENGTH(minsize=1)(1)
- self.assertEqual(rtn, ('1', None))
- rtn = IS_LENGTH(minsize=2)(1)
- self.assertEqual(rtn, (1, 'Enter from 2 to 255 characters'))
- # test FieldStorage
- import cgi
- from io import BytesIO
- a = cgi.FieldStorage()
- a.file = BytesIO(b'abc')
- rtn = IS_LENGTH(minsize=4)(a)
- self.assertEqual(rtn, (a, 'Enter from 4 to 255 characters'))
- urlencode_data = b"key2=value2x&key3=value3&key4=value4"
- urlencode_environ = {
- 'CONTENT_LENGTH': str(len(urlencode_data)),
- 'CONTENT_TYPE': 'application/x-www-form-urlencoded',
- 'QUERY_STRING': 'key1=value1&key2=value2y',
- 'REQUEST_METHOD': 'POST',
- }
- fake_stdin = BytesIO(urlencode_data)
- fake_stdin.seek(0)
- a = cgi.FieldStorage(fp=fake_stdin, environ=urlencode_environ)
- rtn = IS_LENGTH(minsize=6)(a)
- self.assertEqual(rtn, (a, 'Enter from 6 to 255 characters'))
- a = cgi.FieldStorage()
- rtn = IS_LENGTH(minsize=6)(a)
- self.assertEqual(rtn, (a, 'Enter from 6 to 255 characters'))
- rtn = IS_LENGTH(6)(a)
- self.assertEqual(rtn, (a, None))
-
- def test_IS_JSON(self):
- rtn = IS_JSON()('{"a": 100}')
- self.assertEqual(rtn, ({u'a': 100}, None))
- rtn = IS_JSON()('spam1234')
- self.assertEqual(rtn, ('spam1234', 'Invalid json'))
- rtn = IS_JSON(native_json=True)('{"a": 100}')
- self.assertEqual(rtn, ('{"a": 100}', None))
- rtn = IS_JSON().formatter(None)
- self.assertEqual(rtn, None)
- rtn = IS_JSON().formatter({'a': 100})
- self.assertEqual(rtn, '{"a": 100}')
- rtn = IS_JSON(native_json=True).formatter({'a': 100})
- self.assertEqual(rtn, {'a': 100})
-
- def test_IS_IN_SET(self):
- rtn = IS_IN_SET(['max', 'john'])('max')
- self.assertEqual(rtn, ('max', None))
- rtn = IS_IN_SET(['max', 'john'])('massimo')
- self.assertEqual(rtn, ('massimo', 'Value not allowed'))
- rtn = IS_IN_SET(['max', 'john'], multiple=True)(('max', 'john'))
- self.assertEqual(rtn, (('max', 'john'), None))
- rtn = IS_IN_SET(['max', 'john'], multiple=True)(('bill', 'john'))
- self.assertEqual(rtn, (('bill', 'john'), 'Value not allowed'))
- rtn = IS_IN_SET(('id1', 'id2'), ['first label', 'second label'])('id1') # Traditional way
- self.assertEqual(rtn, ('id1', None))
- rtn = IS_IN_SET({'id1': 'first label', 'id2': 'second label'})('id1')
- self.assertEqual(rtn, ('id1', None))
- rtn = IS_IN_SET(['id1', 'id2'], error_message='oops', multiple=True)(None)
- self.assertEqual(rtn, ([], None))
- rtn = IS_IN_SET(['id1', 'id2'], error_message='oops', multiple=True)('')
- self.assertEqual(rtn, ([], None))
- rtn = IS_IN_SET(['id1', 'id2'], error_message='oops', multiple=True)('id1')
- self.assertEqual(rtn, (['id1'], None))
- rtn = IS_IN_SET(['id1', 'id2'], error_message='oops', multiple=(1, 2))(None)
- self.assertEqual(rtn, (None, 'oops'))
- import itertools
- rtn = IS_IN_SET(itertools.chain(['1', '3', '5'], ['2', '4', '6']))('1')
- self.assertEqual(rtn, ('1', None))
- rtn = IS_IN_SET([('id1', 'first label'), ('id2', 'second label')])('id1') # Redundant way
- self.assertEqual(rtn, ('id1', None))
- rtn = IS_IN_SET([('id1', 'first label'), ('id2', 'second label')]).options(zero=False)
- self.assertEqual(rtn, [('id1', 'first label'), ('id2', 'second label')])
- rtn = IS_IN_SET(['id1', 'id2']).options(zero=False)
- self.assertEqual(rtn, [('id1', 'id1'), ('id2', 'id2')])
- rtn = IS_IN_SET(['id2', 'id1'], sort=True).options(zero=False)
- self.assertEqual(rtn, [('id1', 'id1'), ('id2', 'id2')])
-
- def test_IS_IN_DB(self):
- from gluon.dal import DAL, Field
- db = DAL('sqlite:memory')
- db.define_table('person', Field('name'))
- george_id = db.person.insert(name='george')
- costanza_id = db.person.insert(name='costanza')
- rtn = IS_IN_DB(db, 'person.id', '%(name)s')(george_id)
- self.assertEqual(rtn, (george_id, None))
- rtn = IS_IN_DB(db, 'person.name', '%(name)s')('george')
- self.assertEqual(rtn, ('george', None))
- rtn = IS_IN_DB(db, db.person, '%(name)s')(george_id)
- self.assertEqual(rtn, (george_id, None))
- rtn = IS_IN_DB(db(db.person.id > 0), db.person, '%(name)s')(george_id)
- self.assertEqual(rtn, (george_id, None))
- rtn = IS_IN_DB(db, 'person.id', '%(name)s', error_message='oops')(george_id + costanza_id)
- self.assertEqual(rtn, (george_id + costanza_id, 'oops'))
- rtn = IS_IN_DB(db, db.person.id, '%(name)s')(george_id)
- self.assertEqual(rtn, (george_id, None))
- rtn = IS_IN_DB(db, db.person.id, '%(name)s', error_message='oops')(george_id + costanza_id)
- self.assertEqual(rtn, (george_id + costanza_id, 'oops'))
- rtn = IS_IN_DB(db, 'person.id', '%(name)s', multiple=True)([george_id, costanza_id])
- self.assertEqual(rtn, ([george_id, costanza_id], None))
- rtn = IS_IN_DB(db, 'person.id', '%(name)s', multiple=True, error_message='oops')("I'm not even an id")
- self.assertEqual(rtn, ("I'm not even an id", 'oops'))
- rtn = IS_IN_DB(db, 'person.id', '%(name)s', multiple=True, delimiter=',')('%d,%d' % (george_id, costanza_id))
- self.assertEqual(rtn, (('%d,%d' % (george_id, costanza_id)).split(','), None))
- rtn = IS_IN_DB(db, 'person.id', '%(name)s', multiple=(1, 3), delimiter=',')('%d,%d' % (george_id, costanza_id))
- self.assertEqual(rtn, (('%d,%d' % (george_id, costanza_id)).split(','), None))
- rtn = IS_IN_DB(db, 'person.id', '%(name)s', multiple=(1, 2), delimiter=',', error_message='oops')('%d,%d' % (george_id, costanza_id))
- self.assertEqual(rtn, (('%d,%d' % (george_id, costanza_id)), 'oops'))
- rtn = IS_IN_DB(db, db.person.id, '%(name)s', error_message='oops').options(zero=False)
- self.assertEqual(sorted(rtn), [('%d' % george_id, 'george'), ('%d' % costanza_id, 'costanza')])
- rtn = IS_IN_DB(db, db.person.id, db.person.name, error_message='oops', sort=True).options(zero=True)
- self.assertEqual(rtn, [('', ''), ('%d' % costanza_id, 'costanza'), ('%d' % george_id, 'george')])
- # Test None
- rtn = IS_IN_DB(db, 'person.id', '%(name)s', error_message='oops')(None)
- self.assertEqual(rtn, (None, 'oops'))
- rtn = IS_IN_DB(db, 'person.name', '%(name)s', error_message='oops')(None)
- self.assertEqual(rtn, (None, 'oops'))
- # Test using the set it made for options
- vldtr = IS_IN_DB(db, 'person.name', '%(name)s', error_message='oops')
- vldtr.options()
- rtn = vldtr('george')
- self.assertEqual(rtn, ('george', None))
- rtn = vldtr('jerry')
- self.assertEqual(rtn, ('jerry', 'oops'))
- vldtr = IS_IN_DB(db, 'person.name', '%(name)s', error_message='oops', multiple=True)
- vldtr.options()
- rtn = vldtr(['george', 'costanza'])
- self.assertEqual(rtn, (['george', 'costanza'], None))
- # Test it works with self reference
- db.define_table('category',
- Field('parent_id', 'reference category', requires=IS_EMPTY_OR(IS_IN_DB(db, 'category.id', '%(name)s'))),
- Field('name')
- )
- ret = db.category.validate_and_insert(name='seinfeld')
- self.assertFalse(list(ret.errors))
- ret = db.category.validate_and_insert(name='characters', parent_id=ret.id)
- self.assertFalse(list(ret.errors))
- rtn = IS_IN_DB(db, 'category.id', '%(name)s')(ret.id)
- self.assertEqual(rtn, (ret.id, None))
- # Test _and
- vldtr = IS_IN_DB(db, 'person.name', '%(name)s', error_message='oops', _and=IS_LENGTH(maxsize=7, error_message='bad'))
- rtn = vldtr('george')
- self.assertEqual(rtn, ('george', None))
- rtn = vldtr('costanza')
- self.assertEqual(rtn, ('costanza', 'bad'))
- rtn = vldtr('jerry')
- self.assertEqual(rtn, ('jerry', 'oops'))
- vldtr.options() # test theset with _and
- rtn = vldtr('jerry')
- self.assertEqual(rtn, ('jerry', 'oops'))
- # Test auto_add
- rtn = IS_IN_DB(db, 'person.id', '%(name)s', error_message='oops')('jerry')
- self.assertEqual(rtn, ('jerry', 'oops'))
- rtn = IS_IN_DB(db, 'person.id', '%(name)s', auto_add=True)('jerry')
- self.assertEqual(rtn, (3, None))
- # Test it works with reference table
- db.define_table('ref_table',
- Field('name'),
- Field('person_id', 'reference person')
- )
- ret = db.ref_table.validate_and_insert(name='test reference table')
- self.assertFalse(list(ret.errors))
- ret = db.ref_table.validate_and_insert(name='test reference table', person_id=george_id)
- self.assertFalse(list(ret.errors))
- rtn = IS_IN_DB(db, 'ref_table.person_id', '%(name)s')(george_id)
- self.assertEqual(rtn, (george_id, None))
- # Test it works with reference table.field and keyed table
- db.define_table('person_keyed',
- Field('name'),
- primarykey=['name'])
- db.person_keyed.insert(name='george')
- db.person_keyed.insert(name='costanza')
- rtn = IS_IN_DB(db, 'person_keyed.name')('george')
- self.assertEqual(rtn, ('george', None))
- db.define_table('ref_table_field',
- Field('name'),
- Field('person_name', 'reference person_keyed.name')
- )
- ret = db.ref_table_field.validate_and_insert(name='test reference table.field')
- self.assertFalse(list(ret.errors))
- ret = db.ref_table_field.validate_and_insert(name='test reference table.field', person_name='george')
- self.assertFalse(list(ret.errors))
- vldtr = IS_IN_DB(db, 'ref_table_field.person_name', '%(name)s')
- vldtr.options()
- rtn = vldtr('george')
- self.assertEqual(rtn, ('george', None))
- # Test it works with list:reference table
- db.define_table('list_ref_table',
- Field('name'),
- Field('person_list', 'list:reference person'))
- ret = db.list_ref_table.validate_and_insert(name='test list:reference table')
- self.assertFalse(list(ret.errors))
- ret = db.list_ref_table.validate_and_insert(name='test list:reference table', person_list=[george_id,costanza_id])
- self.assertFalse(list(ret.errors))
- vldtr = IS_IN_DB(db, 'list_ref_table.person_list')
- vldtr.options()
- rtn = vldtr([george_id,costanza_id])
- self.assertEqual(rtn, ([george_id,costanza_id], None))
- # Test it works with list:reference table.field and keyed table
- #db.define_table('list_ref_table_field',
- # Field('name'),
- # Field('person_list', 'list:reference person_keyed.name'))
- #ret = db.list_ref_table_field.validate_and_insert(name='test list:reference table.field')
- #self.assertFalse(list(ret.errors))
- #ret = db.list_ref_table_field.validate_and_insert(name='test list:reference table.field', person_list=['george','costanza'])
- #self.assertFalse(list(ret.errors))
- #vldtr = IS_IN_DB(db, 'list_ref_table_field.person_list')
- #vldtr.options()
- #rtn = vldtr(['george','costanza'])
- #self.assertEqual(rtn, (['george','costanza'], None))
- db.person.drop()
- db.category.drop()
- db.person_keyed.drop()
- db.ref_table.drop()
- db.ref_table_field.drop()
- db.list_ref_table.drop()
- #db.list_ref_table_field.drop()
-
- def test_IS_NOT_IN_DB(self):
- from gluon.dal import DAL, Field
- db = DAL('sqlite:memory')
- db.define_table('person', Field('name'), Field('nickname'))
- db.person.insert(name='george')
- db.person.insert(name='costanza', nickname='T Bone')
- rtn = IS_NOT_IN_DB(db, 'person.name', error_message='oops')('george')
- self.assertEqual(rtn, ('george', 'oops'))
- rtn = IS_NOT_IN_DB(db, 'person.name', error_message='oops', allowed_override=['george'])('george')
- self.assertEqual(rtn, ('george', None))
- rtn = IS_NOT_IN_DB(db, 'person.name', error_message='oops')(' ')
- self.assertEqual(rtn, (' ', 'oops'))
- rtn = IS_NOT_IN_DB(db, 'person.name')('jerry')
- self.assertEqual(rtn, ('jerry', None))
- rtn = IS_NOT_IN_DB(db, 'person.name')(u'jerry')
- self.assertEqual(rtn, ('jerry', None))
- rtn = IS_NOT_IN_DB(db(db.person.id > 0), 'person.name')(u'jerry')
- self.assertEqual(rtn, ('jerry', None))
- rtn = IS_NOT_IN_DB(db, db.person, error_message='oops')(1)
- self.assertEqual(rtn, (1, 'oops'))
- vldtr = IS_NOT_IN_DB(db, 'person.name', error_message='oops')
- vldtr.set_self_id({'name': 'costanza', 'nickname': 'T Bone'})
- rtn = vldtr('george')
- self.assertEqual(rtn, ('george', 'oops'))
- rtn = vldtr('costanza')
- self.assertEqual(rtn, ('costanza', None))
-
- db.person.drop()
-
- def test_IS_INT_IN_RANGE(self):
- rtn = IS_INT_IN_RANGE(1, 5)('4')
- self.assertEqual(rtn, (4, None))
- rtn = IS_INT_IN_RANGE(1, 5)(4)
- self.assertEqual(rtn, (4, None))
- rtn = IS_INT_IN_RANGE(1, 5)(1)
- self.assertEqual(rtn, (1, None))
- rtn = IS_INT_IN_RANGE(1, 5)(5)
- self.assertEqual(rtn, (5, 'Enter an integer between 1 and 4'))
- rtn = IS_INT_IN_RANGE(1, 5)(5)
- self.assertEqual(rtn, (5, 'Enter an integer between 1 and 4'))
- rtn = IS_INT_IN_RANGE(1, 5)(3.5)
- self.assertEqual(rtn, (3.5, 'Enter an integer between 1 and 4'))
- rtn = IS_INT_IN_RANGE(None, 5)('4')
- self.assertEqual(rtn, (4, None))
- rtn = IS_INT_IN_RANGE(None, 5)('6')
- self.assertEqual(rtn, ('6', 'Enter an integer less than or equal to 4'))
- rtn = IS_INT_IN_RANGE(1, None)('4')
- self.assertEqual(rtn, (4, None))
- rtn = IS_INT_IN_RANGE(1, None)('0')
- self.assertEqual(rtn, ('0', 'Enter an integer greater than or equal to 1'))
- rtn = IS_INT_IN_RANGE()(6)
- self.assertEqual(rtn, (6, None))
- rtn = IS_INT_IN_RANGE()('abc')
- self.assertEqual(rtn, ('abc', 'Enter an integer'))
-
- def test_IS_FLOAT_IN_RANGE(self):
- # with None
- rtn = IS_FLOAT_IN_RANGE(1, 5)(None)
- self.assertEqual(rtn, (None, 'Enter a number between 1 and 5'))
- rtn = IS_FLOAT_IN_RANGE(1, 5)('4')
- self.assertEqual(rtn, (4.0, None))
- rtn = IS_FLOAT_IN_RANGE(1, 5)(4)
- self.assertEqual(rtn, (4.0, None))
- rtn = IS_FLOAT_IN_RANGE(1, 5)(1)
- self.assertEqual(rtn, (1.0, None))
- rtn = IS_FLOAT_IN_RANGE(1, 5)(5.25)
- self.assertEqual(rtn, (5.25, 'Enter a number between 1 and 5'))
- rtn = IS_FLOAT_IN_RANGE(1, 5)(6.0)
- self.assertEqual(rtn, (6.0, 'Enter a number between 1 and 5'))
- rtn = IS_FLOAT_IN_RANGE(1, 5)(3.5)
- self.assertEqual(rtn, (3.5, None))
- rtn = IS_FLOAT_IN_RANGE(1, None)(3.5)
- self.assertEqual(rtn, (3.5, None))
- rtn = IS_FLOAT_IN_RANGE(None, 5)(3.5)
- self.assertEqual(rtn, (3.5, None))
- rtn = IS_FLOAT_IN_RANGE(1, None)(0.5)
- self.assertEqual(rtn, (0.5, 'Enter a number greater than or equal to 1'))
- rtn = IS_FLOAT_IN_RANGE(None, 5)(6.5)
- self.assertEqual(rtn, (6.5, 'Enter a number less than or equal to 5'))
- rtn = IS_FLOAT_IN_RANGE()(6.5)
- self.assertEqual(rtn, (6.5, None))
- rtn = IS_FLOAT_IN_RANGE()('abc')
- self.assertEqual(rtn, ('abc', 'Enter a number'))
- rtn = IS_FLOAT_IN_RANGE()('6,5')
- self.assertEqual(rtn, ('6,5', 'Enter a number'))
- rtn = IS_FLOAT_IN_RANGE(dot=',')('6.5')
- self.assertEqual(rtn, (6.5, None))
- # With .formatter(None)
- rtn = IS_FLOAT_IN_RANGE(dot=',').formatter(None)
- self.assertEqual(rtn, None)
- rtn = IS_FLOAT_IN_RANGE(dot=',').formatter(0.25)
- self.assertEqual(rtn, '0,25')
- # To trigger str2dec "if not '.' in s:" line
- rtn = IS_FLOAT_IN_RANGE(dot=',').formatter(1)
- self.assertEqual(rtn, '1,00')
-
- def test_IS_DECIMAL_IN_RANGE(self):
- # with None
- rtn = IS_DECIMAL_IN_RANGE(1, 5)(None)
- self.assertEqual(rtn, (None, 'Enter a number between 1 and 5'))
- rtn = IS_DECIMAL_IN_RANGE(1, 5)('4')
- self.assertEqual(rtn, (decimal.Decimal('4'), None))
- rtn = IS_DECIMAL_IN_RANGE(1, 5)(4)
- self.assertEqual(rtn, (decimal.Decimal('4'), None))
- rtn = IS_DECIMAL_IN_RANGE(1, 5)(1)
- self.assertEqual(rtn, (decimal.Decimal('1'), None))
- rtn = IS_DECIMAL_IN_RANGE(1, 5)(5.25)
- self.assertEqual(rtn, (5.25, 'Enter a number between 1 and 5'))
- rtn = IS_DECIMAL_IN_RANGE(5.25, 6)(5.25)
- self.assertEqual(rtn, (decimal.Decimal('5.25'), None))
- rtn = IS_DECIMAL_IN_RANGE(5.25, 6)('5.25')
- self.assertEqual(rtn, (decimal.Decimal('5.25'), None))
- rtn = IS_DECIMAL_IN_RANGE(1, 5)(6.0)
- self.assertEqual(rtn, (6.0, 'Enter a number between 1 and 5'))
- rtn = IS_DECIMAL_IN_RANGE(1, 5)(3.5)
- self.assertEqual(rtn, (decimal.Decimal('3.5'), None))
- rtn = IS_DECIMAL_IN_RANGE(1.5, 5.5)(3.5)
- self.assertEqual(rtn, (decimal.Decimal('3.5'), None))
- rtn = IS_DECIMAL_IN_RANGE(1.5, 5.5)(6.5)
- self.assertEqual(rtn, (6.5, 'Enter a number between 1.5 and 5.5'))
- rtn = IS_DECIMAL_IN_RANGE(1.5, None)(6.5)
- self.assertEqual(rtn, (decimal.Decimal('6.5'), None))
- rtn = IS_DECIMAL_IN_RANGE(1.5, None)(0.5)
- self.assertEqual(rtn, (0.5, 'Enter a number greater than or equal to 1.5'))
- rtn = IS_DECIMAL_IN_RANGE(None, 5.5)(4.5)
- self.assertEqual(rtn, (decimal.Decimal('4.5'), None))
- rtn = IS_DECIMAL_IN_RANGE(None, 5.5)(6.5)
- self.assertEqual(rtn, (6.5, 'Enter a number less than or equal to 5.5'))
- rtn = IS_DECIMAL_IN_RANGE()(6.5)
- self.assertEqual(rtn, (decimal.Decimal('6.5'), None))
- rtn = IS_DECIMAL_IN_RANGE(0, 99)(123.123)
- self.assertEqual(rtn, (123.123, 'Enter a number between 0 and 99'))
- rtn = IS_DECIMAL_IN_RANGE(0, 99)('123.123')
- self.assertEqual(rtn, ('123.123', 'Enter a number between 0 and 99'))
- rtn = IS_DECIMAL_IN_RANGE(0, 99)('12.34')
- self.assertEqual(rtn, (decimal.Decimal('12.34'), None))
- rtn = IS_DECIMAL_IN_RANGE()('abc')
- self.assertEqual(rtn, ('abc', 'Enter a number'))
- rtn = IS_DECIMAL_IN_RANGE()('6,5')
- self.assertEqual(rtn, ('6,5', 'Enter a number'))
- rtn = IS_DECIMAL_IN_RANGE(dot=',')('6.5')
- self.assertEqual(rtn, (decimal.Decimal('6.5'), None))
- rtn = IS_DECIMAL_IN_RANGE(1, 5)(decimal.Decimal('4'))
- self.assertEqual(rtn, (decimal.Decimal('4'), None))
- # With .formatter(None)
- rtn = IS_DECIMAL_IN_RANGE(dot=',').formatter(None)
- self.assertEqual(rtn, None)
- rtn = IS_DECIMAL_IN_RANGE(dot=',').formatter(0.25)
- self.assertEqual(rtn, '0,25')
-
- def test_IS_NOT_EMPTY(self):
- rtn = IS_NOT_EMPTY()(1)
- self.assertEqual(rtn, (1, None))
- rtn = IS_NOT_EMPTY()(0)
- self.assertEqual(rtn, (0, None))
- rtn = IS_NOT_EMPTY()('x')
- self.assertEqual(rtn, ('x', None))
- rtn = IS_NOT_EMPTY()(' x ')
- self.assertEqual(rtn, (' x ', None))
- rtn = IS_NOT_EMPTY()(None)
- self.assertEqual(rtn, (None, 'Enter a value'))
- rtn = IS_NOT_EMPTY()('')
- self.assertEqual(rtn, ('', 'Enter a value'))
- rtn = IS_NOT_EMPTY()(b'')
- self.assertEqual(rtn, (b'', 'Enter a value'))
- rtn = IS_NOT_EMPTY()(' ')
- self.assertEqual(rtn, (' ', 'Enter a value'))
- rtn = IS_NOT_EMPTY()(' \n\t')
- self.assertEqual(rtn, (' \n\t', 'Enter a value'))
- rtn = IS_NOT_EMPTY()([])
- self.assertEqual(rtn, ([], 'Enter a value'))
- rtn = IS_NOT_EMPTY(empty_regex='def')('def')
- self.assertEqual(rtn, ('def', 'Enter a value'))
- rtn = IS_NOT_EMPTY(empty_regex='de[fg]')('deg')
- self.assertEqual(rtn, ('deg', 'Enter a value'))
- rtn = IS_NOT_EMPTY(empty_regex='def')('abc')
- self.assertEqual(rtn, ('abc', None))
-
- def test_IS_ALPHANUMERIC(self):
- rtn = IS_ALPHANUMERIC()('1')
- self.assertEqual(rtn, ('1', None))
- rtn = IS_ALPHANUMERIC()('')
- self.assertEqual(rtn, ('', None))
- rtn = IS_ALPHANUMERIC()('A_a')
- self.assertEqual(rtn, ('A_a', None))
- rtn = IS_ALPHANUMERIC()('!')
- self.assertEqual(rtn, ('!', 'Enter only letters, numbers, and underscore'))
-
- def test_IS_EMAIL(self):
- rtn = IS_EMAIL()('a@b.com')
- self.assertEqual(rtn, ('a@b.com', None))
- rtn = IS_EMAIL()('abc@def.com')
- self.assertEqual(rtn, ('abc@def.com', None))
- rtn = IS_EMAIL()('abc@3def.com')
- self.assertEqual(rtn, ('abc@3def.com', None))
- rtn = IS_EMAIL()('abc@def.us')
- self.assertEqual(rtn, ('abc@def.us', None))
- rtn = IS_EMAIL()('abc@d_-f.us')
- self.assertEqual(rtn, ('abc@d_-f.us', None))
- rtn = IS_EMAIL()('@def.com') # missing name
- self.assertEqual(rtn, ('@def.com', 'Enter a valid email address'))
- rtn = IS_EMAIL()('"abc@def".com') # quoted name
- self.assertEqual(rtn, ('"abc@def".com', 'Enter a valid email address'))
- rtn = IS_EMAIL()('abc+def.com') # no @
- self.assertEqual(rtn, ('abc+def.com', 'Enter a valid email address'))
- rtn = IS_EMAIL()('abc@def.x') # one-char TLD
- self.assertEqual(rtn, ('abc@def.x', 'Enter a valid email address'))
- rtn = IS_EMAIL()('abc@def.12') # numeric TLD
- self.assertEqual(rtn, ('abc@def.12', 'Enter a valid email address'))
- rtn = IS_EMAIL()('abc@def..com') # double-dot in domain
- self.assertEqual(rtn, ('abc@def..com', 'Enter a valid email address'))
- rtn = IS_EMAIL()('abc@.def.com') # dot starts domain
- self.assertEqual(rtn, ('abc@.def.com', 'Enter a valid email address'))
- rtn = IS_EMAIL()('abc@def.c_m') # underscore in TLD
- self.assertEqual(rtn, ('abc@def.c_m', 'Enter a valid email address'))
- rtn = IS_EMAIL()('NotAnEmail') # missing @
- self.assertEqual(rtn, ('NotAnEmail', 'Enter a valid email address'))
- rtn = IS_EMAIL()('abc@NotAnEmail') # missing TLD
- self.assertEqual(rtn, ('abc@NotAnEmail', 'Enter a valid email address'))
- rtn = IS_EMAIL()('customer/department@example.com')
- self.assertEqual(rtn, ('customer/department@example.com', None))
- rtn = IS_EMAIL()('$A12345@example.com')
- self.assertEqual(rtn, ('$A12345@example.com', None))
- rtn = IS_EMAIL()('!def!xyz%abc@example.com')
- self.assertEqual(rtn, ('!def!xyz%abc@example.com', None))
- rtn = IS_EMAIL()('_Yosemite.Sam@example.com')
- self.assertEqual(rtn, ('_Yosemite.Sam@example.com', None))
- rtn = IS_EMAIL()('~@example.com')
- self.assertEqual(rtn, ('~@example.com', None))
- rtn = IS_EMAIL()('.wooly@example.com') # dot starts name
- self.assertEqual(rtn, ('.wooly@example.com', 'Enter a valid email address'))
- rtn = IS_EMAIL()('wo..oly@example.com') # adjacent dots in name
- self.assertEqual(rtn, ('wo..oly@example.com', 'Enter a valid email address'))
- rtn = IS_EMAIL()('pootietang.@example.com') # dot ends name
- self.assertEqual(rtn, ('pootietang.@example.com', 'Enter a valid email address'))
- rtn = IS_EMAIL()('.@example.com') # name is bare dot
- self.assertEqual(rtn, ('.@example.com', 'Enter a valid email address'))
- rtn = IS_EMAIL()('Ima.Fool@example.com')
- self.assertEqual(rtn, ('Ima.Fool@example.com', None))
- rtn = IS_EMAIL()('Ima Fool@example.com') # space in name
- self.assertEqual(rtn, ('Ima Fool@example.com', 'Enter a valid email address'))
- rtn = IS_EMAIL()('localguy@localhost') # localhost as domain
- self.assertEqual(rtn, ('localguy@localhost', None))
- # test for banned
- rtn = IS_EMAIL(banned='^.*\.com(|\..*)$')('localguy@localhost') # localhost as domain
- self.assertEqual(rtn, ('localguy@localhost', None))
- rtn = IS_EMAIL(banned='^.*\.com(|\..*)$')('abc@example.com')
- self.assertEqual(rtn, ('abc@example.com', 'Enter a valid email address'))
- # test for forced
- rtn = IS_EMAIL(forced='^.*\.edu(|\..*)$')('localguy@localhost')
- self.assertEqual(rtn, ('localguy@localhost', 'Enter a valid email address'))
- rtn = IS_EMAIL(forced='^.*\.edu(|\..*)$')('localguy@example.edu')
- self.assertEqual(rtn, ('localguy@example.edu', None))
- # test for not a string at all
- rtn = IS_EMAIL(error_message='oops')(42)
- self.assertEqual(rtn, (42, 'oops'))
-
- # test for Internationalized Domain Names, see https://docs.python.org/2/library/codecs.html#module-encodings.idna
- rtn = IS_EMAIL()('web2py@Alliancefrançaise.nu')
- self.assertEqual(rtn, ('web2py@Alliancefrançaise.nu', None))
-
-
- def test_IS_LIST_OF_EMAILS(self):
- emails = ['localguy@localhost', '_Yosemite.Sam@example.com']
- rtn = IS_LIST_OF_EMAILS()(','.join(emails))
- self.assertEqual(rtn, (','.join(emails), None))
- rtn = IS_LIST_OF_EMAILS()(';'.join(emails))
- self.assertEqual(rtn, (';'.join(emails), None))
- rtn = IS_LIST_OF_EMAILS()(' '.join(emails))
- self.assertEqual(rtn, (' '.join(emails), None))
- emails.append('a')
- rtn = IS_LIST_OF_EMAILS()(';'.join(emails))
- self.assertEqual(rtn, ('localguy@localhost;_Yosemite.Sam@example.com;a', 'Invalid emails: a'))
- rtn = IS_LIST_OF_EMAILS().formatter(['test@example.com', 'dude@example.com'])
- self.assertEqual(rtn, 'test@example.com, dude@example.com')
-
- def test_IS_URL(self):
- rtn = IS_URL()('http://example.com')
- self.assertEqual(rtn, ('http://example.com', None))
- rtn = IS_URL(error_message='oops')('http://example,com')
- self.assertEqual(rtn, ('http://example,com', 'oops'))
- rtn = IS_URL(error_message='oops')('http://www.example.com:8800/a/b/c/d/e/f/g/h')
- self.assertEqual(rtn, ('http://www.example.com:8800/a/b/c/d/e/f/g/h', None))
- rtn = IS_URL(error_message='oops', prepend_scheme='http')('example.com')
- self.assertEqual(rtn, ('http://example.com', None))
- rtn = IS_URL()('http://example.com?q=george&p=22')
- self.assertEqual(rtn, ('http://example.com?q=george&p=22', None))
- rtn = IS_URL(mode='generic', prepend_scheme=None)('example.com')
- self.assertEqual(rtn, ('example.com', None))
-
- def test_IS_TIME(self):
- rtn = IS_TIME()('21:30')
- self.assertEqual(rtn, (datetime.time(21, 30), None))
- rtn = IS_TIME()('21-30')
- self.assertEqual(rtn, (datetime.time(21, 30), None))
- rtn = IS_TIME()('21.30')
- self.assertEqual(rtn, (datetime.time(21, 30), None))
- rtn = IS_TIME()('21:30:59')
- self.assertEqual(rtn, (datetime.time(21, 30, 59), None))
- rtn = IS_TIME()('5:30')
- self.assertEqual(rtn, (datetime.time(5, 30), None))
- rtn = IS_TIME()('5:30 am')
- self.assertEqual(rtn, (datetime.time(5, 30), None))
- rtn = IS_TIME()('5:30 pm')
- self.assertEqual(rtn, (datetime.time(17, 30), None))
- rtn = IS_TIME()('5:30 whatever')
- self.assertEqual(rtn, ('5:30 whatever', 'Enter time as hh:mm:ss (seconds, am, pm optional)'))
- rtn = IS_TIME()('5:30 20')
- self.assertEqual(rtn, ('5:30 20', 'Enter time as hh:mm:ss (seconds, am, pm optional)'))
- rtn = IS_TIME()('24:30')
- self.assertEqual(rtn, ('24:30', 'Enter time as hh:mm:ss (seconds, am, pm optional)'))
- rtn = IS_TIME()('21:60')
- self.assertEqual(rtn, ('21:60', 'Enter time as hh:mm:ss (seconds, am, pm optional)'))
- rtn = IS_TIME()('21:30::')
- self.assertEqual(rtn, ('21:30::', 'Enter time as hh:mm:ss (seconds, am, pm optional)'))
- rtn = IS_TIME()('')
- self.assertEqual(rtn, ('', 'Enter time as hh:mm:ss (seconds, am, pm optional)'))
-
- def test_IS_DATE(self):
- v = IS_DATE(format="%m/%d/%Y", error_message="oops")
- rtn = v('03/03/2008')
- self.assertEqual(rtn, (datetime.date(2008, 3, 3), None))
- rtn = v('31/03/2008')
- self.assertEqual(rtn, ('31/03/2008', 'oops'))
- rtn = IS_DATE(format="%m/%d/%Y", error_message="oops").formatter(datetime.date(1834, 12, 14))
- self.assertEqual(rtn, '12/14/1834')
-
- def test_IS_DATETIME(self):
- v = IS_DATETIME(format="%m/%d/%Y %H:%M", error_message="oops")
- rtn = v('03/03/2008 12:40')
- self.assertEqual(rtn, (datetime.datetime(2008, 3, 3, 12, 40), None))
- rtn = v('31/03/2008 29:40')
- self.assertEqual(rtn, ('31/03/2008 29:40', 'oops'))
- # Test timezone is removed and value is properly converted
- #
- # https://github.com/web2py/web2py/issues/1094
-
- class DummyTimezone(datetime.tzinfo):
-
- ONE = datetime.timedelta(hours=1)
-
- def utcoffset(self, dt):
- return DummyTimezone.ONE
-
- def tzname(self, dt):
- return "UTC+1"
-
- def dst(self, dt):
- return DummyTimezone.ONE
-
- def localize(self, dt, is_dst=False):
- return dt.replace(tzinfo=self)
- v = IS_DATETIME(format="%Y-%m-%d %H:%M", error_message="oops", timezone=DummyTimezone())
- rtn = v('1982-12-14 08:00')
- self.assertEqual(rtn, (datetime.datetime(1982, 12, 14, 7, 0), None))
-
- def test_IS_DATE_IN_RANGE(self):
- v = IS_DATE_IN_RANGE(minimum=datetime.date(2008, 1, 1),
- maximum=datetime.date(2009, 12, 31),
- format="%m/%d/%Y", error_message="oops")
-
- rtn = v('03/03/2008')
- self.assertEqual(rtn, (datetime.date(2008, 3, 3), None))
- rtn = v('03/03/2010')
- self.assertEqual(rtn, ('03/03/2010', 'oops'))
- rtn = v(datetime.date(2008, 3, 3))
- self.assertEqual(rtn, (datetime.date(2008, 3, 3), None))
- rtn = v(datetime.date(2010, 3, 3))
- self.assertEqual(rtn, (datetime.date(2010, 3, 3), 'oops'))
- v = IS_DATE_IN_RANGE(maximum=datetime.date(2009, 12, 31),
- format="%m/%d/%Y")
- rtn = v('03/03/2010')
- self.assertEqual(rtn, ('03/03/2010', 'Enter date on or before 12/31/2009'))
- v = IS_DATE_IN_RANGE(minimum=datetime.date(2008, 1, 1),
- format="%m/%d/%Y")
- rtn = v('03/03/2007')
- self.assertEqual(rtn, ('03/03/2007', 'Enter date on or after 01/01/2008'))
- v = IS_DATE_IN_RANGE(minimum=datetime.date(2008, 1, 1),
- maximum=datetime.date(2009, 12, 31),
- format="%m/%d/%Y")
- rtn = v('03/03/2007')
- self.assertEqual(rtn, ('03/03/2007', 'Enter date in range 01/01/2008 12/31/2009'))
-
- def test_IS_DATETIME_IN_RANGE(self):
- v = IS_DATETIME_IN_RANGE(
- minimum=datetime.datetime(2008, 1, 1, 12, 20),
- maximum=datetime.datetime(2009, 12, 31, 12, 20),
- format="%m/%d/%Y %H:%M", error_message="oops")
- rtn = v('03/03/2008 12:40')
- self.assertEqual(rtn, (datetime.datetime(2008, 3, 3, 12, 40), None))
- rtn = v('03/03/2010 10:34')
- self.assertEqual(rtn, ('03/03/2010 10:34', 'oops'))
- rtn = v(datetime.datetime(2008, 3, 3, 0, 0))
- self.assertEqual(rtn, (datetime.datetime(2008, 3, 3, 0, 0), None))
- rtn = v(datetime.datetime(2010, 3, 3, 0, 0))
- self.assertEqual(rtn, (datetime.datetime(2010, 3, 3, 0, 0), 'oops'))
- v = IS_DATETIME_IN_RANGE(maximum=datetime.datetime(2009, 12, 31, 12, 20),
- format='%m/%d/%Y %H:%M:%S')
- rtn = v('03/03/2010 12:20:00')
- self.assertEqual(rtn, ('03/03/2010 12:20:00', 'Enter date and time on or before 12/31/2009 12:20:00'))
- v = IS_DATETIME_IN_RANGE(minimum=datetime.datetime(2008, 1, 1, 12, 20),
- format='%m/%d/%Y %H:%M:%S')
- rtn = v('03/03/2007 12:20:00')
- self.assertEqual(rtn, ('03/03/2007 12:20:00', 'Enter date and time on or after 01/01/2008 12:20:00'))
- v = IS_DATETIME_IN_RANGE(minimum=datetime.datetime(2008, 1, 1, 12, 20),
- maximum=datetime.datetime(2009, 12, 31, 12, 20),
- format='%m/%d/%Y %H:%M:%S')
- rtn = v('03/03/2007 12:20:00')
- self.assertEqual(rtn, ('03/03/2007 12:20:00', 'Enter date and time in range 01/01/2008 12:20:00 12/31/2009 12:20:00'))
- v = IS_DATETIME_IN_RANGE(maximum=datetime.datetime(2009, 12, 31, 12, 20),
- format='%Y-%m-%d %H:%M:%S', error_message='oops')
- rtn = v('clearly not a date')
- self.assertEqual(rtn, ('clearly not a date', 'oops'))
-
- def test_IS_LIST_OF(self):
- values = [0, 1, 2, 3, 4]
- rtn = IS_LIST_OF(IS_INT_IN_RANGE(0, 10))(values)
- self.assertEqual(rtn, (values, None))
- values.append(11)
- rtn = IS_LIST_OF(IS_INT_IN_RANGE(0, 10))(values)
- self.assertEqual(rtn, (values, 'Enter an integer between 0 and 9'))
- rtn = IS_LIST_OF(IS_INT_IN_RANGE(0, 10))(1)
- self.assertEqual(rtn, ([1], None))
- rtn = IS_LIST_OF(IS_INT_IN_RANGE(0, 10), minimum=10)([1, 2])
- self.assertEqual(rtn, ([1, 2], 'Minimum length is 10'))
- rtn = IS_LIST_OF(IS_INT_IN_RANGE(0, 10), maximum=2)([1, 2, 3])
- self.assertEqual(rtn, ([1, 2, 3], 'Maximum length is 2'))
- # regression test for issue 742
- rtn = IS_LIST_OF(minimum=1)('')
- self.assertEqual(rtn, ('', 'Minimum length is 1'))
-
- def test_IS_LOWER(self):
- rtn = IS_LOWER()('ABC')
- self.assertEqual(rtn, ('abc', None))
- rtn = IS_LOWER()(b'ABC')
- self.assertEqual(rtn, (b'abc', None))
- rtn = IS_LOWER()('Ñ')
- self.assertEqual(rtn, ('ñ', None))
-
- def test_IS_UPPER(self):
- rtn = IS_UPPER()('abc')
- self.assertEqual(rtn, ('ABC', None))
- rtn = IS_UPPER()(b'abc')
- self.assertEqual(rtn, (b'ABC', None))
- rtn = IS_UPPER()('ñ')
- self.assertEqual(rtn, ('Ñ', None))
-
- def test_IS_SLUG(self):
- rtn = IS_SLUG()('abc123')
- self.assertEqual(rtn, ('abc123', None))
- rtn = IS_SLUG()('ABC123')
- self.assertEqual(rtn, ('abc123', None))
- rtn = IS_SLUG()('abc-123')
- self.assertEqual(rtn, ('abc-123', None))
- rtn = IS_SLUG()('abc--123')
- self.assertEqual(rtn, ('abc-123', None))
- rtn = IS_SLUG()('abc 123')
- self.assertEqual(rtn, ('abc-123', None))
- rtn = IS_SLUG()('abc\t_123')
- self.assertEqual(rtn, ('abc-123', None))
- rtn = IS_SLUG()('-abc-')
- self.assertEqual(rtn, ('abc', None))
- rtn = IS_SLUG()('--a--b--_ -c--')
- self.assertEqual(rtn, ('a-b-c', None))
- rtn = IS_SLUG()('abc&123')
- self.assertEqual(rtn, ('abc123', None))
- rtn = IS_SLUG()('abc&123&def')
- self.assertEqual(rtn, ('abc123def', None))
- rtn = IS_SLUG()('ñ')
- self.assertEqual(rtn, ('n', None))
- rtn = IS_SLUG(maxlen=4)('abc123')
- self.assertEqual(rtn, ('abc1', None))
- rtn = IS_SLUG()('abc_123')
- self.assertEqual(rtn, ('abc-123', None))
- rtn = IS_SLUG(keep_underscores=False)('abc_123')
- self.assertEqual(rtn, ('abc-123', None))
- rtn = IS_SLUG(keep_underscores=True)('abc_123')
- self.assertEqual(rtn, ('abc_123', None))
- rtn = IS_SLUG(check=False)('abc')
- self.assertEqual(rtn, ('abc', None))
- rtn = IS_SLUG(check=True)('abc')
- self.assertEqual(rtn, ('abc', None))
- rtn = IS_SLUG(check=False)('a bc')
- self.assertEqual(rtn, ('a-bc', None))
- rtn = IS_SLUG(check=True)('a bc')
- self.assertEqual(rtn, ('a bc', 'Must be slug'))
-
- def test_ANY_OF(self):
- rtn = ANY_OF([IS_EMAIL(), IS_ALPHANUMERIC()])('a@b.co')
- self.assertEqual(rtn, ('a@b.co', None))
- rtn = ANY_OF([IS_EMAIL(), IS_ALPHANUMERIC()])('abco')
- self.assertEqual(rtn, ('abco', None))
- rtn = ANY_OF([IS_EMAIL(), IS_ALPHANUMERIC()])('@ab.co')
- self.assertEqual(rtn, ('@ab.co', 'Enter only letters, numbers, and underscore'))
- rtn = ANY_OF([IS_ALPHANUMERIC(), IS_EMAIL()])('@ab.co')
- self.assertEqual(rtn, ('@ab.co', 'Enter a valid email address'))
- rtn = ANY_OF([IS_DATE(), IS_EMAIL()])('a@b.co')
- self.assertEqual(rtn, ('a@b.co', None))
- rtn = ANY_OF([IS_DATE(), IS_EMAIL()])('1982-12-14')
- self.assertEqual(rtn, (datetime.date(1982, 12, 14), None))
- rtn = ANY_OF([IS_DATE(format='%m/%d/%Y'), IS_EMAIL()]).formatter(datetime.date(1834, 12, 14))
- self.assertEqual(rtn, '12/14/1834')
-
- def test_IS_EMPTY_OR(self):
- rtn = IS_EMPTY_OR(IS_EMAIL())('abc@def.com')
- self.assertEqual(rtn, ('abc@def.com', None))
- rtn = IS_EMPTY_OR(IS_EMAIL())(' ')
- self.assertEqual(rtn, (None, None))
- rtn = IS_EMPTY_OR(IS_EMAIL(), null='abc')(' ')
- self.assertEqual(rtn, ('abc', None))
- rtn = IS_EMPTY_OR(IS_EMAIL(), null='abc', empty_regex='def')('def')
- self.assertEqual(rtn, ('abc', None))
- rtn = IS_EMPTY_OR(IS_EMAIL())('abc')
- self.assertEqual(rtn, ('abc', 'Enter a valid email address'))
- rtn = IS_EMPTY_OR(IS_EMAIL())(' abc ')
- self.assertEqual(rtn, (' abc ', 'Enter a valid email address'))
- rtn = IS_EMPTY_OR(IS_IN_SET([('id1', 'first label'), ('id2', 'second label')], zero='zero')).options(zero=False)
- self.assertEqual(rtn, [('', ''), ('id1', 'first label'), ('id2', 'second label')])
- rtn = IS_EMPTY_OR(IS_IN_SET([('id1', 'first label'), ('id2', 'second label')], zero='zero')).options()
- self.assertEqual(rtn, [('', 'zero'), ('id1', 'first label'), ('id2', 'second label')])
- rtn = IS_EMPTY_OR((IS_LOWER(), IS_EMAIL()))('AAA')
- self.assertEqual(rtn, ('AAA', 'Enter a valid email address'))
- rtn = IS_EMPTY_OR([IS_LOWER(), IS_EMAIL()])('AAA')
- self.assertEqual(rtn, ('AAA', 'Enter a valid email address'))
-
- def test_CLEANUP(self):
- rtn = CLEANUP()('helloò')
- self.assertEqual(rtn, ('hello', None))
-
- def test_CRYPT(self):
- rtn = str(CRYPT(digest_alg='md5', salt=True)('test')[0])
- self.myassertRegex(rtn, r'^md5\$.{16}\$.{32}$')
- rtn = str(CRYPT(digest_alg='sha1', salt=True)('test')[0])
- self.myassertRegex(rtn, r'^sha1\$.{16}\$.{40}$')
- rtn = str(CRYPT(digest_alg='sha256', salt=True)('test')[0])
- self.myassertRegex(rtn, r'^sha256\$.{16}\$.{64}$')
- rtn = str(CRYPT(digest_alg='sha384', salt=True)('test')[0])
- self.myassertRegex(rtn, r'^sha384\$.{16}\$.{96}$')
- rtn = str(CRYPT(digest_alg='sha512', salt=True)('test')[0])
- self.myassertRegex(rtn, r'^sha512\$.{16}\$.{128}$')
- alg = 'pbkdf2(1000,20,sha512)'
- rtn = str(CRYPT(digest_alg=alg, salt=True)('test')[0])
- self.myassertRegex(rtn, r'^pbkdf2\(1000,20,sha512\)\$.{16}\$.{40}$')
- rtn = str(CRYPT(digest_alg='md5', key='mykey', salt=True)('test')[0])
- self.myassertRegex(rtn, r'^md5\$.{16}\$.{32}$')
- a = str(CRYPT(digest_alg='sha1', salt=False)('test')[0])
- self.assertEqual(CRYPT(digest_alg='sha1', salt=False)('test')[0], a)
- self.assertEqual(CRYPT(digest_alg='sha1', salt=False)('test')[0], a[6:])
- self.assertEqual(CRYPT(digest_alg='md5', salt=False)('test')[0], a)
- self.assertEqual(CRYPT(digest_alg='md5', salt=False)('test')[0], a[6:])
-
- def test_IS_STRONG(self):
- rtn = IS_STRONG(es=True)('Abcd1234')
- self.assertEqual(rtn, ('Abcd1234',
- 'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|'))
- rtn = IS_STRONG(es=True)('Abcd1234!')
- self.assertEqual(rtn, ('Abcd1234!', None))
- rtn = IS_STRONG(es=True, entropy=1)('a')
- self.assertEqual(rtn, ('a', None))
- rtn = IS_STRONG(es=True, entropy=1, min=2)('a')
- self.assertEqual(rtn, ('a', 'Minimum length is 2'))
- rtn = IS_STRONG(es=True, entropy=100)('abc123')
- self.assertEqual(rtn, ('abc123', 'Entropy (32.35) less than required (100)'))
- rtn = IS_STRONG(es=True, entropy=100)('and')
- self.assertEqual(rtn, ('and', 'Entropy (14.57) less than required (100)'))
- rtn = IS_STRONG(es=True, entropy=100)('aaa')
- self.assertEqual(rtn, ('aaa', 'Entropy (14.42) less than required (100)'))
- rtn = IS_STRONG(es=True, entropy=100)('a1d')
- self.assertEqual(rtn, ('a1d', 'Entropy (15.97) less than required (100)'))
- rtn = IS_STRONG(es=True, entropy=100)('añd')
- if PY2:
- self.assertEqual(rtn, ('a\xc3\xb1d', 'Entropy (18.13) less than required (100)'))
- else:
- self.assertEqual(rtn, ('añd', 'Entropy (18.13) less than required (100)'))
- rtn = IS_STRONG()('********')
- self.assertEqual(rtn, ('********', None))
- rtn = IS_STRONG(es=True, max=4)('abcde')
- self.assertEqual(rtn,
- ('abcde',
- '|'.join(['Minimum length is 8',
- 'Maximum length is 4',
- 'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|',
- 'Must include at least 1 uppercase',
- 'Must include at least 1 number']))
- )
- rtn = IS_STRONG(es=True)('abcde')
- self.assertEqual(rtn,
- ('abcde',
- '|'.join(['Minimum length is 8',
- 'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|',
- 'Must include at least 1 uppercase',
- 'Must include at least 1 number']))
- )
- rtn = IS_STRONG(upper=0, lower=0, number=0, es=True)('Abcde1')
- self.assertEqual(rtn,
- ('Abcde1',
- '|'.join(['Minimum length is 8',
- 'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|',
- 'May not include any uppercase letters',
- 'May not include any lowercase letters',
- 'May not include any numbers']))
- )
- rtn = IS_STRONG(special=0, es=True)('Abcde1!')
- self.assertEqual(rtn,
- ('Abcde1!',
- '|'.join(['Minimum length is 8',
- 'May not contain any of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|']))
- )
- rtn = IS_STRONG(upper=False, number=False, special=False, es=True)('Abcde1!')
- self.assertEqual(rtn, ('Abcde1!', 'Minimum length is 8'))
-
- def test_IS_IMAGE(self):
- class DummyImageFile(object):
-
- def __init__(self, filename, ext, width, height):
- from io import BytesIO
- import struct
- self.filename = filename + '.' + ext
- self.file = BytesIO()
- if ext == 'bmp':
- self.file.write(b'BM')
- self.file.write(b' ' * 16)
- self.file.write(struct.pack('
-| License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
-| Thanks to ga2arch for help with IS_IN_DB and IS_NOT_IN_DB on GAE
-
-Validators
------------
-"""
-import os
-import re
-import math
-import datetime
-import time
-import cgi
-import uuid
-import hashlib
-import hmac
-import json
-import struct
-import decimal
-import binascii
-import ipaddress
-import unicodedata
-import encodings.idna
-
-from pydal._compat import StringIO, integer_types, basestring, unicodeT, urllib_unquote, \
- unichr, to_bytes, PY2, to_unicode, to_native, string_types, urlparse
-from pydal.objects import Field, FieldVirtual, FieldMethod, Table
-
-
-regex_isint = re.compile(r'^[+-]?\d+$')
-
-JSONErrors = (NameError, TypeError, ValueError, AttributeError, KeyError)
-
-__all__ = [
- 'ANY_OF',
- 'CLEANUP',
- 'CRYPT',
- 'IS_ALPHANUMERIC',
- 'IS_DATE_IN_RANGE',
- 'IS_DATE',
- 'IS_DATETIME_IN_RANGE',
- 'IS_DATETIME',
- 'IS_DECIMAL_IN_RANGE',
- 'IS_EMAIL',
- 'IS_LIST_OF_EMAILS',
- 'IS_EMPTY_OR',
- 'IS_EXPR',
- 'IS_FILE',
- 'IS_FLOAT_IN_RANGE',
- 'IS_IMAGE',
- 'IS_IN_DB',
- 'IS_IN_SET',
- 'IS_INT_IN_RANGE',
- 'IS_IPV4',
- 'IS_IPV6',
- 'IS_IPADDRESS',
- 'IS_LENGTH',
- 'IS_LIST_OF',
- 'IS_LOWER',
- 'IS_MATCH',
- 'IS_EQUAL_TO',
- 'IS_NOT_EMPTY',
- 'IS_NOT_IN_DB',
- 'IS_NULL_OR',
- 'IS_SLUG',
- 'IS_STRONG',
- 'IS_TIME',
- 'IS_UPLOAD_FILENAME',
- 'IS_UPPER',
- 'IS_URL',
- 'IS_JSON',
-]
-
-def options_sorter(x, y):
- return (str(x[1]).upper() > str(y[1]).upper() and 1) or -1
-
-def translate(text):
- return Validator.translator(text)
-
-
-class ValidationError(Exception):
- pass
-
-
-class Validator(object):
- """
- Root for all validators, mainly for documentation purposes.
-
- Validators are classes used to validate input fields (including forms
- generated from database tables).
-
- Here is an example of using a validator with a FORM::
-
- INPUT(_name='a', requires=IS_INT_IN_RANGE(0, 10))
-
- Here is an example of how to require a validator for a table field::
-
- db.define_table('person', Field('name'))
- db.person.name.requires=IS_NOT_EMPTY()
-
- Validators are always assigned using the requires attribute of a field. A
- field can have a single validator or multiple validators. Multiple
- validators are made part of a list::
-
- db.person.name.requires=[IS_NOT_EMPTY(), IS_NOT_IN_DB(db, 'person.id')]
-
- Validators are called by the function accepts on a FORM or other HTML
- helper object that contains a form. They are always called in the order in
- which they are listed.
-
- Built-in validators have constructors that take the optional argument error
- message which allows you to change the default error message.
- Here is an example of a validator on a database table::
-
- db.person.name.requires=IS_NOT_EMPTY(error_message=T('Fill this'))
-
- where we have used the translation operator T to allow for
- internationalization.
-
- Notice that default error messages are not translated.
- """
-
- translator = staticmethod(lambda text: text)
-
- def formatter(self, value):
- """
- For some validators returns a formatted version (matching the validator)
- of value. Otherwise just returns the value.
- """
- return value
-
- def validate(self, value):
- raise NotImplementedError
-
- def __call__(self, value):
- try:
- return self.validate(value), None
- except ValidationError as e:
- return value, e.message
-
-
-class IS_MATCH(Validator):
- """
- Example:
- Used as::
-
- INPUT(_type='text', _name='name', requires=IS_MATCH('.+'))
-
- The argument of IS_MATCH is a regular expression::
-
- >>> IS_MATCH('.+')('hello')
- ('hello', None)
-
- >>> IS_MATCH('hell')('hello')
- ('hello', None)
-
- >>> IS_MATCH('hell.*', strict=False)('hello')
- ('hello', None)
-
- >>> IS_MATCH('hello')('shello')
- ('shello', 'invalid expression')
-
- >>> IS_MATCH('hello', search=True)('shello')
- ('shello', None)
-
- >>> IS_MATCH('hello', search=True, strict=False)('shellox')
- ('shellox', None)
-
- >>> IS_MATCH('.*hello.*', search=True, strict=False)('shellox')
- ('shellox', None)
-
- >>> IS_MATCH('.+')('')
- ('', 'invalid expression')
-
- """
-
- def __init__(self, expression, error_message='Invalid expression',
- strict=False, search=False, extract=False,
- is_unicode=False):
-
- if strict or not search:
- if not expression.startswith('^'):
- expression = '^(%s)' % expression
- if strict:
- if not expression.endswith('$'):
- expression = '(%s)$' % expression
- if is_unicode:
- if not isinstance(expression, unicodeT):
- expression = expression.decode('utf8')
- self.regex = re.compile(expression, re.UNICODE)
- else:
- self.regex = re.compile(expression)
- self.error_message = error_message
- self.extract = extract
- self.is_unicode = is_unicode or not PY2
-
- def validate(self, value):
- if not PY2: # PY3 convert bytes to unicode
- value = to_unicode(value)
-
- if self.is_unicode or not PY2:
- if not isinstance(value, unicodeT):
- match = self.regex.search(str(value).decode('utf8'))
- else:
- match = self.regex.search(value)
- else:
- if not isinstance(value, unicodeT):
- match = self.regex.search(str(value))
- else:
- match = self.regex.search(value.encode('utf8'))
- if match is not None:
- return self.extract and match.group() or value
- raise ValidationError(self.translator(self.error_message))
-
-
-class IS_EQUAL_TO(Validator):
- """
- Example:
- Used as::
-
- INPUT(_type='text', _name='password')
- INPUT(_type='text', _name='password2',
- requires=IS_EQUAL_TO(request.vars.password))
-
- The argument of IS_EQUAL_TO is a string::
-
- >>> IS_EQUAL_TO('aaa')('aaa')
- ('aaa', None)
-
- >>> IS_EQUAL_TO('aaa')('aab')
- ('aab', 'no match')
-
- """
-
- def __init__(self, expression, error_message='No match'):
- self.expression = expression
- self.error_message = error_message
-
- def validate(self, value):
- if value != self.expression:
- raise ValidationError(self.translator(self.error_message))
- return value
-
-
-class IS_EXPR(Validator):
- """
- Example:
- Used as::
-
- INPUT(_type='text', _name='name',
- requires=IS_EXPR('5 < int(value) < 10'))
-
- The argument of IS_EXPR must be python condition::
-
- >>> IS_EXPR('int(value) < 2')('1')
- ('1', None)
-
- >>> IS_EXPR('int(value) < 2')('2')
- ('2', 'invalid expression')
-
- """
-
- def __init__(self, expression, error_message='Invalid expression', environment=None):
- self.expression = expression
- self.error_message = error_message
- self.environment = environment or {}
-
- def validate(self, value):
- if callable(self.expression):
- message = self.expression(value)
- if message:
- raise ValidationError(message)
- return value
- # for backward compatibility
- self.environment.update(value=value)
- exec('__ret__=' + self.expression, self.environment)
- if self.environment['__ret__']:
- return value
- raise ValidationError(self.translator(self.error_message))
-
-
-class IS_LENGTH(Validator):
- """
- Checks if length of field's value fits between given boundaries. Works
- for both text and file inputs.
-
- Args:
- maxsize: maximum allowed length / size
- minsize: minimum allowed length / size
-
- Examples:
- Check if text string is shorter than 33 characters::
-
- INPUT(_type='text', _name='name', requires=IS_LENGTH(32))
-
- Check if password string is longer than 5 characters::
-
- INPUT(_type='password', _name='name', requires=IS_LENGTH(minsize=6))
-
- Check if uploaded file has size between 1KB and 1MB::
-
- INPUT(_type='file', _name='name', requires=IS_LENGTH(1048576, 1024))
-
- Other examples::
-
- >>> IS_LENGTH()('')
- ('', None)
- >>> IS_LENGTH()('1234567890')
- ('1234567890', None)
- >>> IS_LENGTH(maxsize=5, minsize=0)('1234567890') # too long
- ('1234567890', 'enter from 0 to 5 characters')
- >>> IS_LENGTH(maxsize=50, minsize=20)('1234567890') # too short
- ('1234567890', 'enter from 20 to 50 characters')
- """
-
- def __init__(self, maxsize=255, minsize=0,
- error_message='Enter from %(min)g to %(max)g characters'):
- self.maxsize = maxsize
- self.minsize = minsize
- self.error_message = error_message
-
- def validate(self, value):
- if value is None:
- length = 0
- elif isinstance(value, str):
- try:
- length = len(to_unicode(value))
- except:
- length = len(value)
- elif isinstance(value, unicodeT):
- length = len(value)
- value = value.encode('utf8')
- elif isinstance(value, (bytes, bytearray, tuple, list)):
- length = len(value)
- elif isinstance(value, cgi.FieldStorage):
- if value.file:
- value.file.seek(0, os.SEEK_END)
- length = value.file.tell()
- value.file.seek(0, os.SEEK_SET)
- elif hasattr(value, 'value'):
- val = value.value
- if val:
- length = len(val)
- else:
- length = 0
- else:
- value = str(value)
- length = len(str(value))
- if self.minsize <= length <= self.maxsize:
- return value
- raise ValidationError(self.translator(self.error_message) % dict(min=self.minsize, max=self.maxsize))
-
-
-class IS_JSON(Validator):
- """
- Example:
- Used as::
-
- INPUT(_type='text', _name='name',
- requires=IS_JSON(error_message="This is not a valid json input")
-
- >>> IS_JSON()('{"a": 100}')
- ({u'a': 100}, None)
-
- >>> IS_JSON()('spam1234')
- ('spam1234', 'invalid json')
- """
-
- def __init__(self, error_message='Invalid json', native_json=False):
- self.native_json = native_json
- self.error_message = error_message
-
- def validate(self, value):
- try:
- if self.native_json:
- json.loads(value) # raises error in case of malformed json
- return value # the serialized value is not passed
- else:
- return json.loads(value)
- except JSONErrors:
- raise ValidationError(self.translator(self.error_message))
-
- def formatter(self, value):
- if value is None:
- return None
- if self.native_json:
- return value
- else:
- return json.dumps(value)
-
-
-class IS_IN_SET(Validator):
- """
- Example:
- Used as::
-
- INPUT(_type='text', _name='name',
- requires=IS_IN_SET(['max', 'john'],zero=''))
-
- The argument of IS_IN_SET must be a list or set::
-
- >>> IS_IN_SET(['max', 'john'])('max')
- ('max', None)
- >>> IS_IN_SET(['max', 'john'])('massimo')
- ('massimo', 'value not allowed')
- >>> IS_IN_SET(['max', 'john'], multiple=True)(('max', 'john'))
- (('max', 'john'), None)
- >>> IS_IN_SET(['max', 'john'], multiple=True)(('bill', 'john'))
- (('bill', 'john'), 'value not allowed')
- >>> IS_IN_SET(('id1','id2'), ['first label','second label'])('id1') # Traditional way
- ('id1', None)
- >>> IS_IN_SET({'id1':'first label', 'id2':'second label'})('id1')
- ('id1', None)
- >>> import itertools
- >>> IS_IN_SET(itertools.chain(['1','3','5'],['2','4','6']))('1')
- ('1', None)
- >>> IS_IN_SET([('id1','first label'), ('id2','second label')])('id1') # Redundant way
- ('id1', None)
-
- """
-
- def __init__(self,
- theset,
- labels=None,
- error_message='Value not allowed',
- multiple=False,
- zero='',
- sort=False):
-
- self.multiple = multiple
- if isinstance(theset, dict):
- self.theset = [str(item) for item in theset]
- self.labels = list(theset.values())
- elif theset and isinstance(theset, (tuple, list)) \
- and isinstance(theset[0], (tuple, list)) and len(theset[0]) == 2:
- self.theset = [str(item) for item, label in theset]
- self.labels = [str(label) for item, label in theset]
- else:
- self.theset = [str(item) for item in theset]
- self.labels = labels
- self.error_message = error_message
- self.zero = zero
- self.sort = sort
-
- def options(self, zero=True):
- if not self.labels:
- items = [(k, k) for (i, k) in enumerate(self.theset)]
- else:
- items = [(k, list(self.labels)[i]) for (i, k) in enumerate(self.theset)]
- if self.sort:
- items.sort(key=lambda o: str(o[1]).upper())
- if zero and self.zero is not None and not self.multiple:
- items.insert(0, ('', self.zero))
- return items
-
- def validate(self, value):
- if self.multiple:
- # if below was values = re.compile("[\w\-:]+").findall(str(value))
- if not value:
- values = []
- elif isinstance(value, (tuple, list)):
- values = value
- else:
- values = [value]
- else:
- values = [value]
- thestrset = [str(x) for x in self.theset]
- failures = [x for x in values if not str(x) in thestrset]
- if failures and self.theset:
- raise ValidationError(self.translator(self.error_message))
- if self.multiple:
- if isinstance(self.multiple, (tuple, list)) and \
- not self.multiple[0] <= len(values) < self.multiple[1]:
- raise ValidationError(self.translator(self.error_message))
- return values
- return value
-
-
-regex1 = re.compile(r'\w+\.\w+')
-regex2 = re.compile(r'%\(([^\)]+)\)\d*(?:\.\d+)?[a-zA-Z]')
-
-
-class IS_IN_DB(Validator):
- """
- Example:
- Used as::
-
- INPUT(_type='text', _name='name',
- requires=IS_IN_DB(db, db.mytable.myfield, zero=''))
-
- used for reference fields, rendered as a dropbox
- """
-
- def __init__(self,
- dbset,
- field,
- label=None,
- error_message='Value not in database',
- orderby=None,
- groupby=None,
- distinct=None,
- cache=None,
- multiple=False,
- zero='',
- sort=False,
- _and=None,
- left=None,
- delimiter=None,
- auto_add=False):
-
- if hasattr(dbset, 'define_table'):
- self.dbset = dbset()
- else:
- self.dbset = dbset
-
- if isinstance(field, Table):
- field = field._id
- elif isinstance(field, str):
- items = field.split('.')
- if len(items) == 1:
- field = items[0] + '.id'
-
- (ktable, kfield) = str(field).split('.')
- if not label:
- label = '%%(%s)s' % kfield
- if isinstance(label, str):
- if regex1.match(str(label)):
- label = '%%(%s)s' % str(label).split('.')[-1]
- fieldnames = regex2.findall(label)
- if kfield not in fieldnames:
- fieldnames.append(kfield) # kfield must be last
- elif isinstance(label, Field):
- fieldnames = [label.name, kfield] # kfield must be last
- label = '%%(%s)s' % label.name
- elif callable(label):
- fieldnames = '*'
- else:
- raise NotImplementedError
-
- self.fieldnames = fieldnames # fields requires to build the formatting
- self.label = label
- self.ktable = ktable
- self.kfield = kfield
- self.error_message = error_message
- self.theset = None
- self.orderby = orderby
- self.groupby = groupby
- self.distinct = distinct
- self.cache = cache
- self.multiple = multiple
- self.zero = zero
- self.sort = sort
- self._and = _and
- self.left = left
- self.delimiter = delimiter
- self.auto_add = auto_add
-
- def set_self_id(self, id):
- if self._and:
- self._and.record_id = id
-
- def build_set(self):
- table = self.dbset.db[self.ktable]
- if self.fieldnames == '*':
- fields = [f for f in table]
- else:
- fields = [table[k] for k in self.fieldnames]
- ignore = (FieldVirtual, FieldMethod)
- fields = [f for f in fields if not isinstance(f, ignore)]
- if self.dbset.db._dbname != 'gae':
- orderby = self.orderby or reduce(lambda a, b: a | b, fields)
- groupby = self.groupby
- distinct = self.distinct
- left = self.left
- dd = dict(orderby=orderby, groupby=groupby,
- distinct=distinct, cache=self.cache,
- cacheable=True, left=left)
- records = self.dbset(table).select(*fields, **dd)
- else:
- orderby = self.orderby or \
- reduce(lambda a, b: a | b, (
- f for f in fields if not f.name == 'id'))
- dd = dict(orderby=orderby, cache=self.cache, cacheable=True)
- records = self.dbset(table).select(table.ALL, **dd)
- self.theset = [str(r[self.kfield]) for r in records]
- if isinstance(self.label, str):
- self.labels = [self.label % r for r in records]
- else:
- self.labels = [self.label(r) for r in records]
-
- def options(self, zero=True):
- self.build_set()
- items = [(k, self.labels[i]) for (i, k) in enumerate(self.theset)]
- if self.sort:
- items.sort(key=lambda o: str(o[1]).upper())
- if zero and self.zero is not None and not self.multiple:
- items.insert(0, ('', self.zero))
- return items
-
- def maybe_add(self, table, fieldname, value):
- d = {fieldname: value}
- record = table(**d)
- if record:
- return record.id
- else:
- return table.insert(**d)
-
- def validate(self, value):
- table = self.dbset.db[self.ktable]
- field = table[self.kfield]
-
- if self.multiple:
- if self._and:
- raise NotImplementedError
- if isinstance(value, list):
- values = value
- elif self.delimiter:
- values = value.split(self.delimiter) # because of autocomplete
- elif value:
- values = [value]
- else:
- values = []
-
- if field.type in ('id', 'integer'):
- new_values = []
- for value in values:
- if not (isinstance(value, integer_types) or value.isdigit()):
- if self.auto_add:
- value = str(self.maybe_add(table, self.fieldnames[0], value))
- else:
- raise ValidationError(self.translator(self.error_message))
- new_values.append(value)
- values = new_values
-
- if isinstance(self.multiple, (tuple, list)) and \
- not self.multiple[0] <= len(values) < self.multiple[1]:
- raise ValidationError(self.translator(self.error_message))
- if self.theset:
- if not [v for v in values if v not in self.theset]:
- return values
- else:
- def count(values, s=self.dbset, f=field):
- return s(f.belongs(list(map(int, values)))).count()
-
- if self.dbset.db._adapter.dbengine == "google:datastore":
- range_ids = range(0, len(values), 30)
- total = sum(count(values[i:i + 30]) for i in range_ids)
- if total == len(values):
- return values
- elif count(values) == len(values):
- return values
- else:
- if field.type in ('id', 'integer'):
- if isinstance(value, integer_types) or (isinstance(value, string_types) and value.isdigit()):
- value = int(value)
- elif self.auto_add:
- value = self.maybe_add(table, self.fieldnames[0], value)
- else:
- raise ValidationError(self.translator(self.error_message))
-
- try:
- value = int(value)
- except TypeError:
- raise ValidationError(self.translator(self.error_message))
-
- if self.theset:
- if str(value) in self.theset:
- if self._and:
- return self._and.validate(value)
- return value
- else:
- if self.dbset(field == value).count():
- if self._and:
- return self._and.validate(value)
- return value
- raise ValidationError(self.translator(self.error_message))
-
-
-class IS_NOT_IN_DB(Validator):
- """
- Example:
- Used as::
-
- INPUT(_type='text', _name='name', requires=IS_NOT_IN_DB(db, db.table))
-
- makes the field unique
- """
-
- def __init__(self,
- dbset,
- field,
- error_message='Value already in database or empty',
- allowed_override=[],
- ignore_common_filters=False):
-
- if isinstance(field, Table):
- field = field._id
-
- if hasattr(dbset, 'define_table'):
- self.dbset = dbset()
- else:
- self.dbset = dbset
- self.field = field
- self.error_message = error_message
- self.record_id = 0
- self.allowed_override = allowed_override
- self.ignore_common_filters = ignore_common_filters
-
- def set_self_id(self, id):
- self.record_id = id
-
- def validate(self, value):
- value = to_native(str(value))
- if not value.strip():
- raise ValidationError(self.translator(self.error_message))
- if value in self.allowed_override:
- return value
- (tablename, fieldname) = str(self.field).split('.')
- table = self.dbset.db[tablename]
- field = table[fieldname]
- subset = self.dbset(field == value,
- ignore_common_filters=self.ignore_common_filters)
- id = self.record_id
- if isinstance(id, dict):
- fields = [table[f] for f in id]
- row = subset.select(*fields, **dict(limitby=(0, 1), orderby_on_limitby=False)).first()
- if row and any(str(row[f]) != str(id[f]) for f in id):
- raise ValidationError(self.translator(self.error_message))
- else:
- row = subset.select(table._id, field, limitby=(0, 1), orderby_on_limitby=False).first()
- if row and str(row[table._id]) != str(id):
- raise ValidationError(self.translator(self.error_message))
- return value
-
-
-def range_error_message(error_message, what_to_enter, minimum, maximum):
- """build the error message for the number range validators"""
- if error_message is None:
- error_message = 'Enter ' + what_to_enter
- if minimum is not None and maximum is not None:
- error_message += ' between %(min)g and %(max)g'
- elif minimum is not None:
- error_message += ' greater than or equal to %(min)g'
- elif maximum is not None:
- error_message += ' less than or equal to %(max)g'
- if type(maximum) in integer_types:
- maximum -= 1
- return translate(error_message) % dict(min=minimum, max=maximum)
-
-
-class IS_INT_IN_RANGE(Validator):
- """
- Determines that the argument is (or can be represented as) an int,
- and that it falls within the specified range. The range is interpreted
- in the Pythonic way, so the test is: min <= value < max.
-
- The minimum and maximum limits can be None, meaning no lower or upper limit,
- respectively.
-
- Example:
- Used as::
-
- INPUT(_type='text', _name='name', requires=IS_INT_IN_RANGE(0, 10))
-
- >>> IS_INT_IN_RANGE(1,5)('4')
- (4, None)
- >>> IS_INT_IN_RANGE(1,5)(4)
- (4, None)
- >>> IS_INT_IN_RANGE(1,5)(1)
- (1, None)
- >>> IS_INT_IN_RANGE(1,5)(5)
- (5, 'enter an integer between 1 and 4')
- >>> IS_INT_IN_RANGE(1,5)(5)
- (5, 'enter an integer between 1 and 4')
- >>> IS_INT_IN_RANGE(1,5)(3.5)
- (3.5, 'enter an integer between 1 and 4')
- >>> IS_INT_IN_RANGE(None,5)('4')
- (4, None)
- >>> IS_INT_IN_RANGE(None,5)('6')
- ('6', 'enter an integer less than or equal to 4')
- >>> IS_INT_IN_RANGE(1,None)('4')
- (4, None)
- >>> IS_INT_IN_RANGE(1,None)('0')
- ('0', 'enter an integer greater than or equal to 1')
- >>> IS_INT_IN_RANGE()(6)
- (6, None)
- >>> IS_INT_IN_RANGE()('abc')
- ('abc', 'enter an integer')
- """
-
- def __init__(self,
- minimum=None,
- maximum=None,
- error_message=None):
-
- self.minimum = int(minimum) if minimum is not None else None
- self.maximum = int(maximum) if maximum is not None else None
- self.error_message = range_error_message(
- error_message, 'an integer', self.minimum, self.maximum)
-
- def validate(self, value):
- if regex_isint.match(str(value)):
- v = int(value)
- if ((self.minimum is None or v >= self.minimum) and
- (self.maximum is None or v < self.maximum)):
- return v
- raise ValidationError(self.error_message)
-
-
-def str2dec(number):
- s = str(number)
- if '.' not in s:
- s += '.00'
- else:
- s += '0' * (2 - len(s.split('.')[1]))
- return s
-
-
-class IS_FLOAT_IN_RANGE(Validator):
- """
- Determines that the argument is (or can be represented as) a float,
- and that it falls within the specified inclusive range.
- The comparison is made with native arithmetic.
-
- The minimum and maximum limits can be None, meaning no lower or upper limit,
- respectively.
-
- Example:
- Used as::
-
- INPUT(_type='text', _name='name', requires=IS_FLOAT_IN_RANGE(0, 10))
-
- >>> IS_FLOAT_IN_RANGE(1,5)('4')
- (4.0, None)
- >>> IS_FLOAT_IN_RANGE(1,5)(4)
- (4.0, None)
- >>> IS_FLOAT_IN_RANGE(1,5)(1)
- (1.0, None)
- >>> IS_FLOAT_IN_RANGE(1,5)(5.25)
- (5.25, 'enter a number between 1 and 5')
- >>> IS_FLOAT_IN_RANGE(1,5)(6.0)
- (6.0, 'enter a number between 1 and 5')
- >>> IS_FLOAT_IN_RANGE(1,5)(3.5)
- (3.5, None)
- >>> IS_FLOAT_IN_RANGE(1,None)(3.5)
- (3.5, None)
- >>> IS_FLOAT_IN_RANGE(None,5)(3.5)
- (3.5, None)
- >>> IS_FLOAT_IN_RANGE(1,None)(0.5)
- (0.5, 'enter a number greater than or equal to 1')
- >>> IS_FLOAT_IN_RANGE(None,5)(6.5)
- (6.5, 'enter a number less than or equal to 5')
- >>> IS_FLOAT_IN_RANGE()(6.5)
- (6.5, None)
- >>> IS_FLOAT_IN_RANGE()('abc')
- ('abc', 'enter a number')
- """
-
- def __init__(self,
- minimum=None,
- maximum=None,
- error_message=None,
- dot='.'):
-
- self.minimum = float(minimum) if minimum is not None else None
- self.maximum = float(maximum) if maximum is not None else None
- self.dot = str(dot)
- self.error_message = range_error_message(
- error_message, 'a number', self.minimum, self.maximum)
-
- def validate(self, value):
- try:
- if self.dot == '.':
- v = float(value)
- else:
- v = float(str(value).replace(self.dot, '.'))
- if ((self.minimum is None or v >= self.minimum) and
- (self.maximum is None or v <= self.maximum)):
- return v
- except (ValueError, TypeError):
- pass
- raise ValidationError(self.error_message)
-
- def formatter(self, value):
- if value is None:
- return None
- return str2dec(value).replace('.', self.dot)
-
-
-class IS_DECIMAL_IN_RANGE(Validator):
- """
- Determines that the argument is (or can be represented as) a Python Decimal,
- and that it falls within the specified inclusive range.
- The comparison is made with Python Decimal arithmetic.
-
- The minimum and maximum limits can be None, meaning no lower or upper limit,
- respectively.
-
- Example:
- Used as::
-
- INPUT(_type='text', _name='name', requires=IS_DECIMAL_IN_RANGE(0, 10))
-
- >>> IS_DECIMAL_IN_RANGE(1,5)('4')
- (Decimal('4'), None)
- >>> IS_DECIMAL_IN_RANGE(1,5)(4)
- (Decimal('4'), None)
- >>> IS_DECIMAL_IN_RANGE(1,5)(1)
- (Decimal('1'), None)
- >>> IS_DECIMAL_IN_RANGE(1,5)(5.25)
- (5.25, 'enter a number between 1 and 5')
- >>> IS_DECIMAL_IN_RANGE(5.25,6)(5.25)
- (Decimal('5.25'), None)
- >>> IS_DECIMAL_IN_RANGE(5.25,6)('5.25')
- (Decimal('5.25'), None)
- >>> IS_DECIMAL_IN_RANGE(1,5)(6.0)
- (6.0, 'enter a number between 1 and 5')
- >>> IS_DECIMAL_IN_RANGE(1,5)(3.5)
- (Decimal('3.5'), None)
- >>> IS_DECIMAL_IN_RANGE(1.5,5.5)(3.5)
- (Decimal('3.5'), None)
- >>> IS_DECIMAL_IN_RANGE(1.5,5.5)(6.5)
- (6.5, 'enter a number between 1.5 and 5.5')
- >>> IS_DECIMAL_IN_RANGE(1.5,None)(6.5)
- (Decimal('6.5'), None)
- >>> IS_DECIMAL_IN_RANGE(1.5,None)(0.5)
- (0.5, 'enter a number greater than or equal to 1.5')
- >>> IS_DECIMAL_IN_RANGE(None,5.5)(4.5)
- (Decimal('4.5'), None)
- >>> IS_DECIMAL_IN_RANGE(None,5.5)(6.5)
- (6.5, 'enter a number less than or equal to 5.5')
- >>> IS_DECIMAL_IN_RANGE()(6.5)
- (Decimal('6.5'), None)
- >>> IS_DECIMAL_IN_RANGE(0,99)(123.123)
- (123.123, 'enter a number between 0 and 99')
- >>> IS_DECIMAL_IN_RANGE(0,99)('123.123')
- ('123.123', 'enter a number between 0 and 99')
- >>> IS_DECIMAL_IN_RANGE(0,99)('12.34')
- (Decimal('12.34'), None)
- >>> IS_DECIMAL_IN_RANGE()('abc')
- ('abc', 'enter a number')
- """
-
- def __init__(self,
- minimum=None,
- maximum=None,
- error_message=None,
- dot='.'):
-
- self.minimum = decimal.Decimal(str(minimum)) if minimum is not None else None
- self.maximum = decimal.Decimal(str(maximum)) if maximum is not None else None
- self.dot = str(dot)
- self.error_message = range_error_message(
- error_message, 'a number', self.minimum, self.maximum)
-
- def validate(self, value):
- try:
- if not isinstance(value, decimal.Decimal):
- value = decimal.Decimal(str(value).replace(self.dot, '.'))
- if ((self.minimum is None or value >= self.minimum) and
- (self.maximum is None or value <= self.maximum)):
- return value
- except (ValueError, TypeError, decimal.InvalidOperation):
- pass
- raise ValidationError(self.error_message)
-
- def formatter(self, value):
- if value is None:
- return None
- return str2dec(value).replace('.', self.dot)
-
-
-def is_empty(value, empty_regex=None):
- _value = value
- """test empty field"""
- if isinstance(value, (str, unicodeT)):
- value = value.strip()
- if empty_regex is not None and empty_regex.match(value):
- value = ''
- if value is None or value == '' or value == b'' or value == []:
- return (_value, True)
- return (_value, False)
-
-
-class IS_NOT_EMPTY(Validator):
- """
- Example:
- Used as::
-
- INPUT(_type='text', _name='name', requires=IS_NOT_EMPTY())
-
- >>> IS_NOT_EMPTY()(1)
- (1, None)
- >>> IS_NOT_EMPTY()(0)
- (0, None)
- >>> IS_NOT_EMPTY()('x')
- ('x', None)
- >>> IS_NOT_EMPTY()(' x ')
- ('x', None)
- >>> IS_NOT_EMPTY()(None)
- (None, 'enter a value')
- >>> IS_NOT_EMPTY()('')
- ('', 'enter a value')
- >>> IS_NOT_EMPTY()(' ')
- ('', 'enter a value')
- >>> IS_NOT_EMPTY()(' \\n\\t')
- ('', 'enter a value')
- >>> IS_NOT_EMPTY()([])
- ([], 'enter a value')
- >>> IS_NOT_EMPTY(empty_regex='def')('def')
- ('', 'enter a value')
- >>> IS_NOT_EMPTY(empty_regex='de[fg]')('deg')
- ('', 'enter a value')
- >>> IS_NOT_EMPTY(empty_regex='def')('abc')
- ('abc', None)
- """
-
- def __init__(self, error_message='Enter a value', empty_regex=None):
- self.error_message = error_message
- if empty_regex is not None:
- self.empty_regex = re.compile(empty_regex)
- else:
- self.empty_regex = None
-
- def validate(self, value):
- value, empty = is_empty(value, empty_regex=self.empty_regex)
- if empty:
- raise ValidationError(self.translator(self.error_message))
- return value
-
-
-class IS_ALPHANUMERIC(IS_MATCH):
- """
- Example:
- Used as::
-
- INPUT(_type='text', _name='name', requires=IS_ALPHANUMERIC())
-
- >>> IS_ALPHANUMERIC()('1')
- ('1', None)
- >>> IS_ALPHANUMERIC()('')
- ('', None)
- >>> IS_ALPHANUMERIC()('A_a')
- ('A_a', None)
- >>> IS_ALPHANUMERIC()('!')
- ('!', 'enter only letters, numbers, and underscore')
- """
-
- def __init__(self, error_message='Enter only letters, numbers, and underscore'):
- IS_MATCH.__init__(self, r'^[\w]*$', error_message)
-
-
-class IS_EMAIL(Validator):
-
- """
- Checks if field's value is a valid email address. Can be set to disallow
- or force addresses from certain domain(s).
-
- Email regex adapted from
- http://haacked.com/archive/2007/08/21/i-knew-how-to-validate-an-email-address-until-i.aspx,
- generally following the RFCs, except that we disallow quoted strings
- and permit underscores and leading numerics in subdomain labels
-
- Args:
- banned: regex text for disallowed address domains
- forced: regex text for required address domains
-
- Both arguments can also be custom objects with a match(value) method.
-
- Example:
- Check for valid email address::
-
- INPUT(_type='text', _name='name',
- requires=IS_EMAIL())
-
- Check for valid email address that can't be from a .com domain::
-
- INPUT(_type='text', _name='name',
- requires=IS_EMAIL(banned='^.*\\.com(|\\..*)$'))
-
- Check for valid email address that must be from a .edu domain::
-
- INPUT(_type='text', _name='name',
- requires=IS_EMAIL(forced='^.*\\.edu(|\\..*)$'))
-
- >>> IS_EMAIL()('a@b.com')
- ('a@b.com', None)
- >>> IS_EMAIL()('abc@def.com')
- ('abc@def.com', None)
- >>> IS_EMAIL()('abc@3def.com')
- ('abc@3def.com', None)
- >>> IS_EMAIL()('abc@def.us')
- ('abc@def.us', None)
- >>> IS_EMAIL()('abc@d_-f.us')
- ('abc@d_-f.us', None)
- >>> IS_EMAIL()('@def.com') # missing name
- ('@def.com', 'enter a valid email address')
- >>> IS_EMAIL()('"abc@def".com') # quoted name
- ('"abc@def".com', 'enter a valid email address')
- >>> IS_EMAIL()('abc+def.com') # no @
- ('abc+def.com', 'enter a valid email address')
- >>> IS_EMAIL()('abc@def.x') # one-char TLD
- ('abc@def.x', 'enter a valid email address')
- >>> IS_EMAIL()('abc@def.12') # numeric TLD
- ('abc@def.12', 'enter a valid email address')
- >>> IS_EMAIL()('abc@def..com') # double-dot in domain
- ('abc@def..com', 'enter a valid email address')
- >>> IS_EMAIL()('abc@.def.com') # dot starts domain
- ('abc@.def.com', 'enter a valid email address')
- >>> IS_EMAIL()('abc@def.c_m') # underscore in TLD
- ('abc@def.c_m', 'enter a valid email address')
- >>> IS_EMAIL()('NotAnEmail') # missing @
- ('NotAnEmail', 'enter a valid email address')
- >>> IS_EMAIL()('abc@NotAnEmail') # missing TLD
- ('abc@NotAnEmail', 'enter a valid email address')
- >>> IS_EMAIL()('customer/department@example.com')
- ('customer/department@example.com', None)
- >>> IS_EMAIL()('$A12345@example.com')
- ('$A12345@example.com', None)
- >>> IS_EMAIL()('!def!xyz%abc@example.com')
- ('!def!xyz%abc@example.com', None)
- >>> IS_EMAIL()('_Yosemite.Sam@example.com')
- ('_Yosemite.Sam@example.com', None)
- >>> IS_EMAIL()('~@example.com')
- ('~@example.com', None)
- >>> IS_EMAIL()('.wooly@example.com') # dot starts name
- ('.wooly@example.com', 'enter a valid email address')
- >>> IS_EMAIL()('wo..oly@example.com') # adjacent dots in name
- ('wo..oly@example.com', 'enter a valid email address')
- >>> IS_EMAIL()('pootietang.@example.com') # dot ends name
- ('pootietang.@example.com', 'enter a valid email address')
- >>> IS_EMAIL()('.@example.com') # name is bare dot
- ('.@example.com', 'enter a valid email address')
- >>> IS_EMAIL()('Ima.Fool@example.com')
- ('Ima.Fool@example.com', None)
- >>> IS_EMAIL()('Ima Fool@example.com') # space in name
- ('Ima Fool@example.com', 'enter a valid email address')
- >>> IS_EMAIL()('localguy@localhost') # localhost as domain
- ('localguy@localhost', None)
-
- """
-
- body_regex = re.compile(r'''
- ^(?!\.) # name may not begin with a dot
- (
- [-a-z0-9!\#$%&'*+/=?^_`{|}~] # all legal characters except dot
- |
- (? obtained on 2008-Nov-10
-
-official_url_schemes = [
- 'aaa',
- 'aaas',
- 'acap',
- 'cap',
- 'cid',
- 'crid',
- 'data',
- 'dav',
- 'dict',
- 'dns',
- 'fax',
- 'file',
- 'ftp',
- 'go',
- 'gopher',
- 'h323',
- 'http',
- 'https',
- 'icap',
- 'im',
- 'imap',
- 'info',
- 'ipp',
- 'iris',
- 'iris.beep',
- 'iris.xpc',
- 'iris.xpcs',
- 'iris.lws',
- 'ldap',
- 'mailto',
- 'mid',
- 'modem',
- 'msrp',
- 'msrps',
- 'mtqp',
- 'mupdate',
- 'news',
- 'nfs',
- 'nntp',
- 'opaquelocktoken',
- 'pop',
- 'pres',
- 'prospero',
- 'rtsp',
- 'service',
- 'shttp',
- 'sip',
- 'sips',
- 'snmp',
- 'soap.beep',
- 'soap.beeps',
- 'tag',
- 'tel',
- 'telnet',
- 'tftp',
- 'thismessage',
- 'tip',
- 'tv',
- 'urn',
- 'vemmi',
- 'wais',
- 'xmlrpc.beep',
- 'xmlrpc.beep',
- 'xmpp',
- 'z39.50r',
- 'z39.50s',
-]
-unofficial_url_schemes = [
- 'about',
- 'adiumxtra',
- 'aim',
- 'afp',
- 'aw',
- 'callto',
- 'chrome',
- 'cvs',
- 'ed2k',
- 'feed',
- 'fish',
- 'gg',
- 'gizmoproject',
- 'iax2',
- 'irc',
- 'ircs',
- 'itms',
- 'jar',
- 'javascript',
- 'keyparc',
- 'lastfm',
- 'ldaps',
- 'magnet',
- 'mms',
- 'msnim',
- 'mvn',
- 'notes',
- 'nsfw',
- 'psyc',
- 'paparazzi:http',
- 'rmi',
- 'rsync',
- 'secondlife',
- 'sgn',
- 'skype',
- 'ssh',
- 'sftp',
- 'smb',
- 'sms',
- 'soldat',
- 'steam',
- 'svn',
- 'teamspeak',
- 'unreal',
- 'ut2004',
- 'ventrilo',
- 'view-source',
- 'webcal',
- 'wyciwyg',
- 'xfire',
- 'xri',
- 'ymsgr',
-]
-all_url_schemes = [None] + official_url_schemes + unofficial_url_schemes
-http_schemes = [None, 'http', 'https']
-
-# Defined in RFC 3490, Section 3.1, Requirement #1
-# Use this regex to split the authority component of a unicode URL into
-# its component labels
-label_split_regex = re.compile(u'[\u002e\u3002\uff0e\uff61]')
-
-
-def escape_unicode(string):
-
- """
- Converts a unicode string into US-ASCII, using a simple conversion scheme.
- Each unicode character that does not have a US-ASCII equivalent is
- converted into a URL escaped form based on its hexadecimal value.
- For example, the unicode character '\\u4e86' will become the string '%4e%86'
-
- Args:
- string: unicode string, the unicode string to convert into an
- escaped US-ASCII form
-
- Returns:
- string: the US-ASCII escaped form of the inputted string
-
- @author: Jonathan Benn
- """
- returnValue = StringIO()
-
- for character in string:
- code = ord(character)
- if code > 0x7F:
- hexCode = hex(code)
- returnValue.write('%' + hexCode[2:4] + '%' + hexCode[4:6])
- else:
- returnValue.write(character)
-
- return returnValue.getvalue()
-
-
-def unicode_to_ascii_authority(authority):
- """
- Follows the steps in RFC 3490, Section 4 to convert a unicode authority
- string into its ASCII equivalent.
- For example, u'www.Alliancefran\\xe7aise.nu' will be converted into
- 'www.xn--alliancefranaise-npb.nu'
-
- Args:
- authority: unicode string, the URL authority component to convert,
- e.g. u'www.Alliancefran\\xe7aise.nu'
-
- Returns:
- string: the US-ASCII character equivalent to the inputed authority,
- e.g. 'www.xn--alliancefranaise-npb.nu'
-
- Raises:
- Exception: if the function is not able to convert the inputed
- authority
-
- @author: Jonathan Benn
- """
- # RFC 3490, Section 4, Step 1
- # The encodings.idna Python module assumes that AllowUnassigned == True
-
- # RFC 3490, Section 4, Step 2
- labels = label_split_regex.split(authority)
-
- # RFC 3490, Section 4, Step 3
- # The encodings.idna Python module assumes that UseSTD3ASCIIRules == False
-
- # RFC 3490, Section 4, Step 4
- # We use the ToASCII operation because we are about to put the authority
- # into an IDN-unaware slot
- asciiLabels = []
- for label in labels:
- if label:
- asciiLabels.append(to_native(encodings.idna.ToASCII(label)))
- else:
- # encodings.idna.ToASCII does not accept an empty string, but
- # it is necessary for us to allow for empty labels so that we
- # don't modify the URL
- asciiLabels.append('')
- # RFC 3490, Section 4, Step 5
- return str(reduce(lambda x, y: x + unichr(0x002E) + y, asciiLabels))
-
-
-def unicode_to_ascii_url(url, prepend_scheme):
- """
- Converts the inputed unicode url into a US-ASCII equivalent. This function
- goes a little beyond RFC 3490, which is limited in scope to the domain name
- (authority) only. Here, the functionality is expanded to what was observed
- on Wikipedia on 2009-Jan-22:
-
- Component Can Use Unicode?
- --------- ----------------
- scheme No
- authority Yes
- path Yes
- query Yes
- fragment No
-
- The authority component gets converted to punycode, but occurrences of
- unicode in other components get converted into a pair of URI escapes (we
- assume 4-byte unicode). E.g. the unicode character U+4E2D will be
- converted into '%4E%2D'. Testing with Firefox v3.0.5 has shown that it can
- understand this kind of URI encoding.
-
- Args:
- url: unicode string, the URL to convert from unicode into US-ASCII
- prepend_scheme: string, a protocol scheme to prepend to the URL if
- we're having trouble parsing it.
- e.g. "http". Input None to disable this functionality
-
- Returns:
- string: a US-ASCII equivalent of the inputed url
-
- @author: Jonathan Benn
- """
- # convert the authority component of the URL into an ASCII punycode string,
- # but encode the rest using the regular URI character encoding
- components = urlparse.urlparse(url)
- prepended = False
- # If no authority was found
- if not components.netloc:
- # Try appending a scheme to see if that fixes the problem
- scheme_to_prepend = prepend_scheme or 'http'
- components = urlparse.urlparse(to_unicode(scheme_to_prepend) + u'://' + url)
- prepended = True
-
- # if we still can't find the authority
- if not components.netloc:
- raise Exception('No authority component found, ' +
- 'could not decode unicode to US-ASCII')
-
- # We're here if we found an authority, let's rebuild the URL
- scheme = components.scheme
- authority = components.netloc
- path = components.path
- query = components.query
- fragment = components.fragment
-
- if prepended:
- scheme = ''
-
- unparsed = urlparse.urlunparse((scheme,
- unicode_to_ascii_authority(authority),
- escape_unicode(path),
- '',
- escape_unicode(query),
- str(fragment)))
- if unparsed.startswith('//'):
- unparsed = unparsed[2:] # Remove the // urlunparse puts in the beginning
- return unparsed
-
-
-class IS_GENERIC_URL(Validator):
- """
- Rejects a URL string if any of the following is true:
- * The string is empty or None
- * The string uses characters that are not allowed in a URL
- * The URL scheme specified (if one is specified) is not valid
-
- Based on RFC 2396: http://www.faqs.org/rfcs/rfc2396.html
-
- This function only checks the URL's syntax. It does not check that the URL
- points to a real document, for example, or that it otherwise makes sense
- semantically. This function does automatically prepend 'http://' in front
- of a URL if and only if that's necessary to successfully parse the URL.
- Please note that a scheme will be prepended only for rare cases
- (e.g. 'google.ca:80')
-
- The list of allowed schemes is customizable with the allowed_schemes
- parameter. If you exclude None from the list, then abbreviated URLs
- (lacking a scheme such as 'http') will be rejected.
-
- The default prepended scheme is customizable with the prepend_scheme
- parameter. If you set prepend_scheme to None then prepending will be
- disabled. URLs that require prepending to parse will still be accepted,
- but the return value will not be modified.
-
- @author: Jonathan Benn
-
- >>> IS_GENERIC_URL()('http://user@abc.com')
- ('http://user@abc.com', None)
-
- Args:
- error_message: a string, the error message to give the end user
- if the URL does not validate
- allowed_schemes: a list containing strings or None. Each element
- is a scheme the inputed URL is allowed to use
- prepend_scheme: a string, this scheme is prepended if it's
- necessary to make the URL valid
-
- """
-
- def __init__(self,
- error_message='Enter a valid URL',
- allowed_schemes=None,
- prepend_scheme=None):
-
- self.error_message = error_message
- if allowed_schemes is None:
- self.allowed_schemes = all_url_schemes
- else:
- self.allowed_schemes = allowed_schemes
- self.prepend_scheme = prepend_scheme
- if self.prepend_scheme not in self.allowed_schemes:
- raise SyntaxError("prepend_scheme='%s' is not in allowed_schemes=%s"
- % (self.prepend_scheme, self.allowed_schemes))
-
- GENERIC_URL = re.compile(r"%[^0-9A-Fa-f]{2}|%[^0-9A-Fa-f][0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]|%$|%[0-9A-Fa-f]$|%[^0-9A-Fa-f]$")
- GENERIC_URL_VALID = re.compile(r"[A-Za-z0-9;/?:@&=+$,\-_\.!~*'\(\)%]+$")
- URL_FRAGMENT_VALID = re.compile(r"[|A-Za-z0-9;/?:@&=+$,\-_\.!~*'\(\)%]+$")
-
- def validate(self, value):
- """
- Args:
- value: a string, the URL to validate
-
- Returns:
- a tuple, where tuple[0] is the inputed value (possible
- prepended with prepend_scheme), and tuple[1] is either
- None (success!) or the string error_message
- """
-
- # if we dont have anything or the URL misuses the '%' character
-
- if not value or self.GENERIC_URL.search(value):
- raise ValidationError(self.translator(self.error_message))
-
- if '#' in value:
- url, fragment_part = value.split('#')
- else:
- url, fragment_part = value, ''
- # if the URL is only composed of valid characters
- if not self.GENERIC_URL_VALID.match(url) or (fragment_part and not self.URL_FRAGMENT_VALID.match(fragment_part)):
- raise ValidationError(self.translator(self.error_message))
- # Then parse the URL into its components and check on
- try:
- components = urlparse.urlparse(urllib_unquote(value))._asdict()
- except ValueError:
- raise ValidationError(self.translator(self.error_message))
-
- # Clean up the scheme before we check it
- scheme = components['scheme']
- if len(scheme) == 0:
- scheme = None
- else:
- scheme = components['scheme'].lower()
- # If the scheme doesn't really exists
- if scheme not in self.allowed_schemes or not scheme and ':' in components['path']:
- # for the possible case of abbreviated URLs with
- # ports, check to see if adding a valid scheme fixes
- # the problem (but only do this if it doesn't have
- # one already!)
- if '://' not in value and None in self.allowed_schemes:
- schemeToUse = self.prepend_scheme or 'http'
- new_value = self.validate(schemeToUse + '://' + value)
- return new_value if self.prepend_scheme else value
- raise ValidationError(self.translator(self.error_message))
- return value
-
-# Sources (obtained 2017-Nov-11):
-# http://data.iana.org/TLD/tlds-alpha-by-domain.txt
-# see scripts/parse_top_level_domains.py for an easy update
-
-official_top_level_domains = [
- # a
- 'aaa', 'aarp', 'abarth', 'abb', 'abbott', 'abbvie', 'abc',
- 'able', 'abogado', 'abudhabi', 'ac', 'academy', 'accenture',
- 'accountant', 'accountants', 'aco', 'active', 'actor', 'ad',
- 'adac', 'ads', 'adult', 'ae', 'aeg', 'aero', 'aetna', 'af',
- 'afamilycompany', 'afl', 'africa', 'ag', 'agakhan', 'agency',
- 'ai', 'aig', 'aigo', 'airbus', 'airforce', 'airtel', 'akdn',
- 'al', 'alfaromeo', 'alibaba', 'alipay', 'allfinanz', 'allstate',
- 'ally', 'alsace', 'alstom', 'am', 'americanexpress',
- 'americanfamily', 'amex', 'amfam', 'amica', 'amsterdam',
- 'analytics', 'android', 'anquan', 'anz', 'ao', 'aol',
- 'apartments', 'app', 'apple', 'aq', 'aquarelle', 'ar', 'arab',
- 'aramco', 'archi', 'army', 'arpa', 'art', 'arte', 'as', 'asda',
- 'asia', 'associates', 'at', 'athleta', 'attorney', 'au',
- 'auction', 'audi', 'audible', 'audio', 'auspost', 'author',
- 'auto', 'autos', 'avianca', 'aw', 'aws', 'ax', 'axa', 'az',
- 'azure',
- # b
- 'ba', 'baby', 'baidu', 'banamex', 'bananarepublic', 'band',
- 'bank', 'bar', 'barcelona', 'barclaycard', 'barclays',
- 'barefoot', 'bargains', 'baseball', 'basketball', 'bauhaus',
- 'bayern', 'bb', 'bbc', 'bbt', 'bbva', 'bcg', 'bcn', 'bd', 'be',
- 'beats', 'beauty', 'beer', 'bentley', 'berlin', 'best',
- 'bestbuy', 'bet', 'bf', 'bg', 'bh', 'bharti', 'bi', 'bible',
- 'bid', 'bike', 'bing', 'bingo', 'bio', 'biz', 'bj', 'black',
- 'blackfriday', 'blanco', 'blockbuster', 'blog', 'bloomberg',
- 'blue', 'bm', 'bms', 'bmw', 'bn', 'bnl', 'bnpparibas', 'bo',
- 'boats', 'boehringer', 'bofa', 'bom', 'bond', 'boo', 'book',
- 'booking', 'boots', 'bosch', 'bostik', 'boston', 'bot',
- 'boutique', 'box', 'br', 'bradesco', 'bridgestone', 'broadway',
- 'broker', 'brother', 'brussels', 'bs', 'bt', 'budapest',
- 'bugatti', 'build', 'builders', 'business', 'buy', 'buzz', 'bv',
- 'bw', 'by', 'bz', 'bzh',
- # c
- 'ca', 'cab', 'cafe', 'cal', 'call', 'calvinklein', 'cam',
- 'camera', 'camp', 'cancerresearch', 'canon', 'capetown',
- 'capital', 'capitalone', 'car', 'caravan', 'cards', 'care',
- 'career', 'careers', 'cars', 'cartier', 'casa', 'case', 'caseih',
- 'cash', 'casino', 'cat', 'catering', 'catholic', 'cba', 'cbn',
- 'cbre', 'cbs', 'cc', 'cd', 'ceb', 'center', 'ceo', 'cern', 'cf',
- 'cfa', 'cfd', 'cg', 'ch', 'chanel', 'channel', 'chase', 'chat',
- 'cheap', 'chintai', 'christmas', 'chrome', 'chrysler', 'church',
- 'ci', 'cipriani', 'circle', 'cisco', 'citadel', 'citi', 'citic',
- 'city', 'cityeats', 'ck', 'cl', 'claims', 'cleaning', 'click',
- 'clinic', 'clinique', 'clothing', 'cloud', 'club', 'clubmed',
- 'cm', 'cn', 'co', 'coach', 'codes', 'coffee', 'college',
- 'cologne', 'com', 'comcast', 'commbank', 'community', 'company',
- 'compare', 'computer', 'comsec', 'condos', 'construction',
- 'consulting', 'contact', 'contractors', 'cooking',
- 'cookingchannel', 'cool', 'coop', 'corsica', 'country', 'coupon',
- 'coupons', 'courses', 'cr', 'credit', 'creditcard',
- 'creditunion', 'cricket', 'crown', 'crs', 'cruise', 'cruises',
- 'csc', 'cu', 'cuisinella', 'cv', 'cw', 'cx', 'cy', 'cymru',
- 'cyou', 'cz',
- # d
- 'dabur', 'dad', 'dance', 'data', 'date', 'dating', 'datsun',
- 'day', 'dclk', 'dds', 'de', 'deal', 'dealer', 'deals', 'degree',
- 'delivery', 'dell', 'deloitte', 'delta', 'democrat', 'dental',
- 'dentist', 'desi', 'design', 'dev', 'dhl', 'diamonds', 'diet',
- 'digital', 'direct', 'directory', 'discount', 'discover', 'dish',
- 'diy', 'dj', 'dk', 'dm', 'dnp', 'do', 'docs', 'doctor', 'dodge',
- 'dog', 'doha', 'domains', 'dot', 'download', 'drive', 'dtv',
- 'dubai', 'duck', 'dunlop', 'duns', 'dupont', 'durban', 'dvag',
- 'dvr', 'dz',
- # e
- 'earth', 'eat', 'ec', 'eco', 'edeka', 'edu', 'education', 'ee',
- 'eg', 'email', 'emerck', 'energy', 'engineer', 'engineering',
- 'enterprises', 'epost', 'epson', 'equipment', 'er', 'ericsson',
- 'erni', 'es', 'esq', 'estate', 'esurance', 'et', 'etisalat',
- 'eu', 'eurovision', 'eus', 'events', 'everbank', 'exchange',
- 'expert', 'exposed', 'express', 'extraspace',
- # f
- 'fage', 'fail', 'fairwinds', 'faith', 'family', 'fan', 'fans',
- 'farm', 'farmers', 'fashion', 'fast', 'fedex', 'feedback',
- 'ferrari', 'ferrero', 'fi', 'fiat', 'fidelity', 'fido', 'film',
- 'final', 'finance', 'financial', 'fire', 'firestone', 'firmdale',
- 'fish', 'fishing', 'fit', 'fitness', 'fj', 'fk', 'flickr',
- 'flights', 'flir', 'florist', 'flowers', 'fly', 'fm', 'fo',
- 'foo', 'food', 'foodnetwork', 'football', 'ford', 'forex',
- 'forsale', 'forum', 'foundation', 'fox', 'fr', 'free',
- 'fresenius', 'frl', 'frogans', 'frontdoor', 'frontier', 'ftr',
- 'fujitsu', 'fujixerox', 'fun', 'fund', 'furniture', 'futbol',
- 'fyi',
- # g
- 'ga', 'gal', 'gallery', 'gallo', 'gallup', 'game', 'games',
- 'gap', 'garden', 'gb', 'gbiz', 'gd', 'gdn', 'ge', 'gea', 'gent',
- 'genting', 'george', 'gf', 'gg', 'ggee', 'gh', 'gi', 'gift',
- 'gifts', 'gives', 'giving', 'gl', 'glade', 'glass', 'gle',
- 'global', 'globo', 'gm', 'gmail', 'gmbh', 'gmo', 'gmx', 'gn',
- 'godaddy', 'gold', 'goldpoint', 'golf', 'goo', 'goodhands',
- 'goodyear', 'goog', 'google', 'gop', 'got', 'gov', 'gp', 'gq',
- 'gr', 'grainger', 'graphics', 'gratis', 'green', 'gripe',
- 'grocery', 'group', 'gs', 'gt', 'gu', 'guardian', 'gucci',
- 'guge', 'guide', 'guitars', 'guru', 'gw', 'gy',
- # h
- 'hair', 'hamburg', 'hangout', 'haus', 'hbo', 'hdfc', 'hdfcbank',
- 'health', 'healthcare', 'help', 'helsinki', 'here', 'hermes',
- 'hgtv', 'hiphop', 'hisamitsu', 'hitachi', 'hiv', 'hk', 'hkt',
- 'hm', 'hn', 'hockey', 'holdings', 'holiday', 'homedepot',
- 'homegoods', 'homes', 'homesense', 'honda', 'honeywell', 'horse',
- 'hospital', 'host', 'hosting', 'hot', 'hoteles', 'hotels',
- 'hotmail', 'house', 'how', 'hr', 'hsbc', 'ht', 'hu', 'hughes',
- 'hyatt', 'hyundai',
- # i
- 'ibm', 'icbc', 'ice', 'icu', 'id', 'ie', 'ieee', 'ifm', 'ikano',
- 'il', 'im', 'imamat', 'imdb', 'immo', 'immobilien', 'in',
- 'industries', 'infiniti', 'info', 'ing', 'ink', 'institute',
- 'insurance', 'insure', 'int', 'intel', 'international', 'intuit',
- 'investments', 'io', 'ipiranga', 'iq', 'ir', 'irish', 'is',
- 'iselect', 'ismaili', 'ist', 'istanbul', 'it', 'itau', 'itv',
- 'iveco', 'iwc',
- # j
- 'jaguar', 'java', 'jcb', 'jcp', 'je', 'jeep', 'jetzt', 'jewelry',
- 'jio', 'jlc', 'jll', 'jm', 'jmp', 'jnj', 'jo', 'jobs', 'joburg',
- 'jot', 'joy', 'jp', 'jpmorgan', 'jprs', 'juegos', 'juniper',
- # k
- 'kaufen', 'kddi', 'ke', 'kerryhotels', 'kerrylogistics',
- 'kerryproperties', 'kfh', 'kg', 'kh', 'ki', 'kia', 'kim',
- 'kinder', 'kindle', 'kitchen', 'kiwi', 'km', 'kn', 'koeln',
- 'komatsu', 'kosher', 'kp', 'kpmg', 'kpn', 'kr', 'krd', 'kred',
- 'kuokgroup', 'kw', 'ky', 'kyoto', 'kz',
- # l
- 'la', 'lacaixa', 'ladbrokes', 'lamborghini', 'lamer',
- 'lancaster', 'lancia', 'lancome', 'land', 'landrover', 'lanxess',
- 'lasalle', 'lat', 'latino', 'latrobe', 'law', 'lawyer', 'lb',
- 'lc', 'lds', 'lease', 'leclerc', 'lefrak', 'legal', 'lego',
- 'lexus', 'lgbt', 'li', 'liaison', 'lidl', 'life',
- 'lifeinsurance', 'lifestyle', 'lighting', 'like', 'lilly',
- 'limited', 'limo', 'lincoln', 'linde', 'link', 'lipsy', 'live',
- 'living', 'lixil', 'lk', 'loan', 'loans', 'localhost', 'locker',
- 'locus', 'loft', 'lol', 'london', 'lotte', 'lotto', 'love',
- 'lpl', 'lplfinancial', 'lr', 'ls', 'lt', 'ltd', 'ltda', 'lu',
- 'lundbeck', 'lupin', 'luxe', 'luxury', 'lv', 'ly',
- # m
- 'ma', 'macys', 'madrid', 'maif', 'maison', 'makeup', 'man',
- 'management', 'mango', 'map', 'market', 'marketing', 'markets',
- 'marriott', 'marshalls', 'maserati', 'mattel', 'mba', 'mc',
- 'mckinsey', 'md', 'me', 'med', 'media', 'meet', 'melbourne',
- 'meme', 'memorial', 'men', 'menu', 'meo', 'merckmsd', 'metlife',
- 'mg', 'mh', 'miami', 'microsoft', 'mil', 'mini', 'mint', 'mit',
- 'mitsubishi', 'mk', 'ml', 'mlb', 'mls', 'mm', 'mma', 'mn', 'mo',
- 'mobi', 'mobile', 'mobily', 'moda', 'moe', 'moi', 'mom',
- 'monash', 'money', 'monster', 'mopar', 'mormon', 'mortgage',
- 'moscow', 'moto', 'motorcycles', 'mov', 'movie', 'movistar',
- 'mp', 'mq', 'mr', 'ms', 'msd', 'mt', 'mtn', 'mtr', 'mu',
- 'museum', 'mutual', 'mv', 'mw', 'mx', 'my', 'mz',
- # n
- 'na', 'nab', 'nadex', 'nagoya', 'name', 'nationwide', 'natura',
- 'navy', 'nba', 'nc', 'ne', 'nec', 'net', 'netbank', 'netflix',
- 'network', 'neustar', 'new', 'newholland', 'news', 'next',
- 'nextdirect', 'nexus', 'nf', 'nfl', 'ng', 'ngo', 'nhk', 'ni',
- 'nico', 'nike', 'nikon', 'ninja', 'nissan', 'nissay', 'nl', 'no',
- 'nokia', 'northwesternmutual', 'norton', 'now', 'nowruz',
- 'nowtv', 'np', 'nr', 'nra', 'nrw', 'ntt', 'nu', 'nyc', 'nz',
- # o
- 'obi', 'observer', 'off', 'office', 'okinawa', 'olayan',
- 'olayangroup', 'oldnavy', 'ollo', 'om', 'omega', 'one', 'ong',
- 'onl', 'online', 'onyourside', 'ooo', 'open', 'oracle', 'orange',
- 'org', 'organic', 'origins', 'osaka', 'otsuka', 'ott', 'ovh',
- # p
- 'pa', 'page', 'panasonic', 'panerai', 'paris', 'pars',
- 'partners', 'parts', 'party', 'passagens', 'pay', 'pccw', 'pe',
- 'pet', 'pf', 'pfizer', 'pg', 'ph', 'pharmacy', 'phd', 'philips',
- 'phone', 'photo', 'photography', 'photos', 'physio', 'piaget',
- 'pics', 'pictet', 'pictures', 'pid', 'pin', 'ping', 'pink',
- 'pioneer', 'pizza', 'pk', 'pl', 'place', 'play', 'playstation',
- 'plumbing', 'plus', 'pm', 'pn', 'pnc', 'pohl', 'poker',
- 'politie', 'porn', 'post', 'pr', 'pramerica', 'praxi', 'press',
- 'prime', 'pro', 'prod', 'productions', 'prof', 'progressive',
- 'promo', 'properties', 'property', 'protection', 'pru',
- 'prudential', 'ps', 'pt', 'pub', 'pw', 'pwc', 'py',
- # q
- 'qa', 'qpon', 'quebec', 'quest', 'qvc',
- # r
- 'racing', 'radio', 'raid', 're', 'read', 'realestate', 'realtor',
- 'realty', 'recipes', 'red', 'redstone', 'redumbrella', 'rehab',
- 'reise', 'reisen', 'reit', 'reliance', 'ren', 'rent', 'rentals',
- 'repair', 'report', 'republican', 'rest', 'restaurant', 'review',
- 'reviews', 'rexroth', 'rich', 'richardli', 'ricoh',
- 'rightathome', 'ril', 'rio', 'rip', 'rmit', 'ro', 'rocher',
- 'rocks', 'rodeo', 'rogers', 'room', 'rs', 'rsvp', 'ru', 'rugby',
- 'ruhr', 'run', 'rw', 'rwe', 'ryukyu',
- # s
- 'sa', 'saarland', 'safe', 'safety', 'sakura', 'sale', 'salon',
- 'samsclub', 'samsung', 'sandvik', 'sandvikcoromant', 'sanofi',
- 'sap', 'sapo', 'sarl', 'sas', 'save', 'saxo', 'sb', 'sbi', 'sbs',
- 'sc', 'sca', 'scb', 'schaeffler', 'schmidt', 'scholarships',
- 'school', 'schule', 'schwarz', 'science', 'scjohnson', 'scor',
- 'scot', 'sd', 'se', 'search', 'seat', 'secure', 'security',
- 'seek', 'select', 'sener', 'services', 'ses', 'seven', 'sew',
- 'sex', 'sexy', 'sfr', 'sg', 'sh', 'shangrila', 'sharp', 'shaw',
- 'shell', 'shia', 'shiksha', 'shoes', 'shop', 'shopping',
- 'shouji', 'show', 'showtime', 'shriram', 'si', 'silk', 'sina',
- 'singles', 'site', 'sj', 'sk', 'ski', 'skin', 'sky', 'skype',
- 'sl', 'sling', 'sm', 'smart', 'smile', 'sn', 'sncf', 'so',
- 'soccer', 'social', 'softbank', 'software', 'sohu', 'solar',
- 'solutions', 'song', 'sony', 'soy', 'space', 'spiegel', 'spot',
- 'spreadbetting', 'sr', 'srl', 'srt', 'st', 'stada', 'staples',
- 'star', 'starhub', 'statebank', 'statefarm', 'statoil', 'stc',
- 'stcgroup', 'stockholm', 'storage', 'store', 'stream', 'studio',
- 'study', 'style', 'su', 'sucks', 'supplies', 'supply', 'support',
- 'surf', 'surgery', 'suzuki', 'sv', 'swatch', 'swiftcover',
- 'swiss', 'sx', 'sy', 'sydney', 'symantec', 'systems', 'sz',
- # t
- 'tab', 'taipei', 'talk', 'taobao', 'target', 'tatamotors',
- 'tatar', 'tattoo', 'tax', 'taxi', 'tc', 'tci', 'td', 'tdk',
- 'team', 'tech', 'technology', 'tel', 'telecity', 'telefonica',
- 'temasek', 'tennis', 'teva', 'tf', 'tg', 'th', 'thd', 'theater',
- 'theatre', 'tiaa', 'tickets', 'tienda', 'tiffany', 'tips',
- 'tires', 'tirol', 'tj', 'tjmaxx', 'tjx', 'tk', 'tkmaxx', 'tl',
- 'tm', 'tmall', 'tn', 'to', 'today', 'tokyo', 'tools', 'top',
- 'toray', 'toshiba', 'total', 'tours', 'town', 'toyota', 'toys',
- 'tr', 'trade', 'trading', 'training', 'travel', 'travelchannel',
- 'travelers', 'travelersinsurance', 'trust', 'trv', 'tt', 'tube',
- 'tui', 'tunes', 'tushu', 'tv', 'tvs', 'tw', 'tz',
- # u
- 'ua', 'ubank', 'ubs', 'uconnect', 'ug', 'uk', 'unicom',
- 'university', 'uno', 'uol', 'ups', 'us', 'uy', 'uz',
- # v
- 'va', 'vacations', 'vana', 'vanguard', 'vc', 've', 'vegas',
- 'ventures', 'verisign', 'versicherung', 'vet', 'vg', 'vi',
- 'viajes', 'video', 'vig', 'viking', 'villas', 'vin', 'vip',
- 'virgin', 'visa', 'vision', 'vista', 'vistaprint', 'viva',
- 'vivo', 'vlaanderen', 'vn', 'vodka', 'volkswagen', 'volvo',
- 'vote', 'voting', 'voto', 'voyage', 'vu', 'vuelos',
- # w
- 'wales', 'walmart', 'walter', 'wang', 'wanggou', 'warman',
- 'watch', 'watches', 'weather', 'weatherchannel', 'webcam',
- 'weber', 'website', 'wed', 'wedding', 'weibo', 'weir', 'wf',
- 'whoswho', 'wien', 'wiki', 'williamhill', 'win', 'windows',
- 'wine', 'winners', 'wme', 'wolterskluwer', 'woodside', 'work',
- 'works', 'world', 'wow', 'ws', 'wtc', 'wtf',
- # x
- 'xbox', 'xerox', 'xfinity', 'xihuan', 'xin', 'xn--11b4c3d',
- 'xn--1ck2e1b', 'xn--1qqw23a', 'xn--2scrj9c', 'xn--30rr7y',
- 'xn--3bst00m', 'xn--3ds443g', 'xn--3e0b707e', 'xn--3hcrj9c',
- 'xn--3oq18vl8pn36a', 'xn--3pxu8k', 'xn--42c2d9a', 'xn--45br5cyl',
- 'xn--45brj9c', 'xn--45q11c', 'xn--4gbrim', 'xn--54b7fta0cc',
- 'xn--55qw42g', 'xn--55qx5d', 'xn--5su34j936bgsg', 'xn--5tzm5g',
- 'xn--6frz82g', 'xn--6qq986b3xl', 'xn--80adxhks', 'xn--80ao21a',
- 'xn--80aqecdr1a', 'xn--80asehdb', 'xn--80aswg', 'xn--8y0a063a',
- 'xn--90a3ac', 'xn--90ae', 'xn--90ais', 'xn--9dbq2a',
- 'xn--9et52u', 'xn--9krt00a', 'xn--b4w605ferd',
- 'xn--bck1b9a5dre4c', 'xn--c1avg', 'xn--c2br7g', 'xn--cck2b3b',
- 'xn--cg4bki', 'xn--clchc0ea0b2g2a9gcd', 'xn--czr694b',
- 'xn--czrs0t', 'xn--czru2d', 'xn--d1acj3b', 'xn--d1alf',
- 'xn--e1a4c', 'xn--eckvdtc9d', 'xn--efvy88h', 'xn--estv75g',
- 'xn--fct429k', 'xn--fhbei', 'xn--fiq228c5hs', 'xn--fiq64b',
- 'xn--fiqs8s', 'xn--fiqz9s', 'xn--fjq720a', 'xn--flw351e',
- 'xn--fpcrj9c3d', 'xn--fzc2c9e2c', 'xn--fzys8d69uvgm',
- 'xn--g2xx48c', 'xn--gckr3f0f', 'xn--gecrj9c', 'xn--gk3at1e',
- 'xn--h2breg3eve', 'xn--h2brj9c', 'xn--h2brj9c8c', 'xn--hxt814e',
- 'xn--i1b6b1a6a2e', 'xn--imr513n', 'xn--io0a7i', 'xn--j1aef',
- 'xn--j1amh', 'xn--j6w193g', 'xn--jlq61u9w7b', 'xn--jvr189m',
- 'xn--kcrx77d1x4a', 'xn--kprw13d', 'xn--kpry57d', 'xn--kpu716f',
- 'xn--kput3i', 'xn--l1acc', 'xn--lgbbat1ad8j', 'xn--mgb9awbf',
- 'xn--mgba3a3ejt', 'xn--mgba3a4f16a', 'xn--mgba7c0bbn0a',
- 'xn--mgbaakc7dvf', 'xn--mgbaam7a8h', 'xn--mgbab2bd',
- 'xn--mgbai9azgqp6j', 'xn--mgbayh7gpa', 'xn--mgbb9fbpob',
- 'xn--mgbbh1a', 'xn--mgbbh1a71e', 'xn--mgbc0a9azcg',
- 'xn--mgbca7dzdo', 'xn--mgberp4a5d4ar', 'xn--mgbgu82a',
- 'xn--mgbi4ecexp', 'xn--mgbpl2fh', 'xn--mgbt3dhd', 'xn--mgbtx2b',
- 'xn--mgbx4cd0ab', 'xn--mix891f', 'xn--mk1bu44c', 'xn--mxtq1m',
- 'xn--ngbc5azd', 'xn--ngbe9e0a', 'xn--ngbrx', 'xn--node',
- 'xn--nqv7f', 'xn--nqv7fs00ema', 'xn--nyqy26a', 'xn--o3cw4h',
- 'xn--ogbpf8fl', 'xn--p1acf', 'xn--p1ai', 'xn--pbt977c',
- 'xn--pgbs0dh', 'xn--pssy2u', 'xn--q9jyb4c', 'xn--qcka1pmc',
- 'xn--qxam', 'xn--rhqv96g', 'xn--rovu88b', 'xn--rvc1e0am3e',
- 'xn--s9brj9c', 'xn--ses554g', 'xn--t60b56a', 'xn--tckwe',
- 'xn--tiq49xqyj', 'xn--unup4y', 'xn--vermgensberater-ctb',
- 'xn--vermgensberatung-pwb', 'xn--vhquv', 'xn--vuq861b',
- 'xn--w4r85el8fhu5dnra', 'xn--w4rs40l', 'xn--wgbh1c',
- 'xn--wgbl6a', 'xn--xhq521b', 'xn--xkc2al3hye2a',
- 'xn--xkc2dl3a5ee0h', 'xn--y9a3aq', 'xn--yfro4i67o',
- 'xn--ygbi2ammx', 'xn--zfr164b', 'xperia', 'xxx', 'xyz',
- # y
- 'yachts', 'yahoo', 'yamaxun', 'yandex', 'ye', 'yodobashi',
- 'yoga', 'yokohama', 'you', 'youtube', 'yt', 'yun',
- # z
- 'za', 'zappos', 'zara', 'zero', 'zip', 'zippo', 'zm', 'zone',
- 'zuerich', 'zw'
-]
-
-
-class IS_HTTP_URL(Validator):
- """
- Rejects a URL string if any of the following is true:
- * The string is empty or None
- * The string uses characters that are not allowed in a URL
- * The string breaks any of the HTTP syntactic rules
- * The URL scheme specified (if one is specified) is not 'http' or 'https'
- * The top-level domain (if a host name is specified) does not exist
-
- Based on RFC 2616: http://www.faqs.org/rfcs/rfc2616.html
-
- This function only checks the URL's syntax. It does not check that the URL
- points to a real document, for example, or that it otherwise makes sense
- semantically. This function does automatically prepend 'http://' in front
- of a URL in the case of an abbreviated URL (e.g. 'google.ca').
-
- The list of allowed schemes is customizable with the allowed_schemes
- parameter. If you exclude None from the list, then abbreviated URLs
- (lacking a scheme such as 'http') will be rejected.
-
- The default prepended scheme is customizable with the prepend_scheme
- parameter. If you set prepend_scheme to None then prepending will be
- disabled. URLs that require prepending to parse will still be accepted,
- but the return value will not be modified.
-
- @author: Jonathan Benn
-
- >>> IS_HTTP_URL()('http://1.2.3.4')
- ('http://1.2.3.4', None)
- >>> IS_HTTP_URL()('http://abc.com')
- ('http://abc.com', None)
- >>> IS_HTTP_URL()('https://abc.com')
- ('https://abc.com', None)
- >>> IS_HTTP_URL()('httpx://abc.com')
- ('httpx://abc.com', 'enter a valid URL')
- >>> IS_HTTP_URL()('http://abc.com:80')
- ('http://abc.com:80', None)
- >>> IS_HTTP_URL()('http://user@abc.com')
- ('http://user@abc.com', None)
- >>> IS_HTTP_URL()('http://user@1.2.3.4')
- ('http://user@1.2.3.4', None)
-
- Args:
- error_message: a string, the error message to give the end user
- if the URL does not validate
- allowed_schemes: a list containing strings or None. Each element
- is a scheme the inputed URL is allowed to use
- prepend_scheme: a string, this scheme is prepended if it's
- necessary to make the URL valid
- """
-
- GENERIC_VALID_IP = re.compile(
- r"([\w.!~*'|;:&=+$,-]+@)?\d+\.\d+\.\d+\.\d+(:\d*)*$")
- GENERIC_VALID_DOMAIN = re.compile(r"([\w.!~*'|;:&=+$,-]+@)?(([A-Za-z0-9]+[A-Za-z0-9\-]*[A-Za-z0-9]+\.)*([A-Za-z0-9]+\.)*)*([A-Za-z]+[A-Za-z0-9\-]*[A-Za-z0-9]+)\.?(:\d*)*$")
-
- def __init__(self,
- error_message='Enter a valid URL',
- allowed_schemes=None,
- prepend_scheme='http',
- allowed_tlds=None):
-
- self.error_message = error_message
- if allowed_schemes is None:
- self.allowed_schemes = http_schemes
- else:
- self.allowed_schemes = allowed_schemes
- if allowed_tlds is None:
- self.allowed_tlds = official_top_level_domains
- else:
- self.allowed_tlds = allowed_tlds
- self.prepend_scheme = prepend_scheme
-
- for i in self.allowed_schemes:
- if i not in http_schemes:
- raise SyntaxError("allowed_scheme value '%s' is not in %s" %
- (i, http_schemes))
-
- if self.prepend_scheme not in self.allowed_schemes:
- raise SyntaxError("prepend_scheme='%s' is not in allowed_schemes=%s" %
- (self.prepend_scheme, self.allowed_schemes))
-
- def validate(self, value):
- """
- Args:
- value: a string, the URL to validate
-
- Returns:
- a tuple, where tuple[0] is the inputed value
- (possible prepended with prepend_scheme), and tuple[1] is either
- None (success!) or the string error_message
- """
- try:
- # if the URL passes generic validation
- x = IS_GENERIC_URL(error_message=self.error_message,
- allowed_schemes=self.allowed_schemes,
- prepend_scheme=self.prepend_scheme)
- if x(value)[1] is None:
- components = urlparse.urlparse(value)
- authority = components.netloc
- # if there is an authority component
- if authority:
- # if authority is a valid IP address
- if self.GENERIC_VALID_IP.match(authority):
- # Then this HTTP URL is valid
- return value
- else:
- # else if authority is a valid domain name
- domainMatch = self.GENERIC_VALID_DOMAIN.match(
- authority)
- if domainMatch:
- # if the top-level domain really exists
- if domainMatch.group(5).lower() in self.allowed_tlds:
- # Then this HTTP URL is valid
- return value
- else:
- # else this is a relative/abbreviated URL, which will parse
- # into the URL's path component
- path = components.path
- # relative case: if this is a valid path (if it starts with
- # a slash)
- if not path.startswith('/'):
- # abbreviated case: if we haven't already, prepend a
- # scheme and see if it fixes the problem
- if '://' not in value and None in self.allowed_schemes:
- schemeToUse = self.prepend_scheme or 'http'
- new_value = self.validate(schemeToUse + '://' + value)
- return new_value if self.prepend_scheme else value
- return value
- except:
- pass
- raise ValidationError(self.translator(self.error_message))
-
-
-class IS_URL(Validator):
- """
- Rejects a URL string if any of the following is true:
-
- * The string is empty or None
- * The string uses characters that are not allowed in a URL
- * The string breaks any of the HTTP syntactic rules
- * The URL scheme specified (if one is specified) is not 'http' or 'https'
- * The top-level domain (if a host name is specified) does not exist
-
- (These rules are based on RFC 2616: http://www.faqs.org/rfcs/rfc2616.html)
-
- This function only checks the URL's syntax. It does not check that the URL
- points to a real document, for example, or that it otherwise makes sense
- semantically. This function does automatically prepend 'http://' in front
- of a URL in the case of an abbreviated URL (e.g. 'google.ca').
-
- If the parameter mode='generic' is used, then this function's behavior
- changes. It then rejects a URL string if any of the following is true:
-
- * The string is empty or None
- * The string uses characters that are not allowed in a URL
- * The URL scheme specified (if one is specified) is not valid
-
- (These rules are based on RFC 2396: http://www.faqs.org/rfcs/rfc2396.html)
-
- The list of allowed schemes is customizable with the allowed_schemes
- parameter. If you exclude None from the list, then abbreviated URLs
- (lacking a scheme such as 'http') will be rejected.
-
- The default prepended scheme is customizable with the prepend_scheme
- parameter. If you set prepend_scheme to None then prepending will be
- disabled. URLs that require prepending to parse will still be accepted,
- but the return value will not be modified.
-
- IS_URL is compatible with the Internationalized Domain Name (IDN) standard
- specified in RFC 3490 (http://tools.ietf.org/html/rfc3490). As a result,
- URLs can be regular strings or unicode strings.
- If the URL's domain component (e.g. google.ca) contains non-US-ASCII
- letters, then the domain will be converted into Punycode (defined in
- RFC 3492, http://tools.ietf.org/html/rfc3492). IS_URL goes a bit beyond
- the standards, and allows non-US-ASCII characters to be present in the path
- and query components of the URL as well. These non-US-ASCII characters will
- be escaped using the standard '%20' type syntax. e.g. the unicode
- character with hex code 0x4e86 will become '%4e%86'
-
- Args:
- error_message: a string, the error message to give the end user
- if the URL does not validate
- allowed_schemes: a list containing strings or None. Each element
- is a scheme the inputed URL is allowed to use
- prepend_scheme: a string, this scheme is prepended if it's
- necessary to make the URL valid
-
- Code Examples::
-
- INPUT(_type='text', _name='name', requires=IS_URL())
- >>> IS_URL()('abc.com')
- ('http://abc.com', None)
-
- INPUT(_type='text', _name='name', requires=IS_URL(mode='generic'))
- >>> IS_URL(mode='generic')('abc.com')
- ('abc.com', None)
-
- INPUT(_type='text', _name='name',
- requires=IS_URL(allowed_schemes=['https'], prepend_scheme='https'))
- >>> IS_URL(allowed_schemes=['https'], prepend_scheme='https')('https://abc.com')
- ('https://abc.com', None)
-
- INPUT(_type='text', _name='name',
- requires=IS_URL(prepend_scheme='https'))
- >>> IS_URL(prepend_scheme='https')('abc.com')
- ('https://abc.com', None)
-
- INPUT(_type='text', _name='name',
- requires=IS_URL(mode='generic', allowed_schemes=['ftps', 'https'],
- prepend_scheme='https'))
- >>> IS_URL(mode='generic', allowed_schemes=['ftps', 'https'], prepend_scheme='https')('https://abc.com')
- ('https://abc.com', None)
- >>> IS_URL(mode='generic', allowed_schemes=['ftps', 'https', None], prepend_scheme='https')('abc.com')
- ('abc.com', None)
-
- @author: Jonathan Benn
- """
-
- def __init__(self,
- error_message='Enter a valid URL',
- mode='http',
- allowed_schemes=None,
- prepend_scheme='http',
- allowed_tlds=None):
-
- self.error_message = error_message
- self.mode = mode.lower()
- if self.mode not in ['generic', 'http']:
- raise SyntaxError("invalid mode '%s' in IS_URL" % self.mode)
- self.allowed_schemes = allowed_schemes
- if allowed_tlds is None:
- self.allowed_tlds = official_top_level_domains
- else:
- self.allowed_tlds = allowed_tlds
-
- if self.allowed_schemes:
- if prepend_scheme not in self.allowed_schemes:
- raise SyntaxError("prepend_scheme='%s' is not in allowed_schemes=%s"
- % (prepend_scheme, self.allowed_schemes))
-
- # if allowed_schemes is None, then we will defer testing
- # prepend_scheme's validity to a sub-method
-
- self.prepend_scheme = prepend_scheme
-
- def validate(self, value):
- """
- Args:
- value: a unicode or regular string, the URL to validate
-
- Returns:
- a (string, string) tuple, where tuple[0] is the modified
- input value and tuple[1] is either None (success!) or the
- string error_message. The input value will never be modified in the
- case of an error. However, if there is success then the input URL
- may be modified to (1) prepend a scheme, and/or (2) convert a
- non-compliant unicode URL into a compliant US-ASCII version.
- """
- if self.mode == 'generic':
- subMethod = IS_GENERIC_URL(error_message=self.error_message,
- allowed_schemes=self.allowed_schemes,
- prepend_scheme=self.prepend_scheme)
- elif self.mode == 'http':
- subMethod = IS_HTTP_URL(error_message=self.error_message,
- allowed_schemes=self.allowed_schemes,
- prepend_scheme=self.prepend_scheme,
- allowed_tlds=self.allowed_tlds)
- else:
- raise SyntaxError("invalid mode '%s' in IS_URL" % self.mode)
-
- if isinstance(value, unicodeT):
- try:
- value = unicode_to_ascii_url(value, self.prepend_scheme)
- except Exception as e:
- # If we are not able to convert the unicode url into a
- # US-ASCII URL, then the URL is not valid
- raise ValidationError(self.translator(self.error_message))
- return subMethod.validate(value)
-
-
-regex_time = re.compile(
- '((?P[0-9]+))([^0-9 ]+(?P[0-9 ]+))?([^0-9ap ]+(?P[0-9]*))?((?P[ap]m))?')
-
-
-class IS_TIME(Validator):
- """
- Example:
- Use as::
-
- INPUT(_type='text', _name='name', requires=IS_TIME())
-
- understands the following formats
- hh:mm:ss [am/pm]
- hh:mm [am/pm]
- hh [am/pm]
-
- [am/pm] is optional, ':' can be replaced by any other non-space non-digit::
-
- >>> IS_TIME()('21:30')
- (datetime.time(21, 30), None)
- >>> IS_TIME()('21-30')
- (datetime.time(21, 30), None)
- >>> IS_TIME()('21.30')
- (datetime.time(21, 30), None)
- >>> IS_TIME()('21:30:59')
- (datetime.time(21, 30, 59), None)
- >>> IS_TIME()('5:30')
- (datetime.time(5, 30), None)
- >>> IS_TIME()('5:30 am')
- (datetime.time(5, 30), None)
- >>> IS_TIME()('5:30 pm')
- (datetime.time(17, 30), None)
- >>> IS_TIME()('5:30 whatever')
- ('5:30 whatever', 'enter time as hh:mm:ss (seconds, am, pm optional)')
- >>> IS_TIME()('5:30 20')
- ('5:30 20', 'enter time as hh:mm:ss (seconds, am, pm optional)')
- >>> IS_TIME()('24:30')
- ('24:30', 'enter time as hh:mm:ss (seconds, am, pm optional)')
- >>> IS_TIME()('21:60')
- ('21:60', 'enter time as hh:mm:ss (seconds, am, pm optional)')
- >>> IS_TIME()('21:30::')
- ('21:30::', 'enter time as hh:mm:ss (seconds, am, pm optional)')
- >>> IS_TIME()('')
- ('', 'enter time as hh:mm:ss (seconds, am, pm optional)')ù
-
- """
-
- def __init__(self, error_message='Enter time as hh:mm:ss (seconds, am, pm optional)'):
- self.error_message = error_message
-
- def validate(self, value):
- try:
- ivalue = value
- value = regex_time.match(value.lower())
- (h, m, s) = (int(value.group('h')), 0, 0)
- if not value.group('m') is None:
- m = int(value.group('m'))
- if not value.group('s') is None:
- s = int(value.group('s'))
- if value.group('d') == 'pm' and 0 < h < 12:
- h += 12
- if value.group('d') == 'am' and h == 12:
- h = 0
- if not (h in range(24) and m in range(60) and s
- in range(60)):
- raise ValueError('Hours or minutes or seconds are outside of allowed range')
- value = datetime.time(h, m, s)
- return value
- except Exception:
- raise ValidationError(self.translator(self.error_message))
-
-
-# A UTC class.
-class UTC(datetime.tzinfo):
- """UTC"""
- ZERO = datetime.timedelta(0)
-
- def utcoffset(self, dt):
- return UTC.ZERO
-
- def tzname(self, dt):
- return "UTC"
-
- def dst(self, dt):
- return UTC.ZERO
-utc = UTC()
-
-
-class IS_DATE(Validator):
- """
- Examples:
- Use as::
-
- INPUT(_type='text', _name='name', requires=IS_DATE())
-
- date has to be in the ISO8960 format YYYY-MM-DD
- """
-
- def __init__(self, format='%Y-%m-%d',
- error_message='Enter date as %(format)s'):
- self.format = self.translator(format)
- self.error_message = str(error_message)
- self.extremes = {}
-
- def validate(self, value):
- if isinstance(value, datetime.date):
- return value
- try:
- (y, m, d, hh, mm, ss, t0, t1, t2) = \
- time.strptime(value, str(self.format))
- value = datetime.date(y, m, d)
- return value
- except:
- self.extremes.update(IS_DATETIME.nice(self.format))
- raise ValidationError(self.translator(self.error_message) % self.extremes)
-
- def formatter(self, value):
- if value is None:
- return None
- format = self.format
- year = value.year
- y = '%.4i' % year
- format = format.replace('%y', y[-2:])
- format = format.replace('%Y', y)
- if year < 1900:
- year = 2000
- d = datetime.date(year, value.month, value.day)
- return d.strftime(format)
-
-
-class IS_DATETIME(Validator):
- """
- Examples:
- Use as::
-
- INPUT(_type='text', _name='name', requires=IS_DATETIME())
-
- datetime has to be in the ISO8960 format YYYY-MM-DD hh:mm:ss
- timezome must be None or a pytz.timezone("America/Chicago") object
- """
-
- isodatetime = '%Y-%m-%d %H:%M:%S'
-
- @staticmethod
- def nice(format):
- code = (('%Y', '1963'),
- ('%y', '63'),
- ('%d', '28'),
- ('%m', '08'),
- ('%b', 'Aug'),
- ('%B', 'August'),
- ('%H', '14'),
- ('%I', '02'),
- ('%p', 'PM'),
- ('%M', '30'),
- ('%S', '59'))
- for (a, b) in code:
- format = format.replace(a, b)
- return dict(format=format)
-
- def __init__(self, format='%Y-%m-%d %H:%M:%S',
- error_message='Enter date and time as %(format)s',
- timezone=None):
- self.format = self.translator(format)
- self.error_message = str(error_message)
- self.extremes = {}
- self.timezone = timezone
-
- def validate(self, value):
- if isinstance(value, datetime.datetime):
- return value
- try:
- (y, m, d, hh, mm, ss, t0, t1, t2) = \
- time.strptime(value, str(self.format))
- value = datetime.datetime(y, m, d, hh, mm, ss)
- if self.timezone is not None:
- # TODO: https://github.com/web2py/web2py/issues/1094 (temporary solution)
- value = self.timezone.localize(value).astimezone(utc).replace(tzinfo=None)
- return value
- except:
- self.extremes.update(IS_DATETIME.nice(self.format))
- raise ValidationError(self.translator(self.error_message) % self.extremes)
-
- def formatter(self, value):
- if value is None:
- return None
- format = self.format
- year = value.year
- y = '%.4i' % year
- format = format.replace('%y', y[-2:])
- format = format.replace('%Y', y)
- if year < 1900:
- year = 2000
- d = datetime.datetime(year, value.month, value.day,
- value.hour, value.minute, value.second)
- if self.timezone is not None:
- d = d.replace(tzinfo=utc).astimezone(self.timezone)
- return d.strftime(format)
-
-
-class IS_DATE_IN_RANGE(IS_DATE):
- """
- Examples:
- Use as::
-
- >>> v = IS_DATE_IN_RANGE(minimum=datetime.date(2008,1,1), \
- maximum=datetime.date(2009,12,31), \
- format="%m/%d/%Y",error_message="Oops")
-
- >>> v('03/03/2008')
- (datetime.date(2008, 3, 3), None)
-
- >>> v('03/03/2010')
- ('03/03/2010', 'oops')
-
- >>> v(datetime.date(2008,3,3))
- (datetime.date(2008, 3, 3), None)
-
- >>> v(datetime.date(2010,3,3))
- (datetime.date(2010, 3, 3), 'oops')
-
- """
-
- def __init__(self,
- minimum=None,
- maximum=None,
- format='%Y-%m-%d',
- error_message=None):
- self.minimum = minimum
- self.maximum = maximum
- if error_message is None:
- if minimum is None:
- error_message = "Enter date on or before %(max)s"
- elif maximum is None:
- error_message = "Enter date on or after %(min)s"
- else:
- error_message = "Enter date in range %(min)s %(max)s"
- IS_DATE.__init__(self,
- format=format,
- error_message=error_message)
- self.extremes = dict(min=self.formatter(minimum),
- max=self.formatter(maximum))
-
- def validate(self, value):
- value = IS_DATE.validate(self, value)
- if self.minimum and self.minimum > value:
- raise ValidationError(self.translator(self.error_message) % self.extremes)
- if self.maximum and value > self.maximum:
- raise ValidationError(self.translator(self.error_message) % self.extremes)
- return value
-
-
-class IS_DATETIME_IN_RANGE(IS_DATETIME):
- """
- Examples:
- Use as::
- >>> v = IS_DATETIME_IN_RANGE(\
- minimum=datetime.datetime(2008,1,1,12,20), \
- maximum=datetime.datetime(2009,12,31,12,20), \
- format="%m/%d/%Y %H:%M",error_message="Oops")
- >>> v('03/03/2008 12:40')
- (datetime.datetime(2008, 3, 3, 12, 40), None)
-
- >>> v('03/03/2010 10:34')
- ('03/03/2010 10:34', 'oops')
-
- >>> v(datetime.datetime(2008,3,3,0,0))
- (datetime.datetime(2008, 3, 3, 0, 0), None)
-
- >>> v(datetime.datetime(2010,3,3,0,0))
- (datetime.datetime(2010, 3, 3, 0, 0), 'oops')
-
- """
-
- def __init__(self,
- minimum=None,
- maximum=None,
- format='%Y-%m-%d %H:%M:%S',
- error_message=None,
- timezone=None):
- self.minimum = minimum
- self.maximum = maximum
- if error_message is None:
- if minimum is None:
- error_message = "Enter date and time on or before %(max)s"
- elif maximum is None:
- error_message = "Enter date and time on or after %(min)s"
- else:
- error_message = "Enter date and time in range %(min)s %(max)s"
- IS_DATETIME.__init__(self,
- format=format,
- error_message=error_message,
- timezone=timezone)
- self.extremes = dict(min=self.formatter(minimum),
- max=self.formatter(maximum))
-
- def validate(self, value):
- value = IS_DATETIME.validate(self, value)
- if self.minimum and self.minimum > value:
- raise ValidationError(self.translator(self.error_message) % self.extremes)
- if self.maximum and value > self.maximum:
- raise ValidationError(self.translator(self.error_message) % self.extremes)
- return value
-
-
-class IS_LIST_OF(Validator):
-
- def __init__(self, other=None, minimum=None, maximum=None, error_message=None):
- self.other = other
- self.minimum = minimum
- self.maximum = maximum
- self.error_message = error_message
-
- def validate(self, value):
- ivalue = value
- if not isinstance(value, list):
- ivalue = [ivalue]
- ivalue = [i for i in ivalue if str(i).strip()]
- if self.minimum is not None and len(ivalue) < self.minimum:
- raise ValidationError(self.translator(self.error_message or 'Minimum length is %(min)s') %
- dict(min=self.minimum, max=self.maximum))
- if self.maximum is not None and len(ivalue) > self.maximum:
- raise ValidationError(self.translator(self.error_message or 'Maximum length is %(max)s') %
- dict(min=self.minimum, max=self.maximum))
- new_value = []
- other = self.other
- if self.other:
- if not isinstance(other, (list, tuple)):
- other = [other]
- for item in ivalue:
- v = item
- for validator in other:
- v = validator.validate(v)
- new_value.append(v)
- ivalue = new_value
- return ivalue
-
-
-class IS_LOWER(Validator):
- """
- Converts to lowercase::
-
- >>> IS_LOWER()('ABC')
- ('abc', None)
- >>> IS_LOWER()('Ñ')
- ('\\xc3\\xb1', None)
-
- """
-
- def validate(self, value):
- cast_back = lambda x: x
- if isinstance(value, str):
- cast_back = to_native
- elif isinstance(value, bytes):
- cast_back = to_bytes
- value = to_unicode(value).lower()
- return cast_back(value)
-
-
-class IS_UPPER(Validator):
- """
- Converts to uppercase::
-
- >>> IS_UPPER()('abc')
- ('ABC', None)
- >>> IS_UPPER()('ñ')
- ('\\xc3\\x91', None)
-
- """
-
- def validate(self, value):
- cast_back = lambda x: x
- if isinstance(value, str):
- cast_back = to_native
- elif isinstance(value, bytes):
- cast_back = to_bytes
- value = to_unicode(value).upper()
- return cast_back(value)
-
-
-def urlify(s, maxlen=80, keep_underscores=False):
- """
- Converts incoming string to a simplified ASCII subset.
- if (keep_underscores): underscores are retained in the string
- else: underscores are translated to hyphens (default)
- """
- s = to_unicode(s) # to unicode
- s = s.lower() # to lowercase
- s = unicodedata.normalize('NFKD', s) # replace special characters
- s = to_native(s, charset='ascii', errors='ignore') # encode as ASCII
- s = re.sub(r'&\w+?;', '', s) # strip html entities
- if keep_underscores:
- s = re.sub(r'\s+', '-', s) # whitespace to hyphens
- s = re.sub(r'[^\w\-]', '', s)
- # strip all but alphanumeric/underscore/hyphen
- else:
- s = re.sub(r'[\s_]+', '-', s) # whitespace & underscores to hyphens
- s = re.sub(r'[^a-z0-9\-]', '', s) # strip all but alphanumeric/hyphen
- s = re.sub(r'[-_][-_]+', '-', s) # collapse strings of hyphens
- s = s.strip('-') # remove leading and trailing hyphens
- return s[:maxlen] # enforce maximum length
-
-
-class IS_SLUG(Validator):
- """
- converts arbitrary text string to a slug::
-
- >>> IS_SLUG()('abc123')
- ('abc123', None)
- >>> IS_SLUG()('ABC123')
- ('abc123', None)
- >>> IS_SLUG()('abc-123')
- ('abc-123', None)
- >>> IS_SLUG()('abc--123')
- ('abc-123', None)
- >>> IS_SLUG()('abc 123')
- ('abc-123', None)
- >>> IS_SLUG()('abc\t_123')
- ('abc-123', None)
- >>> IS_SLUG()('-abc-')
- ('abc', None)
- >>> IS_SLUG()('--a--b--_ -c--')
- ('a-b-c', None)
- >>> IS_SLUG()('abc&123')
- ('abc123', None)
- >>> IS_SLUG()('abc&123&def')
- ('abc123def', None)
- >>> IS_SLUG()('ñ')
- ('n', None)
- >>> IS_SLUG(maxlen=4)('abc123')
- ('abc1', None)
- >>> IS_SLUG()('abc_123')
- ('abc-123', None)
- >>> IS_SLUG(keep_underscores=False)('abc_123')
- ('abc-123', None)
- >>> IS_SLUG(keep_underscores=True)('abc_123')
- ('abc_123', None)
- >>> IS_SLUG(check=False)('abc')
- ('abc', None)
- >>> IS_SLUG(check=True)('abc')
- ('abc', None)
- >>> IS_SLUG(check=False)('a bc')
- ('a-bc', None)
- >>> IS_SLUG(check=True)('a bc')
- ('a bc', 'must be slug')
- """
-
- @staticmethod
- def urlify(value, maxlen=80, keep_underscores=False):
- return urlify(value, maxlen, keep_underscores)
-
- def __init__(self, maxlen=80, check=False, error_message='Must be slug', keep_underscores=False):
- self.maxlen = maxlen
- self.check = check
- self.error_message = error_message
- self.keep_underscores = keep_underscores
-
- def validate(self, value):
- if self.check and value != urlify(value, self.maxlen, self.keep_underscores):
- raise ValidationError(self.translator(self.error_message))
- return urlify(value, self.maxlen, self.keep_underscores)
-
-
-class ANY_OF(Validator):
- """
- Tests if any of the validators in a list returns successfully::
-
- >>> ANY_OF([IS_EMAIL(),IS_ALPHANUMERIC()])('a@b.co')
- ('a@b.co', None)
- >>> ANY_OF([IS_EMAIL(),IS_ALPHANUMERIC()])('abco')
- ('abco', None)
- >>> ANY_OF([IS_EMAIL(),IS_ALPHANUMERIC()])('@ab.co')
- ('@ab.co', 'enter only letters, numbers, and underscore')
- >>> ANY_OF([IS_ALPHANUMERIC(),IS_EMAIL()])('@ab.co')
- ('@ab.co', 'enter a valid email address')
-
- """
-
- def __init__(self, subs, error_message=None):
- self.subs = subs
- self.error_message = error_message
-
- def validate(self, value):
- for validator in self.subs:
- v, e = validator(value)
- if not e:
- return v
- raise ValidationError(e)
-
- def formatter(self, value):
- # Use the formatter of the first subvalidator
- # that validates the value and has a formatter
- for validator in self.subs:
- if hasattr(validator, 'formatter') and validator(value)[1] is None:
- return validator.formatter(value)
-
-
-class IS_EMPTY_OR(Validator):
- """
- Dummy class for testing IS_EMPTY_OR::
-
- >>> IS_EMPTY_OR(IS_EMAIL())('abc@def.com')
- ('abc@def.com', None)
- >>> IS_EMPTY_OR(IS_EMAIL())(' ')
- (None, None)
- >>> IS_EMPTY_OR(IS_EMAIL(), null='abc')(' ')
- ('abc', None)
- >>> IS_EMPTY_OR(IS_EMAIL(), null='abc', empty_regex='def')('def')
- ('abc', None)
- >>> IS_EMPTY_OR(IS_EMAIL())('abc')
- ('abc', 'enter a valid email address')
- >>> IS_EMPTY_OR(IS_EMAIL())(' abc ')
- ('abc', 'enter a valid email address')
- """
-
- def __init__(self, other, null=None, empty_regex=None):
- (self.other, self.null) = (other, null)
- if empty_regex is not None:
- self.empty_regex = re.compile(empty_regex)
- else:
- self.empty_regex = None
- if hasattr(other, 'multiple'):
- self.multiple = other.multiple
- if hasattr(other, 'options'):
- self.options = self._options
-
- def _options(self, *args, **kwargs):
- options = self.other.options(*args, **kwargs)
- if (not options or options[0][0] != '') and not self.multiple:
- options.insert(0, ('', ''))
- return options
-
- def set_self_id(self, id):
- if isinstance(self.other, (list, tuple)):
- for item in self.other:
- if hasattr(item, 'set_self_id'):
- item.set_self_id(id)
- else:
- if hasattr(self.other, 'set_self_id'):
- self.other.set_self_id(id)
-
- def validate(self, value):
- value, empty = is_empty(value, empty_regex=self.empty_regex)
- if empty:
- return self.null
- if isinstance(self.other, (list, tuple)):
- for item in self.other:
- value = item.validate(value)
- return value
- return self.other.validate(value)
-
- def formatter(self, value):
- if hasattr(self.other, 'formatter'):
- return self.other.formatter(value)
- return value
-
-IS_NULL_OR = IS_EMPTY_OR # for backward compatibility
-
-
-class CLEANUP(Validator):
- """
- Examples:
- Use as::
-
- INPUT(_type='text', _name='name', requires=CLEANUP())
-
- removes special characters on validation
- """
- REGEX_CLEANUP = re.compile('[^\x09\x0a\x0d\x20-\x7e]')
-
- def __init__(self, regex=None):
- self.regex = self.REGEX_CLEANUP if regex is None \
- else re.compile(regex)
-
- def validate(self, value):
- v = self.regex.sub('', str(value).strip())
- return v
-
-
-def pbkdf2_hex(data, salt, iterations=1000, keylen=24, hashfunc=None):
- hashfunc = hashfunc or sha1
- hmac = hashlib.pbkdf2_hmac(hashfunc().name, to_bytes(data),
- to_bytes(salt), iterations, keylen)
- return binascii.hexlify(hmac)
-
-
-def simple_hash(text, key='', salt='', digest_alg='md5'):
- """Generate hash with the given text using the specified digest algorithm."""
- text = to_bytes(text)
- key = to_bytes(key)
- salt = to_bytes(salt)
- if not digest_alg:
- raise RuntimeError("simple_hash with digest_alg=None")
- elif not isinstance(digest_alg, str): # manual approach
- h = digest_alg(text + key + salt)
- elif digest_alg.startswith('pbkdf2'): # latest and coolest!
- iterations, keylen, alg = digest_alg[7:-1].split(',')
- return to_native(pbkdf2_hex(text, salt, int(iterations),
- int(keylen), get_digest(alg)))
- elif key: # use hmac
- digest_alg = get_digest(digest_alg)
- h = hmac.new(key + salt, text, digest_alg)
- else: # compatible with third party systems
- h = get_digest(digest_alg)()
- h.update(text + salt)
- return h.hexdigest()
-
-
-def get_digest(value):
- """Return a hashlib digest algorithm from a string."""
- if isinstance(value, str):
- value = value.lower()
- if value not in ('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'):
- raise ValueError("Invalid digest algorithm: %s" % value)
- value = getattr(hashlib, value)
- return value
-
-DIGEST_ALG_BY_SIZE = {
- 128 // 4: 'md5',
- 160 // 4: 'sha1',
- 224 // 4: 'sha224',
- 256 // 4: 'sha256',
- 384 // 4: 'sha384',
- 512 // 4: 'sha512',
-}
-
-
-class LazyCrypt(object):
- """
- Stores a lazy password hash
- """
-
- def __init__(self, crypt, password):
- """
- crypt is an instance of the CRYPT validator,
- password is the password as inserted by the user
- """
- self.crypt = crypt
- self.password = password
- self.crypted = None
-
- def __str__(self):
- """
- Encrypted self.password and caches it in self.crypted.
- If self.crypt.salt the output is in the format $$
-
- Try get the digest_alg from the key (if it exists)
- else assume the default digest_alg. If not key at all, set key=''
-
- If a salt is specified use it, if salt is True, set salt to uuid
- (this should all be backward compatible)
-
- Options:
- key = 'uuid'
- key = 'md5:uuid'
- key = 'sha512:uuid'
- ...
- key = 'pbkdf2(1000,64,sha512):uuid' 1000 iterations and 64 chars length
- """
- if self.crypted:
- return self.crypted
- if self.crypt.key:
- if ':' in self.crypt.key:
- digest_alg, key = self.crypt.key.split(':', 1)
- else:
- digest_alg, key = self.crypt.digest_alg, self.crypt.key
- else:
- digest_alg, key = self.crypt.digest_alg, ''
- if self.crypt.salt:
- if self.crypt.salt is True:
- salt = str(uuid.uuid4()).replace('-', '')[-16:]
- else:
- salt = self.crypt.salt
- else:
- salt = ''
- hashed = simple_hash(self.password, key, salt, digest_alg)
- self.crypted = '%s$%s$%s' % (digest_alg, salt, hashed)
- return self.crypted
-
- def __eq__(self, stored_password):
- """
- compares the current lazy crypted password with a stored password
- """
-
- # LazyCrypt objects comparison
- if isinstance(stored_password, self.__class__):
- return ((self is stored_password) or
- ((self.crypt.key == stored_password.crypt.key) and
- (self.password == stored_password.password)))
-
- if self.crypt.key:
- if ':' in self.crypt.key:
- key = self.crypt.key.split(':')[1]
- else:
- key = self.crypt.key
- else:
- key = ''
- if stored_password is None:
- return False
- elif stored_password.count('$') == 2:
- (digest_alg, salt, hash) = stored_password.split('$')
- h = simple_hash(self.password, key, salt, digest_alg)
- temp_pass = '%s$%s$%s' % (digest_alg, salt, h)
- else: # no salting
- # guess digest_alg
- digest_alg = DIGEST_ALG_BY_SIZE.get(len(stored_password), None)
- if not digest_alg:
- return False
- else:
- temp_pass = simple_hash(self.password, key, '', digest_alg)
- return temp_pass == stored_password
-
- def __ne__(self, other):
- return not self.__eq__(other)
-
-
-class CRYPT(Validator):
- """
- Examples:
- Use as::
-
- INPUT(_type='text', _name='name', requires=CRYPT())
-
- encodes the value on validation with a digest.
-
- If no arguments are provided CRYPT uses the MD5 algorithm.
- If the key argument is provided the HMAC+MD5 algorithm is used.
- If the digest_alg is specified this is used to replace the
- MD5 with, for example, SHA512. The digest_alg can be
- the name of a hashlib algorithm as a string or the algorithm itself.
-
- min_length is the minimal password length (default 4) - IS_STRONG for serious security
- error_message is the message if password is too short
-
- Notice that an empty password is accepted but invalid. It will not allow login back.
- Stores junk as hashed password.
-
- Specify an algorithm or by default we will use sha512.
-
- Typical available algorithms:
- md5, sha1, sha224, sha256, sha384, sha512
-
- If salt, it hashes a password with a salt.
- If salt is True, this method will automatically generate one.
- Either case it returns an encrypted password string in the following format:
-
- $$
-
- Important: hashed password is returned as a LazyCrypt object and computed only if needed.
- The LasyCrypt object also knows how to compare itself with an existing salted password
-
- Supports standard algorithms
-
- >>> for alg in ('md5','sha1','sha256','sha384','sha512'):
- ... print(str(CRYPT(digest_alg=alg,salt=True)('test')[0]))
- md5$...$...
- sha1$...$...
- sha256$...$...
- sha384$...$...
- sha512$...$...
-
- The syntax is always alg$salt$hash
-
- Supports for pbkdf2
-
- >>> alg = 'pbkdf2(1000,20,sha512)'
- >>> print(str(CRYPT(digest_alg=alg,salt=True)('test')[0]))
- pbkdf2(1000,20,sha512)$...$...
-
- An optional hmac_key can be specified and it is used as salt prefix
-
- >>> a = str(CRYPT(digest_alg='md5',key='mykey',salt=True)('test')[0])
- >>> print(a)
- md5$...$...
-
- Even if the algorithm changes the hash can still be validated
-
- >>> CRYPT(digest_alg='sha1',key='mykey',salt=True)('test')[0] == a
- True
-
- If no salt is specified CRYPT can guess the algorithms from length:
-
- >>> a = str(CRYPT(digest_alg='sha1',salt=False)('test')[0])
- >>> a
- 'sha1$$a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'
- >>> CRYPT(digest_alg='sha1',salt=False)('test')[0] == a
- True
- >>> CRYPT(digest_alg='sha1',salt=False)('test')[0] == a[6:]
- True
- >>> CRYPT(digest_alg='md5',salt=False)('test')[0] == a
- True
- >>> CRYPT(digest_alg='md5',salt=False)('test')[0] == a[6:]
- True
- """
-
- def __init__(self,
- key=None,
- digest_alg='pbkdf2(1000,20,sha512)',
- min_length=0,
- error_message='Too short', salt=True,
- max_length=1024):
- """
- important, digest_alg='md5' is not the default hashing algorithm for
- web2py. This is only an example of usage of this function.
-
- The actual hash algorithm is determined from the key which is
- generated by web2py in tools.py. This defaults to hmac+sha512.
- """
- self.key = key
- self.digest_alg = digest_alg
- self.min_length = min_length
- self.max_length = max_length
- self.error_message = error_message
- self.salt = salt
-
- def validate(self, value):
- v = value and str(value)[:self.max_length]
- if not v or len(v) < self.min_length:
- raise ValidationError(self.translator(self.error_message))
- if isinstance(value, LazyCrypt):
- return value
- return LazyCrypt(self, value)
-
-# entropy calculator for IS_STRONG
-#
-lowerset = frozenset(u'abcdefghijklmnopqrstuvwxyz')
-upperset = frozenset(u'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
-numberset = frozenset(u'0123456789')
-sym1set = frozenset(u'!@#$%^&*()')
-sym2set = frozenset(u'~`-_=+[]{}\\|;:\'",.<>?/')
-otherset = frozenset(
- u'0123456789abcdefghijklmnopqrstuvwxyz') # anything else
-
-
-def calc_entropy(string):
- """ calculates a simple entropy for a given string """
- alphabet = 0 # alphabet size
- other = set()
- seen = set()
- lastset = None
- string = to_unicode(string)
- for c in string:
- # classify this character
- inset = otherset
- for cset in (lowerset, upperset, numberset, sym1set, sym2set):
- if c in cset:
- inset = cset
- break
- # calculate effect of character on alphabet size
- if inset not in seen:
- seen.add(inset)
- alphabet += len(inset) # credit for a new character set
- elif c not in other:
- alphabet += 1 # credit for unique characters
- other.add(c)
- if inset is not lastset:
- alphabet += 1 # credit for set transitions
- lastset = cset
- entropy = len(
- string) * math.log(alphabet) / 0.6931471805599453 # math.log(2)
- return round(entropy, 2)
-
-
-class IS_STRONG(Validator):
- """
- Examples:
- Use as::
-
- INPUT(_type='password', _name='passwd',
- requires=IS_STRONG(min=10, special=2, upper=2))
-
- enforces complexity requirements on a field
-
- >>> IS_STRONG(es=True)('Abcd1234')
- ('Abcd1234',
- 'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|')
- >>> IS_STRONG(es=True)('Abcd1234!')
- ('Abcd1234!', None)
- >>> IS_STRONG(es=True, entropy=1)('a')
- ('a', None)
- >>> IS_STRONG(es=True, entropy=1, min=2)('a')
- ('a', 'Minimum length is 2')
- >>> IS_STRONG(es=True, entropy=100)('abc123')
- ('abc123', 'Entropy (32.35) less than required (100)')
- >>> IS_STRONG(es=True, entropy=100)('and')
- ('and', 'Entropy (14.57) less than required (100)')
- >>> IS_STRONG(es=True, entropy=100)('aaa')
- ('aaa', 'Entropy (14.42) less than required (100)')
- >>> IS_STRONG(es=True, entropy=100)('a1d')
- ('a1d', 'Entropy (15.97) less than required (100)')
- >>> IS_STRONG(es=True, entropy=100)('añd')
- ('a\\xc3\\xb1d', 'Entropy (18.13) less than required (100)')
-
- """
-
- def __init__(self, min=None, max=None, upper=None, lower=None, number=None,
- entropy=None,
- special=None, specials=r'~!@#$%^&*()_+-=?<>,.:;{}[]|',
- invalid=' "', error_message=None, es=False):
- self.entropy = entropy
- if entropy is None:
- # enforce default requirements
- self.min = 8 if min is None else min
- self.max = max # was 20, but that doesn't make sense
- self.upper = 1 if upper is None else upper
- self.lower = 1 if lower is None else lower
- self.number = 1 if number is None else number
- self.special = 1 if special is None else special
- else:
- # by default, an entropy spec is exclusive
- self.min = min
- self.max = max
- self.upper = upper
- self.lower = lower
- self.number = number
- self.special = special
- self.specials = specials
- self.invalid = invalid
- self.error_message = error_message
- self.estring = es # return error message as string (for doctest)
-
- def validate(self, value):
- failures = []
- if value and len(value) == value.count('*') > 4:
- return value
- if self.entropy is not None:
- entropy = calc_entropy(value)
- if entropy < self.entropy:
- failures.append(self.translator("Entropy (%(have)s) less than required (%(need)s)")
- % dict(have=entropy, need=self.entropy))
- if isinstance(self.min, int) and self.min > 0:
- if not len(value) >= self.min:
- failures.append(self.translator("Minimum length is %s") % self.min)
- if isinstance(self.max, int) and self.max > 0:
- if not len(value) <= self.max:
- failures.append(self.translator("Maximum length is %s") % self.max)
- if isinstance(self.special, int):
- all_special = [ch in value for ch in self.specials]
- if self.special > 0:
- if not all_special.count(True) >= self.special:
- failures.append(self.translator("Must include at least %s of the following: %s")
- % (self.special, self.specials))
- elif self.special is 0:
- if len(all_special) > 0:
- failures.append(self.translator("May not contain any of the following: %s")
- % self.specials)
- if self.invalid:
- all_invalid = [ch in value for ch in self.invalid]
- if all_invalid.count(True) > 0:
- failures.append(self.translator("May not contain any of the following: %s")
- % self.invalid)
- if isinstance(self.upper, int):
- all_upper = re.findall("[A-Z]", value)
- if self.upper > 0:
- if not len(all_upper) >= self.upper:
- failures.append(self.translator("Must include at least %s uppercase")
- % str(self.upper))
- elif self.upper is 0:
- if len(all_upper) > 0:
- failures.append(
- self.translator("May not include any uppercase letters"))
- if isinstance(self.lower, int):
- all_lower = re.findall("[a-z]", value)
- if self.lower > 0:
- if not len(all_lower) >= self.lower:
- failures.append(self.translator("Must include at least %s lowercase")
- % str(self.lower))
- elif self.lower is 0:
- if len(all_lower) > 0:
- failures.append(
- self.translator("May not include any lowercase letters"))
- if isinstance(self.number, int):
- all_number = re.findall("[0-9]", value)
- if self.number > 0:
- numbers = "number"
- if self.number > 1:
- numbers = "numbers"
- if not len(all_number) >= self.number:
- failures.append(self.translator("Must include at least %s %s")
- % (str(self.number), numbers))
- elif self.number is 0:
- if len(all_number) > 0:
- failures.append(self.translator("May not include any numbers"))
- if len(failures) == 0:
- return value
- if not self.error_message:
- if self.estring:
- raise ValidationError('|'.join(map(str, failures)))
- raise ValidationError(', '.join(failures))
- else:
- raise ValidationError(self.translator(self.error_message))
-
-
-class IS_IMAGE(Validator):
- """
- Checks if file uploaded through file input was saved in one of selected
- image formats and has dimensions (width and height) within given boundaries.
-
- Does *not* check for maximum file size (use IS_LENGTH for that). Returns
- validation failure if no data was uploaded.
-
- Supported file formats: BMP, GIF, JPEG, PNG.
-
- Code parts taken from
- http://mail.python.org/pipermail/python-list/2007-June/617126.html
-
- Args:
- extensions: iterable containing allowed *lowercase* image file extensions
- ('jpg' extension of uploaded file counts as 'jpeg')
- maxsize: iterable containing maximum width and height of the image
- minsize: iterable containing minimum width and height of the image
- aspectratio: iterable containing target aspect ratio
-
- Use (-1, -1) as minsize to pass image size check.
- Use (-1, -1) as aspectratio to pass aspect ratio check.
-
- Examples:
- Check if uploaded file is in any of supported image formats:
-
- INPUT(_type='file', _name='name', requires=IS_IMAGE())
-
- Check if uploaded file is either JPEG or PNG:
-
- INPUT(_type='file', _name='name',
- requires=IS_IMAGE(extensions=('jpeg', 'png')))
-
- Check if uploaded file is PNG with maximum size of 200x200 pixels:
-
- INPUT(_type='file', _name='name',
- requires=IS_IMAGE(extensions=('png'), maxsize=(200, 200)))
-
- Check if uploaded file has a 16:9 aspect ratio:
-
- INPUT(_type='file', _name='name',
- requires=IS_IMAGE(aspectratio=(16, 9)))
- """
-
- def __init__(self,
- extensions=('bmp', 'gif', 'jpeg', 'png'),
- maxsize=(10000, 10000),
- minsize=(0, 0),
- aspectratio=(-1, -1),
- error_message='Invalid image'):
-
- self.extensions = extensions
- self.maxsize = maxsize
- self.minsize = minsize
- self.aspectratio = aspectratio
- self.error_message = error_message
-
- def validate(self, value):
- try:
- extension = value.filename.rfind('.')
- assert extension >= 0
- extension = value.filename[extension + 1:].lower()
- if extension == 'jpg':
- extension = 'jpeg'
- assert extension in self.extensions
- if extension == 'bmp':
- width, height = self.__bmp(value.file)
- elif extension == 'gif':
- width, height = self.__gif(value.file)
- elif extension == 'jpeg':
- width, height = self.__jpeg(value.file)
- elif extension == 'png':
- width, height = self.__png(value.file)
- else:
- width = -1
- height = -1
-
- assert self.minsize[0] <= width <= self.maxsize[0] \
- and self.minsize[1] <= height <= self.maxsize[1]
-
- if self.aspectratio > (-1, -1):
- target_ratio = (1.0 * self.aspectratio[1]) / self.aspectratio[0]
- actual_ratio = (1.0 * height) / width
-
- assert actual_ratio == target_ratio
-
- value.file.seek(0)
- return value
- except Exception as e:
- raise ValidationError(self.translator(self.error_message))
-
- def __bmp(self, stream):
- if stream.read(2) == b'BM':
- stream.read(16)
- return struct.unpack("= 0xC0 and code <= 0xC3:
- return tuple(reversed(
- struct.unpack("!xHH", stream.read(5))))
- else:
- stream.read(length - 2)
- return (-1, -1)
-
- def __png(self, stream):
- if stream.read(8) == b'\211PNG\r\n\032\n':
- stream.read(4)
- if stream.read(4) == b"IHDR":
- return struct.unpack("!LL", stream.read(8))
- return (-1, -1)
-
-
-class IS_FILE(Validator):
- """
- Checks if name and extension of file uploaded through file input matches
- given criteria.
-
- Does *not* ensure the file type in any way. Returns validation failure
- if no data was uploaded.
-
- Args:
- filename: string/compiled regex or a list of strings/regex of valid filenames
- extension: string/compiled regex or a list of strings/regex of valid extensions
- lastdot: which dot should be used as a filename / extension separator:
- True means last dot, eg. file.jpg.png -> file.jpg / png
- False means first dot, eg. file.tar.gz -> file / tar.gz
- case: 0 - keep the case, 1 - transform the string into lowercase (default),
- 2 - transform the string into uppercase
-
- If there is no dot present, extension checks will be done against empty
- string and filename checks against whole value.
-
- Examples:
- Check if file has a pdf extension (case insensitive):
-
- INPUT(_type='file', _name='name',
- requires=IS_FILE(extension='pdf'))
-
- Check if file is called 'thumbnail' and has a jpg or png extension
- (case insensitive):
-
- INPUT(_type='file', _name='name',
- requires=IS_FILE(filename='thumbnail',
- extension=['jpg', 'png']))
-
- Check if file has a tar.gz extension and name starting with backup:
-
- INPUT(_type='file', _name='name',
- requires=IS_FILE(filename=re.compile('backup.*'),
- extension='tar.gz', lastdot=False))
-
- Check if file has no extension and name matching README
- (case sensitive):
-
- INPUT(_type='file', _name='name',
- requires=IS_FILE(filename='README',
- extension='', case=0)
-
- """
-
- def __init__(self, filename=None, extension=None, lastdot=True, case=1,
- error_message='Enter valid filename'):
- self.filename = filename
- self.extension = extension
- self.lastdot = lastdot
- self.case = case
- self.error_message = error_message
-
- def match(self, value1, value2):
- if isinstance(value1, (list, tuple)):
- for v in value1:
- if self.match(v, value2):
- return True
- return False
- elif isinstance(value1, type(regex_isint)):
- return value1.match(value2)
- elif isinstance(value1, str):
- return value1 == value2
-
- def validate(self, value):
- try:
- string = value.filename
- except:
- raise ValidationError(self.translator(self.error_message))
- if self.case == 1:
- string = string.lower()
- elif self.case == 2:
- string = string.upper()
- if self.lastdot:
- dot = string.rfind('.')
- else:
- dot = string.find('.')
- if dot == -1:
- dot = len(string)
- if self.filename and not self.match(self.filename, string[:dot]):
- raise ValidationError(self.translator(self.error_message))
- elif self.extension and not self.match(self.extension, string[dot + 1:]):
- raise ValidationError(self.translator(self.error_message))
- else:
- return value
-
-
-class IS_UPLOAD_FILENAME(Validator):
- """
- For new applications, use IS_FILE().
-
- Checks if name and extension of file uploaded through file input matches
- given criteria.
-
- Does *not* ensure the file type in any way. Returns validation failure
- if no data was uploaded.
-
- Args:
- filename: filename (before dot) regex
- extension: extension (after dot) regex
- lastdot: which dot should be used as a filename / extension separator:
- True means last dot, eg. file.png -> file / png
- False means first dot, eg. file.tar.gz -> file / tar.gz
- case: 0 - keep the case, 1 - transform the string into lowercase (default),
- 2 - transform the string into uppercase
-
- If there is no dot present, extension checks will be done against empty
- string and filename checks against whole value.
-
- Examples:
- Check if file has a pdf extension (case insensitive):
-
- INPUT(_type='file', _name='name',
- requires=IS_UPLOAD_FILENAME(extension='pdf'))
-
- Check if file has a tar.gz extension and name starting with backup:
-
- INPUT(_type='file', _name='name',
- requires=IS_UPLOAD_FILENAME(filename='backup.*',
- extension='tar.gz', lastdot=False))
-
- Check if file has no extension and name matching README
- (case sensitive):
-
- INPUT(_type='file', _name='name',
- requires=IS_UPLOAD_FILENAME(filename='^README$',
- extension='^$', case=0)
-
- """
-
- def __init__(self, filename=None, extension=None, lastdot=True, case=1,
- error_message='Enter valid filename'):
- if isinstance(filename, str):
- filename = re.compile(filename)
- if isinstance(extension, str):
- extension = re.compile(extension)
- self.filename = filename
- self.extension = extension
- self.lastdot = lastdot
- self.case = case
- self.error_message = error_message
-
- def validate(self, value):
- try:
- string = value.filename
- except:
- raise ValidationError(self.translator(self.error_message))
- if self.case == 1:
- string = string.lower()
- elif self.case == 2:
- string = string.upper()
- if self.lastdot:
- dot = string.rfind('.')
- else:
- dot = string.find('.')
- if dot == -1:
- dot = len(string)
- if self.filename and not self.filename.match(string[:dot]):
- raise ValidationError(self.translator(self.error_message))
- elif self.extension and not self.extension.match(string[dot + 1:]):
- raise ValidationError(self.translator(self.error_message))
- else:
- return value
-
-
-class IS_IPV4(Validator):
- """
- Checks if field's value is an IP version 4 address in decimal form. Can
- be set to force addresses from certain range.
-
- IPv4 regex taken from: http://regexlib.com/REDetails.aspx?regexp_id=1411
-
- Args:
-
- minip: lowest allowed address; accepts:
-
- - str, eg. 192.168.0.1
- - list or tuple of octets, eg. [192, 168, 0, 1]
- maxip: highest allowed address; same as above
- invert: True to allow addresses only from outside of given range; note
- that range boundaries are not matched this way
- is_localhost: localhost address treatment:
-
- - None (default): indifferent
- - True (enforce): query address must match localhost address (127.0.0.1)
- - False (forbid): query address must not match localhost address
- is_private: same as above, except that query address is checked against
- two address ranges: 172.16.0.0 - 172.31.255.255 and
- 192.168.0.0 - 192.168.255.255
- is_automatic: same as above, except that query address is checked against
- one address range: 169.254.0.0 - 169.254.255.255
-
- Minip and maxip may also be lists or tuples of addresses in all above
- forms (str, int, list / tuple), allowing setup of multiple address ranges::
-
- minip = (minip1, minip2, ... minipN)
- | | |
- | | |
- maxip = (maxip1, maxip2, ... maxipN)
-
- Longer iterable will be truncated to match length of shorter one.
-
- Examples:
- Check for valid IPv4 address:
-
- INPUT(_type='text', _name='name', requires=IS_IPV4())
-
- Check for valid IPv4 address belonging to specific range:
-
- INPUT(_type='text', _name='name',
- requires=IS_IPV4(minip='100.200.0.0', maxip='100.200.255.255'))
-
- Check for valid IPv4 address belonging to either 100.110.0.0 -
- 100.110.255.255 or 200.50.0.0 - 200.50.0.255 address range:
-
- INPUT(_type='text', _name='name',
- requires=IS_IPV4(minip=('100.110.0.0', '200.50.0.0'),
- maxip=('100.110.255.255', '200.50.0.255')))
-
- Check for valid IPv4 address belonging to private address space:
-
- INPUT(_type='text', _name='name', requires=IS_IPV4(is_private=True))
-
- Check for valid IPv4 address that is not a localhost address:
-
- INPUT(_type='text', _name='name', requires=IS_IPV4(is_localhost=False))
-
- >>> IS_IPV4()('1.2.3.4')
- ('1.2.3.4', None)
- >>> IS_IPV4()('255.255.255.255')
- ('255.255.255.255', None)
- >>> IS_IPV4()('1.2.3.4 ')
- ('1.2.3.4 ', 'enter valid IPv4 address')
- >>> IS_IPV4()('1.2.3.4.5')
- ('1.2.3.4.5', 'enter valid IPv4 address')
- >>> IS_IPV4()('123.123')
- ('123.123', 'enter valid IPv4 address')
- >>> IS_IPV4()('1111.2.3.4')
- ('1111.2.3.4', 'enter valid IPv4 address')
- >>> IS_IPV4()('0111.2.3.4')
- ('0111.2.3.4', 'enter valid IPv4 address')
- >>> IS_IPV4()('256.2.3.4')
- ('256.2.3.4', 'enter valid IPv4 address')
- >>> IS_IPV4()('300.2.3.4')
- ('300.2.3.4', 'enter valid IPv4 address')
- >>> IS_IPV4(minip='1.2.3.4', maxip='1.2.3.4')('1.2.3.4')
- ('1.2.3.4', None)
- >>> IS_IPV4(minip='1.2.3.5', maxip='1.2.3.9', error_message='Bad ip')('1.2.3.4')
- ('1.2.3.4', 'bad ip')
- >>> IS_IPV4(maxip='1.2.3.4', invert=True)('127.0.0.1')
- ('127.0.0.1', None)
- >>> IS_IPV4(maxip='1.2.3.4', invert=True)('1.2.3.4')
- ('1.2.3.4', 'enter valid IPv4 address')
- >>> IS_IPV4(is_localhost=True)('127.0.0.1')
- ('127.0.0.1', None)
- >>> IS_IPV4(is_localhost=True)('1.2.3.4')
- ('1.2.3.4', 'enter valid IPv4 address')
- >>> IS_IPV4(is_localhost=False)('127.0.0.1')
- ('127.0.0.1', 'enter valid IPv4 address')
- >>> IS_IPV4(maxip='100.0.0.0', is_localhost=True)('127.0.0.1')
- ('127.0.0.1', 'enter valid IPv4 address')
-
- """
-
- regex = re.compile(
- r'^(([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.){3}([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])$')
- numbers = (16777216, 65536, 256, 1)
- localhost = 2130706433
- private = ((2886729728, 2886795263), (3232235520, 3232301055))
- automatic = (2851995648, 2852061183)
-
- def __init__(self,
- minip='0.0.0.0',
- maxip='255.255.255.255',
- invert=False,
- is_localhost=None,
- is_private=None,
- is_automatic=None,
- error_message='Enter valid IPv4 address'):
-
- for n, value in enumerate((minip, maxip)):
- temp = []
- if isinstance(value, str):
- temp.append(value.split('.'))
- elif isinstance(value, (list, tuple)):
- if len(value) == len([item for item in value if isinstance(item, int)]) == 4:
- temp.append(value)
- else:
- for item in value:
- if isinstance(item, str):
- temp.append(item.split('.'))
- elif isinstance(item, (list, tuple)):
- temp.append(item)
- numbers = []
- for item in temp:
- number = 0
- for i, j in zip(self.numbers, item):
- number += i * int(j)
- numbers.append(number)
- if n == 0:
- self.minip = numbers
- else:
- self.maxip = numbers
- self.invert = invert
- self.is_localhost = is_localhost
- self.is_private = is_private
- self.is_automatic = is_automatic
- self.error_message = error_message
-
- def validate(self, value):
- if self.regex.match(value):
- number = 0
- for i, j in zip(self.numbers, value.split('.')):
- number += i * int(j)
- ok = False
-
- for bottom, top in zip(self.minip, self.maxip):
- if self.invert != (bottom <= number <= top):
- ok = True
-
- if ok and self.is_localhost is not None and \
- self.is_localhost != (number == self.localhost):
- ok = False
-
- private = any([private_number[0] <= number <= private_number[1]
- for private_number in self.private])
- if ok and self.is_private is not None and self.is_private != private:
- ok = False
-
- automatic = self.automatic[0] <= number <= self.automatic[1]
- if ok and self.is_automatic is not None and self.is_automatic != automatic:
- ok = False
-
- if ok:
- return value
-
- raise ValidationError(self.translator(self.error_message))
-
-
-class IS_IPV6(Validator):
- """
- Checks if field's value is an IP version 6 address.
-
- Uses the ipaddress from the Python 3 standard library
- and its Python 2 backport (in contrib/ipaddress.py).
-
- Args:
- is_private: None (default): indifferent
- True (enforce): address must be in fc00::/7 range
- False (forbid): address must NOT be in fc00::/7 range
- is_link_local: Same as above but uses fe80::/10 range
- is_reserved: Same as above but uses IETF reserved range
- is_multicast: Same as above but uses ff00::/8 range
- is_routeable: Similar to above but enforces not private, link_local,
- reserved or multicast
- is_6to4: Same as above but uses 2002::/16 range
- is_teredo: Same as above but uses 2001::/32 range
- subnets: value must be a member of at least one from list of subnets
-
- Examples:
- Check for valid IPv6 address:
-
- INPUT(_type='text', _name='name', requires=IS_IPV6())
-
- Check for valid IPv6 address is a link_local address:
-
- INPUT(_type='text', _name='name', requires=IS_IPV6(is_link_local=True))
-
- Check for valid IPv6 address that is Internet routeable:
-
- INPUT(_type='text', _name='name', requires=IS_IPV6(is_routeable=True))
-
- Check for valid IPv6 address in specified subnet:
-
- INPUT(_type='text', _name='name', requires=IS_IPV6(subnets=['2001::/32'])
-
- >>> IS_IPV6()('fe80::126c:8ffa:fe22:b3af')
- ('fe80::126c:8ffa:fe22:b3af', None)
- >>> IS_IPV6()('192.168.1.1')
- ('192.168.1.1', 'enter valid IPv6 address')
- >>> IS_IPV6(error_message='Bad ip')('192.168.1.1')
- ('192.168.1.1', 'bad ip')
- >>> IS_IPV6(is_link_local=True)('fe80::126c:8ffa:fe22:b3af')
- ('fe80::126c:8ffa:fe22:b3af', None)
- >>> IS_IPV6(is_link_local=False)('fe80::126c:8ffa:fe22:b3af')
- ('fe80::126c:8ffa:fe22:b3af', 'enter valid IPv6 address')
- >>> IS_IPV6(is_link_local=True)('2001::126c:8ffa:fe22:b3af')
- ('2001::126c:8ffa:fe22:b3af', 'enter valid IPv6 address')
- >>> IS_IPV6(is_multicast=True)('2001::126c:8ffa:fe22:b3af')
- ('2001::126c:8ffa:fe22:b3af', 'enter valid IPv6 address')
- >>> IS_IPV6(is_multicast=True)('ff00::126c:8ffa:fe22:b3af')
- ('ff00::126c:8ffa:fe22:b3af', None)
- >>> IS_IPV6(is_routeable=True)('2001::126c:8ffa:fe22:b3af')
- ('2001::126c:8ffa:fe22:b3af', None)
- >>> IS_IPV6(is_routeable=True)('ff00::126c:8ffa:fe22:b3af')
- ('ff00::126c:8ffa:fe22:b3af', 'enter valid IPv6 address')
- >>> IS_IPV6(subnets='2001::/32')('2001::8ffa:fe22:b3af')
- ('2001::8ffa:fe22:b3af', None)
- >>> IS_IPV6(subnets='fb00::/8')('2001::8ffa:fe22:b3af')
- ('2001::8ffa:fe22:b3af', 'enter valid IPv6 address')
- >>> IS_IPV6(subnets=['fc00::/8','2001::/32'])('2001::8ffa:fe22:b3af')
- ('2001::8ffa:fe22:b3af', None)
- >>> IS_IPV6(subnets='invalidsubnet')('2001::8ffa:fe22:b3af')
- ('2001::8ffa:fe22:b3af', 'invalid subnet provided')
-
- """
-
- def __init__(self,
- is_private=None,
- is_link_local=None,
- is_reserved=None,
- is_multicast=None,
- is_routeable=None,
- is_6to4=None,
- is_teredo=None,
- subnets=None,
- error_message='Enter valid IPv6 address'):
-
- self.is_private = is_private
- self.is_link_local = is_link_local
- self.is_reserved = is_reserved
- self.is_multicast = is_multicast
- self.is_routeable = is_routeable
- self.is_6to4 = is_6to4
- self.is_teredo = is_teredo
- self.subnets = subnets
- self.error_message = error_message
-
- def validate(self, value):
- try:
- ip = ipaddress.IPv6Address(to_unicode(value))
- ok = True
- except ipaddress.AddressValueError:
- raise ValidationError(self.translator(self.error_message))
-
- if self.subnets:
- # iterate through self.subnets to see if value is a member
- ok = False
- if isinstance(self.subnets, str):
- self.subnets = [self.subnets]
- for network in self.subnets:
- try:
- ipnet = ipaddress.IPv6Network(to_unicode(network))
- except (ipaddress.NetmaskValueError, ipaddress.AddressValueError):
- raise ValidationError(self.translator('invalid subnet provided'))
- if ip in ipnet:
- ok = True
-
- if self.is_routeable:
- self.is_private = False
- self.is_reserved = False
- self.is_multicast = False
-
- if ok and self.is_private is not None and \
- self.is_private != ip.is_private:
- ok = False
- if ok and self.is_link_local is not None and \
- self.is_link_local != ip.is_link_local:
- ok = False
- if ok and self.is_reserved is not None and \
- self.is_reserved != ip.is_reserved:
- ok = False
- if ok and self.is_multicast is not None and \
- self.is_multicast != ip.is_multicast:
- ok = False
- if ok and self.is_6to4 is not None and \
- self.is_6to4 != bool(ip.sixtofour):
- ok = False
- if ok and self.is_teredo is not None and \
- self.is_teredo != bool(ip.teredo):
- ok = False
-
- if ok:
- return value
-
- raise ValidationError(self.translator(self.error_message))
-
-
-class IS_IPADDRESS(Validator):
- """
- Checks if field's value is an IP Address (v4 or v6). Can be set to force
- addresses from within a specific range. Checks are done with the correct
- IS_IPV4 and IS_IPV6 validators.
-
- Uses the ipaddress from the Python 3 standard library
- and its Python 2 backport (in contrib/ipaddress.py).
-
- Args:
- minip: lowest allowed address; accepts:
- str, eg. 192.168.0.1
- list or tuple of octets, eg. [192, 168, 0, 1]
- maxip: highest allowed address; same as above
- invert: True to allow addresses only from outside of given range; note
- that range boundaries are not matched this way
-
- IPv4 specific arguments:
-
- - is_localhost: localhost address treatment:
-
- - None (default): indifferent
- - True (enforce): query address must match localhost address
- (127.0.0.1)
- - False (forbid): query address must not match localhost address
- - is_private: same as above, except that query address is checked against
- two address ranges: 172.16.0.0 - 172.31.255.255 and
- 192.168.0.0 - 192.168.255.255
- - is_automatic: same as above, except that query address is checked against
- one address range: 169.254.0.0 - 169.254.255.255
- - is_ipv4: either:
-
- - None (default): indifferent
- - True (enforce): must be an IPv4 address
- - False (forbid): must NOT be an IPv4 address
-
- IPv6 specific arguments:
-
- - is_link_local: Same as above but uses fe80::/10 range
- - is_reserved: Same as above but uses IETF reserved range
- - is_multicast: Same as above but uses ff00::/8 range
- - is_routeable: Similar to above but enforces not private, link_local,
- reserved or multicast
- - is_6to4: Same as above but uses 2002::/16 range
- - is_teredo: Same as above but uses 2001::/32 range
- - subnets: value must be a member of at least one from list of subnets
- - is_ipv6: either:
-
- - None (default): indifferent
- - True (enforce): must be an IPv6 address
- - False (forbid): must NOT be an IPv6 address
-
- Minip and maxip may also be lists or tuples of addresses in all above
- forms (str, int, list / tuple), allowing setup of multiple address ranges::
-
- minip = (minip1, minip2, ... minipN)
- | | |
- | | |
- maxip = (maxip1, maxip2, ... maxipN)
-
- Longer iterable will be truncated to match length of shorter one.
-
- >>> IS_IPADDRESS()('192.168.1.5')
- ('192.168.1.5', None)
- >>> IS_IPADDRESS(is_ipv6=False)('192.168.1.5')
- ('192.168.1.5', None)
- >>> IS_IPADDRESS()('255.255.255.255')
- ('255.255.255.255', None)
- >>> IS_IPADDRESS()('192.168.1.5 ')
- ('192.168.1.5 ', 'enter valid IP address')
- >>> IS_IPADDRESS()('192.168.1.1.5')
- ('192.168.1.1.5', 'enter valid IP address')
- >>> IS_IPADDRESS()('123.123')
- ('123.123', 'enter valid IP address')
- >>> IS_IPADDRESS()('1111.2.3.4')
- ('1111.2.3.4', 'enter valid IP address')
- >>> IS_IPADDRESS()('0111.2.3.4')
- ('0111.2.3.4', 'enter valid IP address')
- >>> IS_IPADDRESS()('256.2.3.4')
- ('256.2.3.4', 'enter valid IP address')
- >>> IS_IPADDRESS()('300.2.3.4')
- ('300.2.3.4', 'enter valid IP address')
- >>> IS_IPADDRESS(minip='192.168.1.0', maxip='192.168.1.255')('192.168.1.100')
- ('192.168.1.100', None)
- >>> IS_IPADDRESS(minip='1.2.3.5', maxip='1.2.3.9', error_message='Bad ip')('1.2.3.4')
- ('1.2.3.4', 'bad ip')
- >>> IS_IPADDRESS(maxip='1.2.3.4', invert=True)('127.0.0.1')
- ('127.0.0.1', None)
- >>> IS_IPADDRESS(maxip='192.168.1.4', invert=True)('192.168.1.4')
- ('192.168.1.4', 'enter valid IP address')
- >>> IS_IPADDRESS(is_localhost=True)('127.0.0.1')
- ('127.0.0.1', None)
- >>> IS_IPADDRESS(is_localhost=True)('192.168.1.10')
- ('192.168.1.10', 'enter valid IP address')
- >>> IS_IPADDRESS(is_localhost=False)('127.0.0.1')
- ('127.0.0.1', 'enter valid IP address')
- >>> IS_IPADDRESS(maxip='100.0.0.0', is_localhost=True)('127.0.0.1')
- ('127.0.0.1', 'enter valid IP address')
-
- >>> IS_IPADDRESS()('fe80::126c:8ffa:fe22:b3af')
- ('fe80::126c:8ffa:fe22:b3af', None)
- >>> IS_IPADDRESS(is_ipv4=False)('fe80::126c:8ffa:fe22:b3af')
- ('fe80::126c:8ffa:fe22:b3af', None)
- >>> IS_IPADDRESS()('fe80::126c:8ffa:fe22:b3af ')
- ('fe80::126c:8ffa:fe22:b3af ', 'enter valid IP address')
- >>> IS_IPADDRESS(is_ipv4=True)('fe80::126c:8ffa:fe22:b3af')
- ('fe80::126c:8ffa:fe22:b3af', 'enter valid IP address')
- >>> IS_IPADDRESS(is_ipv6=True)('192.168.1.1')
- ('192.168.1.1', 'enter valid IP address')
- >>> IS_IPADDRESS(is_ipv6=True, error_message='Bad ip')('192.168.1.1')
- ('192.168.1.1', 'bad ip')
- >>> IS_IPADDRESS(is_link_local=True)('fe80::126c:8ffa:fe22:b3af')
- ('fe80::126c:8ffa:fe22:b3af', None)
- >>> IS_IPADDRESS(is_link_local=False)('fe80::126c:8ffa:fe22:b3af')
- ('fe80::126c:8ffa:fe22:b3af', 'enter valid IP address')
- >>> IS_IPADDRESS(is_link_local=True)('2001::126c:8ffa:fe22:b3af')
- ('2001::126c:8ffa:fe22:b3af', 'enter valid IP address')
- >>> IS_IPADDRESS(is_multicast=True)('2001::126c:8ffa:fe22:b3af')
- ('2001::126c:8ffa:fe22:b3af', 'enter valid IP address')
- >>> IS_IPADDRESS(is_multicast=True)('ff00::126c:8ffa:fe22:b3af')
- ('ff00::126c:8ffa:fe22:b3af', None)
- >>> IS_IPADDRESS(is_routeable=True)('2001::126c:8ffa:fe22:b3af')
- ('2001::126c:8ffa:fe22:b3af', None)
- >>> IS_IPADDRESS(is_routeable=True)('ff00::126c:8ffa:fe22:b3af')
- ('ff00::126c:8ffa:fe22:b3af', 'enter valid IP address')
- >>> IS_IPADDRESS(subnets='2001::/32')('2001::8ffa:fe22:b3af')
- ('2001::8ffa:fe22:b3af', None)
- >>> IS_IPADDRESS(subnets='fb00::/8')('2001::8ffa:fe22:b3af')
- ('2001::8ffa:fe22:b3af', 'enter valid IP address')
- >>> IS_IPADDRESS(subnets=['fc00::/8','2001::/32'])('2001::8ffa:fe22:b3af')
- ('2001::8ffa:fe22:b3af', None)
- >>> IS_IPADDRESS(subnets='invalidsubnet')('2001::8ffa:fe22:b3af')
- ('2001::8ffa:fe22:b3af', 'invalid subnet provided')
- """
-
- def __init__(self,
- minip='0.0.0.0',
- maxip='255.255.255.255',
- invert=False,
- is_localhost=None,
- is_private=None,
- is_automatic=None,
- is_ipv4=None,
- is_link_local=None,
- is_reserved=None,
- is_multicast=None,
- is_routeable=None,
- is_6to4=None,
- is_teredo=None,
- subnets=None,
- is_ipv6=None,
- error_message='Enter valid IP address'):
-
- self.minip = minip,
- self.maxip = maxip,
- self.invert = invert
- self.is_localhost = is_localhost
- self.is_private = is_private
- self.is_automatic = is_automatic
- self.is_ipv4 = is_ipv4 or is_ipv6 is False
- self.is_private = is_private
- self.is_link_local = is_link_local
- self.is_reserved = is_reserved
- self.is_multicast = is_multicast
- self.is_routeable = is_routeable
- self.is_6to4 = is_6to4
- self.is_teredo = is_teredo
- self.subnets = subnets
- self.is_ipv6 = is_ipv6 or is_ipv4 is False
- self.error_message = error_message
-
- def validate(self, value):
- IPAddress = ipaddress.ip_address
- IPv6Address = ipaddress.IPv6Address
- IPv4Address = ipaddress.IPv4Address
-
- try:
- ip = IPAddress(to_unicode(value))
- except ValueError:
- raise ValidationError(self.translator(self.error_message))
-
- if self.is_ipv4 and isinstance(ip, IPv6Address):
- raise ValidationError(self.translator(self.error_message))
- elif self.is_ipv6 and isinstance(ip, IPv4Address):
- raise ValidationError(self.translator(self.error_message))
- elif self.is_ipv4 or isinstance(ip, IPv4Address):
- return IS_IPV4(
- minip=self.minip,
- maxip=self.maxip,
- invert=self.invert,
- is_localhost=self.is_localhost,
- is_private=self.is_private,
- is_automatic=self.is_automatic,
- error_message=self.error_message
- ).validate(value)
- elif self.is_ipv6 or isinstance(ip, IPv6Address):
- return IS_IPV6(
- is_private=self.is_private,
- is_link_local=self.is_link_local,
- is_reserved=self.is_reserved,
- is_multicast=self.is_multicast,
- is_routeable=self.is_routeable,
- is_6to4=self.is_6to4,
- is_teredo=self.is_teredo,
- subnets=self.subnets,
- error_message=self.error_message
- ).validate(value)
- else:
- raise ValidationError(self.translator(self.error_message))