~ubuntu-branches/ubuntu/trusty/python-formencode/trusty-proposed

« back to all changes in this revision

Viewing changes to formencode/national.py

  • Committer: Package Import Robot
  • Author(s): Matthias Klose
  • Date: 2014-02-24 12:54:57 UTC
  • mfrom: (1.1.6) (3.1.7 sid)
  • Revision ID: package-import@ubuntu.com-20140224125457-l6vywmz9ip22q60e
Tags: 1.2.6-1ubuntu1
Remove tests that fail without network connection. See #738722.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
"""
2
 
Country specific validators for use with FormEncode.
3
 
"""
4
 
import re
5
 
 
6
 
from api import FancyValidator
7
 
from compound import Any
8
 
from validators import Regex, Invalid, _
9
 
 
10
 
try:
11
 
    import pycountry
12
 
    has_pycountry = True
13
 
except:
14
 
    has_pycountry = False
15
 
try:
16
 
    from turbogears.i18n import format as tgformat
17
 
    has_turbogears = True
18
 
except:
19
 
    has_turbogears = False
20
 
 
21
 
no_country = False
22
 
if not (has_pycountry or has_turbogears):
23
 
    import warnings
24
 
    no_country = ('Please easy_install pycountry or validators handling'
25
 
                  ' country names and/or languages will not work.')
26
 
 
27
 
############################################################
28
 
## country lists and functions
29
 
############################################################
30
 
 
31
 
country_additions = [
32
 
    ('BY', _('Belarus')),
33
 
    ('ME', _('Montenegro')),
34
 
    ('AU', _('Tasmania')),
35
 
]
36
 
 
37
 
fuzzy_countrynames = [
38
 
    ('US', 'U.S.A'),
39
 
    ('US', 'USA'),
40
 
    ('GB', _('Britain')),
41
 
    ('GB', _('Great Britain')),
42
 
    ('CI', _('Cote de Ivoire')),
43
 
]
44
 
 
45
 
if has_turbogears:
46
 
 
47
 
    def get_countries():
48
 
        c1 = tgformat.get_countries('en')
49
 
        c2 = tgformat.get_countries()
50
 
        if len(c1) > len(c2):
51
 
            d = dict(country_additions)
52
 
            d.update(dict(c1))
53
 
            d.update(dict(c2))
54
 
        else:
55
 
            d = dict(country_additions)
56
 
            d.update(dict(c2))
57
 
        ret = d.items() + fuzzy_countrynames
58
 
        return ret
59
 
 
60
 
    def get_country(code):
61
 
        return dict(get_countries())[code]
62
 
 
63
 
    def get_languages():
64
 
        c1 = tgformat.get_languages('en')
65
 
        c2 = tgformat.get_languages()
66
 
        if len(c1) > len(c2):
67
 
            d = dict(c1)
68
 
            d.update(dict(c2))
69
 
            return d.items()
70
 
        else:
71
 
            return c2
72
 
 
73
 
    def get_language(code):
74
 
        try:
75
 
            return tgformat.get_language(code)
76
 
        except KeyError:
77
 
            return tgformat.get_language(code, 'en')
78
 
 
79
 
elif has_pycountry:
80
 
    
81
 
    # @@ mark: interestingly, common gettext notation does not work here
82
 
    import gettext
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)
87
 
 
88
 
    def get_countries():
89
 
        c1 = set([(e.alpha2, _c(e.name)) for e in pycountry.countries])
90
 
        ret = c1.union(country_additions + fuzzy_countrynames)
91
 
        return ret
92
 
 
93
 
    def get_country(code):
94
 
        return _c(pycountry.countries.get(alpha2=code).name)
95
 
 
96
 
    def get_languages():
97
 
        return [(e.alpha2, _l(e.name)) for e in pycountry.languages
98
 
            if e.name and getattr(e, 'alpha2', None)]
99
 
 
100
 
    def get_language(code):
101
 
        return _l(pycountry.languages.get(alpha2=code).name)
102
 
 
103
 
 
104
 
############################################################
105
 
## country, state and postal code validators
106
 
############################################################
107
 
 
108
 
class DelimitedDigitsPostalCode(Regex):
109
 
    """
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.
113
 
 
114
 
    ::
115
 
 
116
 
        >>> german = DelimitedDigitsPostalCode(5)
117
 
        >>> german.to_python('55555')
118
 
        '55555'
119
 
        >>> german.to_python('5555')
120
 
        Traceback (most recent call last):
121
 
            ...
122
 
        Invalid: Please enter a zip code (5 digits)
123
 
        >>> polish = DelimitedDigitsPostalCode([2, 3], '-')
124
 
        >>> polish.to_python('55555')
125
 
        '55-555'
126
 
        >>> polish.to_python('55-555')
127
 
        '55-555'
128
 
        >>> polish.to_python('5555')
129
 
        Traceback (most recent call last):
130
 
            ...
131
 
        Invalid: Please enter a zip code (nn-nnn)
132
 
        >>> nicaragua = DelimitedDigitsPostalCode([3, 3, 1], '-')
133
 
        >>> nicaragua.to_python('5554443')
134
 
        '555-444-3'
135
 
        >>> nicaragua.to_python('555-4443')
136
 
        '555-444-3'
137
 
        >>> nicaragua.to_python('5555')
138
 
        Traceback (most recent call last):
139
 
            ...
140
 
        Invalid: Please enter a zip code (nnn-nnn-n)
141
 
    """
142
 
 
143
 
    strip = True
144
 
 
145
 
    def assembly_formatstring(self, partition_lengths, delimiter):
146
 
        if len(partition_lengths) == 1:
147
 
            return _('%d digits') % partition_lengths[0]
148
 
        else:
149
 
            return delimiter.join(['n'*l for l in partition_lengths])
150
 
 
151
 
    def assembly_regex(self, partition_lengths, delimiter):
152
 
        mg = [r'(\d{%d})' % l for l in partition_lengths]
153
 
        rd = r'\%s?' % delimiter
154
 
        return rd.join(mg)
155
 
 
156
 
    def __init__(self, partition_lengths, delimiter = None,
157
 
                 *args, **kw):
158
 
        if type(partition_lengths) == type(1):
