formencode.validators
Covered: 299 lines
Missed: 2195 lines
Skipped 499 lines
Percent: 11 %
   3
"""
   4
Validator/Converters for use with FormEncode.
   5
"""
   7
import locale
   8
import warnings
   9
import re
  10
DateTime = None
  11
httplib = None
  12
urlparse = None
  13
import socket
  14
from interfaces import *
  15
from api import *
  16
sha1 = random = None
  18
filters = warnings.filters[:]
  19
warnings.simplefilter('ignore', DeprecationWarning)
  20
import sets
  21
warnings.filters = filters
  22
try:
  23
    set
  24
except NameError:
  25
    set = sets.Set
  27
import cgi
  29
import fieldstorage
  31
try:
  32
    import DNS
  33
    DNS.DiscoverNameServers()
  34
    have_dns=True
  35
except ImportError:
  36
    have_dns=False
  38
True, False = (1==1), (0==1)
  45
_ = lambda s: s
  53
datetime_module = None
  54
mxDateTime_module = None
  56
def import_datetime(module_type):
  57
    global datetime_module, mxDateTime_module
  58
    if module_type is None:
  59
        try:
  60
            if datetime_module is None:
  61
                import datetime as datetime_module
  62
            return datetime_module
  63
        except ImportError:
  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
  74
    else:
  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()
  82
    else:
  83
        return module.now()
  85
def datetime_makedate(module, year, month, day):
  86
    if module.__name__ == 'datetime':
  87
        return module.date(year, month, day)
  88
    else:
  89
        try:
  90
            return module.DateTime(year, month, day)
  91
        except module.RangeError, e:
  92
            raise ValueError(str(e))
  96
def datetime_time(module):
  97
    if module.__name__ == 'datetime':
  98
        return module.time
 101
def datetime_isotime(module):
 102
    if module.__name__ == 'datetime':
 103
        return module.time.isoformat
 111
class ConfirmType(FancyValidator):
 113
    """
 114
    Confirms that the input/output is of the proper type.
 116
    Uses the parameters:
 118
    subclass:
 119
        The class or a tuple of classes; the item must be an instance
 120
        of the class or a subclass.
 121
    type:
 122
        A type or tuple of types (or classes); the item must be of
 123
        the exact class or type.  Subclasses are not allowed.
 125
    Examples::
 127
        >>> cint = ConfirmType(subclass=int)
 128
        >>> cint.to_python(True)
 129
        True
 130
        >>> cint.to_python('1')
 131
        Traceback (most recent call last):
 132
            ...
 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)
 136
        (1.0, 1.0)
 137
        >>> cintfloat.to_python(1), cintfloat.from_python(1)
 138
        (1, 1)
 139
        >>> cintfloat.to_python(None)
 140
        Traceback (most recent call last):
 141
            ...
 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):
 146
            ...
 147
        Invalid: True must be of the type <type 'int'>
 148
    """
 150
    subclass = None
 151
    type = None
 153
    messages = {
 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"),
 158
        }
 160
    def __init__(self, *args, **kw):
 161
        FancyValidator.__init__(self, *args, **kw)
 162
        if self.subclass:
 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
 168
        if self.type:
 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])
 180
            else:
 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):
 187
        for t in self.type:
 188
            if type(value) is t:
 189
                break
 190
        else:
 191
            if len(self.type) == 1:
 192
                msg = self.message('type', state, object=value,
 193
                                   type=self.type[0])
 194
            else:
 195
                msg = self.message('inType', state, object=value,
 196
                                   typeList=', '.join(map(str, self.type)))
 197
            raise Invalid(msg, value, state)
 198
        return value
 200
    def is_empty(self, value):
 201
        return False
 203
class Wrapper(FancyValidator):
 205
    """
 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.
 216
    Examples::
 218
        >>> def downcase(v):
 219
        ...     return v.lower()
 220
        >>> wrap = Wrapper(to_python=downcase)
 221
        >>> wrap.to_python('This')
 222
        'this'
 223
        >>> wrap.from_python('This')
 224
        'This'
 225
        >>> wrap2 = Wrapper(from_python=downcase)
 226
        >>> wrap2.from_python('This')
 227
        'this'
 228
        >>> wrap2.from_python(1)
 229
        Traceback (most recent call last):
 230
          ...
 231
        Invalid: 'int' object has no attribute 'lower'
 232
        >>> wrap3 = Wrapper(validate_python=int)
 233
        >>> wrap3.to_python('1')
 234
        '1'
 235
        >>> wrap3.to_python('a') # doctest: +ELLIPSIS
 236
        Traceback (most recent call last):
 237
          ...
 238
        Invalid: invalid literal for int()...
 239
    """
 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',
 248
                  'validate_other']:
 249
            if kw.has_key(n):
 250
                kw['func_%s' % n] = kw[n]
 251
                del 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):
 259
        if not func:
 260
            return None
 261
        def result(value, state, func=func):
 262
            try:
 263
                return func(value)
 264
            except Exception, e:
 265
                raise Invalid(str(e), {}, value, state)
 266
        return result
 268
class Constant(FancyValidator):
 270
    """
 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(
 279
                               ValidEmailAddress(),
 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.
 286
    Examples::
 288
        >>> Constant('X').to_python('y')
 289
        'X'
 290
    """
 292
    __unpackargs__ = ('value',)
 294
    def _to_python(self, value, state):
 295
        return self.value
 297
    _from_python = _to_python
 303
class MaxLength(FancyValidator):
 305
    """
 306
    Invalid if the value is longer than `maxLength`.  Uses len(),
 307
    so it can work for strings, lists, or anything with length.
 309
    Examples::
 311
        >>> max5 = MaxLength(5)
 312
        >>> max5.to_python('12345')
 313
        '12345'
 314
        >>> max5.from_python('12345')
 315
        '12345'
 316
        >>> max5.to_python('123456')
 317
        Traceback (most recent call last):
 318
          ...
 319
        Invalid: Enter a value less than 5 characters long
 320
        >>> max5(accept_python=False).from_python('123456')
 321
        Traceback (most recent call last):
 322
          ...
 323
        Invalid: Enter a value less than 5 characters long
 324
        >>> max5.to_python([1, 2, 3])
 325
        [1, 2, 3]
 326
        >>> max5.to_python([1, 2, 3, 4, 5, 6])
 327
        Traceback (most recent call last):
 328
          ...
 329
        Invalid: Enter a value less than 5 characters long
 330
        >>> max5.to_python(5)
 331
        Traceback (most recent call last):
 332
          ...
 333
        Invalid: Invalid value (value with length expected)
 334
    """
 336
    __unpackargs__ = ('maxLength',)
 337
    messages = {
 338
        'tooLong': _("Enter a value less than %(maxLength)i characters long"),
 339
        'invalid': _("Invalid value (value with length expected)"),
 340
        }
 342
    def validate_python(self, value, state):
 343
        try:
 344
            if value and \
 345
               len(value) > self.maxLength:
 346
                raise Invalid(self.message('tooLong', state,
 347
                                           maxLength=self.maxLength),
 348
                              value, state)
 349
            else:
 350
                return None
 351
        except TypeError:
 352
            raise Invalid(self.message('invalid', state),
 353
                          value, state)
 355
