From e0dc28df5517d44dab05c01d1e8855711dab6c0a Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Fri, 1 Jan 2010 21:34:50 +0000 Subject: [PATCH] Fixed #8612 - Added Indonesian (id) localflavor. Thanks to Ronny Haryanto for the patch. git-svn-id: http://code.djangoproject.com/svn/django/trunk@12046 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- AUTHORS | 1 + django/contrib/localflavor/id/__init__.py | 0 django/contrib/localflavor/id/forms.py | 210 ++++++++++++++++++ django/contrib/localflavor/id/id_choices.py | 101 +++++++++ docs/ref/contrib/localflavor.txt | 35 +++ tests/regressiontests/forms/localflavor/id.py | 175 +++++++++++++++ tests/regressiontests/forms/tests.py | 2 + 7 files changed, 524 insertions(+) create mode 100644 django/contrib/localflavor/id/__init__.py create mode 100644 django/contrib/localflavor/id/forms.py create mode 100644 django/contrib/localflavor/id/id_choices.py create mode 100644 tests/regressiontests/forms/localflavor/id.py diff --git a/AUTHORS b/AUTHORS index 5593562b40..066a07e213 100644 --- a/AUTHORS +++ b/AUTHORS @@ -196,6 +196,7 @@ answer newbie questions, and generally made Django that much better: hambaloney Brian Harring Brant Harris + Ronny Haryanto Hawkeye Joe Heck Joel Heenan diff --git a/django/contrib/localflavor/id/__init__.py b/django/contrib/localflavor/id/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/localflavor/id/forms.py b/django/contrib/localflavor/id/forms.py new file mode 100644 index 0000000000..0d68fa32d5 --- /dev/null +++ b/django/contrib/localflavor/id/forms.py @@ -0,0 +1,210 @@ +""" +ID-specific Form helpers +""" + +import re +import time + +from django.forms import ValidationError +from django.forms.fields import Field, Select, EMPTY_VALUES +from django.utils.translation import ugettext_lazy as _ +from django.utils.encoding import smart_unicode + +postcode_re = re.compile(r'^[1-9]\d{4}$') +phone_re = re.compile(r'^(\+62|0)[2-9]\d{7,10}$') +plate_re = re.compile(r'^(?P[A-Z]{1,2}) ' + \ + r'(?P\d{1,5})( (?P([A-Z]{1,3}|[1-9][0-9]{,2})))?$') +nik_re = re.compile(r'^\d{16}$') + + +class IDPostCodeField(Field): + """ + An Indonesian post code field. + + http://id.wikipedia.org/wiki/Kode_pos + """ + default_error_messages = { + 'invalid': _('Enter a valid post code'), + } + + def clean(self, value): + super(IDPostCodeField, self).clean(value) + if value in EMPTY_VALUES: + return u'' + + value = value.strip() + if not postcode_re.search(value): + raise ValidationError(self.error_messages['invalid']) + + if int(value) < 10110: + raise ValidationError(self.error_messages['invalid']) + + # 1xxx0 + if value[0] == '1' and value[4] != '0': + raise ValidationError(self.error_messages['invalid']) + + return u'%s' % (value, ) + + +class IDProvinceSelect(Select): + """ + A Select widget that uses a list of provinces of Indonesia as its + choices. + """ + + def __init__(self, attrs=None): + from id_choices import PROVINCE_CHOICES + super(IDProvinceSelect, self).__init__(attrs, choices=PROVINCE_CHOICES) + + +class IDPhoneNumberField(Field): + """ + An Indonesian telephone number field. + + http://id.wikipedia.org/wiki/Daftar_kode_telepon_di_Indonesia + """ + default_error_messages = { + 'invalid': _('Enter a valid phone number'), + } + + def clean(self, value): + super(IDPhoneNumberField, self).clean(value) + if value in EMPTY_VALUES: + return u'' + + phone_number = re.sub(r'[\-\s\(\)]', '', smart_unicode(value)) + + if phone_re.search(phone_number): + return smart_unicode(value) + + raise ValidationError(self.error_messages['invalid']) + + +class IDLicensePlatePrefixSelect(Select): + """ + A Select widget that uses a list of vehicle license plate prefix code + of Indonesia as its choices. + + http://id.wikipedia.org/wiki/Tanda_Nomor_Kendaraan_Bermotor + """ + + def __init__(self, attrs=None): + from id_choices import LICENSE_PLATE_PREFIX_CHOICES + super(IDLicensePlatePrefixSelect, self).__init__(attrs, + choices=LICENSE_PLATE_PREFIX_CHOICES) + + +class IDLicensePlateField(Field): + """ + An Indonesian vehicle license plate field. + + http://id.wikipedia.org/wiki/Tanda_Nomor_Kendaraan_Bermotor + + Plus: "B 12345 12" + """ + default_error_messages = { + 'invalid': _('Enter a valid vehicle license plate number'), + } + + def clean(self, value): + super(IDLicensePlateField, self).clean(value) + if value in EMPTY_VALUES: + return u'' + + plate_number = re.sub(r'\s+', ' ', + smart_unicode(value.strip())).upper() + + matches = plate_re.search(plate_number) + if matches is None: + raise ValidationError(self.error_messages['invalid']) + + # Make sure prefix is in the list of known codes. + from id_choices import LICENSE_PLATE_PREFIX_CHOICES + prefix = matches.group('prefix') + if prefix not in [choice[0] for choice in LICENSE_PLATE_PREFIX_CHOICES]: + raise ValidationError(self.error_messages['invalid']) + + # Only Jakarta (prefix B) can have 3 letter suffix. + suffix = matches.group('suffix') + if suffix is not None and len(suffix) == 3 and prefix != 'B': + raise ValidationError(self.error_messages['invalid']) + + # RI plates don't have suffix. + if prefix == 'RI' and suffix is not None and suffix != '': + raise ValidationError(self.error_messages['invalid']) + + # Number can't be zero. + number = matches.group('number') + if number == '0': + raise ValidationError(self.error_messages['invalid']) + + # CD, CC and B 12345 12 + if len(number) == 5 or prefix in ('CD', 'CC'): + # suffix must be numeric and non-empty + if re.match(r'^\d+$', suffix) is None: + raise ValidationError(self.error_messages['invalid']) + + # Known codes range is 12-124 + if prefix in ('CD', 'CC') and not (12 <= int(number) <= 124): + raise ValidationError(self.error_messages['invalid']) + if len(number) == 5 and not (12 <= int(suffix) <= 124): + raise ValidationError(self.error_messages['invalid']) + else: + # suffix must be non-numeric + if suffix is not None and re.match(r'^[A-Z]{,3}$', suffix) is None: + raise ValidationError(self.error_messages['invalid']) + + return plate_number + + +class IDNationalIdentityNumberField(Field): + """ + An Indonesian national identity number (NIK/KTP#) field. + + http://id.wikipedia.org/wiki/Nomor_Induk_Kependudukan + + xx.xxxx.ddmmyy.xxxx - 16 digits (excl. dots) + """ + default_error_messages = { + 'invalid': _('Enter a valid NIK/KTP number'), + } + + def clean(self, value): + super(IDNationalIdentityNumberField, self).clean(value) + if value in EMPTY_VALUES: + return u'' + + value = re.sub(r'[\s.]', '', smart_unicode(value)) + + if not nik_re.search(value): + raise ValidationError(self.error_messages['invalid']) + + if int(value) == 0: + raise ValidationError(self.error_messages['invalid']) + + def valid_nik_date(year, month, day): + try: + t1 = (int(year), int(month), int(day), 0, 0, 0, 0, 0, -1) + d = time.mktime(t1) + t2 = time.localtime(d) + if t1[:3] != t2[:3]: + return False + else: + return True + except (OverflowError, ValueError): + return False + + year = int(value[10:12]) + month = int(value[8:10]) + day = int(value[6:8]) + current_year = time.localtime().tm_year + if year < int(str(current_year)[-2:]): + if not valid_nik_date(2000 + int(year), month, day): + raise ValidationError(self.error_messages['invalid']) + elif not valid_nik_date(1900 + int(year), month, day): + raise ValidationError(self.error_messages['invalid']) + + if value[:6] == '000000' or value[12:] == '0000': + raise ValidationError(self.error_messages['invalid']) + + return '%s.%s.%s.%s' % (value[:2], value[2:6], value[6:12], value[12:]) diff --git a/django/contrib/localflavor/id/id_choices.py b/django/contrib/localflavor/id/id_choices.py new file mode 100644 index 0000000000..ed1ea017b9 --- /dev/null +++ b/django/contrib/localflavor/id/id_choices.py @@ -0,0 +1,101 @@ +from django.utils.translation import ugettext_lazy as _ + +# Reference: http://id.wikipedia.org/wiki/Daftar_provinsi_Indonesia + +# Indonesia does not have an official Province code standard. +# I decided to use unambiguous and consistent (some are common) 3-letter codes. + +PROVINCE_CHOICES = ( + ('BLI', _('Bali')), + ('BTN', _('Banten')), + ('BKL', _('Bengkulu')), + ('DIY', _('Yogyakarta')), + ('JKT', _('Jakarta')), + ('GOR', _('Gorontalo')), + ('JMB', _('Jambi')), + ('JBR', _('Jawa Barat')), + ('JTG', _('Jawa Tengah')), + ('JTM', _('Jawa Timur')), + ('KBR', _('Kalimantan Barat')), + ('KSL', _('Kalimantan Selatan')), + ('KTG', _('Kalimantan Tengah')), + ('KTM', _('Kalimantan Timur')), + ('BBL', _('Kepulauan Bangka-Belitung')), + ('KRI', _('Kepulauan Riau')), + ('LPG', _('Lampung')), + ('MLK', _('Maluku')), + ('MUT', _('Maluku Utara')), + ('NAD', _('Nanggroe Aceh Darussalam')), + ('NTB', _('Nusa Tenggara Barat')), + ('NTT', _('Nusa Tenggara Timur')), + ('PPA', _('Papua')), + ('PPB', _('Papua Barat')), + ('RIU', _('Riau')), + ('SLB', _('Sulawesi Barat')), + ('SLS', _('Sulawesi Selatan')), + ('SLT', _('Sulawesi Tengah')), + ('SLR', _('Sulawesi Tenggara')), + ('SLU', _('Sulawesi Utara')), + ('SMB', _('Sumatera Barat')), + ('SMS', _('Sumatera Selatan')), + ('SMU', _('Sumatera Utara')), +) + +LICENSE_PLATE_PREFIX_CHOICES = ( + ('A', _('Banten')), + ('AA', _('Magelang')), + ('AB', _('Yogyakarta')), + ('AD', _('Surakarta - Solo')), + ('AE', _('Madiun')), + ('AG', _('Kediri')), + ('B', _('Jakarta')), + ('BA', _('Sumatera Barat')), + ('BB', _('Tapanuli')), + ('BD', _('Bengkulu')), + ('BE', _('Lampung')), + ('BG', _('Sumatera Selatan')), + ('BH', _('Jambi')), + ('BK', _('Sumatera Utara')), + ('BL', _('Nanggroe Aceh Darussalam')), + ('BM', _('Riau')), + ('BN', _('Kepulauan Bangka Belitung')), + ('BP', _('Kepulauan Riau')), + ('CC', _('Corps Consulate')), + ('CD', _('Corps Diplomatic')), + ('D', _('Bandung')), + ('DA', _('Kalimantan Selatan')), + ('DB', _('Sulawesi Utara Daratan')), + ('DC', _('Sulawesi Barat')), + ('DD', _('Sulawesi Selatan')), + ('DE', _('Maluku')), + ('DG', _('Maluku Utara')), + ('DH', _('NTT - Timor')), + ('DK', _('Bali')), + ('DL', _('Sulawesi Utara Kepulauan')), + ('DM', _('Gorontalo')), + ('DN', _('Sulawesi Tengah')), + ('DR', _('NTB - Lombok')), + ('DS', _('Papua dan Papua Barat')), + ('DT', _('Sulawesi Tenggara')), + ('E', _('Cirebon')), + ('EA', _('NTB - Sumbawa')), + ('EB', _('NTT - Flores')), + ('ED', _('NTT - Sumba')), + ('F', _('Bogor')), + ('G', _('Pekalongan')), + ('H', _('Semarang')), + ('K', _('Pati')), + ('KB', _('Kalimantan Barat')), + ('KH', _('Kalimantan Tengah')), + ('KT', _('Kalimantan Timur')), + ('L', _('Surabaya')), + ('M', _('Madura')), + ('N', _('Malang')), + ('P', _('Jember')), + ('R', _('Banyumas')), + ('RI', _('Federal Government')), + ('S', _('Bojonegoro')), + ('T', _('Purwakarta')), + ('W', _('Sidoarjo')), + ('Z', _('Garut')), +) diff --git a/docs/ref/contrib/localflavor.txt b/docs/ref/contrib/localflavor.txt index 251729fbf6..81c6f2431b 100644 --- a/docs/ref/contrib/localflavor.txt +++ b/docs/ref/contrib/localflavor.txt @@ -50,6 +50,7 @@ Countries currently supported by :mod:`~django.contrib.localflavor` are: * Germany_ * Iceland_ * India_ + * Indonesia_ * Ireland_ * Italy_ * Japan_ @@ -95,6 +96,7 @@ Here's an example of how to use them:: .. _The Netherlands: `The Netherlands (nl)`_ .. _Iceland: `Iceland (is\_)`_ .. _India: `India (in\_)`_ +.. _Indonesia: `Indonesia (id)`_ .. _Ireland: `Ireland (ie)`_ .. _Italy: `Italy (it)`_ .. _Japan: `Japan (jp)`_ @@ -382,6 +384,39 @@ Ireland (``ie``) A ``Select`` widget that uses a list of Irish Counties as its choices. +Indonesia (``id``) +================== + +.. class:: id.forms.IDPostCodeField + + A form field that validates input as an Indonesian post code field. + +.. class:: id.forms.IDProvinceSelect + + A ``Select`` widget that uses a list of Indonesian provinces as its choices. + +.. class:: id.forms.IDPhoneNumberField + + A form field that validates input as an Indonesian telephone number. + +.. class:: id.forms.IDLicensePlatePrefixSelect + + A ``Select`` widget that uses a list of Indonesian license plate + prefix code as its choices. + +.. class:: id.forms.IDLicensePlateField + + A form field that validates input as an Indonesian vehicle license plate. + +.. class:: id.forms.IDNationalIdentityNumberField + + A form field that validates input as an Indonesian national identity + number (`NIK`_/KTP). The output will be in the format of + 'XX.XXXX.DDMMYY.XXXX'. Dots or spaces can be used in the input to break + down the numbers. + +.. _NIK: http://en.wikipedia.org/wiki/Indonesian_identity_card + Italy (``it``) ============== diff --git a/tests/regressiontests/forms/localflavor/id.py b/tests/regressiontests/forms/localflavor/id.py new file mode 100644 index 0000000000..9098b9d6c0 --- /dev/null +++ b/tests/regressiontests/forms/localflavor/id.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +# Tests for the contrib/localflavor/ ID form fields. + +tests = r""" + +# IDPhoneNumberField ######################################################## + +>>> from django.contrib.localflavor.id.forms import IDPhoneNumberField +>>> f = IDPhoneNumberField(required=False) +>>> f.clean('') +u'' +>>> f.clean('0812-3456789') +u'0812-3456789' +>>> f.clean('081234567890') +u'081234567890' +>>> f.clean('021 345 6789') +u'021 345 6789' +>>> f.clean('0213456789') +u'0213456789' +>>> f.clean('0123456789') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid phone number'] +>>> f.clean('+62-21-3456789') +u'+62-21-3456789' +>>> f.clean('+62-021-3456789') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid phone number'] +>>> f.clean('(021) 345 6789') +u'(021) 345 6789' +>>> f.clean('+62-021-3456789') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid phone number'] +>>> f.clean('+62-0812-3456789') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid phone number'] +>>> f.clean('0812345678901') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid phone number'] +>>> f.clean('foo') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid phone number'] + +# IDPostCodeField ############################################################ + +>>> from django.contrib.localflavor.id.forms import IDPostCodeField +>>> f = IDPostCodeField(required=False) +>>> f.clean('') +u'' +>>> f.clean('12340') +u'12340' +>>> f.clean('25412') +u'25412' +>>> f.clean(' 12340 ') +u'12340' +>>> f.clean('12 3 4 0') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid post code'] +>>> f.clean('12345') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid post code'] +>>> f.clean('10100') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid post code'] +>>> f.clean('123456') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid post code'] +>>> f.clean('foo') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid post code'] + +# IDNationalIdentityNumberField ######################################################### + +>>> from django.contrib.localflavor.id.forms import IDNationalIdentityNumberField +>>> f = IDNationalIdentityNumberField(required=False) +>>> f.clean('') +u'' +>>> f.clean(' 12.3456.010178 3456 ') +u'12.3456.010178.3456' +>>> f.clean('1234560101783456') +u'12.3456.010178.3456' +>>> f.clean('12.3456.010101.3456') +u'12.3456.010101.3456' +>>> f.clean('12.3456.310278.3456') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid NIK/KTP number'] +>>> f.clean('00.0000.010101.0000') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid NIK/KTP number'] +>>> f.clean('1234567890123456') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid NIK/KTP number'] +>>> f.clean('foo') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid NIK/KTP number'] + +# IDProvinceSelect ########################################################## + +>>> from django.contrib.localflavor.id.forms import IDProvinceSelect +>>> s = IDProvinceSelect() +>>> s.render('provinces', 'LPG') +u'' + +# IDLicensePlatePrefixelect ######################################################################## + +>>> from django.contrib.localflavor.id.forms import IDLicensePlatePrefixSelect +>>> s = IDLicensePlatePrefixSelect() +>>> s.render('codes', 'BE') +u'' + +# IDLicensePlateField ####################################################################### + +>>> from django.contrib.localflavor.id.forms import IDLicensePlateField +>>> f = IDLicensePlateField(required=False) +>>> f.clean('') +u'' +>>> f.clean(' b 1234 ab ') +u'B 1234 AB' +>>> f.clean('B 1234 ABC') +u'B 1234 ABC' +>>> f.clean('A 12') +u'A 12' +>>> f.clean('DK 12345 12') +u'DK 12345 12' +>>> f.clean('RI 10') +u'RI 10' +>>> f.clean('CD 12 12') +u'CD 12 12' +>>> f.clean('CD 10 12') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid vehicle license plate number'] +>>> f.clean('CD 1234 12') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid vehicle license plate number'] +>>> f.clean('RI 10 AB') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid vehicle license plate number'] +>>> f.clean('B 12345 01') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid vehicle license plate number'] +>>> f.clean('N 1234 12') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid vehicle license plate number'] +>>> f.clean('A 12 XYZ') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid vehicle license plate number'] +>>> f.clean('Q 1234 AB') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid vehicle license plate number'] +>>> f.clean('foo') +Traceback (most recent call last): + ... +ValidationError: [u'Enter a valid vehicle license plate number'] +""" \ No newline at end of file diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py index e246d7290c..72dcad89c9 100644 --- a/tests/regressiontests/forms/tests.py +++ b/tests/regressiontests/forms/tests.py @@ -15,6 +15,7 @@ from localflavor.es import tests as localflavor_es_tests from localflavor.fi import tests as localflavor_fi_tests from localflavor.fr import tests as localflavor_fr_tests from localflavor.generic import tests as localflavor_generic_tests +from localflavor.id import tests as localflavor_id_tests from localflavor.ie import tests as localflavor_ie_tests from localflavor.is_ import tests as localflavor_is_tests from localflavor.it import tests as localflavor_it_tests @@ -54,6 +55,7 @@ __test__ = { 'localflavor_fi_tests': localflavor_fi_tests, 'localflavor_fr_tests': localflavor_fr_tests, 'localflavor_generic_tests': localflavor_generic_tests, + 'localflavor_id_tests': localflavor_id_tests, 'localflavor_ie_tests': localflavor_ie_tests, 'localflavor_is_tests': localflavor_is_tests, 'localflavor_it_tests': localflavor_it_tests,