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 15KB

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