class MinLength(FancyValidator):
 357
    """
 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.
 363
    Examples::
 365
        >>> min5 = MinLength(5)
 366
        >>> min5.to_python('12345')
 367
        '12345'
 368
        >>> min5.from_python('12345')
 369
        '12345'
 370
        >>> min5.to_python('1234')
 371
        Traceback (most recent call last):
 372
          ...
 373
        Invalid: Enter a value at least 5 characters long
 374
        >>> min5(accept_python=False).from_python('1234')
 375
        Traceback (most recent call last):
 376
          ...
 377
        Invalid: Enter a value at least 5 characters long
 378
        >>> min5.to_python([1, 2, 3, 4, 5])
 379
        [1, 2, 3, 4, 5]
 380
        >>> min5.to_python([1, 2, 3])
 381
        Traceback (most recent call last):
 382
          ...
 383
        Invalid: Enter a value at least 5 characters long
 384
        >>> min5.to_python(5)
 385
        Traceback (most recent call last):
 386
          ...
 387
        Invalid: Invalid value (value with length expected)
 389
    """
 391
    __unpackargs__ = ('minLength',)
 393
    messages = {
 394
        'tooShort': _("Enter a value at least %(minLength)i characters long"),
 395
        'invalid': _("Invalid value (value with length expected)"),
 396
        }
 398
    def validate_python(self, value, state):
 399
        try:
 400
            if len(value) < self.minLength:
 401
                raise Invalid(self.message('tooShort', state,
 402
                                           minLength=self.minLength),
 403
                              value, state)
 404
        except TypeError:
 405
            raise Invalid(self.message('invalid', state),
 406
                          value, state)
 408
class NotEmpty(FancyValidator):
 410
    """
 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.
 416
    Examples::
 418
        >>> ne = NotEmpty(messages={'empty': 'enter something'})
 419
        >>> ne.to_python('')
 420
        Traceback (most recent call last):
 421
          ...
 422
        Invalid: enter something
 423
        >>> ne.to_python(0)
 424
        0
 425
    """
 426
    not_empty = True
 428
    messages = {
 429
        'empty': _("Please enter a value"),
 430
        }
 432
    def validate_python(self, value, state):
 433
        if value == 0:
 435
            return value
 436
        if not value:
 437
            raise Invalid(self.message('empty', state),
 438
                          value, state)
 440
class Empty(FancyValidator):
 442
    """
 443
    Invalid unless the value is empty.  Use cleverly, if at all.
 445
    Examples::
 447
        >>> Empty.to_python(0)
 448
        Traceback (most recent call last):
 449
          ...
 450
        Invalid: You cannot enter a value here
 451
    """
 453
    messages = {
 454
        'notEmpty': _("You cannot enter a value here"),
 455
        }
 457
    def validate_python(self, value, state):
 458
        if value or value == 0:
 459
            raise Invalid(self.message('notEmpty', state),
 460
                          value, state)
 462
class Regex(FancyValidator):
 464
    """
 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).
 473
    Examples::
 475
        >>> cap = Regex(r'^[A-Z]+$')
 476
        >>> cap.to_python('ABC')
 477
        'ABC'
 479
    Note that ``.from_python()`` calls (in general) do not validate
 480
    the input::
 482
        >>> cap.from_python('abc')
 483
        'abc'
 484
        >>> cap(accept_python=False).from_python('abc')
 485
        Traceback (most recent call last):
 486
          ...
 487
        Invalid: The input is not valid
 488
        >>> cap.to_python(1)
 489
        Traceback (most recent call last):
 490
          ...
 491
        Invalid: The input must be a string (not a <type 'int'>: 1)
 492
        >>> Regex(r'^[A-Z]+$', strip=True).to_python('  ABC  ')
 493
        'ABC'
 494
        >>> Regex(r'this', regexOps=('I',)).to_python('THIS')
 495
        'THIS'
 496
    """
 498
    regexOps = ()
 499
    strip = False
 500
    regex = None
 502
    __unpackargs__ = ('regex',)
 504
    messages = {
 505
        'invalid': _("The input is not valid"),
 506
        }
 508
    def __init__(self, *args, **kw):
 509
        FancyValidator.__init__(self, *args, **kw)
 510
        if isinstance(self.regex, basestring):
 511
            ops = 0
 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)
 518
                else:
 519
                    ops |= 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),
 528
                          value, state)
 530
    def _to_python(self, value, state):
 531
        if self.strip and isinstance(value, basestring):
 532
            return value.strip()
 533
        return value
 535
class PlainText(Regex):
 537
    """
 538
    Test that the field contains only letters, numbers, underscore,
 539
    and the hyphen.  Subclasses Regex.
 541
    Examples::
 543
        >>> PlainText.to_python('_this9_')
 544
        '_this9_'
 545
        >>> PlainText.from_python('  this  ')
 546
        '  this  '
 547
        >>> PlainText(accept_python=False).from_python('  this  ')
 548
        Traceback (most recent call last):
 549
          ...
 550
        Invalid: Enter only letters, numbers, or _ (underscore)
 551
        >>> PlainText(strip=True).to_python('  this  ')
 552
        'this'
 553
        >>> PlainText(strip=True).from_python('  this  ')
 554
        'this'
 555
    """
 557
    regex = r"^[a-zA-Z_\-0-9]*$"
 559
    messages = {
 560
        'invalid': _('Enter only letters, numbers, or _ (underscore)'),
 561
        }
 563
class OneOf(FancyValidator):
 565
    """
 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.
 575
    Examples::
 577
        >>> oneof = OneOf([1, 2, 3])
 578
        >>> oneof.to_python(1)
 579
        1
 580
        >>> oneof.to_python(4)
 581
        Traceback (most recent call last):
 582
          ...
 583
        Invalid: Value must be one of: 1; 2; 3 (not 4)
 584
        >>> oneof(testValueList=True).to_python([2, 3, [1, 2, 3]])
 585
        [2, 3, [1, 2, 3]]
 586
        >>> oneof.to_python([2, 3, [1, 2, 3]])
 587
        Traceback (most recent call last):
 588
          ...
 589
        Invalid: Value must be one of: 1; 2; 3 (not [2, 3, [1, 2, 3]])
 590
    """
 592
    list = None
 593
    testValueList = False
 594
    hideList = False
 596
    __unpackargs__ = ('list',)
 598
    messages = {
 599
        'invalid': _("Invalid value"),
 600
        'notIn': _("Value must be one of: %(items)s (not %(value)r)"),
 601
        }
 603
    def validate_python(self, value, state):
 604
        if self.testValueList and isinstance(value, (list, tuple)):
 605
            for v in value:
 606
                self.validate_python(v, state)
 607
        else:
 608
            if not value in self.list:
 609
                if self.hideList:
 610
                    raise Invalid(self.message('invalid', state),
 611
                                  value, state)
 612
                else:
 613
                    items = '; '.join(map(str, self.list))
 614
                    raise Invalid(self.message('notIn', state,
 615
                                               items=items,
 616
                                               value=value),
 617
                                  value, state)
 619
class DictConverter(FancyValidator):
 621
    """
 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
 629
    gives keys.
 631
    If you give hideDict=True, then the contents of the dictionary
 632
    will not show up in error messages.
 634
    Examples::
 636
        >>> dc = DictConverter({1: 'one', 2: 'two'})
 637
        >>> dc.to_python(1)
 638
        'one'
 639
        >>> dc.from_python('one')
 640
        1
 641
        >>> dc.to_python(3)
 642
        Traceback (most recent call last):
 643
        Invalid: Enter a value from: 1; 2
 644
        >>> dc2 = dc(hideDict=True)
 645
        >>> dc2.hideDict
 646
        True
 647
        >>> dc2.dict
 648
        {1: 'one', 2: 'two'}
 649
        >>> dc2.to_python(3)
 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'
 655
    """
 657
    dict = None
 658
    hideDict = False
 660
    __unpackargs__ = ('dict',)
 662
    messages = {
 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"),
 667
        }
 669
    def _to_python(self, value, state):
 670
        try:
 671
            return self.dict[value]
 672
        except KeyError:
 673
            if self.hideDict:
 674
                raise Invalid(self.message('keyNotFound', state),
 675
                              value, state)
 676
            else:
 677
                items = '; '.join(map(repr, self.dict.keys()))
 678
                raise Invalid(self.message('chooseKey', state,
 679
                                           items=items),
 680
                              value, state)
 682
    def _from_python(self, value, state):
 683
        for k, v in self.dict.items():
 684
            if value == v:
 685
                return k
 686
        if self.hideDict:
 687
            raise Invalid(self.message('valueNotFound', state),
 688
                          value, state)
 689
        else:
 690
            items = '; '.join(map(repr, self.dict.values()))
 691
            raise Invalid(self.message('chooseValue', state,
 692
                                       value=repr(value),
 693
                                       items=items),
 694
                          value, state)
 696
