Skip to content

Commit 0fbadfd

Browse files
committed
Fixed #16497 -- Added new form and model fields to the Mexican local flavor. Many thanks to Andrés Torres Marroquín and Gerardo Orozco.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@16572 bcc190cf-cafb-0310-a4f2-bffc1f526a37
1 parent 4a6e1b5 commit 0fbadfd

File tree

16 files changed

+578
-5
lines changed

16 files changed

+578
-5
lines changed

AUTHORS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ answer newbie questions, and generally made Django that much better:
340340
Nuno Mariz <nmariz@gmail.com>
341341
mark@junklight.com
342342
Orestis Markou <orestis@orestis.gr>
343+
Andrés Torres Marroquín <andres.torres.marroquin@gmail.com>
343344
Takashi Matsuo <matsuo.takashi@gmail.com>
344345
Zlatko Mašek <zlatko.masek@gmail.com>
345346
Yasushi Masuda <whosaysni@gmail.com>
@@ -380,6 +381,7 @@ answer newbie questions, and generally made Django that much better:
380381
Neal Norwitz <nnorwitz@google.com>
381382
Todd O'Bryan <toddobryan@mac.com>
382383
Selwin Ong <selwin@ui.co.id>
384+
Gerardo Orozco <gerardo.orozco.mosqueda@gmail.com>
383385
Christian Oudard <christian.oudard@gmail.com>
384386
oggie rob <oz.robharvey@gmail.com>
385387
oggy <ognjen.maric@gmail.com>
Lines changed: 213 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,225 @@
1+
# -*- coding: utf-8 -*-
12
"""
23
Mexican-specific form helpers.
34
"""
5+
import re
46

5-
from django.forms.fields import Select
7+
from django.forms import ValidationError
8+
from django.forms.fields import Select, RegexField
9+
from django.utils.translation import ugettext_lazy as _
10+
from django.core.validators import EMPTY_VALUES
11+
from django.contrib.localflavor.mx.mx_states import STATE_CHOICES
12+
13+
DATE_RE = r'\d{2}((01|03|05|07|08|10|12)(0[1-9]|[12]\d|3[01])|02(0[1-9]|[12]\d)|(04|06|09|11)(0[1-9]|[12]\d|30))'
14+
15+
"""
16+
This is the list of inconvenient words according to the `Anexo IV` of the
17+
document described in the next link:
18+
http://www.sisi.org.mx/jspsi/documentos/2005/seguimiento/06101/0610100162005_065.doc
19+
"""
20+
21+
RFC_INCONVENIENT_WORDS = [
22+
u'BUEI', u'BUEY', u'CACA', u'CACO', u'CAGA', u'CAGO', u'CAKA', u'CAKO',
23+
u'COGE', u'COJA', u'COJE', u'COJI', u'COJO', u'CULO', u'FETO', u'GUEY',
24+
u'JOTO', u'KACA', u'KACO', u'KAGA', u'KAGO', u'KOGE', u'KOJO', u'KAKA',
25+
u'KULO', u'MAME', u'MAMO', u'MEAR', u'MEAS', u'MEON', u'MION', u'MOCO',
26+
u'MULA', u'PEDA', u'PEDO', u'PENE', u'PUTA', u'PUTO', u'QULO', u'RATA',
27+
u'RUIN',
28+
]
29+
30+
"""
31+
This is the list of inconvenient words according to the `Anexo 2` of the
32+
document described in the next link:
33+
http://portal.veracruz.gob.mx/pls/portal/url/ITEM/444112558A57C6E0E040A8C02E00695C
34+
"""
35+
CURP_INCONVENIENT_WORDS = [
36+
u'BACA', u'BAKA', u'BUEI', u'BUEY', u'CACA', u'CACO', u'CAGA', u'CAGO',
37+
u'CAKA', u'CAKO', u'COGE', u'COGI', u'COJA', u'COJE', u'COJI', u'COJO',
38+
u'COLA', u'CULO', u'FALO', u'FETO', u'GETA', u'GUEI', u'GUEY', u'JETA',
39+
u'JOTO', u'KACA', u'KACO', u'KAGA', u'KAGO', u'KAKA', u'KAKO', u'KOGE',
40+
u'KOGI', u'KOJA', u'KOJE', u'KOJI', u'KOJO', u'KOLA', u'KULO', u'LILO',
41+
u'LOCA', u'LOCO', u'LOKA', u'LOKO', u'MAME', u'MAMO', u'MEAR', u'MEAS',
42+
u'MEON', u'MIAR', u'MION', u'MOCO', u'MOKO', u'MULA', u'MULO', u'NACA',
43+
u'NACO', u'PEDA', u'PEDO', u'PENE', u'PIPI', u'PITO', u'POPO', u'PUTA',
44+
u'PUTO', u'QULO', u'RATA', u'ROBA', u'ROBE', u'ROBO', u'RUIN', u'SENO',
45+
u'TETA', u'VACA', u'VAGA', u'VAGO', u'VAKA', u'VUEI', u'VUEY', u'WUEI',
46+
u'WUEY',
47+
]
648