159
 
            partition_lengths = [partition_lengths]
160
 
        if not delimiter:
161
 
            delimiter = ''
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)
166
 
 
167
 
    messages = dict(
168
 
        invalid=_('Please enter a zip code (%(format)s)'))
169
 
 
170
 
    def _to_python(self, value, state):
171
 
        self.assert_string(value, state)
172
 
        match = self.regex.search(value)
173
 
        if not match:
174
 
            raise Invalid(
175
 
                self.message('invalid', state, format=self.format),
176
 
                value, state)
177
 
        return self.delimiter.join(match.groups())
178
 
 
179
 
 
180
 
def USPostalCode(*args, **kw):
181
 
    """
182
 
    US Postal codes (aka Zip Codes).
183
 
 
184
 
    ::
185
 
 
186
 
        >>> uspc = USPostalCode()
187
 
        >>> uspc.to_python('55555')
188
 
        '55555'
189
 
        >>> uspc.to_python('55555-5555')
190
 
        '55555-5555'
191
 
        >>> uspc.to_python('5555')
192
 
        Traceback (most recent call last):
193
 
            ...
194
 
        Invalid: Please enter a zip code (5 digits)
195
 
    """
196
 
    return Any(DelimitedDigitsPostalCode(5, None, *args, **kw),
197
 
               DelimitedDigitsPostalCode([5, 4], '-', *args, **kw))
198
 
 
199
 
 
200
 
def GermanPostalCode(*args, **kw):
201
 
    return DelimitedDigitsPostalCode(5, None, *args, **kw)
202
 
 
203
 
 
204
 
def FourDigitsPostalCode(*args, **kw):
205
 
    return DelimitedDigitsPostalCode(4, None, *args, **kw)
206
 
 
207
 
 
208
 
def PolishPostalCode(*args, **kw):
209
 
    return DelimitedDigitsPostalCode([2, 3], '-', *args, **kw)
210
 
 
211
 
 
212
 
class ArgentinianPostalCode(Regex):
213
 
    """
214
 
    Argentinian Postal codes.
215
 
 
216
 
    ::
217
 
 
218
 
        >>> ArgentinianPostalCode.to_python('C1070AAM')
219
 
        'C1070AAM'
220
 
        >>> ArgentinianPostalCode.to_python('c 1070 aam')
221
 
        'C1070AAM'
222
 
        >>> ArgentinianPostalCode.to_python('5555')
223
 
        Traceback (most recent call last):
224
 
            ...
225
 
        Invalid: Please enter a zip code (LnnnnLLL)
226
 
    """
227
 
 
228
 
    regex = re.compile(r'^([a-zA-Z]{1})\s*(\d{4})\s*([a-zA-Z]{3})$')
229
 
    strip = True
230
 
 
231
 
    messages = dict(
232
 
        invalid=_('Please enter a zip code (%s)') % _('LnnnnLLL'))
233
 
 
234
 
    def _to_python(self, value, state):
235
 
        self.assert_string(value, state)
236
 
        match = self.regex.search(value)
237
 
        if not match:
238
 
            raise Invalid(
239
 
                self.message('invalid', state),
240
 
                value, state)
241
 
        return '%s%s%s' % (match.group(1).upper(),
242
 
                           match.group(2),
243
 
                           match.group(3).upper())
244
 
 
245
 
 
246
 
class CanadianPostalCode(Regex):
247
 
    """
248
 
    Canadian Postal codes.
249
 
 
250
 
    ::
251
 
 
252
 
        >>> CanadianPostalCode.to_python('V3H 1Z7')
253
 
        'V3H 1Z7'
254
 
        >>> CanadianPostalCode.to_python('v3h1z7')
255
 
        'V3H 1Z7'
256
 
        >>> CanadianPostalCode.to_python('5555')
257
 
        Traceback (most recent call last):
258
 
            ...
259
 
        Invalid: Please enter a zip code (LnL nLn)
260
 
    """
261
 
 
262
 
    regex = re.compile(r'^([a-zA-Z]\d[a-zA-Z])\s?(\d[a-zA-Z]\d)$')
263
 
    strip = True
264
 
 
265
 
    messages = dict(
266
 
        invalid=_('Please enter a zip code (%s)') % _('LnL nLn'))
267
 
 
268
 
    def _to_python(self, value, state):
269
 
        self.assert_string(value, state)
270
 
        match = self.regex.search(value)
271
 
        if not match:
272
 
            raise Invalid(
273
 
                self.message('invalid', state),
274
 
                value, state)
275
 
        return '%s %s' % (match.group(1).upper(), match.group(2).upper())
276
 
 
277
 
 
278
 
class UKPostalCode(Regex):
279
 
    """
280
 
    UK Postal codes. Please see BS 7666.
281
 
 
282
 
    ::
283
 
 
284
 
        >>> UKPostalCode.to_python('BFPO 3')
285
 
        'BFPO 3'
286
 
        >>> UKPostalCode.to_python('LE11 3GR')
287
 
        'LE11 3GR'
288
 
        >>> UKPostalCode.to_python('l1a 3gr')
289
 
        'L1A 3GR'
290
 
        >>> UKPostalCode.to_python('5555')
291
 
        Traceback (most recent call last):
292
 
            ...
293
 
        Invalid: Please enter a valid postal code (for format see BS 7666)
294
 
    """
295
 
 
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)
297
 
    strip = True
298
 
 
299
 
    messages = dict(
300
 
        invalid=_('Please enter a valid postal code (for format see BS 7666)'))
301
 
 
302
 
    def _to_python(self, value, state):
303
 
        self.assert_string(value, state)
304
 
        match = self.regex.search(value)
305
 
        if not match:
306
 
            raise Invalid(
307
 
                self.message('invalid', state),
308
 
                value, state)
309
 
        return match.group(1).upper()
310
 
 
311
 
 
312
 
