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