class IndexListConverter(FancyValidator):
 698
    """
 699
    Converts a index (which may be a string like '2') to the value in
 700
    the given list.
 702
    Examples::
 704
        >>> index = IndexListConverter(['zero', 'one', 'two'])
 705
        >>> index.to_python(0)
 706
        'zero'
 707
        >>> index.from_python('zero')
 708
        0
 709
        >>> index.to_python('1')
 710
        'one'
 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
 720
    """
 722
    list = None
 724
    __unpackargs__ = ('list',)
 726
    messages = {
 727
        'integer': _("Must be an integer index"),
 728
        'outOfRange': _("Index out of range"),
 729
        'notFound': _("Item %(value)s was not found in the list"),
 730
        }
 732
    def _to_python(self, value, state):
 733
        try:
 734
            value = int(value)
 735
        except (ValueError, TypeError):
 736
            raise Invalid(self.message('integer', state),
 737
                          value, state)
 738
        try:
 739
            return self.list[value]
 740
        except IndexError:
 741
            raise Invalid(self.message('outOfRange', state),
 742
                          value, state)
 744
    def _from_python(self, value, state):
 745
        for i in range(len(self.list)):
 746
            if self.list[i] == value:
 747
                return i
 748
        raise Invalid(self.message('notFound', state,
 749
                                   value=repr(value)),
 750
                      value, state)
 752
class DateValidator(FancyValidator):
 754
    """
 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.
 765
    Examples::
 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):
 773
            ...
 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)
 780
        True
 781
        >>> d.to_python(now-timedelta(days=1))
 782
        Traceback (most recent call last):
 783
            ...
 784
        Invalid: The date must be sometime in the future
 785
        >>> d.to_python(now+timedelta(days=1)) > now
 786
        True
 787
        >>> d = DateValidator(today_or_after=True)
 788
        >>> d.to_python(now) == now
 789
        True
 791
    """
 793
    earliest_date = None
 794
    latest_date = None
 795
    after_now = False
 797
    today_or_after = False
 801
    datetime_module = None
 803
    messages = {
 804
        'after': _("Date must be after %(date)s"),
 805
        'before': _("Date must be before %(date)s"),
 807
        'date_format': _("%%A, %%d %%B %%Y"),
 808
        'future': _("The date must be sometime in the future"),
 809
        }
 811
    def validate_python(self, value, state):
 812
        date_format = self.message('date_format', state)
 813
        if isinstance(date_format, unicode):
 815
            encoding = locale.getlocale(locale.LC_TIME)[1] or 'utf-8'
 816
            date_format = date_format.encode(encoding)
 817
        else:
 818
            encoding = None
 819
        if self.earliest_date:
 820
            if callable(self.earliest_date):
 821
                earliest_date = self.earliest_date()
 822
            else:
 823
                earliest_date = self.earliest_date
 824
            if value < earliest_date:
 825
                date_formatted = earliest_date.strftime(date_format)
 826
                if encoding:
 827
                    date_formatted = date_formatted.decode(encoding)
 828
                raise Invalid(
 829
                    self.message('after', state,
 830
                                 date=date_formatted),
 831
                    value, state)
 832
        if self.latest_date:
 833
            if callable(self.latest_date):
 834
                latest_date = self.latest_date()
 835
            else:
 836
                latest_date = self.latest_date
 837
            if value > latest_date:
 838
                date_formatted = latest_date.strftime(date_format)
 839
                if encoding:
 840
                    date_formatted = date_formatted.decode(encoding)
 841
                raise Invalid(
 842
                    self.message('before', state,
 843
                                 date=date_formatted),
 844
                    value, state)
 845
        if self.after_now:
 846
            dt_mod = import_datetime(self.datetime_module)
 847
            now = datetime_now(dt_mod)
 848
            if value < now:
 849
                date_formatted = now.strftime(date_format)
 850
                if encoding:
 851
                    date_formatted = date_formatted.decode(encoding)
 852
                raise Invalid(
 853
                    self.message('future', state,
 854
                                 date=date_formatted),
 855
                    value, state)
 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)
 865
                if encoding:
 866
                    date_formatted = date_formatted.decode(encoding)
 867
                raise Invalid(
 868
                    self.message('future', state,
 869
                                 date=date_formatted),
 870
                    value, state)
 873
