Fixed #15705 -- Added Croatian (hr) localflavor. Thanks, Zlatko Mašek and Julien Phalip.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16077 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jannis Leidel 2011-04-22 12:03:42 +00:00
parent e1f7bc0a41
commit 7478aeb0a7
8 changed files with 652 additions and 0 deletions

View File

@ -531,6 +531,7 @@ answer newbie questions, and generally made Django that much better:
Gasper Zejn <zejn@kiberpipa.org>
Jarek Zgoda <jarek.zgoda@gmail.com>
Cheng Zhang
Zlatko Mašek <zlatko.masek@gmail.com>
A big THANK YOU goes to:

View File

@ -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<dd>\d{2})(?P<mm>\d{2})(?P<yyy>\d{3})' + \
r'(?P<rr>\d{2})(?P<bbb>\d{3})(?P<k>\d{1})$')
oib_re = re.compile(r'^\d{11}$')
plate_re = re.compile(ur'^(?P<prefix>[A-ZČŠŽ]{2})' + \
ur'(?P<number>\d{3,4})(?P<suffix>[ABCDEFGHIJKLMNOPRSTUVZ]{1,2})$')
postal_code_re = re.compile(r'^\d{5}$')
phone_re = re.compile(r'^(\+385|00385|0)(?P<prefix>\d{2})(?P<number>\d{6,7})$')
jmbag_re = re.compile(r'^601983(?P<copy>\d{1})1(?P<jmbag>\d{10})(?P<k>\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<int(value)<60000:
raise ValidationError(self.error_messages['invalid'])
return '%s' % (value, )
class HRPhoneNumberField(Field):
"""
Phone number of Croatia field.
Format: Complete country code or leading zero, area code prefix, 6 or 7
digit number.
Validates fixed, mobile and FGSM numbers. Normalizes to a full number with
country code (+385 prefix).
"""
default_error_messages = {
'invalid': _('Enter a valid phone number'),
'area': _('Enter a valid area or mobile network code'),
'number': _('The phone nubmer is too long'),
}
def clean(self, value):
super(HRPhoneNumberField, self).clean(value)
if value in EMPTY_VALUES:
return u''
value = re.sub(r'[\-\s\(\)]', '', smart_unicode(value))
matches = phone_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_PHONE_NUMBER_PREFIX_CHOICES
prefix = matches.group('prefix')
number = matches.group('number')
if prefix[0] == '1':
number = prefix[1] + number
prefix = prefix[0]
if prefix not in [choice[0] for choice in HR_PHONE_NUMBER_PREFIX_CHOICES]:
raise ValidationError(self.error_messages['area'])
# Make sure the number is of adequate length.
if prefix=='1' and len(number)!=7:
raise ValidationError(self.error_messages['number'])
return '%s%s%s' % ('+385',prefix,number)
class HRJMBAGField(Field):
"""
Unique Master Academic Citizen Number of Croatia (JMBAG) field.
This number is used by college students and professors in Croatia.
http://www.cap.srce.hr/IzgledX.aspx
"""
default_error_messages = {
'invalid': _('Enter a valid 19 digit JMBAG starting with 601983'),
'copy': _('Card issue number cannot be zero'),
}
def clean(self, value):
super(HRJMBAGField, self).clean(value)
if value in EMPTY_VALUES:
return u''
value = re.sub(r'[\-\s]', '', value.strip())
matches = jmbag_re.search(value)
if matches is None:
raise ValidationError(self.error_messages['invalid'])
# Make sure the issue number is not zero.
if matches.group('copy')=='0':
raise ValidationError(self.error_messages['copy'])
# Validate checksum using Luhn algorithm.
num = [int(x) for x in value]
if not sum(num[::-2] + [sum(divmod(d * 2, 10)) for d in num[-2::-2]]) % 10 == 0:
raise ValidationError(self.error_messages['invalid'])
return '%s' % (value, )

View File

@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
"""
Sources:
Croatian Counties: http://en.wikipedia.org/wiki/ISO_3166-2:HR
Croatia doesn't have official abbreviations for counties.
The ones provided are in common use.
"""
from django.utils.translation import ugettext_lazy as _
HR_COUNTY_CHOICES = (
('GZG', _('Grad Zagreb')),
(u'BBŽ', _(u'Bjelovarsko-bilogorska županija')),
(u'BPŽ', _(u'Brodsko-posavska županija')),
(u'DNŽ', _(u'Dubrovačko-neretvanska županija')),
(u'', _(u'Istarska županija')),
(u'', _(u'Karlovačka županija')),
(u'KKŽ', _(u'Koprivničko-križevačka županija')),
(u'KZŽ', _(u'Krapinsko-zagorska županija')),
(u'LSŽ', _(u'Ličko-senjska županija')),
(u'', _(u'Međimurska županija')),
(u'OBŽ', _(u'Osječko-baranjska županija')),
(u'PSŽ', _(u'Požeško-slavonska županija')),
(u'PGŽ', _(u'Primorsko-goranska županija')),
(u'SMŽ', _(u'Sisačko-moslavačka županija')),
(u'SDŽ', _(u'Splitsko-dalmatinska županija')),
(u'ŠKŽ', _(u'Šibensko-kninska županija')),
(u'', _(u'Varaždinska županija')),
(u'VPŽ', _(u'Virovitičko-podravska županija')),
(u'VSŽ', _(u'Vukovarsko-srijemska županija')),
(u'ZDŽ', _(u'Zadarska županija')),
(u'ZGŽ', _(u'Zagrebačka županija')),
)
"""
Sources:
http://hr.wikipedia.org/wiki/Dodatak:Popis_registracijskih_oznaka_za_cestovna_vozila_u_Hrvatskoj
Only common license plate prefixes are provided. Special cases and obsolete prefixes are omitted.
"""
HR_LICENSE_PLATE_PREFIX_CHOICES = (
('BJ', 'BJ'),
('BM', 'BM'),
(u'ČK', u'ČK'),
('DA', 'DA'),
('DE', 'DE'),
('DJ', 'DJ'),
('DU', 'DU'),
('GS', 'GS'),
('IM', 'IM'),
('KA', 'KA'),
('KC', 'KC'),
('KR', 'KR'),
('KT', 'KT'),
(u'', u''),
('MA', 'MA'),
('NA', 'NA'),
('NG', 'NG'),
('OG', 'OG'),
('OS', 'OS'),
('PU', 'PU'),
(u'', u''),
('RI', 'RI'),
('SB', 'SB'),
('SK', 'SK'),
('SL', 'SL'),
('ST', 'ST'),
(u'ŠI', u'ŠI'),
('VK', 'VK'),
('VT', 'VT'),
('VU', 'VU'),
(u'', u''),
('ZD', 'ZD'),
('ZG', 'ZG'),
(u'ŽU', u'ŽU'),
)
"""
The list includes county and cellular network phone number prefixes.
"""
HR_PHONE_NUMBER_PREFIX_CHOICES = (
('1', '01'),
('20', '020'),
('21', '021'),
('22', '022'),
('23', '023'),
('31', '031'),
('32', '032'),
('33', '033'),
('34', '034'),
('35', '035'),
('40', '040'),
('42', '042'),
('43', '043'),
('44', '044'),
('47', '047'),
('48', '048'),
('49', '049'),
('51', '051'),
('52', '052'),
('53', '053'),
('91', '091'),
('92', '092'),
('95', '095'),
('97', '097'),
('98', '098'),
('99', '099'),
)

View File

@ -44,6 +44,7 @@ Countries currently supported by :mod:`~django.contrib.localflavor` are:
* Canada_
* Chile_
* China_
* Croatia_
* Czech_
* Finland_
* France_
@ -95,6 +96,7 @@ Here's an example of how to use them::
.. _Canada: `Canada (ca)`_
.. _Chile: `Chile (cl)`_
.. _China: `China (cn)`_
.. _Croatia: `Croatia (hr)`_
.. _Czech: `Czech (cz)`_
.. _Finland: `Finland (fi)`_
.. _France: `France (fr)`_
@ -374,6 +376,72 @@ China (``cn``)
Valid formats are like 1XXXXXXXXXX, where X is digit.
The second digit could only be 3, 5 and 8.
Croatia (``hr``)
==============
.. versionadded:: 1.4
.. class:: hr.forms.HRCountySelect
A ``Select`` widget that uses a list of counties of Croatia as its choices.
.. class:: hr.forms.HRPhoneNumberPrefixSelect
A ``Select`` widget that uses a list of phone number prefixes of Croatia as
its choices.
.. class:: hr.forms.HRLicensePlatePrefixSelect
A ``Select`` widget that uses a list of vehicle license plate prefixes of
Croatia as its choices.
.. class:: hr.forms.HRPhoneNumberField
A form field that validates input as a phone number of Croatia.
A valid format is a country code or a leading zero, area code prefix, 6 or 7
digit number; e.g. +385XXXXXXXX or 0XXXXXXXX
Validates fixed, mobile and FGSM numbers. Normalizes to a full number with
country code (+385 prefix).
.. class:: hr.forms.HRLicensePlateField
A form field that validates input as a vehicle license plate of Croatia.
Normalizes to the specific format XX YYYY-XX where X is a letter and Y a
digit. There can be three or four digits.
Suffix is constructed from the shared letters of the Croatian and English
alphabets.
It is used for standardized license plates only. Special cases like license
plates for oldtimers, temporary license plates, government institution
license plates and customized license plates are not covered by this field.
.. class:: hr.forms.HRPostalCodeField
A form field that validates input as a postal code of Croatia.
It consists of exactly five digits ranging from 10000 to 59999 inclusive.
.. class:: hr.forms.HROIBField
A form field that validates input as a Personal Identification Number (OIB)
of Croatia.
It consists of exactly eleven digits.
.. class:: hr.forms.HRJMBGField
A form field that validates input as a Unique Master Citizen Number (JMBG).
The number is still in use in Croatia, but it is being replaced by OIB.
This field works for other ex-Yugoslavia countries as well where the JMBG is
still in use.
The area segment of the JMBG is not validated because the citizens might
have emigrated to another ex-Yugoslavia country.
The number consists of exactly thirteen digits.
.. class:: hr.forms.HRJMBAGField
A form field that validates input as a Unique Master Academic Citizen Number
(JMBAG) of Croatia.
This number is used by college students and professors in Croatia.
The number consists of exactly nineteen digits.
Czech (``cz``)
==============

View File

@ -0,0 +1,190 @@
# -*- coding: utf-8 -*-
from django.contrib.localflavor.hr.forms import (HRCountySelect,
HRPhoneNumberPrefixSelect, HRLicensePlatePrefixSelect, HRPhoneNumberField,
HRLicensePlateField, HRPostalCodeField, HROIBField, HRJMBGField,
HRJMBAGField)
from utils import LocalFlavorTestCase
class HRLocalFlavorTests(LocalFlavorTestCase):
def test_HRCountySelect(self):
f = HRCountySelect()
out = u'''<select name="county">
<option value="GZG" selected="selected">Grad Zagreb</option>
<option value="BBŽ">Bjelovarsko-bilogorska županija</option>
<option value="BPŽ">Brodsko-posavska županija</option>
<option value="DNŽ">Dubrovačko-neretvanska županija</option>
<option value="">Istarska županija</option>
<option value="">Karlovačka županija</option>
<option value="KKŽ">Koprivničko-križevačka županija</option>
<option value="KZŽ">Krapinsko-zagorska županija</option>
<option value="LSŽ">Ličko-senjska županija</option>
<option value="">Međimurska županija</option>
<option value="OBŽ">Osječko-baranjska županija</option>
<option value="PSŽ">Požeško-slavonska županija</option>
<option value="PGŽ">Primorsko-goranska županija</option>
<option value="SMŽ">Sisačko-moslavačka županija</option>
<option value="SDŽ">Splitsko-dalmatinska županija</option>
<option value="ŠKŽ">Šibensko-kninska županija</option>
<option value="">Varaždinska županija</option>
<option value="VPŽ">Virovitičko-podravska županija</option>
<option value="VSŽ">Vukovarsko-srijemska županija</option>
<option value="ZDŽ">Zadarska županija</option>
<option value="ZGŽ">Zagrebačka županija</option>
</select>'''
self.assertEqual(f.render('county', 'GZG'), out)
def test_HRPhoneNumberPrefixSelect(self):
f = HRPhoneNumberPrefixSelect()
out = u'''<select name="phone">
<option value="1" selected="selected">01</option>
<option value="20">020</option>
<option value="21">021</option>
<option value="22">022</option>
<option value="23">023</option>
<option value="31">031</option>
<option value="32">032</option>
<option value="33">033</option>
<option value="34">034</option>
<option value="35">035</option>
<option value="40">040</option>
<option value="42">042</option>
<option value="43">043</option>
<option value="44">044</option>
<option value="47">047</option>
<option value="48">048</option>
<option value="49">049</option>
<option value="51">051</option>
<option value="52">052</option>
<option value="53">053</option>
<option value="91">091</option>
<option value="92">092</option>
<option value="95">095</option>
<option value="97">097</option>
<option value="98">098</option>
<option value="99">099</option>
</select>'''
self.assertEqual(f.render('phone', '1'), out)
def test_HRLicensePlatePrefixSelect(self):
f = HRLicensePlatePrefixSelect()
out = u'''<select name="license">
<option value="BJ" selected="selected">BJ</option>
<option value="BM">BM</option>
<option value="ČK">ČK</option>
<option value="DA">DA</option>
<option value="DE">DE</option>
<option value="DJ">DJ</option>
<option value="DU">DU</option>
<option value="GS">GS</option>
<option value="IM">IM</option>
<option value="KA">KA</option>
<option value="KC">KC</option>
<option value="KR">KR</option>
<option value="KT">KT</option>
<option value=""></option>
<option value="MA">MA</option>
<option value="NA">NA</option>
<option value="NG">NG</option>
<option value="OG">OG</option>
<option value="OS">OS</option>
<option value="PU">PU</option>
<option value=""></option>
<option value="RI">RI</option>
<option value="SB">SB</option>
<option value="SK">SK</option>
<option value="SL">SL</option>
<option value="ST">ST</option>
<option value="ŠI">ŠI</option>
<option value="VK">VK</option>
<option value="VT">VT</option>
<option value="VU">VU</option>
<option value=""></option>
<option value="ZD">ZD</option>
<option value="ZG">ZG</option>
<option value="ŽU">ŽU</option>
</select>'''
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)

View File

@ -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

View File

@ -27,6 +27,7 @@ from regressiontests.forms.localflavortests import (
FILocalFlavorTests,
FRLocalFlavorTests,
GenericLocalFlavorTests,
HRLocalFlavorTests,
IDLocalFlavorTests,
IELocalFlavorTests,
ILLocalFlavorTests,