class CountryValidator(FancyValidator):
313
 
    """
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
316
 
    reverse step.
317
 
 
318
 
    @See http://www.iso.org/iso/country_codes/iso_3166_code_lists.htm
319
 
 
320
 
    ::
321
 
 
322
 
        >>> CountryValidator.to_python('Germany')
323
 
        'DE'
324
 
        >>> CountryValidator.to_python('Finland')
325
 
        'FI'
326
 
        >>> CountryValidator.to_python('UNITED STATES')
327
 
        'US'
328
 
        >>> CountryValidator.to_python('Krakovia')
329
 
        Traceback (most recent call last):
330
 
            ...
331
 
        Invalid: That country is not listed in ISO 3166
332
 
        >>> CountryValidator.from_python('DE')
333
 
        'Germany'
334
 
        >>> CountryValidator.from_python('FI')
335
 
        'Finland'
336
 
    """
337
 
 
338
 
    key_ok = True
339
 
 
340
 
    messages = dict(
341
 
        valueNotFound=_('That country is not listed in ISO 3166'))
342
 
 
343
 
    def __init__(self, *args, **kw):
344
 
        FancyValidator.__init__(self, *args, **kw)
345
 
        if no_country:
346
 
            warnings.warn(no_country, Warning, 2)
347
 
 
348
 
    def _to_python(self, value, state):
349
 
        upval = value.upper()
350
 
        if self.key_ok:
351
 
            try:
352
 
                c = get_country(upval)
353
 
                return upval
354
 
            except:
355
 
                pass
356
 
        for k, v in get_countries():
357
 
            if v.upper() == upval:
358
 
                return k
359
 
        raise Invalid(self.message('valueNotFound', state), value, state)
360
 
 
361
 
    def _from_python(self, value, state):
362
 
        try:
363
 
            return get_country(value.upper())
364
 
        except KeyError:
365
 
            return value
366
 
 
367
 
 
368
 
class PostalCodeInCountryFormat(FancyValidator):
369
 
    """
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
372
 
    format, too.
373
 
 
374
 
    ::
375
 
 
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):
381
 
            ...
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'}
388
 
    """
389
 
 
390
 
    country_field = 'country'
391
 
    zip_field = 'zip'
392
 
    
393
 
    __unpackargs__ = ('country_field', 'zip_field')
394
 
    
395
 
    messages = dict(
396
 
        badFormat=_("Given postal code does not match the country's format."))
397
 
 
398
 
    _vd = {
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),
413
 
        'GB': UKPostalCode,
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),
434
 
        'US': USPostalCode,
435
 
        'UY': lambda: DelimitedDigitsPostalCode(5),
436
 
    }
437
 
 
438
 
    def validate_python(self, fields_dict, state):
439
 
        if fields_dict[self.country_field] in self._vd:
440
 
            try:
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])
444
 
            except Invalid, e:
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})
449
 
 
450
 
 
451
 
class USStateProvince(FancyValidator):
452
 
    """
453
 
    Valid state or province code (two-letter).
454
 
 
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.
459
 
 
460
 
    ::
461
 
 
462
 
        >>> s = USStateProvince('XX')
463
 
        >>> s.to_python('IL')
464
 
        'IL'
465
 
        >>> s.to_python('XX')
466
 
        'XX'
467
 
        >>> s.to_python('xx')
468
 
        'XX'
469
 
        >>> s.to_python('YY')
470
 
        Traceback (most recent call last):
471
 
            ...
472
 
        Invalid: That is not a valid state code
473
 
    """
474
 
 
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']
481
 
 
482
 
    extra_states = []
483
 
 
484
 
    __unpackargs__ = ('extra_states',)