class Bool(FancyValidator):
 875
    """
 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``.
 882
    Examples::
 884
        >>> Bool.to_python(0)
 885
        False
 886
        >>> Bool.to_python(1)
 887
        True
 888
        >>> Bool.to_python('')
 889
        False
 890
        >>> Bool.to_python(None)
 891
        False
 892
    """
 894
    if_missing = False
 896
    def _to_python(self, value, state):
 897
        return bool(value)
 898
    _from_python = _to_python
 900
    def empty_value(self, value):
 901
        return False
 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
 911
    Number.)
 913
    """
 915
    messages = {
 916
        'tooLow': _("Please enter a number that is %(min)s or greater"),
 917
        'tooHigh': _("Please enter a number that is %(max)s or smaller"),
 918
    }
 920
    min = None
 921
    max = None
 923
    def validate_python(self, value, state):
 924
        if self.min is not None:
 925
            if value < self.min:
 926
                msg = self.message("tooLow", state, min=self.min)
 927
                raise Invalid(msg, value, state)
 928
        if self.max is not None:
 929
            if value > self.max:
 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.
 938
    Example::
 940
        >>> Int.to_python('10')
 941
        10
 942
        >>> Int.to_python('ten')
 943
        Traceback (most recent call last):
 944
            ...
 945
        Invalid: Please enter an integer value
 946
        >>> Int(min=5).to_python('6')
 947
        6
 948
        >>> Int(max=10).to_python('11')
 949
        Traceback (most recent call last):
 950
            ...
 951
        Invalid: Please enter a number that is 10 or smaller
 953
    """
 955
    messages = {
 956
        'integer': _("Please enter an integer value")
 957
    }
 959
    def _to_python(self, value, state):
 960
        try:
 961
            return int(value)
 962
        except (ValueError, TypeError):
 963
            raise Invalid(self.message('integer', state),
 964
                          value, 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.
 975
    Example::
 977
        >>> Number.to_python('10')
 978
        10
 979
        >>> Number.to_python('10.5')
 980
        10.5
 981
        >>> Number.to_python('ten')
 982
        Traceback (most recent call last):
 983
            ...
 984
        Invalid: Please enter a number
 985
        >>> Number(min=5).to_python('6.5')
 986
        6.5
 987
        >>> Number(max=10.5).to_python('11.5')
 988
        Traceback (most recent call last):
 989
            ...
 990
        Invalid: Please enter a number that is 10.5 or smaller
 991
        >>> Number().to_python('infinity')
 992
        inf
 994
    """
 996
    messages = {
 997
        'number': _("Please enter a number")
 998
    }
1000
    def _to_python(self, value, state):
1001
        try:
1002
            value = float(value)
1003
            try:
1004
                int_value = int(value)
1005
            except OverflowError:
1006
                int_value = None
1007
            if value == int_value:
1008
                return int_value
1009
            return value
1010
        except ValueError:
1011
            raise Invalid(self.message('number', state),
1012
                          value, state)
1015
class String(FancyValidator):
1016
    """
1017
    Converts things to string, but treats empty things as the empty
1018
    string.
1020
    Also takes a `max` and `min` argument, and the string length must
1021
    fall in that range.
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``.
1027
    ::
1029
        >>> String(min=2).to_python('a')
1030
        Traceback (most recent call last):
1031
            ...
1032
        Invalid: Enter a value 2 characters long or more
1033
        >>> String(max=10).to_python('xxxxxxxxxxx')
1034
        Traceback (most recent call last):
1035
            ...
1036
        Invalid: Enter a value less than 10 characters long
1037
        >>> String().from_python(None)
1038
        ''
1039
        >>> String().from_python([])
1040
        ''
1041
        >>> String().to_python(None)
1042
        ''
1043
        >>> String(min=3).to_python(None)
1044
        Traceback (most recent call last):
1045
            ...
1046
        Invalid: Please enter a value
1047
        >>> String(min=1).to_python('')
1048
        Traceback (most recent call last):
1049
            ...
1050
        Invalid: Please enter a value
1052
    """
1054
    min = None
1055
    max = None
1056
    not_empty = None
1057
    encoding = None
1058
    list_joiner = ', '
1060
    messages = {
1061
        'tooLong': _("Enter a value less than %(max)i characters long"),
1062
        'tooShort': _("Enter a value %(min)i characters long or more"),
1063
        }
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):
1070
        if not value:
1071
            value = ''
1072
        if not isinstance(value, basestring):
1073
            try:
1074
                value = str(value)
1075
            except UnicodeEncodeError:
1076
                value = unicode(value)
1077
        if self.encoding is not None and isinstance(value, unicode):
1078
            value = value.encode(self.encoding)
1079
        return value
1081
    def _from_python(self, value, state):
1082
        if not value and value != 0:
1083
            value = ''
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])
1088
            try:
1089
                value = str(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)
1094
        if self.strip:
1095
            value = value.strip()
1096
        return value
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,
1102
                                       max=self.max),
1103
                          value, state)
1104
        if (self.min is not None
1105
            and (not value or len(value) < self.min)):
1106
            raise Invalid(self.message('tooShort', state,
1107
                                       min=self.min),
1108
                          value, state)
1110
    def empty_value(self, value):
1111
        return ''
1113
class UnicodeString(String):
1114
    """
1115
    Converts things to unicode string, this is a specialization of
1116
    the String class.
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.
1126
    ::
1128
        >>> UnicodeString().to_python(None)
1129
        u''
1130
        >>> UnicodeString().to_python([])
1131
        u''
1132
        >>> UnicodeString(encoding='utf-7').to_python('Ni Ni Ni')
1133
        u'Ni Ni Ni'
1135
    """
1136
    encoding = 'utf-8'
1137
    inputEncoding = NoDefault
1138
    outputEncoding = NoDefault
1139
    messages = {
1140
        'badEncoding' : _("Invalid data or incorrect encoding"),
1141
    }
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):
1151
        if not value:
1152
            return u''
1153
        if isinstance(value, unicode):
1154
            return value
1155
        if not isinstance(value, unicode):
1156
            if hasattr(value, '__unicode__'):
1157
                value = unicode(value)
1158
                return value
1159
            else:
1160
                value = str(value)
1161
        if self.inputEncoding:
1162
            try:
1163
                value = unicode(value, self.inputEncoding)
1164
            except UnicodeDecodeError:
1165
                raise Invalid(self.message('badEncoding', state), value, state)
1166
            except TypeError:
1167
                raise Invalid(self.message('badType', state, type=type(value),
1168
                    value=value), value, state)
1169
        return value
1171
    def _from_python(self, value, state):
1172
        if not isinstance(value, unicode):
1173
            if hasattr(value, '__unicode__'):
1174
                value = unicode(value)
1175
            else:
1176
                value = str(value)
1177
        if self.outputEncoding and isinstance(value, unicode):
1178
            value = value.encode(self.outputEncoding)
1179
        return value
1181
    def empty_value(self, value):
1182
        return u''
1184
class Set(FancyValidator):
1186
    """
1187
    This is for when you think you may return multiple values for a
1188
    certain field.
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
1194
    ``set`` object.
1196
    ::
1198
       >>> Set.to_python(None)
1199
       []
1200
       >>> Set.to_python('this')
1201
       ['this']
1202
       >>> Set.to_python(('this', 'that'))
1203
       ['this', 'that']
1204
       >>> s = Set(use_set=True)
1205
       >>> s.to_python(None)
1206
       set([])
1207
       >>> s.to_python('this')
1208
       set(['this'])
1209
       >>> s.to_python(('this',))
1210
       set(['this'])
1211
    """
1213
    use_set = False
1215
    if_missing = ()
1217
    def _to_python(self, value, state):
1218
        if self.use_set:
1219
            if isinstance(value, (set, sets.Set)):
1220
                return value
1221
            elif isinstance(value, (list, tuple)):
1222
                return set(value)
1223
            elif value is None:
1224
                return set()
1225
            else:
1226
                return set([value])
1227
        else:
1228
            if isinstance(value, list):
1229
                return value
1230
            elif sets and isinstance(value, (set, sets.Set)):
1231
                return list(value)
1232
            elif isinstance(value, tuple):
1233
                return list(value)
1234
            elif value is None:
1235
                return []
1236
            else:
1237
                return [value]
1239
    def empty_value(self, value):
1240
        if self.use_set:
1241
            return set()
1242
        else:
1243
            return []
1245
class Email(FancyValidator):
1246
    r"""
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.
1254
    ::
1256
        >>> e = Email()
1257
        >>> e.to_python(' test@foo.com ')
1258
        'test@foo.com'
1259
        >>> e.to_python('test')
1260
        Traceback (most recent call last):
1261
            ...
1262
        Invalid: An email address must contain a single @
1263
        >>> e.to_python('test@foobar')
1264
        Traceback (most recent call last):
1265
            ...
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):
1269
            ...
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):
1273
            ...
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):
1277
            ...
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')
1282
        'o*reilly@test.com'
1283
        >>> e = Email(resolve_domain=True)
1284
        >>> e.resolve_domain
1285
        True
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):
1293
            ...
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')
1296
        u'test@google.com'
1297
        >>> e = Email(not_empty=False)
1298
        >>> e.to_python('')
1300
    """
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)
1307
        [a-z]{2,}$                       # TLD
1308
    ''', re.I | re.VERBOSE)
1310
    messages = {
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)'),
1317
        }
1319
    def __init__(self, *args, **kw):
1320
        FancyValidator.__init__(self, *args, **kw)
1321
        if self.resolve_domain:
1322
            if not have_dns:
1323
                import warnings
1324
                warnings.warn(
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):
1331
        if not value:
1332
            raise Invalid(
1333
                self.message('empty', state),
1334
                value, state)
1335
        value = value.strip()
1336
        splitted = value.split('@', 1)
1337
        try:
1338
            username, domain=splitted
1339
        except ValueError:
1340
            raise Invalid(
1341
                self.message('noAt', state),
1342
                value, state)
1343
        if not self.usernameRE.search(username):
1344
            raise Invalid(
1345
                self.message('badUsername', state,
1346
                             username=username),
1347
                value, state)
1348
        if not self.domainRE.search(domain):
1349
            raise Invalid(
1350
                self.message('badDomain', state,
1351
                             domain=domain),
1352
                value, state)
1353
        if self.resolve_domain:
1354
            assert have_dns, "pyDNS should be available"
1355
            try:
1356
                a=DNS.DnsRequest(domain, qtype='mx').req().answers
1357
                if not a:
1358
                    a=DNS.DnsRequest(domain, qtype='a').req().answers
1359
                dnsdomains=[x['data'] for x in a]
1360
            except (socket.error, DNS.DNSError), e:
1361
                raise Invalid(
1362
	            self.message('socketError', state, error=e),
1363
		    value, state)
1364
            if not dnsdomains:
1365
                raise Invalid(
1366
                    self.message('domainDoesNotExist', state,
1367
                                 domain=domain),
1368
                    value, state)
1370
    def _to_python(self, value, state):
1371
        return value.strip()
1373
class URL(FancyValidator):
1375
    """
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
1380
    http://
1382
    ::
1384
        >>> u = URL(add_http=True)
1385
        >>> u.to_python('foo.com')
1386
        'http://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):
1399
            ...
1400
        Invalid: That is not a valid URL
1401
        >>> u.to_python('https://test.com')
1402
        'https://test.com'
