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.

generic.py 7.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import json
  2. from django import forms
  3. from django.core import validators
  4. from django.core.exceptions import ValidationError
  5. from django.shortcuts import redirect
  6. from django.utils.encoding import smart_str
  7. from django.contrib import messages
  8. from django.http import HttpResponse
  9. from django.utils import six
  10. from django.utils.six.moves import map
  11. from django.utils.translation import ugettext_lazy as _
  12. from django.views.generic.base import View
  13. import phonenumbers
  14. from oscar.core.utils import safe_referrer
  15. from oscar.core.phonenumber import PhoneNumber
  16. class PostActionMixin(object):
  17. """
  18. Simple mixin to forward POST request that contain a key 'action'
  19. onto a method of form "do_{action}".
  20. This only works with DetailView
  21. """
  22. def post(self, request, *args, **kwargs):
  23. if 'action' in self.request.POST:
  24. model = self.get_object()
  25. # The do_* method is required to do what it needs to with the model
  26. # it is passed, and then to assign the HTTP response to
  27. # self.response.
  28. method_name = "do_%s" % self.request.POST['action'].lower()
  29. if hasattr(self, method_name):
  30. getattr(self, method_name)(model)
  31. return self.response
  32. else:
  33. messages.error(request, _("Invalid form submission"))
  34. return super(PostActionMixin, self).post(request, *args, **kwargs)
  35. class BulkEditMixin(object):
  36. """
  37. Mixin for views that have a bulk editing facility. This is normally in the
  38. form of tabular data where each row has a checkbox. The UI allows a number
  39. of rows to be selected and then some 'action' to be performed on them.
  40. """
  41. action_param = 'action'
  42. # Permitted methods that can be used to act on the selected objects
  43. actions = None
  44. checkbox_object_name = None
  45. def get_checkbox_object_name(self):
  46. if self.checkbox_object_name:
  47. return self.checkbox_object_name
  48. return smart_str(self.model._meta.object_name.lower())
  49. def get_error_url(self, request):
  50. return safe_referrer(request.META, '.')
  51. def get_success_url(self, request):
  52. return safe_referrer(request.META, '.')
  53. def post(self, request, *args, **kwargs):
  54. # Dynamic dispatch pattern - we forward POST requests onto a method
  55. # designated by the 'action' parameter. The action has to be in a
  56. # whitelist to avoid security issues.
  57. action = request.POST.get(self.action_param, '').lower()
  58. if not self.actions or action not in self.actions:
  59. messages.error(self.request, _("Invalid action"))
  60. return redirect(self.get_error_url(request))
  61. ids = request.POST.getlist(
  62. 'selected_%s' % self.get_checkbox_object_name())
  63. ids = list(map(int, ids))
  64. if not ids:
  65. messages.error(
  66. self.request,
  67. _("You need to select some %ss")
  68. % self.get_checkbox_object_name())
  69. return redirect(self.get_error_url(request))
  70. objects = self.get_objects(ids)
  71. return getattr(self, action)(request, objects)
  72. def get_objects(self, ids):
  73. object_dict = self.get_object_dict(ids)
  74. # Rearrange back into the original order
  75. return [object_dict[id] for id in ids]
  76. def get_object_dict(self, ids):
  77. return self.get_queryset().in_bulk(ids)
  78. class ObjectLookupView(View):
  79. """Base view for json lookup for objects"""
  80. def get_queryset(self):
  81. return self.model.objects.all()
  82. def format_object(self, obj):
  83. return {
  84. 'id': obj.pk,
  85. 'text': six.text_type(obj),
  86. }
  87. def initial_filter(self, qs, value):
  88. return qs.filter(pk__in=value.split(','))
  89. def lookup_filter(self, qs, term):
  90. return qs
  91. def paginate(self, qs, page, page_limit):
  92. total = qs.count()
  93. start = (page - 1) * page_limit
  94. stop = start + page_limit
  95. qs = qs[start:stop]
  96. return qs, (page_limit * page < total)
  97. def get_args(self):
  98. GET = self.request.GET
  99. return (GET.get('initial', None),
  100. GET.get('q', None),
  101. int(GET.get('page', 1)),
  102. int(GET.get('page_limit', 20)))
  103. def get(self, request):
  104. self.request = request
  105. qs = self.get_queryset()
  106. initial, q, page, page_limit = self.get_args()
  107. if initial:
  108. qs = self.initial_filter(qs, initial)
  109. more = False
  110. else:
  111. if q:
  112. qs = self.lookup_filter(qs, q)
  113. qs, more = self.paginate(qs, page, page_limit)
  114. return HttpResponse(json.dumps({
  115. 'results': [self.format_object(obj) for obj in qs],
  116. 'more': more,
  117. }), content_type='application/json')
  118. class PhoneNumberMixin(object):
  119. """
  120. Validation mixin for forms with a phone number, and optionally a country.
  121. It tries to validate the phone number, and on failure tries to validate it
  122. using a hint (the country provided), and treating it as a local number.
  123. It looks for ``self.country``, or ``self.fields['country'].queryset``
  124. """
  125. phone_number = forms.CharField(max_length=32, required=False)
  126. def get_country(self):
  127. if hasattr(self.instance, 'country'):
  128. return self.instance.country
  129. if hasattr(self.fields.get('country'), 'queryset'):
  130. return self.fields['country'].queryset[0]
  131. return self.cleaned_data.get('country')
  132. def get_region_code(self, country):
  133. return country.iso_3166_1_a2
  134. def clean_phone_number(self):
  135. number = self.cleaned_data['phone_number']
  136. # empty
  137. if number in validators.EMPTY_VALUES:
  138. return None
  139. # Check for an international phone format
  140. try:
  141. phone_number = PhoneNumber.from_string(number)
  142. except phonenumbers.NumberParseException:
  143. # Try hinting with the shipping country
  144. country = self.get_country()
  145. if not country:
  146. # There is no shipping country, not a valid international
  147. # number
  148. raise ValidationError(
  149. _(u'This is not a valid international phone format.'))
  150. region_code = self.get_region_code(country)
  151. # The PhoneNumber class does not allow specifying
  152. # the region. So we drop down to the underlying phonenumbers
  153. # library, which luckily allows parsing into a PhoneNumber
  154. # instance
  155. try:
  156. phone_number = PhoneNumber.from_string(number,
  157. region=region_code)
  158. if not phone_number.is_valid():
  159. raise ValidationError(
  160. _(u'This is not a valid local phone format for %s.')
  161. % country)
  162. except phonenumbers.NumberParseException:
  163. # Not a valid local or international phone number
  164. raise ValidationError(
  165. _(u'This is not a valid local or international phone'
  166. u' format.'))
  167. return phone_number