diff --git a/django/contrib/localflavor/mk/__init__.py b/django/contrib/localflavor/mk/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/django/contrib/localflavor/mk/forms.py b/django/contrib/localflavor/mk/forms.py
new file mode 100644
index 0000000000..0548de3a40
--- /dev/null
+++ b/django/contrib/localflavor/mk/forms.py
@@ -0,0 +1,100 @@
+import datetime
+
+from django.core.validators import EMPTY_VALUES
+from django.forms import ValidationError
+from django.forms.fields import RegexField, Select
+from django.utils.translation import ugettext_lazy as _
+
+from mk_choices import MK_MUNICIPALITIES
+
+
+class MKIdentityCardNumberField(RegexField):
+ """
+ A Macedonian ID card number. Accepts both old and new format.
+ """
+ default_error_messages = {
+ 'invalid': _(u'Identity card numbers must contain'
+ ' either 4 to 7 digits or an uppercase letter and 7 digits.'),
+ }
+
+ def __init__(self, *args, **kwargs):
+ kwargs['min_length'] = None
+ kwargs['max_length'] = 8
+ regex = ur'(^[A-Z]{1}\d{7}$)|(^\d{4,7}$)'
+ super(MKIdentityCardNumberField, self).__init__(regex, *args, **kwargs)
+
+
+class MKMunicipalitySelect(Select):
+ """
+ A form ``Select`` widget that uses a list of Macedonian municipalities as
+ choices. The label is the name of the municipality and the value
+ is a 2 character code for the municipality.
+ """
+
+ def __init__(self, attrs=None):
+ super(MKMunicipalitySelect, self).__init__(attrs, choices = MK_MUNICIPALITIES)
+
+
+class UMCNField(RegexField):
+ """
+ A form field that validates input as a unique master citizen
+ number.
+
+ The format of the unique master citizen number has been kept the same from
+ Yugoslavia. It is still in use in other countries as well, it is not applicable
+ solely in Macedonia. For more information see:
+ https://secure.wikimedia.org/wikipedia/en/wiki/Unique_Master_Citizen_Number
+
+ A value will pass validation if it complies to the following rules:
+
+ * Consists of exactly 13 digits
+ * The first 7 digits represent a valid past date in the format DDMMYYY
+ * The last digit of the UMCN passes a checksum test
+ """
+ default_error_messages = {
+ 'invalid': _(u'This field should contain exactly 13 digits.'),
+ 'date': _(u'The first 7 digits of the UMCN must represent a valid past date.'),
+ 'checksum': _(u'The UMCN is not valid.'),
+ }
+
+ def __init__(self, *args, **kwargs):
+ kwargs['min_length'] = None
+ kwargs['max_length'] = 13
+ super(UMCNField, self).__init__(r'^\d{13}$', *args, **kwargs)
+
+ def clean(self, value):
+ value = super(UMCNField, self).clean(value)
+
+ if value in EMPTY_VALUES:
+ return u''
+
+ if not self._validate_date_part(value):
+ raise ValidationError(self.error_messages['date'])
+ if self._validate_checksum(value):
+ return value
+ else:
+ raise ValidationError(self.error_messages['checksum'])
+
+ def _validate_checksum(self, value):
+ a,b,c,d,e,f,g,h,i,j,k,l,K = [int(digit) for digit in value]
+ m = 11 - (( 7*(a+g) + 6*(b+h) + 5*(c+i) + 4*(d+j) + 3*(e+k) + 2*(f+l)) % 11)
+ if (m >= 1 and m <= 9) and K == m:
+ return True
+ elif m == 11 and K == 0:
+ return True
+ else:
+ return False
+
+ def _validate_date_part(self, value):
+ daypart, monthpart, yearpart = int(value[:2]), int(value[2:4]), int(value[4:7])
+ if yearpart >= 800:
+ yearpart += 1000
+ else:
+ yearpart += 2000
+ try:
+ date = datetime.datetime(year = yearpart, month = monthpart, day = daypart).date()
+ except ValueError:
+ return False
+ if date >= datetime.datetime.now().date():
+ return False
+ return True
diff --git a/django/contrib/localflavor/mk/mk_choices.py b/django/contrib/localflavor/mk/mk_choices.py
new file mode 100644
index 0000000000..d6d1efa049
--- /dev/null
+++ b/django/contrib/localflavor/mk/mk_choices.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+"""
+Macedonian municipalities per the reorganization from 2004.
+"""
+from django.utils.translation import ugettext_lazy as _
+
+MK_MUNICIPALITIES = (
+ ('AD', _(u'Aerodrom')),
+ ('AR', _(u'Aračinovo')),
+ ('BR', _(u'Berovo')),
+ ('TL', _(u'Bitola')),
+ ('BG', _(u'Bogdanci')),
+ ('VJ', _(u'Bogovinje')),
+ ('BS', _(u'Bosilovo')),
+ ('BN', _(u'Brvenica')),
+ ('BU', _(u'Butel')),
+ ('VA', _(u'Valandovo')),
+ ('VL', _(u'Vasilevo')),
+ ('VV', _(u'Vevčani')),
+ ('VE', _(u'Veles')),
+ ('NI', _(u'Vinica')),
+ ('VC', _(u'Vraneštica')),
+ ('VH', _(u'Vrapčište')),
+ ('GB', _(u'Gazi Baba')),
+ ('GV', _(u'Gevgelija')),
+ ('GT', _(u'Gostivar')),
+ ('GR', _(u'Gradsko')),
+ ('DB', _(u'Debar')),
+ ('DA', _(u'Debarca')),
+ ('DL', _(u'Delčevo')),
+ ('DK', _(u'Demir Kapija')),
+ ('DM', _(u'Demir Hisar')),
+ ('DE', _(u'Dolneni')),
+ ('DR', _(u'Drugovo')),
+ ('GP', _(u'Gjorče Petrov')),
+ ('ZE', _(u'Želino')),
+ ('ZA', _(u'Zajas')),
+ ('ZK', _(u'Zelenikovo')),
+ ('ZR', _(u'Zrnovci')),
+ ('IL', _(u'Ilinden')),
+ ('JG', _(u'Jegunovce')),
+ ('AV', _(u'Kavadarci')),
+ ('KB', _(u'Karbinci')),
+ ('KX', _(u'Karpoš')),
+ ('VD', _(u'Kisela Voda')),
+ ('KH', _(u'Kičevo')),
+ ('KN', _(u'Konče')),
+ ('OC', _(u'Koćani')),
+ ('KY', _(u'Kratovo')),
+ ('KZ', _(u'Kriva Palanka')),
+ ('KG', _(u'Krivogaštani')),
+ ('KS', _(u'Kruševo')),
+ ('UM', _(u'Kumanovo')),
+ ('LI', _(u'Lipkovo')),
+ ('LO', _(u'Lozovo')),
+ ('MR', _(u'Mavrovo i Rostuša')),
+ ('MK', _(u'Makedonska Kamenica')),
+ ('MD', _(u'Makedonski Brod')),
+ ('MG', _(u'Mogila')),
+ ('NG', _(u'Negotino')),
+ ('NV', _(u'Novaci')),
+ ('NS', _(u'Novo Selo')),
+ ('OS', _(u'Oslomej')),
+ ('OD', _(u'Ohrid')),
+ ('PE', _(u'Petrovec')),
+ ('PH', _(u'Pehčevo')),
+ ('PN', _(u'Plasnica')),
+ ('PP', _(u'Prilep')),
+ ('PT', _(u'Probištip')),
+ ('RV', _(u'Radoviš')),
+ ('RN', _(u'Rankovce')),
+ ('RE', _(u'Resen')),
+ ('RO', _(u'Rosoman')),
+ ('AJ', _(u'Saraj')),
+ ('SL', _(u'Sveti Nikole')),
+ ('SS', _(u'Sopište')),
+ ('SD', _(u'Star Dojran')),
+ ('NA', _(u'Staro Nagoričane')),
+ ('UG', _(u'Struga')),
+ ('RU', _(u'Strumica')),
+ ('SU', _(u'Studeničani')),
+ ('TR', _(u'Tearce')),
+ ('ET', _(u'Tetovo')),
+ ('CE', _(u'Centar')),
+ ('CZ', _(u'Centar-Župa')),
+ ('CI', _(u'Čair')),
+ ('CA', _(u'Čaška')),
+ ('CH', _(u'Češinovo-Obleševo')),
+ ('CS', _(u'Čučer-Sandevo')),
+ ('ST', _(u'Štip')),
+ ('SO', _(u'Šuto Orizari')),
+)
diff --git a/django/contrib/localflavor/mk/models.py b/django/contrib/localflavor/mk/models.py
new file mode 100644
index 0000000000..b636357290
--- /dev/null
+++ b/django/contrib/localflavor/mk/models.py
@@ -0,0 +1,44 @@
+from django.db.models.fields import CharField
+from django.utils.translation import ugettext_lazy as _
+
+from django.contrib.localflavor.mk.mk_choices import MK_MUNICIPALITIES
+from django.contrib.localflavor.mk.forms import (UMCNField as UMCNFormField,
+ MKIdentityCardNumberField as MKIdentityCardNumberFormField)
+
+
+class MKIdentityCardNumberField(CharField):
+
+ description = _("Macedonian identity card number")
+
+ def __init__(self, *args, **kwargs):
+ kwargs['max_length'] = 8
+ super(MKIdentityCardNumberField, self).__init__(*args, **kwargs)
+
+ def formfield(self, **kwargs):
+ defaults = {'form_class' : MKIdentityCardNumberFormField}
+ defaults.update(kwargs)
+ return super(MKIdentityCardNumberField, self).formfield(**defaults)
+
+
+class MKMunicipalityField(CharField):
+
+ description = _("A Macedonian municipality (2 character code)")
+
+ def __init__(self, *args, **kwargs):
+ kwargs['choices'] = MK_MUNICIPALITIES
+ kwargs['max_length'] = 2
+ super(MKMunicipalityField, self).__init__(*args, **kwargs)
+
+
+class UMCNField(CharField):
+
+ description = _("Unique master citizen number (13 digits)")
+
+ def __init__(self, *args, **kwargs):
+ kwargs['max_length'] = 13
+ super(UMCNField, self).__init__(*args, **kwargs)
+
+ def formfield(self, **kwargs):
+ defaults = {'form_class' : UMCNFormField}
+ defaults.update(kwargs)
+ return super(UMCNField, self).formfield(**defaults)
diff --git a/docs/ref/contrib/localflavor.txt b/docs/ref/contrib/localflavor.txt
index f6dab64217..ff9efdc24f 100644
--- a/docs/ref/contrib/localflavor.txt
+++ b/docs/ref/contrib/localflavor.txt
@@ -57,6 +57,7 @@ Countries currently supported by :mod:`~django.contrib.localflavor` are:
* Italy_
* Japan_
* Kuwait_
+ * Macedonia_
* Mexico_
* `The Netherlands`_
* Norway_
@@ -110,6 +111,7 @@ Here's an example of how to use them::
.. _Italy: `Italy (it)`_
.. _Japan: `Japan (jp)`_
.. _Kuwait: `Kuwait (kw)`_
+.. _Macedonia: `Macedonia (mk)`_
.. _Mexico: `Mexico (mx)`_
.. _Norway: `Norway (no)`_
.. _Peru: `Peru (pe)`_
@@ -652,8 +654,6 @@ Israel (``il``)
.. _Israeli identification number: http://he.wikipedia.org/wiki/%D7%9E%D7%A1%D7%A4%D7%A8_%D7%96%D7%94%D7%95%D7%AA_(%D7%99%D7%A9%D7%A8%D7%90%D7%9C)
.. _Luhn algorithm: http://en.wikipedia.org/wiki/Luhn_algorithm
-
-
Italy (``it``)
==============
@@ -705,6 +705,57 @@ Kuwait (``kw``)
* The birthdate of the person is a valid date.
* The calculated checksum equals to the last digit of the Civil ID.
+Macedonia (``mk``)
+===================
+
+.. versionadded:: 1.4
+
+.. class:: mk.forms.MKIdentityCardNumberField
+
+ A form field that validates input as a Macedonian identity card number.
+ Both old and new identity card numbers are supported.
+
+
+.. class:: mk.forms.MKMunicipalitySelect
+
+ A form ``Select`` widget that uses a list of Macedonian municipalities as
+ choices.
+
+
+.. class:: mk.forms.UMCNField
+
+ A form field that validates input as a unique master citizen
+ number.
+
+ The format of the unique master citizen number is not unique
+ to Macedonia. For more information see:
+ https://secure.wikimedia.org/wikipedia/en/wiki/Unique_Master_Citizen_Number
+
+ A value will pass validation if it complies to the following rules:
+
+ * Consists of exactly 13 digits
+ * The first 7 digits represent a valid past date in the format DDMMYYY
+ * The last digit of the UMCN passes a checksum test
+
+
+.. class:: mk.models.MKIdentityCardNumberField
+
+ A model field that forms represent as a
+ ``forms.MKIdentityCardNumberField`` field.
+
+
+.. class:: mk.models.MKMunicipalityField
+
+ A model field that forms represent as a
+ ``forms.MKMunicipalitySelect`` and stores the 2 character code of the
+ municipality in the database.
+
+
+.. class:: mk.models.UMCNField
+
+ A model field that forms represent as a ``forms.UMCNField`` field.
+
+
Mexico (``mx``)
===============
diff --git a/tests/regressiontests/forms/localflavor/mk.py b/tests/regressiontests/forms/localflavor/mk.py
new file mode 100644
index 0000000000..8ef84af384
--- /dev/null
+++ b/tests/regressiontests/forms/localflavor/mk.py
@@ -0,0 +1,131 @@
+from django.contrib.localflavor.mk.forms import (
+ MKIdentityCardNumberField, MKMunicipalitySelect, UMCNField)
+
+from utils import LocalFlavorTestCase
+
+
+class MKLocalFlavorTests(LocalFlavorTestCase):
+
+ def test_MKIdentityCardNumberField(self):
+ error_invalid = (u'Identity card numbers must contain'
+ ' either 4 to 7 digits or an uppercase letter and 7 digits.')
+ valid = {
+ 'L0018077':'L0018077',
+ 'A0078315' : 'A0078315',
+ }
+ invalid = {
+ '123': error_invalid,
+ 'abcdf': error_invalid,
+ '234390a': error_invalid,
+ }
+ self.assertFieldOutput(MKIdentityCardNumberField, valid, invalid)
+
+ def test_MKMunicipalitySelect(self):
+ f = MKMunicipalitySelect()
+ out=u''''''
+ self.assertEqual(f.render('municipality', 'DL' ), out)
+
+ def test_UMCNField(self):
+ error_invalid = [u'This field should contain exactly 13 digits.']
+ error_checksum = [u'The UMCN is not valid.']
+ error_date = [u'The first 7 digits of the UMCN '
+ 'must represent a valid past date.']
+ valid = {
+ '2402983450006': '2402983450006',
+ '2803984430038': '2803984430038',
+ '1909982045004': '1909982045004',
+ }
+ invalid = {
+ '240298345': error_invalid,
+ 'abcdefghj': error_invalid,
+ '2402082450006': error_date,
+ '3002982450006': error_date,
+ '2402983450007': error_checksum,
+ '2402982450006': error_checksum,
+ }
+ self.assertFieldOutput(UMCNField, valid, invalid)
diff --git a/tests/regressiontests/forms/localflavortests.py b/tests/regressiontests/forms/localflavortests.py
index e2d5aa65ff..5a28a2b137 100644
--- a/tests/regressiontests/forms/localflavortests.py
+++ b/tests/regressiontests/forms/localflavortests.py
@@ -23,6 +23,7 @@ from localflavor.is_ import ISLocalFlavorTests
from localflavor.it import ITLocalFlavorTests
from localflavor.jp import JPLocalFlavorTests
from localflavor.kw import KWLocalFlavorTests
+from localflavor.mk import MKLocalFlavorTests
from localflavor.nl import NLLocalFlavorTests
from localflavor.pl import PLLocalFlavorTests
from localflavor.pt import PTLocalFlavorTests
diff --git a/tests/regressiontests/forms/tests/__init__.py b/tests/regressiontests/forms/tests/__init__.py
index 39db39f0df..cb5f83cdac 100644
--- a/tests/regressiontests/forms/tests/__init__.py
+++ b/tests/regressiontests/forms/tests/__init__.py
@@ -36,6 +36,7 @@ from regressiontests.forms.localflavortests import (
ITLocalFlavorTests,
JPLocalFlavorTests,
KWLocalFlavorTests,
+ MKLocalFlavorTests,
NLLocalFlavorTests,
PLLocalFlavorTests,
PTLocalFlavorTests,
diff --git a/tests/regressiontests/localflavor/mk/__init__.py b/tests/regressiontests/localflavor/mk/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/regressiontests/localflavor/mk/forms.py b/tests/regressiontests/localflavor/mk/forms.py
new file mode 100644
index 0000000000..50fcf05f94
--- /dev/null
+++ b/tests/regressiontests/localflavor/mk/forms.py
@@ -0,0 +1,7 @@
+from django.forms import ModelForm
+from models import MKPerson
+
+class MKPersonForm(ModelForm):
+
+ class Meta:
+ model = MKPerson
diff --git a/tests/regressiontests/localflavor/mk/models.py b/tests/regressiontests/localflavor/mk/models.py
new file mode 100644
index 0000000000..b79239a9bf
--- /dev/null
+++ b/tests/regressiontests/localflavor/mk/models.py
@@ -0,0 +1,14 @@
+from django.db import models
+from django.contrib.localflavor.mk.models import (
+ MKIdentityCardNumberField, MKMunicipalityField, UMCNField)
+
+class MKPerson(models.Model):
+ first_name = models.CharField(max_length = 20)
+ last_name = models.CharField(max_length = 20)
+ umcn = UMCNField()
+ id_number = MKIdentityCardNumberField()
+ municipality = MKMunicipalityField(blank = True)
+ municipality_req = MKMunicipalityField(blank = False)
+
+ class Meta:
+ app_label = 'localflavor'
diff --git a/tests/regressiontests/localflavor/mk/tests.py b/tests/regressiontests/localflavor/mk/tests.py
new file mode 100644
index 0000000000..f7f2981a85
--- /dev/null
+++ b/tests/regressiontests/localflavor/mk/tests.py
@@ -0,0 +1,176 @@
+from django.test import TestCase
+from forms import MKPersonForm
+
+class MKLocalflavorTests(TestCase):
+ def setUp(self):
+ self.form = MKPersonForm({
+ 'first_name':'Someone',
+ 'last_name':'Something',
+ 'umcn': '2402983450006',
+ 'municipality':'OD',
+ 'municipality_req':'VE',
+ 'id_number':'A1234567',
+ })
+
+ def test_get_display_methods(self):
+ """
+ Test that the get_*_display() methods are added to the model instances.
+ """
+ person = self.form.save()
+ self.assertEqual(person.get_municipality_display(), 'Ohrid')
+ self.assertEqual(person.get_municipality_req_display(), 'Veles')
+
+ def test_municipality_required(self):
+ """
+ Test that required MKMunicipalityFields throw appropriate errors.
+ """
+
+ form = MKPersonForm({
+ 'first_name':'Someone',
+ 'last_name':'Something',
+ 'umcn': '2402983450006',
+ 'municipality':'OD',
+ 'id_number':'A1234567',
+ })
+ self.assertFalse(form.is_valid())
+ self.assertEqual(
+ form.errors['municipality_req'], [u'This field is required.'])
+
+ def test_umcn_invalid(self):
+ """
+ Test that UMCNFields throw appropriate errors for invalid UMCNs.
+ """
+ form = MKPersonForm({
+ 'first_name':'Someone',
+ 'last_name':'Something',
+ 'umcn': '2402983450007',
+ 'municipality':'OD',
+ 'municipality_req':'VE',
+ 'id_number':'A1234567',
+ })
+ self.assertFalse(form.is_valid())
+ self.assertEqual(form.errors['umcn'], [u'The UMCN is not valid.'])
+
+ form = MKPersonForm({
+ 'first_name':'Someone',
+ 'last_name':'Something',
+ 'umcn': '3002983450007',
+ 'municipality':'OD',
+ 'municipality_req':'VE',
+ 'id_number':'A1234567',
+ })
+ self.assertEqual(form.errors['umcn'],
+ [u'The first 7 digits of the UMCN must represent a valid past date.'])
+
+ def test_idnumber_invalid(self):
+ """
+ Test that MKIdentityCardNumberFields throw
+ appropriate errors for invalid values
+ """
+
+ form = MKPersonForm({
+ 'first_name':'Someone',
+ 'last_name':'Something',
+ 'umcn': '2402983450007',
+ 'municipality':'OD',
+ 'municipality_req':'VE',
+ 'id_number':'A123456a',
+ })
+ self.assertFalse(form.is_valid())
+ self.assertEqual(form.errors['id_number'],
+ [u'Identity card numbers must contain either 4 to 7 '
+ 'digits or an uppercase letter and 7 digits.'])
+
+ def test_field_blank_option(self):
+ """
+ Test that the empty option is there.
+ """
+ municipality_select_html = """\
+"""
+ self.assertEqual(str(self.form['municipality']), municipality_select_html)
diff --git a/tests/regressiontests/localflavor/tests.py b/tests/regressiontests/localflavor/tests.py
index e22fc0f5be..6a02d99004 100644
--- a/tests/regressiontests/localflavor/tests.py
+++ b/tests/regressiontests/localflavor/tests.py
@@ -2,5 +2,7 @@ from django.test import TestCase
from django.utils import unittest
# just import your tests here
-from us.tests import *
from au.tests import *
+from mk.tests import *
+from us.tests import *
+