You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

forms.py 9.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. from datetime import date
  2. from calendar import monthrange
  3. import re
  4. import six
  5. from django import forms
  6. from django.core.exceptions import ImproperlyConfigured
  7. from django.utils.translation import ugettext_lazy as _
  8. from oscar.core.loading import get_model
  9. from oscar.apps.address.forms import AbstractAddressForm
  10. from oscar.views.generic import PhoneNumberMixin
  11. from . import bankcards
  12. Country = get_model('address', 'Country')
  13. BillingAddress = get_model('order', 'BillingAddress')
  14. Bankcard = get_model('payment', 'Bankcard')
  15. # List of card names for all the card types supported in payment.bankcards
  16. VALID_CARDS = set([card_type[0] for card_type in bankcards.CARD_TYPES])
  17. class BankcardNumberField(forms.CharField):
  18. def __init__(self, *args, **kwargs):
  19. _kwargs = {
  20. 'max_length': 20,
  21. 'widget': forms.TextInput(attrs={'autocomplete': 'off'}),
  22. 'label': _("Card number")
  23. }
  24. if 'types' in kwargs:
  25. self.accepted_cards = set(kwargs.pop('types'))
  26. difference = self.accepted_cards - VALID_CARDS
  27. if difference:
  28. raise ImproperlyConfigured('The following accepted_cards are '
  29. 'unknown: %s' % difference)
  30. _kwargs.update(kwargs)
  31. super(BankcardNumberField, self).__init__(*args, **_kwargs)
  32. def clean(self, value):
  33. """
  34. Check if given CC number is valid and one of the
  35. card types we accept
  36. """
  37. non_decimal = re.compile(r'\D+')
  38. value = non_decimal.sub('', value.strip())
  39. if value and not bankcards.luhn(value):
  40. raise forms.ValidationError(
  41. _("Please enter a valid credit card number."))
  42. if hasattr(self, 'accepted_cards'):
  43. card_type = bankcards.bankcard_type(value)
  44. if card_type not in self.accepted_cards:
  45. raise forms.ValidationError(
  46. _("%s cards are not accepted." % card_type))
  47. return super(BankcardNumberField, self).clean(value)
  48. class BankcardMonthWidget(forms.MultiWidget):
  49. """
  50. Widget containing two select boxes for selecting the month and year
  51. """
  52. def decompress(self, value):
  53. return [value.month, value.year] if value else [None, None]
  54. def format_output(self, rendered_widgets):
  55. html = u' '.join(rendered_widgets)
  56. return u'<span style="white-space: nowrap">%s</span>' % html
  57. class BankcardMonthField(forms.MultiValueField):
  58. """
  59. A modified version of the snippet: http://djangosnippets.org/snippets/907/
  60. """
  61. default_error_messages = {
  62. 'invalid_month': _('Enter a valid month.'),
  63. 'invalid_year': _('Enter a valid year.'),
  64. }
  65. num_years = 5
  66. def __init__(self, *args, **kwargs):
  67. # Allow the number of years to be specified
  68. if 'num_years' in kwargs:
  69. self.num_years = kwargs.pop('num_years')
  70. errors = self.default_error_messages.copy()
  71. if 'error_messages' in kwargs:
  72. errors.update(kwargs['error_messages'])
  73. fields = (
  74. forms.ChoiceField(
  75. choices=self.month_choices(),
  76. error_messages={'invalid': errors['invalid_month']}),
  77. forms.ChoiceField(
  78. choices=self.year_choices(),
  79. error_messages={'invalid': errors['invalid_year']}),
  80. )
  81. if 'widget' not in kwargs:
  82. kwargs['widget'] = BankcardMonthWidget(
  83. widgets=[fields[0].widget, fields[1].widget])
  84. super(BankcardMonthField, self).__init__(fields, *args, **kwargs)
  85. def month_choices(self):
  86. return []
  87. def year_choices(self):
  88. return []
  89. class BankcardExpiryMonthField(BankcardMonthField):
  90. num_years = 10
  91. def __init__(self, *args, **kwargs):
  92. today = date.today()
  93. _kwargs = {
  94. 'required': True,
  95. 'label': _("Valid to"),
  96. 'initial': ["%.2d" % today.month, today.year]
  97. }
  98. _kwargs.update(kwargs)
  99. super(BankcardExpiryMonthField, self).__init__(*args, **_kwargs)
  100. def month_choices(self):
  101. return [("%.2d" % x, "%.2d" % x) for x in six.moves.xrange(1, 13)]
  102. def year_choices(self):
  103. return [(x, x) for x in six.moves.xrange(
  104. date.today().year,
  105. date.today().year + self.num_years)]
  106. def clean(self, value):
  107. expiry_date = super(BankcardExpiryMonthField, self).clean(value)
  108. if date.today() > expiry_date:
  109. raise forms.ValidationError(
  110. _("The expiration date you entered is in the past."))
  111. return expiry_date
  112. def compress(self, data_list):
  113. if data_list:
  114. if data_list[1] in forms.fields.EMPTY_VALUES:
  115. error = self.error_messages['invalid_year']
  116. raise forms.ValidationError(error)
  117. if data_list[0] in forms.fields.EMPTY_VALUES:
  118. error = self.error_messages['invalid_month']
  119. raise forms.ValidationError(error)
  120. year = int(data_list[1])
  121. month = int(data_list[0])
  122. # find last day of the month
  123. day = monthrange(year, month)[1]
  124. return date(year, month, day)
  125. return None
  126. class BankcardStartingMonthField(BankcardMonthField):
  127. def __init__(self, *args, **kwargs):
  128. _kwargs = {
  129. 'required': False,
  130. 'label': _("Valid from"),
  131. }
  132. _kwargs.update(kwargs)
  133. super(BankcardStartingMonthField, self).__init__(*args, **_kwargs)
  134. def month_choices(self):
  135. months = [("%.2d" % x, "%.2d" % x) for x in six.moves.xrange(1, 13)]
  136. months.insert(0, ("", "--"))
  137. return months
  138. def year_choices(self):
  139. today = date.today()
  140. years = [(x, x) for x in six.moves.xrange(
  141. today.year - self.num_years,
  142. today.year + 1)]
  143. years.insert(0, ("", "--"))
  144. return years
  145. def clean(self, value):
  146. starting_date = super(BankcardMonthField, self).clean(value)
  147. if starting_date and date.today() < starting_date:
  148. raise forms.ValidationError(
  149. _("The starting date you entered is in the future."))
  150. return starting_date
  151. def compress(self, data_list):
  152. if data_list:
  153. if data_list[1] in forms.fields.EMPTY_VALUES:
  154. error = self.error_messages['invalid_year']
  155. raise forms.ValidationError(error)
  156. if data_list[0] in forms.fields.EMPTY_VALUES:
  157. error = self.error_messages['invalid_month']
  158. raise forms.ValidationError(error)
  159. year = int(data_list[1])
  160. month = int(data_list[0])
  161. return date(year, month, 1)
  162. return None
  163. class BankcardCCVField(forms.RegexField):
  164. def __init__(self, *args, **kwargs):
  165. _kwargs = {
  166. 'required': True,
  167. 'label': _("CCV number"),
  168. 'widget': forms.TextInput(attrs={'size': '5'}),
  169. 'error_message': _("Please enter a 3 or 4 digit number"),
  170. 'help_text': _("This is the 3 or 4 digit security number "
  171. "on the back of your bankcard")
  172. }
  173. _kwargs.update(kwargs)
  174. super(BankcardCCVField, self).__init__(
  175. r'^\d{3,4}$', *args, **_kwargs)
  176. def clean(self, value):
  177. if value is not None:
  178. value = value.strip()
  179. return super(BankcardCCVField, self).clean(value)
  180. class BankcardForm(forms.ModelForm):
  181. # By default, this number field will accept any number. The only validation
  182. # is whether it passes the luhn check. If you wish to only accept certain
  183. # types of card, you can pass a types kwarg to BankcardNumberField, e.g.
  184. #
  185. # BankcardNumberField(types=[bankcards.VISA, bankcards.VISA_ELECTRON,])
  186. number = BankcardNumberField()
  187. ccv = BankcardCCVField()
  188. start_month = BankcardStartingMonthField()
  189. expiry_month = BankcardExpiryMonthField()
  190. class Meta:
  191. model = Bankcard
  192. fields = ('number', 'start_month', 'expiry_month', 'ccv')
  193. def clean(self):
  194. data = self.cleaned_data
  195. number, ccv = data.get('number'), data.get('ccv')
  196. if number and ccv:
  197. if bankcards.is_amex(number) and len(ccv) != 4:
  198. raise forms.ValidationError(_(
  199. "American Express cards use a 4 digit security code"))
  200. return data
  201. def save(self, *args, **kwargs):
  202. # It doesn't really make sense to save directly from the form as saving
  203. # will obfuscate some of the card details which you normally need to
  204. # pass to a payment gateway. Better to use the bankcard property below
  205. # to get the cleaned up data, then once you've used the sensitive
  206. # details, you can save.
  207. raise RuntimeError("Don't save bankcards directly from form")
  208. @property
  209. def bankcard(self):
  210. """
  211. Return an instance of the Bankcard model (unsaved)
  212. """
  213. return Bankcard(number=self.cleaned_data['number'],
  214. expiry_date=self.cleaned_data['expiry_month'],
  215. start_date=self.cleaned_data['start_month'],
  216. ccv=self.cleaned_data['ccv'])
  217. class BillingAddressForm(PhoneNumberMixin, AbstractAddressForm):
  218. def __init__(self, *args, **kwargs):
  219. super(BillingAddressForm, self).__init__(*args, **kwargs)
  220. self.set_country_queryset()
  221. def set_country_queryset(self):
  222. self.fields['country'].queryset = Country._default_manager.all()
  223. class Meta:
  224. model = BillingAddress
  225. exclude = ('search_text',)