1403
        >>> u.to_python('http://test')
1404
        Traceback (most recent call last):
1405
            ...
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):
1409
            ...
1410
        Invalid: That is not a valid URL
1411
        >>> u = URL(add_http=False, check_exists=True)
1412
        >>> u.to_python('http://google.com')
1413
        'http://google.com'
1414
        >>> u.to_python('google.com')
1415
        Traceback (most recent call last):
1416
            ...
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):
1420
            ...
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):
1424
            ...
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')
1430
        'http://localhost'
1432
    """
1434
    check_exists = False
1435
    add_http = True
1436
    require_tld = True
1438
    url_re = re.compile(r'''
1439
        ^(http|https)://
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
1443
        (?::[0-9]+)?                            # port
1446
        (?P<path>/[a-z0-9\-\._~:/\?#\[\]@!%\$&\'\(\)\*\+,;=]*)?
1447
        $
1448
    ''', re.I | re.VERBOSE)
1450
    scheme_re = re.compile(r'^[a-zA-Z]+:')
1452
    messages = {
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)'),
1460
        }
1462
    def _to_python(self, value, state):
1463
        value = value.strip()
1464
        if self.add_http:
1465
            if not self.scheme_re.search(value):
1466
                value = 'http://' + value
1467
        match = self.scheme_re.search(value)
1468
        if not match:
1469
            raise Invalid(
1470
                self.message('noScheme', state),
1471
                value, state)
1472
        value = match.group(0).lower() + value[len(match.group(0)):]
1473
        match = self.url_re.search(value)
1474
        if not match:
1475
            raise Invalid(
1476
                self.message('badURL', state),
1477
                value, state)
1478
        if self.require_tld and not match.group('domain'):
1479
            raise Invalid(
1480
                self.message('noTLD', state, domain=match.group('tld')),
1481
                value, state)
1482
        if self.check_exists and (value.startswith('http://')
1483
                                  or value.startswith('https://')):
1484
            self._check_url_exists(value, state)
1485
        return value
1487
    def _check_url_exists(self, url, state):
1488
        global httplib, urlparse, socket
1489
        if httplib is None:
1490
            import httplib
1491
        if urlparse is None:
1492
            import urlparse
1493
        if socket is None:
1494
            import socket
1495
        scheme, netloc, path, params, query, fragment = urlparse.urlparse(
1496
            url, 'http')
1497
        if scheme == 'http':
1498
            ConnClass = httplib.HTTPConnection
1499
        else:
1500
            ConnClass = httplib.HTTPSConnection
1501
        try:
1502
            conn = ConnClass(netloc)
1503
            if params:
1504
                path += ';' + params
1505
            if query:
1506
                path += '?' + query
1507
            conn.request('HEAD', path)
1508
            res = conn.getresponse()
1509
        except httplib.HTTPException, e:
1510
            raise Invalid(
1511
                self.message('httpError', state, error=e),
1512
                state, url)
1513
        except socket.error, e:
1514
            raise Invalid(
1515
                self.message('socketError', state, error=e),
1516
                state, url)
1517
        else:
1518
            if res.status == 404:
1519
                raise Invalid(
1520
                    self.message('notFound', state),
1521
                    state, url)
1522
            if (res.status < 200
1523
                or res.status >= 500):
1524
                raise Invalid(
1525
                    self.message('status', state, status=res.status),
1526
                    state, url)
1529
class XRI(FancyValidator):
1530
    r"""
1531
    Validator for XRIs.
1533
    It supports both i-names and i-numbers, of the first version of the XRI
1534
    standard.
1536
    ::
1538
        >>> inames = XRI(xri_type="i-name")
1539
        >>> inames.to_python("   =John.Smith ")
1540
        '=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):
1545
            ...
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):
1549
            ...
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):
1553
            ...
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):
1560
            ...
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'
1568
    """
1570
    iname_valid_pattern = re.compile(r"""
1571
    ^
1572
    [\w]+                  # A global alphanumeric i-name
1573
    (\.[\w]+)*             # An i-name with dots
1574
    (\*[\w]+(\.[\w]+)*)*   # A community i-name
1575
    $
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"""
1583
    ^
1584
    (
1585
    [=@]!       # It's a personal or organization i-number
1586
    |
1587
    !!          # It's a network i-number
1588
    )
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
1591
    $
1592
    """, re.VERBOSE|re.IGNORECASE)
1594
    messages = {
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 "
1599
                           "marks"),
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')
1603
        }
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
1609
            it's optional.
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}
1615
        """
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
1624
        spaces"""
1625
        value = value.strip()
1626
        if self.add_xri and not value.startswith("xri://"):
1627
            value = "xri://" + value
1628
        return value
1630
    def validate_python(self, value, state=None):
1631
        """Validate an XRI
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
1637
                is not valid.
1639
        """
1640
        if not (isinstance(value, str) or isinstance(value, unicode)):
1641
            raise Invalid(self.message("badType", state, type=str(type(value)),
1642
                                       value=value),
1643
                          value, state)
1646
        if value.startswith("xri://"):
1647
            value = value[6:]
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)
1655
        else:
1656
            self._validate_inumber(value, state)
1658
    def _validate_iname(self, iname, state):
1659
        """Validate an i-name"""
1661
        iname = iname[1:]
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,
1668
                          state)
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,
1674
                                       value=inumber),
1675
                          inumber, state)
1678
class OpenId(FancyValidator):
1679
    r"""
1680
    OpenId validator.
1682
    ::
1683
        >>> v = OpenId(add_schema=True)
1684
        >>> v.to_python(' example.net ')
1685
        'http://example.net'
1686
        >>> v.to_python('@TurboGears')
1687
        'xri://@TurboGears'
1688
        >>> w = OpenId(add_schema=False)
1689
        >>> w.to_python(' example.net ')
1690
        Traceback (most recent call last):
1691
        ...
1692
        Invalid: "example.net" is not a valid OpenId (it is neither an URL nor an XRI)
1693
        >>> w.to_python('!!1000')
1694
        '!!1000'
1695
        >>> w.to_python('look@me.com')
1696
        Traceback (most recent call last):
1697
        ...
1698
        Invalid: "look@me.com" is not a valid OpenId (it is neither an URL nor an XRI)
1700
    """
1702
    messages = {
1703
        'badId': _('"%(id)s" is not a valid OpenId (it is neither an URL nor an XRI)')
1704
        }
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}
1712
        """
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()
1719
        try:
1720
            return self.url_validator.to_python(value, state)
1721
        except Invalid:
1722
            try:
1723
                return self.iname_validator.to_python(value, state)
1724
            except Invalid:
1725
                try:
1726
                    return self.inumber_validator.to_python(value, state)
1727
                except Invalid:
1728
                    pass
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):
1752
    """
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).
1758
    """
1759
    def _to_python(self, value, state=None):
1760
        if isinstance(value, cgi.FieldStorage):
1761
            if getattr(value, 'filename', None):
1762
                return value
1763
            raise Invalid('invalid', value, state)
1764
        else:
1765
            return value
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):
1773
    """
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
1781
    values.
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
1791
    something like::
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.
1803
    """
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:
1816
            filename = None
1818
            content = upload
1819
        if not content and static:
1820
            filename, content = static.split(None, 1)
1821
            if filename == '-':
1822
                filename = ''
1823
            else:
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}
1837
        else:
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)
1845
        return result
1847
class DateConverter(FancyValidator):
1849
    """
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
1859
    signaling an error.
1861
    Use accept_day=False if you just want a month/year (like for a
1862
    credit card expiration date).
1864
    ::
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):
1873
            ...
1874
        Invalid: That month only has 29 days
1875
        >>> d.to_python('13/2/05')
1876
        Traceback (most recent call last):
1877
            ...
1878
        Invalid: Please enter a month from 1 to 12
1879
        >>> d.to_python('1/1/200')
1880
        Traceback (most recent call last):
1881
            ...
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')
1888
        >>> date
