Package web2py :: Package gluon :: Module validators
[hide private]
[frames] | no frames]

Source Code for Module web2py.gluon.validators

   1  #!/bin/env python 
   2  # -*- coding: utf-8 -*- 
   3   
   4  """ 
   5  This file is part of the web2py Web Framework 
   6  Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu> 
   7  License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) 
   8   
   9  Thanks to ga2arch for help with IS_IN_DB and IS_NOT_IN_DB on GAE 
  10  """ 
  11   
  12  import os 
  13  import re 
  14  import datetime 
  15  import time 
  16  import cgi 
  17  import hmac 
  18  import urllib 
  19  import struct 
  20  import decimal 
  21  import unicodedata 
  22  from cStringIO import StringIO 
  23  from utils import hash, get_digest 
  24   
  25  __all__ = [ 
  26      'CLEANUP', 
  27      'CRYPT', 
  28      'IS_ALPHANUMERIC', 
  29      'IS_DATE_IN_RANGE', 
  30      'IS_DATE', 
  31      'IS_DATETIME_IN_RANGE', 
  32      'IS_DATETIME', 
  33      'IS_DECIMAL_IN_RANGE', 
  34      'IS_EMAIL', 
  35      'IS_EMPTY_OR', 
  36      'IS_EXPR', 
  37      'IS_FLOAT_IN_RANGE', 
  38      'IS_IMAGE', 
  39      'IS_IN_DB', 
  40      'IS_IN_SET', 
  41      'IS_INT_IN_RANGE', 
  42      'IS_IPV4', 
  43      'IS_LENGTH', 
  44      'IS_LIST_OF', 
  45      'IS_LOWER', 
  46      'IS_MATCH', 
  47      'IS_EQUAL_TO', 
  48      'IS_NOT_EMPTY', 
  49      'IS_NOT_IN_DB', 
  50      'IS_NULL_OR', 
  51      'IS_SLUG', 
  52      'IS_STRONG', 
  53      'IS_TIME', 
  54      'IS_UPLOAD_FILENAME', 
  55      'IS_UPPER', 
  56      'IS_URL', 
  57      ] 
  58   