485
 
 
486
 
    messages = dict(
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'))
490
 
 
491
 
    def validate_python(self, value, state):
492
 
        value = str(value).strip().upper()
493
 
        if not value:
494
 
            raise Invalid(
495
 
                self.message('empty', state),
496
 
                value, state)
497
 
        if not value or len(value) != 2:
498
 
            raise Invalid(
499
 
                self.message('wrongLength', state),
500
 
                value, state)
501
 
        if value not in self.states and not (
502
 
                self.extra_states and value in self.extra_states):
503
 
            raise Invalid(
504
 
                self.message('invalid', state),
505
 
                value, state)
506
 
 
507
 
    def _to_python(self, value, state):
508
 
        return str(value).strip().upper()
509
 
 
510
 
 
511
 
############################################################
512
 
## phone number validators
513
 
############################################################
514
 
 
515
 
class USPhoneNumber(FancyValidator):
516
 
    """
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.
520
 
 
521
 
    ::
522
 
 
523
 
        >>> p = USPhoneNumber()
524
 
        >>> p.to_python('333-3333')
525
 
        Traceback (most recent call last):
526
 
            ...
527
 
        Invalid: Please enter a number, with area code, in the form ###-###-####, optionally with "ext.####"
528
 
        >>> p.to_python('555-555-5555')
529
 
        '555-555-5555'
530
 
        >>> p.to_python('1-393-555-3939')
531
 
        '1-393-555-3939'
532
 
        >>> p.to_python('321.555.4949')
533
 
        '321.555.4949'
534
 
        >>> p.to_python('3335550000')
535
 
        '3335550000'
536
 
    """
537
 
    # for emacs: "
538
 
 
539
 
    _phoneRE = re.compile(r'^\s*(?:1-)?(\d\d\d)[\- \.]?(\d\d\d)[\- \.]?(\d\d\d\d)(?:\s*ext\.?\s*(\d+))?\s*$', re.I)
540
 
 
541
 
    messages = dict(
542
 
        phoneFormat=_('Please enter a number, with area code,'
543
 
            ' in the form ###-###-####, optionally with "ext.####"'))
544
 
 
545
 
    def _to_python(self, value, state):
546
 
        self.assert_string(value, state)
547
 
        match = self._phoneRE.search(value)
548
 
        if not match:
549
 
            raise Invalid(
550
 
                self.message('phoneFormat', state),
551
 
                value, state)
552
 
        return value
553
 
 
554
 
    def _from_python(self, value, state):
555
 
        self.assert_string(value, state)
556
 
        match = self._phoneRE.search(value)
557
 
        if not match:
558
 
            raise Invalid(self.message('phoneFormat', state),
559
 
                          value, state)
560
 
        result = '%s-%s-%s' % (match.group(1), match.group(2), match.group(3))
561
 
        if match.group(4):
562
 
            result += " ext.%s" % match.group(4)
563
 
        return result
564
 
 
565
 
 
566
 
class InternationalPhoneNumber(FancyValidator):
567
 
    """
568
 
    Validates, and converts phone numbers to +##-###-#######.
569
 
    Adapted from RFC 3966
570
 
 
571
 
    @param  default_cc      country code for prepending if none is provided
572
 
                            can be a paramerless callable
573
 
 
574
 
    ::
575
 
 
576
 
        >>> c = InternationalPhoneNumber(default_cc=lambda: 49)
577
 
        >>> c.to_python('0555/8114100')
578
 
        '+49-555-8114100'
579
 
        >>> p = InternationalPhoneNumber(default_cc=49)
580
 
        >>> p.to_python('333-3333')
581
 
        Traceback (most recent call last):
582
 
            ...
583
 
        Invalid: Please enter a number, with area code, in the form +##-###-#######.
584
 
        >>> p.to_python('0555/4860-300')
585
 
        '+49-555-4860-300'
586
 
        >>> p.to_python('0555-49924-51')
587
 
        '+49-555-49924-51'
588
 
        >>> p.to_python('0555 / 8114100')
589
 
        '+49-555-8114100'
590
 
        >>> p.to_python('0555/8114100')
591
 
        '+49-555-8114100'
592
 
        >>> p.to_python('0555 8114100')
593
 
        '+49-555-8114100'
594
 
        >>> p.to_python(' +49 (0)555 350 60 0')
595
 
        '+49-555-35060-0'
596
 
        >>> p.to_python('+49 555 350600')
597
 
        '+49-555-350600'
598
 
        >>> p.to_python('0049/ 555/ 871 82 96')
599
 
        '+49-555-87182-96'
600
 
        >>> p.to_python('0555-2 50-30')
601
 
        '+49-555-250-30'
602
 
        >>> p.to_python('0555 43-1200')
603
 
        '+49-555-43-1200'
604
 
        >>> p.to_python('(05 55)4 94 33 47')
605
 
        '+49-555-49433-47'
606
 
        >>> p.to_python('(00 48-555)2 31 72 41')
607
 
        '+48-555-23172-41'
608
 
        >>> p.to_python('+973-555431')
609
 
        '+973-555431'
610
 
        >>> p.to_python('1-393-555-3939')
611
 
        '+1-393-555-3939'
612
 
        >>> p.to_python('+43 (1) 55528/0')
613
 
        '+43-1-55528-0'
614
 
        >>> p.to_python('+43 5555 429 62-0')
615
 
        '+43-5555-42962-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')
619
 
        '+218-55-3636639-38'
620
 
        >>> p.to_python('032 555555 367')
621
 
        '+49-32-555555-367'
622
 
        >>> p.to_python('(+86) 555 3876693')
623
 
        '+86-555-3876693'
624
 
    """
625
 
 
626
 
    strip = True
627
 
    # Use if there's a default country code you want to use:
628
 
    default_cc = None
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'),
638
 
        ]
639
 
    _ccIncluder = [
640
 
        (re.compile(r'^\(?0([1-9]\d*)[-)](\d.*)$'), '+%d-%s-%s'),
641
 
        ]
642
 
    _postTransformations = [
643
 
        (re.compile(r'^(\+\d+)[-\s]\(?(\d+)\)?[-\s](\d+.+)$'), '%s-%s-%s'),
644
 
        (re.compile(r'^(.+)\s(\d+)$'), '%s-%s'),
645
 
        ]
646
 
    _phoneIsSane = re.compile(r'^(\+[1-9]\d*)-([\d\-]+)$')
647
 
 
648
 
    messages = dict(
649
 
        phoneFormat=_('Please enter a number, with area code,'
650
 
            ' in the form +##-###-#######.'))
651
 
 
652
 
    def _perform_rex_transformation(self, value, transformations):
653
 
        for rex, trf in transformations:
654
 
            match = rex.search(value)
655
 
            if match:
656
 
                value = trf % match.groups()
657
 
        return value
658
 
 
659
 
    def _prepend_country_code(self, value, transformations, country_code):
660
 
        for rex, trf in transformations:
661
 
            match = rex.search(value)
662
 
            if match:
663
 
                return trf % ((country_code,)+match.groups())
664
 
        return value
665
 
 
666
 
    def _to_python(self, value, state):
667
 
        self.assert_string(value, state)
668
 
        try:
669
 
            value = value.encode('ascii', 'replace')
670
 
        except:
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)
677
 
        if self.default_cc:
678
 
            if callable(self.default_cc):
679
 
                cc = self.default_cc()
680
 
            else:
681
 
                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)
688
 
        return value
689
 
 
690
 
 
691
 
############################################################
692
 
## language validators
693
 
############################################################
694
 
 
695
 
class LanguageValidator(FancyValidator):
696
 
    """
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.
699
 
 
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
704
 
 
705
 
    @param  key_ok      accept the language's code instead of its name for input
706
 
                        defaults to True
707
 
 
708
 
    ::
709
 
 
710
 
        >>> l = LanguageValidator()
711
 
        >>> l.to_python('German')
712
 
        'de'
713
 
        >>> l.to_python('Chinese')
714
 
        'zh'
715
 
        >>> l.to_python('Klingonian')
716
 
        Traceback (most recent call last):
717
 
            ...
718
 
        Invalid: That language is not listed in ISO 639
719
 
        >>> l.from_python('de')
720
 
        'German'
721
 
        >>> l.from_python('zh')
722
 
        'Chinese'
723
 
    """
724
 
 
725
 
    key_ok = True
726
 
 
727
 
    messages = dict(
728
 
        valueNotFound=_('That language is not listed in ISO 639'))
729
 
 
730
 
    def __init__(self, *args, **kw):
731
 
        FancyValidator.__init__(self, *args, **kw)
732
 
        if no_country:
733
 
            warnings.warn(no_country, Warning, 2)
734
 
 
735
 
    def _to_python(self, value, state):
736
 
        upval = value.upper()
737
 
        if self.key_ok:
738
 
            try:
739
 
                c = get_language(value)
740
 
                return value
741
 
            except:
742
 
                pass
743
 
        for k, v in get_languages():
744
 
            if v.upper() == upval:
745
 
                return k
