Code

Ticket #4036: 4036.diff

File 4036.diff, 15.5 KB (added by oggie_rob, 7 years ago)

New patch with simplified Identity number field & error handling

Line 
1Index: django/contrib/localflavor/es/es_province.py
2===================================================================
3--- django/contrib/localflavor/es/es_province.py        (revision 0)
4+++ django/contrib/localflavor/es/es_province.py        (revision 0)
5@@ -0,0 +1,63 @@
6+# -*- coding: utf-8 -*-
7+"""
8+An alphabetical list of spanish provinces, including two autonomous
9+cities, for use as `choices` in a formfield.
10+
11+This exists in this standalone file so that it's only imported into
12+memory when explicitly needed.
13+"""
14+
15+PROVINCE_CHOICES = (
16+    ('VI', u'Álava'),
17+    ('AB', u'Albacete'),
18+    ('A', u'Alicante'),
19+    ('AL', u'Almería'),
20+    ('O', u'Asturias'),
21+    ('AV', u'Ávila'),
22+    ('BA', u'Badajoz'),
23+    ('PM', u'Islas Baleares'),
24+    ('B', u'Barcelona'),
25+    ('Bu', u'Burgos'),
26+    ('CC', u'Cáceres'),
27+    ('CA', u'Cádiz'),
28+    ('S', u'Cantabria'),
29+    ('CS', u'Castellón'),
30+    ('CE', u'Ceuta'),
31+    ('CR', u'Ciudad Real'),
32+    ('CO', u'Córdoba'),
33+    ('Cu', u'Cuenca'),
34+    ('GI', u'Gerona'),
35+    ('GR', u'Granada'),
36+    ('Gu', u'Guadalajara'),
37+    ('SS', u'Guipúzcoa'),
38+    ('H', u'Huelva'),
39+    ('Hu', u'Huesca'),
40+    ('J', u'Jaén'),
41+    ('C', u'La Coruña'),           
42+    ('LO', u'La Rioja'),
43+    ('GC', u'Las Palmas'),
44+    ('LE', u'León'),
45+    ('L', u'Lérida'),
46+    ('Lu', u'Lugo'),
47+    ('M', u'Madrid'),
48+    ('MA', u'Málaga'),
49+    ('ML', u'Melilla'),
50+    ('Mu', u'Murcia'),
51+    ('NA', u'Navarra'),
52+    ('OR', u'Orense'),
53+    ('P', u'Palencia'),
54+    ('PO', u'Pontevedra'),
55+    ('SA', u'Salamanca'),
56+    ('TF', u'Santa Cruz de Tenerife'),
57+    ('SG', u'Segovia'),
58+    ('SE', u'Sevilla'),
59+    ('SO', u'Soria'),
60+    ('T', u'Tarragona'),
61+    ('TE', u'Teruel'),
62+    ('TO', u'Toledo'),
63+    ('V', u'Valencia'),
64+    ('VA', u'Valladolid'),
65+    ('BI', u'Vizcaya'),
66+    ('ZA', u'Zamora'),
67+    ('Z', u'Zaragoza'),
68+)
69Index: django/contrib/localflavor/es/__init__.py
70===================================================================
71Index: django/contrib/localflavor/es/forms.py
72===================================================================
73--- django/contrib/localflavor/es/forms.py      (revision 0)
74+++ django/contrib/localflavor/es/forms.py      (revision 0)
75@@ -0,0 +1,123 @@
76+# -*- coding: utf-8 -*-
77+"""
78+Spanish-specific Form helpers
79+"""
80+
81+from django.newforms import ValidationError
82+from django.newforms.fields import RegexField, Select, EMPTY_VALUES
83+from django.utils.translation import ugettext as _
84+import re
85+
86+class ESPostalCodeField(RegexField):
87+    """
88+    A form field that validates its input is a spanish postal code.
89+   
90+    Spanish postal code is a five digits string, with two first digits
91+    between 01 and 52, assigned to provinces code.
92+    """
93+    def __init__(self, *args, **kwargs):
94+        super(ESPostalCodeField, self).__init__(r'^(0[1-9]|[1-4][0-9]|5[0-2])\d{3}$',
95+            max_length=None, min_length=None,
96+            error_message=_('Enter a valid postal code in the range and format 01XXX - 52XXX.'),
97+            *args, **kwargs)
98+
99+
100+class ESSubdivisionSelect(Select):
101+    """
102+    A Select widget that uses a list of spanish provinces and autonomous
103+    cities as its choices.
104+    """
105+    def __init__(self, attrs=None):
106+        from es_province import PROVINCE_CHOICES
107+        super(ESSubdivisionSelect, self).__init__(attrs, choices=PROVINCE_CHOICES)
108+
109+
110+class ESIdentityCardNumberField(RegexField):
111+    """
112+    A form field that validates its input as an Spanish NIF, NIE or CIF identity values
113+    Apart from validating the format, this also checks the checksum-like control character
114+   
115+    The NIF (Nœmero de identificaci—n fiscal):
116+        Assigned to Spanish citizens
117+         - 8 digits representing individual (known as the DNI)
118+         - 1 control character using modulus of DNI value against an alphabetical index
119+        e.g. '78699688J'
120+   
121+    The NIE (Nœmero de Identificaci—n de Extranjeros):
122+        Assigned to foreigners
123+         - "X" or "T"
124+         - 7 or 8 digits representing individual (if 7 digits, 0 must be prepended)
125+         - 1 control character using modulus of DNI value against an alphabetical index
126+        e.g. 'X3287690R'
127+       
128+    The CIF (C—digo de identificaci—n fiscal):
129+        Assigned to corporations
130+         - 1 character representing "type"
131+         - 2 digits representing the province (01 - 52)
132+         - 5 digits representing business registry number from the province
133+         - 1 control digit or character using calculation of 7 digits & type
134+        e.g. 'B38790911'
135+   
136+    It acepts two optional arguments:
137+    nif (default True)
138+        accept NIF or NIE values
139+    cif (default True)
140+        accept CIF values
141+    """   
142+    def __init__(self, nif=True, cif=True, *args, **kwargs):
143+        super(ESIdentityCardNumberField, self).__init__(r'^(\d{8}((-|\s|)[A-Za-z]{1}|)|([Xx](-|\s|)(\d{7}|\d{8})((-|\s|)[A-Za-z]{1}|))|([A-Sa-s]{1}(-|\s|)\d{7}(-|\s|)[A-Za-z0-9]{1}))$',
144+            max_length=None, min_length=None,
145+            error_message=_('Please enter a valid NIF, NIE or CIF.'),
146+            *args, **kwargs)
147+        assert nif or cif, _('"nif" or "cif" must be True')
148+        self.nif = nif
149+        self.cif = cif
150+
151+    def clean(self, value):
152+        super(ESIdentityCardNumberField, self).clean(value)       
153+        if value in EMPTY_VALUES:
154+            return u''
155+        value = unicode(value).replace('-','').replace(' ','').upper()
156+        ni_control_chars = 'TRWAGMYFPDXBNJZSQVHLCKE'
157+        ni_control_suffix = lambda x: ni_control_chars[long(x) % 23]
158+        cif_type = 'ABCDEFGHKLMNPQS'
159+       
160+        try:
161+            if re.match(r'^\d{8}[%s]$' % ni_control_chars, value):
162+                if not self.nif or value[-1] != ni_control_suffix(value[:8]):
163+                    raise # invalid NIF
164+            elif re.match(r'^(X|T)\d{7,8}[%s]$' % ni_control_chars, value):
165+                test_val = value[1:]
166+                if len(value) == 9: # 7 digit number only
167+                    test_val = '0' + test_val
168+                if not self.nif or test_val[-1] != ni_control_suffix(test_val[:8]):
169+                    raise # invalid NIE
170+            elif re.match(r'^[%s](0[1-9]|[1-4][0-9]|5[0-2])\d{5}[A-Z0-9]$' % cif_type, value):
171+                digits = value[1:8]
172+                # a = sum of odd digits
173+                # b = sum of digits of 2 * even digits
174+                # c = a + b
175+                # control = 10 - last digit of c
176+                a = sum([int(v) for k,v in enumerate(digits) if k%2]) # sum of odd values
177+                b_list = map(lambda x: 2*int(x), [v for k,v in enumerate(digits) if k%2 == 0])
178+                b = sum([sum(map(int, list(str(x)))) for x in b_list])
179+                c = a + b
180+                control = 10 - int(str(c)[-1])
181+                control_letters = 'JABCDEFGHI'
182+                # the first char determines whether to use the control value or the corresponding control_letter
183+                if (not self.cif) or \
184+                   (value[0] in 'KPQS' and value[-1] != control_letters[control]) or \
185+                   (value[0] in 'ABEH' and value[-1] != str(control)) or \
186+                   (value[0] not in 'ABEHKPQS' and value[-1] != str(control) and value[-1] != control_letters[control]):
187+                   raise # invalid CIF
188+            else:
189+                raise # not valid format
190+            return value
191+        except Exception, e:
192+            # produce appropriate error
193+            if not self.nif:
194+                raise ValidationError, _('Please enter a valid CIF.')
195+            elif not self.cif:
196+                raise ValidationError, _('Please enter a valid NIF or NIE.')
197+            else:
198+                raise ValidationError, _('Please enter a valid NIF, NIE or CIF.')
199Index: tests/regressiontests/forms/localflavor.py
200===================================================================
201--- tests/regressiontests/forms/localflavor.py  (revision 6194)
202+++ tests/regressiontests/forms/localflavor.py  (working copy)
203@@ -1818,4 +1818,198 @@
204 u''
205 >>> f.clean(u'')
206 u''
207+
208+# ESPostalCodeField #############################################################
209+
210+ESPostalCodeField validates that the data is a valid ES postal code.
211+>>> from django.contrib.localflavor.es.forms import *
212+>>> f = ESPostalCodeField()
213+>>> f.clean('01000')
214+u'01000'
215+>>> f.clean('52999')
216+u'52999'
217+>>> f.clean('00999')
218+Traceback (most recent call last):
219+...
220+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
221+>>> f.clean('53000')
222+Traceback (most recent call last):
223+...
224+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
225+>>> f.clean('0A200')
226+Traceback (most recent call last):
227+...
228+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
229+>>> f.clean('380001')
230+Traceback (most recent call last):
231+...
232+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
233+>>> f.clean(None)
234+Traceback (most recent call last):
235+...
236+ValidationError: [u'This field is required.']
237+>>> f.clean('')
238+Traceback (most recent call last):
239+...
240+ValidationError: [u'This field is required.']
241+
242+>>> f = ESPostalCodeField(required=False)
243+>>> f.clean('01000')
244+u'01000'
245+>>> f.clean('52999')
246+u'52999'
247+>>> f.clean('00999')
248+Traceback (most recent call last):
249+...
250+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
251+>>> f.clean('53000')
252+Traceback (most recent call last):
253+...
254+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
255+>>> f.clean('2A200')
256+Traceback (most recent call last):
257+...
258+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
259+>>> f.clean('380001')
260+Traceback (most recent call last):
261+...
262+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
263+>>> f.clean(None)
264+u''
265+>>> f.clean('')
266+u''
267+
268+# ESSubdivisionSelect ###############################################################
269+
270+ESSubdivisionSelect
271+>>> w = ESSubdivisionSelect()
272+>>> w.render('provinces', 'TF')
273+u'<select name="provinces">\n<option value="VI">\xc1lava</option>\n<option value="AB">Albacete</option>\n<option value="A">Alicante</option>\n<option value="AL">Almer\xeda</option>\n<option value="O">Asturias</option>\n<option value="AV">\xc1vila</option>\n<option value="BA">Badajoz</option>\n<option value="PM">Islas Baleares</option>\n<option value="B">Barcelona</option>\n<option value="Bu">Burgos</option>\n<option value="CC">C\xe1ceres</option>\n<option value="CA">C\xe1diz</option>\n<option value="S">Cantabria</option>\n<option value="CS">Castell\xf3n</option>\n<option value="CE">Ceuta</option>\n<option value="CR">Ciudad Real</option>\n<option value="CO">C\xf3rdoba</option>\n<option value="Cu">Cuenca</option>\n<option value="GI">Gerona</option>\n<option value="GR">Granada</option>\n<option value="Gu">Guadalajara</option>\n<option value="SS">Guip\xfazcoa</option>\n<option value="H">Huelva</option>\n<option value="Hu">Huesca</option>\n<option value="J">Ja\xe9n</option>\n<option value="C">La Coru\xf1a</option>\n<option value="LO">La Rioja</option>\n<option value="GC">Las Palmas</option>\n<option value="LE">Le\xf3n</option>\n<option value="L">L\xe9rida</option>\n<option value="Lu">Lugo</option>\n<option value="M">Madrid</option>\n<option value="MA">M\xe1laga</option>\n<option value="ML">Melilla</option>\n<option value="Mu">Murcia</option>\n<option value="NA">Navarra</option>\n<option value="OR">Orense</option>\n<option value="P">Palencia</option>\n<option value="PO">Pontevedra</option>\n<option value="SA">Salamanca</option>\n<option value="TF" selected="selected">Santa Cruz de Tenerife</option>\n<option value="SG">Segovia</option>\n<option value="SE">Sevilla</option>\n<option value="SO">Soria</option>\n<option value="T">Tarragona</option>\n<option value="TE">Teruel</option>\n<option value="TO">Toledo</option>\n<option value="V">Valencia</option>\n<option value="VA">Valladolid</option>\n<option value="BI">Vizcaya</option>\n<option value="ZA">Zamora</option>\n<option value="Z">Zaragoza</option>\n</select>'
274+
275+# ESIdentityCardNumberField #############################################################
276+
277+ESIdentityCardNumberField
278+>>> f = ESIdentityCardNumberField()
279+>>> f.clean('78699688J')
280+u'78699688J'
281+>>> f.clean('78699688-J')
282+u'78699688J'
283+>>> f.clean('78699688 J')
284+u'78699688J'
285+>>> f.clean('78699688 j')
286+u'78699688J'
287+>>> f.clean('78699688T')
288+Traceback (most recent call last):
289+...
290+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
291+>>> f.clean('X0901797J')
292+u'X0901797J'
293+>>> f.clean('X-6124387-Q')
294+u'X6124387Q'
295+>>> f.clean('X 0012953 G')
296+u'X0012953G'
297+>>> f.clean('x-3287690-r')
298+u'X3287690R'
299+>>> f.clean('X-03287690r')
300+u'X03287690R'
301+>>> f.clean('X-03287690')
302+Traceback (most recent call last):
303+...
304+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
305+>>> f.clean('X-03287690-T')
306+Traceback (most recent call last):
307+...
308+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
309+>>> f.clean('B38790911')
310+u'B38790911'
311+>>> f.clean('B-3879091A')
312+Traceback (most recent call last):
313+...
314+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
315+>>> f.clean('B 38790917')
316+Traceback (most recent call last):
317+...
318+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
319+>>> f.clean('P-3900800-H')
320+u'P3900800H'
321+>>> f.clean('P 39008008')
322+Traceback (most recent call last):
323+...
324+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
325+>>> f.clean('C-28795565')
326+u'C28795565'
327+>>> f.clean('C 2879556E')
328+u'C2879556E'
329+>>> f.clean('C28795567')
330+Traceback (most recent call last):
331+...
332+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
333+>>> f.clean('I38790911')
334+Traceback (most recent call last):
335+...
336+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
337+>>> f.clean('78699688-2')
338+Traceback (most recent call last):
339+...
340+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
341+>>> f.clean('999999999')
342+Traceback (most recent call last):
343+...
344+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
345+>>> f.clean(None)
346+Traceback (most recent call last):
347+...
348+ValidationError: [u'This field is required.']
349+>>> f.clean('')
350+Traceback (most recent call last):
351+...
352+ValidationError: [u'This field is required.']
353+>>> f = ESIdentityCardNumberField(required=False)
354+>>> f.clean(None)
355+u''
356+>>> f.clean('')
357+u''
358+>>> f = ESIdentityCardNumberField()
359+>>> f.clean('78699688J')
360+u'78699688J'
361+>>> f.clean('78699688T')
362+Traceback (most recent call last):
363+...
364+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
365+>>> f.clean('X-03287690-T')
366+Traceback (most recent call last):
367+...
368+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
369+>>> f.clean('B-3879091A')
370+Traceback (most recent call last):
371+...
372+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
373+>>> f.clean('P 39008008')
374+Traceback (most recent call last):
375+...
376+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
377+>>> f.clean('C28795567')
378+Traceback (most recent call last):
379+...
380+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
381+>>> f = ESIdentityCardNumberField(cif=False)
382+>>> f.clean('78699688J')
383+u'78699688J'
384+>>> f.clean('X-6124387-Q')
385+u'X6124387Q'
386+>>> f.clean('B38790911')
387+Traceback (most recent call last):
388+...
389+ValidationError: [u'Please enter a valid NIF or NIE.']
390+>>> f = ESIdentityCardNumberField(nif=False)
391+>>> f.clean('78699688J')
392+Traceback (most recent call last):
393+...
394+ValidationError: [u'Please enter a valid CIF.']
395+>>> f.clean('X-6124387-Q')
396+Traceback (most recent call last):
397+...
398+ValidationError: [u'Please enter a valid CIF.']
399+>>> f.clean('B38790911')
400+u'B38790911'
401 """