59 -def options_sorter(x,y):
60 return (str(x[1]).upper()>str(y[1]).upper() and 1) or -1
61
62 -class Validator(object):
63 """ 64 Root for all validators, mainly for documentation purposes. 65 66 Validators are classes used to validate input fields (including forms 67 generated from database tables). 68 69 Here is an example of using a validator with a FORM:: 70 71 INPUT(_name='a', requires=IS_INT_IN_RANGE(0, 10)) 72 73 Here is an example of how to require a validator for a table field:: 74 75 db.define_table('person', SQLField('name')) 76 db.person.name.requires=IS_NOT_EMPTY() 77 78 Validators are always assigned using the requires attribute of a field. A 79 field can have a single validator or multiple validators. Multiple 80 validators are made part of a list:: 81 82 db.person.name.requires=[IS_NOT_EMPTY(), IS_NOT_IN_DB(db, 'person.id')] 83 84 Validators are called by the function accepts on a FORM or other HTML 85 helper object that contains a form. They are always called in the order in 86 which they are listed. 87 88 Built-in validators have constructors that take the optional argument error 89 message which allows you to change the default error message. 90 Here is an example of a validator on a database table:: 91 92 db.person.name.requires=IS_NOT_EMPTY(error_message=T('fill this')) 93 94 where we have used the translation operator T to allow for 95 internationalization. 96 97 Notice that default error messages are not translated. 98 """ 99
100 - def formatter(self, value):
101 """ 102 For some validators returns a formatted version (matching the validator) 103 of value. Otherwise just returns the value. 104 """ 105 return value
106 107
108 -class IS_MATCH(Validator):
109 """ 110 example:: 111 112 INPUT(_type='text', _name='name', requires=IS_MATCH('.+')) 113 114 the argument of IS_MATCH is a regular expression:: 115 116 >>> IS_MATCH('.+')('hello') 117 ('hello', None) 118 119 >>> IS_MATCH('.+')('') 120 ('', 'invalid expression') 121 """ 122
123 - def __init__(self, expression, error_message='invalid expression', strict=True):
124 if strict: 125 if not expression.endswith('$'): 126 expression = '(%s)$' % expression 127 self.regex = re.compile(expression) 128 self.error_message = error_message
129
130 - def __call__(self, value):
131 match = self.regex.match(value) 132 if match: 133 return (match.group(), None) 134 return (value, self.error_message)
135 136
137 -class IS_EQUAL_TO(Validator):
138 """ 139 example:: 140 141 INPUT(_type='text', _name='password') 142 INPUT(_type='text', _name='password2', 143 requires=IS_EQUAL_TO(request.vars.password)) 144 145 the argument of IS_EQUAL_TO is a string 146 147 >>> IS_EQUAL_TO('aaa')('aaa') 148 ('aaa', None) 149 150 >>> IS_EQUAL_TO('aaa')('aab') 151 ('aab', 'no match') 152 """ 153
154 - def __init__(self, expression, error_message='no match'):
155 self.expression = expression 156 self.error_message = error_message
157
158 - def __call__(self, value):
159 if value == self.expression: 160 return (value, None) 161 return (value, self.error_message)
162 163
164 -class IS_EXPR(Validator):
165 """ 166 example:: 167 168 INPUT(_type='text', _name='name', 169 requires=IS_EXPR('5 < int(value) < 10')) 170 171 the argument of IS_EXPR must be python condition:: 172 173 >>> IS_EXPR('int(value) < 2')('1') 174 ('1', None) 175 176 >>> IS_EXPR('int(value) < 2')('2') 177 ('2', 'invalid expression') 178 """ 179
180 - def __init__(self, expression, error_message='invalid expression'):
181 self.expression = expression 182 self.error_message = error_message
183
184 - def __call__(self, value):
185 environment = {'value': value} 186 exec '__ret__=' + self.expression in environment 187 if environment['__ret__']: 188 return (value, None) 189 return (value, self.error_message)
190 191
192 -class IS_LENGTH(Validator):
193 """ 194 Checks if length of field's value fits between given boundaries. Works 195 for both text and file inputs. 196 197 Arguments: 198 199 maxsize: maximum allowed length / size 200 minsize: minimum allowed length / size 201 202 Examples:: 203 204 #Check if text string is shorter than 33 characters: 205 INPUT(_type='text', _name='name', requires=IS_LENGTH(32)) 206 207 #Check if password string is longer than 5 characters: 208 INPUT(_type='password', _name='name', requires=IS_LENGTH(minsize=6)) 209 210 #Check if uploaded file has size between 1KB and 1MB: 211 INPUT(_type='file', _name='name', requires=IS_LENGTH(1048576, 1024)) 212 213 >>> IS_LENGTH()('') 214 ('', None) 215 >>> IS_LENGTH()('1234567890') 216 ('1234567890', None) 217 >>> IS_LENGTH(maxsize=5, minsize=0)('1234567890') # too long 218 ('1234567890', 'enter from 0 to 5 characters') 219 >>> IS_LENGTH(maxsize=50, minsize=20)('1234567890') # too short 220 ('1234567890', 'enter from 20 to 50 characters') 221 """ 222
223 - def __init__(self, maxsize=255, minsize=0, error_message='enter from %(min)g to %(max)g characters'):
224 self.maxsize = maxsize 225 self.minsize = minsize 226 self.error_message = error_message % dict(min=minsize, max=maxsize)
227
228 - def __call__(self, value):
229 if isinstance(value, cgi.FieldStorage): 230 if value.file: 231 value.file.seek(0, os.SEEK_END) 232 length = value.file.tell() 233 value.file.seek(0, os.SEEK_SET) 234 else: 235 val = value.value 236 if val: 237 length = len(val) 238 else: 239 length = 0 240 if self.minsize <= length <= self.maxsize: 241 return (value, None) 242 elif isinstance(value, (str, unicode, list)): 243 if self.minsize <= len(value) <= self.maxsize: 244 return (value, None) 245 elif self.minsize <= len(str(value)) <= self.maxsize: 246 try: 247 value.decode('utf8') 248 return (value, None) 249 except: 250 pass 251 return (value, self.error_message)
252 253
254 -class IS_IN_SET(Validator):
255 """ 256 example:: 257 258 INPUT(_type='text', _name='name', 259 requires=IS_IN_SET(['max', 'john'],zero='')) 260 261 the argument of IS_IN_SET must be a list or set 262 263 >>> IS_IN_SET(['max', 'john'])('max') 264 ('max', None) 265 >>> IS_IN_SET(['max', 'john'])('massimo') 266 ('massimo', 'value not allowed') 267 >>> IS_IN_SET(['max', 'john'], multiple=True)(('max', 'john')) 268 (('max', 'john'), None) 269 >>> IS_IN_SET(['max', 'john'], multiple=True)(('bill', 'john')) 270 (('bill', 'john'), 'value not allowed') 271 >>> IS_IN_SET(('id1','id2'), ['first label','second label'])('id1') # Traditional way 272 ('id1', None) 273 >>> IS_IN_SET({'id1':'first label', 'id2':'second label'})('id1') 274 ('id1', None) 275 >>> import itertools 276 >>> IS_IN_SET(itertools.chain(['1','3','5'],['2','4','6']))('1') 277 ('1', None) 278 >>> IS_IN_SET([('id1','first label'), ('id2','second label')])('id1') # Redundant way 279 ('id1', None) 280 """ 281
282 - def __init__( 283 self, 284 theset, 285 labels=None, 286 error_message='value not allowed', 287 multiple=False, 288 zero='', 289 sort=False, 290 ):
291 self.multiple = multiple 292 if isinstance(theset, dict): 293 self.theset = [str(item) for item in theset] 294 self.labels = theset.values() 295 elif theset and isinstance(theset, (tuple,list)) \ 296 and isinstance(theset[0], (tuple,list)) and len(theset[0])==2: 297 self.theset = [str(item) for item,label in theset] 298 self.labels = [str(label) for item,label in theset] 299 else: 300 self.theset = [str(item) for item in theset] 301 self.labels = labels 302 self.error_message = error_message 303 self.zero = zero 304 self.sort = sort
305
306 - def options(self):
307 if not self.labels: 308 items = [(k, k) for (i, k) in enumerate(self.theset)] 309 else: 310 items = [(k, self.labels[i]) for (i, k) in enumerate(self.theset)] 311 if self.sort: 312 items.sort(options_sorter) 313 if self.zero != None and not self.multiple: 314 items.insert(0,('',self.zero)) 315 return items
316
317 - def __call__(self, value):
318 if self.multiple: 319 ### if below was values = re.compile("[\w\-:]+").findall(str(value)) 320 if isinstance(value, (str,unicode)): 321 values = [value] 322 elif isinstance(value, (tuple, list)): 323 values = value 324 elif not value: 325 values = [] 326 else: 327 values = [value] 328 failures = [x for x in values if not x in self.theset] 329 if failures and self.theset: 330 if self.multiple and (value == None or value == ''): 331 return ([], None) 332 return (value, self.error_message) 333 if self.multiple: 334 if isinstance(self.multiple,(tuple,list)) and \ 335 not self.multiple[0]<=len(values)<self.multiple[1]: 336 return (values, self.error_message) 337 return (values, None) 338 return (value, None)
339 340 341 regex1 = re.compile('[\w_]+\.[\w_]+') 342 regex2 = re.compile('%\((?P<name>[^\)]+)\)s') 343 344
345 -class IS_IN_DB(Validator):
346 """ 347 example:: 348 349 INPUT(_type='text', _name='name', 350 requires=IS_IN_DB(db, db.mytable.myfield, zero='')) 351 352 used for reference fields, rendered as a dropbox 353 """ 354
355 - def __init__( 356 self, 357 dbset, 358 field, 359 label=None, 360 error_message='value not in database', 361 orderby=None, 362 groupby=None, 363 cache=None, 364 multiple=False, 365 zero='', 366 sort=False, 367 _and=None, 368 ):
369 from gluon.dal import Table 370 if isinstance(field,Table): field = field._id 371 372 if hasattr(dbset, 'define_table'): 373 self.dbset = dbset() 374 else: 375 self.dbset = dbset 376 self.field = field 377 (ktable, kfield) = str(self.field).split('.') 378 if not label: 379 label = '%%(%s)s' % kfield 380 if isinstance(label,str): 381 if regex1.match(str(label)): 382 label = '%%(%s)s' % str(label).split('.')[-1] 383 ks = regex2.findall(label) 384 if not kfield in ks: 385 ks += [kfield] 386 fields = ks 387 else: 388 ks = [kfield] 389 fields = 'all' 390 self.fields = fields 391 self.label = label 392 self.ktable = ktable 393 self.kfield = kfield 394 self.ks = ks 395 self.error_message = error_message 396 self.theset = None 397 self.orderby = orderby 398 self.groupby = groupby 399 self.cache = cache 400 self.multiple = multiple 401 self.zero = zero 402 self.sort = sort 403 self._and = _and
404
405 - def set_self_id(self, id):
406 if self._and: 407 self._and.record_id = id
408
409 - def build_set(self):
410 if self.fields == 'all': 411 fields = [f for f in self.dbset.db[self.ktable]] 412 else: 413 fields = [self.dbset.db[self.ktable][k] for k in self.fields] 414 if self.dbset.db._dbname != 'gae': 415 orderby = self.orderby or reduce(lambda a,b:a|b,fields) 416 groupby = self.groupby 417 dd = dict(orderby=orderby, groupby=groupby, cache=self.cache) 418 records = self.dbset.select(*fields, **dd) 419 else: 420 orderby = self.orderby or reduce(lambda a,b:a|b,(f for f in fields if not f.name=='id')) 421 dd = dict(orderby=orderby, cache=self.cache) 422 records = self.dbset.select(self.dbset.db[self.ktable].ALL, **dd) 423 self.theset = [str(r[self.kfield]) for r in records] 424 if isinstance(self.label,str): 425 self.labels = [self.label % dict(r) for r in records] 426 else: 427 self.labels = [self.label(r) for r in records]
428
429 - def options(self):
430 self.build_set() 431 items = [(k, self.labels[i]) for (i, k) in enumerate(self.theset)] 432 if self.sort: 433 items.sort(options_sorter) 434 if self.zero != None and not self.multiple: 435 items.insert(0,('',self.zero)) 436 return items
437
438 - def __call__(self, value):
439 if self.multiple: 440 if isinstance(value,list): 441 values=value 442 elif value: 443 values = [value] 444 else: 445 values = [] 446 if isinstance(self.multiple,(tuple,list)) and \ 447 not self.multiple[0]<=len(values)<self.multiple[1]: 448 return (values, self.error_message) 449 if not [x for x in values if not x in self.theset]: 450 return (values, None) 451 elif self.theset: 452 if value in self.theset: 453 if self._and: 454 return self._and(value) 455 else: 456 return (value, None) 457 else: 458 (ktable, kfield) = str(self.field).split('.') 459 field = self.dbset.db[ktable][kfield] 460 if self.dbset(field == value).count(): 461 if self._and: 462 return self._and(value) 463 else: 464 return (value, None) 465 return (value, self.error_message)
466 467
468 -class IS_NOT_IN_DB(Validator):
469 """ 470 example:: 471 472 INPUT(_type='text', _name='name', requires=IS_NOT_IN_DB(db, db.table)) 473 474 makes the field unique 475 """ 476
477 - def __init__( 478 self, 479 dbset, 480 field, 481 error_message='value already in database or empty', 482 allowed_override=[], 483 ):
484 485 from gluon.dal import Table 486 if isinstance(field,Table): field = field._id 487 488 if hasattr(dbset, 'define_table'): 489 self.dbset = dbset() 490 else: 491 self.dbset = dbset 492 self.field = field 493 self.error_message = error_message 494 self.record_id = 0 495 self.allowed_override = allowed_override
496
497 - def set_self_id(self, id):
498 self.record_id = id
499
500 - def __call__(self, value):
501 value=str(value) 502 if not value.strip(): 503 return (value, self.error_message) 504 if value in self.allowed_override: 505 return (value, None) 506 (tablename, fieldname) = str(self.field).split('.') 507 field = self.dbset.db[tablename][fieldname] 508 rows = self.dbset(field == value).select(limitby=(0, 1)) 509 if len(rows) > 0: 510 if isinstance(self.record_id, dict): 511 for f in self.record_id: 512 if str(getattr(rows[0], f)) != str(self.record_id[f]): 513 return (value, self.error_message) 514 elif str(rows[0].id) != str(self.record_id): 515 return (value, self.error_message) 516 return (value, None)
517 518
519 -class IS_INT_IN_RANGE(Validator):
520 """ 521 Determine that the argument is (or can be represented as) an int, 522 and that it falls within the specified range. The range is interpreted 523 in the Pythonic way, so the test is: min <= value < max. 524 525 The minimum and maximum limits can be None, meaning no lower or upper limit, 526 respectively. 527 528 example:: 529 530 INPUT(_type='text', _name='name', requires=IS_INT_IN_RANGE(0, 10)) 531 532 >>> IS_INT_IN_RANGE(1,5)('4') 533 (4, None) 534 >>> IS_INT_IN_RANGE(1,5)(4) 535 (4, None) 536 >>> IS_INT_IN_RANGE(1,5)(1) 537 (1, None) 538 >>> IS_INT_IN_RANGE(1,5)(5) 539 (5, 'enter an integer between 1 and 4') 540 >>> IS_INT_IN_RANGE(1,5)(5) 541 (5, 'enter an integer between 1 and 4') 542 >>> IS_INT_IN_RANGE(1,5)(3.5) 543 (3, 'enter an integer between 1 and 4') 544 >>> IS_INT_IN_RANGE(None,5)('4') 545 (4, None) 546 >>> IS_INT_IN_RANGE(None,5)('6') 547 (6, 'enter an integer less than or equal to 4') 548 >>> IS_INT_IN_RANGE(1,None)('4') 549 (4, None) 550 >>> IS_INT_IN_RANGE(1,None)('0') 551 (0, 'enter an integer greater than or equal to 1') 552 >>> IS_INT_IN_RANGE()(6) 553 (6, None) 554 >>> IS_INT_IN_RANGE()('abc') 555 ('abc', 'enter an integer') 556 """ 557
558 - def __init__( 559 self, 560 minimum=None, 561 maximum=None, 562 error_message=None, 563 ):
564 self.minimum = self.maximum = None 565 if minimum is None: 566 if maximum is None: 567 self.error_message = error_message or 'enter an integer' 568 else: 569 self.maximum = int(maximum) 570 if error_message is None: 571 error_message = 'enter an integer less than or equal to %(max)g' 572 self.error_message = error_message % dict(max=self.maximum-1) 573 elif maximum is None: 574 self.minimum = int(minimum) 575 if error_message is None: 576 error_message = 'enter an integer greater than or equal to %(min)g' 577 self.error_message = error_message % dict(min=self.minimum) 578 else: 579 self.minimum = int(minimum) 580 self.maximum = int(maximum) 581 if error_message is None: 582 error_message = 'enter an integer between %(min)g and %(max)g' 583 self.error_message = error_message % dict(min=self.minimum, max=self.maximum-1)
584
585 - def __call__(self, value):
586 try: 587 fvalue = float(value) 588 value = int(value) 589 if value != fvalue: 590 return (value, self.error_message) 591 if self.minimum is None: 592 if self.maximum is None or value < self.maximum: 593 return (value, None) 594 elif self.maximum is None: 595 if value >= self.minimum: 596 return (value, None) 597 elif self.minimum <= value < self.maximum: 598 return (value, None) 599 except ValueError: 600 pass 601 return (value, self.error_message)
602 603
604 -class IS_FLOAT_IN_RANGE(Validator):
605 """ 606 Determine that the argument is (or can be represented as) a float, 607 and that it falls within the specified inclusive range. 608 The comparison is made with native arithmetic. 609 610 The minimum and maximum limits can be None, meaning no lower or upper limit, 611 respectively. 612 613 example:: 614 615 INPUT(_type='text', _name='name', requires=IS_FLOAT_IN_RANGE(0, 10)) 616 617 >>> IS_FLOAT_IN_RANGE(1,5)('4') 618 (4.0, None) 619 >>> IS_FLOAT_IN_RANGE(1,5)(4) 620 (4.0, None) 621 >>> IS_FLOAT_IN_RANGE(1,5)(1) 622 (1.0, None) 623 >>> IS_FLOAT_IN_RANGE(1,5)(5.25) 624 (5.25, 'enter a number between 1 and 5') 625 >>> IS_FLOAT_IN_RANGE(1,5)(6.0) 626 (6.0, 'enter a number between 1 and 5') 627 >>> IS_FLOAT_IN_RANGE(1,5)(3.5) 628 (3.5, None) 629 >>> IS_FLOAT_IN_RANGE(1,None)(3.5) 630 (3.5, None) 631 >>> IS_FLOAT_IN_RANGE(None,5)(3.5) 632 (3.5, None) 633 >>> IS_FLOAT_IN_RANGE(1,None)(0.5) 634 (0.5, 'enter a number greater than or equal to 1') 635 >>> IS_FLOAT_IN_RANGE(None,5)(6.5) 636 (6.5, 'enter a number less than or equal to 5') 637 >>> IS_FLOAT_IN_RANGE()(6.5) 638 (6.5, None) 639 >>> IS_FLOAT_IN_RANGE()('abc') 640 ('abc', 'enter a number') 641 """ 642
643 - def __init__( 644 self, 645 minimum=None, 646 maximum=None, 647 error_message=None, 648 dot='.' 649 ):
650 self.minimum = self.maximum = None 651 self.dot = dot 652 if minimum is None: 653 if maximum is None: 654 if error_message is None: 655 error_message = 'enter a number' 656 else: 657 self.maximum = float(maximum) 658 if error_message is None: 659 error_message = 'enter a number less than or equal to %(max)g' 660 elif maximum is None: 661 self.minimum = float(minimum) 662 if error_message is None: 663 error_message = 'enter a number greater than or equal to %(min)g' 664 else: 665 self.minimum = float(minimum) 666 self.maximum = float(maximum) 667 if error_message is None: 668 error_message = 'enter a number between %(min)g and %(max)g' 669 self.error_message = error_message % dict(min=self.minimum, max=self.maximum)
670
671 - def __call__(self, value):
672 try: 673 if self.dot=='.': 674 fvalue = float(value) 675 else: 676 fvalue = float(str(value).replace(self.dot,'.')) 677 if self.minimum is None: 678 if self.maximum is None or fvalue <= self.maximum: 679 return (fvalue, None) 680 elif self.maximum is None: 681 if fvalue >= self.minimum: 682 return (fvalue, None) 683 elif self.minimum <= fvalue <= self.maximum: 684 return (fvalue, None) 685 except (ValueError, TypeError): 686 pass 687 return (value, self.error_message)
688
689 - def formatter(self,value):
690 if self.dot=='.': 691 return str(value) 692 else: 693 return str(value).replace('.',self.dot)
694 695
696 -class IS_DECIMAL_IN_RANGE(Validator):
697 """ 698 Determine that the argument is (or can be represented as) a Python Decimal, 699 and that it falls within the specified inclusive range. 700 The comparison is made with Python Decimal arithmetic. 701 702 The minimum and maximum limits can be None, meaning no lower or upper limit, 703 respectively. 704 705 example:: 706 707 INPUT(_type='text', _name='name', requires=IS_DECIMAL_IN_RANGE(0, 10)) 708 709 >>> IS_DECIMAL_IN_RANGE(1,5)('4') 710 (Decimal('4'), None) 711 >>> IS_DECIMAL_IN_RANGE(1,5)(4) 712 (Decimal('4'), None) 713 >>> IS_DECIMAL_IN_RANGE(1,5)(1) 714 (Decimal('1'), None) 715 >>> IS_DECIMAL_IN_RANGE(1,5)(5.25) 716 (5.25, 'enter a number between 1 and 5') 717 >>> IS_DECIMAL_IN_RANGE(5.25,6)(5.25) 718 (Decimal('5.25'), None) 719 >>> IS_DECIMAL_IN_RANGE(5.25,6)('5.25') 720 (Decimal('5.25'), None) 721 >>> IS_DECIMAL_IN_RANGE(1,5)(6.0) 722 (6.0, 'enter a number between 1 and 5') 723 >>> IS_DECIMAL_IN_RANGE(1,5)(3.5) 724 (Decimal('3.5'), None) 725 >>> IS_DECIMAL_IN_RANGE(1.5,5.5)(3.5) 726 (Decimal('3.5'), None) 727 >>> IS_DECIMAL_IN_RANGE(1.5,5.5)(6.5) 728 (6.5, 'enter a number between 1.5 and 5.5') 729 >>> IS_DECIMAL_IN_RANGE(1.5,None)(6.5) 730 (Decimal('6.5'), None) 731 >>> IS_DECIMAL_IN_RANGE(1.5,None)(0.5) 732 (0.5, 'enter a number greater than or equal to 1.5') 733 >>> IS_DECIMAL_IN_RANGE(None,5.5)(4.5) 734 (Decimal('4.5'), None) 735 >>> IS_DECIMAL_IN_RANGE(None,5.5)(6.5) 736 (6.5, 'enter a number less than or equal to 5.5') 737 >>> IS_DECIMAL_IN_RANGE()(6.5) 738 (Decimal('6.5'), None) 739 >>> IS_DECIMAL_IN_RANGE(0,99)(123.123) 740 (123.123, 'enter a number between 0 and 99') 741 >>> IS_DECIMAL_IN_RANGE(0,99)('123.123') 742 ('123.123', 'enter a number between 0 and 99') 743 >>> IS_DECIMAL_IN_RANGE(0,99)('12.34') 744 (Decimal('12.34'), None) 745 >>> IS_DECIMAL_IN_RANGE()('abc') 746 ('abc', 'enter a decimal number') 747 """ 748
749 - def __init__( 750 self, 751 minimum=None, 752 maximum=None, 753 error_message=None, 754 dot='.' 755 ):
756 self.minimum = self.maximum = None 757 self.dot = dot 758 if minimum is None: 759 if maximum is None: 760 if error_message is None: 761 error_message = 'enter a decimal number' 762 else: 763 self.maximum = decimal.Decimal(str(maximum)) 764 if error_message is None: 765 error_message = 'enter a number less than or equal to %(max)g' 766 elif maximum is None: 767 self.minimum = decimal.Decimal(str(minimum)) 768 if error_message is None: 769 error_message = 'enter a number greater than or equal to %(min)g' 770 else: 771 self.minimum = decimal.Decimal(str(minimum)) 772 self.maximum = decimal.Decimal(str(maximum)) 773 if error_message is None: 774 error_message = 'enter a number between %(min)g and %(max)g' 775 self.error_message = error_message % dict(min=self.minimum, max=self.maximum)
776
777 - def __call__(self, value):
778 try: 779 if self.dot=='.': 780 v = decimal.Decimal(str(value)) 781 else: 782 v = decimal.Decimal(str(value).replace(self.dot,'.')) 783 if self.minimum is None: 784 if self.maximum is None or v <= self.maximum: 785 return (v, None) 786 elif self.maximum is None: 787 if v >= self.minimum: 788 return (v, None) 789 elif self.minimum <= v <= self.maximum: 790 return (v, None) 791 except (ValueError, TypeError, decimal.InvalidOperation): 792 pass 793 return (value, self.error_message)
794
795 - def formatter(self, value):
796 return str(value).replace('.',self.dot)
797
798 -def is_empty(value, empty_regex=None):
799 "test empty field" 800 if isinstance(value, (str, unicode)): 801 value = value.strip() 802 if empty_regex is not None and empty_regex.match(value): 803 value = '' 804 if value == None or value == '' or value == []: 805 return (value, True) 806 return (value, False)
807
808 -class IS_NOT_EMPTY(Validator):
809 """ 810 example:: 811 812 INPUT(_type='text', _name='name', requires=IS_NOT_EMPTY()) 813 814 >>> IS_NOT_EMPTY()(1) 815 (1, None) 816 >>> IS_NOT_EMPTY()(0) 817 (0, None) 818 >>> IS_NOT_EMPTY()('x') 819 ('x', None) 820 >>> IS_NOT_EMPTY()(' x ') 821 ('x', None) 822 >>> IS_NOT_EMPTY()(None) 823 (None, 'enter a value') 824 >>> IS_NOT_EMPTY()('') 825 ('', 'enter a value') 826 >>> IS_NOT_EMPTY()(' ') 827 ('', 'enter a value') 828 >>> IS_NOT_EMPTY()(' \\n\\t') 829 ('', 'enter a value') 830 >>> IS_NOT_EMPTY()([]) 831 ([], 'enter a value') 832 >>> IS_NOT_EMPTY(empty_regex='def')('def') 833 ('', 'enter a value') 834 >>> IS_NOT_EMPTY(empty_regex='de[fg]')('deg') 835 ('', 'enter a value') 836 >>> IS_NOT_EMPTY(empty_regex='def')('abc') 837 ('abc', None) 838 """ 839
840 - def __init__(self, error_message='enter a value', empty_regex=None):
841 self.error_message = error_message 842 if empty_regex is not None: 843 self.empty_regex = re.compile(empty_regex) 844 else: 845 self.empty_regex = None
846
847 - def __call__(self, value):
848 value, empty = is_empty(value, empty_regex=self.empty_regex) 849 if empty: 850 return (value, self.error_message) 851 return (value, None)
852 853
854 -class IS_ALPHANUMERIC(IS_MATCH):
855 """ 856 example:: 857 858 INPUT(_type='text', _name='name', requires=IS_ALPHANUMERIC()) 859 860 >>> IS_ALPHANUMERIC()('1') 861 ('1', None) 862 >>> IS_ALPHANUMERIC()('') 863 ('', None) 864 >>> IS_ALPHANUMERIC()('A_a') 865 ('A_a', None) 866 >>> IS_ALPHANUMERIC()('!') 867 ('!', 'enter only letters, numbers, and underscore') 868 """ 869
870 - def __init__(self, error_message='enter only letters, numbers, and underscore'):
871 IS_MATCH.__init__(self, '^[\w]*$', error_message)
872 873
874 -class IS_EMAIL(Validator):
875 """ 876 Checks if field's value is a valid email address. Can be set to disallow 877 or force addresses from certain domain(s). 878 879 Email regex adapted from 880 http://haacked.com/archive/2007/08/21/i-knew-how-to-validate-an-email-address-until-i.aspx, 881 generally following the RFCs, except that we disallow quoted strings 882 and permit underscores and leading numerics in subdomain labels 883 884 Arguments: 885 886 - banned: regex text for disallowed address domains 887 - forced: regex text for required address domains 888 889 Both arguments can also be custom objects with a match(value) method. 890 891 Examples:: 892 893 #Check for valid email address: 894 INPUT(_type='text', _name='name', 895 requires=IS_EMAIL()) 896 897 #Check for valid email address that can't be from a .com domain: 898 INPUT(_type='text', _name='name', 899 requires=IS_EMAIL(banned='^.*\.com(|\..*)$')) 900 901 #Check for valid email address that must be from a .edu domain: 902 INPUT(_type='text', _name='name', 903 requires=IS_EMAIL(forced='^.*\.edu(|\..*)$')) 904 905 >>> IS_EMAIL()('a@b.com') 906 ('a@b.com', None) 907 >>> IS_EMAIL()('abc@def.com') 908 ('abc@def.com', None) 909 >>> IS_EMAIL()('abc@3def.com') 910 ('abc@3def.com', None) 911 >>> IS_EMAIL()('abc@def.us') 912 ('abc@def.us', None) 913 >>> IS_EMAIL()('abc@d_-f.us') 914 ('abc@d_-f.us', None) 915 >>> IS_EMAIL()('@def.com') # missing name 916 ('@def.com', 'enter a valid email address') 917 >>> IS_EMAIL()('"abc@def".com') # quoted name 918 ('"abc@def".com', 'enter a valid email address') 919 >>> IS_EMAIL()('abc+def.com') # no @ 920 ('abc+def.com', 'enter a valid email address') 921 >>> IS_EMAIL()('abc@def.x') # one-char TLD 922 ('abc@def.x', 'enter a valid email address') 923 >>> IS_EMAIL()('abc@def.12') # numeric TLD 924 ('abc@def.12', 'enter a valid email address') 925 >>> IS_EMAIL()('abc@def..com') # double-dot in domain 926 ('abc@def..com', 'enter a valid email address') 927 >>> IS_EMAIL()('abc@.def.com') # dot starts domain 928 ('abc@.def.com', 'enter a valid email address') 929 >>> IS_EMAIL()('abc@def.c_m') # underscore in TLD 930 ('abc@def.c_m', 'enter a valid email address') 931 >>> IS_EMAIL()('NotAnEmail') # missing @ 932 ('NotAnEmail', 'enter a valid email address') 933 >>> IS_EMAIL()('abc@NotAnEmail') # missing TLD 934 ('abc@NotAnEmail', 'enter a valid email address') 935 >>> IS_EMAIL()('customer/department@example.com') 936 ('customer/department@example.com', None) 937 >>> IS_EMAIL()('$A12345@example.com') 938 ('$A12345@example.com', None) 939 >>> IS_EMAIL()('!def!xyz%abc@example.com') 940 ('!def!xyz%abc@example.com', None) 941 >>> IS_EMAIL()('_Yosemite.Sam@example.com') 942 ('_Yosemite.Sam@example.com', None) 943 >>> IS_EMAIL()('~@example.com') 944 ('~@example.com', None) 945 >>> IS_EMAIL()('.wooly@example.com') # dot starts name 946 ('.wooly@example.com', 'enter a valid email address') 947 >>> IS_EMAIL()('wo..oly@example.com') # adjacent dots in name 948 ('wo..oly@example.com', 'enter a valid email address') 949 >>> IS_EMAIL()('pootietang.@example.com') # dot ends name 950 ('pootietang.@example.com', 'enter a valid email address') 951 >>> IS_EMAIL()('.@example.com') # name is bare dot 952 ('.@example.com', 'enter a valid email address') 953 >>> IS_EMAIL()('Ima.Fool@example.com') 954 ('Ima.Fool@example.com', None) 955 >>> IS_EMAIL()('Ima Fool@example.com') # space in name 956 ('Ima Fool@example.com', 'enter a valid email address') 957 >>> IS_EMAIL()('localguy@localhost') # localhost as domain 958 ('localguy@localhost', None) 959 960 """ 961 962 regex = re.compile(''' 963 ^(?!\.) # name may not begin with a dot 964 ( 965 [-a-z0-9!\#$%&'*+/=?^_`{|}~] # all legal characters except dot 966 | 967 (?<!\.)\. # single dots only 968 )+ 969 (?<!\.) # name may not end with a dot 970 @ 971 ( 972 localhost 973 | 974 ( 975 [a-z0-9] # [sub]domain begins with alphanumeric 976 ( 977 [-\w]* # alphanumeric, underscore, dot, hyphen 978 [a-z0-9] # ending alphanumeric 979 )? 980 \. # ending dot 981 )+ 982 [a-z]{2,} # TLD alpha-only 983 )$ 984 ''', re.VERBOSE|re.IGNORECASE) 985 986 regex_proposed_but_failed = re.compile('^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$',re.VERBOSE|re.IGNORECASE) 987
988 - def __init__(self, 989 banned=None, 990 forced=None, 991 error_message='enter a valid email address'):
992 if isinstance(banned, str): 993 banned = re.compile(banned) 994 if isinstance(forced, str): 995 forced = re.compile(forced) 996 self.banned = banned 997 self.forced = forced 998 self.error_message = error_message
999
1000 - def __call__(self, value):
1001 match = self.regex.match(value) 1002 if match: 1003 domain = value.split('@')[1] 1004 if (not self.banned or not self.banned.match(domain)) \ 1005 and (not self.forced or self.forced.match(domain)): 1006 return (value, None) 1007 return (value, self.error_message)
1008 1009 1010 # URL scheme source: 1011 # <http://en.wikipedia.org/wiki/URI_scheme> obtained on 2008-Nov-10 1012 1013 official_url_schemes = [ 1014 'aaa', 1015 'aaas', 1016 'acap', 1017 'cap', 1018 'cid', 1019 'crid', 1020 'data', 1021 'dav', 1022 'dict', 1023 'dns', 1024 'fax', 1025 'file', 1026 'ftp', 1027 'go', 1028 'gopher', 1029 'h323', 1030 'http', 1031 'https', 1032 'icap', 1033 'im', 1034 'imap', 1035 'info', 1036 'ipp', 1037 'iris', 1038 'iris.beep', 1039 'iris.xpc', 1040 'iris.xpcs', 1041 'iris.lws', 1042 'ldap', 1043 'mailto', 1044 'mid', 1045 'modem', 1046 'msrp', 1047 'msrps', 1048 'mtqp', 1049 'mupdate', 1050 'news', 1051 'nfs', 1052 'nntp', 1053 'opaquelocktoken', 1054 'pop', 1055 'pres', 1056 'prospero', 1057 'rtsp', 1058 'service', 1059 'shttp', 1060 'sip', 1061 'sips', 1062 'snmp', 1063 'soap.beep', 1064 'soap.beeps', 1065 'tag', 1066 'tel', 1067 'telnet', 1068 'tftp', 1069 'thismessage', 1070 'tip', 1071 'tv', 1072 'urn', 1073 'vemmi', 1074 'wais', 1075 'xmlrpc.beep', 1076 'xmlrpc.beep', 1077 'xmpp', 1078 'z39.50r', 1079 'z39.50s', 1080 ] 1081 unofficial_url_schemes = [ 1082 'about', 1083 'adiumxtra', 1084 'aim', 1085 'afp', 1086 'aw', 1087 'callto', 1088 'chrome', 1089 'cvs', 1090 'ed2k', 1091 'feed', 1092 'fish', 1093 'gg', 1094 'gizmoproject', 1095 'iax2', 1096 'irc', 1097 'ircs', 1098 'itms', 1099 'jar', 1100 'javascript', 1101 'keyparc', 1102 'lastfm', 1103 'ldaps', 1104 'magnet', 1105 'mms', 1106 'msnim', 1107 'mvn', 1108 'notes', 1109 'nsfw', 1110 'psyc', 1111 'paparazzi:http', 1112 'rmi', 1113 'rsync', 1114 'secondlife', 1115 'sgn', 1116 'skype', 1117 'ssh', 1118 'sftp', 1119 'smb', 1120 'sms', 1121 'soldat', 1122 'steam', 1123 'svn', 1124 'teamspeak', 1125 'unreal', 1126 'ut2004', 1127 'ventrilo', 1128 'view-source', 1129 'webcal', 1130 'wyciwyg', 1131 'xfire', 1132 'xri', 1133 'ymsgr', 1134 ] 1135 all_url_schemes = [None] + official_url_schemes + unofficial_url_schemes 1136 http_schemes = [None, 'http', 'https'] 1137 1138 1139 # This regex comes from RFC 2396, Appendix B. It's used to split a URL into 1140 # its component parts 1141 # Here are the regex groups that it extracts: 1142 # scheme = group(2) 1143 # authority = group(4) 1144 # path = group(5) 1145 # query = group(7) 1146 # fragment = group(9) 1147 1148 url_split_regex = \ 1149 re.compile('^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?') 1150 1151 # Defined in RFC 3490, Section 3.1, Requirement #1 1152 # Use this regex to split the authority component of a unicode URL into 1153 # its component labels 1154 label_split_regex = re.compile(u'[\u002e\u3002\uff0e\uff61]') 1155 1156
1157 -def escape_unicode(string):
1158 ''' 1159 Converts a unicode string into US-ASCII, using a simple conversion scheme. 1160 Each unicode character that does not have a US-ASCII equivalent is 1161 converted into a URL escaped form based on its hexadecimal value. 1162 For example, the unicode character '\u4e86' will become the string '%4e%86' 1163 1164 :param string: unicode string, the unicode string to convert into an 1165 escaped US-ASCII form 1166 :returns: the US-ASCII escaped form of the inputted string 1167 :rtype: string 1168 1169 @author: Jonathan Benn 1170 ''' 1171 returnValue = StringIO() 1172 1173 for character in string: 1174 code = ord(character) 1175 if code > 0x7F: 1176 hexCode = hex(code) 1177 returnValue.write('%' + hexCode[2:4] + '%' + hexCode[4:6]) 1178 else: 1179 returnValue.write(character) 1180 1181 return returnValue.getvalue()
1182 1183
1184 -def unicode_to_ascii_authority(authority):
1185 ''' 1186 Follows the steps in RFC 3490, Section 4 to convert a unicode authority 1187 string into its ASCII equivalent. 1188 For example, u'www.Alliancefran\xe7aise.nu' will be converted into 1189 'www.xn--alliancefranaise-npb.nu' 1190 1191 :param authority: unicode string, the URL authority component to convert, 1192 e.g. u'www.Alliancefran\xe7aise.nu' 1193 :returns: the US-ASCII character equivalent to the inputed authority, 1194 e.g. 'www.xn--alliancefranaise-npb.nu' 1195 :rtype: string 1196 :raises Exception: if the function is not able to convert the inputed 1197 authority 1198 1199 @author: Jonathan Benn 1200 ''' 1201 #RFC 3490, Section 4, Step 1 1202 #The encodings.idna Python module assumes that AllowUnassigned == True 1203 1204 #RFC 3490, Section 4, Step 2 1205 labels = label_split_regex.split(authority) 1206 1207 #RFC 3490, Section 4, Step 3 1208 #The encodings.idna Python module assumes that UseSTD3ASCIIRules == False 1209 1210 #RFC 3490, Section 4, Step 4 1211 #We use the ToASCII operation because we are about to put the authority 1212 #into an IDN-unaware slot 1213 asciiLabels = [] 1214 try: 1215 import encodings.idna 1216 for label in labels: 1217 if label: 1218 asciiLabels.append(encodings.idna.ToASCII(label)) 1219 else: 1220 #encodings.idna.ToASCII does not accept an empty string, but 1221 #it is necessary for us to allow for empty labels so that we 1222 #don't modify the URL 1223 asciiLabels.append('') 1224 except: 1225 asciiLabels=[str(label) for label in labels] 1226 #RFC 3490, Section 4, Step 5 1227 return str(reduce(lambda x, y: x + unichr(0x002E) + y, asciiLabels))
1228 1229
1230 -def unicode_to_ascii_url(url, prepend_scheme):
1231 ''' 1232 Converts the inputed unicode url into a US-ASCII equivalent. This function 1233 goes a little beyond RFC 3490, which is limited in scope to the domain name 1234 (authority) only. Here, the functionality is expanded to what was observed 1235 on Wikipedia on 2009-Jan-22: 1236 1237 Component Can Use Unicode? 1238 --------- ---------------- 1239 scheme No 1240 authority Yes 1241 path Yes 1242 query Yes 1243 fragment No 1244 1245 The authority component gets converted to punycode, but occurrences of 1246 unicode in other components get converted into a pair of URI escapes (we 1247 assume 4-byte unicode). E.g. the unicode character U+4E2D will be 1248 converted into '%4E%2D'. Testing with Firefox v3.0.5 has shown that it can 1249 understand this kind of URI encoding. 1250 1251 :param url: unicode string, the URL to convert from unicode into US-ASCII 1252 :param prepend_scheme: string, a protocol scheme to prepend to the URL if 1253 we're having trouble parsing it. 1254 e.g. "http". Input None to disable this functionality 1255 :returns: a US-ASCII equivalent of the inputed url 1256 :rtype: string 1257 1258 @author: Jonathan Benn 1259 ''' 1260 #convert the authority component of the URL into an ASCII punycode string, 1261 #but encode the rest using the regular URI character encoding 1262 1263 groups = url_split_regex.match(url).groups() 1264 #If no authority was found 1265 if not groups[3]: 1266 #Try appending a scheme to see if that fixes the problem 1267 scheme_to_prepend = prepend_scheme or 'http' 1268 groups = url_split_regex.match( 1269 unicode(scheme_to_prepend) + u'://' + url).groups() 1270 #if we still can't find the authority 1271 if not groups[3]: 1272 raise Exception('No authority component found, '+ \ 1273 'could not decode unicode to US-ASCII') 1274 1275 #We're here if we found an authority, let's rebuild the URL 1276 scheme = groups[1] 1277 authority = groups[3] 1278 path = groups[4] or '' 1279 query = groups[5] or '' 1280 fragment = groups[7] or '' 1281 1282 if prepend_scheme: 1283 scheme = str(scheme) + '://' 1284 else: 1285 scheme = '' 1286 return scheme + unicode_to_ascii_authority(authority) +\ 1287 escape_unicode(path) + escape_unicode(query) + str(fragment)
1288 1289
1290 -class IS_GENERIC_URL(Validator):
1291 """ 1292 Rejects a URL string if any of the following is true: 1293 * The string is empty or None 1294 * The string uses characters that are not allowed in a URL 1295 * The URL scheme specified (if one is specified) is not valid 1296 1297 Based on RFC 2396: http://www.faqs.org/rfcs/rfc2396.html 1298 1299 This function only checks the URL's syntax. It does not check that the URL 1300 points to a real document, for example, or that it otherwise makes sense 1301 semantically. This function does automatically prepend 'http://' in front 1302 of a URL if and only if that's necessary to successfully parse the URL. 1303 Please note that a scheme will be prepended only for rare cases 1304 (e.g. 'google.ca:80') 1305 1306 The list of allowed schemes is customizable with the allowed_schemes 1307 parameter. If you exclude None from the list, then abbreviated URLs 1308 (lacking a scheme such as 'http') will be rejected. 1309 1310 The default prepended scheme is customizable with the prepend_scheme 1311 parameter. If you set prepend_scheme to None then prepending will be 1312 disabled. URLs that require prepending to parse will still be accepted, 1313 but the return value will not be modified. 1314 1315 @author: Jonathan Benn 1316 1317 >>> IS_GENERIC_URL()('http://user@abc.com') 1318 ('http://user@abc.com', None) 1319 1320 """ 1321
1322 - def __init__( 1323 self, 1324 error_message='enter a valid URL', 1325 allowed_schemes=None, 1326 prepend_scheme=None, 1327 ):
1328 """ 1329 :param error_message: a string, the error message to give the end user 1330 if the URL does not validate 1331 :param allowed_schemes: a list containing strings or None. Each element 1332 is a scheme the inputed URL is allowed to use 1333 :param prepend_scheme: a string, this scheme is prepended if it's 1334 necessary to make the URL valid 1335 """ 1336 1337 self.error_message = error_message 1338 if allowed_schemes == None: 1339 self.allowed_schemes = all_url_schemes 1340 else: 1341 self.allowed_schemes = allowed_schemes 1342 self.prepend_scheme = prepend_scheme 1343 if self.prepend_scheme not in self.allowed_schemes: 1344 raise SyntaxError, \ 1345 "prepend_scheme='%s' is not in allowed_schemes=%s" \ 1346 % (self.prepend_scheme, self.allowed_schemes)
1347
1348 - def __call__(self, value):
1349 """ 1350 :param value: a string, the URL to validate 1351 :returns: a tuple, where tuple[0] is the inputed value (possible 1352 prepended with prepend_scheme), and tuple[1] is either 1353 None (success!) or the string error_message 1354 """ 1355 try: 1356 # if the URL does not misuse the '%' character 1357 if not re.compile( 1358 r"%[^0-9A-Fa-f]{2}|%[^0-9A-Fa-f][0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]|%$|%[0-9A-Fa-f]$|%[^0-9A-Fa-f]$" 1359 ).search(value): 1360 # if the URL is only composed of valid characters 1361 if re.compile( 1362 r"[A-Za-z0-9;/?:@&=+$,\-_\.!~*'\(\)%#]+$").match(value): 1363 # Then split up the URL into its components and check on 1364 # the scheme 1365 scheme = url_split_regex.match(value).group(2) 1366 # Clean up the scheme before we check it 1367 if scheme != None: 1368 scheme = urllib.unquote(scheme).lower() 1369 # If the scheme really exists 1370 if scheme in self.allowed_schemes: 1371 # Then the URL is valid 1372 return (value, None) 1373 else: 1374 # else, for the possible case of abbreviated URLs with 1375 # ports, check to see if adding a valid scheme fixes 1376 # the problem (but only do this if it doesn't have 1377 # one already!) 1378 if not re.compile('://').search(value) and None\ 1379 in self.allowed_schemes: 1380 schemeToUse = self.prepend_scheme or 'http' 1381 prependTest = self.__call__(schemeToUse 1382 + '://' + value) 1383 # if the prepend test succeeded 1384 if prependTest[1] == None: 1385 # if prepending in the output is enabled 1386 if self.prepend_scheme: 1387 return prependTest 1388 else: 1389 # else return the original, 1390 # non-prepended value 1391 return (value, None) 1392 except: 1393 pass 1394 # else the URL is not valid 1395 return (value, self.error_message)
1396 1397 # Sources (obtained 2008-Nov-11): 1398 # http://en.wikipedia.org/wiki/Top-level_domain 1399 # http://www.iana.org/domains/root/db/ 1400 1401 official_top_level_domains = [ 1402 'ac', 1403 'ad', 1404 'ae', 1405 'aero', 1406 'af', 1407 'ag', 1408 'ai', 1409 'al', 1410 'am', 1411 'an', 1412 'ao', 1413 'aq', 1414 'ar', 1415 'arpa', 1416 'as', 1417 'asia', 1418 'at', 1419 'au', 1420 'aw', 1421 'ax', 1422 'az', 1423 'ba', 1424 'bb', 1425 'bd', 1426 'be', 1427 'bf', 1428 'bg', 1429 'bh', 1430 'bi', 1431 'biz', 1432 'bj', 1433 'bl', 1434 'bm', 1435 'bn', 1436 'bo', 1437 'br', 1438 'bs', 1439 'bt', 1440 'bv', 1441 'bw', 1442 'by', 1443 'bz', 1444 'ca', 1445 'cat', 1446 'cc', 1447 'cd', 1448 'cf', 1449 'cg', 1450 'ch', 1451 'ci', 1452 'ck', 1453 'cl', 1454 'cm', 1455 'cn', 1456 'co', 1457 'com', 1458 'coop', 1459 'cr', 1460 'cu', 1461 'cv', 1462 'cx', 1463 'cy', 1464 'cz', 1465 'de', 1466 'dj', 1467 'dk', 1468 'dm', 1469 'do', 1470 'dz', 1471 'ec', 1472 'edu', 1473 'ee', 1474 'eg', 1475 'eh', 1476 'er', 1477 'es', 1478 'et', 1479 'eu', 1480 'example', 1481 'fi', 1482 'fj', 1483 'fk', 1484 'fm', 1485 'fo', 1486 'fr', 1487 'ga', 1488 'gb', 1489 'gd', 1490 'ge', 1491 'gf', 1492 'gg', 1493 'gh', 1494 'gi', 1495 'gl', 1496 'gm', 1497 'gn', 1498 'gov', 1499 'gp', 1500 'gq', 1501 'gr', 1502 'gs', 1503 'gt', 1504 'gu', 1505 'gw', 1506 'gy', 1507 'hk', 1508 'hm', 1509 'hn', 1510 'hr', 1511 'ht', 1512 'hu', 1513 'id', 1514 'ie', 1515 'il', 1516 'im', 1517 'in', 1518 'info', 1519 'int', 1520 'invalid', 1521 'io', 1522 'iq', 1523 'ir', 1524 'is', 1525 'it', 1526 'je', 1527 'jm', 1528 'jo', 1529 'jobs', 1530 'jp', 1531 'ke', 1532 'kg', 1533 'kh', 1534 'ki', 1535 'km', 1536 'kn', 1537 'kp', 1538 'kr', 1539 'kw', 1540 'ky', 1541 'kz', 1542 'la', 1543 'lb', 1544 'lc', 1545 'li', 1546 'lk', 1547 'localhost', 1548 'lr', 1549 'ls', 1550 'lt', 1551 'lu', 1552 'lv', 1553 'ly', 1554 'ma', 1555 'mc', 1556 'md', 1557 'me', 1558 'mf', 1559 'mg', 1560 'mh', 1561 'mil', 1562 'mk', 1563 'ml', 1564 'mm', 1565 'mn', 1566 'mo', 1567 'mobi', 1568 'mp', 1569 'mq', 1570 'mr', 1571 'ms', 1572 'mt', 1573 'mu', 1574 'museum', 1575 'mv', 1576 'mw', 1577 'mx', 1578 'my', 1579 'mz', 1580 'na', 1581 'name', 1582 'nc', 1583 'ne', 1584 'net', 1585 'nf', 1586 'ng', 1587 'ni', 1588 'nl', 1589 'no', 1590 'np', 1591 'nr', 1592 'nu', 1593 'nz', 1594 'om', 1595 'org', 1596 'pa', 1597 'pe', 1598 'pf', 1599 'pg', 1600 'ph', 1601 'pk', 1602 'pl', 1603 'pm', 1604 'pn', 1605 'pr', 1606 'pro', 1607 'ps', 1608 'pt', 1609 'pw', 1610 'py', 1611 'qa', 1612 're', 1613 'ro', 1614 'rs', 1615 'ru', 1616 'rw', 1617 'sa', 1618 'sb', 1619 'sc', 1620 'sd', 1621 'se', 1622 'sg', 1623 'sh', 1624 'si', 1625 'sj', 1626 'sk', 1627 'sl', 1628 'sm', 1629 'sn', 1630 'so', 1631 'sr', 1632 'st', 1633 'su', 1634 'sv', 1635 'sy', 1636 'sz', 1637 'tc', 1638 'td', 1639 'tel', 1640 'test', 1641 'tf', 1642 'tg', 1643 'th', 1644 'tj', 1645 'tk', 1646 'tl', 1647 'tm', 1648 'tn', 1649 'to', 1650 'tp', 1651 'tr', 1652 'travel', 1653 'tt', 1654 'tv', 1655 'tw', 1656 'tz', 1657 'ua', 1658 'ug', 1659 'uk', 1660 'um', 1661 'us', 1662 'uy', 1663 'uz', 1664 'va', 1665 'vc', 1666 've', 1667 'vg', 1668 'vi', 1669 'vn', 1670 'vu', 1671 'wf', 1672 'ws', 1673 'xn--0zwm56d', 1674 'xn--11b5bs3a9aj6g', 1675 'xn--80akhbyknj4f', 1676 'xn--9t4b11yi5a', 1677 'xn--deba0ad', 1678 'xn--g6w251d', 1679 'xn--hgbk6aj7f53bba', 1680 'xn--hlcj6aya9esc7a', 1681 'xn--jxalpdlp', 1682 'xn--kgbechtv', 1683 'xn--zckzah', 1684 'ye', 1685 'yt', 1686 'yu', 1687 'za', 1688 'zm', 1689 'zw', 1690 ] 1691 1692
1693 -class IS_HTTP_URL(Validator):
1694 """ 1695 Rejects a URL string if any of the following is true: 1696 * The string is empty or None 1697 * The string uses characters that are not allowed in a URL 1698 * The string breaks any of the HTTP syntactic rules 1699 * The URL scheme specified (if one is specified) is not 'http' or 'https' 1700 * The top-level domain (if a host name is specified) does not exist 1701 1702 Based on RFC 2616: http://www.faqs.org/rfcs/rfc2616.html 1703 1704 This function only checks the URL's syntax. It does not check that the URL 1705 points to a real document, for example, or that it otherwise makes sense 1706 semantically. This function does automatically prepend 'http://' in front 1707 of a URL in the case of an abbreviated URL (e.g. 'google.ca'). 1708 1709 The list of allowed schemes is customizable with the allowed_schemes 1710 parameter. If you exclude None from the list, then abbreviated URLs 1711 (lacking a scheme such as 'http') will be rejected. 1712 1713 The default prepended scheme is customizable with the prepend_scheme 1714 parameter. If you set prepend_scheme to None then prepending will be 1715 disabled. URLs that require prepending to parse will still be accepted, 1716 but the return value will not be modified. 1717 1718 @author: Jonathan Benn 1719 1720 >>> IS_HTTP_URL()('http://1.2.3.4') 1721 ('http://1.2.3.4', None) 1722 >>> IS_HTTP_URL()('http://abc.com') 1723 ('http://abc.com', None) 1724 >>> IS_HTTP_URL()('https://abc.com') 1725 ('https://abc.com', None) 1726 >>> IS_HTTP_URL()('httpx://abc.com') 1727 ('httpx://abc.com', 'enter a valid URL') 1728 >>> IS_HTTP_URL()('http://abc.com:80') 1729 ('http://abc.com:80', None) 1730 >>> IS_HTTP_URL()('http://user@abc.com') 1731 ('http://user@abc.com', None) 1732 >>> IS_HTTP_URL()('http://user@1.2.3.4') 1733 ('http://user@1.2.3.4', None) 1734 1735 """ 1736
1737 - def __init__( 1738 self, 1739 error_message='enter a valid URL', 1740 allowed_schemes=None, 1741 prepend_scheme='http', 1742 ):
1743 """ 1744 :param error_message: a string, the error message to give the end user 1745 if the URL does not validate 1746 :param allowed_schemes: a list containing strings or None. Each element 1747 is a scheme the inputed URL is allowed to use 1748 :param prepend_scheme: a string, this scheme is prepended if it's 1749 necessary to make the URL valid 1750 """ 1751 1752 self.error_message = error_message 1753 if allowed_schemes == None: 1754 self.allowed_schemes = http_schemes 1755 else: 1756 self.allowed_schemes = allowed_schemes 1757 self.prepend_scheme = prepend_scheme 1758 1759 for i in self.allowed_schemes: 1760 if i not in http_schemes: 1761 raise SyntaxError, \ 1762 "allowed_scheme value '%s' is not in %s" % \ 1763 (i, http_schemes) 1764 1765 if self.prepend_scheme not in self.allowed_schemes: 1766 raise SyntaxError, \ 1767 "prepend_scheme='%s' is not in allowed_schemes=%s" % \ 1768 (self.prepend_scheme, self.allowed_schemes)
1769
1770 - def __call__(self, value):
1771 """ 1772 :param value: a string, the URL to validate 1773 :returns: a tuple, where tuple[0] is the inputed value 1774 (possible prepended with prepend_scheme), and tuple[1] is either 1775 None (success!) or the string error_message 1776 """ 1777 1778 try: 1779 # if the URL passes generic validation 1780 x = IS_GENERIC_URL(error_message=self.error_message, 1781 allowed_schemes=self.allowed_schemes, 1782 prepend_scheme=self.prepend_scheme) 1783 if x(value)[1] == None: 1784 componentsMatch = url_split_regex.match(value) 1785 authority = componentsMatch.group(4) 1786 # if there is an authority component 1787 if authority: 1788 # if authority is a valid IP address 1789 if re.compile( 1790 "([\w.!~*'|;:&=+$,-]+@)?\d+\.\d+\.\d+\.\d+(:\d*)*$").match(authority): 1791 # Then this HTTP URL is valid 1792 return (value, None) 1793 else: 1794 # else if authority is a valid domain name 1795 domainMatch = \ 1796 re.compile( 1797 "([\w.!~*'|;:&=+$,-]+@)?(([A-Za-z0-9]+[A-Za-z0-9\-]*[A-Za-z0-9]+\.)*([A-Za-z0-9]+\.)*)*([A-Za-z]+[A-Za-z0-9\-]*[A-Za-z0-9]+)\.?(:\d*)*$" 1798 ).match(authority) 1799 if domainMatch: 1800 # if the top-level domain really exists 1801 if domainMatch.group(5).lower()\ 1802 in official_top_level_domains: 1803 # Then this HTTP URL is valid 1804 return (value, None) 1805 else: 1806 # else this is a relative/abbreviated URL, which will parse 1807 # into the URL's path component 1808 path = componentsMatch.group(5) 1809 # relative case: if this is a valid path (if it starts with 1810 # a slash) 1811 if re.compile('/').match(path): 1812 # Then this HTTP URL is valid 1813 return (value, None) 1814 else: 1815 # abbreviated case: if we haven't already, prepend a 1816 # scheme and see if it fixes the problem 1817 if not re.compile('://').search(value): 1818 schemeToUse = self.prepend_scheme or 'http' 1819 prependTest = self.__call__(schemeToUse 1820 + '://' + value) 1821 # if the prepend test succeeded 1822 if prependTest[1] == None: 1823 # if prepending in the output is enabled 1824 if self.prepend_scheme: 1825 return prependTest 1826 else: 1827 # else return the original, non-prepended 1828 # value 1829 return (value, None) 1830 except: 1831 pass 1832 # else the HTTP URL is not valid 1833 return (value, self.error_message)
1834 1835
1836 -class IS_URL(Validator):
1837 """ 1838 Rejects a URL string if any of the following is true: 1839 * The string is empty or None 1840 * The string uses characters that are not allowed in a URL 1841 * The string breaks any of the HTTP syntactic rules 1842 * The URL scheme specified (if one is specified) is not 'http' or 'https' 1843 * The top-level domain (if a host name is specified) does not exist 1844 1845 (These rules are based on RFC 2616: http://www.faqs.org/rfcs/rfc2616.html) 1846 1847 This function only checks the URL's syntax. It does not check that the URL 1848 points to a real document, for example, or that it otherwise makes sense 1849 semantically. This function does automatically prepend 'http://' in front 1850 of a URL in the case of an abbreviated URL (e.g. 'google.ca'). 1851 1852 If the parameter mode='generic' is used, then this function's behavior 1853 changes. It then rejects a URL string if any of the following is true: 1854 * The string is empty or None 1855 * The string uses characters that are not allowed in a URL 1856 * The URL scheme specified (if one is specified) is not valid 1857 1858 (These rules are based on RFC 2396: http://www.faqs.org/rfcs/rfc2396.html) 1859 1860 The list of allowed schemes is customizable with the allowed_schemes 1861 parameter. If you exclude None from the list, then abbreviated URLs 1862 (lacking a scheme such as 'http') will be rejected. 1863 1864 The default prepended scheme is customizable with the prepend_scheme 1865 parameter. If you set prepend_scheme to None then prepending will be 1866 disabled. URLs that require prepending to parse will still be accepted, 1867 but the return value will not be modified. 1868 1869 IS_URL is compatible with the Internationalized Domain Name (IDN) standard 1870 specified in RFC 3490 (http://tools.ietf.org/html/rfc3490). As a result, 1871 URLs can be regular strings or unicode strings. 1872 If the URL's domain component (e.g. google.ca) contains non-US-ASCII 1873 letters, then the domain will be converted into Punycode (defined in 1874 RFC 3492, http://tools.ietf.org/html/rfc3492). IS_URL goes a bit beyond 1875 the standards, and allows non-US-ASCII characters to be present in the path 1876 and query components of the URL as well. These non-US-ASCII characters will 1877 be escaped using the standard '%20' type syntax. e.g. the unicode 1878 character with hex code 0x4e86 will become '%4e%86' 1879 1880 Code Examples:: 1881 1882 INPUT(_type='text', _name='name', requires=IS_URL()) 1883 >>> IS_URL()('abc.com') 1884 ('http://abc.com', None) 1885 1886 INPUT(_type='text', _name='name', requires=IS_URL(mode='generic')) 1887 >>> IS_URL(mode='generic')('abc.com') 1888 ('abc.com', None) 1889 1890 INPUT(_type='text', _name='name', 1891 requires=IS_URL(allowed_schemes=['https'], prepend_scheme='https')) 1892 >>> IS_URL(allowed_schemes=['https'], prepend_scheme='https')('https://abc.com') 1893 ('https://abc.com', None) 1894 1895 INPUT(_type='text', _name='name', 1896 requires=IS_URL(prepend_scheme='https')) 1897 >>> IS_URL(prepend_scheme='https')('abc.com') 1898 ('https://abc.com', None) 1899 1900 INPUT(_type='text', _name='name', 1901 requires=IS_URL(mode='generic', allowed_schemes=['ftps', 'https'], 1902 prepend_scheme='https')) 1903 >>> IS_URL(mode='generic', allowed_schemes=['ftps', 'https'], prepend_scheme='https')('https://abc.com') 1904 ('https://abc.com', None) 1905 >>> IS_URL(mode='generic', allowed_schemes=['ftps', 'https', None], prepend_scheme='https')('abc.com') 1906 ('abc.com', None) 1907 1908 @author: Jonathan Benn 1909 """ 1910
1911 - def __init__( 1912 self, 1913 error_message='enter a valid URL', 1914 mode='http', 1915 allowed_schemes=None, 1916 prepend_scheme='http', 1917 ):
1918 """ 1919 :param error_message: a string, the error message to give the end user 1920 if the URL does not validate 1921 :param allowed_schemes: a list containing strings or None. Each element 1922 is a scheme the inputed URL is allowed to use 1923 :param prepend_scheme: a string, this scheme is prepended if it's 1924 necessary to make the URL valid 1925 """ 1926 1927 self.error_message = error_message 1928 self.mode = mode.lower() 1929 if not self.mode in ['generic', 'http']: 1930 raise SyntaxError, "invalid mode '%s' in IS_URL" % self.mode 1931 self.allowed_schemes = allowed_schemes 1932 1933 if self.allowed_schemes: 1934 if prepend_scheme not in self.allowed_schemes: 1935 raise SyntaxError, \ 1936 "prepend_scheme='%s' is not in allowed_schemes=%s" \ 1937 % (prepend_scheme, self.allowed_schemes) 1938 1939 # if allowed_schemes is None, then we will defer testing 1940 # prepend_scheme's validity to a sub-method 1941 1942 self.prepend_scheme = prepend_scheme
1943
1944 - def __call__(self, value):
1945 """ 1946 :param value: a unicode or regular string, the URL to validate 1947 :returns: a (string, string) tuple, where tuple[0] is the modified 1948 input value and tuple[1] is either None (success!) or the 1949 string error_message. The input value will never be modified in the 1950 case of an error. However, if there is success then the input URL 1951 may be modified to (1) prepend a scheme, and/or (2) convert a 1952 non-compliant unicode URL into a compliant US-ASCII version. 1953 """ 1954 1955 if self.mode == 'generic': 1956 subMethod = IS_GENERIC_URL(error_message=self.error_message, 1957 allowed_schemes=self.allowed_schemes, 1958 prepend_scheme=self.prepend_scheme) 1959 elif self.mode == 'http': 1960 subMethod = IS_HTTP_URL(error_message=self.error_message, 1961 allowed_schemes=self.allowed_schemes, 1962 prepend_scheme=self.prepend_scheme) 1963 else: 1964 raise SyntaxError, "invalid mode '%s' in IS_URL" % self.mode 1965 1966 if type(value) != unicode: 1967 return subMethod(value) 1968 else: 1969 try: 1970 asciiValue = unicode_to_ascii_url(value, self.prepend_scheme) 1971 except Exception: 1972 #If we are not able to convert the unicode url into a 1973 # US-ASCII URL, then the URL is not valid 1974 return (value, self.error_message) 1975 1976 methodResult = subMethod(asciiValue) 1977 #if the validation of the US-ASCII version of the value failed 1978 if methodResult[1] != None: 1979 # then return the original input value, not the US-ASCII version 1980 return (value, methodResult[1]) 1981 else: 1982 return methodResult
1983 1984 1985 regex_time = re.compile( 1986 '((?P<h>[0-9]+))([^0-9 ]+(?P<m>[0-9 ]+))?([^0-9ap ]+(?P<s>[0-9]*))?((?P<d>[ap]m))?') 1987 1988
1989 -class IS_TIME(Validator):
1990 """ 1991 example:: 1992 1993 INPUT(_type='text', _name='name', requires=IS_TIME()) 1994 1995 understands the following formats 1996 hh:mm:ss [am/pm] 1997 hh:mm [am/pm] 1998 hh [am/pm] 1999 2000 [am/pm] is optional, ':' can be replaced by any other non-space non-digit 2001 2002 >>> IS_TIME()('21:30') 2003 (datetime.time(21, 30), None) 2004 >>> IS_TIME()('21-30') 2005 (datetime.time(21, 30), None) 2006 >>> IS_TIME()('21.30') 2007 (datetime.time(21, 30), None) 2008 >>> IS_TIME()('21:30:59') 2009 (datetime.time(21, 30, 59), None) 2010 >>> IS_TIME()('5:30') 2011 (datetime.time(5, 30), None) 2012 >>> IS_TIME()('5:30 am') 2013 (datetime.time(5, 30), None) 2014 >>> IS_TIME()('5:30 pm') 2015 (datetime.time(17, 30), None) 2016 >>> IS_TIME()('5:30 whatever') 2017 ('5:30 whatever', 'enter time as hh:mm:ss (seconds, am, pm optional)') 2018 >>> IS_TIME()('5:30 20') 2019 ('5:30 20', 'enter time as hh:mm:ss (seconds, am, pm optional)') 2020 >>> IS_TIME()('24:30') 2021 ('24:30', 'enter time as hh:mm:ss (seconds, am, pm optional)') 2022 >>> IS_TIME()('21:60') 2023 ('21:60', 'enter time as hh:mm:ss (seconds, am, pm optional)') 2024 >>> IS_TIME()('21:30::') 2025 ('21:30::', 'enter time as hh:mm:ss (seconds, am, pm optional)') 2026 >>> IS_TIME()('') 2027 ('', 'enter time as hh:mm:ss (seconds, am, pm optional)') 2028 """ 2029
2030 - def __init__(self, error_message='enter time as hh:mm:ss (seconds, am, pm optional)'):
2031 self.error_message = error_message
2032
2033 - def __call__(self, value):
2034 try: 2035 ivalue = value 2036 value = regex_time.match(value.lower()) 2037 (h, m, s) = (int(value.group('h')), 0, 0) 2038 if value.group('m') != None: 2039 m = int(value.group('m')) 2040 if value.group('s') != None: 2041 s = int(value.group('s')) 2042 if value.group('d') == 'pm' and 0 < h < 12: 2043 h = h + 12 2044 if not (h in range(24) and m in range(60) and s 2045 in range(60)): 2046 raise ValueError\ 2047 ('Hours or minutes or seconds are outside of allowed range') 2048 value = datetime.time(h, m, s) 2049 return (value, None) 2050 except AttributeError: 2051 pass 2052 except ValueError: 2053 pass 2054 return (ivalue, self.error_message)
2055 2056
2057 -class IS_DATE(Validator):
2058 """ 2059 example:: 2060 2061 INPUT(_type='text', _name='name', requires=IS_DATE()) 2062 2063 date has to be in the ISO8960 format YYYY-MM-DD 2064 """ 2065
2066 - def __init__(self, format='%Y-%m-%d', 2067 error_message='enter date as %(format)s'):
2068 self.format = str(format) 2069 self.error_message = str(error_message)
2070
2071 - def __call__(self, value):
2072 try: 2073 (y, m, d, hh, mm, ss, t0, t1, t2) = \ 2074 time.strptime(value, str(self.format)) 2075 value = datetime.date(y, m, d) 2076 return (value, None) 2077 except: 2078 return (value, self.error_message % IS_DATETIME.nice(self.format))
2079
2080 - def formatter(self, value):
2081 format = self.format 2082 year = value.year 2083 y = '%.4i' % year 2084 format = format.replace('%y',y[-2:]) 2085 format = format.replace('%Y',y) 2086 if year<1900: 2087 year = 2000 2088 d = datetime.date(year,value.month,value.day) 2089 return d.strftime(format)
2090 2091
2092 -class IS_DATETIME(Validator):
2093 """ 2094 example:: 2095 2096 INPUT(_type='text', _name='name', requires=IS_DATETIME()) 2097 2098 datetime has to be in the ISO8960 format YYYY-MM-DD hh:mm:ss 2099 """ 2100 2101 isodatetime = '%Y-%m-%d %H:%M:%S' 2102 2103 @staticmethod
2104 - def nice(format):
2105 code=(('%Y','1963'), 2106 ('%y','63'), 2107 ('%d','28'), 2108 ('%m','08'), 2109 ('%b','Aug'), 2110 ('%b','August'), 2111 ('%H','14'), 2112 ('%I','02'), 2113 ('%p','PM'), 2114 ('%M','30'), 2115 ('%S','59')) 2116 for (a,b) in code: 2117 format=format.replace(a,b) 2118 return dict(format=format)
2119
2120 - def __init__(self, format='%Y-%m-%d %H:%M:%S', 2121 error_message='enter date and time as %(format)s'):
2122 self.format = str(format) 2123 self.error_message = str(error_message)
2124
2125 - def __call__(self, value):
2126 try: 2127 (y, m, d, hh, mm, ss, t0, t1, t2) = \ 2128 time.strptime(value, str(self.format)) 2129 value = datetime.datetime(y, m, d, hh, mm, ss) 2130 return (value, None) 2131 except: 2132 return (value, self.error_message % IS_DATETIME.nice(self.format))
2133
2134 - def formatter(self, value):
2135 format = self.format 2136 year = value.year 2137 y = '%.4i' % year 2138 format = format.replace('%y',y[-2:]) 2139 format = format.replace('%Y',y) 2140 if year<1900: 2141 year = 2000 2142 d = datetime.datetime(year,value.month,value.day,value.hour,value.minute,value.second) 2143 return d.strftime(format)
2144
2145 -class IS_DATE_IN_RANGE(IS_DATE):
2146 """ 2147 example:: 2148 2149 >>> v = IS_DATE_IN_RANGE(minimum=datetime.date(2008,1,1), \ 2150 maximum=datetime.date(2009,12,31), \ 2151 format="%m/%d/%Y",error_message="oops") 2152 2153 >>> v('03/03/2008') 2154 (datetime.date(2008, 3, 3), None) 2155 2156 >>> v('03/03/2010') 2157 (datetime.date(2010, 3, 3), 'oops') 2158 2159 """
2160 - def __init__(self, 2161 minimum = None, 2162 maximum = None, 2163 format='%Y-%m-%d', 2164 error_message = None):
2165 self.minimum = minimum 2166 self.maximum = maximum 2167 if error_message is None: 2168 if minimum is None: 2169 error_message = "enter date on or before %(max)s" 2170 elif maximum is None: 2171 error_message = "enter date on or after %(min)s" 2172 else: 2173 error_message = "enter date in range %(min)s %(max)s" 2174 d = dict(min=minimum, max=maximum) 2175 IS_DATE.__init__(self, 2176 format = format, 2177 error_message = error_message % d)
2178
2179 - def __call__(self, value):
2180 (value, msg) = IS_DATE.__call__(self,value) 2181 if msg is not None: 2182 return (value, msg) 2183 if self.minimum and self.minimum > value: 2184 return (value, self.error_message) 2185 if self.maximum and value > self.maximum: 2186 return (value, self.error_message) 2187 return (value, None)
2188 2189
2190 -class IS_DATETIME_IN_RANGE(IS_DATETIME):
2191 """ 2192 example:: 2193 2194 >>> v = IS_DATETIME_IN_RANGE(\ 2195 minimum=datetime.datetime(2008,1,1,12,20), \ 2196 maximum=datetime.datetime(2009,12,31,12,20), \ 2197 format="%m/%d/%Y %H:%M",error_message="oops") 2198 >>> v('03/03/2008 12:40') 2199 (datetime.datetime(2008, 3, 3, 12, 40), None) 2200 2201 >>> v('03/03/2010 10:34') 2202 (datetime.datetime(2010, 3, 3, 10, 34), 'oops') 2203 """
2204 - def __init__(self, 2205 minimum = None, 2206 maximum = None, 2207 format = '%Y-%m-%d %H:%M:%S', 2208 error_message = None):
2209 self.minimum = minimum 2210 self.maximum = maximum 2211 if error_message is None: 2212 if minimum is None: 2213 error_message = "enter date and time on or before %(max)s" 2214 elif maximum is None: 2215 error_message = "enter date and time on or after %(min)s" 2216 else: 2217 error_message = "enter date and time in range %(min)s %(max)s" 2218 d = dict(min = minimum, max = maximum) 2219 IS_DATETIME.__init__(self, 2220 format = format, 2221 error_message = error_message % d)
2222
2223 - def __call__(self, value):
2224 (value, msg) = IS_DATETIME.__call__(self, value) 2225 if msg is not None: 2226 return (value, msg) 2227 if self.minimum and self.minimum > value: 2228 return (value, self.error_message) 2229 if self.maximum and value > self.maximum: 2230 return (value, self.error_message) 2231 return (value, None)
2232 2233
2234 -class IS_LIST_OF(Validator):
2235
2236 - def __init__(self, other):
2237 self.other = other
2238
2239 - def __call__(self, value):
2240 ivalue = value 2241 if not isinstance(value, list): 2242 ivalue = [ivalue] 2243 new_value = [] 2244 for item in ivalue: 2245 (v, e) = self.other(item) 2246 if e: 2247 return (value, e) 2248 else: 2249 new_value.append(v) 2250 return (new_value, None)
2251 2252
2253 -class IS_LOWER(Validator):
2254 """ 2255 convert to lower case 2256 2257 >>> IS_LOWER()('ABC') 2258 ('abc', None) 2259 >>> IS_LOWER()('Ñ') 2260 ('\\xc3\\xb1', None) 2261 """ 2262
2263 - def __call__(self, value):
2264 return (value.decode('utf8').lower().encode('utf8'), None)
2265 2266
2267 -class IS_UPPER(Validator):
2268 """ 2269 convert to upper case 2270 2271 >>> IS_UPPER()('abc') 2272 ('ABC', None) 2273 >>> IS_UPPER()('ñ') 2274 ('\\xc3\\x91', None) 2275 """ 2276
2277 - def __call__(self, value):
2278 return (value.decode('utf8').upper().encode('utf8'), None)
2279 2280
2281 -class IS_SLUG(Validator):
2282 """ 2283 convert arbitrary text string to a slug 2284 2285 >>> IS_SLUG()('abc123') 2286 ('abc123', None) 2287 >>> IS_SLUG()('ABC123') 2288 ('abc123', None) 2289 >>> IS_SLUG()('abc-123') 2290 ('abc-123', None) 2291 >>> IS_SLUG()('abc--123') 2292 ('abc-123', None) 2293 >>> IS_SLUG()('abc 123') 2294 ('abc-123', None) 2295 >>> IS_SLUG()('-abc-') 2296 ('abc', None) 2297 >>> IS_SLUG()('abc&amp;123') 2298 ('abc123', None) 2299 >>> IS_SLUG()('abc&amp;123&amp;def') 2300 ('abc123def', None) 2301 >>> IS_SLUG()('ñ') 2302 ('n', None) 2303 >>> IS_SLUG(maxlen=4)('abc123') 2304 ('abc1', None) 2305 """ 2306
2307 - def __init__(self, maxlen=80, check=False, error_message='must be slug'):
2308 self.maxlen = maxlen 2309 self.check = check 2310 self.error_message = error_message
2311 2312 @staticmethod
2313 - def urlify(value, maxlen=80):
2314 s = value.decode('utf-8').lower() # to lowercase utf-8 2315 s = unicodedata.normalize('NFKD', s) # normalize eg è => e, ñ => n 2316 s = s.encode('ASCII', 'ignore') # encode as ASCII 2317 s = re.sub('&\w+?;', '', s) # strip html entities 2318 s = re.sub('[^a-z0-9\-\s]', '', s) # strip all but alphanumeric/hyphen/space 2319 s = s.replace(' ', '-') # spaces to hyphens 2320 s = re.sub('--+', '-', s) # collapse strings of hyphens 2321 s = s.strip('-') # remove leading and traling hyphens 2322 return s[:maxlen].strip('-') # enforce maximum length
2323
2324 - def __call__(self,value):
2325 if self.check and value != IS_SLUG.urlify(value,self.maxlen): 2326 return (value,self.error_message) 2327 return (IS_SLUG.urlify(value,self.maxlen), None)
2328
2329 -class IS_EMPTY_OR(Validator):
2330 """ 2331 dummy class for testing IS_EMPTY_OR 2332 2333 >>> IS_EMPTY_OR(IS_EMAIL())('abc@def.com') 2334 ('abc@def.com', None) 2335 >>> IS_EMPTY_OR(IS_EMAIL())(' ') 2336 (None, None) 2337 >>> IS_EMPTY_OR(IS_EMAIL(), null='abc')(' ') 2338 ('abc', None) 2339 >>> IS_EMPTY_OR(IS_EMAIL(), null='abc', empty_regex='def')('def') 2340 ('abc', None) 2341 >>> IS_EMPTY_OR(IS_EMAIL())('abc') 2342 ('abc', 'enter a valid email address') 2343 >>> IS_EMPTY_OR(IS_EMAIL())(' abc ') 2344 ('abc', 'enter a valid email address') 2345 """ 2346
2347 - def __init__(self, other, null=None, empty_regex=None):
2348 (self.other, self.null) = (other, null) 2349 if empty_regex is not None: 2350 self.empty_regex = re.compile(empty_regex) 2351 else: 2352 self.empty_regex = None 2353 if hasattr(other, 'multiple'): 2354 self.multiple = other.multiple 2355 if hasattr(other, 'options'): 2356 self.options=self._options
2357
2358 - def _options(self):
2359 options = self.other.options() 2360 if (not options or options[0][0]!='') and not self.multiple: 2361 options.insert(0,('','')) 2362 return options
2363
2364 - def set_self_id(self, id):
2365 if hasattr(self.other, 'set_self_id'): 2366 self.other.set_self_id(id)
2367
2368 - def __call__(self, value):
2369 value, empty = is_empty(value, empty_regex=self.empty_regex) 2370 if empty: 2371 return (self.null, None) 2372 if isinstance(self.other, (list, tuple)): 2373 for item in self.other: 2374 value, error = item(value) 2375 if error: break 2376 return value, error 2377 else: 2378 return self.other(value)
2379
2380 - def formatter(self, value):
2381 if hasattr(self.other, 'formatter'): 2382 return self.other.formatter(value) 2383 return value
2384 2385 IS_NULL_OR = IS_EMPTY_OR # for backward compatibility 2386 2387
2388 -class CLEANUP(Validator):
2389 """ 2390 example:: 2391 2392 INPUT(_type='text', _name='name', requires=CLEANUP()) 2393 2394 removes special characters on validation 2395 """ 2396
2397 - def __init__(self, regex='[^ \n\w]'):
2398 self.regex = re.compile(regex)
2399
2400 - def __call__(self, value):
2401 v = self.regex.sub('',str(value).strip()) 2402 return (v, None)
2403 2404
2405 -class CRYPT(object):
2406 """ 2407 example:: 2408 2409 INPUT(_type='text', _name='name', requires=CRYPT()) 2410 2411 encodes the value on validation with a digest. 2412 2413 If no arguments are provided CRYPT uses the MD5 algorithm. 2414 If the key argument is provided the HMAC+MD5 algorithm is used. 2415 If the digest_alg is specified this is used to replace the 2416 MD5 with, for example, SHA512. The digest_alg can be 2417 the name of a hashlib algorithm as a string or the algorithm itself. 2418 """ 2419
2420 - def __init__(self, key=None, digest_alg=None):
2421 if key and not digest_alg: 2422 if key.count(':')==1: 2423 (digest_alg, key) = key.split(':') 2424 if not digest_alg: 2425 digest_alg = 'md5' # for backward compatibility 2426 self.key = key 2427 self.digest_alg = digest_alg
2428
2429 - def __call__(self, value):
2430 if self.key: 2431 alg = get_digest(self.digest_alg) 2432 return (hmac.new(self.key, value, alg).hexdigest(), None) 2433 else: 2434 return (hash(value, self.digest_alg), None)
2435 2436
2437 -class IS_STRONG(object):
2438 """ 2439 example:: 2440 2441 INPUT(_type='password', _name='passwd', 2442 requires=IS_STRONG(min=10, special=2, upper=2)) 2443 2444 enforces complexity requirements on a field 2445 """ 2446
2447 - def __init__(self, min=8, max=20, upper=1, lower=1, number=1, 2448 special=1, specials=r'~!@#$%^&*()_+-=?<>,.:;{}[]|', 2449 invalid=' "', error_message=None):
2450 self.min = min 2451 self.max = max 2452 self.upper = upper 2453 self.lower = lower 2454 self.number = number 2455 self.special = special 2456 self.specials = specials 2457 self.invalid = invalid 2458 self.error_message = error_message
2459
2460 - def __call__(self, value):
2461 failures = [] 2462 if type(self.min) == int and self.min > 0: 2463 if not len(value) >= self.min: 2464 failures.append("Minimum length is %s" % self.min) 2465 if type(self.max) == int and self.max > 0: 2466 if not len(value) <= self.max: 2467 failures.append("Maximum length is %s" % self.max) 2468 if type(self.special) == int: 2469 all_special = [ch in value for ch in self.specials] 2470 if self.special > 0: 2471 if not all_special.count(True) >= self.special: 2472 failures.append("Must include at least %s of the following : %s" % (self.special, self.specials)) 2473 if self.invalid: 2474 all_invalid = [ch in value for ch in self.invalid] 2475 if all_invalid.count(True) > 0: 2476 failures.append("May not contain any of the following: %s" \ 2477 % self.invalid) 2478 if type(self.upper) == int: 2479 all_upper = re.findall("[A-Z]", value) 2480 if self.upper > 0: 2481 if not len(all_upper) >= self.upper: 2482 failures.append("Must include at least %s upper case" \ 2483 % str(self.upper)) 2484 else: 2485 if len(all_upper) > 0: 2486 failures.append("May not include any upper case letters") 2487 if type(self.lower) == int: 2488 all_lower = re.findall("[a-z]", value) 2489 if self.lower > 0: 2490 if not len(all_lower) >= self.lower: 2491 failures.append("Must include at least %s lower case" \ 2492 % str(self.lower)) 2493 else: 2494 if len(all_lower) > 0: 2495 failures.append("May not include any lower case letters") 2496 if type(self.number) == int: 2497 all_number = re.findall("[0-9]", value) 2498 if self.number > 0: 2499 numbers = "number" 2500 if self.number > 1: 2501 numbers = "numbers" 2502 if not len(all_number) >= self.number: 2503 failures.append("Must include at least %s %s" \ 2504 % (str(self.number), numbers)) 2505 else: 2506 if len(all_number) > 0: 2507 failures.append("May not include any numbers") 2508 if len(failures) == 0: 2509 return (value, None) 2510 if not self.error_message: 2511 from html import XML 2512 return (value, XML('<br />'.join(failures))) 2513 else: 2514 return (value, self.error_message)
2515 2516
2517 -class IS_IN_SUBSET(IS_IN_SET):
2518
2519 - def __init__(self, *a, **b):
2520 IS_IN_SET.__init__(self, *a, **b)
2521
2522 - def __call__(self, value):
2523 values = re.compile("\w+").findall(str(value)) 2524 failures = [x for x in values if IS_IN_SET.__call__(self, x)[1]] 2525 if failures: 2526 return (value, self.error_message) 2527 return (value, None)
2528 2529
2530 -class IS_IMAGE(Validator):
2531 """ 2532 Checks if file uploaded through file input was saved in one of selected 2533 image formats and has dimensions (width and height) within given boundaries. 2534 2535 Does *not* check for maximum file size (use IS_LENGTH for that). Returns 2536 validation failure if no data was uploaded. 2537 2538 Supported file formats: BMP, GIF, JPEG, PNG. 2539 2540 Code parts taken from 2541 http://mail.python.org/pipermail/python-list/2007-June/617126.html 2542 2543 Arguments: 2544 2545 extensions: iterable containing allowed *lowercase* image file extensions 2546 ('jpg' extension of uploaded file counts as 'jpeg') 2547 maxsize: iterable containing maximum width and height of the image 2548 minsize: iterable containing minimum width and height of the image 2549 2550 Use (-1, -1) as minsize to pass image size check. 2551 2552 Examples:: 2553 2554 #Check if uploaded file is in any of supported image formats: 2555 INPUT(_type='file', _name='name', requires=IS_IMAGE()) 2556 2557 #Check if uploaded file is either JPEG or PNG: 2558 INPUT(_type='file', _name='name', 2559 requires=IS_IMAGE(extensions=('jpeg', 'png'))) 2560 2561 #Check if uploaded file is PNG with maximum size of 200x200 pixels: 2562 INPUT(_type='file', _name='name', 2563 requires=IS_IMAGE(extensions=('png'), maxsize=(200, 200))) 2564 """ 2565
2566 - def __init__(self, 2567 extensions=('bmp', 'gif', 'jpeg', 'png'), 2568 maxsize=(10000, 10000), 2569 minsize=(0, 0), 2570 error_message='invalid image'):
2571 2572 self.extensions = extensions 2573 self.maxsize = maxsize 2574 self.minsize = minsize 2575 self.error_message = error_message
2576
2577 - def __call__(self, value):
2578 try: 2579 extension = value.filename.rfind('.') 2580 assert extension >= 0 2581 extension = value.filename[extension + 1:].lower() 2582 if extension == 'jpg': 2583 extension = 'jpeg' 2584 assert extension in self.extensions 2585 if extension == 'bmp': 2586 width, height = self.__bmp(value.file) 2587 elif extension == 'gif': 2588 width, height = self.__gif(value.file) 2589 elif extension == 'jpeg': 2590 width, height = self.__jpeg(value.file) 2591 elif extension == 'png': 2592 width, height = self.__png(value.file) 2593 else: 2594 width = -1 2595 height = -1 2596 assert self.minsize[0] <= width <= self.maxsize[0] \ 2597 and self.minsize[1] <= height <= self.maxsize[1] 2598 value.file.seek(0) 2599 return (value, None) 2600 except: 2601 return (value, self.error_message)
2602
2603 - def __bmp(self, stream):
2604 if stream.read(2) == 'BM': 2605 stream.read(16) 2606 return struct.unpack("<LL", stream.read(8)) 2607 return (-1, -1)
2608
2609 - def __gif(self, stream):
2610 if stream.read(6) in ('GIF87a', 'GIF89a'): 2611 stream = stream.read(5) 2612 if len(stream) == 5: 2613 return tuple(struct.unpack("<HHB", stream)[:-1]) 2614 return (-1, -1)
2615
2616 - def __jpeg(self, stream):
2617 if stream.read(2) == '\xFF\xD8': 2618 while True: 2619 (marker, code, length) = struct.unpack("!BBH", stream.read(4)) 2620 if marker != 0xFF: 2621 break 2622 elif code >= 0xC0 and code <= 0xC3: 2623 return tuple(reversed( 2624 struct.unpack("!xHH", stream.read(5)))) 2625 else: 2626 stream.read(length - 2) 2627 return (-1, -1)
2628
2629 - def __png(self, stream):
2630 if stream.read(8) == '\211PNG\r\n\032\n': 2631 stream.read(4) 2632 if stream.read(4) == "IHDR": 2633 return struct.unpack("!LL", stream.read(8)) 2634 return (-1, -1)
2635 2636
2637 -class IS_UPLOAD_FILENAME(Validator):
2638 """ 2639 Checks if name and extension of file uploaded through file input matches 2640 given criteria. 2641 2642 Does *not* ensure the file type in any way. Returns validation failure 2643 if no data was uploaded. 2644 2645 Arguments:: 2646 2647 filename: filename (before dot) regex 2648 extension: extension (after dot) regex 2649 lastdot: which dot should be used as a filename / extension separator: 2650 True means last dot, eg. file.png -> file / png 2651 False means first dot, eg. file.tar.gz -> file / tar.gz 2652 case: 0 - keep the case, 1 - transform the string into lowercase (default), 2653 2 - transform the string into uppercase 2654 2655 If there is no dot present, extension checks will be done against empty 2656 string and filename checks against whole value. 2657 2658 Examples:: 2659 2660 #Check if file has a pdf extension (case insensitive): 2661 INPUT(_type='file', _name='name', 2662 requires=IS_UPLOAD_FILENAME(extension='pdf')) 2663 2664 #Check if file has a tar.gz extension and name starting with backup: 2665 INPUT(_type='file', _name='name', 2666 requires=IS_UPLOAD_FILENAME(filename='backup.*', 2667 extension='tar.gz', lastdot=False)) 2668 2669 #Check if file has no extension and name matching README 2670 #(case sensitive): 2671 INPUT(_type='file', _name='name', 2672 requires=IS_UPLOAD_FILENAME(filename='^README$', 2673 extension='^$', case=0)) 2674 """ 2675
2676 - def __init__(self, filename=None, extension=None, lastdot=True, case=1, 2677 error_message='enter valid filename'):
2678 if isinstance(filename, str): 2679 filename = re.compile(filename) 2680 if isinstance(extension, str): 2681 extension = re.compile(extension) 2682 self.filename = filename 2683 self.extension = extension 2684 self.lastdot = lastdot 2685 self.case = case 2686 self.error_message = error_message
2687
2688 - def __call__(self, value):
2689 try: 2690 string = value.filename 2691 except: 2692 return (value, self.error_message) 2693 if self.case == 1: 2694 string = string.lower() 2695 elif self.case == 2: 2696 string = string.upper() 2697 if self.lastdot: 2698 dot = string.rfind('.') 2699 else: 2700 dot = string.find('.') 2701 if dot == -1: 2702 dot = len(string) 2703 if self.filename and not self.filename.match(string[:dot]): 2704 return (value, self.error_message) 2705 elif self.extension and not self.extension.match(string[dot + 1:]): 2706 return (value, self.error_message) 2707 else: 2708 return (value, None)
2709 2710
2711 -class IS_IPV4(Validator):
2712 """ 2713 Checks if field's value is an IP version 4 address in decimal form. Can 2714 be set to force addresses from certain range. 2715 2716 IPv4 regex taken from: http://regexlib.com/REDetails.aspx?regexp_id=1411 2717 2718 Arguments: 2719 2720 minip: lowest allowed address; accepts: 2721 str, eg. 192.168.0.1 2722 list or tuple of octets, eg. [192, 168, 0, 1] 2723 maxip: highest allowed address; same as above 2724 invert: True to allow addresses only from outside of given range; note 2725 that range boundaries are not matched this way 2726 is_localhost: localhost address treatment: 2727 None (default): indifferent 2728 True (enforce): query address must match localhost address 2729 (127.0.0.1) 2730 False (forbid): query address must not match localhost 2731 address 2732 is_private: same as above, except that query address is checked against 2733 two address ranges: 172.16.0.0 - 172.31.255.255 and 2734 192.168.0.0 - 192.168.255.255 2735 is_automatic: same as above, except that query address is checked against 2736 one address range: 169.254.0.0 - 169.254.255.255 2737 2738 Minip and maxip may also be lists or tuples of addresses in all above 2739 forms (str, int, list / tuple), allowing setup of multiple address ranges: 2740 2741 minip = (minip1, minip2, ... minipN) 2742 | | | 2743 | | | 2744 maxip = (maxip1, maxip2, ... maxipN) 2745 2746 Longer iterable will be truncated to match length of shorter one. 2747 2748 Examples:: 2749 2750 #Check for valid IPv4 address: 2751 INPUT(_type='text', _name='name', requires=IS_IPV4()) 2752 2753 #Check for valid IPv4 address belonging to specific range: 2754 INPUT(_type='text', _name='name', 2755 requires=IS_IPV4(minip='100.200.0.0', maxip='100.200.255.255')) 2756 2757 #Check for valid IPv4 address belonging to either 100.110.0.0 - 2758 #100.110.255.255 or 200.50.0.0 - 200.50.0.255 address range: 2759 INPUT(_type='text', _name='name', 2760 requires=IS_IPV4(minip=('100.110.0.0', '200.50.0.0'), 2761 maxip=('100.110.255.255', '200.50.0.255'))) 2762 2763 #Check for valid IPv4 address belonging to private address space: 2764 INPUT(_type='text', _name='name', requires=IS_IPV4(is_private=True)) 2765 2766 #Check for valid IPv4 address that is not a localhost address: 2767 INPUT(_type='text', _name='name', requires=IS_IPV4(is_localhost=False)) 2768 2769 >>> IS_IPV4()('1.2.3.4') 2770 ('1.2.3.4', None) 2771 >>> IS_IPV4()('255.255.255.255') 2772 ('255.255.255.255', None) 2773 >>> IS_IPV4()('1.2.3.4 ') 2774 ('1.2.3.4 ', 'enter valid IPv4 address') 2775 >>> IS_IPV4()('1.2.3.4.5') 2776 ('1.2.3.4.5', 'enter valid IPv4 address') 2777 >>> IS_IPV4()('123.123') 2778 ('123.123', 'enter valid IPv4 address') 2779 >>> IS_IPV4()('1111.2.3.4') 2780 ('1111.2.3.4', 'enter valid IPv4 address') 2781 >>> IS_IPV4()('0111.2.3.4') 2782 ('0111.2.3.4', 'enter valid IPv4 address') 2783 >>> IS_IPV4()('256.2.3.4') 2784 ('256.2.3.4', 'enter valid IPv4 address') 2785 >>> IS_IPV4()('300.2.3.4') 2786 ('300.2.3.4', 'enter valid IPv4 address') 2787 >>> IS_IPV4(minip='1.2.3.4', maxip='1.2.3.4')('1.2.3.4') 2788 ('1.2.3.4', None) 2789 >>> IS_IPV4(minip='1.2.3.5', maxip='1.2.3.9', error_message='bad ip')('1.2.3.4') 2790 ('1.2.3.4', 'bad ip') 2791 >>> IS_IPV4(maxip='1.2.3.4', invert=True)('127.0.0.1') 2792 ('127.0.0.1', None) 2793 >>> IS_IPV4(maxip='1.2.3.4', invert=True)('1.2.3.4') 2794 ('1.2.3.4', 'enter valid IPv4 address') 2795 >>> IS_IPV4(is_localhost=True)('127.0.0.1') 2796 ('127.0.0.1', None) 2797 >>> IS_IPV4(is_localhost=True)('1.2.3.4') 2798 ('1.2.3.4', 'enter valid IPv4 address') 2799 >>> IS_IPV4(is_localhost=False)('127.0.0.1') 2800 ('127.0.0.1', 'enter valid IPv4 address') 2801 >>> IS_IPV4(maxip='100.0.0.0', is_localhost=True)('127.0.0.1') 2802 ('127.0.0.1', 'enter valid IPv4 address') 2803 """ 2804 2805 regex = re.compile( 2806 '^(([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.){3}([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])$') 2807 numbers = (16777216, 65536, 256, 1) 2808 localhost = 2130706433 2809 private = ((2886729728L, 2886795263L), (3232235520L, 3232301055L)) 2810 automatic = (2851995648L, 2852061183L) 2811
2812 - def __init__( 2813 self, 2814 minip='0.0.0.0', 2815 maxip='255.255.255.255', 2816 invert=False, 2817 is_localhost=None, 2818 is_private=None, 2819 is_automatic=None, 2820 error_message='enter valid IPv4 address'):
2821 for n, value in enumerate((minip, maxip)): 2822 temp = [] 2823 if isinstance(value, str): 2824 temp.append(value.split('.')) 2825 elif isinstance(value, (list, tuple)): 2826 if len(value) == len(filter(lambda item: isinstance(item, int), value)) == 4: 2827 temp.append(value) 2828 else: 2829 for item in value: 2830 if isinstance(item, str): 2831 temp.append(item.split('.')) 2832 elif isinstance(item, (list, tuple)): 2833 temp.append(item) 2834 numbers = [] 2835 for item in temp: 2836 number = 0 2837 for i, j in zip(self.numbers, item): 2838 number += i * int(j) 2839 numbers.append(number) 2840 if n == 0: 2841 self.minip = numbers 2842 else: 2843 self.maxip = numbers 2844 self.invert = invert 2845 self.is_localhost = is_localhost 2846 self.is_private = is_private 2847 self.is_automatic = is_automatic 2848 self.error_message = error_message
2849
2850 - def __call__(self, value):
2851 if self.regex.match(value): 2852 number = 0 2853 for i, j in zip(self.numbers, value.split('.')): 2854 number += i * int(j) 2855 ok = False 2856 for bottom, top in zip(self.minip, self.maxip): 2857 if self.invert != (bottom <= number <= top): 2858 ok = True 2859 if not (self.is_localhost == None or self.is_localhost == \ 2860 (number == self.localhost)): 2861 ok = False 2862 if not (self.is_private == None or self.is_private == \ 2863 (sum([number[0] <= number <= number[1] for number in self.private]) > 0)): 2864 ok = False 2865 if not (self.is_automatic == None or self.is_automatic == \ 2866 (self.automatic[0] <= number <= self.automatic[1])): 2867 ok = False 2868 if ok: 2869 return (value, None) 2870 return (value, self.error_message)
2871 2872 if __name__ == '__main__': 2873 import doctest 2874 doctest.testmod() 2875