| 1 |
""" |
|---|
| 2 |
Swiss-specific Form helpers |
|---|
| 3 |
""" |
|---|
| 4 |
|
|---|
| 5 |
from django.forms import ValidationError |
|---|
| 6 |
from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES |
|---|
| 7 |
from django.utils.encoding import smart_unicode |
|---|
| 8 |
from django.utils.translation import ugettext_lazy as _ |
|---|
| 9 |
import re |
|---|
| 10 |
|
|---|
| 11 |
id_re = re.compile(r"^(?P<idnumber>\w{8})(?P<pos9>(\d{1}|<))(?P<checksum>\d{1})$") |
|---|
| 12 |
phone_digits_re = re.compile(r'^0([1-9]{1})\d{8}$') |
|---|
| 13 |
|
|---|
| 14 |
class CHZipCodeField(RegexField): |
|---|
| 15 |
default_error_messages = { |
|---|
| 16 |
'invalid': _('Enter a zip code in the format XXXX.'), |
|---|
| 17 |
} |
|---|
| 18 |
|
|---|
| 19 |
def __init__(self, *args, **kwargs): |
|---|
| 20 |
super(CHZipCodeField, self).__init__(r'^\d{4}$', |
|---|
| 21 |
max_length=None, min_length=None, *args, **kwargs) |
|---|
| 22 |
|
|---|
| 23 |
class CHPhoneNumberField(Field): |
|---|
| 24 |
""" |
|---|
| 25 |
Validate local Swiss phone number (not international ones) |
|---|
| 26 |
The correct format is '0XX XXX XX XX'. |
|---|
| 27 |
'0XX.XXX.XX.XX' and '0XXXXXXXXX' validate but are corrected to |
|---|
| 28 |
'0XX XXX XX XX'. |
|---|
| 29 |
""" |
|---|
| 30 |
default_error_messages = { |
|---|
| 31 |
'invalid': 'Phone numbers must be in 0XX XXX XX XX format.', |
|---|
| 32 |
} |
|---|
| 33 |
|
|---|
| 34 |
def clean(self, value): |
|---|
| 35 |
super(CHPhoneNumberField, self).clean(value) |
|---|
| 36 |
if value in EMPTY_VALUES: |
|---|
| 37 |
return u'' |
|---|
| 38 |
value = re.sub('(\.|\s|/|-)', '', smart_unicode(value)) |
|---|
| 39 |
m = phone_digits_re.search(value) |
|---|
| 40 |
if m: |
|---|
| 41 |
return u'%s %s %s %s' % (value[0:3], value[3:6], value[6:8], value[8:10]) |
|---|
| 42 |
raise ValidationError(self.error_messages['invalid']) |
|---|
| 43 |
|
|---|
| 44 |
class CHStateSelect(Select): |
|---|
| 45 |
""" |
|---|
| 46 |
A Select widget that uses a list of CH states as its choices. |
|---|
| 47 |
""" |
|---|
| 48 |
def __init__(self, attrs=None): |
|---|
| 49 |
from ch_states import STATE_CHOICES # relative import |
|---|
| 50 |
super(CHStateSelect, self).__init__(attrs, choices=STATE_CHOICES) |
|---|
| 51 |
|
|---|
| 52 |
class CHIdentityCardNumberField(Field): |
|---|
| 53 |
""" |
|---|
| 54 |
A Swiss identity card number. |
|---|
| 55 |
|
|---|
| 56 |
Checks the following rules to determine whether the number is valid: |
|---|
| 57 |
|
|---|
| 58 |
* Conforms to the X1234567<0 or 1234567890 format. |
|---|
| 59 |
* Included checksums match calculated checksums |
|---|
| 60 |
|
|---|
| 61 |
Algorithm is documented at http://adi.kousz.ch/artikel/IDCHE.htm |
|---|
| 62 |
""" |
|---|
| 63 |
default_error_messages = { |
|---|
| 64 |
'invalid': _('Enter a valid Swiss identity or passport card number in X1234567<0 or 1234567890 format.'), |
|---|
| 65 |
} |
|---|
| 66 |
|
|---|
| 67 |
def has_valid_checksum(self, number): |
|---|
| 68 |
given_number, given_checksum = number[:-1], number[-1] |
|---|
| 69 |
new_number = given_number |
|---|
| 70 |
calculated_checksum = 0 |
|---|
| 71 |
fragment = "" |
|---|
| 72 |
parameter = 7 |
|---|
| 73 |
|
|---|
| 74 |
first = str(number[:1]) |
|---|
| 75 |
if first.isalpha(): |
|---|
| 76 |
num = ord(first.upper()) - 65 |
|---|
| 77 |
if num < 0 or num > 8: |
|---|
| 78 |
return False |
|---|
| 79 |
new_number = str(num) + new_number[1:] |
|---|
| 80 |
new_number = new_number[:8] + '0' |
|---|
| 81 |
|
|---|
| 82 |
if not new_number.isdigit(): |
|---|
| 83 |
return False |
|---|
| 84 |
|
|---|
| 85 |
for i in range(len(new_number)): |
|---|
| 86 |
fragment = int(new_number[i])*parameter |
|---|
| 87 |
calculated_checksum += fragment |
|---|
| 88 |
|
|---|
| 89 |
if parameter == 1: |
|---|
| 90 |
parameter = 7 |
|---|
| 91 |
elif parameter == 3: |
|---|
| 92 |
parameter = 1 |
|---|
| 93 |
elif parameter ==7: |
|---|
| 94 |
parameter = 3 |
|---|
| 95 |
|
|---|
| 96 |
return str(calculated_checksum)[-1] == given_checksum |
|---|
| 97 |
|
|---|
| 98 |
def clean(self, value): |
|---|
| 99 |
super(CHIdentityCardNumberField, self).clean(value) |
|---|
| 100 |
if value in EMPTY_VALUES: |
|---|
| 101 |
return u'' |
|---|
| 102 |
|
|---|
| 103 |
match = re.match(id_re, value) |
|---|
| 104 |
if not match: |
|---|
| 105 |
raise ValidationError(self.error_messages['invalid']) |
|---|
| 106 |
|
|---|
| 107 |
idnumber, pos9, checksum = match.groupdict()['idnumber'], match.groupdict()['pos9'], match.groupdict()['checksum'] |
|---|
| 108 |
|
|---|
| 109 |
if idnumber == '00000000' or \ |
|---|
| 110 |
idnumber == 'A0000000': |
|---|
| 111 |
raise ValidationError(self.error_messages['invalid']) |
|---|
| 112 |
|
|---|
| 113 |
all_digits = "%s%s%s" % (idnumber, pos9, checksum) |
|---|
| 114 |
if not self.has_valid_checksum(all_digits): |
|---|
| 115 |
raise ValidationError(self.error_messages['invalid']) |
|---|
| 116 |
|
|---|
| 117 |
return u'%s%s%s' % (idnumber, pos9, checksum) |
|---|