1889
        datetime.date(2009, 3, 12)
1890
        >>> d.from_python(date)
1891
        '12/03/2009'
1892
    """
1895
    accept_day = True
1897
    month_style = 'mm/dd/yyyy'
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)
1906
    _month_names = {
1907
        'jan': 1, 'january': 1,
1908
        'feb': 2, 'febuary': 2,
1909
        'mar': 3, 'march': 3,
1910
        'apr': 4, 'april': 4,
1911
        'may': 5,
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,
1919
        }
1922
    _monthDays = {
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}
1926
    messages = {
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'),
1936
        }
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):
1944
        if self.accept_day:
1945
            return self.convert_day(value, state)
1946
        else:
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)
1952
        if not match:
1953
            raise Invalid(self.message('badFormat', state,
1954
                                       format=self.month_style),
1955
                          value, state)
1956
        day = int(match.group(1))
1957
        try:
1958
            month = int(match.group(2))
1959
        except TypeError:
1960
            month = self.make_month(match.group(2), state)
1961
        else:
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),
1967
                          value, state)
1968
        if day < 1:
1969
            raise Invalid(self.message('invalidDay', state),
1970
                          value, state)
1971
        if self._monthDays[month] < day:
1972
            raise Invalid(self.message('dayRange', state,
1973
                                       days=self._monthDays[month]),
1974
                          value, state)
1975
        dt_mod = import_datetime(self.datetime_module)
1976
        try:
1977
            return datetime_makedate(dt_mod, year, month, day)
1978
        except ValueError, v:
1979
            raise Invalid(self.message('invalidDate', state,
1980
                                       exception=str(v)),
1981
                          value, state)
1983
    def make_month(self, value, state):
1984
        try:
1985
            return int(value)
1986
        except ValueError:
1987
            value = value.lower().strip()
1988
            if self._month_names.has_key(value):
1989
                return self._month_names[value]
1990
            else:
1991
                raise Invalid(self.message('unknownMonthName', state,
1992
                                           month=value),
1993
                              value, state)
1995
    def make_year(self, year, state):
1996
        try:
1997
            year = int(year)
1998
        except ValueError:
1999
            raise Invalid(self.message('invalidYear', state),
2000
                          year, state)
2001
        if year <= 20:
2002
            year = year + 2000
2003
        if year >= 50 and year < 100:
2004
            year = year + 1900
2005
        if (year > 20 and year < 50) or (year>99 and year<1900):
2006
            raise Invalid(self.message('fourDigitYear', state),
2007
                          year, state)
2008
        return year
2010
    def convert_month(self, value, state):
2011
        match = self._month_date_re.search(value)
2012
        if not match:
2013
            raise Invalid(self.message('wrongFormat', state,
2014
                                       format='mm/yyyy'),
2015
                          value, 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),
2020
                          value, 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:
2026
            return ''
2027
        if self.accept_day:
2028
            return self.unconvert_day(value, state)
2029
        else:
2030
            return self.unconvert_month(value, state)
2032
    def unconvert_day(self, value, state):
2034
        if self.month_style == 'mm/dd/yyyy':
2035
            return value.strftime("%m/%d/%Y")
2036
        else:
2037
            return value.strftime("%d/%m/%Y")
2039
    def unconvert_month(self, value, state):
2041
        return value.strftime("%m/%Y")
2043
class TimeConverter(FancyValidator):
2045
    """
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.
2052
    Examples::
2054
        >>> tim = TimeConverter()
2055
        >>> tim.to_python('8:30')
2056
        (8, 30)
2057
        >>> tim.to_python('20:30')
2058
        (20, 30)
2059
        >>> tim.to_python('30:00')
2060
        Traceback (most recent call last):
2061
            ...
2062
        Invalid: You must enter an hour in the range 0-23
2063
        >>> tim.to_python('13:00pm')
2064
        Traceback (most recent call last):
2065
            ...
2066
        Invalid: You must enter an hour in the range 1-12
2067
        >>> tim.to_python('12:-1')
2068
        Traceback (most recent call last):
2069
            ...
2070
        Invalid: You must enter a minute in the range 0-59
2071
        >>> tim.to_python('12:02pm')
2072
        (12, 2)
2073
        >>> tim.to_python('12:02am')
2074
        (0, 2)
2075
        >>> tim.to_python('1:00PM')
2076
        (13, 0)
2077
        >>> tim.from_python((13, 0))
2078
        '13:00:00'
2079
        >>> tim2 = tim(use_ampm=True, use_seconds=False)
2080
        >>> tim2.from_python((13, 0))
2081
        '1:00pm'
2082
        >>> tim2.from_python((0, 0))
2083
        '12:00am'
2084
        >>> tim2.from_python((12, 0))
2085
        '12:00pm'
2087
    Examples with ``datetime.time``::
2089
        >>> v = TimeConverter(use_datetime=True)
2090
        >>> a = v.to_python('18:00')
2091
        >>> a
2092
        datetime.time(18, 0)
2093
        >>> b = v.to_python('30:00')
2094
        Traceback (most recent call last):
2095
            ...
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)
2099
        '6:00:00pm'
2100
        >>> v3 = TimeConverter(prefer_ampm=True,
2101
        ...                    use_seconds=False, use_datetime=True)
2102
        >>> a = v3.to_python('18:00')
2103
        >>> a
2104
        datetime.time(18, 0)
2105
        >>> v3.from_python(a)
2106
        '6:00pm'
2107
        >>> a = v3.to_python('18:00:00')
2108
        Traceback (most recent call last):
2109
            ...
2110
        Invalid: You may not enter seconds
2111
    """
2113
    use_ampm = 'optional'
2114
    prefer_ampm = False
2115
    use_seconds = 'optional'
2116
    use_datetime = False
2118
    datetime_module = None
2120
    messages = {
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'),
2130
        }
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)
2138
        else:
2139
            return result
2141
    def _to_python_tuple(self, value, state):
2142
        time = value.strip()
2143
        explicit_ampm = False
2144
        if self.use_ampm:
2145
            last_two = time[-2:].lower()
2146
            if last_two not in ('am', 'pm'):
2147
                if self.use_ampm != 'optional':
2148
                    raise Invalid(
2149
                        self.message('noAMPM', state),
2150
                        value, state)
2151
                else:
2152
                    offset = 0
2153
            else:
2154
                explicit_ampm = True
2155
                if last_two == 'pm':
2156
                    offset = 12
2157
                else:
2158
                    offset = 0
2159
                time = time[:-2]
2160
        else:
2161
            offset = 0
2162
        parts = time.split(':')
2163
        if len(parts) > 3:
2164
            raise Invalid(
2165
                self.message('tooManyColon', state),
2166
                value, state)
2167
        if len(parts) == 3 and not self.use_seconds:
2168
            raise Invalid(
2169
                self.message('noSeconds', state),
2170
                value, state)
2171
        if (len(parts) == 2
2172
            and self.use_seconds
2173
            and self.use_seconds != 'optional'):
2174
            raise Invalid(
2175
                self.message('secondsRequired', state),
2176
                value, state)
2177
        if len(parts) == 1:
2178
            raise Invalid(
2179
                self.message('minutesRequired', state),
2180
                value, state)
2181
        try:
2182
            hour = int(parts[0])
2183
        except ValueError:
2184
            raise Invalid(
2185
                self.message('badNumber', state, number=parts[0], part='hour'),
2186
                value, state)
2187
        if explicit_ampm:
2188
            if hour > 12 or hour < 1:
2189
                raise Invalid(
2190
                    self.message('badHour', state, number=hour, range='1-12'),
2191
                    value, state)
2192
            if hour == 12 and offset == 12:
2194
                pass
2195
            elif hour == 12 and offset == 0:
2197
                hour = 0
2198
            else:
2199
                hour += offset
2200
        else:
2201
            if hour > 23 or hour < 0:
2202
                raise Invalid(
2203
                    self.message('badHour', state,
2204
                                 number=hour, range='0-23'),
2205
                    value, state)
2206
        try:
2207
            minute = int(parts[1])
2208
        except ValueError:
2209
            raise Invalid(
2210
                self.message('badNumber', state,
2211
                             number=parts[1], part='minute'),
2212
                value, state)
2213
        if minute > 59 or minute < 0:
2214
            raise Invalid(
2215
                self.message('badMinute', state, number=minute),
2216
                value, state)
2217
        if len(parts) == 3:
2218
            try:
2219
                second = int(parts[2])
2220
            except ValueError:
2221
                raise Invalid(
2222
                    self.message('badNumber', state,
2223
                                 number=parts[2], part='second'))
2224
            if second > 59 or second < 0:
2225
                raise Invalid(
2226
                    self.message('badSecond', state, number=second),
2227
                    value, state)
2228
        else:
2229
            second = None
2230
        if second is None:
2231
            return (hour, minute)
2232
        else:
2233
            return (hour, minute, second)
2235
    def _from_python(self, value, state):
2236
        if isinstance(value, (str, unicode)):
2237
            return value
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
2245
            second = 0
2246
        ampm = ''
2247
        if ((self.use_ampm == 'optional' and self.prefer_ampm)
2248
            or (self.use_ampm and self.use_ampm != 'optional')):
2249
            ampm = 'am'
2250
            if hour > 12:
2251
                hour -= 12
2252
                ampm = 'pm'
2253
            elif hour == 12:
2254
                ampm = 'pm'
2255
            elif hour == 0:
2256
                hour = 12
2257
        if self.use_seconds:
2258
            return '%i:%02i:%02i%s' % (hour, minute, second, ampm)
2259
        else:
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):
2269
    """