746
 
        raise Invalid(self.message('valueNotFound', state), value, state)
747
 
 
748
 
    def _from_python(self, value, state):
749
 
        try:
750
 
            return get_language(value.lower())
751
 
        except KeyError:
752
 
            return value
 
1
"""
 
2
Country specific validators for use with FormEncode.
 
3
"""
 
4
import re
 
5
 
 
6
from api import FancyValidator
 
7
from compound import Any
 
8
from validators import Regex, Invalid, _
 
9
 
 
10
try:
 
11
    import pycountry
 
12
except:
 
13
    pycountry = None
 
14
try:
 
15
    from turbogears.i18n import format as tgformat
 
16
except:
 
17
    tgformat = None
 
18
 
 
19
if pycountry or tgformat:
 
20
    no_country = False
 
21
else:
 
22
    import warnings
 
23
    no_country = ('Please easy_install pycountry or validators handling'
 
24
                  ' country names and/or languages will not work.')
 
25
 
 
26
############################################################
 
27
## country lists and functions
 
28
############################################################
 
29
 
 
30
country_additions = [
 
31
    ('BY', _('Belarus')),
 
32
    ('ME', _('Montenegro')),
 
33
    ('AU', _('Tasmania')),
 
34
]
 
35
 
 
36
fuzzy_countrynames = [
 
37
    ('US', 'U.S.A'),
 
38
    ('US', 'USA'),
 
39
    ('GB', _('Britain')),
 
40
    ('GB', _('Great Britain')),
 
41
    ('CI', _('Cote de Ivoire')),
 
42
]
 
43
 
 
44
if tgformat:
 
45
 
 
46
    def get_countries():
 
47
        c1 = tgformat.get_countries('en')
 
48
        c2 = tgformat.get_countries()
 
49
        if len(c1) > len(c2):
 
50
            d = dict(country_additions)
 
51
            d.update(dict(c1))
 
52
            d.update(dict(c2))
 
53
        else:
 
54
            d = dict(country_additions)
 
55
            d.update(dict(c2))
 
56
        ret = d.items() + fuzzy_countrynames
 
57
        return ret
 
58
 
 
59
    def get_country(code):
 
60
        return dict(get_countries())[code]
 
61
 
 
62
    def get_languages():
 
63
        c1 = tgformat.get_languages('en')
 
64
        c2 = tgformat.get_languages()
 
65
        if len(c1) > len(c2):
 
66
            d = dict(c1)
 
67
            d.update(dict(c2))
 
68
            return d.items()
 
69
        else:
 
70
            return c2
 
71
 
 
72
    def get_language(code):
 
73
        try:
 
74
            return tgformat.get_language(code)
 
75
        except KeyError:
 
76
            return tgformat.get_language(code, 'en')
 
77
 
 
78
elif pycountry:
 
79
 
 
80
    # @@ mark: interestingly, common gettext notation does not work here
 
81
    import gettext
 
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)
 
86
 
 
87
    def get_countries():
 
88
        c1 = set([(e.alpha2, _c(e.name)) for e in pycountry.countries])
 
89
        ret = c1.union(country_additions + fuzzy_countrynames)
 
90
        return ret
 
91
 
 
92
    def get_country(code):
 
93
        return _c(pycountry.countries.get(alpha2=code).name)
 
94
 
 
95
    def get_languages():
 
96
        return [(e.alpha2, _l(e.name)) for e in pycountry.languages
 
97
            if e.name and getattr(e, 'alpha2', None)]
 
98
 
 
99
    def get_language(code):
 
100
        return _l(pycountry.languages.get(alpha2=code).name)
 
101
 
 
102
 
 
103
############################################################
 
104
## country, state and postal code validators
 
105
############################################################
 
106
 
 
107
class DelimitedDigitsPostalCode(Regex):
 
108
    """
 
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.
 
112
 
 
113
    ::
 
114
 
 
115
        >>> german = DelimitedDigitsPostalCode(5)
 
116
        >>> german.to_python('55555')
 
117
        '55555'
 
118
        >>> german.to_python('5555')
 
119
        Traceback (most recent call last):
 
120
            ...
 
121
        Invalid: Please enter a zip code (5 digits)
 
122
        >>> polish = DelimitedDigitsPostalCode([2, 3], '-')
 
123
        >>> polish.to_python('55555')
 
124
        '55-555'
 
125
        >>> polish.to_python('55-555')
 
126
        '55-555'
 
127
        >>> polish.to_python('5555')
 
128
        Traceback (most recent call last):
 
129
            ...
 
130
        Invalid: Please enter a zip code (nn-nnn)
 
131
        >>> nicaragua = DelimitedDigitsPostalCode([3, 3, 1], '-')
 
132
        >>> nicaragua.to_python('5554443')
 
133
        '555-444-3'
 
134
        >>> nicaragua.to_python('555-4443')
 
135
        '555-444-3'
 
136
        >>> nicaragua.to_python('5555')
 
137
        Traceback (most recent call last):
 
138
            ...
 
139
        Invalid: Please enter a zip code (nnn-nnn-n)
 
140
    """
 
141
 
 
142
    strip = True
 
143
 
 
144
    def assembly_formatstring(self, partition_lengths, delimiter):
 
145
        if len(partition_lengths) == 1:
 
146
            return _('%d digits') % partition_lengths[0]
 
147
        else:
 
148
            return delimiter.join(['n'*l for l in partition_lengths])
 
149
 
 
150
    def assembly_regex(self, partition_lengths, delimiter):
 
151
        mg = [r'(\d{%d})' % l for l in partition_lengths]
 
152
        rd = r'\%s?' % delimiter
 
153
        return rd.join(mg)
 
154
 
 
155
    def __init__(self, partition_lengths, delimiter = None,
 
156
                 *args, **kw):
 
157
        if type(partition_lengths) == type(1):
 
158
            partition_lengths = [partition_lengths]
 
159
        if not delimiter:
 
160
            delimiter = ''
 
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)
 
165
 
 
166
    messages = dict(
 
167
        invalid=_('Please enter a zip code (%(format)s)'))
 
168
 
 
169
    def _to_python(self, value, state):
 
170
        self.assert_string(value, state)
 
