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.

views.py 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import datetime
  2. import json
  3. from django.views.generic import ListView, FormView, DeleteView
  4. from django.core.urlresolvers import reverse
  5. from django.contrib import messages
  6. from django.http import HttpResponseRedirect
  7. from django.shortcuts import get_object_or_404
  8. from django.utils.translation import ugettext_lazy as _
  9. from django.core import serializers
  10. from django.core.serializers.json import DjangoJSONEncoder
  11. from oscar.core.loading import get_classes, get_class, get_model
  12. from oscar.views import sort_queryset
  13. ConditionalOffer = get_model('offer', 'ConditionalOffer')
  14. Condition = get_model('offer', 'Condition')
  15. Range = get_model('offer', 'Range')
  16. Product = get_model('catalogue', 'Product')
  17. OrderDiscount = get_model('order', 'OrderDiscount')
  18. Benefit = get_model('offer', 'Benefit')
  19. MetaDataForm, ConditionForm, BenefitForm, RestrictionsForm, OfferSearchForm \
  20. = get_classes('dashboard.offers.forms',
  21. ['MetaDataForm', 'ConditionForm', 'BenefitForm',
  22. 'RestrictionsForm', 'OfferSearchForm'])
  23. OrderDiscountCSVFormatter = get_class(
  24. 'dashboard.offers.reports', 'OrderDiscountCSVFormatter')
  25. class OfferListView(ListView):
  26. model = ConditionalOffer
  27. context_object_name = 'offers'
  28. template_name = 'dashboard/offers/offer_list.html'
  29. form_class = OfferSearchForm
  30. def get_queryset(self):
  31. qs = self.model._default_manager.exclude(
  32. offer_type=ConditionalOffer.VOUCHER)
  33. qs = sort_queryset(qs, self.request,
  34. ['name', 'start_datetime', 'end_datetime',
  35. 'num_applications', 'total_discount'])
  36. self.description = _("All offers")
  37. # We track whether the queryset is filtered to determine whether we
  38. # show the search form 'reset' button.
  39. self.is_filtered = False
  40. self.form = self.form_class(self.request.GET)
  41. if not self.form.is_valid():
  42. return qs
  43. data = self.form.cleaned_data
  44. if data['name']:
  45. qs = qs.filter(name__icontains=data['name'])
  46. self.description = _("Offers matching '%s'") % data['name']
  47. self.is_filtered = True
  48. if data['is_active']:
  49. self.is_filtered = True
  50. today = datetime.date.today()
  51. qs = qs.filter(start_date__lte=today, end_date__gte=today)
  52. return qs
  53. def get_context_data(self, **kwargs):
  54. ctx = super(OfferListView, self).get_context_data(**kwargs)
  55. ctx['queryset_description'] = self.description
  56. ctx['form'] = self.form
  57. ctx['is_filtered'] = self.is_filtered
  58. return ctx
  59. class OfferWizardStepView(FormView):
  60. wizard_name = 'offer_wizard'
  61. form_class = None
  62. step_name = None
  63. update = False
  64. url_name = None
  65. # Keep a reference to previous view class to allow checks to be made on
  66. # whether prior steps have been completed
  67. previous_view = None
  68. def dispatch(self, request, *args, **kwargs):
  69. if self.update:
  70. self.offer = get_object_or_404(ConditionalOffer, id=kwargs['pk'])
  71. if not self.is_previous_step_complete(request):
  72. messages.warning(
  73. request, _("%s step not complete") % (
  74. self.previous_view.step_name.title(),))
  75. return HttpResponseRedirect(self.get_back_url())
  76. return super(OfferWizardStepView, self).dispatch(request, *args,
  77. **kwargs)
  78. def is_previous_step_complete(self, request):
  79. if not self.previous_view:
  80. return True
  81. return self.previous_view.is_valid(self, request)
  82. def _key(self, step_name=None, is_object=False):
  83. key = step_name if step_name else self.step_name
  84. if self.update:
  85. key += str(self.offer.id)
  86. if is_object:
  87. key += '_obj'
  88. return key
  89. def _store_form_kwargs(self, form):
  90. session_data = self.request.session.setdefault(self.wizard_name, {})
  91. # Adjust kwargs to avoid trying to save the range instance
  92. form_data = form.cleaned_data.copy()
  93. range = form_data.get('range', None)
  94. if range is not None:
  95. form_data['range_id'] = range.id
  96. del form_data['range']
  97. form_kwargs = {'data': form_data}
  98. json_data = json.dumps(form_kwargs, cls=DjangoJSONEncoder)
  99. session_data[self._key()] = json_data
  100. self.request.session.save()
  101. def _fetch_form_kwargs(self, step_name=None):
  102. if not step_name:
  103. step_name = self.step_name
  104. session_data = self.request.session.setdefault(self.wizard_name, {})
  105. json_data = session_data.get(self._key(step_name), None)
  106. if json_data:
  107. form_kwargs = json.loads(json_data)
  108. if 'range_id' in form_kwargs['data']:
  109. form_kwargs['data']['range'] = Range.objects.get(
  110. id=form_kwargs['data']['range_id'])
  111. del form_kwargs['data']['range_id']
  112. return form_kwargs
  113. return {}
  114. def _store_object(self, form):
  115. session_data = self.request.session.setdefault(self.wizard_name, {})
  116. # We don't store the object instance as that is not JSON serialisable.
  117. # Instead, we save an alternative form
  118. instance = form.save(commit=False)
  119. json_qs = serializers.serialize('json', [instance])
  120. session_data[self._key(is_object=True)] = json_qs
  121. self.request.session.save()
  122. def _fetch_object(self, step_name, request=None):
  123. if request is None:
  124. request = self.request
  125. session_data = request.session.setdefault(self.wizard_name, {})
  126. json_qs = session_data.get(self._key(step_name, is_object=True), None)
  127. if json_qs:
  128. # Recreate model instance from passed data
  129. deserialised_obj = list(serializers.deserialize('json', json_qs))
  130. return deserialised_obj[0].object
  131. def _fetch_session_offer(self):
  132. """
  133. Return the offer instance loaded with the data stored in the
  134. session. When updating an offer, the updated fields are used with the
  135. existing offer data.
  136. """
  137. offer = self._fetch_object('metadata')
  138. if offer is None and self.update:
  139. offer = self.offer
  140. if offer is not None:
  141. condition = self._fetch_object('condition')
  142. if condition:
  143. offer.condition = condition
  144. benefit = self._fetch_object('benefit')
  145. if benefit:
  146. offer.benefit = benefit
  147. return offer
  148. def _flush_session(self):
  149. self.request.session[self.wizard_name] = {}
  150. self.request.session.save()
  151. def get_form_kwargs(self, *args, **kwargs):
  152. form_kwargs = {}
  153. if self.update:
  154. form_kwargs['instance'] = self.get_instance()
  155. session_kwargs = self._fetch_form_kwargs()
  156. form_kwargs.update(session_kwargs)
  157. parent_kwargs = super(OfferWizardStepView, self).get_form_kwargs(
  158. *args, **kwargs)
  159. form_kwargs.update(parent_kwargs)
  160. return form_kwargs
  161. def get_context_data(self, **kwargs):
  162. ctx = super(OfferWizardStepView, self).get_context_data(**kwargs)
  163. if self.update:
  164. ctx['offer'] = self.offer
  165. ctx['session_offer'] = self._fetch_session_offer()
  166. ctx['title'] = self.get_title()
  167. return ctx
  168. def get_back_url(self):
  169. if not self.previous_view:
  170. return None
  171. if self.update:
  172. return reverse(self.previous_view.url_name,
  173. kwargs={'pk': self.kwargs['pk']})
  174. return reverse(self.previous_view.url_name)
  175. def get_title(self):
  176. return self.step_name.title()
  177. def form_valid(self, form):
  178. self._store_form_kwargs(form)
  179. self._store_object(form)
  180. if self.update and 'save' in form.data:
  181. # Save changes to this offer when updating and pressed save button
  182. return self.save_offer(self.offer)
  183. else:
  184. # Proceed to next page
  185. return super(OfferWizardStepView, self).form_valid(form)
  186. def save_offer(self, offer):
  187. # We update the offer with the name/description from step 1
  188. session_offer = self._fetch_session_offer()
  189. offer.name = session_offer.name
  190. offer.description = session_offer.description
  191. # Working around a strange Django issue where saving the related model
  192. # in place does not register it correctly and so it has to be saved and
  193. # reassigned.
  194. benefit = session_offer.benefit
  195. benefit.save()
  196. condition = session_offer.condition
  197. condition.save()
  198. offer.benefit = benefit
  199. offer.condition = condition
  200. offer.save()
  201. self._flush_session()
  202. if self.update:
  203. msg = _("Offer '%s' updated") % offer.name
  204. else:
  205. msg = _("Offer '%s' created!") % offer.name
  206. messages.success(self.request, msg)
  207. return HttpResponseRedirect(reverse(
  208. 'dashboard:offer-detail', kwargs={'pk': offer.pk}))
  209. def get_success_url(self):
  210. if self.update:
  211. return reverse(self.success_url_name,
  212. kwargs={'pk': self.kwargs['pk']})
  213. return reverse(self.success_url_name)
  214. @classmethod
  215. def is_valid(cls, current_view, request):
  216. if current_view.update:
  217. return True
  218. return current_view._fetch_object(cls.step_name, request) is not None
  219. class OfferMetaDataView(OfferWizardStepView):
  220. step_name = 'metadata'
  221. form_class = MetaDataForm
  222. template_name = 'dashboard/offers/metadata_form.html'
  223. url_name = 'dashboard:offer-metadata'
  224. success_url_name = 'dashboard:offer-benefit'
  225. def get_instance(self):
  226. return self.offer
  227. def get_title(self):
  228. return _("Name and description")
  229. class OfferBenefitView(OfferWizardStepView):
  230. step_name = 'benefit'
  231. form_class = BenefitForm
  232. template_name = 'dashboard/offers/benefit_form.html'
  233. url_name = 'dashboard:offer-benefit'
  234. success_url_name = 'dashboard:offer-condition'
  235. previous_view = OfferMetaDataView
  236. def get_instance(self):
  237. return self.offer.benefit
  238. def get_title(self):
  239. # This is referred to as the 'incentive' within the dashboard.
  240. return _("Incentive")
  241. class OfferConditionView(OfferWizardStepView):
  242. step_name = 'condition'
  243. form_class = ConditionForm
  244. template_name = 'dashboard/offers/condition_form.html'
  245. url_name = 'dashboard:offer-condition'
  246. success_url_name = 'dashboard:offer-restrictions'
  247. previous_view = OfferBenefitView
  248. def get_instance(self):
  249. return self.offer.condition
  250. class OfferRestrictionsView(OfferWizardStepView):
  251. step_name = 'restrictions'
  252. form_class = RestrictionsForm
  253. template_name = 'dashboard/offers/restrictions_form.html'
  254. previous_view = OfferConditionView
  255. url_name = 'dashboard:offer-restrictions'
  256. def form_valid(self, form):
  257. offer = form.save(commit=False)
  258. return self.save_offer(offer)
  259. def get_instance(self):
  260. return self.offer
  261. def get_title(self):
  262. return _("Restrictions")
  263. class OfferDeleteView(DeleteView):
  264. model = ConditionalOffer
  265. template_name = 'dashboard/offers/offer_delete.html'
  266. context_object_name = 'offer'
  267. def get_success_url(self):
  268. messages.success(self.request, _("Offer deleted!"))
  269. return reverse('dashboard:offer-list')
  270. class OfferDetailView(ListView):
  271. # Slightly odd, but we treat the offer detail view as a list view so the
  272. # order discounts can be browsed.
  273. model = OrderDiscount
  274. template_name = 'dashboard/offers/offer_detail.html'
  275. context_object_name = 'order_discounts'
  276. def dispatch(self, request, *args, **kwargs):
  277. self.offer = get_object_or_404(ConditionalOffer, pk=kwargs['pk'])
  278. return super(OfferDetailView, self).dispatch(request, *args, **kwargs)
  279. def post(self, request, *args, **kwargs):
  280. if 'suspend' in request.POST:
  281. return self.suspend()
  282. elif 'unsuspend' in request.POST:
  283. return self.unsuspend()
  284. def suspend(self):
  285. if self.offer.is_suspended:
  286. messages.error(self.request, _("Offer is already suspended"))
  287. else:
  288. self.offer.suspend()
  289. messages.success(self.request, _("Offer suspended"))
  290. return HttpResponseRedirect(
  291. reverse('dashboard:offer-detail', kwargs={'pk': self.offer.pk}))
  292. def unsuspend(self):
  293. if not self.offer.is_suspended:
  294. messages.error(
  295. self.request,
  296. _("Offer cannot be reinstated as it is not currently "
  297. "suspended"))
  298. else:
  299. self.offer.unsuspend()
  300. messages.success(self.request, _("Offer reinstated"))
  301. return HttpResponseRedirect(
  302. reverse('dashboard:offer-detail', kwargs={'pk': self.offer.pk}))
  303. def get_queryset(self):
  304. return self.model.objects.filter(offer_id=self.offer.pk)
  305. def get_context_data(self, **kwargs):
  306. ctx = super(OfferDetailView, self).get_context_data(**kwargs)
  307. ctx['offer'] = self.offer
  308. return ctx
  309. def render_to_response(self, context):
  310. if self.request.GET.get('format') == 'csv':
  311. formatter = OrderDiscountCSVFormatter()
  312. return formatter.generate_response(context['order_discounts'],
  313. offer=self.offer)
  314. return super(OfferDetailView, self).render_to_response(context)