2270
    Take a field from a dictionary, removing the key from the
2271
    dictionary.
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})
2277
    (2, {'a': 1})
2278
    >>> StripField('test').to_python({})
2279
    Traceback (most recent call last):
2280
        ...
2281
    Invalid: The name 'test' is missing
2283
    """
2285
    __unpackargs__ = ('name',)
2287
    messages = {
2288
        'missing': _('The name %(name)s is missing'),
2289
        }
2291
    def _to_python(self, valueDict, state):
2292
        v = valueDict.copy()
2293
        try:
2294
            field = v[self.name]
2295
            del v[self.name]
2296
        except KeyError:
2297
            raise Invalid(self.message('missing', state,
2298
                                       name=repr(self.name)),
2299
                          valueDict, state)
2300
        return field, v
2302
    def is_empty(self, value):
2304
        return False
2306
class StringBool(FancyValidator):
2308
    """
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.
2316
    ::
2318
        >>> s = StringBoolean()
2319
        >>> s.to_python('yes'), s.to_python('no')
2320
        (True, False)
2321
        >>> s.to_python(1), s.to_python('N')
2322
        (True, False)
2323
        >>> s.to_python('ye')
2324
        Traceback (most recent call last):
2325
            ...
2326
        Invalid: Value should be 'true' or 'false'
2327
    """
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:
2338
                return True
2339
            if not value or value in self.false_values:
2340
                return False
2341
            raise Invalid(self.message("string", state,
2342
                                       true=self.true_values[0],
2343
                                       false=self.false_values[0]),
2344
                          value, state)
2345
        return bool(value)
2347
    def _from_python(self, value, state):
2348
        if value:
2349
            return self.true_values[0]
2350
        else:
2351
            return self.false_values[0]
2354
StringBoolean = StringBool
2356
class SignedString(FancyValidator):
2358
    """
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
2363
    and consistent.
2364
    """
2366
    messages = {
2367
        'malformed': _('Value does not contain a signature'),
2368
        'badsig': _('Signature is not correct'),
2369
        }
2371
    secret = None
2372
    nonce_length = 4
2374
    def _to_python(self, value, state):
2375
        global sha1
2376
        if not sha1:
2377
            try:
2378
                from hashlib import sha1
2379
            except ImportError:
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),
2386
                          value, state)
2387
        sig, rest = parts
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()
2393
        if expected != sig:
2394
            raise Invalid(self.message('badsig', state),
2395
                          value, state)
2396
        return rest
2398
    def _from_python(self, value, state):
2399
        global sha1
2400
        if not sha1:
2401
            try:
2402
                from hashlib import sha1
2403
            except ImportError:
2404
                from sha import sha as sha1
2405
        nonce = self.make_nonce()
2406
        value = str(value)
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):
2414
        global random
2415
        if not random:
2416
            import random
2417
        return ''.join([
2418
            chr(random.randrange(256))
2419
            for i in range(self.nonce_length)])
2421
class IPAddress(FancyValidator):
2422
    """
2423
    Formencode validator to check whether a string is a correct IP address
2425
    Examples::
2427
        >>> ip = IPAddress()
2428
        >>> ip.to_python('127.0.0.1')
2429
        '127.0.0.1'
2430
        >>> ip.to_python('299.0.0.1')
2431
        Traceback (most recent call last):
2432
            ...
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):
2436
            ...
2437
        Invalid: Please enter a valid IP address (a.b.c.d)
2438
        >>> ip.to_python('asdf')
2439
        Traceback (most recent call last):
2440
            ...
2441
        Invalid: Please enter a valid IP address (a.b.c.d)
2442
    """
2443
    messages = {
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)',
2446
            }
2448
    def validate_python(self, value, state):
2449
        try:
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)
2462
        except ValueError:
2463
            raise Invalid(self.message("bad_format", state), value, state)
2465
class CIDR(IPAddress):
2466
    """
2467
    Formencode validator to check whether a string is in correct CIDR
2468
    notation (IP address, or IP address plus /mask)
2470
    Examples::
2472
        >>> cidr = CIDR()
2473
        >>> cidr.to_python('127.0.0.1')
2474
        '127.0.0.1'
2475
        >>> cidr.to_python('299.0.0.1')
2476
        Traceback (most recent call last):
2477
            ...
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):
2481
            ...
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):
2485
            ...
2486
        Invalid: Please enter a valid IP address (a.b.c.d) or IP network (a.b.c.d/e)
2487
    """
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)',
2491
            )
2493
    def validate_python(self, value, state):
2494
        try:
2496
            if '/' in value: # a.b.c.d/e
2497
                addr, bits = value.split('/')
2498
            else: # a.b.c.d
2499
                addr, bits = value, 32
2502
            IPAddress.validate_python(self, addr, state)
2505
            if int(bits) < 8 or int(bits) > 32:
2506
                    raise Invalid(self.message("illegal_bits", state, bits=bits), value, state)
2509
        except ValueError:
2510
            raise Invalid(self.message("bad_format", state), value, state)
2512
class MACAddress(FancyValidator):
2513
    """
2514
    Formencode validator to check whether a string is a correct hardware
2515
    (MAC) address.
2517
    Examples::
2519
        >>> mac = MACAddress()
2520
        >>> mac.to_python('aa:bb:cc:dd:ee:ff')
2521
        'aabbccddeeff'
2522
        >>> mac.to_python('aa:bb:cc:dd:ee:ff:e')
2523
        Traceback (most recent call last):
2524
            ...
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):
2528
            ...
2529
        Invalid: MAC addresses may only contain 0-9 and A-F (and optionally :), not 'x'
2530
        >>> MACAddress(add_colons=True).to_python('aabbccddeeff')
2531
        'aa:bb:cc:dd:ee:ff'
2532
    """
2534
    strip=True
2535
    valid_characters = '0123456789abcdefABCDEF'
2536
    add_colons = False
2538
    messages = {
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'),
2541
        }
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)
2551
        if self.add_colons:
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])
2555
        return address
2557
    _from_python = _to_python
2559
class FormValidator(FancyValidator):
2560
    """
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.
2578
    """
2580
    validate_partial_form = False
2582
    validate_partial_python = None
2583
    validate_partial_other = None
2585
    def is_empty(self, value):
2586
        return False
2588
class RequireIfMissing(FormValidator):
2590
    """
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.
2603
    ::
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):
2610
            ...