171
        match = self.regex.search(value)
 
172
        if not match:
 
173
            raise Invalid(
 
174
                self.message('invalid', state, format=self.format),
 
175
                value, state)
 
176
        return self.delimiter.join(match.groups())
 
177
 
 
178
 
 
179
def USPostalCode(*args, **kw):
 
180
    """
 
181
    US Postal codes (aka Zip Codes).
 
182
 
 
183
    ::
 
184
 
 
185
        >>> uspc = USPostalCode()
 
186
        >>> uspc.to_python('55555')
 
187
        '55555'
 
188
        >>> uspc.to_python('55555-5555')
 
189
        '55555-5555'
 
190
        >>> uspc.to_python('5555')
 
191
        Traceback (most recent call last):
 
192
            ...
 
193
        Invalid: Please enter a zip code (5 digits)
 
194
    """
 
195
    return Any(DelimitedDigitsPostalCode(5, None, *args, **kw),
 
196
               DelimitedDigitsPostalCode([5, 4], '-', *args, **kw))
 
197
 
 
198
 
 
199
def GermanPostalCode(*args, **kw):
 
200
    return DelimitedDigitsPostalCode(5, None, *args, **kw)
 
201
 
 
202
 
 
203
def FourDigitsPostalCode(*args, **kw):
 
204
    return DelimitedDigitsPostalCode(4, None, *args, **kw)
 
205
 
 
206
 
 
207
def PolishPostalCode(*args, **kw):
 
208
    return DelimitedDigitsPostalCode([2, 3], '-', *args, **kw)
 
209
 
 
210
 
 
211
class ArgentinianPostalCode(Regex):
 
212
    """
 
213
    Argentinian Postal codes.
 
214
 
 
215
    ::
 
216
 
 
217
        >>> ArgentinianPostalCode.to_python('C1070AAM')
 
218
        'C1070AAM'
 
219
        >>> ArgentinianPostalCode.to_python('c 1070 aam')
 
220
        'C1070AAM'
 
221
        >>> ArgentinianPostalCode.to_python('5555')
 
222
        Traceback (most recent call last):
 
223
            ...
 
224
        Invalid: Please enter a zip code (LnnnnLLL)
 
225
    """
 
226
 
 
227
    regex = re.compile(r'^([a-zA-Z]{1})\s*(\d{4})\s*([a-zA-Z]{3})$')
 
228
    strip = True
 
229
 
 
230
    messages = dict(
 
231
        invalid=_('Please enter a zip code (%s)') % _('LnnnnLLL'))
 
232
 
 
233
    def _to_python(self, value, state):
 
234
        self.assert_string(value, state)
 
235
        match = self.regex.search(value)
 
236
        if not match:
 
237
            raise Invalid(
 
238
                self.message('invalid', state),
 
239
                value, state)
 
240
        return '%s%s%s' % (match.group(1).upper(),
 
241
                           match.group(2),
 
242
                           match.group(3).upper())
 
243
 
 
244
 
 
245
class CanadianPostalCode(Regex):
 
246
    """
 
247
    Canadian Postal codes.
 
248
 
 
249
    ::
 
250
 
 
251
        >>> CanadianPostalCode.to_python('V3H 1Z7')
 
252
        'V3H 1Z7'
 
253
        >>> CanadianPostalCode.to_python('v3h1z7')
 
254
        'V3H 1Z7'
 
255
        >>> CanadianPostalCode.to_python('5555')
 
256
        Traceback (most recent call last):
 
257
            ...
 
258
        Invalid: Please enter a zip code (LnL nLn)
 
259
    """
 
260
 
 
261
    regex = re.compile(r'^([a-zA-Z]\d[a-zA-Z])\s?(\d[a-zA-Z]\d)$')
 
262
    strip = True
 
263
 
 
264
    messages = dict(
 
265
        invalid=_('Please enter a zip code (%s)') % _('LnL nLn'))
 
266
 
 
267
    def _to_python(self, value, state):
 
268
        self.assert_string(value, state)
 
269
        match = self.regex.search(value)
 
270
        if not match:
 
271
            raise Invalid(
 
272
                self.message('invalid', state),
 
273
                value, state)
 
274
        return '%s %s' % (match.group(1).upper(), match.group(2).upper())
 
275
 
 
276
 
 
277
class UKPostalCode(Regex):
 
278
    """
 
279
    UK Postal codes. Please see BS 7666.
 
280
 
 
281
    ::
 
282
 
 
283
        >>> UKPostalCode.to_python('BFPO 3')
 
284
        'BFPO 3'
 
285
        >>> UKPostalCode.to_python('LE11 3GR')
 
286
        'LE11 3GR'
 
287
        >>> UKPostalCode.to_python('l1a 3gr')
 
288
        'L1A 3GR'
 
289
        >>> UKPostalCode.to_python('5555')
 
290
        Traceback (most recent call last):
 
291
            ...
 
292
        Invalid: Please enter a valid postal code (for format see BS 7666)
 
293
    """
 
294
 
 
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)
 
296
    strip = True
 
297
 
 
298
    messages = dict(
 
299
        invalid=_('Please enter a valid postal code (for format see BS 7666)'))
 
300
 
 
301
    def _to_python(self, value, state):
 
302
        self.assert_string(value, state)
 
303
        match = self.regex.search(value)
 
304
        if not match:
 
305
            raise Invalid(
 
306
                self.message('invalid', state),
 
307
                value, state)
 
308
        return match.group(1).upper()
 
309
 
 
310
 
 
311
class CountryValidator(FancyValidator):
 
312
    """
 
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
 
315
    reverse step.
 
316
 
 
317
    @See http://www.iso.org/iso/country_codes/iso_3166_code_lists.htm
 
318
 
 
319
    ::
 
320
 
 
321
        >>> CountryValidator.to_python('Germany')
 
322
        u'DE'
 
323
        >>> CountryValidator.to_python('Finland')
 
324
        u'FI'
 
325
        >>> CountryValidator.to_python('UNITED STATES')
 
326
        u'US'
 
327
        >>> CountryValidator.to_python('Krakovia')
 
328
        Traceback (most recent call last):
 
329
            ...
 
330
        Invalid: That country is not listed in ISO 3166
 
331
        >>> CountryValidator.from_python('DE')
 
332
        u'Germany'
 
333
        >>> CountryValidator.from_python('FI')
 
334
        u'Finland'
 
335
    """
 
