Source code for localflavor.cn.forms

"""
China(mainland)-specific Form helpers
"""

from __future__ import unicode_literals

import re

from django.forms import ValidationError
from django.forms.fields import CharField, RegexField, Select
from django.utils.translation import ugettext_lazy as _

from .cn_provinces import CN_PROVINCE_CHOICES

__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[34578]\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
)


[docs]class CNProvinceSelect(Select): """ A select widget providing the list of provinces and districts in People's Republic of China as choices. """ def __init__(self, attrs=None): super(CNProvinceSelect, self).__init__(attrs, choices=CN_PROVINCE_CHOICES)
[docs]class CNPostCodeField(RegexField): """ A form field that validates input as postal codes in mainland China. Valid codes are in the format of XXXXXX where X is a 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)
[docs]class CNIDCardField(CharField): """ A form field that validates input as a Resident Identity Card (PRC) number. This field would check the following restrictions: * the length could only be 15 or 18; * if the length is 18, the last character can be x or X; * has a valid checksum (only for those with a length of 18); * has a valid date of birth; * has a valid province. The checksum algorithm is described in GB11643-1999. See: http://en.wikipedia.org/wiki/Resident_Identity_Card#Identity_card_number """ 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)
[docs] 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
[docs] def has_valid_birthday(self, value): """ This method would grab the date of birth 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
[docs] def has_valid_location(self, value): """ This method checks if the first two digits in the ID Card are valid province code. """ return int(value[:2]) in CN_LOCATION_CODES
[docs] def has_valid_checksum(self, value): """ This method checks if the last letter/digit is valid according to GB11643-1999. """ # 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]
[docs]class CNPhoneNumberField(RegexField): """ A form field that validates input as a telephone number in mainland China. A valid phone number could be like: 010-12345678. Considering there might be extension numbers, this could also be: 010-12345678-35. """ default_error_messages = { 'invalid': _('Enter a valid phone number.'), } def __init__(self, *args, **kwargs): super(CNPhoneNumberField, self).__init__(PHONE_RE, *args, **kwargs)
[docs]class CNCellNumberField(RegexField): """ A form field that validates input as a cellphone number in mainland China. A valid cellphone number could be like: 13012345678. A very rough rule is used here: the first digit should be 1, the second should be 3, 4, 5, 7 or 8, followed by 9 more digits. The total length of a cellphone number should be 11. .. versionchanged:: 1.1 Added 7 as a valid second digit for Chinese virtual mobile ISPs. """ default_error_messages = { 'invalid': _('Enter a valid cell number.'), } def __init__(self, *args, **kwargs): super(CNCellNumberField, self).__init__(CELL_RE, *args, **kwargs)