2611
        Invalid: You must give a value for phone_type
2612
        >>> v.to_python({'phone': ''})
2613
        {'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')]
2623
    """
2626
    required = None
2628
    missing = None
2630
    present = None
2631
    __unpackargs__ = ('required',)
2633
    def _to_python(self, value_dict, state):
2634
        is_required = False
2635
        if self.missing and not value_dict.get(self.missing):
2636
            is_required = True
2637
        if self.present and value_dict.get(self.present):
2638
            is_required = True
2639
        if is_required and not value_dict.get(self.required):
2640
            raise Invalid('You must give a value for %s' % self.required,
2641
                          value, state,
2642
                          error_dict={self.required: Invalid(self.message(
2643
                              'empty', state), value, state)})
2644
        return value_dict
2646
RequireIfPresent = RequireIfMissing
2648
class FieldsMatch(FormValidator):
2650
    """
2651
    Tests that the given fields match, i.e., are identical.  Useful
2652
    for password+confirmation fields.  Pass the list of field names in
2653
    as `field_names`.
2655
    ::
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):
2662
            ...
2663
        Invalid: conf: Fields do not match
2664
    """
2666
    show_match = False
2667
    field_names = None
2668
    validate_partial_form = True
2669
    __unpackargs__ = ('*', 'field_names')
2671
    messages = {
2672
        'invalid': _("Fields do not match (should be %(match)s)"),
2673
        'invalidNoMatch': _("Fields do not match"),
2674
        'notDict': _("Fields should be a dictionary"),
2675
        }
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):
2685
                return
2686
        self.validate_python(field_dict, state)
2688
    def validate_python(self, field_dict, state):
2689
        try:
2690
            ref = field_dict[self.field_names[0]]
2691
        except TypeError:
2693
            raise Invalid(self.message('notDict', state), field_dict, state)
2694
        except KeyError:
2695
            ref = ''
2696
        errors = {}
2697
        for name in self.field_names[1:]:
2698
            if field_dict.get(name, '') != ref:
2699
                if self.show_match:
2700
                    errors[name] = self.message('invalid', state,
2701
                                                match=ref)
2702
                else:
2703
                    errors[name] = self.message('invalidNoMatch', state)
2704
        if errors:
2705
            error_list = errors.items()
2706
            error_list.sort()
2707
            error_message = '<br>\n'.join(
2708
                ['%s: %s' % (name, value) for name, value in error_list])
2709
            raise Invalid(error_message,
2710
                          field_dict, state,
2711
                          error_dict=errors)
2713
class CreditCardValidator(FormValidator):
2714
    """
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).
2725
    ::
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):
2732
            ...
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):
2736
            ...
2737
        Invalid: ccNumber: You did not enter a valid number of digits
2738
        >>> cc().to_python({})
2739
        Traceback (most recent call last):
2740
            ...
2741
        Invalid: The field ccType is missing
2742
    """
2744
    validate_partial_form = True
2746
    cc_type_field = 'ccType'
2747
    cc_number_field = 'ccNumber'
2748
    __unpackargs__ = ('cc_type_field', 'cc_number_field')
2750
    messages = {
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"),
2755
        }
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):
2760
            return None
2761
        self.validate_python(field_dict, state)
2763
    def validate_python(self, field_dict, state):
2764
        errors = self._validateReturn(field_dict, state)
2765
        if errors:
2766
            error_list = errors.items()
2767
            error_list.sort()
2768
            raise Invalid(
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:
2776
                raise Invalid(
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('-', '')
2782
        try:
2783
            long(number)
2784
        except ValueError:
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")
2789
        foundValid = False
2790
        validLength = False
2791
        for prefix, length in self._cardInfo[ccType]:
2792
            if len(number) == length:
2793
                validLength = True
2794
            if (len(number) == length
2795
                and number.startswith(prefix)):
2796
                foundValid = True
2797
                break
2798
        if not validLength:
2799
            return {self.cc_number_field: self.message('badLength', state)}
2800
        if not foundValid:
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)}
2805
        return None
2807
    def _validateMod10(self, s):
2808
        """
2809
        This code by Sean Reifschneider, of tummy.com
2810
        """
2811
        double = 0
2812
        sum = 0
2813
        for i in range(len(s) - 1, -1, -1):
2814
            for c in str((double + 1) * int(s[i])):
2815
                sum = sum + int(c)
2816
            double = (double + 1) % 2
2817
        return((sum % 10) == 0)
2819
    _cardInfo = {
2820
        "visa": [('4', 16),
2821
                 ('4', 13)],
2822
        "mastercard": [('51', 16),
2823
                       ('52', 16),
2824
                       ('53', 16),
2825
                       ('54', 16),
2826
                       ('55', 16)],
2827
        "discover": [('6011', 16)],
2828
        "amex": [('34', 15),
2829
                 ('37', 15)],
2830
        "dinersclub": [('300', 14),
2831
                       ('301', 14),
2832
                       ('302', 14),
2833
                       ('303', 14),
2834
                       ('304', 14),
2835
                       ('305', 14),
2836
                       ('36', 14),
2837
                       ('38', 14)],
2838
        "jcb": [('3', 16),
2839
                ('2131', 15),
2840
                ('1800', 15)],
2841
            }
2843
class CreditCardExpires(FormValidator):
2844
    """
2845
    Checks that credit card expiration date is valid relative to
2846
    the current date.
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
2850
    year.
2852
    ::
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):
2859
            ...
2860
        Invalid: ccExpiresMonth: Invalid Expiration Date<br>
2861
        ccExpiresYear: Invalid Expiration Date
2862
    """
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
2872
    messages = {
2873
        'notANumber': _("Please enter numbers only for month and year"),
2874
        'invalidNumber': _("Invalid Expiration Date"),
2875
        }
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):
2880
            return None
2881
        self.validate_python(field_dict, state)
2883
    def validate_python(self, field_dict, state):
2884
        errors = self._validateReturn(field_dict, state)
2885
        if errors:
2886
            error_list = errors.items()
2887
            error_list.sort()
2888
            raise Invalid(
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()
2897
        try:
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
2904
            if next_month == 1:
2905
                next_month_year = ccExpiresYear + 1
2906
            else:
2907
                next_month_year = ccExpiresYear
2908
            expires_date = datetime_makedate(dt_mod, next_month_year, next_month, 1)
2909
            assert expires_date > today
2910
        except ValueError:
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):
2918
    """
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.
2925
    ::
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):
2932
            ...
2933
        Invalid: ccCode: Invalid credit card security code length
2934
    """
2936
    validate_partial_form = True
2938
    cc_type_field = 'ccType'
2939
    cc_code_field = 'ccCode'
2940
    __unpackargs__ = ('cc_type_field', 'cc_code_field')
2942
    messages = {
2943
        'notANumber': _("Please enter numbers only for credit card security code"),
2944
        'badLength': _("Invalid credit card security code length"),
2945
        }
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):
2950
            return None
2951
        self.validate_python(field_dict, state)
2953
    def validate_python(self, field_dict, state):
2954
        errors = self._validateReturn(field_dict, state)
2955
        if errors:
2956
            error_list = errors.items()
2957
            error_list.sort()
2958
            raise Invalid(
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()
2967
        try:
2968
            int(ccCode)
2969
        except ValueError:
2970
            return {self.cc_code_field: self.message('notANumber', state)}
2972
        length = self._cardInfo[ccType]
2973
        validLength = False
2974
        if len(ccCode) == length:
2975
            validLength = True
2976
        if not validLength:
2977
            return {self.cc_code_field: self.message('badLength', state)}
2981
    _cardInfo = {
2982
        "visa": 3,
2983
        "mastercard": 3,
2984
        "discover": 3,
2985
        "amex": 4,
2986
            }
2989
__all__ = ['Invalid']
2990
for name, value in globals().items():
2991
    if isinstance(value, type) and issubclass(value, Validator):
2992
        __all__.append(name)