336
 
 
337
    key_ok = True
 
338
 
 
339
    messages = dict(
 
340
        valueNotFound=_('That country is not listed in ISO 3166'))
 
341
 
 
342
    def __init__(self, *args, **kw):
 
343
        FancyValidator.__init__(self, *args, **kw)
 
344
        if no_country:
 
345
            warnings.warn(no_country, Warning, 2)
 
346
 
 
347
    def _to_python(self, value, state):
 
348
        upval = value.upper()
 
349
        if self.key_ok:
 
350
            try:
 
351
                c = get_country(upval)
 
352
                return upval
 
353
            except:
 
354
                pass
 
355
        for k, v in get_countries():
 
356
            if v.upper() == upval:
 
357
                return k
 
358
        raise Invalid(self.message('valueNotFound', state), value, state)
 
359
 
 
360
    def _from_python(self, value, state):
 
361
        try:
 
362
            return get_country(value.upper())
 
363
        except KeyError:
 
364
            return value
 
365
 
 
366
 
 
367
class PostalCodeInCountryFormat(FancyValidator):
 
368
    """
 
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
 
371
    format, too.
 
372
 
 
373
    ::
 
374
 
 
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):
 
380
            ...
 
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')]
 
387
    """
 
388
 
 
389
    country_field = 'country'
 
390
    zip_field = 'zip'
 
391
 
 
392
    __unpackargs__ = ('country_field', 'zip_field')
 
393
 
 
394
    messages = dict(
 
395
        badFormat=_("Given postal code does not match the country's format."))
 
396
 
 
397
    _vd = {
 
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),
 
412
        'GB': UKPostalCode,
 
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),
 
433
        'US': USPostalCode,
 
434
        'UY': lambda: DelimitedDigitsPostalCode(5),
 
435
    }
 
436
 
 
437
    def validate_python(self, fields_dict, state):
 
438
        if fields_dict[self.country_field] in self._vd:
 
439
            try:
 
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])
 
443
            except Invalid, e:
 
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})
 
448
 
 
449
 
 
450
class USStateProvince(FancyValidator):
 
451
    """
 
452
    Valid state or province code (two-letter).
 
453
 
 
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.
 
458
 
 
459
    ::
 
460
 
 
461
        >>> s = USStateProvince('XX')
 
462
        >>> s.to_python('IL')
 
463
        'IL'
 
464
        >>> s.to_python('XX')
 
465
        'XX'
 
466
        >>> s.to_python('xx')
 
467
        'XX'
 
468
        >>> s.to_python('YY')
 
469
        Traceback (most recent call last):
 
470
            ...
 
471
        Invalid: That is not a valid state code
 
472
    """
 
473
 
 
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']
 
480
 
 
481
    extra_states = []
 
482
 
 
483
    __unpackargs__ = ('extra_states',)
 
484
 
 
485
    messages = dict(
 
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'))
 
489
 
 
490
    def validate_python(self, value, state):
 
491
        value = str(value).strip().upper()
 
492
        if not value:
 
493
            raise Invalid(
 
494
                self.message('empty', state),
 
495
                value, state)
 
496
        if not value or len(value) != 2:
 
497
            raise Invalid(
 
498
                self.message('wrongLength', state),
 
499
                value, state)
 
500
        if value not in self.states and not (
 
501
                self.extra_states and value in self.extra_states):
 
502
            raise Invalid(
 
503
                self.message('invalid', state),
 
504
                value, state)
 
505
 
 
506
    def _to_python(self, value, state):
 
507
        return str(value).strip().upper()
 
508
 
 
509
 
 
510
############################################################
 
511
## phone number validators
 
512
############################################################
 
513
 
 
514
class USPhoneNumber(FancyValidator):
 
515
    """
 
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.
 
519
 
 
520
    ::
 
521
 
 
522
        >>> p = USPhoneNumber()
 
523
        >>> p.to_python('333-3333')
 
524
        Traceback (most recent call last):
 
525
            ...
 
526
        Invalid: Please enter a number, with area code, in the form ###-###-####, optionally with "ext.####"
 
527
        >>> p.to_python('555-555-5555')
 
528
        '555-555-5555'
 
529
        >>> p.to_python('1-393-555-3939')
 
530
        '1-393-555-3939'
 
531
        >>> p.to_python('321.555.4949')
 
532
        '321.555.4949'
 
533
        >>> p.to_python('3335550000')
 
534
        '3335550000'
 
535
    """
 
536
    # for emacs: "
 
537
 
 
538
    _phoneRE = re.compile(r'^\s*(?:1-)?(\d\d\d)[\- \.]?(\d\d\d)[\- \.]?(\d\d\d\d)(?:\s*ext\.?\s*(\d+))?\s*$', re.I)
 
539
 
 
540
    messages = dict(
 
541
        phoneFormat=_('Please enter a number, with area code,'
 
542
            ' in the form ###-###-####, optionally with "ext.####"'))
 
543
 
 
544
    def _to_python(self, value, state):
 
545
        self.assert_string(value, state)
 
546
        match = self._phoneRE.search(value)
 
547
        if not match:
 
548
            raise Invalid(
 
549
                self.message('phoneFormat', state),
 
550
                value, state)
 
551
        return value
 
552
 
 
553
    def _from_python(self, value, state):
 
554
        self.assert_string(value, state)
 
555
        match = self._phoneRE.search(value)
 
556
        if not match:
 
557
            raise Invalid(self.message('phoneFormat', state),
 
558
                          value, state)
 
559
        result = '%s-%s-%s' % (match.group(1), match.group(2), match.group(3))
 
560
        if match.group(4):
 
561
            result += " ext.%s" % match.group(4)
 
562
        return result
 
563
 
 
564
 
 
565
class InternationalPhoneNumber(FancyValidator):
 
566
    """
 
567
    Validates, and converts phone numbers to +##-###-#######.
 
568
    Adapted from RFC 3966
 
569
 
 
570
    @param  default_cc      country code for prepending if none is provided
 
571
                            can be a paramerless callable
 
572
 
 
573
    ::
 
574
 
 
575
        >>> c = InternationalPhoneNumber(default_cc=lambda: 49)
 
576
        >>> c.to_python('0555/8114100')
 
577
        '+49-555-8114100'
 
578
        >>> p = InternationalPhoneNumber(default_cc=49)
 
579
        >>> p.to_python('333-3333')
 
580
        Traceback (most recent call last):
 
581
            ...
 
582
        Invalid: Please enter a number, with area code, in the form +##-###-#######.
 
583
        >>> p.to_python('0555/4860-300')
 
584
        '+49-555-4860-300'
 
585
        >>> p.to_python('0555-49924-51')
 
586
        '+49-555-49924-51'
 
587
        >>> p.to_python('0555 / 8114100')
 
588
        '+49-555-8114100'
 
589
        >>> p.to_python('0555/8114100')
 
590
        '+49-555-8114100'
 
591
        >>> p.to_python('0555 8114100')
 
592
        '+49-555-8114100'
 
593
        >>> p.to_python(' +49 (0)555 350 60 0')
 
594
        '+49-555-35060-0'
 
595
        >>> p.to_python('+49 555 350600')
 
596
        '+49-555-350600'
 
597
        >>> p.to_python('0049/ 555/ 871 82 96')
 
598
        '+49-555-87182-96'
 
599
        >>> p.to_python('0555-2 50-30')
 
600
        '+49-555-250-30'
 
601
        >>> p.to_python('0555 43-1200')
 
602
        '+49-555-43-1200'
 
603
        >>> p.to_python('(05 55)4 94 33 47')
 
604
        '+49-555-49433-47'
 
605
        >>> p.to_python('(00 48-555)2 31 72 41')
 
606
        '+48-555-23172-41'
 
607
        >>> p.to_python('+973-555431')
 
608
        '+973-555431'
 
609
        >>> p.to_python('1-393-555-3939')
 
610
        '+1-393-555-3939'
 
611
        >>> p.to_python('+43 (1) 55528/0')
 
612
        '+43-1-55528-0'
 
613
        >>> p.to_python('+43 5555 429 62-0')
 
614
        '+43-5555-42962-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')
 
618
        '+218-55-3636639-38'
 
619
        >>> p.to_python('032 555555 367')
 
620
        '+49-32-555555-367'
 
621
        >>> p.to_python('(+86) 555 3876693')
 
622
        '+86-555-3876693'
 
623
    """
 
624
 
 
625
    strip = True
 
626
    # Use if there's a default country code you want to use:
 
627
    default_cc = None
 
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'),
 
637
        ]
 
