Index: django/contrib/localflavor/cn/cn_provinces.py
===================================================================
--- django/contrib/localflavor/cn/cn_provinces.py (revision 0)
+++ django/contrib/localflavor/cn/cn_provinces.py (revision 0)
@@ -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"河南"),
+ ("Hong Kong", u"香港"),
+ ("Hubei", u"湖北"),
+ ("Hunan", u"湖南"),
+ ("Jiangsu", u"江苏"),
+ ("Jiangxi", u"江西"),
+ ("Jilin", u"吉林"),
+ ("Liaoning", u"辽宁"),
+ ("Macao", u"澳门"),
+ ("Nei Mongol", 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"浙江"),
+)
Index: django/contrib/localflavor/cn/__init__.py
===================================================================
Index: django/contrib/localflavor/cn/cn_location_code.py
===================================================================
--- django/contrib/localflavor/cn/cn_location_code.py (revision 0)
+++ django/contrib/localflavor/cn/cn_location_code.py (revision 0)
@@ -0,0 +1,46 @@
+#coding=utf-8
+"""
+This file contains a tuple of valid Chinese location codes as was
+described in GB/T2260-1995.
+
+Please note that this tuple is not complete and detailed. We do not want to
+check the last four digits of the location code, which might be changed
+(relatively) frequently.
+"""
+
+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
+)
Index: django/contrib/localflavor/cn/forms.py
===================================================================
--- django/contrib/localflavor/cn/forms.py (revision 0)
+++ django/contrib/localflavor/cn/forms.py (revision 0)
@@ -0,0 +1,177 @@
+#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}$'
+
+
+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 function would check the first two digits in the ID card number to
+ see whether it is valid.
+ """
+ from cn_location_code import CN_LOCATION_CODES
+ location_code = int(value[:2])
+ return location_code in CN_LOCATION_CODES
+
+ def has_valid_checksum(self, value):
+ """
+ This function would check whether the last digit of the ID card number
+ is a valid checksum.
+ """
+ # 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)
Index: tests/regressiontests/forms/localflavor/cn.py
===================================================================
--- tests/regressiontests/forms/localflavor/cn.py (revision 0)
+++ tests/regressiontests/forms/localflavor/cn.py (revision 0)
@@ -0,0 +1,177 @@
+tests = r"""
+
+############################################################
+##################### CNProvinceSelect #####################
+############################################################
+>>> from django.contrib.localflavor.cn.forms import CNProvinceSelect
+>>> s = CNProvinceSelect()
+>>> s.render('provinces', 'Hubei')
+u''
+
+############################################################
+##################### CNPostCodeField ######################
+############################################################
+>>> from django.contrib.localflavor.cn.forms import CNPostCodeField
+>>> f = CNPostCodeField(required=False)
+>>> f.clean('')
+u''
+>>> f.clean('091209')
+u'091209'
+>>> f.clean('09120')
+Traceback (most recent call last):
+ ...
+ValidationError: [u'Enter a post code in the format XXXXXX.']
+>>> f.clean('09120916')
+Traceback (most recent call last):
+ ...
+ValidationError: [u'Enter a post code in the format XXXXXX.']
+
+############################################################
+##################### CNIDCardField ########################
+############################################################
+>>> from django.contrib.localflavor.cn.forms import CNIDCardField
+>>> f = CNIDCardField(required=False)
+>>> f.clean('')
+u''
+
+>>> # A string of 16 characters.
+>>> f.clean("abcdefghijklmnop")
+Traceback (most recent call last):
+ ...
+ValidationError: [u'ID Card Number consists of 15 or 18 digits.']
+
+>>> # A string of 16 digits.
+>>> f.clean("1010101010101010")
+Traceback (most recent call last):
+ ...
+ValidationError: [u'ID Card Number consists of 15 or 18 digits.']
+
+>>> # A valid 1st generation ID Card number.
+>>> f.clean('110101491001001')
+u'110101491001001'
+
+>>> # A 1st generation ID Card number with invalid location
+>>> f.clean('010101491001001') # 01, an invalid location.
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid ID Card Number: Wrong location code']
+
+>>> # A 1st generation ID Card number with invalid birthdate.
+>>> f.clean('110101491041001') # 491041, an invalid day, 41
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid ID Card Number: Wrong birthdate']
+
+>>> # A valid 2nd generation ID Card number.
+>>> f.clean('11010119491001001X')
+u'11010119491001001X'
+
+>>> # Another valid 2nd generation ID Card number, notice that the case of the last
+>>> # character is changed.
+>>> f.clean('11010119491001001x')
+u'11010119491001001X'
+>>> # An invalid 2nd generation ID Card number.
+>>> f.clean('92010119491001001X') # 92 is an invalid location code.
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid ID Card Number: Wrong location code']
+
+>>> # Another invalid 2nd generation ID Card number.
+>>> f.clean('91010119491301001X') # 19491301 is an invalid date.
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid ID Card Number: Wrong birthdate']
+
+>>> # Yet another invalid 2nd generation ID Card number.
+>>> f.clean('910101194910010014')
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid ID Card Number: Wrong checksum']
+
+############################################################
+##################### CNPhoneNumberField ###################
+############################################################
+>>> from django.contrib.localflavor.cn.forms import CNPhoneNumberField
+>>> f = CNPhoneNumberField(required=False)
+>>> f.clean('')
+u''
+>>> f.clean('010-12345678')
+u'010-12345678'
+>>> f.clean('010-1234567')
+u'010-1234567'
+>>> f.clean('0101-12345678')
+u'0101-12345678'
+>>> f.clean('0101-1234567')
+u'0101-1234567'
+>>> f.clean('01x-12345678')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid phone number.']
+>>> f.clean('01123-12345678')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid phone number.']
+>>> f.clean('010-123456789')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid phone number.']
+>>> f.clean('010-12345678-020')
+u'010-12345678-020'
+>>> f.clean('010-12345678-')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid phone number.']
+
+############################################################
+##################### CNCellNumberField ####################
+############################################################
+>>> from django.contrib.localflavor.cn.forms import CNCellNumberField
+>>> f = CNCellNumberField(required=False)
+>>> f.clean('')
+u''
+>>> f.clean('13012345678')
+u'13012345678'
+>>> f.clean('130123456789')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid cell number.']
+>>> f.clean('14012345678')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid cell number.']
+"""
Index: tests/regressiontests/forms/tests.py
===================================================================
--- tests/regressiontests/forms/tests.py (revision 11866)
+++ tests/regressiontests/forms/tests.py (working copy)
@@ -9,6 +9,7 @@
from localflavor.ca import tests as localflavor_ca_tests
from localflavor.ch import tests as localflavor_ch_tests
from localflavor.cl import tests as localflavor_cl_tests
+from localflavor.cn import tests as localflavor_cn_tests
from localflavor.cz import tests as localflavor_cz_tests
from localflavor.de import tests as localflavor_de_tests
from localflavor.es import tests as localflavor_es_tests
@@ -44,6 +45,7 @@
'localflavor_ca_tests': localflavor_ca_tests,
'localflavor_ch_tests': localflavor_ch_tests,
'localflavor_cl_tests': localflavor_cl_tests,
+ 'localflavor_cn_tests': localflavor_cn_tests,
'localflavor_cz_tests': localflavor_cz_tests,
'localflavor_de_tests': localflavor_de_tests,
'localflavor_es_tests': localflavor_es_tests,
Index: AUTHORS
===================================================================
--- AUTHORS (revision 11866)
+++ AUTHORS (working copy)
@@ -197,6 +197,7 @@
Brant Harris
Hawkeye
Joe Heck
+ Xia Kai
Joel Heenan
Mikko Hellsing
Sebastian Hillig
Index: docs/ref/contrib/localflavor.txt
===================================================================
--- docs/ref/contrib/localflavor.txt (revision 11866)
+++ docs/ref/contrib/localflavor.txt (working copy)
@@ -44,6 +44,7 @@
* Brazil_
* Canada_
* Chile_
+ * China_
* Czech_
* Finland_
* France_
@@ -84,6 +85,7 @@
.. _Brazil: `Brazil (br)`_
.. _Canada: `Canada (ca)`_
.. _Chile: `Chile (cl)`_
+.. _China: `China (cn)`_
.. _Czech: `Czech (cz)`_
.. _Finland: `Finland (fi)`_
.. _France: `France (fr)`_
@@ -233,6 +235,35 @@
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``)
==============