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