|
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:]) |