# -*- coding: utf-8 -*- """ Chinese-specific form helpers """ from __future__ import absolute_import, unicode_literals import re from django.contrib.localflavor.cn.cn_provinces import CN_PROVINCE_CHOICES 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): 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': _('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': _('ID Card Number consists of 15 or 18 digits.'), 'checksum': _('Invalid ID Card Number: Wrong checksum'), 'birthday': _('Invalid ID Card Number: Wrong birthdate'), 'location': _('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 "" # 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 '%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': _('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': _('Enter a valid cell number.'), } def __init__(self, *args, **kwargs): super(CNCellNumberField, self).__init__(CELL_RE, *args, **kwargs)