638
    _ccIncluder = [
 
639
        (re.compile(r'^\(?0([1-9]\d*)[-)](\d.*)$'), '+%d-%s-%s'),
 
640
        ]
 
641
    _postTransformations = [
 
642
        (re.compile(r'^(\+\d+)[-\s]\(?(\d+)\)?[-\s](\d+.+)$'), '%s-%s-%s'),
 
643
        (re.compile(r'^(.+)\s(\d+)$'), '%s-%s'),
 
644
        ]
 
645
    _phoneIsSane = re.compile(r'^(\+[1-9]\d*)-([\d\-]+)$')
 
646
 
 
647
    messages = dict(
 
648
        phoneFormat=_('Please enter a number, with area code,'
 
649
            ' in the form +##-###-#######.'))
 
650
 
 
651
    def _perform_rex_transformation(self, value, transformations):
 
652
        for rex, trf in transformations:
 
653
            match = rex.search(value)
 
654
            if match:
 
655
                value = trf % match.groups()
 
656
        return value
 
657
 
 
658
    def _prepend_country_code(self, value, transformations, country_code):
 
659
        for rex, trf in transformations:
 
660
            match = rex.search(value)
 
661
            if match:
 
662
                return trf % ((country_code,)+match.groups())
 
663
        return value
 
664
 
 
665
    def _to_python(self, value, state):
 
666
        self.assert_string(value, state)
 
667
        try:
 
668
            value = value.encode('ascii', 'replace')
 
669
        except:
 
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)
 
676
        if self.default_cc:
 
677
            if callable(self.default_cc):
 
678
                cc = self.default_cc()
 
679
            else:
 
680
                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)
 
687
        return value
 
688
 
 
689
 
 
690
############################################################
 
691
## language validators
 
692
############################################################
 
693
 
 
694
class LanguageValidator(FancyValidator):
 
695
    """
 
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.
 
698
 
 
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
 
703
 
 
704
    @param  key_ok      accept the language's code instead of its name for input
 
705
                        defaults to True
 
706
 
 
707
    ::
 
708
 
 
709
        >>> l = LanguageValidator()
 
710
        >>> l.to_python('German')
 
711
        u'de'
 
712
        >>> l.to_python('Chinese')
 
713
        u'zh'
 
714
        >>> l.to_python('Klingonian')
 
715
        Traceback (most recent call last):
 
716
            ...
 
717
        Invalid: That language is not listed in ISO 639
 
718
        >>> l.from_python('de')
 
719
        u'German'
 
720
        >>> l.from_python('zh')
 
721
        u'Chinese'
 
722
    """
 
723
 
 
724
    key_ok = True
 
725
 
 
726
    messages = dict(
 
727
        valueNotFound=_('That language is not listed in ISO 639'))
 
728
 
 
729
    def __init__(self, *args, **kw):
 
730
        FancyValidator.__init__(self, *args, **kw)
 
731
        if no_country:
 
732
            warnings.warn(no_country, Warning, 2)
 
733
 
 
734
    def _to_python(self, value, state):
 
735
        upval = value.upper()
 
736
        if self.key_ok:
 
737
            try:
 
738
                c = get_language(value)
 
739
                return value
 
740
            except:
 
741
                pass
 
742
        for k, v in get_languages():
 
743
            if v.upper() == upval:
 
744
                return k
 
745
        raise Invalid(self.message('valueNotFound', state), value, state)
 
746
 
 
747
    def _from_python(self, value, state):
 
748
        try:
 
749
            return get_language(value.lower())
 
750
        except KeyError:
 
751
            return value