forms.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. """
  2. Canada-specific Form helpers
  3. """
  4. from __future__ import absolute_import, unicode_literals
  5. import re
  6. from django.core.validators import EMPTY_VALUES
  7. from django.forms import ValidationError
  8. from django.forms.fields import Field, CharField, Select
  9. from django.utils.encoding import smart_text
  10. from django.utils.translation import ugettext_lazy as _
  11. phone_digits_re = re.compile(r'^(?:1-?)?(\d{3})[-\.]?(\d{3})[-\.]?(\d{4})$')
  12. sin_re = re.compile(r"^(\d{3})-(\d{3})-(\d{3})$")
  13. class CAPostalCodeField(CharField):
  14. """
  15. Canadian postal code field.
  16. Validates against known invalid characters: D, F, I, O, Q, U
  17. Additionally the first character cannot be Z or W.
  18. For more info see:
  19. http://www.canadapost.ca/tools/pg/manual/PGaddress-e.asp#1402170
  20. """
  21. default_error_messages = {
  22. 'invalid': _('Enter a postal code in the format XXX XXX.'),
  23. }
  24. postcode_regex = re.compile(r'^([ABCEGHJKLMNPRSTVXY]\d[ABCEGHJKLMNPRSTVWXYZ]) *(\d[ABCEGHJKLMNPRSTVWXYZ]\d)$')
  25. def clean(self, value):
  26. value = super(CAPostalCodeField, self).clean(value)
  27. if value in EMPTY_VALUES:
  28. return ''
  29. postcode = value.upper().strip()
  30. m = self.postcode_regex.match(postcode)
  31. if not m:
  32. raise ValidationError(self.default_error_messages['invalid'])
  33. return "%s %s" % (m.group(1), m.group(2))
  34. class CAPhoneNumberField(Field):
  35. """Canadian phone number field."""
  36. default_error_messages = {
  37. 'invalid': _('Phone numbers must be in XXX-XXX-XXXX format.'),
  38. }
  39. def clean(self, value):
  40. """Validate a phone number.
  41. """
  42. super(CAPhoneNumberField, self).clean(value)
  43. if value in EMPTY_VALUES:
  44. return ''
  45. value = re.sub('(\(|\)|\s+)', '', smart_text(value))
  46. m = phone_digits_re.search(value)
  47. if m:
  48. return '%s-%s-%s' % (m.group(1), m.group(2), m.group(3))
  49. raise ValidationError(self.error_messages['invalid'])
  50. class CAProvinceField(Field):
  51. """
  52. A form field that validates its input is a Canadian province name or abbreviation.
  53. It normalizes the input to the standard two-leter postal service
  54. abbreviation for the given province.
  55. """
  56. default_error_messages = {
  57. 'invalid': _('Enter a Canadian province or territory.'),
  58. }
  59. def clean(self, value):
  60. super(CAProvinceField, self).clean(value)
  61. if value in EMPTY_VALUES:
  62. return ''
  63. try:
  64. value = value.strip().lower()
  65. except AttributeError:
  66. pass
  67. else:
  68. # Load data in memory only when it is required, see also #17275
  69. from .ca_provinces import PROVINCES_NORMALIZED
  70. try:
  71. return PROVINCES_NORMALIZED[value.strip().lower()]
  72. except KeyError:
  73. pass
  74. raise ValidationError(self.error_messages['invalid'])
  75. class CAProvinceSelect(Select):
  76. """
  77. A Select widget that uses a list of Canadian provinces and
  78. territories as its choices.
  79. """
  80. def __init__(self, attrs=None):
  81. # Load data in memory only when it is required, see also #17275
  82. from .ca_provinces import PROVINCE_CHOICES
  83. super(CAProvinceSelect, self).__init__(attrs, choices=PROVINCE_CHOICES)
  84. class CASocialInsuranceNumberField(Field):
  85. """
  86. A Canadian Social Insurance Number (SIN).
  87. Checks the following rules to determine whether the number is valid:
  88. * Conforms to the XXX-XXX-XXX format.
  89. * Passes the check digit process "Luhn Algorithm"
  90. See: http://en.wikipedia.org/wiki/Social_Insurance_Number
  91. """
  92. default_error_messages = {
  93. 'invalid': _('Enter a valid Canadian Social Insurance number in XXX-XXX-XXX format.'),
  94. }
  95. def clean(self, value):
  96. super(CASocialInsuranceNumberField, self).clean(value)
  97. if value in EMPTY_VALUES:
  98. return ''
  99. match = re.match(sin_re, value)
  100. if not match:
  101. raise ValidationError(self.error_messages['invalid'])
  102. number = '%s-%s-%s' % (match.group(1), match.group(2), match.group(3))
  103. check_number = '%s%s%s' % (match.group(1), match.group(2), match.group(3))
  104. if not self.luhn_checksum_is_valid(check_number):
  105. raise ValidationError(self.error_messages['invalid'])
  106. return number
  107. def luhn_checksum_is_valid(self, number):
  108. """
  109. Checks to make sure that the SIN passes a luhn mod-10 checksum
  110. See: http://en.wikipedia.org/wiki/Luhn_algorithm
  111. """
  112. sum = 0
  113. num_digits = len(number)
  114. oddeven = num_digits & 1
  115. for count in range(0, num_digits):
  116. digit = int(number[count])
  117. if not (( count & 1 ) ^ oddeven ):
  118. digit = digit * 2
  119. if digit > 9:
  120. digit = digit - 9
  121. sum = sum + digit
  122. return ( (sum % 10) == 0 )