diff --git a/AUTHORS b/AUTHORS
index 4921f7c..b991c23 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -209,6 +209,7 @@ answer newbie questions, and generally made Django that much better:
Ronny Haryanto
Hawkeye
Joe Heck
+ Xia Kai
Joel Heenan
Mikko Hellsing
Sebastian Hillig
@@ -510,6 +511,7 @@ answer newbie questions, and generally made Django that much better:
Cheng Zhang
Glenn Maynard
bthomas
+ Daniel Duan
A big THANK YOU goes to:
diff --git a/django/contrib/localflavor/cn/cn_provinces.py b/django/contrib/localflavor/cn/cn_provinces.py
new file mode 100644
index 0000000..fe0aa37
--- /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 0000000..9749349
--- /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 1c58e2d..7d8a31c 100644
--- a/docs/ref/contrib/localflavor.txt
+++ b/docs/ref/contrib/localflavor.txt
@@ -44,6 +44,7 @@ Countries currently supported by :mod:`~django.contrib.localflavor` are:
* Brazil_
* Canada_
* Chile_
+ * China_
* Czech_
* Finland_
* France_
@@ -90,6 +91,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)`_
@@ -245,6 +247,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 0000000..c1cc27f
--- /dev/null
+++ b/tests/regressiontests/forms/localflavor/cn.py
@@ -0,0 +1,183 @@
+# Tests for contrib/localflavor/ CN Form Fields
+
+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('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.']
+"""
diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py
index 8757e79..2cef443 100644
--- a/tests/regressiontests/forms/tests.py
+++ b/tests/regressiontests/forms/tests.py
@@ -9,6 +9,7 @@ from localflavor.br import tests as localflavor_br_tests
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
@@ -52,6 +53,7 @@ __test__ = {
'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,