|
29
|
1 |
""" |
|
|
2 |
ID-specific Form helpers |
|
|
3 |
""" |
|
|
4 |
|
|
|
5 |
import re |
|
|
6 |
import time |
|
|
7 |
|
|
|
8 |
from django.core.validators import EMPTY_VALUES |
|
|
9 |
from django.forms import ValidationError |
|
|
10 |
from django.forms.fields import Field, Select |
|
|
11 |
from django.utils.translation import ugettext_lazy as _ |
|
|
12 |
from django.utils.encoding import smart_unicode |
|
|
13 |
|
|
|
14 |
postcode_re = re.compile(r'^[1-9]\d{4}$') |
|
|
15 |
phone_re = re.compile(r'^(\+62|0)[2-9]\d{7,10}$') |
|
|
16 |
plate_re = re.compile(r'^(?P<prefix>[A-Z]{1,2}) ' + \ |
|
|
17 |
r'(?P<number>\d{1,5})( (?P<suffix>([A-Z]{1,3}|[1-9][0-9]{,2})))?$') |
|
|
18 |
nik_re = re.compile(r'^\d{16}$') |
|
|
19 |
|
|
|
20 |
|
|
|
21 |
class IDPostCodeField(Field): |
|
|
22 |
""" |
|
|
23 |
An Indonesian post code field. |
|
|
24 |
|
|
|
25 |
http://id.wikipedia.org/wiki/Kode_pos |
|
|
26 |
""" |
|
|
27 |
default_error_messages = { |
|
|
28 |
'invalid': _('Enter a valid post code'), |
|
|
29 |
} |
|
|
30 |
|
|
|
31 |
def clean(self, value): |
|
|
32 |
super(IDPostCodeField, self).clean(value) |
|
|
33 |
if value in EMPTY_VALUES: |
|
|
34 |
return u'' |
|
|
35 |
|
|
|
36 |
value = value.strip() |
|
|
37 |
if not postcode_re.search(value): |
|
|
38 |
raise ValidationError(self.error_messages['invalid']) |
|
|
39 |
|
|
|
40 |
if int(value) < 10110: |
|
|
41 |
raise ValidationError(self.error_messages['invalid']) |
|
|
42 |
|
|
|
43 |
# 1xxx0 |
|
|
44 |
if value[0] == '1' and value[4] != '0': |
|
|
45 |
raise ValidationError(self.error_messages['invalid']) |
|
|
46 |
|
|
|
47 |
return u'%s' % (value, ) |
|
|
48 |
|
|
|
49 |
|
|
|
50 |
class IDProvinceSelect(Select): |
|
|
51 |
""" |
|
|
52 |
A Select widget that uses a list of provinces of Indonesia as its |
|
|
53 |
choices. |
|
|
54 |
""" |
|
|
55 |
|
|
|
56 |
def __init__(self, attrs=None): |
|
|
57 |
from id_choices import PROVINCE_CHOICES |
|
|
58 |
super(IDProvinceSelect, self).__init__(attrs, choices=PROVINCE_CHOICES) |
|
|
59 |
|
|
|
60 |
|
|
|
61 |
class IDPhoneNumberField(Field): |
|
|
62 |
""" |
|
|
63 |
An Indonesian telephone number field. |
|
|
64 |
|
|
|
65 |
http://id.wikipedia.org/wiki/Daftar_kode_telepon_di_Indonesia |
|
|
66 |
""" |
|
|
67 |
default_error_messages = { |
|
|
68 |
'invalid': _('Enter a valid phone number'), |
|
|
69 |
} |
|
|
70 |
|
|
|
71 |
def clean(self, value): |
|
|
72 |
super(IDPhoneNumberField, self).clean(value) |
|
|
73 |
if value in EMPTY_VALUES: |
|
|
74 |
return u'' |
|
|
75 |
|
|
|
76 |
phone_number = re.sub(r'[\-\s\(\)]', '', smart_unicode(value)) |
|
|
77 |
|
|
|
78 |
if phone_re.search(phone_number): |
|
|
79 |
return smart_unicode(value) |
|
|
80 |
|
|
|
81 |
raise ValidationError(self.error_messages['invalid']) |
|
|
82 |
|
|
|
83 |
|
|
|
84 |
class IDLicensePlatePrefixSelect(Select): |
|
|
85 |
""" |
|
|
86 |
A Select widget that uses a list of vehicle license plate prefix code |
|
|
87 |
of Indonesia as its choices. |
|
|
88 |
|
|
|
89 |
http://id.wikipedia.org/wiki/Tanda_Nomor_Kendaraan_Bermotor |
|
|
90 |
""" |
|
|
91 |
|
|
|
92 |
def __init__(self, attrs=None): |
|
|
93 |
from id_choices import LICENSE_PLATE_PREFIX_CHOICES |
|
|
94 |
super(IDLicensePlatePrefixSelect, self).__init__(attrs, |
|
|
95 |
choices=LICENSE_PLATE_PREFIX_CHOICES) |
|
|
96 |
|
|
|
97 |
|
|
|
98 |
class IDLicensePlateField(Field): |
|
|
99 |
""" |
|
|
100 |
An Indonesian vehicle license plate field. |
|
|
101 |
|
|
|
102 |
http://id.wikipedia.org/wiki/Tanda_Nomor_Kendaraan_Bermotor |
|
|
103 |
|
|
|
104 |
Plus: "B 12345 12" |
|
|
105 |
""" |
|
|
106 |
default_error_messages = { |
|
|
107 |
'invalid': _('Enter a valid vehicle license plate number'), |
|
|
108 |
} |
|
|
109 |
|
|
|
110 |
def clean(self, value): |
|
|
111 |
super(IDLicensePlateField, self).clean(value) |
|
|
112 |
if value in EMPTY_VALUES: |
|
|
113 |
return u'' |
|
|
114 |
|
|
|
115 |
plate_number = re.sub(r'\s+', ' ', |
|
|
116 |
smart_unicode(value.strip())).upper() |
|
|
117 |
|
|
|
118 |
matches = plate_re.search(plate_number) |
|
|
119 |
if matches is None: |
|
|
120 |
raise ValidationError(self.error_messages['invalid']) |
|
|
121 |
|
|
|
122 |
# Make sure prefix is in the list of known codes. |
|
|
123 |
from id_choices import LICENSE_PLATE_PREFIX_CHOICES |
|
|
124 |
prefix = matches.group('prefix') |
|
|
125 |
if prefix not in [choice[0] for choice in LICENSE_PLATE_PREFIX_CHOICES]: |
|
|
126 |
raise ValidationError(self.error_messages['invalid']) |
|
|
127 |
|
|
|
128 |
# Only Jakarta (prefix B) can have 3 letter suffix. |
|
|
129 |
suffix = matches.group('suffix') |
|
|
130 |
if suffix is not None and len(suffix) == 3 and prefix != 'B': |
|
|
131 |
raise ValidationError(self.error_messages['invalid']) |
|
|
132 |
|
|
|
133 |
# RI plates don't have suffix. |
|
|
134 |
if prefix == 'RI' and suffix is not None and suffix != '': |
|
|
135 |
raise ValidationError(self.error_messages['invalid']) |
|
|
136 |
|
|
|
137 |
# Number can't be zero. |
|
|
138 |
number = matches.group('number') |
|
|
139 |
if number == '0': |
|
|
140 |
raise ValidationError(self.error_messages['invalid']) |
|
|
141 |
|
|
|
142 |
# CD, CC and B 12345 12 |
|
|
143 |
if len(number) == 5 or prefix in ('CD', 'CC'): |
|
|
144 |
# suffix must be numeric and non-empty |
|
|
145 |
if re.match(r'^\d+$', suffix) is None: |
|
|
146 |
raise ValidationError(self.error_messages['invalid']) |
|
|
147 |
|
|
|
148 |
# Known codes range is 12-124 |
|
|
149 |
if prefix in ('CD', 'CC') and not (12 <= int(number) <= 124): |
|
|
150 |
raise ValidationError(self.error_messages['invalid']) |
|
|
151 |
if len(number) == 5 and not (12 <= int(suffix) <= 124): |
|
|
152 |
raise ValidationError(self.error_messages['invalid']) |
|
|
153 |
else: |
|
|
154 |
# suffix must be non-numeric |
|
|
155 |
if suffix is not None and re.match(r'^[A-Z]{,3}$', suffix) is None: |
|
|
156 |
raise ValidationError(self.error_messages['invalid']) |
|
|
157 |
|
|
|
158 |
return plate_number |
|
|
159 |
|
|
|
160 |
|
|
|
161 |
class IDNationalIdentityNumberField(Field): |
|
|
162 |
""" |
|
|
163 |
An Indonesian national identity number (NIK/KTP#) field. |
|
|
164 |
|
|
|
165 |
http://id.wikipedia.org/wiki/Nomor_Induk_Kependudukan |
|
|
166 |
|
|
|
167 |
xx.xxxx.ddmmyy.xxxx - 16 digits (excl. dots) |
|
|
168 |
""" |
|
|
169 |
default_error_messages = { |
|
|
170 |
'invalid': _('Enter a valid NIK/KTP number'), |
|
|
171 |
} |
|
|
172 |
|
|
|
173 |
def clean(self, value): |
|
|
174 |
super(IDNationalIdentityNumberField, self).clean(value) |
|
|
175 |
if value in EMPTY_VALUES: |
|
|
176 |
return u'' |
|
|
177 |
|
|
|
178 |
value = re.sub(r'[\s.]', '', smart_unicode(value)) |
|
|
179 |
|
|
|
180 |
if not nik_re.search(value): |
|
|
181 |
raise ValidationError(self.error_messages['invalid']) |
|
|
182 |
|
|
|
183 |
if int(value) == 0: |
|
|
184 |
raise ValidationError(self.error_messages['invalid']) |
|
|
185 |
|
|
|
186 |
def valid_nik_date(year, month, day): |
|
|
187 |
try: |
|
|
188 |
t1 = (int(year), int(month), int(day), 0, 0, 0, 0, 0, -1) |
|
|
189 |
d = time.mktime(t1) |
|
|
190 |
t2 = time.localtime(d) |
|
|
191 |
if t1[:3] != t2[:3]: |
|
|
192 |
return False |
|
|
193 |
else: |
|
|
194 |
return True |
|
|
195 |
except (OverflowError, ValueError): |
|
|
196 |
return False |
|
|
197 |
|
|
|
198 |
year = int(value[10:12]) |
|
|
199 |
month = int(value[8:10]) |
|
|
200 |
day = int(value[6:8]) |
|
|
201 |
current_year = time.localtime().tm_year |
|
|
202 |
if year < int(str(current_year)[-2:]): |
|
|
203 |
if not valid_nik_date(2000 + int(year), month, day): |
|
|
204 |
raise ValidationError(self.error_messages['invalid']) |
|
|
205 |
elif not valid_nik_date(1900 + int(year), month, day): |
|
|
206 |
raise ValidationError(self.error_messages['invalid']) |
|
|
207 |
|
|
|
208 |
if value[:6] == '000000' or value[12:] == '0000': |
|
|
209 |
raise ValidationError(self.error_messages['invalid']) |
|
|
210 |
|
|
|
211 |
return '%s.%s.%s.%s' % (value[:2], value[2:6], value[6:12], value[12:]) |