749
class MXStateSelect(Select):
850
"""
951
A Select widget that uses a list of Mexican states as its choices.
1052
"""
1153
def __init__(self, attrs=None):
12-
from mx_states import STATE_CHOICES
1354
super(MXStateSelect, self).__init__(attrs, choices=STATE_CHOICES)
1455

56+
57+
class MXZipCodeField(RegexField):
58+
"""
59+
A form field that accepts a Mexican Zip Code.
60+
61+
More info about this:
62+
http://en.wikipedia.org/wiki/List_of_postal_codes_in_Mexico
63+
"""
64+
default_error_messages = {
65+
'invalid': _(u'Enter a valid zip code in the format XXXXX.'),
66+
}
67+
68+
def __init__(self, *args, **kwargs):
69+
zip_code_re = ur'^(0[1-9]|[1][0-6]|[2-9]\d)(\d{3})$'
70+
super(MXZipCodeField, self).__init__(zip_code_re, *args, **kwargs)
71+
72+
73+
class MXRFCField(RegexField):
74+
"""
75+
A form field that validates a Mexican *Registro Federal de Contribuyentes*
76+
for either `Persona física` or `Persona moral`.
77+
78+
The Persona física RFC string is integrated by a juxtaposition of
79+
characters following the next pattern:
80+
81+
===== ====== ===========================================
82+
Index Format Accepted Characters
83+
===== ====== ===========================================
84+
1 X Any letter
85+
2 X Any vowel
86+
3-4 XX Any letter
87+
5-10 YYMMDD Any valid date
88+
11-12 XX Any letter or number between 0 and 9
89+
13 X Any digit between 0 and 9 or the letter *A*
90+
===== ====== ===========================================
91+
92+
The Persona moral RFC string is integrated by a juxtaposition of
93+
characters following the next pattern:
94+
95+
===== ====== ============================================
96+
Index Format Accepted Characters
97+
===== ====== ============================================
98+
1-3 XXX Any letter including *&* and *Ñ* chars
99+
4-9 YYMMDD Any valid date
100+
10-11 XX Any letter or number between 0 and 9
101+
12 X Any number between 0 and 9 or the letter *A*
102+
===== ====== ============================================
103+
104+
More info about this:
105+
http://es.wikipedia.org/wiki/Registro_Federal_de_Contribuyentes_(M%C3%A9xico)
106+
"""
107+
default_error_messages = {
108+
'invalid': _('Enter a valid RFC.'),
109+
'invalid_checksum': _('Invalid checksum for RFC.'),
110+
}
111+
112+
def __init__(self, min_length=9, max_length=13, *args, **kwargs):
113+
rfc_re = re.compile(ur'^([A-Z&Ññ]{3}|[A-Z][AEIOU][A-Z]{2})%s([A-Z0-9]{2}[0-9A])?$' % DATE_RE,
114+
re.IGNORECASE)
115+
super(MXRFCField, self).__init__(rfc_re, min_length=min_length,
116+
max_length=max_length, *args, **kwargs)
117+
118+
def clean(self, value):
119+
value = super(MXRFCField, self).clean(value)
120+
if value in EMPTY_VALUES:
121+
return u''
122+
value = value.upper()
123+
if self._has_homoclave(value):
124+
if not value[-1] == self._checksum(value[:-1]):
125+
raise ValidationError(self.default_error_messages['invalid_checksum'])
126+
if self._has_inconvenient_word(value):
127+
raise ValidationError(self.default_error_messages['invalid'])
128+
return value
129+
130+
def _has_homoclave(self, rfc):
131+
"""
132+
This check is done due to the existance of RFCs without a *homoclave*
133+
since the current algorithm to calculate it had not been created for
134+
the first RFCs ever in Mexico.
135+
"""
136+
rfc_without_homoclave_re = re.compile(ur'^[A-Z&Ññ]{3,4}%s$' % DATE_RE,
137+
re.IGNORECASE)
138+
return not rfc_without_homoclave_re.match(rfc)
139+
140+
def _checksum(self, rfc):
141+
"""
142+
More info about this procedure:
143+
www.sisi.org.mx/jspsi/documentos/2005/seguimiento/06101/0610100162005_065.doc
144+
"""
145+
chars = u'0123456789ABCDEFGHIJKLMN&OPQRSTUVWXYZ-Ñ'
146+
if len(rfc) is 11:
147+
rfc = '-' + rfc
148+
149+
sum_ = sum(i * chars.index(c) for i, c in zip(reversed(xrange(14)), rfc))
150+
checksum = 11 - sum_ % 11
151+
152+
if checksum == 10:
153+
return u'A'
154+
elif checksum == 11:
155+
return u'0'
156+
157+
return unicode(checksum)
158+
159+
def _has_inconvenient_word(self, rfc):
160+
first_four = rfc[:4]
161+
return first_four in RFC_INCONVENIENT_WORDS
162+
163+
164+
class MXCURPField(RegexField):
165+
"""
166+
A field that validates a Mexican Clave Única de Registro de Población.
167+
168+
The CURP is integrated by a juxtaposition of characters following the next
169+
pattern:
170+
171+
===== ====== ===================================================
172+
Index Format Accepted Characters
173+
===== ====== ===================================================
174+
1 X Any letter
175+
2 X Any vowel
176+
3-4 XX Any letter
177+
5-10 YYMMDD Any valid date
178+
11 X Either `H` or `M`, depending on the person's gender
179+
12-13 XX Any valid acronym for a state in Mexico
180+
14-16 XXX Any consonant
181+
17 X Any number between 0 and 9 or any letter
182+
18 X Any number between 0 and 9
183+
===== ====== ===================================================
184+
185+
More info about this:
186+
http://www.condusef.gob.mx/index.php/clave-unica-de-registro-de-poblacion-curp
187+
"""
188+
default_error_messages = {
189+
'invalid': _('Enter a valid CURP.'),
190+
'invalid_checksum': _(u'Invalid checksum for CURP.'),
191+
}
192+
193+
def __init__(self, min_length=18, max_length=18, *args, **kwargs):
194+
states_re = r'(AS|BC|BS|CC|CL|CM|CS|CH|DF|DG|GT|GR|HG|JC|MC|MN|MS|NT|NL|OC|PL|QT|QR|SP|SL|SR|TC|TS|TL|VZ|YN|ZS|NE)'
195+
consonants_re = r'[B-DF-HJ-NP-TV-Z]'
196+
curp_re = (ur'^[A-Z][AEIOU][A-Z]{2}%s[HM]%s%s{3}[0-9A-Z]\d$' %
197+
(DATE_RE, states_re, consonants_re))
198+
curp_re = re.compile(curp_re, re.IGNORECASE)
199+
super(MXCURPField, self).__init__(curp_re, min_length=min_length,
200+
max_length=max_length, *args, **kwargs)
201+
202+
def clean(self, value):
203+
value = super(MXCURPField, self).clean(value)
204+
if value in EMPTY_VALUES:
205+
return u''
206+
value = value.upper()
207+
if value[-1] != self._checksum(value[:-1]):
208+
raise ValidationError(self.default_error_messages['invalid_checksum'])
209+
if self._has_inconvenient_word(value):
210+
raise ValidationError(self.default_error_messages['invalid'])
211+
return value
212+
213+
def _checksum(self, value):
214+
chars = u'0123456789ABCDEFGHIJKLMN&OPQRSTUVWXYZ'
215+
216+
s = sum(i * chars.index(c) for i, c in zip(reversed(xrange(19)), value))
217+
checksum = 10 - s % 10
218+
219+
if checksum == 10:
220+
return u'0'
221+
return unicode(checksum)
222+
223+
def _has_inconvenient_word(self, curp):
224+
first_four = curp[:4]
225+
return first_four in CURP_INCONVENIENT_WORDS
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from django.utils.translation import ugettext_lazy as _
2+
from django.db.models.fields import CharField
3+
4+
from django.contrib.localflavor.mx.mx_states import STATE_CHOICES
5+
from django.contrib.localflavor.mx.forms import (MXRFCField as MXRFCFormField,
6+
MXZipCodeField as MXZipCodeFormField, MXCURPField as MXCURPFormField)
7+
8+
9+
class MXStateField(CharField):
10+
"""
11+
A model field that stores the three-letter Mexican state abbreviation in the
12+
database.
13+
"""
14+
description = _("Mexico state (three uppercase letters)")
15+
16+
def __init__(self, *args, **kwargs):
17+
kwargs['choices'] = STATE_CHOICES
18+
kwargs['max_length'] = 3
19+
super(MXStateField, self).__init__(*args, **kwargs)
20+
21+
22+
class MXZipCodeField(CharField):
23+
"""
24+
A model field that forms represent as a forms.MXZipCodeField field and
25+
stores the five-digit Mexican zip code.
26+
"""
27+
description = _("Mexico zip code")
28+
29+
def __init__(self, *args, **kwargs):
30+
kwargs['max_length'] = 5
31+
super(MXZipCodeField, self).__init__(*args, **kwargs)
32+
33+
def formfield(self, **kwargs):
34+
defaults = {'form_class': MXZipCodeFormField}
35+
defaults.update(kwargs)
36+
return super(MXZipCodeField, self).formfield(**defaults)
37+
38+
39+
class MXRFCField(CharField):
40+
"""
41+
A model field that forms represent as a forms.MXRFCField field and
42+
stores the value of a valid Mexican RFC.
43+
"""
44+
description = _("Mexican RFC")
45+
46+
def __init__(self, *args, **kwargs):
47+
kwargs['max_length'] = 13
48+
super(MXRFCField, self).__init__(*args, **kwargs)
49+
50+
def formfield(self, **kwargs):
51+
defaults = {'form_class': MXRFCFormField}
52+
defaults.update(kwargs)
53+
return super(MXRFCField, self).formfield(**defaults)
54+
55+
56+
class MXCURPField(CharField):
57+
"""
58+
A model field that forms represent as a forms.MXCURPField field and
59+
stores the value of a valid Mexican CURP.
60+
"""
61+
description = _("Mexican CURP")
62+
63+
def __init__(self, *args, **kwargs):
64+
kwargs['max_length'] = 18
65+
super(MXCURPField, self).__init__(*args, **kwargs)
66+
67+
def formfield(self, **kwargs):
68+
defaults = {'form_class': MXCURPFormField}
69+
defaults.update(kwargs)
70+
return super(MXCURPField, self).formfield(**defaults)

django/contrib/localflavor/mx/mx_states.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from django.utils.translation import ugettext_lazy as _
1010

11+
# All 31 states, plus the `Distrito Federal`.
1112
STATE_CHOICES = (
1213
('AGU', _(u'Aguascalientes')),
1314
('BCN', _(u'Baja California')),
@@ -42,4 +43,3 @@
4243
('YUC', _(u'Yucatán')),
4344
('ZAC', _(u'Zacatecas')),
4445
)
45-

0 commit comments

Comments
 (0)