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):
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 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):
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 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 self.dot=='.': 791 v = decimal.Decimal(str(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 try: 2084 (y, m, d, hh, mm, ss, t0, t1, t2) = \ 2085 time.strptime(value, str(self.format)) 2086 value = datetime.date(y, m, d) 2087 return (value, None) 2088 except: 2089 return (value, translate(self.error_message) % IS_DATETIME.nice(self.format))
2090
2091 - def formatter(self, value):
2092 format = self.format 2093 year = value.year 2094 y = '%.4i' % year 2095 format = format.replace('%y',y[-2:]) 2096 format = format.replace('%Y',y) 2097 if year<1900: 2098 year = 2000 2099 d = datetime.date(year,value.month,value.day) 2100 return d.strftime(format)
2101 2102
2103 -class IS_DATETIME(Validator):
2104 """ 2105 example:: 2106 2107 INPUT(_type='text', _name='name', requires=IS_DATETIME()) 2108 2109 datetime has to be in the ISO8960 format YYYY-MM-DD hh:mm:ss 2110 """ 2111 2112 isodatetime = '%Y-%m-%d %H:%M:%S' 2113 2114 @staticmethod
2115 - def nice(format):
2116 code=(('%Y','1963'), 2117 ('%y','63'), 2118 ('%d','28'), 2119 ('%m','08'), 2120 ('%b','Aug'), 2121 ('%b','August'), 2122 ('%H','14'), 2123 ('%I','02'), 2124 ('%p','PM'), 2125 ('%M','30'), 2126 ('%S','59')) 2127 for (a,b) in code: 2128 format=format.replace(a,b) 2129 return dict(format=format)
2130
2131 - def __init__(self, format='%Y-%m-%d %H:%M:%S', 2132 error_message='enter date and time as %(format)s'):
2133 self.format = str(format) 2134 self.error_message = str(error_message)
2135
2136 - def __call__(self, value):
2137 try: 2138 (y, m, d, hh, mm, ss, t0, t1, t2) = \ 2139 time.strptime(value, str(self.format)) 2140 value = datetime.datetime(y, m, d, hh, mm, ss) 2141 return (value, None) 2142 except: 2143 return (value, translate(self.error_message) % IS_DATETIME.nice(self.format))
2144
2145 - def formatter(self, value):
2146 format = self.format 2147 year = value.year 2148 y = '%.4i' % year 2149 format = format.replace('%y',y[-2:]) 2150 format = format.replace('%Y',y) 2151 if year<1900: 2152 year = 2000 2153 d = datetime.datetime(year,value.month,value.day,value.hour,value.minute,value.second) 2154 return d.strftime(format)
2155
2156 -class IS_DATE_IN_RANGE(IS_DATE):
2157 """ 2158 example:: 2159 2160 >>> v = IS_DATE_IN_RANGE(minimum=datetime.date(2008,1,1), \ 2161 maximum=datetime.date(2009,12,31), \ 2162 format="%m/%d/%Y",error_message="oops") 2163 2164 >>> v('03/03/2008') 2165 (datetime.date(2008, 3, 3), None) 2166 2167 >>> v('03/03/2010') 2168 (datetime.date(2010, 3, 3), 'oops') 2169 2170 """
2171 - def __init__(self, 2172 minimum = None, 2173 maximum = None, 2174 format='%Y-%m-%d', 2175 error_message = None):
2176 self.minimum = minimum 2177 self.maximum = maximum 2178 if error_message is None: 2179 if minimum is None: 2180 error_message = "enter date on or before %(max)s" 2181 elif maximum is None: 2182 error_message = "enter date on or after %(min)s" 2183 else: 2184 error_message = "enter date in range %(min)s %(max)s" 2185 d = dict(min=minimum, max=maximum) 2186 IS_DATE.__init__(self, 2187 format = format, 2188 error_message = error_message % d)
2189
2190 - def __call__(self, value):
2191 (value, msg) = IS_DATE.__call__(self,value) 2192 if msg is not None: 2193 return (value, msg) 2194 if self.minimum and self.minimum > value: 2195 return (value, translate(self.error_message)) 2196 if self.maximum and value > self.maximum: 2197 return (value, translate(self.error_message)) 2198 return (value, None)
2199 2200
2201 -class IS_DATETIME_IN_RANGE(IS_DATETIME):
2202 """ 2203 example:: 2204 2205 >>> v = IS_DATETIME_IN_RANGE(\ 2206 minimum=datetime.datetime(2008,1,1,12,20), \ 2207 maximum=datetime.datetime(2009,12,31,12,20), \ 2208 format="%m/%d/%Y %H:%M",error_message="oops") 2209 >>> v('03/03/2008 12:40') 2210 (datetime.datetime(2008, 3, 3, 12, 40), None) 2211 2212 >>> v('03/03/2010 10:34') 2213 (datetime.datetime(2010, 3, 3, 10, 34), 'oops') 2214 """
2215 - def __init__(self, 2216 minimum = None, 2217 maximum = None, 2218 format = '%Y-%m-%d %H:%M:%S', 2219 error_message = None):
2220 self.minimum = minimum 2221 self.maximum = maximum 2222 if error_message is None: 2223 if minimum is None: 2224 error_message = "enter date and time on or before %(max)s" 2225 elif maximum is None: 2226 error_message = "enter date and time on or after %(min)s" 2227 else: 2228 error_message = "enter date and time in range %(min)s %(max)s" 2229 d = dict(min = minimum, max = maximum) 2230 IS_DATETIME.__init__(self, 2231 format = format, 2232 error_message = error_message % d)
2233
2234 - def __call__(self, value):
2235 (value, msg) = IS_DATETIME.__call__(self, value) 2236 if msg is not None: 2237 return (value, msg) 2238 if self.minimum and self.minimum > value: 2239 return (value, translate(self.error_message)) 2240 if self.maximum and value > self.maximum: 2241 return (value, translate(self.error_message)) 2242 return (value, None)
2243 2244
2245 -class IS_LIST_OF(Validator):
2246
2247 - def __init__(self, other):
2248 self.other = other
2249
2250 - def __call__(self, value):
2251 ivalue = value 2252 if not isinstance(value, list): 2253 ivalue = [ivalue] 2254 new_value = [] 2255 for item in ivalue: 2256 (v, e) = self.other(item) 2257 if e: 2258 return (value, e) 2259 else: 2260 new_value.append(v) 2261 return (new_value, None)
2262 2263
2264 -class IS_LOWER(Validator):
2265 """ 2266 convert to lower case 2267 2268 >>> IS_LOWER()('ABC') 2269 ('abc', None) 2270 >>> IS_LOWER()('Ñ') 2271 ('\\xc3\\xb1', None) 2272 """ 2273
2274 - def __call__(self, value):
2275 return (value.decode('utf8').lower().encode('utf8'), None)
2276 2277
2278 -class IS_UPPER(Validator):
2279 """ 2280 convert to upper case 2281 2282 >>> IS_UPPER()('abc') 2283 ('ABC', None) 2284 >>> IS_UPPER()('ñ') 2285 ('\\xc3\\x91', None) 2286 """ 2287
2288 - def __call__(self, value):
2289 return (value.decode('utf8').upper().encode('utf8'), None)
2290 2291
2292 -def urlify(value, maxlen=80, remove_underscores=True):
2293 s = value.lower() # to lowercase 2294 s = s.decode('utf-8') # to utf-8 2295 s = unicodedata.normalize('NFKD', s) # normalize eg è => e, ñ => n 2296 s = s.encode('ASCII', 'ignore') # encode as ASCII 2297 s = re.sub('&\w+;', '', s) # strip html entities 2298 if remove_underscores: 2299 s = re.sub('[\s_]+', '-', s) # whitespace & underscores to hyphens 2300 s = re.sub('[^a-z0-9\-]', '', s) # strip all but alphanumeric/hyphen 2301 else: 2302 s = re.sub('\s+', '-', s) # whitespace to hyphens 2303 s = re.sub('[^a-z0-9\-_]', '', s) # strip all but alphanumeric/hyphen 2304 # (already lowercase) 2305 s = re.sub('[-_][-_]+', '-', s) # collapse strings of hyphens 2306 s = s.strip('-') # remove leading and trailing hyphens 2307 return s[:maxlen] # enforce maximum length
2308 2309
2310 -class IS_SLUG(Validator):
2311 """ 2312 convert arbitrary text string to a slug 2313 2314 >>> IS_SLUG()('abc123') 2315 ('abc123', None) 2316 >>> IS_SLUG()('ABC123') 2317 ('abc123', None) 2318 >>> IS_SLUG()('abc-123') 2319 ('abc-123', None) 2320 >>> IS_SLUG()('abc--123') 2321 ('abc-123', None) 2322 >>> IS_SLUG()('abc 123') 2323 ('abc-123', None) 2324 >>> IS_SLUG()('abc\t_123') 2325 ('abc-123', None) 2326 >>> IS_SLUG()('-abc-') 2327 ('abc', None) 2328 >>> IS_SLUG()('abc&amp;123') 2329 ('abc123', None) 2330 >>> IS_SLUG()('abc&amp;123&amp;def') 2331 ('abc123def', None) 2332 >>> IS_SLUG()('ñ') 2333 ('n', None) 2334 >>> IS_SLUG(maxlen=4)('abc123') 2335 ('abc1', None) 2336 """ 2337 2338 @staticmethod
2339 - def urlify(value, maxlen=80, remove_underscores=True):
2340 return urlify(value, maxlen, remove_underscores)
2341
2342 - def __init__(self, maxlen=80, check=False, error_message='must be slug'):
2343 self.maxlen = maxlen 2344 self.check = check 2345 self.error_message = error_message
2346
2347 - def __call__(self, value):
2348 if self.check and value != IS_SLUG.urlify(value, self.maxlen): 2349 return (value, translate(self.error_message)) 2350 return (urlify(value,self.maxlen), None)
2351
2352 -class IS_EMPTY_OR(Validator):
2353 """ 2354 dummy class for testing IS_EMPTY_OR 2355 2356 >>> IS_EMPTY_OR(IS_EMAIL())('abc@def.com') 2357 ('abc@def.com', None) 2358 >>> IS_EMPTY_OR(IS_EMAIL())(' ') 2359 (None, None) 2360 >>> IS_EMPTY_OR(IS_EMAIL(), null='abc')(' ') 2361 ('abc', None) 2362 >>> IS_EMPTY_OR(IS_EMAIL(), null='abc', empty_regex='def')('def') 2363 ('abc', None) 2364 >>> IS_EMPTY_OR(IS_EMAIL())('abc') 2365 ('abc', 'enter a valid email address') 2366 >>> IS_EMPTY_OR(IS_EMAIL())(' abc ') 2367 ('abc', 'enter a valid email address') 2368 """ 2369
2370 - def __init__(self, other, null=None, empty_regex=None):
2371 (self.other, self.null) = (other, null) 2372 if empty_regex is not None: 2373 self.empty_regex = re.compile(empty_regex) 2374 else: 2375 self.empty_regex = None 2376 if hasattr(other, 'multiple'): 2377 self.multiple = other.multiple 2378 if hasattr(other, 'options'): 2379 self.options=self._options
2380
2381 - def _options(self):
2382 options = self.other.options() 2383 if (not options or options[0][0]!='') and not self.multiple: 2384 options.insert(0,('','')) 2385 return options
2386
2387 - def set_self_id(self, id):
2388 if isinstance(self.other, (list, tuple)): 2389 for item in self.other: 2390 if hasattr(item, 'set_self_id'): 2391 item.set_self_id(id) 2392 else: 2393 if hasattr(self.other, 'set_self_id'): 2394 self.other.set_self_id(id)
2395
2396 - def __call__(self, value):
2397 value, empty = is_empty(value, empty_regex=self.empty_regex) 2398 if empty: 2399 return (self.null, None) 2400 if isinstance(self.other, (list, tuple)): 2401 for item in self.other: 2402 value, error = item(value) 2403 if error: break 2404 return value, error 2405 else: 2406 return self.other(value)
2407
2408 - def formatter(self, value):
2409 if hasattr(self.other, 'formatter'): 2410 return self.other.formatter(value) 2411 return value
2412 2413 IS_NULL_OR = IS_EMPTY_OR # for backward compatibility 2414 2415
2416 -class CLEANUP(Validator):
2417 """ 2418 example:: 2419 2420 INPUT(_type='text', _name='name', requires=CLEANUP()) 2421 2422 removes special characters on validation 2423 """ 2424
2425 - def __init__(self, regex='[^\x09\x0a\x0d\x20-\x7e]'):
2426 self.regex = re.compile(regex)
2427
2428 - def __call__(self, value):
2429 v = self.regex.sub('',str(value).strip()) 2430 return (v, None)
2431 2432
2433 -class CRYPT(object):
2434 """ 2435 example:: 2436 2437 INPUT(_type='text', _name='name', requires=CRYPT()) 2438 2439 encodes the value on validation with a digest. 2440 2441 If no arguments are provided CRYPT uses the MD5 algorithm. 2442 If the key argument is provided the HMAC+MD5 algorithm is used. 2443 If the digest_alg is specified this is used to replace the 2444 MD5 with, for example, SHA512. The digest_alg can be 2445 the name of a hashlib algorithm as a string or the algorithm itself. 2446 """ 2447
2448 - def __init__(self, key=None, digest_alg='md5'):
2449 self.key = key 2450 self.digest_alg = digest_alg
2451
2452 - def __call__(self, value):
2453 if self.key: 2454 return (hmac_hash(value, self.key, self.digest_alg), None) 2455 else: 2456 return (simple_hash(value, self.digest_alg), None)
2457 2458
2459 -class IS_STRONG(object):
2460 """ 2461 example:: 2462 2463 INPUT(_type='password', _name='passwd', 2464 requires=IS_STRONG(min=10, special=2, upper=2)) 2465 2466 enforces complexity requirements on a field 2467 """ 2468
2469 - def __init__(self, min=8, max=20, upper=1, lower=1, number=1, 2470 special=1, specials=r'~!@#$%^&*()_+-=?<>,.:;{}[]|', 2471 invalid=' "', error_message=None):
2472 self.min = min 2473 self.max = max 2474 self.upper = upper 2475 self.lower = lower 2476 self.number = number 2477 self.special = special 2478 self.specials = specials 2479 self.invalid = invalid 2480 self.error_message = error_message
2481
2482 - def __call__(self, value):
2483 failures = [] 2484 if type(self.min) == int and self.min > 0: 2485 if not len(value) >= self.min: 2486 failures.append("Minimum length is %s" % self.min) 2487 if type(self.max) == int and self.max > 0: 2488 if not len(value) <= self.max: 2489 failures.append("Maximum length is %s" % self.max) 2490 if type(self.special) == int: 2491 all_special = [ch in value for ch in self.specials] 2492 if self.special > 0: 2493 if not all_special.count(True) >= self.special: 2494 failures.append("Must include at least %s of the following : %s" % (self.special, self.specials)) 2495 if self.invalid: 2496 all_invalid = [ch in value for ch in self.invalid] 2497 if all_invalid.count(True) > 0: 2498 failures.append("May not contain any of the following: %s" \ 2499 % self.invalid) 2500 if type(self.upper) == int: 2501 all_upper = re.findall("[A-Z]", value) 2502 if self.upper > 0: 2503 if not len(all_upper) >= self.upper: 2504 failures.append("Must include at least %s upper case" \ 2505 % str(self.upper)) 2506 else: 2507 if len(all_upper) > 0: 2508 failures.append("May not include any upper case letters") 2509 if type(self.lower) == int: 2510 all_lower = re.findall("[a-z]", value) 2511 if self.lower > 0: 2512 if not len(all_lower) >= self.lower: 2513 failures.append("Must include at least %s lower case" \ 2514 % str(self.lower)) 2515 else: 2516 if len(all_lower) > 0: 2517 failures.append("May not include any lower case letters") 2518 if type(self.number) == int: 2519 all_number = re.findall("[0-9]", value) 2520 if self.number > 0: 2521 numbers = "number" 2522 if self.number > 1: 2523 numbers = "numbers" 2524 if not len(all_number) >= self.number: 2525 failures.append("Must include at least %s %s" \ 2526 % (str(self.number), numbers)) 2527 else: 2528 if len(all_number) > 0: 2529 failures.append("May not include any numbers") 2530 if len(failures) == 0: 2531 return (value, None) 2532 if not translate(self.error_message): 2533 from html import XML 2534 return (value, XML('<br />'.join(failures))) 2535 else: 2536 return (value, translate(self.error_message))
2537 2538
2539 -class IS_IN_SUBSET(IS_IN_SET):
2540
2541 - def __init__(self, *a, **b):
2542 IS_IN_SET.__init__(self, *a, **b)
2543
2544 - def __call__(self, value):
2545 values = re.compile("\w+").findall(str(value)) 2546 failures = [x for x in values if IS_IN_SET.__call__(self, x)[1]] 2547 if failures: 2548 return (value, translate(self.error_message)) 2549 return (value, None)
2550 2551
2552 -class IS_IMAGE(Validator):
2553 """ 2554 Checks if file uploaded through file input was saved in one of selected 2555 image formats and has dimensions (width and height) within given boundaries. 2556 2557 Does *not* check for maximum file size (use IS_LENGTH for that). Returns 2558 validation failure if no data was uploaded. 2559 2560 Supported file formats: BMP, GIF, JPEG, PNG. 2561 2562 Code parts taken from 2563 http://mail.python.org/pipermail/python-list/2007-June/617126.html 2564 2565 Arguments: 2566 2567 extensions: iterable containing allowed *lowercase* image file extensions 2568 ('jpg' extension of uploaded file counts as 'jpeg') 2569 maxsize: iterable containing maximum width and height of the image 2570 minsize: iterable containing minimum width and height of the image 2571 2572 Use (-1, -1) as minsize to pass image size check. 2573 2574 Examples:: 2575 2576 #Check if uploaded file is in any of supported image formats: 2577 INPUT(_type='file', _name='name', requires=IS_IMAGE()) 2578 2579 #Check if uploaded file is either JPEG or PNG: 2580 INPUT(_type='file', _name='name', 2581 requires=IS_IMAGE(extensions=('jpeg', 'png'))) 2582 2583 #Check if uploaded file is PNG with maximum size of 200x200 pixels: 2584 INPUT(_type='file', _name='name', 2585 requires=IS_IMAGE(extensions=('png'), maxsize=(200, 200))) 2586 """ 2587
2588 - def __init__(self, 2589 extensions=('bmp', 'gif', 'jpeg', 'png'), 2590 maxsize=(10000, 10000), 2591 minsize=(0, 0), 2592 error_message='invalid image'):
2593 2594 self.extensions = extensions 2595 self.maxsize = maxsize 2596 self.minsize = minsize 2597 self.error_message = error_message
2598
2599 - def __call__(self, value):
2600 try: 2601 extension = value.filename.rfind('.') 2602 assert extension >= 0 2603 extension = value.filename[extension + 1:].lower() 2604 if extension == 'jpg': 2605 extension = 'jpeg' 2606 assert extension in self.extensions 2607 if extension == 'bmp': 2608 width, height = self.__bmp(value.file) 2609 elif extension == 'gif': 2610 width, height = self.__gif(value.file) 2611 elif extension == 'jpeg': 2612 width, height = self.__jpeg(value.file) 2613 elif extension == 'png': 2614 width, height = self.__png(value.file) 2615 else: 2616 width = -1 2617 height = -1 2618 assert self.minsize[0] <= width <= self.maxsize[0] \ 2619 and self.minsize[1] <= height <= self.maxsize[1] 2620 value.file.seek(0) 2621 return (value, None) 2622 except: 2623 return (value, translate(self.error_message))
2624
2625 - def __bmp(self, stream):
2626 if stream.read(2) == 'BM': 2627 stream.read(16) 2628 return struct.unpack("<LL", stream.read(8)) 2629 return (-1, -1)
2630
2631 - def __gif(self, stream):
2632 if stream.read(6) in ('GIF87a', 'GIF89a'): 2633 stream = stream.read(5) 2634 if len(stream) == 5: 2635 return tuple(struct.unpack("<HHB", stream)[:-1]) 2636 return (-1, -1)
2637
2638 - def __jpeg(self, stream):
2639 if stream.read(2) == '\xFF\xD8': 2640 while True: 2641 (marker, code, length) = struct.unpack("!BBH", stream.read(4)) 2642 if marker != 0xFF: 2643 break 2644 elif code >= 0xC0 and code <= 0xC3: 2645 return tuple(reversed( 2646 struct.unpack("!xHH", stream.read(5)))) 2647 else: 2648 stream.read(length - 2) 2649 return (-1, -1)
2650
2651 - def __png(self, stream):
2652 if stream.read(8) == '\211PNG\r\n\032\n': 2653 stream.read(4) 2654 if stream.read(4) == "IHDR": 2655 return struct.unpack("!LL", stream.read(8)) 2656 return (-1, -1)
2657 2658
2659 -class IS_UPLOAD_FILENAME(Validator):
2660 """ 2661 Checks if name and extension of file uploaded through file input matches 2662 given criteria. 2663 2664 Does *not* ensure the file type in any way. Returns validation failure 2665 if no data was uploaded. 2666 2667 Arguments:: 2668 2669 filename: filename (before dot) regex 2670 extension: extension (after dot) regex 2671 lastdot: which dot should be used as a filename / extension separator: 2672 True means last dot, eg. file.png -> file / png 2673 False means first dot, eg. file.tar.gz -> file / tar.gz 2674 case: 0 - keep the case, 1 - transform the string into lowercase (default), 2675 2 - transform the string into uppercase 2676 2677 If there is no dot present, extension checks will be done against empty 2678 string and filename checks against whole value. 2679 2680 Examples:: 2681 2682 #Check if file has a pdf extension (case insensitive): 2683 INPUT(_type='file', _name='name', 2684 requires=IS_UPLOAD_FILENAME(extension='pdf')) 2685 2686 #Check if file has a tar.gz extension and name starting with backup: 2687 INPUT(_type='file', _name='name', 2688 requires=IS_UPLOAD_FILENAME(filename='backup.*', 2689 extension='tar.gz', lastdot=False)) 2690 2691 #Check if file has no extension and name matching README 2692 #(case sensitive): 2693 INPUT(_type='file', _name='name', 2694 requires=IS_UPLOAD_FILENAME(filename='^README$', 2695 extension='^$', case=0)) 2696 """ 2697
2698 - def __init__(self, filename=None, extension=None, lastdot=True, case=1, 2699 error_message='enter valid filename'):
2700 if isinstance(filename, str): 2701 filename = re.compile(filename) 2702 if isinstance(extension, str): 2703 extension = re.compile(extension) 2704 self.filename = filename 2705 self.extension = extension 2706 self.lastdot = lastdot 2707 self.case = case 2708 self.error_message = error_message
2709
2710 - def __call__(self, value):
2711 try: 2712 string = value.filename 2713 except: 2714 return (value, translate(self.error_message)) 2715 if self.case == 1: 2716 string = string.lower() 2717 elif self.case == 2: 2718 string = string.upper() 2719 if self.lastdot: 2720 dot = string.rfind('.') 2721 else: 2722 dot = string.find('.') 2723 if dot == -1: 2724 dot = len(string) 2725 if self.filename and not self.filename.match(string[:dot]): 2726 return (value, translate(self.error_message)) 2727 elif self.extension and not self.extension.match(string[dot + 1:]): 2728 return (value, translate(self.error_message)) 2729 else: 2730 return (value, None)
2731 2732
2733 -class IS_IPV4(Validator):
2734 """ 2735 Checks if field's value is an IP version 4 address in decimal form. Can 2736 be set to force addresses from certain range. 2737 2738 IPv4 regex taken from: http://regexlib.com/REDetails.aspx?regexp_id=1411 2739 2740 Arguments: 2741 2742 minip: lowest allowed address; accepts: 2743 str, eg. 192.168.0.1 2744 list or tuple of octets, eg. [192, 168, 0, 1] 2745 maxip: highest allowed address; same as above 2746 invert: True to allow addresses only from outside of given range; note 2747 that range boundaries are not matched this way 2748 is_localhost: localhost address treatment: 2749 None (default): indifferent 2750 True (enforce): query address must match localhost address 2751 (127.0.0.1) 2752 False (forbid): query address must not match localhost 2753 address 2754 is_private: same as above, except that query address is checked against 2755 two address ranges: 172.16.0.0 - 172.31.255.255 and 2756 192.168.0.0 - 192.168.255.255 2757 is_automatic: same as above, except that query address is checked against 2758 one address range: 169.254.0.0 - 169.254.255.255 2759 2760 Minip and maxip may also be lists or tuples of addresses in all above 2761 forms (str, int, list / tuple), allowing setup of multiple address ranges: 2762 2763 minip = (minip1, minip2, ... minipN) 2764 | | | 2765 | | | 2766 maxip = (maxip1, maxip2, ... maxipN) 2767 2768 Longer iterable will be truncated to match length of shorter one. 2769 2770 Examples:: 2771 2772 #Check for valid IPv4 address: 2773 INPUT(_type='text', _name='name', requires=IS_IPV4()) 2774 2775 #Check for valid IPv4 address belonging to specific range: 2776 INPUT(_type='text', _name='name', 2777 requires=IS_IPV4(minip='100.200.0.0', maxip='100.200.255.255')) 2778 2779 #Check for valid IPv4 address belonging to either 100.110.0.0 - 2780 #100.110.255.255 or 200.50.0.0 - 200.50.0.255 address range: 2781 INPUT(_type='text', _name='name', 2782 requires=IS_IPV4(minip=('100.110.0.0', '200.50.0.0'), 2783 maxip=('100.110.255.255', '200.50.0.255'))) 2784 2785 #Check for valid IPv4 address belonging to private address space: 2786 INPUT(_type='text', _name='name', requires=IS_IPV4(is_private=True)) 2787 2788 #Check for valid IPv4 address that is not a localhost address: 2789 INPUT(_type='text', _name='name', requires=IS_IPV4(is_localhost=False)) 2790 2791 >>> IS_IPV4()('1.2.3.4') 2792 ('1.2.3.4', None) 2793 >>> IS_IPV4()('255.255.255.255') 2794 ('255.255.255.255', None) 2795 >>> IS_IPV4()('1.2.3.4 ') 2796 ('1.2.3.4 ', 'enter valid IPv4 address') 2797 >>> IS_IPV4()('1.2.3.4.5') 2798 ('1.2.3.4.5', 'enter valid IPv4 address') 2799 >>> IS_IPV4()('123.123') 2800 ('123.123', 'enter valid IPv4 address') 2801 >>> IS_IPV4()('1111.2.3.4') 2802 ('1111.2.3.4', 'enter valid IPv4 address') 2803 >>> IS_IPV4()('0111.2.3.4') 2804 ('0111.2.3.4', 'enter valid IPv4 address') 2805 >>> IS_IPV4()('256.2.3.4') 2806 ('256.2.3.4', 'enter valid IPv4 address') 2807 >>> IS_IPV4()('300.2.3.4') 2808 ('300.2.3.4', 'enter valid IPv4 address') 2809 >>> IS_IPV4(minip='1.2.3.4', maxip='1.2.3.4')('1.2.3.4') 2810 ('1.2.3.4', None) 2811 >>> IS_IPV4(minip='1.2.3.5', maxip='1.2.3.9', error_message='bad ip')('1.2.3.4') 2812 ('1.2.3.4', 'bad ip') 2813 >>> IS_IPV4(maxip='1.2.3.4', invert=True)('127.0.0.1') 2814 ('127.0.0.1', None) 2815 >>> IS_IPV4(maxip='1.2.3.4', invert=True)('1.2.3.4') 2816 ('1.2.3.4', 'enter valid IPv4 address') 2817 >>> IS_IPV4(is_localhost=True)('127.0.0.1') 2818 ('127.0.0.1', None) 2819 >>> IS_IPV4(is_localhost=True)('1.2.3.4') 2820 ('1.2.3.4', 'enter valid IPv4 address') 2821 >>> IS_IPV4(is_localhost=False)('127.0.0.1') 2822 ('127.0.0.1', 'enter valid IPv4 address') 2823 >>> IS_IPV4(maxip='100.0.0.0', is_localhost=True)('127.0.0.1') 2824 ('127.0.0.1', 'enter valid IPv4 address') 2825 """ 2826 2827 regex = re.compile( 2828 '^(([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])$') 2829 numbers = (16777216, 65536, 256, 1) 2830 localhost = 2130706433 2831 private = ((2886729728L, 2886795263L), (3232235520L, 3232301055L)) 2832 automatic = (2851995648L, 2852061183L) 2833
2834 - def __init__( 2835 self, 2836 minip='0.0.0.0', 2837 maxip='255.255.255.255', 2838 invert=False, 2839 is_localhost=None, 2840 is_private=None, 2841 is_automatic=None, 2842 error_message='enter valid IPv4 address'):
2843 for n, value in enumerate((minip, maxip)): 2844 temp = [] 2845 if isinstance(value, str): 2846 temp.append(value.split('.')) 2847 elif isinstance(value, (list, tuple)): 2848 if len(value) == len(filter(lambda item: isinstance(item, int), value)) == 4: 2849 temp.append(value) 2850 else: 2851 for item in value: 2852 if isinstance(item, str): 2853 temp.append(item.split('.')) 2854 elif isinstance(item, (list, tuple)): 2855 temp.append(item) 2856 numbers = [] 2857 for item in temp: 2858 number = 0 2859 for i, j in zip(self.numbers, item): 2860 number += i * int(j) 2861 numbers.append(number) 2862 if n == 0: 2863 self.minip = numbers 2864 else: 2865 self.maxip = numbers 2866 self.invert = invert 2867 self.is_localhost = is_localhost 2868 self.is_private = is_private 2869 self.is_automatic = is_automatic 2870 self.error_message = error_message
2871
2872 - def __call__(self, value):
2873 if self.regex.match(value): 2874 number = 0 2875 for i, j in zip(self.numbers, value.split('.')): 2876 number += i * int(j) 2877 ok = False 2878 for bottom, top in zip(self.minip, self.maxip): 2879 if self.invert != (bottom <= number <= top): 2880 ok = True 2881 if not (self.is_localhost == None or self.is_localhost == \ 2882 (number == self.localhost)): 2883 ok = False 2884 if not (self.is_private == None or self.is_private == \ 2885 (sum([number[0] <= number <= number[1] for number in self.private]) > 0)): 2886 ok = False 2887 if not (self.is_automatic == None or self.is_automatic == \ 2888 (self.automatic[0] <= number <= self.automatic[1])): 2889 ok = False 2890 if ok: 2891 return (value, None) 2892 return (value, translate(self.error_message))
2893 2894 if __name__ == '__main__': 2895 import doctest 2896 doctest.testmod() 2897