diff --git a/AUTHORS b/AUTHORS index f9c352b2ad..5760128368 100644 --- a/AUTHORS +++ b/AUTHORS @@ -531,6 +531,7 @@ answer newbie questions, and generally made Django that much better: Gasper Zejn Jarek Zgoda Cheng Zhang + Zlatko Mašek A big THANK YOU goes to: diff --git a/django/contrib/localflavor/hr/__init__.py b/django/contrib/localflavor/hr/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/localflavor/hr/forms.py b/django/contrib/localflavor/hr/forms.py new file mode 100644 index 0000000000..7bc9ba153a --- /dev/null +++ b/django/contrib/localflavor/hr/forms.py @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- +""" +HR-specific Form helpers +""" +import re + +from django.forms.fields import Field, Select, RegexField +from django.core.validators import EMPTY_VALUES +from django.forms import ValidationError +from django.utils.translation import ugettext_lazy as _ +from django.utils.encoding import smart_unicode + +jmbg_re = re.compile(r'^(?P
\d{2})(?P\d{2})(?P\d{3})' + \ + r'(?P\d{2})(?P\d{3})(?P\d{1})$') +oib_re = re.compile(r'^\d{11}$') +plate_re = re.compile(ur'^(?P[A-ZČŠŽ]{2})' + \ + ur'(?P\d{3,4})(?P[ABCDEFGHIJKLMNOPRSTUVZ]{1,2})$') +postal_code_re = re.compile(r'^\d{5}$') +phone_re = re.compile(r'^(\+385|00385|0)(?P\d{2})(?P\d{6,7})$') +jmbag_re = re.compile(r'^601983(?P\d{1})1(?P\d{10})(?P\d{1})$') + + +class HRCountySelect(Select): + """ + A Select widget that uses a list of counties of Croatia as its choices. + """ + + def __init__(self, attrs=None): + from hr_choices import HR_COUNTY_CHOICES + super(HRCountySelect, self).__init__(attrs, choices=HR_COUNTY_CHOICES) + + +class HRLicensePlatePrefixSelect(Select): + """ + A Select widget that uses a list of vehicle license plate prefixes of + Croatia as its choices. + """ + + def __init__(self, attrs=None): + from hr_choices import HR_LICENSE_PLATE_PREFIX_CHOICES + super(HRLicensePlatePrefixSelect, self).__init__(attrs, + choices=HR_LICENSE_PLATE_PREFIX_CHOICES) + + +class HRPhoneNumberPrefixSelect(Select): + """ + A Select widget that uses a list of phone number prefixes of Croatia as its + choices. + """ + + def __init__(self, attrs=None): + from hr_choices import HR_PHONE_NUMBER_PREFIX_CHOICES + super(HRPhoneNumberPrefixSelect, self).__init__(attrs, + choices=HR_PHONE_NUMBER_PREFIX_CHOICES) + + +class HRJMBGField(Field): + """ + Unique Master Citizen Number (JMBG) field. + The number is still in use in Croatia, but it is being replaced by OIB. + + Source: http://en.wikipedia.org/wiki/Unique_Master_Citizen_Number + + For who might be reimplementing: + The "area" regular expression group is used to calculate the region where a + person was registered. Additional validation can be implemented in + accordance with it, however this could result in exclusion of legit + immigrated citizens. Therefore, this field works for any ex-Yugoslavia + country. + """ + default_error_messages = { + 'invalid': _('Enter a valid 13 digit JMBG'), + 'date': _('Error in date segment'), + } + + def clean(self, value): + super(HRJMBGField, self).clean(value) + if value in EMPTY_VALUES: + return u'' + + value = value.strip() + + matches = jmbg_re.search(value) + if matches is None: + raise ValidationError(self.error_messages['invalid']) + + # Make sure the date part is correct. + dd = int(matches.group('dd')) + mm = int(matches.group('mm')) + yyy = int(matches.group('yyy')) + import datetime + try: + datetime.date(yyy,mm,dd) + except: + raise ValidationError(self.error_messages['date']) + + # Validate checksum. + k = matches.group('k') + checksum = 0 + for i,j in zip(range(7,1,-1),range(6)): + checksum+=i*(int(value[j])+int(value[13-i])) + m = 11 - checksum % 11 + if m == 10: + raise ValidationError(self.error_messages['invalid']) + if m == 11 and k != '0': + raise ValidationError(self.error_messages['invalid']) + if not str(m) == k: + raise ValidationError(self.error_messages['invalid']) + + return u'%s' % (value, ) + + +class HROIBField(RegexField): + """ + Personal Identification Number of Croatia (OIB) field. + + http://www.oib.hr/ + """ + default_error_messages = { + 'invalid': _('Enter a valid 11 digit OIB'), + } + + def __init__(self, *args, **kwargs): + super(HROIBField, self).__init__(r'^\d{11}$', + min_length=11, max_length=11, *args, **kwargs) + + def clean(self, value): + super(HROIBField, self).clean(value) + if value in EMPTY_VALUES: + return u'' + + return '%s' % (value, ) + + +class HRLicensePlateField(Field): + """ + Vehicle license plate of Croatia field. Normalizes to the specific format + below. Suffix is constructed from the shared letters of the Croatian and + English alphabets. + + Format examples: + SB 123-A + (but also supports more characters) + ZG 1234-AA + + Used for standardized license plates only. + """ + default_error_messages = { + 'invalid': _('Enter a valid vehicle license plate number'), + 'area': _('Enter a valid location code'), + 'number': _('Number part cannot be zero'), + } + + def clean(self, value): + super(HRLicensePlateField, self).clean(value) + if value in EMPTY_VALUES: + return u'' + + value = re.sub(r'[\s\-]+', '', smart_unicode(value.strip())).upper() + + matches = plate_re.search(value) + if matches is None: + raise ValidationError(self.error_messages['invalid']) + + # Make sure the prefix is in the list of known codes. + from hr_choices import HR_LICENSE_PLATE_PREFIX_CHOICES + prefix = matches.group('prefix') + if prefix not in [choice[0] for choice in HR_LICENSE_PLATE_PREFIX_CHOICES]: + raise ValidationError(self.error_messages['area']) + + # Make sure the number portion is not zero. + number = matches.group('number') + if int(number) == 0: + raise ValidationError(self.error_messages['number']) + + return u'%s %s-%s' % (prefix,number,matches.group('suffix'), ) + + +class HRPostalCodeField(Field): + """ + Postal code of Croatia field. + It consists of exactly five digits ranging from 10000 to possibly less than + 60000. + + http://www.posta.hr/main.aspx?id=66 + """ + default_error_messages = { + 'invalid': _('Enter a valid 5 digit postal code'), + } + + def clean(self, value): + super(HRPostalCodeField, self).clean(value) + if value in EMPTY_VALUES: + return u'' + + value = value.strip() + if not postal_code_re.search(value): + raise ValidationError(self.error_messages['invalid']) + + # Make sure the number is in valid range. + if not 9999 + + + + + + + + + + + + + + + + + + + + + +''' + self.assertEqual(f.render('county', 'GZG'), out) + + def test_HRPhoneNumberPrefixSelect(self): + f = HRPhoneNumberPrefixSelect() + out = u'''''' + self.assertEqual(f.render('phone', '1'), out) + + def test_HRLicensePlatePrefixSelect(self): + f = HRLicensePlatePrefixSelect() + out = u'''''' + self.assertEqual(f.render('license', 'BJ'), out) + + def test_HRPhoneNumberField(self): + error_invalid = [u'Enter a valid phone number'] + error_area = [u'Enter a valid area or mobile network code'] + error_number = [u'The phone nubmer is too long'] + valid = { + '+38511234567': '+38511234567', + '0038511234567': '+38511234567', + '011234567': '+38511234567', + '+38521123456': '+38521123456', + '0038521123456': '+38521123456', + '021123456': '+38521123456', + } + invalid = { + '123456789': error_invalid, + '0811234567': error_area, + '0111234567': error_number, + } + self.assertFieldOutput(HRPhoneNumberField, valid, invalid) + + def test_HRLicensePlateField(self): + error_invalid = [u'Enter a valid vehicle license plate number'] + error_area = [u'Enter a valid location code'] + error_number = [u'Number part cannot be zero'] + valid = { + 'ZG 1234-AA': u'ZG 1234-AA', + 'ZG 123-A': u'ZG 123-A', + } + invalid = { + 'PV12345': error_invalid, + 'PV1234AA': error_area, + 'ZG0000CC': error_number, + } + self.assertFieldOutput(HRLicensePlateField, valid, invalid) + + def test_HRPostalCodeField(self): + error_invalid = [u'Enter a valid 5 digit postal code'] + valid = { + '10000': '10000', + '35410': '35410', + } + invalid = { + 'ABCD': error_invalid, + '99999': error_invalid, + } + self.assertFieldOutput(HRPostalCodeField, valid, invalid) + + def test_HROIBField(self): + error_invalid = [u'Enter a valid 11 digit OIB'] + valid = { + '12345678901': '12345678901', + } + invalid = { + '1234567890': error_invalid, + 'ABCDEFGHIJK': error_invalid, + } + self.assertFieldOutput(HROIBField, valid, invalid) + + def test_HRJMBGField(self): + error_invalid = [u'Enter a valid 13 digit JMBG'] + error_date = [u'Error in date segment'] + valid = { + '1211984302155': '1211984302155', + '2701984307107': '2701984307107', + } + invalid = { + '1211984302156': error_invalid, + 'ABCDEFG': error_invalid, + '9999999123456': error_date, + } + self.assertFieldOutput(HRJMBGField, valid, invalid) + + def test_HRJMBAGField(self): + error_invalid = [u'Enter a valid 19 digit JMBAG starting with 601983'] + error_copy = [u'Card issue number cannot be zero'] + valid = { + '601983 11 0130185856 4': '6019831101301858564', + } + invalid = { + '601983 11 0130185856 5': error_invalid, + '601983 01 0130185856 4': error_copy, + } + self.assertFieldOutput(HRJMBAGField, valid, invalid) diff --git a/tests/regressiontests/forms/localflavortests.py b/tests/regressiontests/forms/localflavortests.py index 7cf838193e..73f46a7201 100644 --- a/tests/regressiontests/forms/localflavortests.py +++ b/tests/regressiontests/forms/localflavortests.py @@ -13,6 +13,7 @@ from localflavor.es import ESLocalFlavorTests from localflavor.fi import FILocalFlavorTests from localflavor.fr import FRLocalFlavorTests from localflavor.generic import GenericLocalFlavorTests +from localflavor.hr import HRLocalFlavorTests from localflavor.id import IDLocalFlavorTests from localflavor.ie import IELocalFlavorTests from localflavor.il import ILLocalFlavorTests diff --git a/tests/regressiontests/forms/tests/__init__.py b/tests/regressiontests/forms/tests/__init__.py index 5567da49b4..2d8f9e9efe 100644 --- a/tests/regressiontests/forms/tests/__init__.py +++ b/tests/regressiontests/forms/tests/__init__.py @@ -27,6 +27,7 @@ from regressiontests.forms.localflavortests import ( FILocalFlavorTests, FRLocalFlavorTests, GenericLocalFlavorTests, + HRLocalFlavorTests, IDLocalFlavorTests, IELocalFlavorTests, ILLocalFlavorTests,