forms.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. # -*- coding: utf-8 -*-
  2. """
  3. Chinese-specific form helpers
  4. """
  5. from __future__ import absolute_import, unicode_literals
  6. import re
  7. from django.contrib.localflavor.cn.cn_provinces import CN_PROVINCE_CHOICES
  8. from django.forms import ValidationError
  9. from django.forms.fields import CharField, RegexField, Select
  10. from django.utils.translation import ugettext_lazy as _
  11. __all__ = (
  12. 'CNProvinceSelect',
  13. 'CNPostCodeField',
  14. 'CNIDCardField',
  15. 'CNPhoneNumberField',
  16. 'CNCellNumberField',
  17. )
  18. ID_CARD_RE = r'^\d{15}(\d{2}[0-9xX])?$'
  19. POST_CODE_RE = r'^\d{6}$'
  20. PHONE_RE = r'^\d{3,4}-\d{7,8}(-\d+)?$'
  21. CELL_RE = r'^1[358]\d{9}$'
  22. # Valid location code used in id card checking algorithm
  23. CN_LOCATION_CODES = (
  24. 11, # Beijing
  25. 12, # Tianjin
  26. 13, # Hebei
  27. 14, # Shanxi
  28. 15, # Nei Mongol
  29. 21, # Liaoning
  30. 22, # Jilin
  31. 23, # Heilongjiang
  32. 31, # Shanghai
  33. 32, # Jiangsu
  34. 33, # Zhejiang
  35. 34, # Anhui
  36. 35, # Fujian
  37. 36, # Jiangxi
  38. 37, # Shandong
  39. 41, # Henan
  40. 42, # Hubei
  41. 43, # Hunan
  42. 44, # Guangdong
  43. 45, # Guangxi
  44. 46, # Hainan
  45. 50, # Chongqing
  46. 51, # Sichuan
  47. 52, # Guizhou
  48. 53, # Yunnan
  49. 54, # Xizang
  50. 61, # Shaanxi
  51. 62, # Gansu
  52. 63, # Qinghai
  53. 64, # Ningxia
  54. 65, # Xinjiang
  55. 71, # Taiwan
  56. 81, # Hong Kong
  57. 91, # Macao
  58. )
  59. class CNProvinceSelect(Select):
  60. """
  61. A select widget with list of Chinese provinces as choices.
  62. """
  63. def __init__(self, attrs=None):
  64. super(CNProvinceSelect, self).__init__(
  65. attrs, choices=CN_PROVINCE_CHOICES,
  66. )
  67. class CNPostCodeField(RegexField):
  68. """
  69. A form field that validates as Chinese post code.
  70. Valid code is XXXXXX where X is digit.
  71. """
  72. default_error_messages = {
  73. 'invalid': _('Enter a post code in the format XXXXXX.'),
  74. }
  75. def __init__(self, *args, **kwargs):
  76. super(CNPostCodeField, self).__init__(POST_CODE_RE, *args, **kwargs)
  77. class CNIDCardField(CharField):
  78. """
  79. A form field that validates as Chinese Identification Card Number.
  80. This field would check the following restrictions:
  81. * the length could only be 15 or 18.
  82. * if the length is 18, the last digit could be x or X.
  83. * has a valid checksum.(length 18 only)
  84. * has a valid birthdate.
  85. * has a valid location.
  86. The checksum algorithm is described in GB11643-1999.
  87. """
  88. default_error_messages = {
  89. 'invalid': _('ID Card Number consists of 15 or 18 digits.'),
  90. 'checksum': _('Invalid ID Card Number: Wrong checksum'),
  91. 'birthday': _('Invalid ID Card Number: Wrong birthdate'),
  92. 'location': _('Invalid ID Card Number: Wrong location code'),
  93. }
  94. def __init__(self, max_length=18, min_length=15, *args, **kwargs):
  95. super(CNIDCardField, self).__init__(max_length, min_length, *args,
  96. **kwargs)
  97. def clean(self, value):
  98. """
  99. Check whether the input is a valid ID Card Number.
  100. """
  101. # Check the length of the ID card number.
  102. super(CNIDCardField, self).clean(value)
  103. if not value:
  104. return ""
  105. # Check whether this ID card number has valid format
  106. if not re.match(ID_CARD_RE, value):
  107. raise ValidationError(self.error_messages['invalid'])
  108. # Check the birthday of the ID card number.
  109. if not self.has_valid_birthday(value):
  110. raise ValidationError(self.error_messages['birthday'])
  111. # Check the location of the ID card number.
  112. if not self.has_valid_location(value):
  113. raise ValidationError(self.error_messages['location'])
  114. # Check the checksum of the ID card number.
  115. value = value.upper()
  116. if not self.has_valid_checksum(value):
  117. raise ValidationError(self.error_messages['checksum'])
  118. return '%s' % value
  119. def has_valid_birthday(self, value):
  120. """
  121. This function would grab the birthdate from the ID card number and test
  122. whether it is a valid date.
  123. """
  124. from datetime import datetime
  125. if len(value) == 15:
  126. # 1st generation ID card
  127. time_string = value[6:12]
  128. format_string = "%y%m%d"
  129. else:
  130. # 2nd generation ID card
  131. time_string = value[6:14]
  132. format_string = "%Y%m%d"
  133. try:
  134. datetime.strptime(time_string, format_string)
  135. return True
  136. except ValueError:
  137. # invalid date
  138. return False
  139. def has_valid_location(self, value):
  140. """
  141. This method checks if the first two digits in the ID Card are valid.
  142. """
  143. return int(value[:2]) in CN_LOCATION_CODES
  144. def has_valid_checksum(self, value):
  145. """
  146. This method checks if the last letter/digit in value is valid
  147. according to the algorithm the ID Card follows.
  148. """
  149. # If the length of the number is not 18, then the number is a 1st
  150. # generation ID card number, and there is no checksum to be checked.
  151. if len(value) != 18:
  152. return True
  153. checksum_index = sum(
  154. map(
  155. lambda a,b:a*(ord(b)-ord('0')),
  156. (7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2),
  157. value[:17],
  158. ),
  159. ) % 11
  160. return '10X98765432'[checksum_index] == value[-1]
  161. class CNPhoneNumberField(RegexField):
  162. """
  163. A form field that validates as Chinese phone number
  164. A valid phone number could be like:
  165. 010-55555555
  166. Considering there might be extension phone numbers, so this could also be:
  167. 010-55555555-35
  168. """
  169. default_error_messages = {
  170. 'invalid': _('Enter a valid phone number.'),
  171. }
  172. def __init__(self, *args, **kwargs):
  173. super(CNPhoneNumberField, self).__init__(PHONE_RE, *args, **kwargs)
  174. class CNCellNumberField(RegexField):
  175. """
  176. A form field that validates as Chinese cell number
  177. A valid cell number could be like:
  178. 13012345678
  179. We used a rough rule here, the first digit should be 1, the second could be
  180. 3, 5 and 8, the rest could be what so ever.
  181. The length of the cell number should be 11.
  182. """
  183. default_error_messages = {
  184. 'invalid': _('Enter a valid cell number.'),
  185. }
  186. def __init__(self, *args, **kwargs):
  187. super(CNCellNumberField, self).__init__(CELL_RE, *args, **kwargs)