1
## FormEncode, a Form processor
2
## Copyright (C) 2003, Ian Bicking <ianb@colorstudy.com>
4
Validator/Converters for use with FormEncode.
14
from interfaces import *
18
filters = warnings.filters[:]
19
warnings.simplefilter('ignore', DeprecationWarning)
21
warnings.filters = filters
33
DNS.DiscoverNameServers()
38
True, False = (1==1), (0==1)
40
# dummy translation function, nothing is translated here.
41
# Instead this is actually done in api.message.
42
# The surrounding _("string") of the strings is only for extracting
43
# the strings automatically
44
# (Note: we can't use a def statement, because pygettext dislikes it)
47
############################################################
49
############################################################
51
# These all deal with accepting both mxDateTime and datetime
54
mxDateTime_module = None
56
def import_datetime(module_type):
57
global datetime_module, mxDateTime_module
58
if module_type is None:
60
if datetime_module is None:
61
import datetime as datetime_module
62
return datetime_module
64
if mxDateTime_module is None:
65
from mx import DateTime as mxDateTime_module
66
return mxDateTime_module
68
module_type = module_type.lower()
69
assert module_type in ('datetime', 'mxdatetime')
70
if module_type == 'datetime':
71
if datetime_module is None:
72
import datetime as datetime_module
73
return datetime_module
75
if mxDateTime_module is None:
76
from mx import DateTime as mxDateTime_module
77
return mxDateTime_module
79
def datetime_now(module):
80
if module.__name__ == 'datetime':
81
return module.datetime.now()
85
def datetime_makedate(module, year, month, day):
86
if module.__name__ == 'datetime':
87
return module.date(year, month, day)
90
return module.DateTime(year, month, day)
91
except module.RangeError, e:
92
raise ValueError(str(e))
95
# TODO: Needs being extended to support mx.DateTime as well.
96
def datetime_time(module):
97
if module.__name__ == 'datetime':
100
# TODO: Needs being extended to support mx.DateTime as well.
101
def datetime_isotime(module):
102
if module.__name__ == 'datetime':
103
return module.time.isoformat
107
############################################################
109
############################################################
111
class ConfirmType(FancyValidator):
114
Confirms that the input/output is of the proper type.
119
The class or a tuple of classes; the item must be an instance
120
of the class or a subclass.
122
A type or tuple of types (or classes); the item must be of
123
the exact class or type. Subclasses are not allowed.
127
>>> cint = ConfirmType(subclass=int)
128
>>> cint.to_python(True)
130
>>> cint.to_python('1')
131
Traceback (most recent call last):
133
Invalid: '1' is not a subclass of <type 'int'>
134
>>> cintfloat = ConfirmType(subclass=(float, int))
135
>>> cintfloat.to_python(1.0), cintfloat.from_python(1.0)
137
>>> cintfloat.to_python(1), cintfloat.from_python(1)
139
>>> cintfloat.to_python(None)
140
Traceback (most recent call last):
142
Invalid: None is not a subclass of one of the types <type 'float'>, <type 'int'>
143
>>> cint2 = ConfirmType(type=int)
144
>>> cint2(accept_python=False).from_python(True)
145
Traceback (most recent call last):
147
Invalid: True must be of the type <type 'int'>
154
'subclass': _("%(object)r is not a subclass of %(subclass)s"),
155
'inSubclass': _("%(object)r is not a subclass of one of the types %(subclassList)s"),
156
'inType': _("%(object)r must be one of the types %(typeList)s"),
157
'type': _("%(object)r must be of the type %(type)s"),
160
def __init__(self, *args, **kw):
161
FancyValidator.__init__(self, *args, **kw)
163
if isinstance(self.subclass, list):
164
self.subclass = tuple(self.subclass)
165
elif not isinstance(self.subclass, tuple):
166
self.subclass = (self.subclass,)
167
self.validate_python = self.confirm_subclass
169
if isinstance(self.type, list):
170
self.type = tuple(self.type)
171
elif not isinstance(self.type, tuple):
172
self.type = (self.type,)
173
self.validate_python = self.confirm_type
175
def confirm_subclass(self, value, state):
176
if not isinstance(value, self.subclass):
177
if len(self.subclass) == 1:
178
msg = self.message('subclass', state, object=value,
179
subclass=self.subclass[0])
181
subclass_list = ', '.join(map(str, self.subclass))
182
msg = self.message('inSubclass', state, object=value,
183
subclassList=subclass_list)
184
raise Invalid(msg, value, state)
186
def confirm_type(self, value, state):
191
if len(self.type) == 1:
192
msg = self.message('type', state, object=value,
195
msg = self.message('inType', state, object=value,
196
typeList=', '.join(map(str, self.type)))
197
raise Invalid(msg, value, state)
200
def is_empty(self, value):
203
class Wrapper(FancyValidator):
206
Used to convert functions to validator/converters.
208
You can give a simple function for `to_python`, `from_python`,
209
`validate_python` or `validate_other`. If that function raises an
210
exception, the value is considered invalid. Whatever value the
211
function returns is considered the converted value.
213
Unlike validators, the `state` argument is not used. Functions
214
like `int` can be used here, that take a single argument.
220
>>> wrap = Wrapper(to_python=downcase)
221
>>> wrap.to_python('This')
223
>>> wrap.from_python('This')
225
>>> wrap2 = Wrapper(from_python=downcase)
226
>>> wrap2.from_python('This')
228
>>> wrap2.from_python(1)
229
Traceback (most recent call last):
231
Invalid: 'int' object has no attribute 'lower'
232
>>> wrap3 = Wrapper(validate_python=int)
233
>>> wrap3.to_python('1')
235
>>> wrap3.to_python('a') # doctest: +ELLIPSIS
236
Traceback (most recent call last):
238
Invalid: invalid literal for int()...
241
func_to_python = None
242
func_from_python = None
243
func_validate_python = None
244
func_validate_other = None
246
def __init__(self, *args, **kw):
247
for n in ['to_python', 'from_python', 'validate_python',
250
kw['func_%s' % n] = kw[n]
252
FancyValidator.__init__(self, *args, **kw)
253
self._to_python = self.wrap(self.func_to_python)
254
self._from_python = self.wrap(self.func_from_python)
255
self.validate_python = self.wrap(self.func_validate_python)
256
self.validate_other = self.wrap(self.func_validate_other)
258
def wrap(self, func):
261
def result(value, state, func=func):
265
raise Invalid(str(e), {}, value, state)
268
class Constant(FancyValidator):
271
This converter converts everything to the same thing.
273
I.e., you pass in the constant value when initializing, then all
274
values get converted to that constant value.
276
This is only really useful for funny situations, like::
278
fromEmailValidator = ValidateAny(
280
Constant('unknown@localhost'))
282
In this case, the if the email is not valid
283
``'unknown@localhost'`` will be used instead. Of course, you
284
could use ``if_invalid`` instead.
288
>>> Constant('X').to_python('y')
292
__unpackargs__ = ('value',)
294
def _to_python(self, value, state):
297
_from_python = _to_python
299
############################################################
301
############################################################
303
class MaxLength(FancyValidator):
306
Invalid if the value is longer than `maxLength`. Uses len(),
307
so it can work for strings, lists, or anything with length.
311
>>> max5 = MaxLength(5)
312
>>> max5.to_python('12345')
314
>>> max5.from_python('12345')
316
>>> max5.to_python('123456')
317
Traceback (most recent call last):
319
Invalid: Enter a value less than 5 characters long
320
>>> max5(accept_python=False).from_python('123456')
321
Traceback (most recent call last):
323
Invalid: Enter a value less than 5 characters long
324
>>> max5.to_python([1, 2, 3])
326
>>> max5.to_python([1, 2, 3, 4, 5, 6])
327
Traceback (most recent call last):
329
Invalid: Enter a value less than 5 characters long
330
>>> max5.to_python(5)
331
Traceback (most recent call last):
333
Invalid: Invalid value (value with length expected)
336
__unpackargs__ = ('maxLength',)
338
'tooLong': _("Enter a value less than %(maxLength)i characters long"),
339
'invalid': _("Invalid value (value with length expected)"),
342
def validate_python(self, value, state):
345
len(value) > self.maxLength:
346
raise Invalid(self.message('tooLong', state,
347
maxLength=self.maxLength),
352
raise Invalid(self.message('invalid', state),
355
class MinLength(FancyValidator):
358
Invalid if the value is shorter than `minlength`. Uses len(), so
359
it can work for strings, lists, or anything with length. Note
360
that you **must** use ``not_empty=True`` if you don't want to
361
accept empty values -- empty values are not tested for length.
365
>>> min5 = MinLength(5)
366
>>> min5.to_python('12345')
368
>>> min5.from_python('12345')
370
>>> min5.to_python('1234')
371
Traceback (most recent call last):
373
Invalid: Enter a value at least 5 characters long
374
>>> min5(accept_python=False).from_python('1234')
375
Traceback (most recent call last):
377
Invalid: Enter a value at least 5 characters long
378
>>> min5.to_python([1, 2, 3, 4, 5])
380
>>> min5.to_python([1, 2, 3])
381
Traceback (most recent call last):
383
Invalid: Enter a value at least 5 characters long
384
>>> min5.to_python(5)
385
Traceback (most recent call last):
387
Invalid: Invalid value (value with length expected)
391
__unpackargs__ = ('minLength',)
394
'tooShort': _("Enter a value at least %(minLength)i characters long"),
395
'invalid': _("Invalid value (value with length expected)"),
398
def validate_python(self, value, state):
400
if len(value) < self.minLength:
401
raise Invalid(self.message('tooShort', state,
402
minLength=self.minLength),
405
raise Invalid(self.message('invalid', state),
408
class NotEmpty(FancyValidator):
411
Invalid if value is empty (empty string, empty list, etc).
413
Generally for objects that Python considers false, except zero
414
which is not considered invalid.
418
>>> ne = NotEmpty(messages={'empty': 'enter something'})
420
Traceback (most recent call last):
422
Invalid: enter something
429
'empty': _("Please enter a value"),
432
def validate_python(self, value, state):
434
# This isn't "empty" for this definition.
437
raise Invalid(self.message('empty', state),
440
class Empty(FancyValidator):
443
Invalid unless the value is empty. Use cleverly, if at all.
447
>>> Empty.to_python(0)
448
Traceback (most recent call last):
450
Invalid: You cannot enter a value here
454
'notEmpty': _("You cannot enter a value here"),
457
def validate_python(self, value, state):
458
if value or value == 0:
459
raise Invalid(self.message('notEmpty', state),
462
class Regex(FancyValidator):
465
Invalid if the value doesn't match the regular expression `regex`.
467
The regular expression can be a compiled re object, or a string
468
which will be compiled for you.
470
Use strip=True if you want to strip the value before validation,
471
and as a form of conversion (often useful).
475
>>> cap = Regex(r'^[A-Z]+$')
476
>>> cap.to_python('ABC')
479
Note that ``.from_python()`` calls (in general) do not validate
482
>>> cap.from_python('abc')
484
>>> cap(accept_python=False).from_python('abc')
485
Traceback (most recent call last):
487
Invalid: The input is not valid
489
Traceback (most recent call last):
491
Invalid: The input must be a string (not a <type 'int'>: 1)
492
>>> Regex(r'^[A-Z]+$', strip=True).to_python(' ABC ')
494
>>> Regex(r'this', regexOps=('I',)).to_python('THIS')
502
__unpackargs__ = ('regex',)
505
'invalid': _("The input is not valid"),
508
def __init__(self, *args, **kw):
509
FancyValidator.__init__(self, *args, **kw)
510
if isinstance(self.regex, basestring):
512
assert not isinstance(self.regexOps, basestring), (
513
"regexOps should be a list of options from the re module "
514
"(names, or actual values)")
515
for op in self.regexOps:
516
if isinstance(op, basestring):
517
ops |= getattr(re, op)
520
self.regex = re.compile(self.regex, ops)
522
def validate_python(self, value, state):
523
self.assert_string(value, state)
524
if self.strip and isinstance(value, basestring):
525
value = value.strip()
526
if not self.regex.search(value):
527
raise Invalid(self.message('invalid', state),
530
def _to_python(self, value, state):
531
if self.strip and isinstance(value, basestring):
535
class PlainText(Regex):
538
Test that the field contains only letters, numbers, underscore,
539
and the hyphen. Subclasses Regex.
543
>>> PlainText.to_python('_this9_')
545
>>> PlainText.from_python(' this ')
547
>>> PlainText(accept_python=False).from_python(' this ')
548
Traceback (most recent call last):
550
Invalid: Enter only letters, numbers, or _ (underscore)
551
>>> PlainText(strip=True).to_python(' this ')
553
>>> PlainText(strip=True).from_python(' this ')
557
regex = r"^[a-zA-Z_\-0-9]*$"
560
'invalid': _('Enter only letters, numbers, or _ (underscore)'),
563
class OneOf(FancyValidator):
566
Tests that the value is one of the members of a given list.
568
If ``testValueList=True``, then if the input value is a list or
569
tuple, all the members of the sequence will be checked (i.e., the
570
input must be a subset of the allowed values).
572
Use ``hideList=True`` to keep the list of valid values out of the
573
error message in exceptions.
577
>>> oneof = OneOf([1, 2, 3])
578
>>> oneof.to_python(1)
580
>>> oneof.to_python(4)
581
Traceback (most recent call last):
583
Invalid: Value must be one of: 1; 2; 3 (not 4)
584
>>> oneof(testValueList=True).to_python([2, 3, [1, 2, 3]])
586
>>> oneof.to_python([2, 3, [1, 2, 3]])
587
Traceback (most recent call last):
589
Invalid: Value must be one of: 1; 2; 3 (not [2, 3, [1, 2, 3]])
593
testValueList = False
596
__unpackargs__ = ('list',)
599
'invalid': _("Invalid value"),
600
'notIn': _("Value must be one of: %(items)s (not %(value)r)"),
603
def validate_python(self, value, state):
604
if self.testValueList and isinstance(value, (list, tuple)):
606
self.validate_python(v, state)
608
if not value in self.list:
610
raise Invalid(self.message('invalid', state),
613
items = '; '.join(map(str, self.list))
614
raise Invalid(self.message('notIn', state,
619
class DictConverter(FancyValidator):
622
Converts values based on a dictionary which has values as keys for
623
the resultant values.
625
If ``allowNull`` is passed, it will not balk if a false value
626
(e.g., '' or None) is given (it will return None in these cases).
628
to_python takes keys and gives values, from_python takes values and
631
If you give hideDict=True, then the contents of the dictionary
632
will not show up in error messages.
636
>>> dc = DictConverter({1: 'one', 2: 'two'})
639
>>> dc.from_python('one')
642
Traceback (most recent call last):
643
Invalid: Enter a value from: 1; 2
644
>>> dc2 = dc(hideDict=True)
650
Traceback (most recent call last):
651
Invalid: Choose something
652
>>> dc.from_python('three')
653
Traceback (most recent call last):
654
Invalid: Nothing in my dictionary goes by the value 'three'. Choose one of: 'one'; 'two'
660
__unpackargs__ = ('dict',)
663
'keyNotFound': _("Choose something"),
664
'chooseKey': _("Enter a value from: %(items)s"),
665
'valueNotFound': _("That value is not known"),
666
'chooseValue': _("Nothing in my dictionary goes by the value %(value)s. Choose one of: %(items)s"),
669
def _to_python(self, value, state):
671
return self.dict[value]
674
raise Invalid(self.message('keyNotFound', state),
677
items = '; '.join(map(repr, self.dict.keys()))
678
raise Invalid(self.message('chooseKey', state,
682
def _from_python(self, value, state):
683
for k, v in self.dict.items():
687
raise Invalid(self.message('valueNotFound', state),
690
items = '; '.join(map(repr, self.dict.values()))
691
raise Invalid(self.message('chooseValue', state,
696
class IndexListConverter(FancyValidator):
699
Converts a index (which may be a string like '2') to the value in
704
>>> index = IndexListConverter(['zero', 'one', 'two'])
705
>>> index.to_python(0)
707
>>> index.from_python('zero')
709
>>> index.to_python('1')
711
>>> index.to_python(5)
712
Traceback (most recent call last):
713
Invalid: Index out of range
714
>>> index(not_empty=True).to_python(None)
715
Traceback (most recent call last):
716
Invalid: Please enter a value
717
>>> index.from_python('five')
718
Traceback (most recent call last):
719
Invalid: Item 'five' was not found in the list
724
__unpackargs__ = ('list',)
727
'integer': _("Must be an integer index"),
728
'outOfRange': _("Index out of range"),
729
'notFound': _("Item %(value)s was not found in the list"),
732
def _to_python(self, value, state):
735
except (ValueError, TypeError):
736
raise Invalid(self.message('integer', state),
739
return self.list[value]
741
raise Invalid(self.message('outOfRange', state),
744
def _from_python(self, value, state):
745
for i in range(len(self.list)):
746
if self.list[i] == value:
748
raise Invalid(self.message('notFound', state,
752
class DateValidator(FancyValidator):
755
Validates that a date is within the given range. Be sure to call
756
DateConverter first if you aren't expecting mxDateTime input.
758
``earliest_date`` and ``latest_date`` may be functions; if so,
759
they will be called each time before validating.
761
``after_now`` means a time after the current timestamp; note that
762
just a few milliseconds before now is invalid! ``today_or_after``
763
is more permissive, and ignores hours and minutes.
767
>>> from datetime import datetime, timedelta
768
>>> d = DateValidator(earliest_date=datetime(2003, 1, 1))
769
>>> d.to_python(datetime(2004, 1, 1))
770
datetime.datetime(2004, 1, 1, 0, 0)
771
>>> d.to_python(datetime(2002, 1, 1))
772
Traceback (most recent call last):
774
Invalid: Date must be after Wednesday, 01 January 2003
775
>>> d.to_python(datetime(2003, 1, 1))
776
datetime.datetime(2003, 1, 1, 0, 0)
777
>>> d = DateValidator(after_now=True)
778
>>> now = datetime.now()
779
>>> d.to_python(now+timedelta(seconds=5)) == now+timedelta(seconds=5)
781
>>> d.to_python(now-timedelta(days=1))
782
Traceback (most recent call last):
784
Invalid: The date must be sometime in the future
785
>>> d.to_python(now+timedelta(days=1)) > now
787
>>> d = DateValidator(today_or_after=True)
788
>>> d.to_python(now) == now
796
# Like after_now, but just after this morning:
797
today_or_after = False
798
# Use 'datetime' to force the Python 2.3+ datetime module, or
799
# 'mxDateTime' to force the mxDateTime module (None means use
800
# datetime, or if not present mxDateTime)
801
datetime_module = None
804
'after': _("Date must be after %(date)s"),
805
'before': _("Date must be before %(date)s"),
806
# Double %'s, because this will be substituted twice:
807
'date_format': _("%%A, %%d %%B %%Y"),
808
'future': _("The date must be sometime in the future"),
811
def validate_python(self, value, state):
812
date_format = self.message('date_format', state)
813
if isinstance(date_format, unicode):
814
# strftime uses the locale encoding, not Unicode
815
encoding = locale.getlocale(locale.LC_TIME)[1] or 'utf-8'
816
date_format = date_format.encode(encoding)
819
if self.earliest_date:
820
if callable(self.earliest_date):
821
earliest_date = self.earliest_date()
823
earliest_date = self.earliest_date
824
if value < earliest_date:
825
date_formatted = earliest_date.strftime(date_format)
827
date_formatted = date_formatted.decode(encoding)
829
self.message('after', state,
830
date=date_formatted),
833
if callable(self.latest_date):
834
latest_date = self.latest_date()
836
latest_date = self.latest_date
837
if value > latest_date:
838
date_formatted = latest_date.strftime(date_format)
840
date_formatted = date_formatted.decode(encoding)
842
self.message('before', state,
843
date=date_formatted),
846
dt_mod = import_datetime(self.datetime_module)
847
now = datetime_now(dt_mod)
849
date_formatted = now.strftime(date_format)
851
date_formatted = date_formatted.decode(encoding)
853
self.message('future', state,
854
date=date_formatted),
856
if self.today_or_after:
857
dt_mod = import_datetime(self.datetime_module)
858
now = datetime_now(dt_mod)
859
today = datetime_makedate(dt_mod,
860
now.year, now.month, now.day)
861
value_as_date = datetime_makedate(
862
dt_mod, value.year, value.month, value.day)
863
if value_as_date < today:
864
date_formatted = now.strftime(date_format)
866
date_formatted = date_formatted.decode(encoding)
868
self.message('future', state,
869
date=date_formatted),
873
class Bool(FancyValidator):
876
Always Valid, returns True or False based on the value and the
877
existance of the value.
879
If you want to convert strings like ``'true'`` to booleans, then
880
use ``StringBoolean``.
884
>>> Bool.to_python(0)
886
>>> Bool.to_python(1)
888
>>> Bool.to_python('')
890
>>> Bool.to_python(None)
896
def _to_python(self, value, state):
898
_from_python = _to_python
900
def empty_value(self, value):
903
class RangeValidator(FancyValidator):
905
"""This is an abstract base class for Int and Number.
907
It verifies that a value is within range. It accepts min and max
908
values in the constructor.
910
(Since this is an abstract base class, the tests are in Int and
916
'tooLow': _("Please enter a number that is %(min)s or greater"),
917
'tooHigh': _("Please enter a number that is %(max)s or smaller"),
923
def validate_python(self, value, state):
924
if self.min is not None:
926
msg = self.message("tooLow", state, min=self.min)
927
raise Invalid(msg, value, state)
928
if self.max is not None:
930
msg = self.message("tooHigh", state, max=self.max)
931
raise Invalid(msg, value, state)
934
class Int(RangeValidator):
936
"""Convert a value to an integer.
940
>>> Int.to_python('10')
942
>>> Int.to_python('ten')
943
Traceback (most recent call last):
945
Invalid: Please enter an integer value
946
>>> Int(min=5).to_python('6')
948
>>> Int(max=10).to_python('11')
949
Traceback (most recent call last):
951
Invalid: Please enter a number that is 10 or smaller
956
'integer': _("Please enter an integer value")
959
def _to_python(self, value, state):
962
except (ValueError, TypeError):
963
raise Invalid(self.message('integer', state),
966
_from_python = _to_python
969
class Number(RangeValidator):
971
"""Convert a value to a float or integer.
973
Tries to convert it to an integer if no information is lost.
977
>>> Number.to_python('10')
979
>>> Number.to_python('10.5')
981
>>> Number.to_python('ten')
982
Traceback (most recent call last):
984
Invalid: Please enter a number
985
>>> Number(min=5).to_python('6.5')
987
>>> Number(max=10.5).to_python('11.5')
988
Traceback (most recent call last):
990
Invalid: Please enter a number that is 10.5 or smaller
991
>>> Number().to_python('infinity')
997
'number': _("Please enter a number")
1000
def _to_python(self, value, state):
1002
value = float(value)
1004
int_value = int(value)
1005
except OverflowError:
1007
if value == int_value:
1011
raise Invalid(self.message('number', state),
1015
class String(FancyValidator):
1017
Converts things to string, but treats empty things as the empty
1020
Also takes a `max` and `min` argument, and the string length must
1023
Also you may give an `encoding` argument, which will encode any
1024
unicode that is found. Lists and tuples are joined with
1025
`list_joiner` (default ``', '``) in ``from_python``.
1029
>>> String(min=2).to_python('a')
1030
Traceback (most recent call last):
1032
Invalid: Enter a value 2 characters long or more
1033
>>> String(max=10).to_python('xxxxxxxxxxx')
1034
Traceback (most recent call last):
1036
Invalid: Enter a value less than 10 characters long
1037
>>> String().from_python(None)
1039
>>> String().from_python([])
1041
>>> String().to_python(None)
1043
>>> String(min=3).to_python(None)
1044
Traceback (most recent call last):
1046
Invalid: Please enter a value
1047
>>> String(min=1).to_python('')
1048
Traceback (most recent call last):
1050
Invalid: Please enter a value
1061
'tooLong': _("Enter a value less than %(max)i characters long"),
1062
'tooShort': _("Enter a value %(min)i characters long or more"),
1065
def __initargs__(self, new_attrs):
1066
if self.not_empty is None and self.min:
1067
self.not_empty = True
1069
def _to_python(self, value, state):
1072
if not isinstance(value, basestring):
1075
except UnicodeEncodeError:
1076
value = unicode(value)
1077
if self.encoding is not None and isinstance(value, unicode):
1078
value = value.encode(self.encoding)
1081
def _from_python(self, value, state):
1082
if not value and value != 0:
1084
if not isinstance(value, basestring):
1085
if isinstance(value, (list, tuple)):
1086
value = self.list_joiner.join([
1087
self._from_python(v, state) for v in value])
1090
except UnicodeEncodeError:
1091
value = unicode(value)
1092
if self.encoding is not None and isinstance(value, unicode):
1093
value = value.encode(self.encoding)
1095
value = value.strip()
1098
def validate_other(self, value, state):
1099
if (self.max is not None and value is not None
1100
and len(value) > self.max):
1101
raise Invalid(self.message('tooLong', state,
1104
if (self.min is not None
1105
and (not value or len(value) < self.min)):
1106
raise Invalid(self.message('tooShort', state,
1110
def empty_value(self, value):
1113
class UnicodeString(String):
1115
Converts things to unicode string, this is a specialization of
1118
In addition to the String arguments, an encoding argument is also
1119
accepted. By default the encoding will be utf-8. You can overwrite
1120
this using the encoding parameter. You can also set inputEncoding
1121
and outputEncoding differently. An inputEncoding of None means
1122
"do not decode", an outputEncoding of None means "do not encode".
1124
All converted strings are returned as Unicode strings.
1128
>>> UnicodeString().to_python(None)
1130
>>> UnicodeString().to_python([])
1132
>>> UnicodeString(encoding='utf-7').to_python('Ni Ni Ni')
1137
inputEncoding = NoDefault
1138
outputEncoding = NoDefault
1140
'badEncoding' : _("Invalid data or incorrect encoding"),
1143
def __init__(self, **kw):
1144
String.__init__(self, **kw)
1145
if self.inputEncoding is NoDefault:
1146
self.inputEncoding = self.encoding
1147
if self.outputEncoding is NoDefault:
1148
self.outputEncoding = self.encoding
1150
def _to_python(self, value, state):
1153
if isinstance(value, unicode):
1155
if not isinstance(value, unicode):
1156
if hasattr(value, '__unicode__'):
1157
value = unicode(value)
1161
if self.inputEncoding:
1163
value = unicode(value, self.inputEncoding)
1164
except UnicodeDecodeError:
1165
raise Invalid(self.message('badEncoding', state), value, state)
1167
raise Invalid(self.message('badType', state, type=type(value),
1168
value=value), value, state)
1171
def _from_python(self, value, state):
1172
if not isinstance(value, unicode):
1173
if hasattr(value, '__unicode__'):
1174
value = unicode(value)
1177
if self.outputEncoding and isinstance(value, unicode):
1178
value = value.encode(self.outputEncoding)
1181
def empty_value(self, value):
1184
class Set(FancyValidator):
1187
This is for when you think you may return multiple values for a
1190
This way the result will always be a list, even if there's only
1191
one result. It's equivalent to ForEach(convertToList=True).
1193
If you give ``use_set=True``, then it will return an actual
1198
>>> Set.to_python(None)
1200
>>> Set.to_python('this')
1202
>>> Set.to_python(('this', 'that'))
1204
>>> s = Set(use_set=True)
1205
>>> s.to_python(None)
1207
>>> s.to_python('this')
1209
>>> s.to_python(('this',))
1217
def _to_python(self, value, state):
1219
if isinstance(value, (set, sets.Set)):
1221
elif isinstance(value, (list, tuple)):
1228
if isinstance(value, list):
1230
elif sets and isinstance(value, (set, sets.Set)):
1232
elif isinstance(value, tuple):
1239
def empty_value(self, value):
1245
class Email(FancyValidator):
1247
Validate an email address.
1249
If you pass ``resolve_domain=True``, then it will try to resolve
1250
the domain name to make sure it's valid. This takes longer, of
1251
course. You must have the `pyDNS <http://pydns.sf.net>`__ modules
1252
installed to look up DNS (MX and A) records.
1257
>>> e.to_python(' test@foo.com ')
1259
>>> e.to_python('test')
1260
Traceback (most recent call last):
1262
Invalid: An email address must contain a single @
1263
>>> e.to_python('test@foobar')
1264
Traceback (most recent call last):
1266
Invalid: The domain portion of the email address is invalid (the portion after the @: foobar)
1267
>>> e.to_python('test@foobar.com.5')
1268
Traceback (most recent call last):
1270
Invalid: The domain portion of the email address is invalid (the portion after the @: foobar.com.5)
1271
>>> e.to_python('test@foo..bar.com')
1272
Traceback (most recent call last):
1274
Invalid: The domain portion of the email address is invalid (the portion after the @: foo..bar.com)
1275
>>> e.to_python('test@.foo.bar.com')
1276
Traceback (most recent call last):
1278
Invalid: The domain portion of the email address is invalid (the portion after the @: .foo.bar.com)
1279
>>> e.to_python('nobody@xn--m7r7ml7t24h.com')
1280
'nobody@xn--m7r7ml7t24h.com'
1281
>>> e.to_python('o*reilly@test.com')
1283
>>> e = Email(resolve_domain=True)
1284
>>> e.resolve_domain
1286
>>> e.to_python('doesnotexist@colorstudy.com')
1287
'doesnotexist@colorstudy.com'
1288
>>> e.to_python('test@forums.nyu.edu')
1289
'test@forums.nyu.edu'
1290
>>> # NOTE: If you do not have PyDNS installed this example won't work:
1291
>>> e.to_python('test@thisdomaindoesnotexistithinkforsure.com')
1292
Traceback (most recent call last):
1294
Invalid: The domain of the email address does not exist (the portion after the @: thisdomaindoesnotexistithinkforsure.com)
1295
>>> e.to_python(u'test@google.com')
1297
>>> e = Email(not_empty=False)
1302
resolve_domain = False
1304
usernameRE = re.compile(r"^[^ \t\n\r@<>()]+$", re.I)
1305
domainRE = re.compile(r'''
1306
^(?:[a-z0-9][a-z0-9\-]{0,62}\.)+ # (sub)domain - alpha followed by 62max chars (63 total)
1308
''', re.I | re.VERBOSE)
1311
'empty': _('Please enter an email address'),
1312
'noAt': _('An email address must contain a single @'),
1313
'badUsername': _('The username portion of the email address is invalid (the portion before the @: %(username)s)'),
1314
'socketError': _('An error occured when trying to connect to the server: %(error)s'),
1315
'badDomain': _('The domain portion of the email address is invalid (the portion after the @: %(domain)s)'),
1316
'domainDoesNotExist': _('The domain of the email address does not exist (the portion after the @: %(domain)s)'),
1319
def __init__(self, *args, **kw):
1320
FancyValidator.__init__(self, *args, **kw)
1321
if self.resolve_domain:
1325
"pyDNS <http://pydns.sf.net> is not installed on "
1326
"your system (or the DNS package cannot be found). "
1327
"I cannot resolve domain names in addresses")
1328
raise ImportError, "no module named DNS"
1330
def validate_python(self, value, state):
1333
self.message('empty', state),
1335
value = value.strip()
1336
splitted = value.split('@', 1)
1338
username, domain=splitted
1341
self.message('noAt', state),
1343
if not self.usernameRE.search(username):
1345
self.message('badUsername', state,
1348
if not self.domainRE.search(domain):
1350
self.message('badDomain', state,
1353
if self.resolve_domain:
1354
assert have_dns, "pyDNS should be available"
1356
a=DNS.DnsRequest(domain, qtype='mx').req().answers
1358
a=DNS.DnsRequest(domain, qtype='a').req().answers
1359
dnsdomains=[x['data'] for x in a]
1360
except (socket.error, DNS.DNSError), e:
1362
self.message('socketError', state, error=e),
1366
self.message('domainDoesNotExist', state,
1370
def _to_python(self, value, state):
1371
return value.strip()
1373
class URL(FancyValidator):
1376
Validate a URL, either http://... or https://. If check_exists
1377
is true, then we'll actually make a request for the page.
1379
If add_http is true, then if no scheme is present we'll add
1384
>>> u = URL(add_http=True)
1385
>>> u.to_python('foo.com')
1387
>>> u.to_python('http://hahaha.ha/bar.html')
1388
'http://hahaha.ha/bar.html'
1389
>>> u.to_python('http://xn--m7r7ml7t24h.com')
1390
'http://xn--m7r7ml7t24h.com'
1391
>>> u.to_python('http://foo.com/test?bar=baz&fleem=morx')
1392
'http://foo.com/test?bar=baz&fleem=morx'
1393
>>> u.to_python('http://foo.com/login?came_from=http%3A%2F%2Ffoo.com%2Ftest')
1394
'http://foo.com/login?came_from=http%3A%2F%2Ffoo.com%2Ftest'
1395
>>> u.to_python('http://foo.com:8000/test.html')
1396
'http://foo.com:8000/test.html'
1397
>>> u.to_python('http://foo.com/something\\nelse')
1398
Traceback (most recent call last):
1400
Invalid: That is not a valid URL
1401
>>> u.to_python('https://test.com')
1403
>>> u.to_python('http://test')
1404
Traceback (most recent call last):
1406
Invalid: You must provide a full domain name (like test.com)
1407
>>> u.to_python('http://test..com')
1408
Traceback (most recent call last):
1410
Invalid: That is not a valid URL
1411
>>> u = URL(add_http=False, check_exists=True)
1412
>>> u.to_python('http://google.com')
1414
>>> u.to_python('google.com')
1415
Traceback (most recent call last):
1417
Invalid: You must start your URL with http://, https://, etc
1418
>>> u.to_python('http://ianbicking.org/doesnotexist.html')
1419
Traceback (most recent call last):
1421
Invalid: The server responded that the page could not be found
1422
>>> u.to_python('http://this.domain.does.not.exist.example.org/test.html')
1423
Traceback (most recent call last):
1425
Invalid: An error occured when trying to connect to the server: ...
1427
If you want to allow addresses without a TLD (e.g., ``localhost``) you can do::
1429
>>> URL(require_tld=False).to_python('http://localhost')
1434
check_exists = False
1438
url_re = re.compile(r'''
1440
(?:[%:\w]*@)? # authenticator
1441
(?P<domain>[a-z0-9][a-z0-9\-]{1,62}\.)* # (sub)domain - alpha followed by 62max chars (63 total)
1442
(?P<tld>[a-z]{2,}) # TLD
1446
(?P<path>/[a-z0-9\-\._~:/\?#\[\]@!%\$&\'\(\)\*\+,;=]*)?
1448
''', re.I | re.VERBOSE)
1450
scheme_re = re.compile(r'^[a-zA-Z]+:')
1453
'noScheme': _('You must start your URL with http://, https://, etc'),
1454
'badURL': _('That is not a valid URL'),
1455
'httpError': _('An error occurred when trying to access the URL: %(error)s'),
1456
'socketError': _('An error occured when trying to connect to the server: %(error)s'),
1457
'notFound': _('The server responded that the page could not be found'),
1458
'status': _('The server responded with a bad status code (%(status)s)'),
1459
'noTLD': _('You must provide a full domain name (like %(domain)s.com)'),
1462
def _to_python(self, value, state):
1463
value = value.strip()
1465
if not self.scheme_re.search(value):
1466
value = 'http://' + value
1467
match = self.scheme_re.search(value)
1470
self.message('noScheme', state),
1472
value = match.group(0).lower() + value[len(match.group(0)):]
1473
match = self.url_re.search(value)
1476
self.message('badURL', state),
1478
if self.require_tld and not match.group('domain'):
1480
self.message('noTLD', state, domain=match.group('tld')),
1482
if self.check_exists and (value.startswith('http://')
1483
or value.startswith('https://')):
1484
self._check_url_exists(value, state)
1487
def _check_url_exists(self, url, state):
1488
global httplib, urlparse, socket
1491
if urlparse is None:
1495
scheme, netloc, path, params, query, fragment = urlparse.urlparse(
1497
if scheme == 'http':
1498
ConnClass = httplib.HTTPConnection
1500
ConnClass = httplib.HTTPSConnection
1502
conn = ConnClass(netloc)
1504
path += ';' + params
1507
conn.request('HEAD', path)
1508
res = conn.getresponse()
1509
except httplib.HTTPException, e:
1511
self.message('httpError', state, error=e),
1513
except socket.error, e:
1515
self.message('socketError', state, error=e),
1518
if res.status == 404:
1520
self.message('notFound', state),
1522
if (res.status < 200
1523
or res.status >= 500):
1525
self.message('status', state, status=res.status),
1529
class XRI(FancyValidator):
1533
It supports both i-names and i-numbers, of the first version of the XRI
1538
>>> inames = XRI(xri_type="i-name")
1539
>>> inames.to_python(" =John.Smith ")
1541
>>> inames.to_python("@Free.Software.Foundation")
1542
'@Free.Software.Foundation'
1543
>>> inames.to_python("Python.Software.Foundation")
1544
Traceback (most recent call last):
1546
Invalid: The type of i-name is not defined; it may be either individual or organizational
1547
>>> inames.to_python("http://example.org")
1548
Traceback (most recent call last):
1550
Invalid: The type of i-name is not defined; it may be either individual or organizational
1551
>>> inames.to_python("=!2C43.1A9F.B6F6.E8E6")
1552
Traceback (most recent call last):
1554
Invalid: "!2C43.1A9F.B6F6.E8E6" is an invalid i-name
1555
>>> iname_with_schema = XRI(True, xri_type="i-name")
1556
>>> iname_with_schema.to_python("=Richard.Stallman")
1557
'xri://=Richard.Stallman'
1558
>>> inames.to_python("=John Smith")
1559
Traceback (most recent call last):
1561
Invalid: "John Smith" is an invalid i-name
1562
>>> inumbers = XRI(xri_type="i-number")
1563
>>> inumbers.to_python("!!1000!de21.4536.2cb2.8074")
1564
'!!1000!de21.4536.2cb2.8074'
1565
>>> inumbers.to_python("@!1000.9554.fabd.129c!2847.df3c")
1566
'@!1000.9554.fabd.129c!2847.df3c'
1570
iname_valid_pattern = re.compile(r"""
1572
[\w]+ # A global alphanumeric i-name
1573
(\.[\w]+)* # An i-name with dots
1574
(\*[\w]+(\.[\w]+)*)* # A community i-name
1576
""", re.VERBOSE|re.UNICODE)
1579
iname_invalid_start = re.compile(r"^[\d\.-]", re.UNICODE)
1580
"""@cvar: These characters must not be at the beggining of the i-name"""
1582
inumber_pattern = re.compile(r"""
1585
[=@]! # It's a personal or organization i-number
1587
!! # It's a network i-number
1589
[\dA-F]{1,4}(\.[\dA-F]{1,4}){0,3} # A global i-number
1590
(![\dA-F]{1,4}(\.[\dA-F]{1,4}){0,3})* # Zero or more sub i-numbers
1592
""", re.VERBOSE|re.IGNORECASE)
1595
'noType': _("The type of i-name is not defined; it may be either individual or organizational"),
1596
'repeatedChar': _("Dots and dashes may not be repeated consecutively"),
1597
'badIname': _('"%(iname)s" is an invalid i-name'),
1598
'badInameStart': _("i-names may not start with numbers nor punctuation "
1600
'badInumber': _('"%(inumber)s" is an invalid i-number'),
1601
'badType': _("The XRI must be a string (not a %(type)s: %(value)r)"),
1602
'badXri': _('"%(xri_type)s" is not a valid type of XRI')
1605
def __init__(self, add_xri=False, xri_type="i-name", **kwargs):
1606
"""Create an XRI validator.
1608
@param add_xri: Should the schema be added if not present? Officially
1610
@type add_xri: C{bool}
1611
@param xri_type: What type of XRI should be validated? Possible values:
1612
C{i-name} or C{i-number}.
1613
@type xri_type: C{str}
1616
self.add_xri = add_xri
1617
assert xri_type in ('i-name', 'i-number'), \
1618
('xri_type must be "i-name" or "i-number"')
1619
self.xri_type = xri_type
1620
super(XRI, self).__init__(**kwargs)
1622
def _to_python(self, value, state):
1623
"""Prepend the 'xri://' schema if necessary and then remove trailing
1625
value = value.strip()
1626
if self.add_xri and not value.startswith("xri://"):
1627
value = "xri://" + value
1630
def validate_python(self, value, state=None):
1633
@raise Invalid: If at least one of the following conditions in met:
1634
- C{value} is not a string.
1635
- The XRI is not a personal, organizational or network one.
1636
- The relevant validator (i-name or i-number) considers the XRI
1640
if not (isinstance(value, str) or isinstance(value, unicode)):
1641
raise Invalid(self.message("badType", state, type=str(type(value)),
1645
# Let's remove the schema, if any
1646
if value.startswith("xri://"):
1649
if not value[0] in ('@', '=') and not (self.xri_type == "i-number" \
1650
and value[0] == '!'):
1651
raise Invalid(self.message("noType", state), value, state)
1653
if self.xri_type == "i-name":
1654
self._validate_iname(value, state)
1656
self._validate_inumber(value, state)
1658
def _validate_iname(self, iname, state):
1659
"""Validate an i-name"""
1660
# The type is not required here:
1662
if ".." in iname or "--" in iname:
1663
raise Invalid(self.message("repeatedChar", state), iname, state)
1664
if self.iname_invalid_start.match(iname):
1665
raise Invalid(self.message("badInameStart", state), iname, state)
1666
if not self.iname_valid_pattern.match(iname) or "_" in iname:
1667
raise Invalid(self.message("badIname", state, iname=iname), iname,
1670
def _validate_inumber(self, inumber, state):
1671
"""Validate an i-number"""
1672
if not self.__class__.inumber_pattern.match(inumber):
1673
raise Invalid(self.message("badInumber", state, inumber=inumber,
1678
class OpenId(FancyValidator):
1683
>>> v = OpenId(add_schema=True)
1684
>>> v.to_python(' example.net ')
1685
'http://example.net'
1686
>>> v.to_python('@TurboGears')
1688
>>> w = OpenId(add_schema=False)
1689
>>> w.to_python(' example.net ')
1690
Traceback (most recent call last):
1692
Invalid: "example.net" is not a valid OpenId (it is neither an URL nor an XRI)
1693
>>> w.to_python('!!1000')
1695
>>> w.to_python('look@me.com')
1696
Traceback (most recent call last):
1698
Invalid: "look@me.com" is not a valid OpenId (it is neither an URL nor an XRI)
1703
'badId': _('"%(id)s" is not a valid OpenId (it is neither an URL nor an XRI)')
1706
def __init__(self, add_schema=False, **kwargs):
1707
"""Create an OpenId validator.
1709
@param add_schema: Should the schema be added if not present?
1710
@type add_schema: C{bool}
1713
self.url_validator = URL(add_http=add_schema)
1714
self.iname_validator = XRI(add_schema, xri_type="i-name")
1715
self.inumber_validator = XRI(add_schema, xri_type="i-number")
1717
def _to_python(self, value, state):
1718
value = value.strip()
1720
return self.url_validator.to_python(value, state)
1723
return self.iname_validator.to_python(value, state)
1726
return self.inumber_validator.to_python(value, state)
1729
# It's not an OpenId!
1730
raise Invalid(self.message("badId", state, id=value), value, state)
1732
def validate_python(self, value, state):
1733
self._to_python(value, state)
1736
def StateProvince(*kw, **kwargs):
1737
warnings.warn("please use formencode.national.USStateProvince", DeprecationWarning, stacklevel=2)
1738
from formencode.national import USStateProvince
1739
return USStateProvince(*kw, **kwargs)
1741
def PhoneNumber(*kw, **kwargs):
1742
warnings.warn("please use formencode.national.USPhoneNumber", DeprecationWarning, stacklevel=2)
1743
from formencode.national import USPhoneNumber
1744
return USPhoneNumber(*kw, **kwargs)
1746
def IPhoneNumberValidator(*kw, **kwargs):
1747
warnings.warn("please use formencode.national.InternationalPhoneNumber", DeprecationWarning, stacklevel=2)
1748
from formencode.national import InternationalPhoneNumber
1749
return InternationalPhoneNumber(*kw, **kwargs)
1751
class FieldStorageUploadConverter(FancyValidator):
1753
Handles cgi.FieldStorage instances that are file uploads.
1755
This doesn't do any conversion, but it can detect empty upload
1756
fields (which appear like normal fields, but have no filename when
1757
no upload was given).
1759
def _to_python(self, value, state=None):
1760
if isinstance(value, cgi.FieldStorage):
1761
if getattr(value, 'filename', None):
1763
raise Invalid('invalid', value, state)
1767
def is_empty(self, value):
1768
if isinstance(value, cgi.FieldStorage):
1769
return not bool(getattr(value, 'filename', None))
1770
return FancyValidator.is_empty(self, value)
1772
class FileUploadKeeper(FancyValidator):
1774
Takes two inputs (a dictionary with keys ``static`` and
1775
``upload``) and converts them into one value on the Python side (a
1776
dictionary with ``filename`` and ``content`` keys). The upload
1777
takes priority over the static value. The filename may be None if
1778
it can't be discovered.
1780
Handles uploads of both text and ``cgi.FieldStorage`` upload
1783
This is basically for use when you have an upload field, and you
1784
want to keep the upload around even if the rest of the form
1785
submission fails. When converting *back* to the form submission,
1786
there may be extra values ``'original_filename'`` and
1787
``'original_content'``, which may want to use in your form to show
1788
the user you still have their content around.
1790
To use this, make sure you are using variabledecode, then use
1793
<input type="file" name="myfield.upload">
1794
<input type="hidden" name="myfield.static">
1796
Then in your scheme::
1798
class MyScheme(Scheme):
1799
myfield = FileUploadKeeper()
1801
Note that big file uploads mean big hidden fields, and lots of
1802
bytes passed back and forth in the case of an error.
1805
upload_key = 'upload'
1806
static_key = 'static'
1808
def _to_python(self, value, state):
1809
upload = value.get(self.upload_key)
1810
static = value.get(self.static_key, '').strip()
1811
filename = content = None
1812
if isinstance(upload, cgi.FieldStorage):
1813
filename = upload.filename
1814
content = upload.value
1815
elif isinstance(upload, basestring) and upload:
1817
# @@: Should this encode upload if it is unicode?
1819
if not content and static:
1820
filename, content = static.split(None, 1)
1824
filename = filename.decode('base64')
1825
content = content.decode('base64')
1826
return {'filename': filename, 'content': content}
1828
def _from_python(self, value, state):
1829
filename = value.get('filename', '')
1830
content = value.get('content', '')
1831
if filename or content:
1832
result = self.pack_content(filename, content)
1833
return {self.upload_key: '',
1834
self.static_key: result,
1835
'original_filename': filename,
1836
'original_content': content}
1838
return {self.upload_key: '',
1839
self.static_key: ''}
1841
def pack_content(self, filename, content):
1842
enc_filename = self.base64encode(filename) or '-'
1843
enc_content = (content or '').encode('base64')
1844
result = '%s %s' % (enc_filename, enc_content)
1847
class DateConverter(FancyValidator):
1850
Validates and converts a string date, like mm/yy, dd/mm/yy,
1851
dd-mm-yy, etc. Using ``month_style`` you can support
1852
``'mm/dd/yyyy'`` or ``'dd/mm/yyyy'``. Only these two general
1853
styles are supported.
1855
Accepts English month names, also abbreviated. Returns value as a
1856
datetime object (you can get mx.DateTime objects if you use
1857
``datetime_module='mxDateTime'``). Two year dates are assumed to
1858
be within 1950-2020, with dates from 21-49 being ambiguous and
1861
Use accept_day=False if you just want a month/year (like for a
1862
credit card expiration date).
1866
>>> d = DateConverter()
1867
>>> d.to_python('12/3/09')
1868
datetime.date(2009, 12, 3)
1869
>>> d.to_python('12/3/2009')
1870
datetime.date(2009, 12, 3)
1871
>>> d.to_python('2/30/04')
1872
Traceback (most recent call last):
1874
Invalid: That month only has 29 days
1875
>>> d.to_python('13/2/05')
1876
Traceback (most recent call last):
1878
Invalid: Please enter a month from 1 to 12
1879
>>> d.to_python('1/1/200')
1880
Traceback (most recent call last):
1882
Invalid: Please enter a four-digit year after 1899
1884
If you change ``month_style`` you can get European-style dates::
1886
>>> d = DateConverter(month_style='dd/mm/yyyy')
1887
>>> date = d.to_python('12/3/09')
1889
datetime.date(2009, 3, 12)
1890
>>> d.from_python(date)
1893
## @@: accepts only US-style dates
1896
# also allowed: 'dd/mm/yyyy'
1897
month_style = 'mm/dd/yyyy'
1898
# Use 'datetime' to force the Python 2.3+ datetime module, or
1899
# 'mxDateTime' to force the mxDateTime module (None means use
1900
# datetime, or if not present mxDateTime)
1901
datetime_module = None
1903
_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)
1904
_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)
1907
'jan': 1, 'january': 1,
1908
'feb': 2, 'febuary': 2,
1909
'mar': 3, 'march': 3,
1910
'apr': 4, 'april': 4,
1912
'jun': 6, 'june': 6,
1913
'jul': 7, 'july': 7,
1914
'aug': 8, 'august': 8,
1915
'sep': 9, 'sept': 9, 'september': 9,
1916
'oct': 10, 'october': 10,
1917
'nov': 11, 'november': 11,
1918
'dec': 12, 'december': 12,
1921
## @@: Feb. should be leap-year aware (but mxDateTime does catch that)
1923
1: 31, 2: 29, 3: 31, 4: 30, 5: 31, 6: 30, 7: 31, 8: 31,
1924
9: 30, 10: 31, 11: 30, 12: 31}
1927
'badFormat': _('Please enter the date in the form %(format)s'),
1928
'monthRange': _('Please enter a month from 1 to 12'),
1929
'invalidDay': _('Please enter a valid day'),
1930
'dayRange': _('That month only has %(days)i days'),
1931
'invalidDate': _('That is not a valid day (%(exception)s)'),
1932
'unknownMonthName': _("Unknown month name: %(month)s"),
1933
'invalidYear': _('Please enter a number for the year'),
1934
'fourDigitYear': _('Please enter a four-digit year after 1899'),
1935
'wrongFormat': _('Please enter the date in the form %(format)s'),
1938
def __init__(self, *args, **kw):
1939
super(DateConverter, self).__init__(*args, **kw)
1940
if not self.month_style in ('dd/mm/yyyy', 'mm/dd/yyyy'):
1941
raise TypeError("Bad month_style: %r" % self.month_style)
1943
def _to_python(self, value, state):
1945
return self.convert_day(value, state)
1947
return self.convert_month(value, state)
1949
def convert_day(self, value, state):
1950
self.assert_string(value, state)
1951
match = self._day_date_re.search(value)
1953
raise Invalid(self.message('badFormat', state,
1954
format=self.month_style),
1956
day = int(match.group(1))
1958
month = int(match.group(2))
1960
month = self.make_month(match.group(2), state)
1962
if self.month_style == 'mm/dd/yyyy':
1963
month, day = day, month
1964
year = self.make_year(match.group(3), state)
1965
if month > 12 or month < 1:
1966
raise Invalid(self.message('monthRange', state),
1969
raise Invalid(self.message('invalidDay', state),
1971
if self._monthDays[month] < day:
1972
raise Invalid(self.message('dayRange', state,
1973
days=self._monthDays[month]),
1975
dt_mod = import_datetime(self.datetime_module)
1977
return datetime_makedate(dt_mod, year, month, day)
1978
except ValueError, v:
1979
raise Invalid(self.message('invalidDate', state,
1983
def make_month(self, value, state):
1987
value = value.lower().strip()
1988
if self._month_names.has_key(value):
1989
return self._month_names[value]
1991
raise Invalid(self.message('unknownMonthName', state,
1995
def make_year(self, year, state):
1999
raise Invalid(self.message('invalidYear', state),
2003
if year >= 50 and year < 100:
2005
if (year > 20 and year < 50) or (year>99 and year<1900):
2006
raise Invalid(self.message('fourDigitYear', state),
2010
def convert_month(self, value, state):
2011
match = self._month_date_re.search(value)
2013
raise Invalid(self.message('wrongFormat', state,
2016
month = self.make_month(match.group(1), state)
2017
year = self.make_year(match.group(2), state)
2018
if month > 12 or month < 1:
2019
raise Invalid(self.message('monthRange', state),
2021
dt_mod = import_datetime(self.datetime_module)
2022
return datetime_makedate(dt_mod, year, month, 1)
2024
def _from_python(self, value, state):
2025
if self.if_empty is not NoDefault and not value:
2028
return self.unconvert_day(value, state)
2030
return self.unconvert_month(value, state)
2032
def unconvert_day(self, value, state):
2033
# @@ ib: double-check, improve
2034
if self.month_style == 'mm/dd/yyyy':
2035
return value.strftime("%m/%d/%Y")
2037
return value.strftime("%d/%m/%Y")
2039
def unconvert_month(self, value, state):
2040
# @@ ib: double-check, improve
2041
return value.strftime("%m/%Y")
2043
class TimeConverter(FancyValidator):
2046
Converts times in the format HH:MM:SSampm to (h, m, s).
2047
Seconds are optional.
2049
For ampm, set use_ampm = True. For seconds, use_seconds = True.
2050
Use 'optional' for either of these to make them optional.
2054
>>> tim = TimeConverter()
2055
>>> tim.to_python('8:30')
2057
>>> tim.to_python('20:30')
2059
>>> tim.to_python('30:00')
2060
Traceback (most recent call last):
2062
Invalid: You must enter an hour in the range 0-23
2063
>>> tim.to_python('13:00pm')
2064
Traceback (most recent call last):
2066
Invalid: You must enter an hour in the range 1-12
2067
>>> tim.to_python('12:-1')
2068
Traceback (most recent call last):
2070
Invalid: You must enter a minute in the range 0-59
2071
>>> tim.to_python('12:02pm')
2073
>>> tim.to_python('12:02am')
2075
>>> tim.to_python('1:00PM')
2077
>>> tim.from_python((13, 0))
2079
>>> tim2 = tim(use_ampm=True, use_seconds=False)
2080
>>> tim2.from_python((13, 0))
2082
>>> tim2.from_python((0, 0))
2084
>>> tim2.from_python((12, 0))
2087
Examples with ``datetime.time``::
2089
>>> v = TimeConverter(use_datetime=True)
2090
>>> a = v.to_python('18:00')
2092
datetime.time(18, 0)
2093
>>> b = v.to_python('30:00')
2094
Traceback (most recent call last):
2096
Invalid: You must enter an hour in the range 0-23
2097
>>> v2 = TimeConverter(prefer_ampm=True, use_datetime=True)
2098
>>> v2.from_python(a)
2100
>>> v3 = TimeConverter(prefer_ampm=True,
2101
... use_seconds=False, use_datetime=True)
2102
>>> a = v3.to_python('18:00')
2104
datetime.time(18, 0)
2105
>>> v3.from_python(a)
2107
>>> a = v3.to_python('18:00:00')
2108
Traceback (most recent call last):
2110
Invalid: You may not enter seconds
2113
use_ampm = 'optional'
2115
use_seconds = 'optional'
2116
use_datetime = False
2117
# This can be set to make it prefer mxDateTime:
2118
datetime_module = None
2121
'noAMPM': _('You must indicate AM or PM'),
2122
'tooManyColon': _('There are too many :\'s'),
2123
'noSeconds': _('You may not enter seconds'),
2124
'secondsRequired': _('You must enter seconds'),
2125
'minutesRequired': _('You must enter minutes (after a :)'),
2126
'badNumber': _('The %(part)s value you gave is not a number: %(number)r'),
2127
'badHour': _('You must enter an hour in the range %(range)s'),
2128
'badMinute': _('You must enter a minute in the range 0-59'),
2129
'badSecond': _('You must enter a second in the range 0-59'),
2132
def _to_python(self, value, state):
2133
result = self._to_python_tuple(value, state)
2134
if self.use_datetime:
2135
dt_mod = import_datetime(self.datetime_module)
2136
time_class = datetime_time(dt_mod)
2137
return time_class(*result)
2141
def _to_python_tuple(self, value, state):
2142
time = value.strip()
2143
explicit_ampm = False
2145
last_two = time[-2:].lower()
2146
if last_two not in ('am', 'pm'):
2147
if self.use_ampm != 'optional':
2149
self.message('noAMPM', state),
2154
explicit_ampm = True
2155
if last_two == 'pm':
2162
parts = time.split(':')
2165
self.message('tooManyColon', state),
2167
if len(parts) == 3 and not self.use_seconds:
2169
self.message('noSeconds', state),
2172
and self.use_seconds
2173
and self.use_seconds != 'optional'):
2175
self.message('secondsRequired', state),
2179
self.message('minutesRequired', state),
2182
hour = int(parts[0])
2185
self.message('badNumber', state, number=parts[0], part='hour'),
2188
if hour > 12 or hour < 1:
2190
self.message('badHour', state, number=hour, range='1-12'),
2192
if hour == 12 and offset == 12:
2195
elif hour == 12 and offset == 0:
2201
if hour > 23 or hour < 0:
2203
self.message('badHour', state,
2204
number=hour, range='0-23'),
2207
minute = int(parts[1])
2210
self.message('badNumber', state,
2211
number=parts[1], part='minute'),
2213
if minute > 59 or minute < 0:
2215
self.message('badMinute', state, number=minute),
2219
second = int(parts[2])
2222
self.message('badNumber', state,
2223
number=parts[2], part='second'))
2224
if second > 59 or second < 0:
2226
self.message('badSecond', state, number=second),
2231
return (hour, minute)
2233
return (hour, minute, second)
2235
def _from_python(self, value, state):
2236
if isinstance(value, (str, unicode)):
2238
if hasattr(value, 'hour'):
2239
hour, minute = value.hour, value.minute
2240
second = value.second
2241
elif len(value) == 3:
2242
hour, minute, second = value
2243
elif len(value) == 2:
2244
hour, minute = value
2247
if ((self.use_ampm == 'optional' and self.prefer_ampm)
2248
or (self.use_ampm and self.use_ampm != 'optional')):
2257
if self.use_seconds:
2258
return '%i:%02i:%02i%s' % (hour, minute, second, ampm)
2260
return '%i:%02i%s' % (hour, minute, ampm)
2262
def PostalCode(*kw, **kwargs):
2263
warnings.warn("please use formencode.national.USPostalCode", DeprecationWarning, stacklevel=2)
2264
from formencode.national import USPostalCode
2265
return USPostalCode(*kw, **kwargs)
2267
class StripField(FancyValidator):
2270
Take a field from a dictionary, removing the key from the
2273
``name`` is the key. The field value and a new copy of the
2274
dictionary with that field removed are returned.
2276
>>> StripField('test').to_python({'a': 1, 'test': 2})
2278
>>> StripField('test').to_python({})
2279
Traceback (most recent call last):
2281
Invalid: The name 'test' is missing
2285
__unpackargs__ = ('name',)
2288
'missing': _('The name %(name)s is missing'),
2291
def _to_python(self, valueDict, state):
2292
v = valueDict.copy()
2294
field = v[self.name]
2297
raise Invalid(self.message('missing', state,
2298
name=repr(self.name)),
2302
def is_empty(self, value):
2303
## Empty dictionaries don't really apply here
2306
class StringBool(FancyValidator):
2307
# Originally from TurboGears
2309
Converts a string to a boolean.
2311
Values like 'true' and 'false' are considered True and False,
2312
respectively; anything in ``true_values`` is true, anything in
2313
``false_values`` is false, case-insensitive). The first item of
2314
those lists is considered the preferred form.
2318
>>> s = StringBoolean()
2319
>>> s.to_python('yes'), s.to_python('no')
2321
>>> s.to_python(1), s.to_python('N')
2323
>>> s.to_python('ye')
2324
Traceback (most recent call last):
2326
Invalid: Value should be 'true' or 'false'
2329
true_values = ['true', 't', 'yes', 'y', 'on', '1']
2330
false_values = ['false', 'f', 'no', 'n', 'off', '0']
2332
messages = { "string" : _("Value should be %(true)r or %(false)r") }
2334
def _to_python(self, value, state):
2335
if isinstance(value, (str, unicode)):
2336
value = value.strip().lower()
2337
if value in self.true_values:
2339
if not value or value in self.false_values:
2341
raise Invalid(self.message("string", state,
2342
true=self.true_values[0],
2343
false=self.false_values[0]),
2347
def _from_python(self, value, state):
2349
return self.true_values[0]
2351
return self.false_values[0]
2354
StringBoolean = StringBool
2356
class SignedString(FancyValidator):
2359
Encodes a string into a signed string, and base64 encodes both the
2360
signature string and a random nonce.
2362
It is up to you to provide a secret, and to keep the secret handy
2367
'malformed': _('Value does not contain a signature'),
2368
'badsig': _('Signature is not correct'),
2374
def _to_python(self, value, state):
2378
from hashlib import sha1
2380
from sha import sha as sha1
2381
assert self.secret is not None, (
2382
"You must give a secret")
2383
parts = value.split(None, 1)
2384
if not parts or len(parts) == 1:
2385
raise Invalid(self.message('malformed', state),
2388
sig = sig.decode('base64')
2389
rest = rest.decode('base64')
2390
nonce = rest[:self.nonce_length]
2391
rest = rest[self.nonce_length:]
2392
expected = sha1(str(self.secret)+nonce+rest).digest()
2394
raise Invalid(self.message('badsig', state),
2398
def _from_python(self, value, state):
2402
from hashlib import sha1
2404
from sha import sha as sha1
2405
nonce = self.make_nonce()
2407
digest = sha1(self.secret+nonce+value).digest()
2408
return self.encode(digest)+' '+self.encode(nonce+value)
2410
def encode(self, value):
2411
return value.encode('base64').strip().replace('\n', '')
2413
def make_nonce(self):
2418
chr(random.randrange(256))
2419
for i in range(self.nonce_length)])
2421
class IPAddress(FancyValidator):
2423
Formencode validator to check whether a string is a correct IP address
2427
>>> ip = IPAddress()
2428
>>> ip.to_python('127.0.0.1')
2430
>>> ip.to_python('299.0.0.1')
2431
Traceback (most recent call last):
2433
Invalid: The octets must be within the range of 0-255 (not '299')
2434
>>> ip.to_python('192.168.0.1/1')
2435
Traceback (most recent call last):
2437
Invalid: Please enter a valid IP address (a.b.c.d)
2438
>>> ip.to_python('asdf')
2439
Traceback (most recent call last):
2441
Invalid: Please enter a valid IP address (a.b.c.d)
2444
'bad_format' : u'Please enter a valid IP address (a.b.c.d)',
2445
'illegal_octets' : u'The octets must be within the range of 0-255 (not %(octet)r)',
2448
def validate_python(self, value, state):
2450
octets = value.split('.')
2453
if len(octets) != 4:
2454
raise Invalid(self.message("bad_format", state, value=value), value, state)
2457
for octet in octets:
2458
if int(octet) < 0 or int(octet) > 255:
2459
raise Invalid(self.message("illegal_octets", state, octet=octet), value, state)
2461
# Splitting faild: wrong syntax
2463
raise Invalid(self.message("bad_format", state), value, state)
2465
class CIDR(IPAddress):
2467
Formencode validator to check whether a string is in correct CIDR
2468
notation (IP address, or IP address plus /mask)
2473
>>> cidr.to_python('127.0.0.1')
2475
>>> cidr.to_python('299.0.0.1')
2476
Traceback (most recent call last):
2478
Invalid: The octets must be within the range of 0-255 (not '299')
2479
>>> cidr.to_python('192.168.0.1/1')
2480
Traceback (most recent call last):
2482
Invalid: The network size (bits) must be within the range of 8-32 (not '1')
2483
>>> cidr.to_python('asdf')
2484
Traceback (most recent call last):
2486
Invalid: Please enter a valid IP address (a.b.c.d) or IP network (a.b.c.d/e)
2488
messages = dict(IPAddress._messages,
2489
bad_format = u'Please enter a valid IP address (a.b.c.d) or IP network (a.b.c.d/e)',
2490
illegal_bits = u'The network size (bits) must be within the range of 8-32 (not %(bits)r)',
2493
def validate_python(self, value, state):
2495
# Split into octets and bits
2496
if '/' in value: # a.b.c.d/e
2497
addr, bits = value.split('/')
2499
addr, bits = value, 32
2501
# Use IPAddress validator to validate the IP part
2502
IPAddress.validate_python(self, addr, state)
2504
# Bits (netmask) correct?
2505
if int(bits) < 8 or int(bits) > 32:
2506
raise Invalid(self.message("illegal_bits", state, bits=bits), value, state)
2508
# Splitting faild: wrong syntax
2510
raise Invalid(self.message("bad_format", state), value, state)
2512
class MACAddress(FancyValidator):
2514
Formencode validator to check whether a string is a correct hardware
2519
>>> mac = MACAddress()
2520
>>> mac.to_python('aa:bb:cc:dd:ee:ff')
2522
>>> mac.to_python('aa:bb:cc:dd:ee:ff:e')
2523
Traceback (most recent call last):
2525
Invalid: A MAC address must contain 12 digits and A-F; the value you gave has 13 characters
2526
>>> mac.to_python('aa:bb:cc:dd:ee:fx')
2527
Traceback (most recent call last):
2529
Invalid: MAC addresses may only contain 0-9 and A-F (and optionally :), not 'x'
2530
>>> MACAddress(add_colons=True).to_python('aabbccddeeff')
2535
valid_characters = '0123456789abcdefABCDEF'
2539
'bad_length': _(u'A MAC address must contain 12 digits and A-F; the value you gave has %(length)s characters'),
2540
'bad_character': _(u'MAC addresses may only contain 0-9 and A-F (and optionally :), not %(char)r'),
2543
def _to_python(self, value, state):
2544
address = value.replace(':','') # remove colons
2545
address = address.lower()
2546
if len(address)!=12:
2547
raise Invalid(self.message("bad_length", state, length=len(address)), address, state)
2548
for char in address:
2549
if char not in self.valid_characters:
2550
raise Invalid(self.message("bad_character", state, char=char), address, state)
2552
address = '%s:%s:%s:%s:%s:%s' % (
2553
address[0:2], address[2:4], address[4:6],
2554
address[6:8], address[8:10], address[10:12])
2557
_from_python = _to_python
2559
class FormValidator(FancyValidator):
2561
A FormValidator is something that can be chained with a
2562
Schema. Unlike normal chaining the FormValidator can
2563
validate forms that aren't entirely valid.
2565
The important method is .validate(), of course. It gets passed a
2566
dictionary of the (processed) values from the form. If you have
2567
.validate_partial_form set to True, then it will get the incomplete
2568
values as well -- use .has_key() to test if the field was able to
2569
process any particular field.
2571
Anyway, .validate() should return a string or a dictionary. If a
2572
string, it's an error message that applies to the whole form. If
2573
not, then it should be a dictionary of fieldName: errorMessage.
2574
The special key "form" is the error message for the form as a whole
2575
(i.e., a string is equivalent to {"form": string}).
2577
Return None on no errors.
2580
validate_partial_form = False
2582
validate_partial_python = None
2583
validate_partial_other = None
2585
def is_empty(self, value):
2588
class RequireIfMissing(FormValidator):
2591
This requires one field based on another field being present or
2592
missing. This is applied to a form, not an individual field
2593
(usually using a Schema's ``pre_validators`` or
2594
``chained_validators``).
2596
If you provide a ``missing`` value (a string key name) then if
2597
that field is missing the field must be entered. This gives you
2598
an either/or situation.
2600
If you provide a ``present`` value (another string key name) then
2601
if that field is present, the required field must also be present.
2605
>>> from formencode import validators
2606
>>> v = validators.RequireIfPresent('phone_type',
2607
... present='phone')
2608
>>> v.to_python({'phone_type':'', 'phone':'510 420 4577'})
2609
Traceback (most recent call last):
2611
Invalid: You must give a value for phone_type
2612
>>> v.to_python({'phone': ''})
2615
Note that if you have a validator on the optionally-required
2616
field, you should probably use ``if_missing=None``. This way you
2617
won't get an error from the Schema about a missing value. For example::
2619
class PhoneInput(Schema):
2620
phone = PhoneNumber()
2621
phone_type = String(if_missing=None)
2622
chained_validators = [RequireifPresent('phone_type', present='phone')]
2625
# Field that potentially is required:
2627
# If this field is missing, then it is required:
2629
# If this field is present, then it is required:
2631
__unpackargs__ = ('required',)
2633
def _to_python(self, value_dict, state):
2635
if self.missing and not value_dict.get(self.missing):
2637
if self.present and value_dict.get(self.present):
2639
if is_required and not value_dict.get(self.required):
2640
raise Invalid('You must give a value for %s' % self.required,
2642
error_dict={self.required: Invalid(self.message(
2643
'empty', state), value, state)})
2646
RequireIfPresent = RequireIfMissing
2648
class FieldsMatch(FormValidator):
2651
Tests that the given fields match, i.e., are identical. Useful
2652
for password+confirmation fields. Pass the list of field names in
2657
>>> f = FieldsMatch('pass', 'conf')
2658
>>> f.to_python({'pass': 'xx', 'conf': 'xx'})
2659
{'conf': 'xx', 'pass': 'xx'}
2660
>>> f.to_python({'pass': 'xx', 'conf': 'yy'})
2661
Traceback (most recent call last):
2663
Invalid: conf: Fields do not match
2668
validate_partial_form = True
2669
__unpackargs__ = ('*', 'field_names')
2672
'invalid': _("Fields do not match (should be %(match)s)"),
2673
'invalidNoMatch': _("Fields do not match"),
2674
'notDict': _("Fields should be a dictionary"),
2677
def __init__(self, *args, **kw):
2678
super(FormValidator, self).__init__(*args, **kw)
2679
if len(self.field_names) < 2:
2680
raise TypeError("FieldsMatch() requires at least two field names")
2682
def validate_partial(self, field_dict, state):
2683
for name in self.field_names:
2684
if not field_dict.has_key(name):
2686
self.validate_python(field_dict, state)
2688
def validate_python(self, field_dict, state):
2690
ref = field_dict[self.field_names[0]]
2692
# Generally because field_dict isn't a dict
2693
raise Invalid(self.message('notDict', state), field_dict, state)
2697
for name in self.field_names[1:]:
2698
if field_dict.get(name, '') != ref:
2700
errors[name] = self.message('invalid', state,
2703
errors[name] = self.message('invalidNoMatch', state)
2705
error_list = errors.items()
2707
error_message = '<br>\n'.join(
2708
['%s: %s' % (name, value) for name, value in error_list])
2709
raise Invalid(error_message,
2713
class CreditCardValidator(FormValidator):
2715
Checks that credit card numbers are valid (if not real).
2717
You pass in the name of the field that has the credit card
2718
type and the field with the credit card number. The credit
2719
card type should be one of "visa", "mastercard", "amex",
2720
"dinersclub", "discover", "jcb".
2722
You must check the expiration date yourself (there is no
2723
relation between CC number/types and expiration dates).
2727
>>> cc = CreditCardValidator()
2728
>>> cc.to_python({'ccType': 'visa', 'ccNumber': '4111111111111111'})
2729
{'ccNumber': '4111111111111111', 'ccType': 'visa'}
2730
>>> cc.to_python({'ccType': 'visa', 'ccNumber': '411111111111111'})
2731
Traceback (most recent call last):
2733
Invalid: ccNumber: You did not enter a valid number of digits
2734
>>> cc.to_python({'ccType': 'visa', 'ccNumber': '411111111111112'})
2735
Traceback (most recent call last):
2737
Invalid: ccNumber: You did not enter a valid number of digits
2738
>>> cc().to_python({})
2739
Traceback (most recent call last):
2741
Invalid: The field ccType is missing
2744
validate_partial_form = True
2746
cc_type_field = 'ccType'
2747
cc_number_field = 'ccNumber'
2748
__unpackargs__ = ('cc_type_field', 'cc_number_field')
2751
'notANumber': _("Please enter only the number, no other characters"),
2752
'badLength': _("You did not enter a valid number of digits"),
2753
'invalidNumber': _("That number is not valid"),
2754
'missing_key': _("The field %(key)s is missing"),
2757
def validate_partial(self, field_dict, state):
2758
if not field_dict.get(self.cc_type_field, None) \
2759
or not field_dict.get(self.cc_number_field, None):
2761
self.validate_python(field_dict, state)
2763
def validate_python(self, field_dict, state):
2764
errors = self._validateReturn(field_dict, state)
2766
error_list = errors.items()
2769
'<br>\n'.join(["%s: %s" % (name, value)
2770
for name, value in error_list]),
2771
field_dict, state, error_dict=errors)
2773
def _validateReturn(self, field_dict, state):
2774
for field in self.cc_type_field, self.cc_number_field:
2775
if field not in field_dict:
2777
self.message('missing_key', state, key=field), value, state)
2778
ccType = field_dict[self.cc_type_field].lower().strip()
2779
number = field_dict[self.cc_number_field].strip()
2780
number = number.replace(' ', '')
2781
number = number.replace('-', '')
2785
return {self.cc_number_field: self.message('notANumber', state)}
2787
assert self._cardInfo.has_key(ccType), (
2788
"I can't validate that type of credit card")
2791
for prefix, length in self._cardInfo[ccType]:
2792
if len(number) == length:
2794
if (len(number) == length
2795
and number.startswith(prefix)):
2799
return {self.cc_number_field: self.message('badLength', state)}
2801
return {self.cc_number_field: self.message('invalidNumber', state)}
2803
if not self._validateMod10(number):
2804
return {self.cc_number_field: self.message('invalidNumber', state)}
2807
def _validateMod10(self, s):
2809
This code by Sean Reifschneider, of tummy.com
2813
for i in range(len(s) - 1, -1, -1):
2814
for c in str((double + 1) * int(s[i])):
2816
double = (double + 1) % 2
2817
return((sum % 10) == 0)
2822
"mastercard": [('51', 16),
2827
"discover": [('6011', 16)],
2828
"amex": [('34', 15),
2830
"dinersclub": [('300', 14),
2843
class CreditCardExpires(FormValidator):
2845
Checks that credit card expiration date is valid relative to
2848
You pass in the name of the field that has the credit card
2849
expiration month and the field with the credit card expiration
2854
>>> ed = CreditCardExpires()
2855
>>> ed.to_python({'ccExpiresMonth': '11', 'ccExpiresYear': '2250'})
2856
{'ccExpiresYear': '2250', 'ccExpiresMonth': '11'}
2857
>>> ed.to_python({'ccExpiresMonth': '10', 'ccExpiresYear': '2005'})
2858
Traceback (most recent call last):
2860
Invalid: ccExpiresMonth: Invalid Expiration Date<br>
2861
ccExpiresYear: Invalid Expiration Date
2864
validate_partial_form = True
2866
cc_expires_month_field = 'ccExpiresMonth'
2867
cc_expires_year_field = 'ccExpiresYear'
2868
__unpackargs__ = ('cc_expires_month_field', 'cc_expires_year_field')
2870
datetime_module = None
2873
'notANumber': _("Please enter numbers only for month and year"),
2874
'invalidNumber': _("Invalid Expiration Date"),
2877
def validate_partial(self, field_dict, state):
2878
if not field_dict.get(self.cc_expires_month_field, None) \
2879
or not field_dict.get(self.cc_expires_year_field, None):
2881
self.validate_python(field_dict, state)
2883
def validate_python(self, field_dict, state):
2884
errors = self._validateReturn(field_dict, state)
2886
error_list = errors.items()
2889
'<br>\n'.join(["%s: %s" % (name, value)
2890
for name, value in error_list]),
2891
field_dict, state, error_dict=errors)
2893
def _validateReturn(self, field_dict, state):
2894
ccExpiresMonth = str(field_dict[self.cc_expires_month_field]).strip()
2895
ccExpiresYear = str(field_dict[self.cc_expires_year_field]).strip()
2898
ccExpiresMonth = int(ccExpiresMonth)
2899
ccExpiresYear = int(ccExpiresYear)
2900
dt_mod = import_datetime(self.datetime_module)
2901
now = datetime_now(dt_mod)
2902
today = datetime_makedate(dt_mod, now.year, now.month, now.day)
2903
next_month = (ccExpiresMonth % 12) + 1
2905
next_month_year = ccExpiresYear + 1
2907
next_month_year = ccExpiresYear
2908
expires_date = datetime_makedate(dt_mod, next_month_year, next_month, 1)
2909
assert expires_date > today
2911
return {self.cc_expires_month_field: self.message('notANumber', state),
2912
self.cc_expires_year_field: self.message('notANumber', state)}
2913
except AssertionError:
2914
return {self.cc_expires_month_field: self.message('invalidNumber', state),
2915
self.cc_expires_year_field: self.message('invalidNumber', state)}
2917
class CreditCardSecurityCode(FormValidator):
2919
Checks that credit card security code has the correct number
2920
of digits for the given credit card type.
2922
You pass in the name of the field that has the credit card
2923
type and the field with the credit card security code.
2927
>>> code = CreditCardSecurityCode()
2928
>>> code.to_python({'ccType': 'visa', 'ccCode': '111'})
2929
{'ccType': 'visa', 'ccCode': '111'}
2930
>>> code.to_python({'ccType': 'visa', 'ccCode': '1111'})
2931
Traceback (most recent call last):
2933
Invalid: ccCode: Invalid credit card security code length
2936
validate_partial_form = True
2938
cc_type_field = 'ccType'
2939
cc_code_field = 'ccCode'
2940
__unpackargs__ = ('cc_type_field', 'cc_code_field')
2943
'notANumber': _("Please enter numbers only for credit card security code"),
2944
'badLength': _("Invalid credit card security code length"),
2947
def validate_partial(self, field_dict, state):
2948
if not field_dict.get(self.cc_type_field, None) \
2949
or not field_dict.get(self.cc_code_field, None):
2951
self.validate_python(field_dict, state)
2953
def validate_python(self, field_dict, state):
2954
errors = self._validateReturn(field_dict, state)
2956
error_list = errors.items()
2959
'<br>\n'.join(["%s: %s" % (name, value)
2960
for name, value in error_list]),
2961
field_dict, state, error_dict=errors)
2963
def _validateReturn(self, field_dict, state):
2964
ccType = str(field_dict[self.cc_type_field]).strip()
2965
ccCode = str(field_dict[self.cc_code_field]).strip()
2970
return {self.cc_code_field: self.message('notANumber', state)}
2972
length = self._cardInfo[ccType]
2974
if len(ccCode) == length:
2977
return {self.cc_code_field: self.message('badLength', state)}
2979
# key = credit card type
2980
# value = length of security code
2989
__all__ = ['Invalid']
2990
for name, value in globals().items():
2991
if isinstance(value, type) and issubclass(value, Validator):
2992
__all__.append(name)