diff --git a/AUTHORS b/AUTHORS index a6e4aa2eed..f9c352b2ad 100644 --- a/AUTHORS +++ b/AUTHORS @@ -151,6 +151,7 @@ answer newbie questions, and generally made Django that much better: dne@mayonnaise.net dready Maximillian Dornseif + Daniel Duan Jeremy Dunck Andrew Durdin dusk@woofle.net @@ -256,6 +257,7 @@ answer newbie questions, and generally made Django that much better: Michael Josephson jpellerin@gmail.com junzhang.jn@gmail.com + Xia Kai Antti Kaihola Bahadır Kandemir Karderio diff --git a/django/contrib/localflavor/cn/__init__.py b/django/contrib/localflavor/cn/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/localflavor/cn/cn_provinces.py b/django/contrib/localflavor/cn/cn_provinces.py new file mode 100644 index 0000000000..fe0aa37cb6 --- /dev/null +++ b/django/contrib/localflavor/cn/cn_provinces.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +""" +An alphabetical list of provinces for use as `choices` in a formfield. + +Reference: +http://en.wikipedia.org/wiki/ISO_3166-2:CN +http://en.wikipedia.org/wiki/Province_%28China%29 +http://en.wikipedia.org/wiki/Direct-controlled_municipality +http://en.wikipedia.org/wiki/Autonomous_regions_of_China +""" + + +CN_PROVINCE_CHOICES = ( + ("anhui", u"安徽"), + ("beijing", u"北京"), + ("chongqing", u"重庆"), + ("fujian", u"福建"), + ("gansu", u"甘肃"), + ("guangdong", u"广东"), + ("guangxi", u"广西壮族自治区"), + ("guizhou", u"贵州"), + ("hainan", u"海南"), + ("hebei", u"河北"), + ("heilongjiang", u"黑龙江"), + ("henan", u"河南"), + ("hongkong", u"香港"), + ("hubei", u"湖北"), + ("hunan", u"湖南"), + ("jiangsu", u"江苏"), + ("jiangxi", u"江西"), + ("jilin", u"吉林"), + ("liaoning", u"辽宁"), + ("macao", u"澳门"), + ("neimongol", u"内蒙古自治区"), + ("ningxia", u"宁夏回族自治区"), + ("qinghai", u"青海"), + ("shaanxi", u"陕西"), + ("shandong", u"山东"), + ("shanghai", u"上海"), + ("shanxi", u"山西"), + ("sichuan", u"四川"), + ("taiwan", u"台湾"), + ("tianjin", u"天津"), + ("xinjiang", u"新疆维吾尔自治区"), + ("xizang", u"西藏自治区"), + ("yunnan", u"云南"), + ("zhejiang", u"浙江"), +) diff --git a/django/contrib/localflavor/cn/forms.py b/django/contrib/localflavor/cn/forms.py new file mode 100644 index 0000000000..3d8d45c53e --- /dev/null +++ b/django/contrib/localflavor/cn/forms.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- + +""" +Chinese-specific form helpers +""" +import re + +from django.forms import ValidationError +from django.forms.fields import CharField, RegexField, Select +from django.utils.translation import ugettext_lazy as _ + + +__all__ = ( + 'CNProvinceSelect', + 'CNPostCodeField', + 'CNIDCardField', + 'CNPhoneNumberField', + 'CNCellNumberField', +) + + +ID_CARD_RE = r'^\d{15}(\d{2}[0-9xX])?$' +POST_CODE_RE = r'^\d{6}$' +PHONE_RE = r'^\d{3,4}-\d{7,8}(-\d+)?$' +CELL_RE = r'^1[358]\d{9}$' + +# Valid location code used in id card checking algorithm +CN_LOCATION_CODES = ( + 11, # Beijing + 12, # Tianjin + 13, # Hebei + 14, # Shanxi + 15, # Nei Mongol + 21, # Liaoning + 22, # Jilin + 23, # Heilongjiang + 31, # Shanghai + 32, # Jiangsu + 33, # Zhejiang + 34, # Anhui + 35, # Fujian + 36, # Jiangxi + 37, # Shandong + 41, # Henan + 42, # Hubei + 43, # Hunan + 44, # Guangdong + 45, # Guangxi + 46, # Hainan + 50, # Chongqing + 51, # Sichuan + 52, # Guizhou + 53, # Yunnan + 54, # Xizang + 61, # Shaanxi + 62, # Gansu + 63, # Qinghai + 64, # Ningxia + 65, # Xinjiang + 71, # Taiwan + 81, # Hong Kong + 91, # Macao +) + +class CNProvinceSelect(Select): + """ + A select widget with list of Chinese provinces as choices. + """ + def __init__(self, attrs=None): + from cn_provinces import CN_PROVINCE_CHOICES + super(CNProvinceSelect, self).__init__( + attrs, choices=CN_PROVINCE_CHOICES, + ) + + +class CNPostCodeField(RegexField): + """ + A form field that validates as Chinese post code. + Valid code is XXXXXX where X is digit. + """ + default_error_messages = { + 'invalid': _(u'Enter a post code in the format XXXXXX.'), + } + + def __init__(self, *args, **kwargs): + super(CNPostCodeField, self).__init__(POST_CODE_RE, *args, **kwargs) + + +class CNIDCardField(CharField): + """ + A form field that validates as Chinese Identification Card Number. + + This field would check the following restrictions: + * the length could only be 15 or 18. + * if the length is 18, the last digit could be x or X. + * has a valid checksum.(length 18 only) + * has a valid birthdate. + * has a valid location. + + The checksum algorithm is described in GB11643-1999. + """ + default_error_messages = { + 'invalid': _(u'ID Card Number consists of 15 or 18 digits.'), + 'checksum': _(u'Invalid ID Card Number: Wrong checksum'), + 'birthday': _(u'Invalid ID Card Number: Wrong birthdate'), + 'location': _(u'Invalid ID Card Number: Wrong location code'), + } + + def __init__(self, max_length=18, min_length=15, *args, **kwargs): + super(CNIDCardField, self).__init__(max_length, min_length, *args, + **kwargs) + + def clean(self, value): + """ + Check whether the input is a valid ID Card Number. + """ + # Check the length of the ID card number. + super(CNIDCardField, self).clean(value) + if not value: + return u"" + # Check whether this ID card number has valid format + if not re.match(ID_CARD_RE, value): + raise ValidationError(self.error_messages['invalid']) + # Check the birthday of the ID card number. + if not self.has_valid_birthday(value): + raise ValidationError(self.error_messages['birthday']) + # Check the location of the ID card number. + if not self.has_valid_location(value): + raise ValidationError(self.error_messages['location']) + # Check the checksum of the ID card number. + value = value.upper() + if not self.has_valid_checksum(value): + raise ValidationError(self.error_messages['checksum']) + return u'%s' % value + + def has_valid_birthday(self, value): + """ + This function would grab the birthdate from the ID card number and test + whether it is a valid date. + """ + from datetime import datetime + if len(value) == 15: + # 1st generation ID card + time_string = value[6:12] + format_string = "%y%m%d" + else: + # 2nd generation ID card + time_string = value[6:14] + format_string = "%Y%m%d" + try: + datetime.strptime(time_string, format_string) + return True + except ValueError: + # invalid date + return False + + def has_valid_location(self, value): + """ + This method checks if the first two digits in the ID Card are valid. + """ + return int(value[:2]) in CN_LOCATION_CODES + + def has_valid_checksum(self, value): + """ + This method checks if the last letter/digit in value is valid + according to the algorithm the ID Card follows. + """ + # If the length of the number is not 18, then the number is a 1st + # generation ID card number, and there is no checksum to be checked. + if len(value) != 18: + return True + checksum_index = sum( + map( + lambda a,b:a*(ord(b)-ord('0')), + (7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2), + value[:17], + ), + ) % 11 + return '10X98765432'[checksum_index] == value[-1] + + +class CNPhoneNumberField(RegexField): + """ + A form field that validates as Chinese phone number + A valid phone number could be like: + 010-55555555 + Considering there might be extension phone numbers, so this could also be: + 010-55555555-35 + """ + default_error_messages = { + 'invalid': _(u'Enter a valid phone number.'), + } + + def __init__(self, *args, **kwargs): + super(CNPhoneNumberField, self).__init__(PHONE_RE, *args, **kwargs) + + +class CNCellNumberField(RegexField): + """ + A form field that validates as Chinese cell number + A valid cell number could be like: + 13012345678 + We used a rough rule here, the first digit should be 1, the second could be + 3, 5 and 8, the rest could be what so ever. + The length of the cell number should be 11. + """ + default_error_messages = { + 'invalid': _(u'Enter a valid cell number.'), + } + + def __init__(self, *args, **kwargs): + super(CNCellNumberField, self).__init__(CELL_RE, *args, **kwargs) diff --git a/docs/ref/contrib/localflavor.txt b/docs/ref/contrib/localflavor.txt index e2515e3e08..9214458c3b 100644 --- a/docs/ref/contrib/localflavor.txt +++ b/docs/ref/contrib/localflavor.txt @@ -43,6 +43,7 @@ Countries currently supported by :mod:`~django.contrib.localflavor` are: * Brazil_ * Canada_ * Chile_ + * China_ * Czech_ * Finland_ * France_ @@ -92,6 +93,7 @@ Here's an example of how to use them:: .. _Brazil: `Brazil (br)`_ .. _Canada: `Canada (ca)`_ .. _Chile: `Chile (cl)`_ +.. _China: `China (cn)`_ .. _Czech: `Czech (cz)`_ .. _Finland: `Finland (fi)`_ .. _France: `France (fr)`_ @@ -337,6 +339,35 @@ Chile (``cl``) A ``Select`` widget that uses a list of Chilean regions (Regiones) as its choices. +China (``cn``) +============== + +.. class:: cn.forms.CNProvinceSelect + + A ``Select`` widget that uses a list of Chinese regions as its choices. + +.. class:: cn.forms.CNPostCodeField + + A form field that validates input as a Chinese post code. + Valid formats are XXXXXX where X is digit. + +.. class:: cn.forms.CNIDCardField + + A form field that validates input as a Chinese Identification Card Number. + Both 1st and 2nd generation ID Card Number are validated. + +.. class:: cn.forms.CNPhoneNumberField + + A form field that validates input as a Chinese phone number. + Valid formats are 0XX-XXXXXXXX, composed of 3 or 4 digits of region code + and 7 or 8 digits of phone number. + +.. class:: cn.forms.CNCellNumberField + + A form field that validates input as a Chinese mobile phone number. + Valid formats are like 1XXXXXXXXXX, where X is digit. + The second digit could only be 3, 5 and 8. + Czech (``cz``) ============== diff --git a/tests/regressiontests/forms/localflavor/cn.py b/tests/regressiontests/forms/localflavor/cn.py new file mode 100644 index 0000000000..6579d95145 --- /dev/null +++ b/tests/regressiontests/forms/localflavor/cn.py @@ -0,0 +1,113 @@ +# Tests for contrib/localflavor/ CN Form Fields + +from django.contrib.localflavor.cn.forms import (CNProvinceSelect, + CNPostCodeField, CNIDCardField, CNPhoneNumberField, CNCellNumberField) +from utils import LocalFlavorTestCase + +class CNLocalFlavorTests(LocalFlavorTestCase): + def test_CNProvinceSelect(self): + f = CNProvinceSelect() + correct_output = u'''''' + self.assertEqual(f.render('provinces', 'hubei'), correct_output) + + def test_CNPostCodeField(self): + error_format = [u'Enter a post code in the format XXXXXX.'] + valid = { + '091209': u'091209' + } + invalid = { + '09120': error_format, + '09120916': error_format + } + self.assertFieldOutput(CNPostCodeField, valid, invalid) + + def test_CNIDCardField(self): + valid = { + # A valid 1st generation ID Card Number. + '110101491001001': u'110101491001001', + # A valid 2nd generation ID Card number. + '11010119491001001X': u'11010119491001001X', + # Another valid 2nd gen ID Number with a case change + '11010119491001001x': u'11010119491001001X' + } + + wrong_format = [u'ID Card Number consists of 15 or 18 digits.'] + wrong_location = [u'Invalid ID Card Number: Wrong location code'] + wrong_bday = [u'Invalid ID Card Number: Wrong birthdate'] + wrong_checksum = [u'Invalid ID Card Number: Wrong checksum'] + + invalid = { + 'abcdefghijklmnop': wrong_format, + '1010101010101010': wrong_format, + '010101491001001' : wrong_location, # 1st gen, 01 is invalid + '110101491041001' : wrong_bday, # 1st gen. There wasn't day 41 + '92010119491001001X': wrong_location, # 2nd gen, 92 is invalid + '91010119491301001X': wrong_bday, # 2nd gen, 19491301 is invalid date + '910101194910010014': wrong_checksum #2nd gen + } + self.assertFieldOutput(CNIDCardField, valid, invalid) + + def test_CNPhoneNumberField(self): + error_format = [u'Enter a valid phone number.'] + valid = { + '010-12345678': u'010-12345678', + '010-1234567': u'010-1234567', + '0101-12345678': u'0101-12345678', + '0101-1234567': u'0101-1234567', + '010-12345678-020':u'010-12345678-020' + } + invalid = { + '01x-12345678': error_format, + '12345678': error_format, + '01123-12345678': error_format, + '010-123456789': error_format, + '010-12345678-': error_format + } + self.assertFieldOutput(CNPhoneNumberField, valid, invalid) + + def test_CNCellNumberField(self): + error_format = [u'Enter a valid cell number.'] + valid = { + '13012345678': u'13012345678', + } + invalid = { + '130123456789': error_format, + '14012345678': error_format + } + self.assertFieldOutput(CNCellNumberField, valid, invalid) + diff --git a/tests/regressiontests/forms/localflavortests.py b/tests/regressiontests/forms/localflavortests.py index 5ee1c32acc..94abed1ec0 100644 --- a/tests/regressiontests/forms/localflavortests.py +++ b/tests/regressiontests/forms/localflavortests.py @@ -7,6 +7,7 @@ from localflavor.ca import CALocalFlavorTests from localflavor.ch import CHLocalFlavorTests from localflavor.cl import CLLocalFlavorTests from localflavor.cz import CZLocalFlavorTests +from localflavor.cn import CNLocalFlavorTests from localflavor.de import DELocalFlavorTests from localflavor.es import ESLocalFlavorTests from localflavor.fi import FILocalFlavorTests diff --git a/tests/regressiontests/forms/tests/__init__.py b/tests/regressiontests/forms/tests/__init__.py index 2d96b2fae8..8d47a41168 100644 --- a/tests/regressiontests/forms/tests/__init__.py +++ b/tests/regressiontests/forms/tests/__init__.py @@ -12,15 +12,37 @@ from validators import TestFieldWithValidators from widgets import * from regressiontests.forms.localflavortests import ( - ARLocalFlavorTests, ATLocalFlavorTests, AULocalFlavorTests, - BELocalFlavorTests, BRLocalFlavorTests, CALocalFlavorTests, - CHLocalFlavorTests, CLLocalFlavorTests, CZLocalFlavorTests, - DELocalFlavorTests, ESLocalFlavorTests, FILocalFlavorTests, - FRLocalFlavorTests, GenericLocalFlavorTests, IDLocalFlavorTests, - IELocalFlavorTests, ILLocalFlavorTests, ISLocalFlavorTests, - ITLocalFlavorTests, JPLocalFlavorTests, KWLocalFlavorTests, - NLLocalFlavorTests, PLLocalFlavorTests, PTLocalFlavorTests, - ROLocalFlavorTests, SELocalFlavorTests, SKLocalFlavorTests, - TRLocalFlavorTests, UKLocalFlavorTests, USLocalFlavorTests, - UYLocalFlavorTests, ZALocalFlavorTests + ARLocalFlavorTests, + ATLocalFlavorTests, + AULocalFlavorTests, + BELocalFlavorTests, + BRLocalFlavorTests, + CALocalFlavorTests, + CHLocalFlavorTests, + CLLocalFlavorTests, + CNLocalFlavorTests, + CZLocalFlavorTests, + DELocalFlavorTests, + ESLocalFlavorTests, + FILocalFlavorTests, + FRLocalFlavorTests, + GenericLocalFlavorTests, + IDLocalFlavorTests, + IELocalFlavorTests, + ILLocalFlavorTests, + ISLocalFlavorTests, + ITLocalFlavorTests, + JPLocalFlavorTests, + KWLocalFlavorTests, + NLLocalFlavorTests, + PLLocalFlavorTests, + PTLocalFlavorTests, + ROLocalFlavorTests, + SELocalFlavorTests, + SKLocalFlavorTests, + TRLocalFlavorTests, + UKLocalFlavorTests, + USLocalFlavorTests, + UYLocalFlavorTests, + ZALocalFlavorTests )