2
Country specific validators for use with FormEncode.
6
from api import FancyValidator
7
from compound import Any
8
from validators import Regex, Invalid, _
16
from turbogears.i18n import format as tgformat
19
has_turbogears = False
22
if not (has_pycountry or has_turbogears):
24
no_country = ('Please easy_install pycountry or validators handling'
25
' country names and/or languages will not work.')
27
############################################################
28
## country lists and functions
29
############################################################
33
('ME', _('Montenegro')),
34
('AU', _('Tasmania')),
37
fuzzy_countrynames = [
41
('GB', _('Great Britain')),
42
('CI', _('Cote de Ivoire')),
48
c1 = tgformat.get_countries('en')
49
c2 = tgformat.get_countries()
51
d = dict(country_additions)
55
d = dict(country_additions)
57
ret = d.items() + fuzzy_countrynames
60
def get_country(code):
61
return dict(get_countries())[code]
64
c1 = tgformat.get_languages('en')
65
c2 = tgformat.get_languages()
73
def get_language(code):
75
return tgformat.get_language(code)
77
return tgformat.get_language(code, 'en')
81
# @@ mark: interestingly, common gettext notation does not work here
83
gettext.bindtextdomain('iso3166', pycountry.LOCALES_DIR)
84
_c = lambda t: gettext.dgettext('iso3166', t)
85
gettext.bindtextdomain('iso639', pycountry.LOCALES_DIR)
86
_l = lambda t: gettext.dgettext('iso639', t)
89
c1 = set([(e.alpha2, _c(e.name)) for e in pycountry.countries])
90
ret = c1.union(country_additions + fuzzy_countrynames)
93
def get_country(code):
94
return _c(pycountry.countries.get(alpha2=code).name)
97
return [(e.alpha2, _l(e.name)) for e in pycountry.languages
98
if e.name and getattr(e, 'alpha2', None)]
100
def get_language(code):
101
return _l(pycountry.languages.get(alpha2=code).name)
104
############################################################
105
## country, state and postal code validators
106
############################################################
108
class DelimitedDigitsPostalCode(Regex):
110
Abstraction of common postal code formats, such as 55555, 55-555 etc.
111
With constant amount of digits. By providing a single digit as partition you
112
can obtain a trivial 'x digits' postal code validator.
116
>>> german = DelimitedDigitsPostalCode(5)
117
>>> german.to_python('55555')
119
>>> german.to_python('5555')
120
Traceback (most recent call last):
122
Invalid: Please enter a zip code (5 digits)
123
>>> polish = DelimitedDigitsPostalCode([2, 3], '-')
124
>>> polish.to_python('55555')
126
>>> polish.to_python('55-555')
128
>>> polish.to_python('5555')
129
Traceback (most recent call last):
131
Invalid: Please enter a zip code (nn-nnn)
132
>>> nicaragua = DelimitedDigitsPostalCode([3, 3, 1], '-')
133
>>> nicaragua.to_python('5554443')
135
>>> nicaragua.to_python('555-4443')
137
>>> nicaragua.to_python('5555')
138
Traceback (most recent call last):
140
Invalid: Please enter a zip code (nnn-nnn-n)
145
def assembly_formatstring(self, partition_lengths, delimiter):
146
if len(partition_lengths) == 1:
147
return _('%d digits') % partition_lengths[0]
149
return delimiter.join(['n'*l for l in partition_lengths])
151
def assembly_regex(self, partition_lengths, delimiter):
152
mg = [r'(\d{%d})' % l for l in partition_lengths]
153
rd = r'\%s?' % delimiter
156
def __init__(self, partition_lengths, delimiter = None,
158
if type(partition_lengths) == type(1):
159
partition_lengths = [partition_lengths]
162
self.format = self.assembly_formatstring(partition_lengths, delimiter)
163
self.regex = self.assembly_regex(partition_lengths, delimiter)
164
(self.partition_lengths, self.delimiter) = (partition_lengths, delimiter)
165
Regex.__init__(self, *args, **kw)
168
invalid=_('Please enter a zip code (%(format)s)'))
170
def _to_python(self, value, state):
171
self.assert_string(value, state)
172
match = self.regex.search(value)
175
self.message('invalid', state, format=self.format),
177
return self.delimiter.join(match.groups())
180
def USPostalCode(*args, **kw):
182
US Postal codes (aka Zip Codes).
186
>>> uspc = USPostalCode()
187
>>> uspc.to_python('55555')
189
>>> uspc.to_python('55555-5555')
191
>>> uspc.to_python('5555')
192
Traceback (most recent call last):
194
Invalid: Please enter a zip code (5 digits)
196
return Any(DelimitedDigitsPostalCode(5, None, *args, **kw),
197
DelimitedDigitsPostalCode([5, 4], '-', *args, **kw))
200
def GermanPostalCode(*args, **kw):
201
return DelimitedDigitsPostalCode(5, None, *args, **kw)
204
def FourDigitsPostalCode(*args, **kw):
205
return DelimitedDigitsPostalCode(4, None, *args, **kw)
208
def PolishPostalCode(*args, **kw):
209
return DelimitedDigitsPostalCode([2, 3], '-', *args, **kw)
212
class ArgentinianPostalCode(Regex):
214
Argentinian Postal codes.
218
>>> ArgentinianPostalCode.to_python('C1070AAM')
220
>>> ArgentinianPostalCode.to_python('c 1070 aam')
222
>>> ArgentinianPostalCode.to_python('5555')
223
Traceback (most recent call last):
225
Invalid: Please enter a zip code (LnnnnLLL)
228
regex = re.compile(r'^([a-zA-Z]{1})\s*(\d{4})\s*([a-zA-Z]{3})$')
232
invalid=_('Please enter a zip code (%s)') % _('LnnnnLLL'))
234
def _to_python(self, value, state):
235
self.assert_string(value, state)
236
match = self.regex.search(value)
239
self.message('invalid', state),
241
return '%s%s%s' % (match.group(1).upper(),
243
match.group(3).upper())
246
class CanadianPostalCode(Regex):
248
Canadian Postal codes.
252
>>> CanadianPostalCode.to_python('V3H 1Z7')
254
>>> CanadianPostalCode.to_python('v3h1z7')
256
>>> CanadianPostalCode.to_python('5555')
257
Traceback (most recent call last):
259
Invalid: Please enter a zip code (LnL nLn)
262
regex = re.compile(r'^([a-zA-Z]\d[a-zA-Z])\s?(\d[a-zA-Z]\d)$')
266
invalid=_('Please enter a zip code (%s)') % _('LnL nLn'))
268
def _to_python(self, value, state):
269
self.assert_string(value, state)
270
match = self.regex.search(value)
273
self.message('invalid', state),
275
return '%s %s' % (match.group(1).upper(), match.group(2).upper())
278
class UKPostalCode(Regex):
280
UK Postal codes. Please see BS 7666.
284
>>> UKPostalCode.to_python('BFPO 3')
286
>>> UKPostalCode.to_python('LE11 3GR')
288
>>> UKPostalCode.to_python('l1a 3gr')
290
>>> UKPostalCode.to_python('5555')
291
Traceback (most recent call last):
293
Invalid: Please enter a valid postal code (for format see BS 7666)
296
regex = re.compile(r'^((ASCN|BBND|BIQQ|FIQQ|PCRN|SIQQ|STHL|TDCU|TKCA) 1ZZ|BFPO (c\/o )?[1-9]{1,4}|GIR 0AA|[A-PR-UWYZ]([0-9]{1,2}|([A-HK-Y][0-9]|[A-HK-Y][0-9]([0-9]|[ABEHMNPRV-Y]))|[0-9][A-HJKS-UW]) [0-9][ABD-HJLNP-UW-Z]{2})$', re.I)
300
invalid=_('Please enter a valid postal code (for format see BS 7666)'))
302
def _to_python(self, value, state):
303
self.assert_string(value, state)
304
match = self.regex.search(value)
307
self.message('invalid', state),
309
return match.group(1).upper()
312
class CountryValidator(FancyValidator):
314
Will convert a country's name into its ISO-3166 abbreviation for unified
315
storage in databases etc. and return a localized country name in the
318
@See http://www.iso.org/iso/country_codes/iso_3166_code_lists.htm
322
>>> CountryValidator.to_python('Germany')
324
>>> CountryValidator.to_python('Finland')
326
>>> CountryValidator.to_python('UNITED STATES')
328
>>> CountryValidator.to_python('Krakovia')
329
Traceback (most recent call last):
331
Invalid: That country is not listed in ISO 3166
332
>>> CountryValidator.from_python('DE')
334
>>> CountryValidator.from_python('FI')
341
valueNotFound=_('That country is not listed in ISO 3166'))
343
def __init__(self, *args, **kw):
344
FancyValidator.__init__(self, *args, **kw)
346
warnings.warn(no_country, Warning, 2)
348
def _to_python(self, value, state):
349
upval = value.upper()
352
c = get_country(upval)
356
for k, v in get_countries():
357
if v.upper() == upval:
359
raise Invalid(self.message('valueNotFound', state), value, state)
361
def _from_python(self, value, state):
363
return get_country(value.upper())
368
class PostalCodeInCountryFormat(FancyValidator):
370
Makes sure the postal code is in the country's format by chosing postal
371
code validator by provided country code. Does convert it into the preferred
376
>>> fs = PostalCodeInCountryFormat('country', 'zip')
377
>>> fs.to_python(dict(country='DE', zip='30167'))
378
{'country': 'DE', 'zip': '30167'}
379
>>> fs.to_python(dict(country='DE', zip='3008'))
380
Traceback (most recent call last):
382
Invalid: Given postal code does not match the country's format.
383
>>> fs.to_python(dict(country='PL', zip='34343'))
384
{'country': 'PL', 'zip': '34-343'}
385
>>> fs = PostalCodeInCountryFormat('staat', 'plz')
386
>>> fs.to_python(dict(staat='GB', plz='l1a 3gr'))
387
{'staat': 'GB', 'plz': 'L1A 3GR'}
390
country_field = 'country'
393
__unpackargs__ = ('country_field', 'zip_field')
396
badFormat=_("Given postal code does not match the country's format."))
399
'AR': ArgentinianPostalCode,
400
'AT': FourDigitsPostalCode,
401
'BE': FourDigitsPostalCode,
402
'BG': FourDigitsPostalCode,
403
'CA': CanadianPostalCode,
404
'CL': lambda: DelimitedDigitsPostalCode(7),
405
'CN': lambda: DelimitedDigitsPostalCode(6),
406
'CR': FourDigitsPostalCode,
407
'DE': GermanPostalCode,
408
'DK': FourDigitsPostalCode,
409
'DO': lambda: DelimitedDigitsPostalCode(5),
410
'ES': lambda: DelimitedDigitsPostalCode(5),
411
'FI': lambda: DelimitedDigitsPostalCode(5),
412
'FR': lambda: DelimitedDigitsPostalCode(5),
414
'GF': lambda: DelimitedDigitsPostalCode(5),
415
'GR': lambda: DelimitedDigitsPostalCode([2, 3], ' '),
416
'HN': lambda: DelimitedDigitsPostalCode(5),
417
'HT': FourDigitsPostalCode,
418
'HU': FourDigitsPostalCode,
419
'IS': lambda: DelimitedDigitsPostalCode(3),
420
'IT': lambda: DelimitedDigitsPostalCode(5),
421
'JP': lambda: DelimitedDigitsPostalCode([3, 4], '-'),
422
'KR': lambda: DelimitedDigitsPostalCode([3, 3], '-'),
423
'LI': FourDigitsPostalCode,
424
'LU': FourDigitsPostalCode,
425
'MC': lambda: DelimitedDigitsPostalCode(5),
426
'NI': lambda: DelimitedDigitsPostalCode([3, 3, 1], '-'),
427
'NO': FourDigitsPostalCode,
428
'PL': PolishPostalCode,
429
'PT': lambda: DelimitedDigitsPostalCode([4, 3], '-'),
430
'PY': FourDigitsPostalCode,
431
'RO': lambda: DelimitedDigitsPostalCode(6),
432
'SE': lambda: DelimitedDigitsPostalCode([3, 2], ' '),
433
'SG': lambda: DelimitedDigitsPostalCode(6),
435
'UY': lambda: DelimitedDigitsPostalCode(5),
438
def validate_python(self, fields_dict, state):
439
if fields_dict[self.country_field] in self._vd:
441
zip_validator = self._vd[fields_dict[self.country_field]]()
442
fields_dict[self.zip_field] = zip_validator.to_python(
443
fields_dict[self.zip_field])
445
message = self.message('badFormat', state)
446
raise Invalid(message, fields_dict, state,
447
error_dict={self.zip_field: e.msg,
448
self.country_field: message})
451
class USStateProvince(FancyValidator):
453
Valid state or province code (two-letter).
455
Well, for now I don't know the province codes, but it does state
456
codes. Give your own `states` list to validate other state-like
457
codes; give `extra_states` to add values without losing the
458
current state values.
462
>>> s = USStateProvince('XX')
463
>>> s.to_python('IL')
465
>>> s.to_python('XX')
467
>>> s.to_python('xx')
469
>>> s.to_python('YY')
470
Traceback (most recent call last):
472
Invalid: That is not a valid state code
475
states = ['AK', 'AL', 'AR', 'AZ', 'CA', 'CO', 'CT', 'DC', 'DE',
476
'FL', 'GA', 'HI', 'IA', 'ID', 'IN', 'IL', 'KS', 'KY',
477
'LA', 'MA', 'MD', 'ME', 'MI', 'MN', 'MO', 'MS', 'MT',
478
'NC', 'ND', 'NE', 'NH', 'NJ', 'NM', 'NV', 'NY', 'OH',
479
'OK', 'OR', 'PA', 'RI', 'SC', 'SD', 'TN', 'TX', 'UT',
480
'VA', 'VT', 'WA', 'WI', 'WV', 'WY']
484
__unpackargs__ = ('extra_states',)
487
empty=_('Please enter a state code'),
488
wrongLength=_('Please enter a state code with TWO letters'),
489
invalid=_('That is not a valid state code'))
491
def validate_python(self, value, state):
492
value = str(value).strip().upper()
495
self.message('empty', state),
497
if not value or len(value) != 2:
499
self.message('wrongLength', state),
501
if value not in self.states and not (
502
self.extra_states and value in self.extra_states):
504
self.message('invalid', state),
507
def _to_python(self, value, state):
508
return str(value).strip().upper()
511
############################################################
512
## phone number validators
513
############################################################
515
class USPhoneNumber(FancyValidator):
517
Validates, and converts to ###-###-####, optionally with extension
518
(as ext.##...). Only support US phone numbers. See
519
InternationalPhoneNumber for support for that kind of phone number.
523
>>> p = USPhoneNumber()
524
>>> p.to_python('333-3333')
525
Traceback (most recent call last):
527
Invalid: Please enter a number, with area code, in the form ###-###-####, optionally with "ext.####"
528
>>> p.to_python('555-555-5555')
530
>>> p.to_python('1-393-555-3939')
532
>>> p.to_python('321.555.4949')
534
>>> p.to_python('3335550000')
539
_phoneRE = re.compile(r'^\s*(?:1-)?(\d\d\d)[\- \.]?(\d\d\d)[\- \.]?(\d\d\d\d)(?:\s*ext\.?\s*(\d+))?\s*$', re.I)
542
phoneFormat=_('Please enter a number, with area code,'
543
' in the form ###-###-####, optionally with "ext.####"'))
545
def _to_python(self, value, state):
546
self.assert_string(value, state)
547
match = self._phoneRE.search(value)
550
self.message('phoneFormat', state),
554
def _from_python(self, value, state):
555
self.assert_string(value, state)
556
match = self._phoneRE.search(value)
558
raise Invalid(self.message('phoneFormat', state),
560
result = '%s-%s-%s' % (match.group(1), match.group(2), match.group(3))
562
result += " ext.%s" % match.group(4)
566
class InternationalPhoneNumber(FancyValidator):
568
Validates, and converts phone numbers to +##-###-#######.
569
Adapted from RFC 3966
571
@param default_cc country code for prepending if none is provided
572
can be a paramerless callable
576
>>> c = InternationalPhoneNumber(default_cc=lambda: 49)
577
>>> c.to_python('0555/8114100')
579
>>> p = InternationalPhoneNumber(default_cc=49)
580
>>> p.to_python('333-3333')
581
Traceback (most recent call last):
583
Invalid: Please enter a number, with area code, in the form +##-###-#######.
584
>>> p.to_python('0555/4860-300')
586
>>> p.to_python('0555-49924-51')
588
>>> p.to_python('0555 / 8114100')
590
>>> p.to_python('0555/8114100')
592
>>> p.to_python('0555 8114100')
594
>>> p.to_python(' +49 (0)555 350 60 0')
596
>>> p.to_python('+49 555 350600')
598
>>> p.to_python('0049/ 555/ 871 82 96')
600
>>> p.to_python('0555-2 50-30')
602
>>> p.to_python('0555 43-1200')
604
>>> p.to_python('(05 55)4 94 33 47')
606
>>> p.to_python('(00 48-555)2 31 72 41')
608
>>> p.to_python('+973-555431')
610
>>> p.to_python('1-393-555-3939')
612
>>> p.to_python('+43 (1) 55528/0')
614
>>> p.to_python('+43 5555 429 62-0')
616
>>> p.to_python('00 218 55 33 50 317 321')
617
'+218-55-3350317-321'
618
>>> p.to_python('+218 (0)55-3636639/38')
620
>>> p.to_python('032 555555 367')
622
>>> p.to_python('(+86) 555 3876693')
627
# Use if there's a default country code you want to use:
629
_mark_chars_re = re.compile(r"[_.!~*'/]")
630
_preTransformations = [
631
(re.compile(r'^(\(?)(?:00\s*)(.+)$'), '%s+%s'),
632
(re.compile(r'^\(\s*(\+?\d+)\s*(\d+)\s*\)(.+)$'), '(%s%s)%s'),
633
(re.compile(r'^\((\+?[-\d]+)\)\s?(\d.+)$'), '%s-%s'),
634
(re.compile(r'^(?:1-)(\d+.+)$'), '+1-%s'),
635
(re.compile(r'^(\+\d+)\s+\(0\)\s*(\d+.+)$'), '%s-%s'),
636
(re.compile(r'^([0+]\d+)[-\s](\d+)$'), '%s-%s'),
637
(re.compile(r'^([0+]\d+)[-\s](\d+)[-\s](\d+)$'), '%s-%s-%s'),
640
(re.compile(r'^\(?0([1-9]\d*)[-)](\d.*)$'), '+%d-%s-%s'),
642
_postTransformations = [
643
(re.compile(r'^(\+\d+)[-\s]\(?(\d+)\)?[-\s](\d+.+)$'), '%s-%s-%s'),
644
(re.compile(r'^(.+)\s(\d+)$'), '%s-%s'),
646
_phoneIsSane = re.compile(r'^(\+[1-9]\d*)-([\d\-]+)$')
649
phoneFormat=_('Please enter a number, with area code,'
650
' in the form +##-###-#######.'))
652
def _perform_rex_transformation(self, value, transformations):
653
for rex, trf in transformations:
654
match = rex.search(value)
656
value = trf % match.groups()
659
def _prepend_country_code(self, value, transformations, country_code):
660
for rex, trf in transformations:
661
match = rex.search(value)
663
return trf % ((country_code,)+match.groups())
666
def _to_python(self, value, state):
667
self.assert_string(value, state)
669
value = value.encode('ascii', 'replace')
671
raise Invalid(self.message('phoneFormat', state), value, state)
672
value = self._mark_chars_re.sub('-', value)
673
for f, t in [(' ', ' '),
674
('--', '-'), (' - ', '-'), ('- ', '-'), (' -', '-')]:
675
value = value.replace(f, t)
676
value = self._perform_rex_transformation(value, self._preTransformations)
678
if callable(self.default_cc):
679
cc = self.default_cc()
682
value = self._prepend_country_code(value, self._ccIncluder, cc)
683
value = self._perform_rex_transformation(value, self._postTransformations)
684
value = value.replace(' ', '')
685
# did we successfully transform that phone number? Thus, is it valid?
686
if not self._phoneIsSane.search(value):
687
raise Invalid(self.message('phoneFormat', state), value, state)
691
############################################################
692
## language validators
693
############################################################
695
class LanguageValidator(FancyValidator):
697
Converts a given language into its ISO 639 alpha 2 code, if there is any.
698
Returns the language's full name in the reverse.
700
Warning: ISO 639 neither differentiates between languages such as Cantonese
701
and Mandarin nor does it contain all spoken languages. E.g., Lechitic
702
languages are missing.
703
Warning: ISO 639 is a smaller subset of ISO 639-2
705
@param key_ok accept the language's code instead of its name for input
710
>>> l = LanguageValidator()
711
>>> l.to_python('German')
713
>>> l.to_python('Chinese')
715
>>> l.to_python('Klingonian')
716
Traceback (most recent call last):
718
Invalid: That language is not listed in ISO 639
719
>>> l.from_python('de')
721
>>> l.from_python('zh')
728
valueNotFound=_('That language is not listed in ISO 639'))
730
def __init__(self, *args, **kw):
731
FancyValidator.__init__(self, *args, **kw)
733
warnings.warn(no_country, Warning, 2)
735
def _to_python(self, value, state):
736
upval = value.upper()
739
c = get_language(value)
743
for k, v in get_languages():
744
if v.upper() == upval:
746
raise Invalid(self.message('valueNotFound', state), value, state)
748
def _from_python(self, value, state):
750
return get_language(value.lower())
2
Country specific validators for use with FormEncode.
6
from api import FancyValidator
7
from compound import Any
8
from validators import Regex, Invalid, _
15
from turbogears.i18n import format as tgformat
19
if pycountry or tgformat:
23
no_country = ('Please easy_install pycountry or validators handling'
24
' country names and/or languages will not work.')
26
############################################################
27
## country lists and functions
28
############################################################
32
('ME', _('Montenegro')),
33
('AU', _('Tasmania')),
36
fuzzy_countrynames = [
40
('GB', _('Great Britain')),
41
('CI', _('Cote de Ivoire')),
47
c1 = tgformat.get_countries('en')
48
c2 = tgformat.get_countries()
50
d = dict(country_additions)
54
d = dict(country_additions)
56
ret = d.items() + fuzzy_countrynames
59
def get_country(code):
60
return dict(get_countries())[code]
63
c1 = tgformat.get_languages('en')
64
c2 = tgformat.get_languages()
72
def get_language(code):
74
return tgformat.get_language(code)
76
return tgformat.get_language(code, 'en')
80
# @@ mark: interestingly, common gettext notation does not work here
82
gettext.bindtextdomain('iso3166', pycountry.LOCALES_DIR)
83
_c = lambda t: gettext.dgettext('iso3166', t)
84
gettext.bindtextdomain('iso639', pycountry.LOCALES_DIR)
85
_l = lambda t: gettext.dgettext('iso639', t)
88
c1 = set([(e.alpha2, _c(e.name)) for e in pycountry.countries])
89
ret = c1.union(country_additions + fuzzy_countrynames)
92
def get_country(code):
93
return _c(pycountry.countries.get(alpha2=code).name)
96
return [(e.alpha2, _l(e.name)) for e in pycountry.languages
97
if e.name and getattr(e, 'alpha2', None)]
99
def get_language(code):
100
return _l(pycountry.languages.get(alpha2=code).name)
103
############################################################
104
## country, state and postal code validators
105
############################################################
107
class DelimitedDigitsPostalCode(Regex):
109
Abstraction of common postal code formats, such as 55555, 55-555 etc.
110
With constant amount of digits. By providing a single digit as partition you
111
can obtain a trivial 'x digits' postal code validator.
115
>>> german = DelimitedDigitsPostalCode(5)
116
>>> german.to_python('55555')
118
>>> german.to_python('5555')
119
Traceback (most recent call last):
121
Invalid: Please enter a zip code (5 digits)
122
>>> polish = DelimitedDigitsPostalCode([2, 3], '-')
123
>>> polish.to_python('55555')
125
>>> polish.to_python('55-555')
127
>>> polish.to_python('5555')
128
Traceback (most recent call last):
130
Invalid: Please enter a zip code (nn-nnn)
131
>>> nicaragua = DelimitedDigitsPostalCode([3, 3, 1], '-')
132
>>> nicaragua.to_python('5554443')
134
>>> nicaragua.to_python('555-4443')
136
>>> nicaragua.to_python('5555')
137
Traceback (most recent call last):
139
Invalid: Please enter a zip code (nnn-nnn-n)
144
def assembly_formatstring(self, partition_lengths, delimiter):
145
if len(partition_lengths) == 1:
146
return _('%d digits') % partition_lengths[0]
148
return delimiter.join(['n'*l for l in partition_lengths])
150
def assembly_regex(self, partition_lengths, delimiter):
151
mg = [r'(\d{%d})' % l for l in partition_lengths]
152
rd = r'\%s?' % delimiter
155
def __init__(self, partition_lengths, delimiter = None,
157
if type(partition_lengths) == type(1):
158
partition_lengths = [partition_lengths]
161
self.format = self.assembly_formatstring(partition_lengths, delimiter)
162
self.regex = self.assembly_regex(partition_lengths, delimiter)
163
(self.partition_lengths, self.delimiter) = (partition_lengths, delimiter)
164
Regex.__init__(self, *args, **kw)
167
invalid=_('Please enter a zip code (%(format)s)'))
169
def _to_python(self, value, state):
170
self.assert_string(value, state)
171
match = self.regex.search(value)
174
self.message('invalid', state, format=self.format),
176
return self.delimiter.join(match.groups())
179
def USPostalCode(*args, **kw):
181
US Postal codes (aka Zip Codes).
185
>>> uspc = USPostalCode()
186
>>> uspc.to_python('55555')
188
>>> uspc.to_python('55555-5555')
190
>>> uspc.to_python('5555')
191
Traceback (most recent call last):
193
Invalid: Please enter a zip code (5 digits)
195
return Any(DelimitedDigitsPostalCode(5, None, *args, **kw),
196
DelimitedDigitsPostalCode([5, 4], '-', *args, **kw))
199
def GermanPostalCode(*args, **kw):
200
return DelimitedDigitsPostalCode(5, None, *args, **kw)
203
def FourDigitsPostalCode(*args, **kw):
204
return DelimitedDigitsPostalCode(4, None, *args, **kw)
207
def PolishPostalCode(*args, **kw):
208
return DelimitedDigitsPostalCode([2, 3], '-', *args, **kw)
211
class ArgentinianPostalCode(Regex):
213
Argentinian Postal codes.
217
>>> ArgentinianPostalCode.to_python('C1070AAM')
219
>>> ArgentinianPostalCode.to_python('c 1070 aam')
221
>>> ArgentinianPostalCode.to_python('5555')
222
Traceback (most recent call last):
224
Invalid: Please enter a zip code (LnnnnLLL)
227
regex = re.compile(r'^([a-zA-Z]{1})\s*(\d{4})\s*([a-zA-Z]{3})$')
231
invalid=_('Please enter a zip code (%s)') % _('LnnnnLLL'))
233
def _to_python(self, value, state):
234
self.assert_string(value, state)
235
match = self.regex.search(value)
238
self.message('invalid', state),
240
return '%s%s%s' % (match.group(1).upper(),
242
match.group(3).upper())
245
class CanadianPostalCode(Regex):
247
Canadian Postal codes.
251
>>> CanadianPostalCode.to_python('V3H 1Z7')
253
>>> CanadianPostalCode.to_python('v3h1z7')
255
>>> CanadianPostalCode.to_python('5555')
256
Traceback (most recent call last):
258
Invalid: Please enter a zip code (LnL nLn)
261
regex = re.compile(r'^([a-zA-Z]\d[a-zA-Z])\s?(\d[a-zA-Z]\d)$')
265
invalid=_('Please enter a zip code (%s)') % _('LnL nLn'))
267
def _to_python(self, value, state):
268
self.assert_string(value, state)
269
match = self.regex.search(value)
272
self.message('invalid', state),
274
return '%s %s' % (match.group(1).upper(), match.group(2).upper())
277
class UKPostalCode(Regex):
279
UK Postal codes. Please see BS 7666.
283
>>> UKPostalCode.to_python('BFPO 3')
285
>>> UKPostalCode.to_python('LE11 3GR')
287
>>> UKPostalCode.to_python('l1a 3gr')
289
>>> UKPostalCode.to_python('5555')
290
Traceback (most recent call last):
292
Invalid: Please enter a valid postal code (for format see BS 7666)
295
regex = re.compile(r'^((ASCN|BBND|BIQQ|FIQQ|PCRN|SIQQ|STHL|TDCU|TKCA) 1ZZ|BFPO (c\/o )?[1-9]{1,4}|GIR 0AA|[A-PR-UWYZ]([0-9]{1,2}|([A-HK-Y][0-9]|[A-HK-Y][0-9]([0-9]|[ABEHMNPRV-Y]))|[0-9][A-HJKS-UW]) [0-9][ABD-HJLNP-UW-Z]{2})$', re.I)
299
invalid=_('Please enter a valid postal code (for format see BS 7666)'))
301
def _to_python(self, value, state):
302
self.assert_string(value, state)
303
match = self.regex.search(value)
306
self.message('invalid', state),
308
return match.group(1).upper()
311
class CountryValidator(FancyValidator):
313
Will convert a country's name into its ISO-3166 abbreviation for unified
314
storage in databases etc. and return a localized country name in the
317
@See http://www.iso.org/iso/country_codes/iso_3166_code_lists.htm
321
>>> CountryValidator.to_python('Germany')
323
>>> CountryValidator.to_python('Finland')
325
>>> CountryValidator.to_python('UNITED STATES')
327
>>> CountryValidator.to_python('Krakovia')
328
Traceback (most recent call last):
330
Invalid: That country is not listed in ISO 3166
331
>>> CountryValidator.from_python('DE')
333
>>> CountryValidator.from_python('FI')
340
valueNotFound=_('That country is not listed in ISO 3166'))
342
def __init__(self, *args, **kw):
343
FancyValidator.__init__(self, *args, **kw)
345
warnings.warn(no_country, Warning, 2)
347
def _to_python(self, value, state):
348
upval = value.upper()
351
c = get_country(upval)
355
for k, v in get_countries():
356
if v.upper() == upval:
358
raise Invalid(self.message('valueNotFound', state), value, state)
360
def _from_python(self, value, state):
362
return get_country(value.upper())
367
class PostalCodeInCountryFormat(FancyValidator):
369
Makes sure the postal code is in the country's format by chosing postal
370
code validator by provided country code. Does convert it into the preferred
375
>>> fs = PostalCodeInCountryFormat('country', 'zip')
376
>>> sorted(fs.to_python(dict(country='DE', zip='30167')).items())
377
[('country', 'DE'), ('zip', '30167')]
378
>>> fs.to_python(dict(country='DE', zip='3008'))
379
Traceback (most recent call last):
381
Invalid: Given postal code does not match the country's format.
382
>>> sorted(fs.to_python(dict(country='PL', zip='34343')).items())
383
[('country', 'PL'), ('zip', '34-343')]
384
>>> fs = PostalCodeInCountryFormat('staat', 'plz')
385
>>> sorted(fs.to_python(dict(staat='GB', plz='l1a 3gr')).items())
386
[('plz', 'L1A 3GR'), ('staat', 'GB')]
389
country_field = 'country'
392
__unpackargs__ = ('country_field', 'zip_field')
395
badFormat=_("Given postal code does not match the country's format."))
398
'AR': ArgentinianPostalCode,
399
'AT': FourDigitsPostalCode,
400
'BE': FourDigitsPostalCode,
401
'BG': FourDigitsPostalCode,
402
'CA': CanadianPostalCode,
403
'CL': lambda: DelimitedDigitsPostalCode(7),
404
'CN': lambda: DelimitedDigitsPostalCode(6),
405
'CR': FourDigitsPostalCode,
406
'DE': GermanPostalCode,
407
'DK': FourDigitsPostalCode,
408
'DO': lambda: DelimitedDigitsPostalCode(5),
409
'ES': lambda: DelimitedDigitsPostalCode(5),
410
'FI': lambda: DelimitedDigitsPostalCode(5),
411
'FR': lambda: DelimitedDigitsPostalCode(5),
413
'GF': lambda: DelimitedDigitsPostalCode(5),
414
'GR': lambda: DelimitedDigitsPostalCode([2, 3], ' '),
415
'HN': lambda: DelimitedDigitsPostalCode(5),
416
'HT': FourDigitsPostalCode,
417
'HU': FourDigitsPostalCode,
418
'IS': lambda: DelimitedDigitsPostalCode(3),
419
'IT': lambda: DelimitedDigitsPostalCode(5),
420
'JP': lambda: DelimitedDigitsPostalCode([3, 4], '-'),
421
'KR': lambda: DelimitedDigitsPostalCode([3, 3], '-'),
422
'LI': FourDigitsPostalCode,
423
'LU': FourDigitsPostalCode,
424
'MC': lambda: DelimitedDigitsPostalCode(5),
425
'NI': lambda: DelimitedDigitsPostalCode([3, 3, 1], '-'),
426
'NO': FourDigitsPostalCode,
427
'PL': PolishPostalCode,
428
'PT': lambda: DelimitedDigitsPostalCode([4, 3], '-'),
429
'PY': FourDigitsPostalCode,
430
'RO': lambda: DelimitedDigitsPostalCode(6),
431
'SE': lambda: DelimitedDigitsPostalCode([3, 2], ' '),
432
'SG': lambda: DelimitedDigitsPostalCode(6),
434
'UY': lambda: DelimitedDigitsPostalCode(5),
437
def validate_python(self, fields_dict, state):
438
if fields_dict[self.country_field] in self._vd:
440
zip_validator = self._vd[fields_dict[self.country_field]]()
441
fields_dict[self.zip_field] = zip_validator.to_python(
442
fields_dict[self.zip_field])
444
message = self.message('badFormat', state)
445
raise Invalid(message, fields_dict, state,
446
error_dict={self.zip_field: e.msg,
447
self.country_field: message})
450
class USStateProvince(FancyValidator):
452
Valid state or province code (two-letter).
454
Well, for now I don't know the province codes, but it does state
455
codes. Give your own `states` list to validate other state-like
456
codes; give `extra_states` to add values without losing the
457
current state values.
461
>>> s = USStateProvince('XX')
462
>>> s.to_python('IL')
464
>>> s.to_python('XX')
466
>>> s.to_python('xx')
468
>>> s.to_python('YY')
469
Traceback (most recent call last):
471
Invalid: That is not a valid state code
474
states = ['AK', 'AL', 'AR', 'AZ', 'CA', 'CO', 'CT', 'DC', 'DE',
475
'FL', 'GA', 'HI', 'IA', 'ID', 'IN', 'IL', 'KS', 'KY',
476
'LA', 'MA', 'MD', 'ME', 'MI', 'MN', 'MO', 'MS', 'MT',
477
'NC', 'ND', 'NE', 'NH', 'NJ', 'NM', 'NV', 'NY', 'OH',
478
'OK', 'OR', 'PA', 'RI', 'SC', 'SD', 'TN', 'TX', 'UT',
479
'VA', 'VT', 'WA', 'WI', 'WV', 'WY']
483
__unpackargs__ = ('extra_states',)
486
empty=_('Please enter a state code'),
487
wrongLength=_('Please enter a state code with TWO letters'),
488
invalid=_('That is not a valid state code'))
490
def validate_python(self, value, state):
491
value = str(value).strip().upper()
494
self.message('empty', state),
496
if not value or len(value) != 2:
498
self.message('wrongLength', state),
500
if value not in self.states and not (
501
self.extra_states and value in self.extra_states):
503
self.message('invalid', state),
506
def _to_python(self, value, state):
507
return str(value).strip().upper()
510
############################################################
511
## phone number validators
512
############################################################
514
class USPhoneNumber(FancyValidator):
516
Validates, and converts to ###-###-####, optionally with extension
517
(as ext.##...). Only support US phone numbers. See
518
InternationalPhoneNumber for support for that kind of phone number.
522
>>> p = USPhoneNumber()
523
>>> p.to_python('333-3333')
524
Traceback (most recent call last):
526
Invalid: Please enter a number, with area code, in the form ###-###-####, optionally with "ext.####"
527
>>> p.to_python('555-555-5555')
529
>>> p.to_python('1-393-555-3939')
531
>>> p.to_python('321.555.4949')
533
>>> p.to_python('3335550000')
538
_phoneRE = re.compile(r'^\s*(?:1-)?(\d\d\d)[\- \.]?(\d\d\d)[\- \.]?(\d\d\d\d)(?:\s*ext\.?\s*(\d+))?\s*$', re.I)
541
phoneFormat=_('Please enter a number, with area code,'
542
' in the form ###-###-####, optionally with "ext.####"'))
544
def _to_python(self, value, state):
545
self.assert_string(value, state)
546
match = self._phoneRE.search(value)
549
self.message('phoneFormat', state),
553
def _from_python(self, value, state):
554
self.assert_string(value, state)
555
match = self._phoneRE.search(value)
557
raise Invalid(self.message('phoneFormat', state),
559
result = '%s-%s-%s' % (match.group(1), match.group(2), match.group(3))
561
result += " ext.%s" % match.group(4)
565
class InternationalPhoneNumber(FancyValidator):
567
Validates, and converts phone numbers to +##-###-#######.
568
Adapted from RFC 3966
570
@param default_cc country code for prepending if none is provided
571
can be a paramerless callable
575
>>> c = InternationalPhoneNumber(default_cc=lambda: 49)
576
>>> c.to_python('0555/8114100')
578
>>> p = InternationalPhoneNumber(default_cc=49)
579
>>> p.to_python('333-3333')
580
Traceback (most recent call last):
582
Invalid: Please enter a number, with area code, in the form +##-###-#######.
583
>>> p.to_python('0555/4860-300')
585
>>> p.to_python('0555-49924-51')
587
>>> p.to_python('0555 / 8114100')
589
>>> p.to_python('0555/8114100')
591
>>> p.to_python('0555 8114100')
593
>>> p.to_python(' +49 (0)555 350 60 0')
595
>>> p.to_python('+49 555 350600')
597
>>> p.to_python('0049/ 555/ 871 82 96')
599
>>> p.to_python('0555-2 50-30')
601
>>> p.to_python('0555 43-1200')
603
>>> p.to_python('(05 55)4 94 33 47')
605
>>> p.to_python('(00 48-555)2 31 72 41')
607
>>> p.to_python('+973-555431')
609
>>> p.to_python('1-393-555-3939')
611
>>> p.to_python('+43 (1) 55528/0')
613
>>> p.to_python('+43 5555 429 62-0')
615
>>> p.to_python('00 218 55 33 50 317 321')
616
'+218-55-3350317-321'
617
>>> p.to_python('+218 (0)55-3636639/38')
619
>>> p.to_python('032 555555 367')
621
>>> p.to_python('(+86) 555 3876693')
626
# Use if there's a default country code you want to use:
628
_mark_chars_re = re.compile(r"[_.!~*'/]")
629
_preTransformations = [
630
(re.compile(r'^(\(?)(?:00\s*)(.+)$'), '%s+%s'),
631
(re.compile(r'^\(\s*(\+?\d+)\s*(\d+)\s*\)(.+)$'), '(%s%s)%s'),
632
(re.compile(r'^\((\+?[-\d]+)\)\s?(\d.+)$'), '%s-%s'),
633
(re.compile(r'^(?:1-)(\d+.+)$'), '+1-%s'),
634
(re.compile(r'^(\+\d+)\s+\(0\)\s*(\d+.+)$'), '%s-%s'),
635
(re.compile(r'^([0+]\d+)[-\s](\d+)$'), '%s-%s'),
636
(re.compile(r'^([0+]\d+)[-\s](\d+)[-\s](\d+)$'), '%s-%s-%s'),
639
(re.compile(r'^\(?0([1-9]\d*)[-)](\d.*)$'), '+%d-%s-%s'),
641
_postTransformations = [
642
(re.compile(r'^(\+\d+)[-\s]\(?(\d+)\)?[-\s](\d+.+)$'), '%s-%s-%s'),
643
(re.compile(r'^(.+)\s(\d+)$'), '%s-%s'),
645
_phoneIsSane = re.compile(r'^(\+[1-9]\d*)-([\d\-]+)$')
648
phoneFormat=_('Please enter a number, with area code,'
649
' in the form +##-###-#######.'))
651
def _perform_rex_transformation(self, value, transformations):
652
for rex, trf in transformations:
653
match = rex.search(value)
655
value = trf % match.groups()
658
def _prepend_country_code(self, value, transformations, country_code):
659
for rex, trf in transformations:
660
match = rex.search(value)
662
return trf % ((country_code,)+match.groups())
665
def _to_python(self, value, state):
666
self.assert_string(value, state)
668
value = value.encode('ascii', 'replace')
670
raise Invalid(self.message('phoneFormat', state), value, state)
671
value = self._mark_chars_re.sub('-', value)
672
for f, t in [(' ', ' '),
673
('--', '-'), (' - ', '-'), ('- ', '-'), (' -', '-')]:
674
value = value.replace(f, t)
675
value = self._perform_rex_transformation(value, self._preTransformations)
677
if callable(self.default_cc):
678
cc = self.default_cc()
681
value = self._prepend_country_code(value, self._ccIncluder, cc)
682
value = self._perform_rex_transformation(value, self._postTransformations)
683
value = value.replace(' ', '')
684
# did we successfully transform that phone number? Thus, is it valid?
685
if not self._phoneIsSane.search(value):
686
raise Invalid(self.message('phoneFormat', state), value, state)
690
############################################################
691
## language validators
692
############################################################
694
class LanguageValidator(FancyValidator):
696
Converts a given language into its ISO 639 alpha 2 code, if there is any.
697
Returns the language's full name in the reverse.
699
Warning: ISO 639 neither differentiates between languages such as Cantonese
700
and Mandarin nor does it contain all spoken languages. E.g., Lechitic
701
languages are missing.
702
Warning: ISO 639 is a smaller subset of ISO 639-2
704
@param key_ok accept the language's code instead of its name for input
709
>>> l = LanguageValidator()
710
>>> l.to_python('German')
712
>>> l.to_python('Chinese')
714
>>> l.to_python('Klingonian')
715
Traceback (most recent call last):
717
Invalid: That language is not listed in ISO 639
718
>>> l.from_python('de')
720
>>> l.from_python('zh')
727
valueNotFound=_('That language is not listed in ISO 639'))
729
def __init__(self, *args, **kw):
730
FancyValidator.__init__(self, *args, **kw)
732
warnings.warn(no_country, Warning, 2)
734
def _to_python(self, value, state):
735
upval = value.upper()
738
c = get_language(value)
742
for k, v in get_languages():
743
if v.upper() == upval:
745
raise Invalid(self.message('valueNotFound', state), value, state)
747
def _from_python(self, value, state):
749
return get_language(value.lower())