mirror of
https://github.com/djohnlewis/stackdump
synced 2025-01-22 22:51:36 +00:00
2948 lines
99 KiB
Python
2948 lines
99 KiB
Python
## FormEncode, a Form processor
|
|
## Copyright (C) 2003, Ian Bicking <ianb@colorstudy.com>
|
|
"""
|
|
Validator/Converters for use with FormEncode.
|
|
"""
|
|
|
|
import cgi
|
|
import locale
|
|
import re
|
|
import warnings
|
|
|
|
try:
|
|
set
|
|
except NameError: # Python < 2.4
|
|
from sets import Set as set
|
|
|
|
try:
|
|
import DNS
|
|
DNS.DiscoverNameServers()
|
|
have_dns = True
|
|
except ImportError:
|
|
have_dns = False
|
|
|
|
# These are only imported when needed
|
|
httplib = None
|
|
random = None
|
|
sha1 = None
|
|
socket = None
|
|
urlparse = None
|
|
|
|
filters = warnings.filters[:]
|
|
warnings.simplefilter('ignore', DeprecationWarning)
|
|
warnings.filters = filters
|
|
|
|
from interfaces import *
|
|
from api import *
|
|
|
|
# Dummy i18n translation function, nothing is translated here.
|
|
# Instead this is actually done in api.message.
|
|
# The surrounding _('string') of the strings is only for extracting
|
|
# the strings automatically.
|
|
# If you run pygettext with this source comment this function out temporarily.
|
|
_ = lambda s: s
|
|
|
|
|
|
############################################################
|
|
## Utility methods
|
|
############################################################
|
|
|
|
# These all deal with accepting both datetime and mxDateTime modules and types
|
|
datetime_module = None
|
|
mxDateTime_module = None
|
|
|
|
|
|
def import_datetime(module_type):
|
|
global datetime_module, mxDateTime_module
|
|
module_type = module_type and module_type.lower() or 'datetime'
|
|
if module_type == 'datetime':
|
|
if datetime_module is None:
|
|
import datetime as datetime_module
|
|
return datetime_module
|
|
elif module_type == 'mxdatetime':
|
|
if mxDateTime_module is None:
|
|
from mx import DateTime as mxDateTime_module
|
|
return mxDateTime_module
|
|
else:
|
|
raise ImportError('Invalid datetime module %r' % module_type)
|
|
|
|
|
|
def datetime_now(module):
|
|
if module.__name__ == 'datetime':
|
|
return module.datetime.now()
|
|
else:
|
|
return module.now()
|
|
|
|
|
|
def datetime_makedate(module, year, month, day):
|
|
if module.__name__ == 'datetime':
|
|
return module.date(year, month, day)
|
|
else:
|
|
try:
|
|
return module.DateTime(year, month, day)
|
|
except module.RangeError, e:
|
|
raise ValueError(str(e))
|
|
|
|
|
|
def datetime_time(module):
|
|
if module.__name__ == 'datetime':
|
|
return module.time
|
|
else:
|
|
return module.Time
|
|
|
|
|
|
def datetime_isotime(module):
|
|
if module.__name__ == 'datetime':
|
|
return module.time.isoformat
|
|
else:
|
|
return module.ISO.Time
|
|
|
|
|
|
############################################################
|
|
## Wrapper Validators
|
|
############################################################
|
|
|
|
class ConfirmType(FancyValidator):
|
|
"""
|
|
Confirms that the input/output is of the proper type.
|
|
|
|
Uses the parameters:
|
|
|
|
subclass:
|
|
The class or a tuple of classes; the item must be an instance
|
|
of the class or a subclass.
|
|
type:
|
|
A type or tuple of types (or classes); the item must be of
|
|
the exact class or type. Subclasses are not allowed.
|
|
|
|
Examples::
|
|
|
|
>>> cint = ConfirmType(subclass=int)
|
|
>>> cint.to_python(True)
|
|
True
|
|
>>> cint.to_python('1')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: '1' is not a subclass of <type 'int'>
|
|
>>> cintfloat = ConfirmType(subclass=(float, int))
|
|
>>> cintfloat.to_python(1.0), cintfloat.from_python(1.0)
|
|
(1.0, 1.0)
|
|
>>> cintfloat.to_python(1), cintfloat.from_python(1)
|
|
(1, 1)
|
|
>>> cintfloat.to_python(None)
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: None is not a subclass of one of the types <type 'float'>, <type 'int'>
|
|
>>> cint2 = ConfirmType(type=int)
|
|
>>> cint2(accept_python=False).from_python(True)
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: True must be of the type <type 'int'>
|
|
"""
|
|
|
|
subclass = None
|
|
type = None
|
|
|
|
messages = dict(
|
|
subclass=_('%(object)r is not a subclass of %(subclass)s'),
|
|
inSubclass=_('%(object)r is not a subclass of one of the types %(subclassList)s'),
|
|
inType=_('%(object)r must be one of the types %(typeList)s'),
|
|
type=_('%(object)r must be of the type %(type)s'))
|
|
|
|
def __init__(self, *args, **kw):
|
|
FancyValidator.__init__(self, *args, **kw)
|
|
if self.subclass:
|
|
if isinstance(self.subclass, list):
|
|
self.subclass = tuple(self.subclass)
|
|
elif not isinstance(self.subclass, tuple):
|
|
self.subclass = (self.subclass,)
|
|
self.validate_python = self.confirm_subclass
|
|
if self.type:
|
|
if isinstance(self.type, list):
|
|
self.type = tuple(self.type)
|
|
elif not isinstance(self.type, tuple):
|
|
self.type = (self.type,)
|
|
self.validate_python = self.confirm_type
|
|
|
|
def confirm_subclass(self, value, state):
|
|
if not isinstance(value, self.subclass):
|
|
if len(self.subclass) == 1:
|
|
msg = self.message('subclass', state, object=value,
|
|
subclass=self.subclass[0])
|
|
else:
|
|
subclass_list = ', '.join(map(str, self.subclass))
|
|
msg = self.message('inSubclass', state, object=value,
|
|
subclassList=subclass_list)
|
|
raise Invalid(msg, value, state)
|
|
|
|
def confirm_type(self, value, state):
|
|
for t in self.type:
|
|
if type(value) is t:
|
|
break
|
|
else:
|
|
if len(self.type) == 1:
|
|
msg = self.message('type', state, object=value,
|
|
type=self.type[0])
|
|
else:
|
|
msg = self.message('inType', state, object=value,
|
|
typeList=', '.join(map(str, self.type)))
|
|
raise Invalid(msg, value, state)
|
|
return value
|
|
|
|
def is_empty(self, value):
|
|
return False
|
|
|
|
|
|
class Wrapper(FancyValidator):
|
|
"""
|
|
Used to convert functions to validator/converters.
|
|
|
|
You can give a simple function for `to_python`, `from_python`,
|
|
`validate_python` or `validate_other`. If that function raises an
|
|
exception, the value is considered invalid. Whatever value the
|
|
function returns is considered the converted value.
|
|
|
|
Unlike validators, the `state` argument is not used. Functions
|
|
like `int` can be used here, that take a single argument.
|
|
|
|
Examples::
|
|
|
|
>>> def downcase(v):
|
|
... return v.lower()
|
|
>>> wrap = Wrapper(to_python=downcase)
|
|
>>> wrap.to_python('This')
|
|
'this'
|
|
>>> wrap.from_python('This')
|
|
'This'
|
|
>>> wrap2 = Wrapper(from_python=downcase)
|
|
>>> wrap2.from_python('This')
|
|
'this'
|
|
>>> wrap2.from_python(1)
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: 'int' object has no attribute 'lower'
|
|
>>> wrap3 = Wrapper(validate_python=int)
|
|
>>> wrap3.to_python('1')
|
|
'1'
|
|
>>> wrap3.to_python('a') # doctest: +ELLIPSIS
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: invalid literal for int()...
|
|
"""
|
|
|
|
func_to_python = None
|
|
func_from_python = None
|
|
func_validate_python = None
|
|
func_validate_other = None
|
|
|
|
def __init__(self, *args, **kw):
|
|
for n in ('to_python', 'from_python',
|
|
'validate_python', 'validate_other'):
|
|
if n in kw:
|
|
kw['func_%s' % n] = kw.pop(n)
|
|
FancyValidator.__init__(self, *args, **kw)
|
|
self._to_python = self.wrap(self.func_to_python)
|
|
self._from_python = self.wrap(self.func_from_python)
|
|
self.validate_python = self.wrap(self.func_validate_python)
|
|
self.validate_other = self.wrap(self.func_validate_other)
|
|
|
|
def wrap(self, func):
|
|
if not func:
|
|
return None
|
|
def result(value, state, func=func):
|
|
try:
|
|
return func(value)
|
|
except Exception, e:
|
|
raise Invalid(str(e), value, state)
|
|
return result
|
|
|
|
|
|
class Constant(FancyValidator):
|
|
"""
|
|
This converter converts everything to the same thing.
|
|
|
|
I.e., you pass in the constant value when initializing, then all
|
|
values get converted to that constant value.
|
|
|
|
This is only really useful for funny situations, like::
|
|
|
|
fromEmailValidator = ValidateAny(
|
|
ValidEmailAddress(),
|
|
Constant('unknown@localhost'))
|
|
|
|
In this case, the if the email is not valid
|
|
``'unknown@localhost'`` will be used instead. Of course, you
|
|
could use ``if_invalid`` instead.
|
|
|
|
Examples::
|
|
|
|
>>> Constant('X').to_python('y')
|
|
'X'
|
|
"""
|
|
|
|
__unpackargs__ = ('value',)
|
|
|
|
def _to_python(self, value, state):
|
|
return self.value
|
|
|
|
_from_python = _to_python
|
|
|
|
|
|
############################################################
|
|
## Normal validators
|
|
############################################################
|
|
|
|
class MaxLength(FancyValidator):
|
|
"""
|
|
Invalid if the value is longer than `maxLength`. Uses len(),
|
|
so it can work for strings, lists, or anything with length.
|
|
|
|
Examples::
|
|
|
|
>>> max5 = MaxLength(5)
|
|
>>> max5.to_python('12345')
|
|
'12345'
|
|
>>> max5.from_python('12345')
|
|
'12345'
|
|
>>> max5.to_python('123456')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Enter a value less than 5 characters long
|
|
>>> max5(accept_python=False).from_python('123456')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Enter a value less than 5 characters long
|
|
>>> max5.to_python([1, 2, 3])
|
|
[1, 2, 3]
|
|
>>> max5.to_python([1, 2, 3, 4, 5, 6])
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Enter a value less than 5 characters long
|
|
>>> max5.to_python(5)
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Invalid value (value with length expected)
|
|
"""
|
|
|
|
__unpackargs__ = ('maxLength',)
|
|
|
|
messages = dict(
|
|
tooLong=_('Enter a value less than %(maxLength)i characters long'),
|
|
invalid=_('Invalid value (value with length expected)'))
|
|
|
|
def validate_python(self, value, state):
|
|
try:
|
|
if value and len(value) > self.maxLength:
|
|
raise Invalid(
|
|
self.message('tooLong', state,
|
|
maxLength=self.maxLength), value, state)
|
|
else:
|
|
return None
|
|
except TypeError:
|
|
raise Invalid(
|
|
self.message('invalid', state), value, state)
|
|
|
|
|
|
class MinLength(FancyValidator):
|
|
"""
|
|
Invalid if the value is shorter than `minlength`. Uses len(), so
|
|
it can work for strings, lists, or anything with length. Note
|
|
that you **must** use ``not_empty=True`` if you don't want to
|
|
accept empty values -- empty values are not tested for length.
|
|
|
|
Examples::
|
|
|
|
>>> min5 = MinLength(5)
|
|
>>> min5.to_python('12345')
|
|
'12345'
|
|
>>> min5.from_python('12345')
|
|
'12345'
|
|
>>> min5.to_python('1234')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Enter a value at least 5 characters long
|
|
>>> min5(accept_python=False).from_python('1234')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Enter a value at least 5 characters long
|
|
>>> min5.to_python([1, 2, 3, 4, 5])
|
|
[1, 2, 3, 4, 5]
|
|
>>> min5.to_python([1, 2, 3])
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Enter a value at least 5 characters long
|
|
>>> min5.to_python(5)
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Invalid value (value with length expected)
|
|
|
|
"""
|
|
|
|
__unpackargs__ = ('minLength',)
|
|
|
|
messages = dict(
|
|
tooShort=_('Enter a value at least %(minLength)i characters long'),
|
|
invalid=_('Invalid value (value with length expected)'))
|
|
|
|
def validate_python(self, value, state):
|
|
try:
|
|
if len(value) < self.minLength:
|
|
raise Invalid(
|
|
self.message('tooShort', state,
|
|
minLength=self.minLength), value, state)
|
|
except TypeError:
|
|
raise Invalid(
|
|
self.message('invalid', state), value, state)
|
|
|
|
|
|
class NotEmpty(FancyValidator):
|
|
"""
|
|
Invalid if value is empty (empty string, empty list, etc).
|
|
|
|
Generally for objects that Python considers false, except zero
|
|
which is not considered invalid.
|
|
|
|
Examples::
|
|
|
|
>>> ne = NotEmpty(messages=dict(empty='enter something'))
|
|
>>> ne.to_python('')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: enter something
|
|
>>> ne.to_python(0)
|
|
0
|
|
"""
|
|
not_empty = True
|
|
|
|
messages = dict(
|
|
empty=_('Please enter a value'))
|
|
|
|
def validate_python(self, value, state):
|
|
if value == 0:
|
|
# This isn't "empty" for this definition.
|
|
return value
|
|
if not value:
|
|
raise Invalid(self.message('empty', state), value, state)
|
|
|
|
|
|
class Empty(FancyValidator):
|
|
"""
|
|
Invalid unless the value is empty. Use cleverly, if at all.
|
|
|
|
Examples::
|
|
|
|
>>> Empty.to_python(0)
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: You cannot enter a value here
|
|
"""
|
|
|
|
messages = dict(
|
|
notEmpty=_('You cannot enter a value here'))
|
|
|
|
def validate_python(self, value, state):
|
|
if value or value == 0:
|
|
raise Invalid(self.message('notEmpty', state), value, state)
|
|
|
|
|
|
class Regex(FancyValidator):
|
|
"""
|
|
Invalid if the value doesn't match the regular expression `regex`.
|
|
|
|
The regular expression can be a compiled re object, or a string
|
|
which will be compiled for you.
|
|
|
|
Use strip=True if you want to strip the value before validation,
|
|
and as a form of conversion (often useful).
|
|
|
|
Examples::
|
|
|
|
>>> cap = Regex(r'^[A-Z]+$')
|
|
>>> cap.to_python('ABC')
|
|
'ABC'
|
|
|
|
Note that ``.from_python()`` calls (in general) do not validate
|
|
the input::
|
|
|
|
>>> cap.from_python('abc')
|
|
'abc'
|
|
>>> cap(accept_python=False).from_python('abc')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: The input is not valid
|
|
>>> cap.to_python(1)
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: The input must be a string (not a <type 'int'>: 1)
|
|
>>> Regex(r'^[A-Z]+$', strip=True).to_python(' ABC ')
|
|
'ABC'
|
|
>>> Regex(r'this', regexOps=('I',)).to_python('THIS')
|
|
'THIS'
|
|
"""
|
|
|
|
regexOps = ()
|
|
strip = False
|
|
regex = None
|
|
|
|
__unpackargs__ = ('regex',)
|
|
|
|
messages = dict(
|
|
invalid=_('The input is not valid'))
|
|
|
|
def __init__(self, *args, **kw):
|
|
FancyValidator.__init__(self, *args, **kw)
|
|
if isinstance(self.regex, basestring):
|
|
ops = 0
|
|
assert not isinstance(self.regexOps, basestring), (
|
|
"regexOps should be a list of options from the re module "
|
|
"(names, or actual values)")
|
|
for op in self.regexOps:
|
|
if isinstance(op, basestring):
|
|
ops |= getattr(re, op)
|
|
else:
|
|
ops |= op
|
|
self.regex = re.compile(self.regex, ops)
|
|
|
|
def validate_python(self, value, state):
|
|
self.assert_string(value, state)
|
|
if self.strip and isinstance(value, basestring):
|
|
value = value.strip()
|
|
if not self.regex.search(value):
|
|
raise Invalid(self.message('invalid', state), value, state)
|
|
|
|
def _to_python(self, value, state):
|
|
if self.strip and isinstance(value, basestring):
|
|
return value.strip()
|
|
return value
|
|
|
|
|
|
class PlainText(Regex):
|
|
"""
|
|
Test that the field contains only letters, numbers, underscore,
|
|
and the hyphen. Subclasses Regex.
|
|
|
|
Examples::
|
|
|
|
>>> PlainText.to_python('_this9_')
|
|
'_this9_'
|
|
>>> PlainText.from_python(' this ')
|
|
' this '
|
|
>>> PlainText(accept_python=False).from_python(' this ')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Enter only letters, numbers, or _ (underscore)
|
|
>>> PlainText(strip=True).to_python(' this ')
|
|
'this'
|
|
>>> PlainText(strip=True).from_python(' this ')
|
|
'this'
|
|
"""
|
|
|
|
regex = r"^[a-zA-Z_\-0-9]*$"
|
|
|
|
messages = dict(
|
|
invalid=_('Enter only letters, numbers, or _ (underscore)'))
|
|
|
|
|
|
class OneOf(FancyValidator):
|
|
"""
|
|
Tests that the value is one of the members of a given list.
|
|
|
|
If ``testValueList=True``, then if the input value is a list or
|
|
tuple, all the members of the sequence will be checked (i.e., the
|
|
input must be a subset of the allowed values).
|
|
|
|
Use ``hideList=True`` to keep the list of valid values out of the
|
|
error message in exceptions.
|
|
|
|
Examples::
|
|
|
|
>>> oneof = OneOf([1, 2, 3])
|
|
>>> oneof.to_python(1)
|
|
1
|
|
>>> oneof.to_python(4)
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Value must be one of: 1; 2; 3 (not 4)
|
|
>>> oneof(testValueList=True).to_python([2, 3, [1, 2, 3]])
|
|
[2, 3, [1, 2, 3]]
|
|
>>> oneof.to_python([2, 3, [1, 2, 3]])
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Value must be one of: 1; 2; 3 (not [2, 3, [1, 2, 3]])
|
|
"""
|
|
|
|
list = None
|
|
testValueList = False
|
|
hideList = False
|
|
|
|
__unpackargs__ = ('list',)
|
|
|
|
messages = dict(
|
|
invalid=_('Invalid value'),
|
|
notIn=_('Value must be one of: %(items)s (not %(value)r)'))
|
|
|
|
def validate_python(self, value, state):
|
|
if self.testValueList and isinstance(value, (list, tuple)):
|
|
for v in value:
|
|
self.validate_python(v, state)
|
|
else:
|
|
if not value in self.list:
|
|
if self.hideList:
|
|
raise Invalid(self.message('invalid', state), value, state)
|
|
else:
|
|
try:
|
|
items = '; '.join(map(str, self.list))
|
|
except UnicodeError:
|
|
items = '; '.join(map(unicode, self.list))
|
|
raise Invalid(
|
|
self.message('notIn', state,
|
|
items=items, value=value), value, state)
|
|
|
|
|
|
class DictConverter(FancyValidator):
|
|
"""
|
|
Converts values based on a dictionary which has values as keys for
|
|
the resultant values.
|
|
|
|
If ``allowNull`` is passed, it will not balk if a false value
|
|
(e.g., '' or None) is given (it will return None in these cases).
|
|
|
|
to_python takes keys and gives values, from_python takes values and
|
|
gives keys.
|
|
|
|
If you give hideDict=True, then the contents of the dictionary
|
|
will not show up in error messages.
|
|
|
|
Examples::
|
|
|
|
>>> dc = DictConverter({1: 'one', 2: 'two'})
|
|
>>> dc.to_python(1)
|
|
'one'
|
|
>>> dc.from_python('one')
|
|
1
|
|
>>> dc.to_python(3)
|
|
Traceback (most recent call last):
|
|
....
|
|
Invalid: Enter a value from: 1; 2
|
|
>>> dc2 = dc(hideDict=True)
|
|
>>> dc2.hideDict
|
|
True
|
|
>>> dc2.dict
|
|
{1: 'one', 2: 'two'}
|
|
>>> dc2.to_python(3)
|
|
Traceback (most recent call last):
|
|
....
|
|
Invalid: Choose something
|
|
>>> dc.from_python('three')
|
|
Traceback (most recent call last):
|
|
....
|
|
Invalid: Nothing in my dictionary goes by the value 'three'. Choose one of: 'one'; 'two'
|
|
"""
|
|
|
|
messages = dict(
|
|
keyNotFound=_('Choose something'),
|
|
chooseKey=_('Enter a value from: %(items)s'),
|
|
valueNotFound=_('That value is not known'),
|
|
chooseValue=_('Nothing in my dictionary goes by the value %(value)s.'
|
|
' Choose one of: %(items)s'))
|
|
|
|
dict = None
|
|
hideDict = False
|
|
|
|
__unpackargs__ = ('dict',)
|
|
|
|
def _to_python(self, value, state):
|
|
try:
|
|
return self.dict[value]
|
|
except KeyError:
|
|
if self.hideDict:
|
|
raise Invalid(self.message('keyNotFound', state), value, state)
|
|
else:
|
|
items = '; '.join(map(repr, self.dict.keys()))
|
|
raise Invalid(
|
|
self.message('chooseKey', state, items=items), value, state)
|
|
|
|
def _from_python(self, value, state):
|
|
for k, v in self.dict.items():
|
|
if value == v:
|
|
return k
|
|
if self.hideDict:
|
|
raise Invalid(self.message('valueNotFound', state), value, state)
|
|
else:
|
|
items = '; '.join(map(repr, self.dict.values()))
|
|
raise Invalid(
|
|
self.message('chooseValue', state,
|
|
value=repr(value), items=items), value, state)
|
|
|
|
|
|
class IndexListConverter(FancyValidator):
|
|
"""
|
|
Converts a index (which may be a string like '2') to the value in
|
|
the given list.
|
|
|
|
Examples::
|
|
|
|
>>> index = IndexListConverter(['zero', 'one', 'two'])
|
|
>>> index.to_python(0)
|
|
'zero'
|
|
>>> index.from_python('zero')
|
|
0
|
|
>>> index.to_python('1')
|
|
'one'
|
|
>>> index.to_python(5)
|
|
Traceback (most recent call last):
|
|
Invalid: Index out of range
|
|
>>> index(not_empty=True).to_python(None)
|
|
Traceback (most recent call last):
|
|
Invalid: Please enter a value
|
|
>>> index.from_python('five')
|
|
Traceback (most recent call last):
|
|
Invalid: Item 'five' was not found in the list
|
|
"""
|
|
|
|
list = None
|
|
|
|
__unpackargs__ = ('list',)
|
|
|
|
messages = dict(
|
|
integer=_('Must be an integer index'),
|
|
outOfRange=_('Index out of range'),
|
|
notFound=_('Item %(value)s was not found in the list'))
|
|
|
|
def _to_python(self, value, state):
|
|
try:
|
|
value = int(value)
|
|
except (ValueError, TypeError):
|
|
raise Invalid(self.message('integer', state), value, state)
|
|
try:
|
|
return self.list[value]
|
|
except IndexError:
|
|
raise Invalid(self.message('outOfRange', state), value, state)
|
|
|
|
def _from_python(self, value, state):
|
|
for i in range(len(self.list)):
|
|
if self.list[i] == value:
|
|
return i
|
|
raise Invalid(
|
|
self.message('notFound', state, value=repr(value)), value, state)
|
|
|
|
|
|
class DateValidator(FancyValidator):
|
|
"""
|
|
Validates that a date is within the given range. Be sure to call
|
|
DateConverter first if you aren't expecting mxDateTime input.
|
|
|
|
``earliest_date`` and ``latest_date`` may be functions; if so,
|
|
they will be called each time before validating.
|
|
|
|
``after_now`` means a time after the current timestamp; note that
|
|
just a few milliseconds before now is invalid! ``today_or_after``
|
|
is more permissive, and ignores hours and minutes.
|
|
|
|
Examples::
|
|
|
|
>>> from datetime import datetime, timedelta
|
|
>>> d = DateValidator(earliest_date=datetime(2003, 1, 1))
|
|
>>> d.to_python(datetime(2004, 1, 1))
|
|
datetime.datetime(2004, 1, 1, 0, 0)
|
|
>>> d.to_python(datetime(2002, 1, 1))
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Date must be after Wednesday, 01 January 2003
|
|
>>> d.to_python(datetime(2003, 1, 1))
|
|
datetime.datetime(2003, 1, 1, 0, 0)
|
|
>>> d = DateValidator(after_now=True)
|
|
>>> now = datetime.now()
|
|
>>> d.to_python(now+timedelta(seconds=5)) == now+timedelta(seconds=5)
|
|
True
|
|
>>> d.to_python(now-timedelta(days=1))
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: The date must be sometime in the future
|
|
>>> d.to_python(now+timedelta(days=1)) > now
|
|
True
|
|
>>> d = DateValidator(today_or_after=True)
|
|
>>> d.to_python(now) == now
|
|
True
|
|
|
|
"""
|
|
|
|
earliest_date = None
|
|
latest_date = None
|
|
after_now = False
|
|
# Like after_now, but just after this morning:
|
|
today_or_after = False
|
|
# Use None or 'datetime' for the datetime module in the standard lib,
|
|
# or 'mxDateTime' to force the mxDateTime module
|
|
datetime_module = None
|
|
|
|
messages = dict(
|
|
after=_('Date must be after %(date)s'),
|
|
before=_('Date must be before %(date)s'),
|
|
# Double %'s, because this will be substituted twice:
|
|
date_format=_('%%A, %%d %%B %%Y'),
|
|
future=_('The date must be sometime in the future'))
|
|
|
|
def validate_python(self, value, state):
|
|
date_format = self.message('date_format', state)
|
|
if isinstance(date_format, unicode):
|
|
# strftime uses the locale encoding, not Unicode
|
|
encoding = locale.getlocale(locale.LC_TIME)[1] or 'utf-8'
|
|
date_format = date_format.encode(encoding)
|
|
else:
|
|
encoding = None
|
|
if self.earliest_date:
|
|
if callable(self.earliest_date):
|
|
earliest_date = self.earliest_date()
|
|
else:
|
|
earliest_date = self.earliest_date
|
|
if value < earliest_date:
|
|
date_formatted = earliest_date.strftime(date_format)
|
|
if encoding:
|
|
date_formatted = date_formatted.decode(encoding)
|
|
raise Invalid(
|
|
self.message('after', state, date=date_formatted),
|
|
value, state)
|
|
if self.latest_date:
|
|
if callable(self.latest_date):
|
|
latest_date = self.latest_date()
|
|
else:
|
|
latest_date = self.latest_date
|
|
if value > latest_date:
|
|
date_formatted = latest_date.strftime(date_format)
|
|
if encoding:
|
|
date_formatted = date_formatted.decode(encoding)
|
|
raise Invalid(
|
|
self.message('before', state, date=date_formatted),
|
|
value, state)
|
|
if self.after_now:
|
|
dt_mod = import_datetime(self.datetime_module)
|
|
now = datetime_now(dt_mod)
|
|
if value < now:
|
|
date_formatted = now.strftime(date_format)
|
|
if encoding:
|
|
date_formatted = date_formatted.decode(encoding)
|
|
raise Invalid(
|
|
self.message('future', state, date=date_formatted),
|
|
value, state)
|
|
if self.today_or_after:
|
|
dt_mod = import_datetime(self.datetime_module)
|
|
now = datetime_now(dt_mod)
|
|
today = datetime_makedate(dt_mod,
|
|
now.year, now.month, now.day)
|
|
value_as_date = datetime_makedate(
|
|
dt_mod, value.year, value.month, value.day)
|
|
if value_as_date < today:
|
|
date_formatted = now.strftime(date_format)
|
|
if encoding:
|
|
date_formatted = date_formatted.decode(encoding)
|
|
raise Invalid(
|
|
self.message('future', state, date=date_formatted),
|
|
value, state)
|
|
|
|
|
|
class Bool(FancyValidator):
|
|
"""
|
|
Always Valid, returns True or False based on the value and the
|
|
existance of the value.
|
|
|
|
If you want to convert strings like ``'true'`` to booleans, then
|
|
use ``StringBool``.
|
|
|
|
Examples::
|
|
|
|
>>> Bool.to_python(0)
|
|
False
|
|
>>> Bool.to_python(1)
|
|
True
|
|
>>> Bool.to_python('')
|
|
False
|
|
>>> Bool.to_python(None)
|
|
False
|
|
"""
|
|
|
|
if_missing = False
|
|
|
|
def _to_python(self, value, state):
|
|
return bool(value)
|
|
_from_python = _to_python
|
|
|
|
def empty_value(self, value):
|
|
return False
|
|
|
|
|
|
class RangeValidator(FancyValidator):
|
|
"""This is an abstract base class for Int and Number.
|
|
|
|
It verifies that a value is within range. It accepts min and max
|
|
values in the constructor.
|
|
|
|
(Since this is an abstract base class, the tests are in Int and Number.)
|
|
|
|
"""
|
|
|
|
messages = dict(
|
|
tooLow=_('Please enter a number that is %(min)s or greater'),
|
|
tooHigh=_('Please enter a number that is %(max)s or smaller'))
|
|
|
|
min = None
|
|
max = None
|
|
|
|
def validate_python(self, value, state):
|
|
if self.min is not None:
|
|
if value < self.min:
|
|
msg = self.message('tooLow', state, min=self.min)
|
|
raise Invalid(msg, value, state)
|
|
if self.max is not None:
|
|
if value > self.max:
|
|
msg = self.message('tooHigh', state, max=self.max)
|
|
raise Invalid(msg, value, state)
|
|
|
|
|
|
class Int(RangeValidator):
|
|
"""Convert a value to an integer.
|
|
|
|
Example::
|
|
|
|
>>> Int.to_python('10')
|
|
10
|
|
>>> Int.to_python('ten')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Please enter an integer value
|
|
>>> Int(min=5).to_python('6')
|
|
6
|
|
>>> Int(max=10).to_python('11')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Please enter a number that is 10 or smaller
|
|
|
|
"""
|
|
|
|
messages = dict(
|
|
integer=_('Please enter an integer value'))
|
|
|
|
def _to_python(self, value, state):
|
|
try:
|
|
return int(value)
|
|
except (ValueError, TypeError):
|
|
raise Invalid(self.message('integer', state), value, state)
|
|
|
|
_from_python = _to_python
|
|
|
|
|
|
class Number(RangeValidator):
|
|
"""Convert a value to a float or integer.
|
|
|
|
Tries to convert it to an integer if no information is lost.
|
|
|
|
Example::
|
|
|
|
>>> Number.to_python('10')
|
|
10
|
|
>>> Number.to_python('10.5')
|
|
10.5
|
|
>>> Number.to_python('ten')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Please enter a number
|
|
>>> Number(min=5).to_python('6.5')
|
|
6.5
|
|
>>> Number(max=10.5).to_python('11.5')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Please enter a number that is 10.5 or smaller
|
|
>>> Number().to_python('infinity')
|
|
inf
|
|
|
|
"""
|
|
|
|
messages = dict(
|
|
number=_('Please enter a number'))
|
|
|
|
def _to_python(self, value, state):
|
|
try:
|
|
value = float(value)
|
|
try:
|
|
int_value = int(value)
|
|
except OverflowError:
|
|
int_value = None
|
|
if value == int_value:
|
|
return int_value
|
|
return value
|
|
except ValueError:
|
|
raise Invalid(self.message('number', state), value, state)
|
|
|
|
|
|
class String(FancyValidator):
|
|
"""
|
|
Converts things to string, but treats empty things as the empty string.
|
|
|
|
Also takes a `max` and `min` argument, and the string length must fall
|
|
in that range.
|
|
|
|
Also you may give an `encoding` argument, which will encode any unicode
|
|
that is found. Lists and tuples are joined with `list_joiner`
|
|
(default ``', '``) in ``from_python``.
|
|
|
|
::
|
|
|
|
>>> String(min=2).to_python('a')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Enter a value 2 characters long or more
|
|
>>> String(max=10).to_python('xxxxxxxxxxx')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Enter a value not more than 10 characters long
|
|
>>> String().from_python(None)
|
|
''
|
|
>>> String().from_python([])
|
|
''
|
|
>>> String().to_python(None)
|
|
''
|
|
>>> String(min=3).to_python(None)
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Please enter a value
|
|
>>> String(min=1).to_python('')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Please enter a value
|
|
|
|
"""
|
|
|
|
min = None
|
|
max = None
|
|
not_empty = None
|
|
encoding = None
|
|
list_joiner = ', '
|
|
|
|
messages = dict(
|
|
tooLong=_('Enter a value not more than %(max)i characters long'),
|
|
tooShort=_('Enter a value %(min)i characters long or more'))
|
|
|
|
def __initargs__(self, new_attrs):
|
|
if self.not_empty is None and self.min:
|
|
self.not_empty = True
|
|
|
|
def _to_python(self, value, state):
|
|
if value is None:
|
|
value = ''
|
|
elif not isinstance(value, basestring):
|
|
try:
|
|
value = str(value)
|
|
except UnicodeEncodeError:
|
|
value = unicode(value)
|
|
if self.encoding is not None and isinstance(value, unicode):
|
|
value = value.encode(self.encoding)
|
|
return value
|
|
|
|
def _from_python(self, value, state):
|
|
if value is None:
|
|
value = ''
|
|
elif not isinstance(value, basestring):
|
|
if isinstance(value, (list, tuple)):
|
|
value = self.list_joiner.join([
|
|
self._from_python(v, state) for v in value])
|
|
try:
|
|
value = str(value)
|
|
except UnicodeEncodeError:
|
|
value = unicode(value)
|
|
if self.encoding is not None and isinstance(value, unicode):
|
|
value = value.encode(self.encoding)
|
|
if self.strip:
|
|
value = value.strip()
|
|
return value
|
|
|
|
def validate_other(self, value, state):
|
|
if self.max is None and self.min is None:
|
|
return
|
|
if value is None:
|
|
value = ''
|
|
elif not isinstance(value, basestring):
|
|
try:
|
|
value = str(value)
|
|
except UnicodeEncodeError:
|
|
value = unicode(value)
|
|
if self.max is not None and len(value) > self.max:
|
|
raise Invalid(
|
|
self.message('tooLong', state, max=self.max), value, state)
|
|
if self.min is not None and len(value) < self.min:
|
|
raise Invalid(
|
|
self.message('tooShort', state, min=self.min), value, state)
|
|
|
|
def empty_value(self, value):
|
|
return ''
|
|
|
|
class UnicodeString(String):
|
|
"""
|
|
Converts things to unicode string, this is a specialization of
|
|
the String class.
|
|
|
|
In addition to the String arguments, an encoding argument is also
|
|
accepted. By default the encoding will be utf-8. You can overwrite
|
|
this using the encoding parameter. You can also set inputEncoding
|
|
and outputEncoding differently. An inputEncoding of None means
|
|
"do not decode", an outputEncoding of None means "do not encode".
|
|
|
|
All converted strings are returned as Unicode strings.
|
|
|
|
::
|
|
|
|
>>> UnicodeString().to_python(None)
|
|
u''
|
|
>>> UnicodeString().to_python([])
|
|
u''
|
|
>>> UnicodeString(encoding='utf-7').to_python('Ni Ni Ni')
|
|
u'Ni Ni Ni'
|
|
|
|
"""
|
|
encoding = 'utf-8'
|
|
inputEncoding = NoDefault
|
|
outputEncoding = NoDefault
|
|
messages = dict(
|
|
badEncoding=_('Invalid data or incorrect encoding'))
|
|
|
|
def __init__(self, **kw):
|
|
String.__init__(self, **kw)
|
|
if self.inputEncoding is NoDefault:
|
|
self.inputEncoding = self.encoding
|
|
if self.outputEncoding is NoDefault:
|
|
self.outputEncoding = self.encoding
|
|
|
|
def _to_python(self, value, state):
|
|
if not value:
|
|
return u''
|
|
if isinstance(value, unicode):
|
|
return value
|
|
if not isinstance(value, unicode):
|
|
if hasattr(value, '__unicode__'):
|
|
value = unicode(value)
|
|
return value
|
|
else:
|
|
value = str(value)
|
|
if self.inputEncoding:
|
|
try:
|
|
value = unicode(value, self.inputEncoding)
|
|
except UnicodeDecodeError:
|
|
raise Invalid(self.message('badEncoding', state), value, state)
|
|
except TypeError:
|
|
raise Invalid(
|
|
self.message('badType', state,
|
|
type=type(value), value=value), value, state)
|
|
return value
|
|
|
|
def _from_python(self, value, state):
|
|
if not isinstance(value, unicode):
|
|
if hasattr(value, '__unicode__'):
|
|
value = unicode(value)
|
|
else:
|
|
value = str(value)
|
|
if self.outputEncoding and isinstance(value, unicode):
|
|
value = value.encode(self.outputEncoding)
|
|
return value
|
|
|
|
def empty_value(self, value):
|
|
return u''
|
|
|
|
class Set(FancyValidator):
|
|
"""
|
|
This is for when you think you may return multiple values for a
|
|
certain field.
|
|
|
|
This way the result will always be a list, even if there's only
|
|
one result. It's equivalent to ForEach(convert_to_list=True).
|
|
|
|
If you give ``use_set=True``, then it will return an actual
|
|
``set`` object.
|
|
|
|
::
|
|
|
|
>>> Set.to_python(None)
|
|
[]
|
|
>>> Set.to_python('this')
|
|
['this']
|
|
>>> Set.to_python(('this', 'that'))
|
|
['this', 'that']
|
|
>>> s = Set(use_set=True)
|
|
>>> s.to_python(None)
|
|
set([])
|
|
>>> s.to_python('this')
|
|
set(['this'])
|
|
>>> s.to_python(('this',))
|
|
set(['this'])
|
|
"""
|
|
|
|
use_set = False
|
|
|
|
if_missing = ()
|
|
|
|
def _to_python(self, value, state):
|
|
if self.use_set:
|
|
if isinstance(value, set):
|
|
return value
|
|
elif isinstance(value, (list, tuple)):
|
|
return set(value)
|
|
elif value is None:
|
|
return set()
|
|
else:
|
|
return set([value])
|
|
else:
|
|
if isinstance(value, list):
|
|
return value
|
|
elif isinstance(value, set):
|
|
return list(value)
|
|
elif isinstance(value, tuple):
|
|
return list(value)
|
|
elif value is None:
|
|
return []
|
|
else:
|
|
return [value]
|
|
|
|
def empty_value(self, value):
|
|
if self.use_set:
|
|
return set()
|
|
else:
|
|
return []
|
|
|
|
class Email(FancyValidator):
|
|
r"""
|
|
Validate an email address.
|
|
|
|
If you pass ``resolve_domain=True``, then it will try to resolve
|
|
the domain name to make sure it's valid. This takes longer, of
|
|
course. You must have the `pyDNS <http://pydns.sf.net>`__ modules
|
|
installed to look up DNS (MX and A) records.
|
|
|
|
::
|
|
|
|
>>> e = Email()
|
|
>>> e.to_python(' test@foo.com ')
|
|
'test@foo.com'
|
|
>>> e.to_python('test')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: An email address must contain a single @
|
|
>>> e.to_python('test@foobar')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: The domain portion of the email address is invalid (the portion after the @: foobar)
|
|
>>> e.to_python('test@foobar.com.5')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: The domain portion of the email address is invalid (the portion after the @: foobar.com.5)
|
|
>>> e.to_python('test@foo..bar.com')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: The domain portion of the email address is invalid (the portion after the @: foo..bar.com)
|
|
>>> e.to_python('test@.foo.bar.com')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: The domain portion of the email address is invalid (the portion after the @: .foo.bar.com)
|
|
>>> e.to_python('nobody@xn--m7r7ml7t24h.com')
|
|
'nobody@xn--m7r7ml7t24h.com'
|
|
>>> e.to_python('o*reilly@test.com')
|
|
'o*reilly@test.com'
|
|
>>> e = Email(resolve_domain=True)
|
|
>>> e.resolve_domain
|
|
True
|
|
>>> e.to_python('doesnotexist@colorstudy.com')
|
|
'doesnotexist@colorstudy.com'
|
|
>>> e.to_python('test@nyu.edu')
|
|
'test@nyu.edu'
|
|
>>> # NOTE: If you do not have PyDNS installed this example won't work:
|
|
>>> e.to_python('test@thisdomaindoesnotexistithinkforsure.com')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: The domain of the email address does not exist (the portion after the @: thisdomaindoesnotexistithinkforsure.com)
|
|
>>> e.to_python(u'test@google.com')
|
|
u'test@google.com'
|
|
>>> e = Email(not_empty=False)
|
|
>>> e.to_python('')
|
|
|
|
"""
|
|
|
|
resolve_domain = False
|
|
resolve_timeout = 10 # timeout in seconds when resolving domains
|
|
|
|
usernameRE = re.compile(r"^[^ \t\n\r@<>()]+$", re.I)
|
|
domainRE = re.compile(r'''
|
|
^(?:[a-z0-9][a-z0-9\-]{0,62}\.)+ # (sub)domain - alpha followed by 62max chars (63 total)
|
|
[a-z]{2,}$ # TLD
|
|
''', re.I | re.VERBOSE)
|
|
|
|
messages = dict(
|
|
empty=_('Please enter an email address'),
|
|
noAt=_('An email address must contain a single @'),
|
|
badUsername=_('The username portion of the email address is invalid'
|
|
' (the portion before the @: %(username)s)'),
|
|
socketError=_('An error occured when trying to connect to the server:'
|
|
' %(error)s'),
|
|
badDomain=_('The domain portion of the email address is invalid'
|
|
' (the portion after the @: %(domain)s)'),
|
|
domainDoesNotExist=_('The domain of the email address does not exist'
|
|
' (the portion after the @: %(domain)s)'))
|
|
|
|
def __init__(self, *args, **kw):
|
|
FancyValidator.__init__(self, *args, **kw)
|
|
if self.resolve_domain:
|
|
if not have_dns:
|
|
warnings.warn(
|
|
"pyDNS <http://pydns.sf.net> is not installed on"
|
|
" your system (or the DNS package cannot be found)."
|
|
" I cannot resolve domain names in addresses")
|
|
raise ImportError("no module named DNS")
|
|
|
|
def validate_python(self, value, state):
|
|
if not value:
|
|
raise Invalid(self.message('empty', state), value, state)
|
|
value = value.strip()
|
|
splitted = value.split('@', 1)
|
|
try:
|
|
username, domain=splitted
|
|
except ValueError:
|
|
raise Invalid(self.message('noAt', state), value, state)
|
|
if not self.usernameRE.search(username):
|
|
raise Invalid(
|
|
self.message('badUsername', state, username=username),
|
|
value, state)
|
|
if not self.domainRE.search(domain):
|
|
raise Invalid(
|
|
self.message('badDomain', state, domain=domain),
|
|
value, state)
|
|
if self.resolve_domain:
|
|
assert have_dns, "pyDNS should be available"
|
|
global socket
|
|
if socket is None:
|
|
import socket
|
|
try:
|
|
answers = DNS.DnsRequest(domain, qtype='a',
|
|
timeout=self.resolve_timeout).req().answers
|
|
if answers:
|
|
answers = DNS.DnsRequest(domain, qtype='mx',
|
|
timeout=self.resolve_timeout).req().answers
|
|
except (socket.error, DNS.DNSError), e:
|
|
raise Invalid(
|
|
self.message('socketError', state, error=e),
|
|
value, state)
|
|
if not answers:
|
|
raise Invalid(
|
|
self.message('domainDoesNotExist', state, domain=domain),
|
|
value, state)
|
|
|
|
def _to_python(self, value, state):
|
|
return value.strip()
|
|
|
|
|
|
class URL(FancyValidator):
|
|
"""
|
|
Validate a URL, either http://... or https://. If check_exists
|
|
is true, then we'll actually make a request for the page.
|
|
|
|
If add_http is true, then if no scheme is present we'll add
|
|
http://
|
|
|
|
::
|
|
|
|
>>> u = URL(add_http=True)
|
|
>>> u.to_python('foo.com')
|
|
'http://foo.com'
|
|
>>> u.to_python('http://hahaha.ha/bar.html')
|
|
'http://hahaha.ha/bar.html'
|
|
>>> u.to_python('http://xn--m7r7ml7t24h.com')
|
|
'http://xn--m7r7ml7t24h.com'
|
|
>>> u.to_python('http://foo.com/test?bar=baz&fleem=morx')
|
|
'http://foo.com/test?bar=baz&fleem=morx'
|
|
>>> u.to_python('http://foo.com/login?came_from=http%3A%2F%2Ffoo.com%2Ftest')
|
|
'http://foo.com/login?came_from=http%3A%2F%2Ffoo.com%2Ftest'
|
|
>>> u.to_python('http://foo.com:8000/test.html')
|
|
'http://foo.com:8000/test.html'
|
|
>>> u.to_python('http://foo.com/something\\nelse')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: That is not a valid URL
|
|
>>> u.to_python('https://test.com')
|
|
'https://test.com'
|
|
>>> u.to_python('http://test')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: You must provide a full domain name (like test.com)
|
|
>>> u.to_python('http://test..com')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: That is not a valid URL
|
|
>>> u = URL(add_http=False, check_exists=True)
|
|
>>> u.to_python('http://google.com')
|
|
'http://google.com'
|
|
>>> u.to_python('google.com')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: You must start your URL with http://, https://, etc
|
|
>>> u.to_python('http://formencode.org/doesnotexist.html')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: The server responded that the page could not be found
|
|
>>> u.to_python('http://this.domain.does.not.exist.example.org/test.html')
|
|
... # doctest: +ELLIPSIS
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: An error occured when trying to connect to the server: ...
|
|
|
|
If you want to allow addresses without a TLD (e.g., ``localhost``) you can do::
|
|
|
|
>>> URL(require_tld=False).to_python('http://localhost')
|
|
'http://localhost'
|
|
|
|
"""
|
|
|
|
check_exists = False
|
|
add_http = True
|
|
require_tld = True
|
|
|
|
url_re = re.compile(r'''
|
|
^(http|https)://
|
|
(?:[%:\w]*@)? # authenticator
|
|
(?P<domain>[a-z0-9][a-z0-9\-]{,62}\.)* # (sub)domain - alpha followed by 62max chars (63 total)
|
|
(?P<tld>[a-z]{2,}) # TLD
|
|
(?::[0-9]+)? # port
|
|
|
|
# files/delims/etc
|
|
(?P<path>/[a-z0-9\-\._~:/\?#\[\]@!%\$&\'\(\)\*\+,;=]*)?
|
|
$
|
|
''', re.I | re.VERBOSE)
|
|
|
|
scheme_re = re.compile(r'^[a-zA-Z]+:')
|
|
|
|
messages = dict(
|
|
noScheme=_('You must start your URL with http://, https://, etc'),
|
|
badURL=_('That is not a valid URL'),
|
|
httpError=_('An error occurred when trying to access the URL:'
|
|
' %(error)s'),
|
|
socketError=_('An error occured when trying to connect to the server:'
|
|
' %(error)s'),
|
|
notFound=_('The server responded that the page could not be found'),
|
|
status=_('The server responded with a bad status code (%(status)s)'),
|
|
noTLD=_('You must provide a full domain name (like %(domain)s.com)'))
|
|
|
|
def _to_python(self, value, state):
|
|
value = value.strip()
|
|
if self.add_http:
|
|
if not self.scheme_re.search(value):
|
|
value = 'http://' + value
|
|
match = self.scheme_re.search(value)
|
|
if not match:
|
|
raise Invalid(self.message('noScheme', state), value, state)
|
|
value = match.group(0).lower() + value[len(match.group(0)):]
|
|
match = self.url_re.search(value)
|
|
if not match:
|
|
raise Invalid(self.message('badURL', state), value, state)
|
|
if self.require_tld and not match.group('domain'):
|
|
raise Invalid(
|
|
self.message('noTLD', state, domain=match.group('tld')),
|
|
value, state)
|
|
if self.check_exists and (
|
|
value.startswith('http://') or value.startswith('https://')):
|
|
self._check_url_exists(value, state)
|
|
return value
|
|
|
|
def _check_url_exists(self, url, state):
|
|
global httplib, urlparse, socket
|
|
if httplib is None:
|
|
import httplib
|
|
if urlparse is None:
|
|
import urlparse
|
|
if socket is None:
|
|
import socket
|
|
scheme, netloc, path, params, query, fragment = urlparse.urlparse(
|
|
url, 'http')
|
|
if scheme == 'http':
|
|
ConnClass = httplib.HTTPConnection
|
|
else:
|
|
ConnClass = httplib.HTTPSConnection
|
|
try:
|
|
conn = ConnClass(netloc)
|
|
if params:
|
|
path += ';' + params
|
|
if query:
|
|
path += '?' + query
|
|
conn.request('HEAD', path)
|
|
res = conn.getresponse()
|
|
except httplib.HTTPException, e:
|
|
raise Invalid(
|
|
self.message('httpError', state, error=e), state, url)
|
|
except socket.error, e:
|
|
raise Invalid(
|
|
self.message('socketError', state, error=e), state, url)
|
|
else:
|
|
if res.status == 404:
|
|
raise Invalid(
|
|
self.message('notFound', state), state, url)
|
|
if not 200 <= res.status < 500:
|
|
raise Invalid(
|
|
self.message('status', state, status=res.status),
|
|
state, url)
|
|
|
|
|
|
class XRI(FancyValidator):
|
|
r"""
|
|
Validator for XRIs.
|
|
|
|
It supports both i-names and i-numbers, of the first version of the XRI
|
|
standard.
|
|
|
|
::
|
|
|
|
>>> inames = XRI(xri_type="i-name")
|
|
>>> inames.to_python(" =John.Smith ")
|
|
'=John.Smith'
|
|
>>> inames.to_python("@Free.Software.Foundation")
|
|
'@Free.Software.Foundation'
|
|
>>> inames.to_python("Python.Software.Foundation")
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: The type of i-name is not defined; it may be either individual or organizational
|
|
>>> inames.to_python("http://example.org")
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: The type of i-name is not defined; it may be either individual or organizational
|
|
>>> inames.to_python("=!2C43.1A9F.B6F6.E8E6")
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: "!2C43.1A9F.B6F6.E8E6" is an invalid i-name
|
|
>>> iname_with_schema = XRI(True, xri_type="i-name")
|
|
>>> iname_with_schema.to_python("=Richard.Stallman")
|
|
'xri://=Richard.Stallman'
|
|
>>> inames.to_python("=John Smith")
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: "John Smith" is an invalid i-name
|
|
>>> inumbers = XRI(xri_type="i-number")
|
|
>>> inumbers.to_python("!!1000!de21.4536.2cb2.8074")
|
|
'!!1000!de21.4536.2cb2.8074'
|
|
>>> inumbers.to_python("@!1000.9554.fabd.129c!2847.df3c")
|
|
'@!1000.9554.fabd.129c!2847.df3c'
|
|
|
|
"""
|
|
|
|
iname_valid_pattern = re.compile(r"""
|
|
^
|
|
[\w]+ # A global alphanumeric i-name
|
|
(\.[\w]+)* # An i-name with dots
|
|
(\*[\w]+(\.[\w]+)*)* # A community i-name
|
|
$
|
|
""", re.VERBOSE|re.UNICODE)
|
|
|
|
|
|
iname_invalid_start = re.compile(r"^[\d\.-]", re.UNICODE)
|
|
"""@cvar: These characters must not be at the beggining of the i-name"""
|
|
|
|
inumber_pattern = re.compile(r"""
|
|
^
|
|
(
|
|
[=@]! # It's a personal or organization i-number
|
|
|
|
|
!! # It's a network i-number
|
|
)
|
|
[\dA-F]{1,4}(\.[\dA-F]{1,4}){0,3} # A global i-number
|
|
(![\dA-F]{1,4}(\.[\dA-F]{1,4}){0,3})* # Zero or more sub i-numbers
|
|
$
|
|
""", re.VERBOSE|re.IGNORECASE)
|
|
|
|
messages = dict(
|
|
noType=_('The type of i-name is not defined;'
|
|
' it may be either individual or organizational'),
|
|
repeatedChar=_('Dots and dashes may not be repeated consecutively'),
|
|
badIname=_('"%(iname)s" is an invalid i-name'),
|
|
badInameStart=_('i-names may not start with numbers'
|
|
' nor punctuation marks'),
|
|
badInumber=_('"%(inumber)s" is an invalid i-number'),
|
|
badType=_('The XRI must be a string (not a %(type)s: %(value)r)'),
|
|
badXri=_('"%(xri_type)s" is not a valid type of XRI'))
|
|
|
|
def __init__(self, add_xri=False, xri_type="i-name", **kwargs):
|
|
"""Create an XRI validator.
|
|
|
|
@param add_xri: Should the schema be added if not present?
|
|
Officially it's optional.
|
|
@type add_xri: C{bool}
|
|
@param xri_type: What type of XRI should be validated?
|
|
Possible values: C{i-name} or C{i-number}.
|
|
@type xri_type: C{str}
|
|
|
|
"""
|
|
self.add_xri = add_xri
|
|
assert xri_type in ('i-name', 'i-number'), (
|
|
'xri_type must be "i-name" or "i-number"')
|
|
self.xri_type = xri_type
|
|
super(XRI, self).__init__(**kwargs)
|
|
|
|
def _to_python(self, value, state):
|
|
"""Prepend the 'xri://' schema if needed and remove trailing spaces"""
|
|
value = value.strip()
|
|
if self.add_xri and not value.startswith('xri://'):
|
|
value = 'xri://' + value
|
|
return value
|
|
|
|
def validate_python(self, value, state=None):
|
|
"""Validate an XRI
|
|
|
|
@raise Invalid: If at least one of the following conditions in met:
|
|
- C{value} is not a string.
|
|
- The XRI is not a personal, organizational or network one.
|
|
- The relevant validator (i-name or i-number) considers the XRI
|
|
is not valid.
|
|
|
|
"""
|
|
if not isinstance(value, basestring):
|
|
raise Invalid(
|
|
self.message('badType', state,
|
|
type=str(type(value)), value=value), value, state)
|
|
|
|
# Let's remove the schema, if any
|
|
if value.startswith('xri://'):
|
|
value = value[6:]
|
|
|
|
if not value[0] in ('@', '=') and not (
|
|
self.xri_type == 'i-number' and value[0] == '!'):
|
|
raise Invalid(self.message('noType', state), value, state)
|
|
|
|
if self.xri_type == 'i-name':
|
|
self._validate_iname(value, state)
|
|
else:
|
|
self._validate_inumber(value, state)
|
|
|
|
def _validate_iname(self, iname, state):
|
|
"""Validate an i-name"""
|
|
# The type is not required here:
|
|
iname = iname[1:]
|
|
if '..' in iname or '--' in iname:
|
|
raise Invalid(self.message('repeatedChar', state), iname, state)
|
|
if self.iname_invalid_start.match(iname):
|
|
raise Invalid(self.message('badInameStart', state), iname, state)
|
|
if not self.iname_valid_pattern.match(iname) or '_' in iname:
|
|
raise Invalid(
|
|
self.message('badIname', state, iname=iname), iname, state)
|
|
|
|
def _validate_inumber(self, inumber, state):
|
|
"""Validate an i-number"""
|
|
if not self.__class__.inumber_pattern.match(inumber):
|
|
raise Invalid(
|
|
self.message('badInumber', state,
|
|
inumber=inumber, value=inumber), inumber, state)
|
|
|
|
|
|
class OpenId(FancyValidator):
|
|
r"""
|
|
OpenId validator.
|
|
|
|
::
|
|
>>> v = OpenId(add_schema=True)
|
|
>>> v.to_python(' example.net ')
|
|
'http://example.net'
|
|
>>> v.to_python('@TurboGears')
|
|
'xri://@TurboGears'
|
|
>>> w = OpenId(add_schema=False)
|
|
>>> w.to_python(' example.net ')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: "example.net" is not a valid OpenId (it is neither an URL nor an XRI)
|
|
>>> w.to_python('!!1000')
|
|
'!!1000'
|
|
>>> w.to_python('look@me.com')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: "look@me.com" is not a valid OpenId (it is neither an URL nor an XRI)
|
|
|
|
"""
|
|
|
|
messages = dict(
|
|
badId=_('"%(id)s" is not a valid OpenId'
|
|
' (it is neither an URL nor an XRI)'))
|
|
|
|
def __init__(self, add_schema=False, **kwargs):
|
|
"""Create an OpenId validator.
|
|
|
|
@param add_schema: Should the schema be added if not present?
|
|
@type add_schema: C{bool}
|
|
|
|
"""
|
|
self.url_validator = URL(add_http=add_schema)
|
|
self.iname_validator = XRI(add_schema, xri_type="i-name")
|
|
self.inumber_validator = XRI(add_schema, xri_type="i-number")
|
|
|
|
def _to_python(self, value, state):
|
|
value = value.strip()
|
|
try:
|
|
return self.url_validator.to_python(value, state)
|
|
except Invalid:
|
|
try:
|
|
return self.iname_validator.to_python(value, state)
|
|
except Invalid:
|
|
try:
|
|
return self.inumber_validator.to_python(value, state)
|
|
except Invalid:
|
|
pass
|
|
# It's not an OpenId!
|
|
raise Invalid(self.message('badId', state, id=value), value, state)
|
|
|
|
def validate_python(self, value, state):
|
|
self._to_python(value, state)
|
|
|
|
|
|
def StateProvince(*kw, **kwargs):
|
|
warnings.warn("please use formencode.national.USStateProvince",
|
|
DeprecationWarning, stacklevel=2)
|
|
from formencode.national import USStateProvince
|
|
return USStateProvince(*kw, **kwargs)
|
|
|
|
|
|
def PhoneNumber(*kw, **kwargs):
|
|
warnings.warn("please use formencode.national.USPhoneNumber",
|
|
DeprecationWarning, stacklevel=2)
|
|
from formencode.national import USPhoneNumber
|
|
return USPhoneNumber(*kw, **kwargs)
|
|
|
|
|
|
def IPhoneNumberValidator(*kw, **kwargs):
|
|
warnings.warn("please use formencode.national.InternationalPhoneNumber",
|
|
DeprecationWarning, stacklevel=2)
|
|
from formencode.national import InternationalPhoneNumber
|
|
return InternationalPhoneNumber(*kw, **kwargs)
|
|
|
|
|
|
class FieldStorageUploadConverter(FancyValidator):
|
|
"""
|
|
Handles cgi.FieldStorage instances that are file uploads.
|
|
|
|
This doesn't do any conversion, but it can detect empty upload
|
|
fields (which appear like normal fields, but have no filename when
|
|
no upload was given).
|
|
"""
|
|
def _to_python(self, value, state=None):
|
|
if isinstance(value, cgi.FieldStorage):
|
|
if getattr(value, 'filename', None):
|
|
return value
|
|
raise Invalid('invalid', value, state)
|
|
else:
|
|
return value
|
|
|
|
def is_empty(self, value):
|
|
if isinstance(value, cgi.FieldStorage):
|
|
return not bool(getattr(value, 'filename', None))
|
|
return FancyValidator.is_empty(self, value)
|
|
|
|
|
|
class FileUploadKeeper(FancyValidator):
|
|
"""
|
|
Takes two inputs (a dictionary with keys ``static`` and
|
|
``upload``) and converts them into one value on the Python side (a
|
|
dictionary with ``filename`` and ``content`` keys). The upload
|
|
takes priority over the static value. The filename may be None if
|
|
it can't be discovered.
|
|
|
|
Handles uploads of both text and ``cgi.FieldStorage`` upload
|
|
values.
|
|
|
|
This is basically for use when you have an upload field, and you
|
|
want to keep the upload around even if the rest of the form
|
|
submission fails. When converting *back* to the form submission,
|
|
there may be extra values ``'original_filename'`` and
|
|
``'original_content'``, which may want to use in your form to show
|
|
the user you still have their content around.
|
|
|
|
To use this, make sure you are using variabledecode, then use
|
|
something like::
|
|
|
|
<input type="file" name="myfield.upload">
|
|
<input type="hidden" name="myfield.static">
|
|
|
|
Then in your scheme::
|
|
|
|
class MyScheme(Scheme):
|
|
myfield = FileUploadKeeper()
|
|
|
|
Note that big file uploads mean big hidden fields, and lots of
|
|
bytes passed back and forth in the case of an error.
|
|
"""
|
|
|
|
upload_key = 'upload'
|
|
static_key = 'static'
|
|
|
|
def _to_python(self, value, state):
|
|
upload = value.get(self.upload_key)
|
|
static = value.get(self.static_key, '').strip()
|
|
filename = content = None
|
|
if isinstance(upload, cgi.FieldStorage):
|
|
filename = upload.filename
|
|
content = upload.value
|
|
elif isinstance(upload, basestring) and upload:
|
|
filename = None
|
|
# @@: Should this encode upload if it is unicode?
|
|
content = upload
|
|
if not content and static:
|
|
filename, content = static.split(None, 1)
|
|
if filename == '-':
|
|
filename = ''
|
|
else:
|
|
filename = filename.decode('base64')
|
|
content = content.decode('base64')
|
|
return {'filename': filename, 'content': content}
|
|
|
|
def _from_python(self, value, state):
|
|
filename = value.get('filename', '')
|
|
content = value.get('content', '')
|
|
if filename or content:
|
|
result = self.pack_content(filename, content)
|
|
return {self.upload_key: '',
|
|
self.static_key: result,
|
|
'original_filename': filename,
|
|
'original_content': content}
|
|
else:
|
|
return {self.upload_key: '',
|
|
self.static_key: ''}
|
|
|
|
def pack_content(self, filename, content):
|
|
enc_filename = self.base64encode(filename) or '-'
|
|
enc_content = (content or '').encode('base64')
|
|
result = '%s %s' % (enc_filename, enc_content)
|
|
return result
|
|
|
|
|
|
class DateConverter(FancyValidator):
|
|
"""
|
|
Validates and converts a string date, like mm/yy, dd/mm/yy,
|
|
dd-mm-yy, etc. Using ``month_style`` you can support
|
|
``'mm/dd/yyyy'`` or ``'dd/mm/yyyy'``. Only these two general
|
|
styles are supported.
|
|
|
|
Accepts English month names, also abbreviated. Returns value as a
|
|
datetime object (you can get mx.DateTime objects if you use
|
|
``datetime_module='mxDateTime'``). Two year dates are assumed to
|
|
be within 1950-2020, with dates from 21-49 being ambiguous and
|
|
signaling an error.
|
|
|
|
Use accept_day=False if you just want a month/year (like for a
|
|
credit card expiration date).
|
|
|
|
::
|
|
|
|
>>> d = DateConverter()
|
|
>>> d.to_python('12/3/09')
|
|
datetime.date(2009, 12, 3)
|
|
>>> d.to_python('12/3/2009')
|
|
datetime.date(2009, 12, 3)
|
|
>>> d.to_python('2/30/04')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: That month only has 29 days
|
|
>>> d.to_python('13/2/05')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Please enter a month from 1 to 12
|
|
>>> d.to_python('1/1/200')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Please enter a four-digit year after 1899
|
|
|
|
If you change ``month_style`` you can get European-style dates::
|
|
|
|
>>> d = DateConverter(month_style='dd/mm/yyyy')
|
|
>>> date = d.to_python('12/3/09')
|
|
>>> date
|
|
datetime.date(2009, 3, 12)
|
|
>>> d.from_python(date)
|
|
'12/03/2009'
|
|
"""
|
|
## @@: accepts only US-style dates
|
|
|
|
accept_day = True
|
|
# also allowed: 'dd/mm/yyyy'
|
|
month_style = 'mm/dd/yyyy'
|
|
# Use 'datetime' to force the Python 2.3+ datetime module, or
|
|
# 'mxDateTime' to force the mxDateTime module (None means use
|
|
# datetime, or if not present mxDateTime)
|
|
datetime_module = None
|
|
|
|
_day_date_re = re.compile(r'^\s*(\d\d?)[\-\./\\](\d\d?|jan|january|feb|febuary|mar|march|apr|april|may|jun|june|jul|july|aug|august|sep|sept|september|oct|october|nov|november|dec|december)[\-\./\\](\d\d\d?\d?)\s*$', re.I)
|
|
_month_date_re = re.compile(r'^\s*(\d\d?|jan|january|feb|febuary|mar|march|apr|april|may|jun|june|jul|july|aug|august|sep|sept|september|oct|october|nov|november|dec|december)[\-\./\\](\d\d\d?\d?)\s*$', re.I)
|
|
|
|
_month_names = {
|
|
'jan': 1, 'january': 1,
|
|
'feb': 2, 'febuary': 2,
|
|
'mar': 3, 'march': 3,
|
|
'apr': 4, 'april': 4,
|
|
'may': 5,
|
|
'jun': 6, 'june': 6,
|
|
'jul': 7, 'july': 7,
|
|
'aug': 8, 'august': 8,
|
|
'sep': 9, 'sept': 9, 'september': 9,
|
|
'oct': 10, 'october': 10,
|
|
'nov': 11, 'november': 11,
|
|
'dec': 12, 'december': 12,
|
|
}
|
|
|
|
## @@: Feb. should be leap-year aware (but mxDateTime does catch that)
|
|
_monthDays = {
|
|
1: 31, 2: 29, 3: 31, 4: 30, 5: 31, 6: 30, 7: 31, 8: 31,
|
|
9: 30, 10: 31, 11: 30, 12: 31}
|
|
|
|
messages = dict(
|
|
badFormat=_('Please enter the date in the form %(format)s'),
|
|
monthRange=_('Please enter a month from 1 to 12'),
|
|
invalidDay=_('Please enter a valid day'),
|
|
dayRange=_('That month only has %(days)i days'),
|
|
invalidDate=_('That is not a valid day (%(exception)s)'),
|
|
unknownMonthName=_('Unknown month name: %(month)s'),
|
|
invalidYear=_('Please enter a number for the year'),
|
|
fourDigitYear=_('Please enter a four-digit year after 1899'),
|
|
wrongFormat=_('Please enter the date in the form %(format)s'))
|
|
|
|
def __init__(self, *args, **kw):
|
|
super(DateConverter, self).__init__(*args, **kw)
|
|
if not self.month_style in ('dd/mm/yyyy', 'mm/dd/yyyy'):
|
|
raise TypeError('Bad month_style: %r' % self.month_style)
|
|
|
|
def _to_python(self, value, state):
|
|
if self.accept_day:
|
|
return self.convert_day(value, state)
|
|
else:
|
|
return self.convert_month(value, state)
|
|
|
|
def convert_day(self, value, state):
|
|
self.assert_string(value, state)
|
|
match = self._day_date_re.search(value)
|
|
if not match:
|
|
raise Invalid(
|
|
self.message('badFormat', state,
|
|
format=self.month_style), value, state)
|
|
day = int(match.group(1))
|
|
try:
|
|
month = int(match.group(2))
|
|
except (TypeError, ValueError):
|
|
month = self.make_month(match.group(2), state)
|
|
else:
|
|
if self.month_style == 'mm/dd/yyyy':
|
|
month, day = day, month
|
|
year = self.make_year(match.group(3), state)
|
|
if not 1 <= month <= 12:
|
|
raise Invalid(self.message('monthRange', state), value, state)
|
|
if day < 1:
|
|
raise Invalid(self.message('invalidDay', state), value, state)
|
|
if self._monthDays[month] < day:
|
|
raise Invalid(
|
|
self.message('dayRange', state,
|
|
days=self._monthDays[month]), value, state)
|
|
dt_mod = import_datetime(self.datetime_module)
|
|
try:
|
|
return datetime_makedate(dt_mod, year, month, day)
|
|
except ValueError, v:
|
|
raise Invalid(
|
|
self.message('invalidDate', state,
|
|
exception=str(v)), value, state)
|
|
|
|
def make_month(self, value, state):
|
|
try:
|
|
return int(value)
|
|
except ValueError:
|
|
value = value.lower().strip()
|
|
if value in self._month_names:
|
|
return self._month_names[value]
|
|
else:
|
|
raise Invalid(
|
|
self.message('unknownMonthName', state,
|
|
month=value), value, state)
|
|
|
|
def make_year(self, year, state):
|
|
try:
|
|
year = int(year)
|
|
except ValueError:
|
|
raise Invalid(self.message('invalidYear', state), year, state)
|
|
if year <= 20:
|
|
year += 2000
|
|
elif 50 <= year < 100:
|
|
year += 1900
|
|
if 20 < year < 50 or 99 < year < 1900:
|
|
raise Invalid(self.message('fourDigitYear', state), year, state)
|
|
return year
|
|
|
|
def convert_month(self, value, state):
|
|
match = self._month_date_re.search(value)
|
|
if not match:
|
|
raise Invalid(
|
|
self.message('wrongFormat', state,
|
|
format='mm/yyyy'), value, state)
|
|
month = self.make_month(match.group(1), state)
|
|
year = self.make_year(match.group(2), state)
|
|
if not 1 <= month <= 12:
|
|
raise Invalid(self.message('monthRange', state), value, state)
|
|
dt_mod = import_datetime(self.datetime_module)
|
|
return datetime_makedate(dt_mod, year, month, 1)
|
|
|
|
def _from_python(self, value, state):
|
|
if self.if_empty is not NoDefault and not value:
|
|
return ''
|
|
if self.accept_day:
|
|
return self.unconvert_day(value, state)
|
|
else:
|
|
return self.unconvert_month(value, state)
|
|
|
|
def unconvert_day(self, value, state):
|
|
# @@ ib: double-check, improve
|
|
if self.month_style == 'mm/dd/yyyy':
|
|
return value.strftime('%m/%d/%Y')
|
|
else:
|
|
return value.strftime('%d/%m/%Y')
|
|
|
|
def unconvert_month(self, value, state):
|
|
# @@ ib: double-check, improve
|
|
return value.strftime('%m/%Y')
|
|
|
|
|
|
class TimeConverter(FancyValidator):
|
|
"""
|
|
Converts times in the format HH:MM:SSampm to (h, m, s).
|
|
Seconds are optional.
|
|
|
|
For ampm, set use_ampm = True. For seconds, use_seconds = True.
|
|
Use 'optional' for either of these to make them optional.
|
|
|
|
Examples::
|
|
|
|
>>> tim = TimeConverter()
|
|
>>> tim.to_python('8:30')
|
|
(8, 30)
|
|
>>> tim.to_python('20:30')
|
|
(20, 30)
|
|
>>> tim.to_python('30:00')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: You must enter an hour in the range 0-23
|
|
>>> tim.to_python('13:00pm')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: You must enter an hour in the range 1-12
|
|
>>> tim.to_python('12:-1')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: You must enter a minute in the range 0-59
|
|
>>> tim.to_python('12:02pm')
|
|
(12, 2)
|
|
>>> tim.to_python('12:02am')
|
|
(0, 2)
|
|
>>> tim.to_python('1:00PM')
|
|
(13, 0)
|
|
>>> tim.from_python((13, 0))
|
|
'13:00:00'
|
|
>>> tim2 = tim(use_ampm=True, use_seconds=False)
|
|
>>> tim2.from_python((13, 0))
|
|
'1:00pm'
|
|
>>> tim2.from_python((0, 0))
|
|
'12:00am'
|
|
>>> tim2.from_python((12, 0))
|
|
'12:00pm'
|
|
|
|
Examples with ``datetime.time``::
|
|
|
|
>>> v = TimeConverter(use_datetime=True)
|
|
>>> a = v.to_python('18:00')
|
|
>>> a
|
|
datetime.time(18, 0)
|
|
>>> b = v.to_python('30:00')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: You must enter an hour in the range 0-23
|
|
>>> v2 = TimeConverter(prefer_ampm=True, use_datetime=True)
|
|
>>> v2.from_python(a)
|
|
'6:00:00pm'
|
|
>>> v3 = TimeConverter(prefer_ampm=True,
|
|
... use_seconds=False, use_datetime=True)
|
|
>>> a = v3.to_python('18:00')
|
|
>>> a
|
|
datetime.time(18, 0)
|
|
>>> v3.from_python(a)
|
|
'6:00pm'
|
|
>>> a = v3.to_python('18:00:00')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: You may not enter seconds
|
|
"""
|
|
|
|
use_ampm = 'optional'
|
|
prefer_ampm = False
|
|
use_seconds = 'optional'
|
|
use_datetime = False
|
|
# This can be set to make it prefer mxDateTime:
|
|
datetime_module = None
|
|
|
|
messages = dict(
|
|
noAMPM=_('You must indicate AM or PM'),
|
|
tooManyColon=_('There are too many :\'s'),
|
|
noSeconds=_('You may not enter seconds'),
|
|
secondsRequired=_('You must enter seconds'),
|
|
minutesRequired=_('You must enter minutes (after a :)'),
|
|
badNumber=_('The %(part)s value you gave is not a number: %(number)r'),
|
|
badHour=_('You must enter an hour in the range %(range)s'),
|
|
badMinute=_('You must enter a minute in the range 0-59'),
|
|
badSecond=_('You must enter a second in the range 0-59'))
|
|
|
|
def _to_python(self, value, state):
|
|
result = self._to_python_tuple(value, state)
|
|
if self.use_datetime:
|
|
dt_mod = import_datetime(self.datetime_module)
|
|
time_class = datetime_time(dt_mod)
|
|
return time_class(*result)
|
|
else:
|
|
return result
|
|
|
|
def _to_python_tuple(self, value, state):
|
|
time = value.strip()
|
|
explicit_ampm = False
|
|
if self.use_ampm:
|
|
last_two = time[-2:].lower()
|
|
if last_two not in ('am', 'pm'):
|
|
if self.use_ampm != 'optional':
|
|
raise Invalid(self.message('noAMPM', state), value, state)
|
|
else:
|
|
offset = 0
|
|
else:
|
|
explicit_ampm = True
|
|
if last_two == 'pm':
|
|
offset = 12
|
|
else:
|
|
offset = 0
|
|
time = time[:-2]
|
|
else:
|
|
offset = 0
|
|
parts = time.split(':')
|
|
if len(parts) > 3:
|
|
raise Invalid(self.message('tooManyColon', state), value, state)
|
|
if len(parts) == 3 and not self.use_seconds:
|
|
raise Invalid(self.message('noSeconds', state), value, state)
|
|
if (len(parts) == 2
|
|
and self.use_seconds and self.use_seconds != 'optional'):
|
|
raise Invalid(self.message('secondsRequired', state), value, state)
|
|
if len(parts) == 1:
|
|
raise Invalid(self.message('minutesRequired', state), value, state)
|
|
try:
|
|
hour = int(parts[0])
|
|
except ValueError:
|
|
raise Invalid(
|
|
self.message('badNumber', state,
|
|
number=parts[0], part='hour'), value, state)
|
|
if explicit_ampm:
|
|
if not 1 <= hour <= 12:
|
|
raise Invalid(
|
|
self.message('badHour', state,
|
|
number=hour, range='1-12'), value, state)
|
|
if hour == 12 and offset == 12:
|
|
# 12pm == 12
|
|
pass
|
|
elif hour == 12 and offset == 0:
|
|
# 12am == 0
|
|
hour = 0
|
|
else:
|
|
hour += offset
|
|
else:
|
|
if not 0 <= hour < 24:
|
|
raise Invalid(
|
|
self.message('badHour', state,
|
|
number=hour, range='0-23'), value, state)
|
|
try:
|
|
minute = int(parts[1])
|
|
except ValueError:
|
|
raise Invalid(
|
|
self.message('badNumber', state,
|
|
number=parts[1], part='minute'), value, state)
|
|
if not 0 <= minute < 60:
|
|
raise Invalid(
|
|
self.message('badMinute', state, number=minute),
|
|
value, state)
|
|
if len(parts) == 3:
|
|
try:
|
|
second = int(parts[2])
|
|
except ValueError:
|
|
raise Invalid(
|
|
self.message('badNumber', state,
|
|
number=parts[2], part='second'), value, state)
|
|
if not 0 <= second < 60:
|
|
raise Invalid(
|
|
self.message('badSecond', state, number=second),
|
|
value, state)
|
|
else:
|
|
second = None
|
|
if second is None:
|
|
return (hour, minute)
|
|
else:
|
|
return (hour, minute, second)
|
|
|
|
def _from_python(self, value, state):
|
|
if isinstance(value, basestring):
|
|
return value
|
|
if hasattr(value, 'hour'):
|
|
hour, minute = value.hour, value.minute
|
|
second = value.second
|
|
elif len(value) == 3:
|
|
hour, minute, second = value
|
|
elif len(value) == 2:
|
|
hour, minute = value
|
|
second = 0
|
|
ampm = ''
|
|
if (self.use_ampm == 'optional' and self.prefer_ampm) or (
|
|
self.use_ampm and self.use_ampm != 'optional'):
|
|
ampm = 'am'
|
|
if hour > 12:
|
|
hour -= 12
|
|
ampm = 'pm'
|
|
elif hour == 12:
|
|
ampm = 'pm'
|
|
elif hour == 0:
|
|
hour = 12
|
|
if self.use_seconds:
|
|
return '%i:%02i:%02i%s' % (hour, minute, second, ampm)
|
|
else:
|
|
return '%i:%02i%s' % (hour, minute, ampm)
|
|
|
|
|
|
def PostalCode(*kw, **kwargs):
|
|
warnings.warn("please use formencode.national.USPostalCode",
|
|
DeprecationWarning, stacklevel=2)
|
|
from formencode.national import USPostalCode
|
|
return USPostalCode(*kw, **kwargs)
|
|
|
|
|
|
class StripField(FancyValidator):
|
|
"""
|
|
Take a field from a dictionary, removing the key from the dictionary.
|
|
|
|
``name`` is the key. The field value and a new copy of the dictionary
|
|
with that field removed are returned.
|
|
|
|
>>> StripField('test').to_python({'a': 1, 'test': 2})
|
|
(2, {'a': 1})
|
|
>>> StripField('test').to_python({})
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: The name 'test' is missing
|
|
|
|
"""
|
|
|
|
__unpackargs__ = ('name',)
|
|
|
|
messages = dict(
|
|
missing=_('The name %(name)s is missing'))
|
|
|
|
def _to_python(self, valueDict, state):
|
|
v = valueDict.copy()
|
|
try:
|
|
field = v.pop(self.name)
|
|
except KeyError:
|
|
raise Invalid(
|
|
self.message('missing', state, name=repr(self.name)),
|
|
valueDict, state)
|
|
return field, v
|
|
|
|
def is_empty(self, value):
|
|
# empty dictionaries don't really apply here
|
|
return False
|
|
|
|
|
|
class StringBool(FancyValidator): # originally from TurboGears
|
|
"""
|
|
Converts a string to a boolean.
|
|
|
|
Values like 'true' and 'false' are considered True and False,
|
|
respectively; anything in ``true_values`` is true, anything in
|
|
``false_values`` is false, case-insensitive). The first item of
|
|
those lists is considered the preferred form.
|
|
|
|
::
|
|
|
|
>>> s = StringBool()
|
|
>>> s.to_python('yes'), s.to_python('no')
|
|
(True, False)
|
|
>>> s.to_python(1), s.to_python('N')
|
|
(True, False)
|
|
>>> s.to_python('ye')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Value should be 'true' or 'false'
|
|
"""
|
|
|
|
true_values = ['true', 't', 'yes', 'y', 'on', '1']
|
|
false_values = ['false', 'f', 'no', 'n', 'off', '0']
|
|
|
|
messages = dict(
|
|
string=_('Value should be %(true)r or %(false)r'))
|
|
|
|
def _to_python(self, value, state):
|
|
if isinstance(value, basestring):
|
|
value = value.strip().lower()
|
|
if value in self.true_values:
|
|
return True
|
|
if not value or value in self.false_values:
|
|
return False
|
|
raise Invalid(
|
|
self.message('string', state,
|
|
true=self.true_values[0], false=self.false_values[0]),
|
|
value, state)
|
|
return bool(value)
|
|
|
|
def _from_python(self, value, state):
|
|
if value:
|
|
return self.true_values[0]
|
|
else:
|
|
return self.false_values[0]
|
|
|
|
# Should deprecate:
|
|
StringBoolean = StringBool
|
|
|
|
|
|
class SignedString(FancyValidator):
|
|
"""
|
|
Encodes a string into a signed string, and base64 encodes both the
|
|
signature string and a random nonce.
|
|
|
|
It is up to you to provide a secret, and to keep the secret handy
|
|
and consistent.
|
|
"""
|
|
|
|
messages = dict(
|
|
malformed=_('Value does not contain a signature'),
|
|
badsig=_('Signature is not correct'))
|
|
|
|
secret = None
|
|
nonce_length = 4
|
|
|
|
def _to_python(self, value, state):
|
|
global sha1
|
|
if not sha1:
|
|
try:
|
|
from hashlib import sha1
|
|
except ImportError: # Python < 2.5
|
|
from sha import sha as sha1
|
|
assert self.secret is not None, (
|
|
"You must give a secret")
|
|
parts = value.split(None, 1)
|
|
if not parts or len(parts) == 1:
|
|
raise Invalid(self.message('malformed', state), value, state)
|
|
sig, rest = parts
|
|
sig = sig.decode('base64')
|
|
rest = rest.decode('base64')
|
|
nonce = rest[:self.nonce_length]
|
|
rest = rest[self.nonce_length:]
|
|
expected = sha1(str(self.secret)+nonce+rest).digest()
|
|
if expected != sig:
|
|
raise Invalid(self.message('badsig', state), value, state)
|
|
return rest
|
|
|
|
def _from_python(self, value, state):
|
|
global sha1
|
|
if not sha1:
|
|
try:
|
|
from hashlib import sha1
|
|
except ImportError:
|
|
from sha import sha as sha1
|
|
nonce = self.make_nonce()
|
|
value = str(value)
|
|
digest = sha1(self.secret+nonce+value).digest()
|
|
return self.encode(digest)+' '+self.encode(nonce+value)
|
|
|
|
def encode(self, value):
|
|
return value.encode('base64').strip().replace('\n', '')
|
|
|
|
def make_nonce(self):
|
|
global random
|
|
if not random:
|
|
import random
|
|
return ''.join([
|
|
chr(random.randrange(256))
|
|
for i in range(self.nonce_length)])
|
|
|
|
|
|
class IPAddress(FancyValidator):
|
|
"""
|
|
Formencode validator to check whether a string is a correct IP address.
|
|
|
|
Examples::
|
|
|
|
>>> ip = IPAddress()
|
|
>>> ip.to_python('127.0.0.1')
|
|
'127.0.0.1'
|
|
>>> ip.to_python('299.0.0.1')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: The octets must be within the range of 0-255 (not '299')
|
|
>>> ip.to_python('192.168.0.1/1')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Please enter a valid IP address (a.b.c.d)
|
|
>>> ip.to_python('asdf')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Please enter a valid IP address (a.b.c.d)
|
|
"""
|
|
|
|
messages = dict(
|
|
badFormat=_('Please enter a valid IP address (a.b.c.d)'),
|
|
illegalOctets=_('The octets must be within the range of 0-255'
|
|
' (not %(octet)r)'))
|
|
|
|
def validate_python(self, value, state):
|
|
try:
|
|
octets = value.split('.')
|
|
# Only 4 octets?
|
|
if len(octets) != 4:
|
|
raise Invalid(
|
|
self.message('badFormat', state, value=value),
|
|
value, state)
|
|
# Correct octets?
|
|
for octet in octets:
|
|
if not 0 <= int(octet) < 256:
|
|
raise Invalid(
|
|
self.message('illegalOctets', state, octet=octet),
|
|
value, state)
|
|
# Splitting faild: wrong syntax
|
|
except ValueError:
|
|
raise Invalid(self.message('badFormat', state), value, state)
|
|
|
|
|
|
class CIDR(IPAddress):
|
|
"""
|
|
Formencode validator to check whether a string is in correct CIDR
|
|
notation (IP address, or IP address plus /mask).
|
|
|
|
Examples::
|
|
|
|
>>> cidr = CIDR()
|
|
>>> cidr.to_python('127.0.0.1')
|
|
'127.0.0.1'
|
|
>>> cidr.to_python('299.0.0.1')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: The octets must be within the range of 0-255 (not '299')
|
|
>>> cidr.to_python('192.168.0.1/1')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: The network size (bits) must be within the range of 8-32 (not '1')
|
|
>>> cidr.to_python('asdf')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: Please enter a valid IP address (a.b.c.d) or IP network (a.b.c.d/e)
|
|
"""
|
|
|
|
messages = dict(IPAddress._messages,
|
|
badFormat=_('Please enter a valid IP address (a.b.c.d)'
|
|
' or IP network (a.b.c.d/e)'),
|
|
illegalBits=_('The network size (bits) must be within the range'
|
|
' of 8-32 (not %(bits)r)'))
|
|
|
|
def validate_python(self, value, state):
|
|
try:
|
|
# Split into octets and bits
|
|
if '/' in value: # a.b.c.d/e
|
|
addr, bits = value.split('/')
|
|
else: # a.b.c.d
|
|
addr, bits = value, 32
|
|
# Use IPAddress validator to validate the IP part
|
|
IPAddress.validate_python(self, addr, state)
|
|
# Bits (netmask) correct?
|
|
if not 8 <= int(bits) <= 32:
|
|
raise Invalid(
|
|
self.message('illegalBits', state, bits=bits),
|
|
value, state)
|
|
# Splitting faild: wrong syntax
|
|
except ValueError:
|
|
raise Invalid(self.message('badFormat', state), value, state)
|
|
|
|
|
|
class MACAddress(FancyValidator):
|
|
"""
|
|
Formencode validator to check whether a string is a correct hardware
|
|
(MAC) address.
|
|
|
|
Examples::
|
|
|
|
>>> mac = MACAddress()
|
|
>>> mac.to_python('aa:bb:cc:dd:ee:ff')
|
|
'aabbccddeeff'
|
|
>>> mac.to_python('aa:bb:cc:dd:ee:ff:e')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: A MAC address must contain 12 digits and A-F; the value you gave has 13 characters
|
|
>>> mac.to_python('aa:bb:cc:dd:ee:fx')
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: MAC addresses may only contain 0-9 and A-F (and optionally :), not 'x'
|
|
>>> MACAddress(add_colons=True).to_python('aabbccddeeff')
|
|
'aa:bb:cc:dd:ee:ff'
|
|
"""
|
|
|
|
strip = True
|
|
valid_characters = '0123456789abcdefABCDEF'
|
|
add_colons = False
|
|
|
|
messages = dict(
|
|
badLength=_('A MAC address must contain 12 digits and A-F;'
|
|
' the value you gave has %(length)s characters'),
|
|
badCharacter=_('MAC addresses may only contain 0-9 and A-F'
|
|
' (and optionally :), not %(char)r'))
|
|
|
|
def _to_python(self, value, state):
|
|
address = value.replace(':', '').lower() # remove colons
|
|
if len(address) != 12:
|
|
raise Invalid(
|
|
self.message('badLength', state,
|
|
length=len(address)), address, state)
|
|
for char in address:
|
|
if char not in self.valid_characters:
|
|
raise Invalid(
|
|
self.message('badCharacter', state,
|
|
char=char), address, state)
|
|
if self.add_colons:
|
|
address = '%s:%s:%s:%s:%s:%s' % (
|
|
address[0:2], address[2:4], address[4:6],
|
|
address[6:8], address[8:10], address[10:12])
|
|
return address
|
|
|
|
_from_python = _to_python
|
|
|
|
|
|
class FormValidator(FancyValidator):
|
|
"""
|
|
A FormValidator is something that can be chained with a Schema.
|
|
|
|
Unlike normal chaining the FormValidator can validate forms that
|
|
aren't entirely valid.
|
|
|
|
The important method is .validate(), of course. It gets passed a
|
|
dictionary of the (processed) values from the form. If you have
|
|
.validate_partial_form set to True, then it will get the incomplete
|
|
values as well -- check with the "in" operator if the form was able
|
|
to process any particular field.
|
|
|
|
Anyway, .validate() should return a string or a dictionary. If a
|
|
string, it's an error message that applies to the whole form. If
|
|
not, then it should be a dictionary of fieldName: errorMessage.
|
|
The special key "form" is the error message for the form as a whole
|
|
(i.e., a string is equivalent to {"form": string}).
|
|
|
|
Returns None on no errors.
|
|
"""
|
|
|
|
validate_partial_form = False
|
|
|
|
validate_partial_python = None
|
|
validate_partial_other = None
|
|
|
|
def is_empty(self, value):
|
|
return False
|
|
|
|
|
|
class RequireIfMissing(FormValidator):
|
|
"""
|
|
Require one field based on another field being present or missing.
|
|
|
|
This validator is applied to a form, not an individual field (usually
|
|
using a Schema's ``pre_validators`` or ``chained_validators``) and is
|
|
available under both names ``RequireIfMissing`` and ``RequireIfPresent``.
|
|
|
|
If you provide a ``missing`` value (a string key name) then
|
|
if that field is missing the field must be entered.
|
|
This gives you an either/or situation.
|
|
|
|
If you provide a ``present`` value (another string key name) then
|
|
if that field is present, the required field must also be present.
|
|
|
|
::
|
|
|
|
>>> from formencode import validators
|
|
>>> v = validators.RequireIfPresent('phone_type', present='phone')
|
|
>>> v.to_python(dict(phone_type='', phone='510 420 4577'))
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: You must give a value for phone_type
|
|
>>> v.to_python(dict(phone=''))
|
|
{'phone': ''}
|
|
|
|
Note that if you have a validator on the optionally-required
|
|
field, you should probably use ``if_missing=None``. This way you
|
|
won't get an error from the Schema about a missing value. For example::
|
|
|
|
class PhoneInput(Schema):
|
|
phone = PhoneNumber()
|
|
phone_type = String(if_missing=None)
|
|
chained_validators = [RequireifPresent('phone_type', present='phone')]
|
|
"""
|
|
|
|
# Field that potentially is required:
|
|
required = None
|
|
# If this field is missing, then it is required:
|
|
missing = None
|
|
# If this field is present, then it is required:
|
|
present = None
|
|
|
|
__unpackargs__ = ('required',)
|
|
|
|
def _to_python(self, value_dict, state):
|
|
is_required = False
|
|
if self.missing and not value_dict.get(self.missing):
|
|
is_required = True
|
|
if self.present and value_dict.get(self.present):
|
|
is_required = True
|
|
if is_required and not value_dict.get(self.required):
|
|
raise Invalid(
|
|
_('You must give a value for %s') % self.required,
|
|
value_dict, state,
|
|
error_dict={self.required:
|
|
Invalid(self.message('empty', state), value_dict, state)})
|
|
return value_dict
|
|
|
|
RequireIfPresent = RequireIfMissing
|
|
|
|
|
|
class FieldsMatch(FormValidator):
|
|
"""
|
|
Tests that the given fields match, i.e., are identical. Useful
|
|
for password+confirmation fields. Pass the list of field names in
|
|
as `field_names`.
|
|
|
|
::
|
|
|
|
>>> f = FieldsMatch('pass', 'conf')
|
|
>>> f.to_python({'pass': 'xx', 'conf': 'xx'})
|
|
{'conf': 'xx', 'pass': 'xx'}
|
|
>>> f.to_python({'pass': 'xx', 'conf': 'yy'})
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: conf: Fields do not match
|
|
"""
|
|
|
|
show_match = False
|
|
field_names = None
|
|
validate_partial_form = True
|
|
|
|
__unpackargs__ = ('*', 'field_names')
|
|
|
|
messages = dict(
|
|
invalid=_('Fields do not match (should be %(match)s)'),
|
|
invalidNoMatch=_('Fields do not match'),
|
|
notDict=_('Fields should be a dictionary'))
|
|
|
|
def __init__(self, *args, **kw):
|
|
super(FieldsMatch, self).__init__(*args, **kw)
|
|
if len(self.field_names) < 2:
|
|
raise TypeError('FieldsMatch() requires at least two field names')
|
|
|
|
def validate_partial(self, field_dict, state):
|
|
for name in self.field_names:
|
|
if name not in field_dict:
|
|
return
|
|
self.validate_python(field_dict, state)
|
|
|
|
def validate_python(self, field_dict, state):
|
|
try:
|
|
ref = field_dict[self.field_names[0]]
|
|
except TypeError:
|
|
# Generally because field_dict isn't a dict
|
|
raise Invalid(self.message('notDict', state), field_dict, state)
|
|
except KeyError:
|
|
ref = ''
|
|
errors = {}
|
|
for name in self.field_names[1:]:
|
|
if field_dict.get(name, '') != ref:
|
|
if self.show_match:
|
|
errors[name] = self.message('invalid', state,
|
|
match=ref)
|
|
else:
|
|
errors[name] = self.message('invalidNoMatch', state)
|
|
if errors:
|
|
error_list = errors.items()
|
|
error_list.sort()
|
|
error_message = '<br>\n'.join(
|
|
['%s: %s' % (name, value) for name, value in error_list])
|
|
raise Invalid(error_message, field_dict, state, error_dict=errors)
|
|
|
|
|
|
class CreditCardValidator(FormValidator):
|
|
"""
|
|
Checks that credit card numbers are valid (if not real).
|
|
|
|
You pass in the name of the field that has the credit card
|
|
type and the field with the credit card number. The credit
|
|
card type should be one of "visa", "mastercard", "amex",
|
|
"dinersclub", "discover", "jcb".
|
|
|
|
You must check the expiration date yourself (there is no
|
|
relation between CC number/types and expiration dates).
|
|
|
|
::
|
|
|
|
>>> cc = CreditCardValidator()
|
|
>>> cc.to_python({'ccType': 'visa', 'ccNumber': '4111111111111111'})
|
|
{'ccNumber': '4111111111111111', 'ccType': 'visa'}
|
|
>>> cc.to_python({'ccType': 'visa', 'ccNumber': '411111111111111'})
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: ccNumber: You did not enter a valid number of digits
|
|
>>> cc.to_python({'ccType': 'visa', 'ccNumber': '411111111111112'})
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: ccNumber: You did not enter a valid number of digits
|
|
>>> cc().to_python({})
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: The field ccType is missing
|
|
"""
|
|
|
|
validate_partial_form = True
|
|
|
|
cc_type_field = 'ccType'
|
|
cc_number_field = 'ccNumber'
|
|
|
|
__unpackargs__ = ('cc_type_field', 'cc_number_field')
|
|
|
|
messages = dict(
|
|
notANumber=_('Please enter only the number, no other characters'),
|
|
badLength=_('You did not enter a valid number of digits'),
|
|
invalidNumber=_('That number is not valid'),
|
|
missing_key=_('The field %(key)s is missing'))
|
|
|
|
def validate_partial(self, field_dict, state):
|
|
if not field_dict.get(self.cc_type_field, None) \
|
|
or not field_dict.get(self.cc_number_field, None):
|
|
return None
|
|
self.validate_python(field_dict, state)
|
|
|
|
def validate_python(self, field_dict, state):
|
|
errors = self._validateReturn(field_dict, state)
|
|
if errors:
|
|
error_list = errors.items()
|
|
error_list.sort()
|
|
raise Invalid(
|
|
'<br>\n'.join(["%s: %s" % (name, value)
|
|
for name, value in error_list]),
|
|
field_dict, state, error_dict=errors)
|
|
|
|
def _validateReturn(self, field_dict, state):
|
|
for field in self.cc_type_field, self.cc_number_field:
|
|
if field not in field_dict:
|
|
raise Invalid(
|
|
self.message('missing_key', state, key=field),
|
|
field_dict, state)
|
|
ccType = field_dict[self.cc_type_field].lower().strip()
|
|
number = field_dict[self.cc_number_field].strip()
|
|
number = number.replace(' ', '')
|
|
number = number.replace('-', '')
|
|
try:
|
|
long(number)
|
|
except ValueError:
|
|
return {self.cc_number_field: self.message('notANumber', state)}
|
|
assert ccType in self._cardInfo, (
|
|
"I can't validate that type of credit card")
|
|
foundValid = False
|
|
validLength = False
|
|
for prefix, length in self._cardInfo[ccType]:
|
|
if len(number) == length:
|
|
validLength = True
|
|
if (len(number) == length
|
|
and number.startswith(prefix)):
|
|
foundValid = True
|
|
break
|
|
if not validLength:
|
|
return {self.cc_number_field: self.message('badLength', state)}
|
|
if not foundValid:
|
|
return {self.cc_number_field: self.message('invalidNumber', state)}
|
|
if not self._validateMod10(number):
|
|
return {self.cc_number_field: self.message('invalidNumber', state)}
|
|
return None
|
|
|
|
def _validateMod10(self, s):
|
|
"""Check string with the mod 10 algorithm (aka "Luhn formula")."""
|
|
checksum, factor = 0, 1
|
|
for c in s[::-1]:
|
|
for c in str(factor * int(c)):
|
|
checksum += int(c)
|
|
factor = 3 - factor
|
|
return checksum % 10 == 0
|
|
|
|
_cardInfo = {
|
|
"visa": [('4', 16),
|
|
('4', 13)],
|
|
"mastercard": [('51', 16),
|
|
('52', 16),
|
|
('53', 16),
|
|
('54', 16),
|
|
('55', 16)],
|
|
"discover": [('6011', 16)],
|
|
"amex": [('34', 15),
|
|
('37', 15)],
|
|
"dinersclub": [('300', 14),
|
|
('301', 14),
|
|
('302', 14),
|
|
('303', 14),
|
|
('304', 14),
|
|
('305', 14),
|
|
('36', 14),
|
|
('38', 14)],
|
|
"jcb": [('3', 16),
|
|
('2131', 15),
|
|
('1800', 15)],
|
|
}
|
|
|
|
|
|
class CreditCardExpires(FormValidator):
|
|
"""
|
|
Checks that credit card expiration date is valid relative to
|
|
the current date.
|
|
|
|
You pass in the name of the field that has the credit card
|
|
expiration month and the field with the credit card expiration
|
|
year.
|
|
|
|
::
|
|
|
|
>>> ed = CreditCardExpires()
|
|
>>> ed.to_python({'ccExpiresMonth': '11', 'ccExpiresYear': '2250'})
|
|
{'ccExpiresYear': '2250', 'ccExpiresMonth': '11'}
|
|
>>> ed.to_python({'ccExpiresMonth': '10', 'ccExpiresYear': '2005'})
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: ccExpiresMonth: Invalid Expiration Date<br>
|
|
ccExpiresYear: Invalid Expiration Date
|
|
"""
|
|
|
|
validate_partial_form = True
|
|
|
|
cc_expires_month_field = 'ccExpiresMonth'
|
|
cc_expires_year_field = 'ccExpiresYear'
|
|
|
|
__unpackargs__ = ('cc_expires_month_field', 'cc_expires_year_field')
|
|
|
|
datetime_module = None
|
|
|
|
messages = dict(
|
|
notANumber=_('Please enter numbers only for month and year'),
|
|
invalidNumber=_('Invalid Expiration Date'))
|
|
|
|
def validate_partial(self, field_dict, state):
|
|
if not field_dict.get(self.cc_expires_month_field, None) \
|
|
or not field_dict.get(self.cc_expires_year_field, None):
|
|
return None
|
|
self.validate_python(field_dict, state)
|
|
|
|
def validate_python(self, field_dict, state):
|
|
errors = self._validateReturn(field_dict, state)
|
|
if errors:
|
|
error_list = errors.items()
|
|
error_list.sort()
|
|
raise Invalid(
|
|
'<br>\n'.join(["%s: %s" % (name, value)
|
|
for name, value in error_list]),
|
|
field_dict, state, error_dict=errors)
|
|
|
|
def _validateReturn(self, field_dict, state):
|
|
ccExpiresMonth = str(field_dict[self.cc_expires_month_field]).strip()
|
|
ccExpiresYear = str(field_dict[self.cc_expires_year_field]).strip()
|
|
|
|
try:
|
|
ccExpiresMonth = int(ccExpiresMonth)
|
|
ccExpiresYear = int(ccExpiresYear)
|
|
dt_mod = import_datetime(self.datetime_module)
|
|
now = datetime_now(dt_mod)
|
|
today = datetime_makedate(dt_mod, now.year, now.month, now.day)
|
|
next_month = (ccExpiresMonth % 12) + 1
|
|
if next_month == 1:
|
|
next_month_year = ccExpiresYear + 1
|
|
else:
|
|
next_month_year = ccExpiresYear
|
|
expires_date = datetime_makedate(
|
|
dt_mod, next_month_year, next_month, 1)
|
|
assert expires_date > today
|
|
except ValueError:
|
|
return {self.cc_expires_month_field:
|
|
self.message('notANumber', state),
|
|
self.cc_expires_year_field:
|
|
self.message('notANumber', state)}
|
|
except AssertionError:
|
|
return {self.cc_expires_month_field:
|
|
self.message('invalidNumber', state),
|
|
self.cc_expires_year_field:
|
|
self.message('invalidNumber', state)}
|
|
|
|
|
|
class CreditCardSecurityCode(FormValidator):
|
|
"""
|
|
Checks that credit card security code has the correct number
|
|
of digits for the given credit card type.
|
|
|
|
You pass in the name of the field that has the credit card
|
|
type and the field with the credit card security code.
|
|
|
|
::
|
|
|
|
>>> code = CreditCardSecurityCode()
|
|
>>> code.to_python({'ccType': 'visa', 'ccCode': '111'})
|
|
{'ccType': 'visa', 'ccCode': '111'}
|
|
>>> code.to_python({'ccType': 'visa', 'ccCode': '1111'})
|
|
Traceback (most recent call last):
|
|
...
|
|
Invalid: ccCode: Invalid credit card security code length
|
|
"""
|
|
|
|
validate_partial_form = True
|
|
|
|
cc_type_field = 'ccType'
|
|
cc_code_field = 'ccCode'
|
|
|
|
__unpackargs__ = ('cc_type_field', 'cc_code_field')
|
|
|
|
messages = dict(
|
|
notANumber=_('Please enter numbers only for credit card security code'),
|
|
badLength=_('Invalid credit card security code length'))
|
|
|
|
def validate_partial(self, field_dict, state):
|
|
if not field_dict.get(self.cc_type_field, None) \
|
|
or not field_dict.get(self.cc_code_field, None):
|
|
return None
|
|
self.validate_python(field_dict, state)
|
|
|
|
def validate_python(self, field_dict, state):
|
|
errors = self._validateReturn(field_dict, state)
|
|
if errors:
|
|
error_list = errors.items()
|
|
error_list.sort()
|
|
raise Invalid(
|
|
'<br>\n'.join(["%s: %s" % (name, value)
|
|
for name, value in error_list]),
|
|
field_dict, state, error_dict=errors)
|
|
|
|
def _validateReturn(self, field_dict, state):
|
|
ccType = str(field_dict[self.cc_type_field]).strip()
|
|
ccCode = str(field_dict[self.cc_code_field]).strip()
|
|
|
|
try:
|
|
int(ccCode)
|
|
except ValueError:
|
|
return {self.cc_code_field: self.message('notANumber', state)}
|
|
|
|
length = self._cardInfo[ccType]
|
|
validLength = False
|
|
if len(ccCode) == length:
|
|
validLength = True
|
|
if not validLength:
|
|
return {self.cc_code_field: self.message('badLength', state)}
|
|
|
|
# key = credit card type, value = length of security code
|
|
_cardInfo = dict(visa=3, mastercard=3, discover=3, amex=4)
|
|
|
|
|
|
|
|
def validators():
|
|
"""Return the names of all validators in this module."""
|
|
return [name for name, value in globals().items()
|
|
if isinstance(value, type) and issubclass(value, Validator)]
|
|
|
|
__all__ = ['Invalid'] + validators()
|
|
|