diff --git a/AUTHORS b/AUTHORS index e3aec98d94ccf..fc5e5d41a8dc1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -43,6 +43,7 @@ answer newbie questions, and generally made Django that much better: ajs alang@bright-green.com A S Alam + Francisco Albarran Cristobal Andi Albrecht Marty Alchin Ahmad Alhashemi diff --git a/django/contrib/localflavor/mx/forms.py b/django/contrib/localflavor/mx/forms.py index deecb4ea41ad6..456d4602e265a 100644 --- a/django/contrib/localflavor/mx/forms.py +++ b/django/contrib/localflavor/mx/forms.py @@ -17,7 +17,6 @@ document described in the next link: http://www.sisi.org.mx/jspsi/documentos/2005/seguimiento/06101/0610100162005_065.doc """ - RFC_INCONVENIENT_WORDS = [ u'BUEI', u'BUEY', u'CACA', u'CACO', u'CAGA', u'CAGO', u'CAKA', u'CAKO', u'COGE', u'COJA', u'COJE', u'COJI', u'COJO', u'CULO', u'FETO', u'GUEY', @@ -46,6 +45,7 @@ u'WUEY', ] + class MXStateSelect(Select): """ A Select widget that uses a list of Mexican states as its choices. @@ -105,8 +105,8 @@ class MXRFCField(RegexField): http://es.wikipedia.org/wiki/Registro_Federal_de_Contribuyentes_(M%C3%A9xico) """ default_error_messages = { - 'invalid': _('Enter a valid RFC.'), - 'invalid_checksum': _('Invalid checksum for RFC.'), + 'invalid': _(u'Enter a valid RFC.'), + 'invalid_checksum': _(u'Invalid checksum for RFC.'), } def __init__(self, min_length=9, max_length=13, *args, **kwargs): @@ -186,7 +186,7 @@ class MXCURPField(RegexField): http://www.condusef.gob.mx/index.php/clave-unica-de-registro-de-poblacion-curp """ default_error_messages = { - 'invalid': _('Enter a valid CURP.'), + 'invalid': _(u'Enter a valid CURP.'), 'invalid_checksum': _(u'Invalid checksum for CURP.'), } @@ -223,3 +223,53 @@ def _checksum(self, value): def _has_inconvenient_word(self, curp): first_four = curp[:4] return first_four in CURP_INCONVENIENT_WORDS + + +class MXSocialSecurityNumberField(RegexField): + """ + A field that validates a Mexican Social Security Number. + + The Social Security Number is integrated by a juxtaposition of digits + following the next pattern: + + ===== ======================================= + Index Required numbers + ===== ======================================= + 1-2 The number of the branch office where the Social Security Number + was designated. + 3-4 The year of inscription to the Social Security. + 5-6 The year of birth of the Social Security Number owner. + 7-10 The progressive number of procedure for the IMSS. + (This digit is provided exclusively by the Institute as it regards + the Folio number of such procedure). + 11 The verification digit. + """ + default_error_messages = { + 'invalid': _(u'Enter a valid Social Security Number.'), + 'invalid_checksum': _(u'Invalid checksum for Social Security Number.'), + } + + def __init__(self, min_length=11, max_length=11, *args, **kwargs): + ssn_re = ur'^\d{11}$' + ssn_re = re.compile(ssn_re) + super(MXSocialSecurityNumberField, self).__init__(ssn_re, + min_length=min_length, max_length=max_length, *args, **kwargs) + + def clean(self, value): + value = super(MXSocialSecurityNumberField, self).clean(value) + if value in EMPTY_VALUES: + return u'' + if value[-1] != self.__checksum(value[:-1]): + raise ValidationError(self.default_error_messages['invalid_checksum']) + return value + + def __checksum(self, value): + multipliers = [1 if i % 2 == 0 else 2 for i in xrange(10)] + + s = [int(v) * m for v, m in zip(value, multipliers)] + s = sum(map(int, ''.join(map(str, s)))) + checksum = 10 - s % 10 + + if checksum == 10: + return u'0' + return unicode(checksum) diff --git a/django/contrib/localflavor/mx/models.py b/django/contrib/localflavor/mx/models.py index 3ef8d5fb03a26..7700f95add733 100644 --- a/django/contrib/localflavor/mx/models.py +++ b/django/contrib/localflavor/mx/models.py @@ -3,7 +3,8 @@ from django.contrib.localflavor.mx.mx_states import STATE_CHOICES from django.contrib.localflavor.mx.forms import (MXRFCField as MXRFCFormField, - MXZipCodeField as MXZipCodeFormField, MXCURPField as MXCURPFormField) + MXZipCodeField as MXZipCodeFormField, MXCURPField as MXCURPFormField, + MXSocialSecurityNumberField as MXSocialSecurityNumberFormField) class MXStateField(CharField): @@ -67,4 +68,21 @@ def __init__(self, *args, **kwargs): def formfield(self, **kwargs): defaults = {'form_class': MXCURPFormField} defaults.update(kwargs) - return super(MXCURPField, self).formfield(**defaults) \ No newline at end of file + return super(MXCURPField, self).formfield(**defaults) + + +class MXSocialSecurityNumberField(CharField): + """ + A model field that forms represent as a forms.MXSocialSecurityNumberField + field and stores the value of a valid Mexican Social Security Number. + """ + description = _("Mexican Social Security Number") + + def __init__(self, *args, **kwargs): + kwargs['max_length'] = 11 + super(MXSocialSecurityNumberField, self).__init__(*args, **kwargs) + + def formfield(self, **kwargs): + defaults = {'form_class': MXSocialSecurityNumberFormField} + defaults.update(kwargs) + return super(MXSocialSecurityNumberField, self).formfield(**defaults) diff --git a/docs/ref/contrib/localflavor.txt b/docs/ref/contrib/localflavor.txt index 61c8c7ae47bff..8435e3bdab17f 100644 --- a/docs/ref/contrib/localflavor.txt +++ b/docs/ref/contrib/localflavor.txt @@ -842,6 +842,11 @@ Mexico (``mx``) .. _curp: http://www.condusef.gob.mx/index.php/clave-unica-de-registro-de-poblacion-curp +.. class:: mx.forms.MXSocialSecurityNumberField + + A field that validates a Mexican Social Security Number(NSS) for *Instituto Mexicano del Seguro Social*. + + .. class:: mx.forms.MXStateSelect A ``Select`` widget that uses a list of Mexican states as its choices. @@ -874,6 +879,11 @@ Mexico (``mx``) A model field that forms represent as a ``forms.MXCURPField`` field and stores the value of a valid Mexican CURP. +.. class:: mx.models.MXSocialSecurityNumberField + + A model field that forms represent as a ``forms.MXSocialSecurityNumberField`` field and + stores the eleven-digit Mexican Social Security Number for *Instituto Mexicano del Seguro Social*. + Additionally, a choice tuple is provided in ``django.contrib.localflavor.mx.mx_states``, allowing customized model and form fields, and form presentations, for subsets of Mexican states abbreviations: diff --git a/tests/regressiontests/localflavor/mx/models.py b/tests/regressiontests/localflavor/mx/models.py index 4905836d3d93c..55ed205471cd0 100644 --- a/tests/regressiontests/localflavor/mx/models.py +++ b/tests/regressiontests/localflavor/mx/models.py @@ -1,5 +1,6 @@ from django.contrib.localflavor.mx.models import ( - MXStateField, MXRFCField, MXCURPField, MXZipCodeField) + MXStateField, MXRFCField, MXCURPField, MXZipCodeField, + MXSocialSecurityNumberField) from django.db import models @@ -8,6 +9,7 @@ class MXPersonProfile(models.Model): rfc = MXRFCField() curp = MXCURPField() zip_code = MXZipCodeField() + ssn = MXSocialSecurityNumberField() class Meta: app_label = 'localflavor' diff --git a/tests/regressiontests/localflavor/mx/tests.py b/tests/regressiontests/localflavor/mx/tests.py index 1657596223fe7..1756215d2455c 100644 --- a/tests/regressiontests/localflavor/mx/tests.py +++ b/tests/regressiontests/localflavor/mx/tests.py @@ -2,7 +2,7 @@ from __future__ import absolute_import from django.contrib.localflavor.mx.forms import (MXZipCodeField, MXRFCField, - MXStateSelect, MXCURPField) + MXStateSelect, MXCURPField, MXSocialSecurityNumberField) from django.test import SimpleTestCase from .forms import MXPersonProfileForm @@ -16,6 +16,7 @@ def setUp(self): 'rfc': 'toma880125kv3', 'curp': 'toma880125hmnrrn02', 'zip_code': '58120', + 'ssn': '53987417457', }) def test_get_display_methods(self): @@ -30,12 +31,14 @@ def test_errors(self): 'rfc': 'invalid rfc', 'curp': 'invalid curp', 'zip_code': 'xxx', + 'ssn': 'invalid ssn', }) self.assertFalse(form.is_valid()) self.assertEqual(form.errors['state'], [u'Select a valid choice. Invalid state is not one of the available choices.']) self.assertEqual(form.errors['rfc'], [u'Enter a valid RFC.']) self.assertEqual(form.errors['curp'], [u'Ensure this value has at least 18 characters (it has 12).', u'Enter a valid CURP.']) self.assertEqual(form.errors['zip_code'], [u'Enter a valid zip code in the format XXXXX.']) + self.assertEqual(form.errors['ssn'], [u'Enter a valid Social Security Number.']) def test_field_blank_option(self): """Test that the empty option is there.""" @@ -196,3 +199,46 @@ def test_MXCURPField(self): 'OOMG890727HMNRSR07': error_checksum, } self.assertFieldOutput(MXCURPField, valid, invalid) + + def test_MXSocialSecurityNumberField(self): + error_format = [u'Enter a valid Social Security Number.'] + error_checksum = [u'Invalid checksum for Social Security Number.'] + valid = { + '53987417457': u'53987417457', + '53916912966': u'53916912966', + '53986504172': u'53986504172', + '17300426925': u'17300426925', + '53067407212': u'53067407212', + '53018000538': u'53018000538', + '10836311612': u'10836311612', + '37007910666': u'37007910666', + '53055700974': u'53055700974', + '17303364941': u'17303364941', + '53078528469': u'53078528469', + } + invalid = { + # Invalid format + '5398741A457': error_format, + '53487G12031': error_format, + '530P8028702': error_format, + '173004K6925': error_format, + '5306T407212': error_format, + '53018N00538': error_format, + 'E0836311612': error_format, + '3700U910666': error_format, + '530557 0974': error_format, + '173033?4941': error_format, + '53#88417917': error_format, + # Incorrect checksum + '53987417451': error_checksum, + '53018522942': error_checksum, + '53897239693': error_checksum, + '01704423244': error_checksum, + '53855919735': error_checksum, + '53926201296': error_checksum, + '53017919037': error_checksum, + '53884201248': error_checksum, + '42805762629': error_checksum, + '53563800130': error_checksum, + } + self.assertFieldOutput(MXSocialSecurityNumberField, valid, invalid)