From 8ac9cbee2f9fc2a6df8c52872011bacf223a952f Mon Sep 17 00:00:00 2001 From: Malcolm Tredinnick Date: Sat, 19 Jul 2008 19:22:44 +0000 Subject: [PATCH] Fixed #7763 -- Added a Romanian localflavor. Thanks, MihaiD. git-svn-id: http://code.djangoproject.com/svn/django/trunk@7989 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/localflavor/ro/__init__.py | 0 django/contrib/localflavor/ro/forms.py | 200 ++++++++++++++++++ django/contrib/localflavor/ro/ro_counties.py | 52 +++++ docs/localflavor.txt | 48 +++++ tests/regressiontests/forms/localflavor/ro.py | 175 +++++++++++++++ tests/regressiontests/forms/tests.py | 2 + 6 files changed, 477 insertions(+) create mode 100644 django/contrib/localflavor/ro/__init__.py create mode 100644 django/contrib/localflavor/ro/forms.py create mode 100644 django/contrib/localflavor/ro/ro_counties.py create mode 100644 tests/regressiontests/forms/localflavor/ro.py diff --git a/django/contrib/localflavor/ro/__init__.py b/django/contrib/localflavor/ro/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/localflavor/ro/forms.py b/django/contrib/localflavor/ro/forms.py new file mode 100644 index 0000000000..ca51d91839 --- /dev/null +++ b/django/contrib/localflavor/ro/forms.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +""" +Romanian specific form helpers. +""" + +import re + +from django.forms import ValidationError, Field, RegexField, Select +from django.forms.fields import EMPTY_VALUES +from django.utils.translation import ugettext_lazy as _ + +class ROCIFField(RegexField): + """ + A Romanian fiscal identity code (CIF) field + + For CIF validation algorithm see http://www.validari.ro/cui.html + """ + default_error_messages = { + 'invalid': _("Enter a valid CIF."), + } + + def __init__(self, *args, **kwargs): + super(ROCIFField, self).__init__(r'^[0-9]{2,10}', max_length=10, + min_length=2, *args, **kwargs) + + def clean(self, value): + """ + CIF validation + """ + value = super(ROCIFField, self).clean(value) + if value in EMPTY_VALUES: + return u'' + # strip RO part + if value[0:2] == 'RO': + value = value[2:] + key = '753217532'[::-1] + value = value[::-1] + key_iter = iter(key) + checksum = 0 + for digit in value[1:]: + checksum += int(digit) * int(key_iter.next()) + checksum = checksum * 10 % 11 + if checksum == 10: + checksum = 0 + if checksum != int(value[0]): + raise ValidationError(self.error_messages['invalid']) + return value[::-1] + +class ROCNPField(RegexField): + """ + A Romanian personal identity code (CNP) field + + For CNP validation algorithm see http://www.validari.ro/cnp.html + """ + default_error_messages = { + 'invalid': _("Enter a valid CNP."), + } + + def __init__(self, *args, **kwargs): + super(ROCNPField, self).__init__(r'^[1-9][0-9]{12}', max_length=13, + min_length=13, *args, **kwargs) + + def clean(self, value): + """ + CNP validations + """ + value = super(ROCNPField, self).clean(value) + # check birthdate digits + import datetime + try: + datetime.date(int(value[1:3]),int(value[3:5]),int(value[5:7])) + except: + raise ValidationError(self.error_messages['invalid']) + # checksum + key = '279146358279' + checksum = 0 + value_iter = iter(value) + for digit in key: + checksum += int(digit) * int(value_iter.next()) + checksum %= 11 + if checksum == 10: + checksum = 1 + if checksum != int(value[12]): + raise ValidationError(self.error_messages['invalid']) + return value + +class ROCountyField(Field): + """ + A form field that validates its input is a Romanian county name or + abbreviation. It normalizes the input to the standard vehicle registration + abbreviation for the given county + + WARNING: This field will only accept names written with diacritics; consider + using ROCountySelect if this behavior is unnaceptable for you + Example: + Argeş => valid + Arges => invalid + """ + default_error_messages = { + 'invalid': u'Enter a Romanian county code or name.', + } + + def clean(self, value): + from ro_counties import COUNTIES_CHOICES + super(ROCountyField, self).clean(value) + if value in EMPTY_VALUES: + return u'' + try: + value = value.strip().upper() + except AttributeError: + pass + # search for county code + for entry in COUNTIES_CHOICES: + if value in entry: + return value + # search for county name + normalized_CC = [] + for entry in COUNTIES_CHOICES: + normalized_CC.append((entry[0],entry[1].upper())) + for entry in normalized_CC: + if entry[1] == value: + return entry[0] + raise ValidationError(self.error_messages['invalid']) + +class ROCountySelect(Select): + """ + A Select widget that uses a list of Romanian counties (judete) as its + choices. + """ + def __init__(self, attrs=None): + from ro_counties import COUNTIES_CHOICES + super(ROCountySelect, self).__init__(attrs, choices=COUNTIES_CHOICES) + +class ROIBANField(RegexField): + """ + Romanian International Bank Account Number (IBAN) field + + For Romanian IBAN validation algorithm see http://validari.ro/iban.html + """ + default_error_messages = { + 'invalid': _('Enter a valid IBAN in ROXX-XXXX-XXXX-XXXX-XXXX-XXXX format'), + } + + def __init__(self, *args, **kwargs): + super(ROIBANField, self).__init__(r'^[0-9A-Za-z\-\s]{24,40}$', + max_length=40, min_length=24, *args, **kwargs) + + def clean(self, value): + """ + Strips - and spaces, performs country code and checksum validation + """ + value = super(ROIBANField, self).clean(value) + value = value.replace('-','') + value = value.replace(' ','') + value = value.upper() + if value[0:2] != 'RO': + raise ValidationError(self.error_messages['invalid']) + numeric_format = '' + for char in value[4:] + value[0:4]: + if char.isalpha(): + numeric_format += str(ord(char) - 55) + else: + numeric_format += char + if int(numeric_format) % 97 != 1: + raise ValidationError(self.error_messages['invalid']) + return value + +class ROPhoneNumberField(RegexField): + """Romanian phone number field""" + default_error_messages = { + 'invalid': _('Phone numbers must be in XXXX-XXXXXX format.'), + } + + def __init__(self, *args, **kwargs): + super(ROPhoneNumberField, self).__init__(r'^[0-9\-\(\)\s]{10,20}$', + max_length=20, min_length=10, *args, **kwargs) + + def clean(self, value): + """ + Strips -, (, ) and spaces. Checks the final length. + """ + value = super(ROPhoneNumberField, self).clean(value) + value = value.replace('-','') + value = value.replace('(','') + value = value.replace(')','') + value = value.replace(' ','') + if len(value) != 10: + raise ValidationError(self.error_messages['invalid']) + return value + +class ROPostalCodeField(RegexField): + """Romanian postal code field.""" + default_error_messages = { + 'invalid': _('Enter a valid postal code in the format XXXXXX'), + } + + def __init__(self, *args, **kwargs): + super(ROPostalCodeField, self).__init__(r'^[0-9][0-8][0-9]{4}$', + max_length=6, min_length=6, *args, **kwargs) + diff --git a/django/contrib/localflavor/ro/ro_counties.py b/django/contrib/localflavor/ro/ro_counties.py new file mode 100644 index 0000000000..40423ddc87 --- /dev/null +++ b/django/contrib/localflavor/ro/ro_counties.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +""" +A list of Romanian counties as `choices` in a formfield. + +This exists as a standalone file so that it's only imported into memory when +explicitly needed. +""" + +COUNTIES_CHOICES = ( + ('AB', u'Alba'), + ('AR', u'Arad'), + ('AG', u'Argeş'), + ('BC', u'Bacău'), + ('BH', u'Bihor'), + ('BN', u'Bistriţa-Năsăud'), + ('BT', u'Botoşani'), + ('BV', u'Braşov'), + ('BR', u'Brăila'), + ('B', u'Bucureşti'), + ('BZ', u'Buzău'), + ('CS', u'Caraş-Severin'), + ('CL', u'Călăraşi'), + ('CJ', u'Cluj'), + ('CT', u'Constanţa'), + ('CV', u'Covasna'), + ('DB', u'Dâmboviţa'), + ('DJ', u'Dolj'), + ('GL', u'Galaţi'), + ('GR', u'Giurgiu'), + ('GJ', u'Gorj'), + ('HR', u'Harghita'), + ('HD', u'Hunedoara'), + ('IL', u'Ialomiţa'), + ('IS', u'Iaşi'), + ('IF', u'Ilfov'), + ('MM', u'Maramureş'), + ('MH', u'Mehedinţi'), + ('MS', u'Mureş'), + ('NT', u'Neamţ'), + ('OT', u'Olt'), + ('PH', u'Prahova'), + ('SM', u'Satu Mare'), + ('SJ', u'Sălaj'), + ('SB', u'Sibiu'), + ('SV', u'Suceava'), + ('TR', u'Teleorman'), + ('TM', u'Timiş'), + ('TL', u'Tulcea'), + ('VS', u'Vaslui'), + ('VL', u'Vâlcea'), + ('VN', u'Vrancea'), +) diff --git a/docs/localflavor.txt b/docs/localflavor.txt index 5a2e5b8fda..f30c6a542b 100644 --- a/docs/localflavor.txt +++ b/docs/localflavor.txt @@ -47,6 +47,7 @@ Countries currently supported by ``localflavor`` are: * Norway_ * Peru_ * Poland_ + * Romania_ * Slovakia_ * `South Africa`_ * Spain_ @@ -84,6 +85,7 @@ them:: .. _Norway: `Norway (django.contrib.localflavor.no)`_ .. _Peru: `Peru (django.contrib.localflavor.pe)`_ .. _Poland: `Poland (django.contrib.localflavor.pl)`_ +.. _Romania: `Romania (django.contrib.localflavor.ro)`_ .. _Slovakia: `Slovakia (django.contrib.localflavor.sk)`_ .. _South Africa: `South Africa (django.contrib.localflavor.za)`_ .. _Spain: `Spain (django.contrib.localflavor.es)`_ @@ -497,6 +499,52 @@ PLVoivodeshipSelect A ``Select`` widget that uses a list of Polish voivodeships (administrative provinces) as its choices. +Romania (``django.contrib.localflavor.ro``) +============================================ + +ROCIFField +---------- + +A form field that validates Romanian fiscal identification codes (CIF). The +return value strips the leading RO, if given. + +ROCNPField +---------- + +A form field that validates Romanian personal numeric codes (CNP). + +ROCountyField +------------- + +A form field that validates its input as a Romanian county (judet) name or +abbreviation. It normalizes the input to the standard vehicle registration +abbreviation for the given county. This field will only accept names written +with diacritics; consider using ROCountySelect as an alternative. + +ROCountySelect +-------------- + +A ``Select`` widget that uses a list of Romanian counties (judete) as its +choices. + +ROIBANField +----------- + +A form field that validates its input as a Romanian International Bank +Account Number (IBAN). The valid format is ROXX-XXXX-XXXX-XXXX-XXXX-XXXX, +with or without hyphens. + +ROPhoneNumberField +------------------ + +A form field that validates Romanian phone numbers, short special numbers +excluded. + +ROPostalCodeField +----------------- + +A form field that validates Romanian postal codes. + Slovakia (``django.contrib.localflavor.sk``) ============================================ diff --git a/tests/regressiontests/forms/localflavor/ro.py b/tests/regressiontests/forms/localflavor/ro.py new file mode 100644 index 0000000000..e885030029 --- /dev/null +++ b/tests/regressiontests/forms/localflavor/ro.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +# Tests for the contrib/localflavor/ RO form fields. + +tests = r""" +>>> from django.contrib.localflavor.ro.forms import * + +##ROCIFField ################################################################ + +f = ROCIFField() +f.clean('21694681') +u'21694681' +f.clean('RO21694681') +u'21694681' +f.clean('21694680') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid CIF'] +f.clean('21694680000') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at most 10 characters (it has 11).'] +f.clean('0') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at least 2 characters (it has 1).'] +f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +##ROCNPField ################################################################# + +f = ROCNPField() +f.clean('1981211204489') +u'1981211204489' +f.clean('1981211204487') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid CNP'] +f.clean('1981232204489') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid CNP'] +f.clean('9981211204489') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid CNP'] +f.clean('9981211209') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at least 13 characters (it has 10).'] +f.clean('19812112044891') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at most 13 characters (it has 14).'] +f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +##ROCountyField ############################################################## + +f = ROCountyField() +f.clean('CJ') +'CJ' +f.clean('cj') +'CJ' +f.clean('Argeş') +'AG' +f.clean('argeş') +'AG' +f.clean('Arges') +Traceback (most recent call last): +... +ValidationError: [u'Enter a Romanian county code or name.'] +f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +##ROCountySelect ############################################################# + +f = ROCountySelect() +f.render('county','CJ') +u'' + +##ROIBANField ################################################################# + +f = ROIBANField() +f.clean('RO56RZBR0000060003291177') +u'RO56RZBR0000060003291177' +f.clean('RO56RZBR0000060003291176') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IBAN in ROXX-XXXX-XXXX-XXXX-XXXX-XXXX format'] + +f.clean('RO56-RZBR-0000-0600-0329-1177') +u'RO56RZBR0000060003291177' +f.clean('AT61 1904 3002 3457 3201') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IBAN in ROXX-XXXX-XXXX-XXXX-XXXX-XXXX format'] + +f.clean('RO56RZBR000006000329117') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at least 24 characters (it has 23).'] +f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +##ROPhoneNumberField ########################################################## + +f = ROPhoneNumberField() +f.clean('0264485936') +u'0264485936' +f.clean('(0264)-485936') +u'0264485936' +f.clean('02644859368') +Traceback (most recent call last): +... +ValidationError: [u'Phone numbers must be in XXXX-XXXXXX format.'] +f.clean('026448593') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at least 10 characters (it has 9).'] +f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +##ROPostalCodeField ########################################################### + +f = ROPostalCodeField() +f.clean('400473') +u'400473' +f.clean('40047') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at least 6 characters (it has 5).'] +f.clean('4004731') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at most 6 characters (it has 7).'] +f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +""" diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py index ff8213c8d9..f5ab34507d 100644 --- a/tests/regressiontests/forms/tests.py +++ b/tests/regressiontests/forms/tests.py @@ -19,6 +19,7 @@ from localflavor.it import tests as localflavor_it_tests from localflavor.jp import tests as localflavor_jp_tests from localflavor.nl import tests as localflavor_nl_tests from localflavor.pl import tests as localflavor_pl_tests +from localflavor.ro import tests as localflavor_ro_tests from localflavor.sk import tests as localflavor_sk_tests from localflavor.uk import tests as localflavor_uk_tests from localflavor.us import tests as localflavor_us_tests @@ -50,6 +51,7 @@ __test__ = { 'localflavor_jp_tests': localflavor_jp_tests, 'localflavor_nl_tests': localflavor_nl_tests, 'localflavor_pl_tests': localflavor_pl_tests, + 'localflavor_ro_tests': localflavor_ro_tests, 'localflavor_sk_tests': localflavor_sk_tests, 'localflavor_uk_tests': localflavor_uk_tests, 'localflavor_us_tests': localflavor_us_tests,