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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. from urlparse import urlparse
  2. from django.contrib import messages
  3. from django.template.loader import render_to_string
  4. from django.template import RequestContext
  5. from django.core.urlresolvers import reverse, resolve
  6. from django.utils import simplejson as json
  7. from django.db.models import get_model
  8. from django.http import HttpResponseRedirect, Http404, HttpResponse
  9. from django.views.generic import FormView, View
  10. from django.utils.translation import ugettext_lazy as _
  11. from django.core.exceptions import ObjectDoesNotExist
  12. from extra_views import ModelFormSetView
  13. from oscar.core import ajax
  14. from oscar.apps.basket.signals import basket_addition, voucher_addition
  15. from oscar.templatetags.currency_filters import currency
  16. from oscar.core.loading import get_class, get_classes
  17. Applicator = get_class('offer.utils', 'Applicator')
  18. (BasketLineForm, AddToBasketForm, BasketVoucherForm,
  19. SavedLineFormSet, SavedLineForm, ProductSelectionForm) = get_classes(
  20. 'basket.forms', ('BasketLineForm', 'AddToBasketForm',
  21. 'BasketVoucherForm', 'SavedLineFormSet',
  22. 'SavedLineForm', 'ProductSelectionForm'))
  23. Repository = get_class('shipping.repository', ('Repository'))
  24. def get_messages(basket, offers_before, offers_after,
  25. include_buttons=True):
  26. """
  27. Return the messages about offer changes
  28. """
  29. # Look for changes in offers
  30. offers_lost = set(offers_before.keys()).difference(
  31. set(offers_after.keys()))
  32. offers_gained = set(offers_after.keys()).difference(
  33. set(offers_before.keys()))
  34. # Build a list of (level, msg) tuples
  35. offer_messages = []
  36. for offer_id in offers_lost:
  37. offer = offers_before[offer_id]
  38. msg = render_to_string(
  39. 'basket/messages/offer_lost.html',
  40. {'offer': offer})
  41. offer_messages.append((
  42. messages.WARNING, msg))
  43. for offer_id in offers_gained:
  44. offer = offers_after[offer_id]
  45. msg = render_to_string(
  46. 'basket/messages/offer_gained.html',
  47. {'offer': offer})
  48. offer_messages.append((
  49. messages.SUCCESS, msg))
  50. # We use the 'include_buttons' parameter to determine whether to show the
  51. # 'Checkout now' buttons. We don't want to show these on the basket page.
  52. msg = render_to_string(
  53. 'basket/messages/new_total.html',
  54. {'basket': basket,
  55. 'include_buttons': include_buttons})
  56. offer_messages.append((
  57. messages.INFO, msg))
  58. return offer_messages
  59. def apply_messages(request, offers_before):
  60. """
  61. Set flash messages triggered by changes to the basket
  62. """
  63. # Re-apply offers to see if any new ones are now available
  64. request.basket.reset_offer_applications()
  65. Applicator().apply(request, request.basket)
  66. offers_after = request.basket.applied_offers()
  67. for level, msg in get_messages(
  68. request.basket, offers_before, offers_after):
  69. messages.add_message(
  70. request, level, msg, extra_tags='safe noicon')
  71. class BasketView(ModelFormSetView):
  72. model = get_model('basket', 'Line')
  73. basket_model = get_model('basket', 'Basket')
  74. form_class = BasketLineForm
  75. extra = 0
  76. can_delete = True
  77. template_name = 'basket/basket.html'
  78. def get_queryset(self):
  79. return self.request.basket.all_lines()
  80. def get_shipping_methods(self, basket):
  81. return Repository().get_shipping_methods(
  82. self.request.user, self.request.basket)
  83. def get_default_shipping_method(self, basket):
  84. return Repository().get_default_shipping_method(
  85. self.request.user, self.request.basket)
  86. def get_basket_warnings(self, basket):
  87. """
  88. Return a list of warnings that apply to this basket
  89. """
  90. warnings = []
  91. for line in basket.all_lines():
  92. warning = line.get_warning()
  93. if warning:
  94. warnings.append(warning)
  95. return warnings
  96. def get_upsell_messages(self, basket):
  97. offers = Applicator().get_offers(self.request, basket)
  98. applied_offers = basket.offer_applications.offers.values()
  99. msgs = []
  100. for offer in offers:
  101. if offer.is_condition_partially_satisfied(basket) and offer not in applied_offers:
  102. data = {
  103. 'message': offer.get_upsell_message(basket),
  104. 'offer': offer}
  105. msgs.append(data)
  106. return msgs
  107. def get_context_data(self, **kwargs):
  108. context = super(BasketView, self).get_context_data(**kwargs)
  109. context['voucher_form'] = BasketVoucherForm()
  110. # Shipping information is included to give an idea of the total order
  111. # cost. It is also important for PayPal Express where the customer
  112. # gets redirected away from the basket page and needs to see what the
  113. # estimated order total is beforehand.
  114. method = self.get_default_shipping_method(self.request.basket)
  115. context['shipping_method'] = method
  116. context['shipping_methods'] = self.get_shipping_methods(
  117. self.request.basket)
  118. context['order_total_incl_tax'] = (
  119. self.request.basket.total_incl_tax +
  120. method.basket_charge_incl_tax())
  121. context['basket_warnings'] = self.get_basket_warnings(
  122. self.request.basket)
  123. context['upsell_messages'] = self.get_upsell_messages(
  124. self.request.basket)
  125. if self.request.user.is_authenticated():
  126. try:
  127. saved_basket = self.basket_model.saved.get(
  128. owner=self.request.user)
  129. except self.basket_model.DoesNotExist:
  130. pass
  131. else:
  132. if not saved_basket.is_empty:
  133. saved_queryset = saved_basket.all_lines().select_related(
  134. 'product', 'product__stockrecord')
  135. formset = SavedLineFormSet(user=self.request.user,
  136. basket=self.request.basket,
  137. queryset=saved_queryset,
  138. prefix='saved')
  139. context['saved_formset'] = formset
  140. return context
  141. def get_success_url(self):
  142. return self.request.META.get('HTTP_REFERER', reverse('basket:summary'))
  143. def formset_valid(self, formset):
  144. # Store offers before any changes are made so we can inform the user of
  145. # any changes
  146. offers_before = self.request.basket.applied_offers()
  147. save_for_later = False
  148. # Keep a list of messages - we don't immediately call
  149. # django.contrib.messages as we may be returning an AJAX response in
  150. # which case we pass the messages back in a JSON payload.
  151. flash_messages = ajax.FlashMessages()
  152. for form in formset:
  153. if (hasattr(form, 'cleaned_data') and
  154. form.cleaned_data['save_for_later']):
  155. line = form.instance
  156. if self.request.user.is_authenticated():
  157. self.move_line_to_saved_basket(line)
  158. msg = render_to_string(
  159. 'basket/messages/line_saved.html',
  160. {'line': line})
  161. flash_messages.info(msg)
  162. save_for_later = True
  163. else:
  164. msg = _("You can't save an item for later if you're "
  165. "not logged in!")
  166. messages.error(self.request, msg)
  167. return HttpResponseRedirect(self.get_success_url())
  168. if save_for_later:
  169. # No need to call super if we're moving lines to the saved basket
  170. response = HttpResponseRedirect(self.get_success_url())
  171. else:
  172. # Save changes to basket as per normal
  173. response = super(BasketView, self).formset_valid(formset)
  174. # If AJAX submission, don't redirect but reload the basket content HTML
  175. if self.request.is_ajax():
  176. # Reload basket and apply offers again
  177. self.request.basket = get_model('basket', 'Basket').objects.get(
  178. id=self.request.basket.id)
  179. Applicator().apply(self.request, self.request.basket)
  180. offers_after = self.request.basket.applied_offers()
  181. for level, msg in get_messages(
  182. self.request.basket, offers_before,
  183. offers_after, include_buttons=False):
  184. flash_messages.add_message(level, msg)
  185. # Reload formset - we have to remove the POST fields from the
  186. # kwargs as, if they are left in, the formset won't construct
  187. # correctly as there will be a state mismatch between the
  188. # management form and the database.
  189. kwargs = self.get_formset_kwargs()
  190. del kwargs['data']
  191. del kwargs['files']
  192. if 'queryset' in kwargs:
  193. del kwargs['queryset']
  194. formset = self.get_formset()(queryset=self.get_queryset(),
  195. **kwargs)
  196. ctx = self.get_context_data(formset=formset,
  197. basket=self.request.basket)
  198. return self.json_response(ctx, flash_messages)
  199. apply_messages(self.request, offers_before)
  200. return response
  201. def json_response(self, ctx, flash_messages):
  202. basket_html = render_to_string(
  203. 'basket/partials/basket_content.html',
  204. RequestContext(self.request, ctx))
  205. payload = {
  206. 'content_html': basket_html,
  207. 'messages': flash_messages.to_json()}
  208. return HttpResponse(json.dumps(payload),
  209. mimetype="application/json")
  210. def move_line_to_saved_basket(self, line):
  211. saved_basket, _ = get_model('basket', 'basket').saved.get_or_create(
  212. owner=self.request.user)
  213. saved_basket.merge_line(line)
  214. def formset_invalid(self, formset):
  215. flash_messages = ajax.FlashMessages()
  216. flash_messages.warning(_("Your basket couldn't be updated"))
  217. if self.request.is_ajax():
  218. ctx = self.get_context_data(formset=formset,
  219. basket=self.request.basket)
  220. return self.json_response(ctx, flash_messages)
  221. flash_messages.apply_to_request(self.request)
  222. return super(BasketView, self).formset_invalid(formset)
  223. class BasketAddView(FormView):
  224. """
  225. Handles the add-to-basket operation, shouldn't be accessed via
  226. GET because there's nothing sensible to render.
  227. """
  228. form_class = AddToBasketForm
  229. product_select_form_class = ProductSelectionForm
  230. product_model = get_model('catalogue', 'product')
  231. add_signal = basket_addition
  232. def get(self, request, *args, **kwargs):
  233. return HttpResponseRedirect(reverse('basket:summary'))
  234. def get_form_kwargs(self):
  235. kwargs = super(BasketAddView, self).get_form_kwargs()
  236. product_select_form = self.product_select_form_class(self.request.POST)
  237. if product_select_form.is_valid():
  238. kwargs['instance'] = product_select_form.cleaned_data['product_id']
  239. else:
  240. kwargs['instance'] = None
  241. kwargs['request'] = self.request
  242. return kwargs
  243. def get_success_url(self):
  244. url = None
  245. if self.request.POST.get('next'):
  246. url = self.request.POST.get('next')
  247. elif 'HTTP_REFERER' in self.request.META:
  248. url = self.request.META['HTTP_REFERER']
  249. if url:
  250. # We only allow internal URLs so we see if the url resolves
  251. try:
  252. resolve(urlparse(url).path)
  253. except Http404:
  254. url = None
  255. if url is None:
  256. url = reverse('basket:summary')
  257. return url
  258. def form_valid(self, form):
  259. offers_before = self.request.basket.applied_offers()
  260. self.request.basket.add_product(
  261. form.instance, form.cleaned_data['quantity'],
  262. form.cleaned_options())
  263. messages.success(self.request, self.get_success_message(form),
  264. extra_tags='safe noicon')
  265. # Check for additional offer messages
  266. apply_messages(self.request, offers_before)
  267. # Send signal for basket addition
  268. self.add_signal.send(
  269. sender=self, product=form.instance, user=self.request.user)
  270. return super(BasketAddView, self).form_valid(form)
  271. def get_success_message(self, form):
  272. return render_to_string(
  273. 'basket/messages/addition.html',
  274. {'product': form.instance,
  275. 'quantity': form.cleaned_data['quantity']})
  276. def form_invalid(self, form):
  277. msgs = []
  278. for error in form.errors.values():
  279. msgs.append(error.as_text())
  280. messages.error(self.request, ",".join(msgs))
  281. return HttpResponseRedirect(
  282. self.request.META.get('HTTP_REFERER', reverse('basket:summary')))
  283. class VoucherAddView(FormView):
  284. form_class = BasketVoucherForm
  285. voucher_model = get_model('voucher', 'voucher')
  286. add_signal = voucher_addition
  287. def get(self, request, *args, **kwargs):
  288. return HttpResponseRedirect(reverse('basket:summary'))
  289. def apply_voucher_to_basket(self, voucher):
  290. if not voucher.is_active():
  291. messages.error(
  292. self.request,
  293. _("The '%(code)s' voucher has expired") % {
  294. 'code': voucher.code})
  295. return
  296. is_available, message = voucher.is_available_to_user(self.request.user)
  297. if not is_available:
  298. messages.error(self.request, message)
  299. return
  300. self.request.basket.vouchers.add(voucher)
  301. # Raise signal
  302. self.add_signal.send(sender=self,
  303. basket=self.request.basket,
  304. voucher=voucher)
  305. # Recalculate discounts to see if the voucher gives any
  306. Applicator().apply(self.request, self.request.basket)
  307. discounts_after = self.request.basket.offer_applications
  308. # Look for discounts from this new voucher
  309. found_discount = False
  310. for discount in discounts_after:
  311. if discount['voucher'] and discount['voucher'] == voucher:
  312. found_discount = True
  313. break
  314. if not found_discount:
  315. messages.warning(
  316. self.request,
  317. _("Your basket does not qualify for a voucher discount"))
  318. self.request.basket.vouchers.remove(voucher)
  319. else:
  320. messages.info(
  321. self.request,
  322. _("Voucher '%(code)s' added to basket") % {
  323. 'code': voucher.code})
  324. def form_valid(self, form):
  325. code = form.cleaned_data['code']
  326. if not self.request.basket.id:
  327. return HttpResponseRedirect(
  328. self.request.META.get('HTTP_REFERER',
  329. reverse('basket:summary')))
  330. if self.request.basket.contains_voucher(code):
  331. messages.error(
  332. self.request,
  333. _("You have already added the '%(code)s' voucher to "
  334. "your basket") % {'code': code})
  335. else:
  336. try:
  337. voucher = self.voucher_model._default_manager.get(code=code)
  338. except self.voucher_model.DoesNotExist:
  339. messages.error(
  340. self.request,
  341. _("No voucher found with code '%(code)s'") % {
  342. 'code': code})
  343. else:
  344. self.apply_voucher_to_basket(voucher)
  345. return HttpResponseRedirect(
  346. self.request.META.get('HTTP_REFERER', reverse('basket:summary')))
  347. def form_invalid(self, form):
  348. messages.error(self.request, _("Please enter a voucher code"))
  349. return HttpResponseRedirect(reverse('basket:summary') + '#voucher')
  350. class VoucherRemoveView(View):
  351. voucher_model = get_model('voucher', 'voucher')
  352. def get(self, request, *args, **kwargs):
  353. return HttpResponseRedirect(reverse('basket:summary'))
  354. def post(self, request, *args, **kwargs):
  355. voucher_id = int(kwargs.pop('pk'))
  356. if not request.basket.id:
  357. # Hacking attempt - the basket must be saved for it to have
  358. # a voucher in it.
  359. return HttpResponseRedirect(reverse('basket:summary'))
  360. try:
  361. voucher = request.basket.vouchers.get(id=voucher_id)
  362. except ObjectDoesNotExist:
  363. messages.error(
  364. request, _("No voucher found with id '%d'") % voucher_id)
  365. else:
  366. request.basket.vouchers.remove(voucher)
  367. request.basket.save()
  368. messages.info(
  369. request, _("Voucher '%s' removed from basket") % voucher.code)
  370. return HttpResponseRedirect(reverse('basket:summary'))
  371. class SavedView(ModelFormSetView):
  372. model = get_model('basket', 'line')
  373. basket_model = get_model('basket', 'basket')
  374. formset_class = SavedLineFormSet
  375. form_class = SavedLineForm
  376. extra = 0
  377. can_delete = True
  378. def get(self, request, *args, **kwargs):
  379. return HttpResponseRedirect(reverse('basket:summary'))
  380. def get_queryset(self):
  381. try:
  382. saved_basket = self.basket_model.saved.get(owner=self.request.user)
  383. return saved_basket.all_lines().select_related(
  384. 'product', 'product__stockrecord')
  385. except self.basket_model.DoesNotExist:
  386. return []
  387. def get_success_url(self):
  388. return self.request.META.get('HTTP_REFERER', reverse('basket:summary'))
  389. def get_formset_kwargs(self):
  390. kwargs = super(SavedView, self).get_formset_kwargs()
  391. kwargs['prefix'] = 'saved'
  392. kwargs['basket'] = self.request.basket
  393. kwargs['user'] = self.request.user
  394. return kwargs
  395. def formset_valid(self, formset):
  396. offers_before = self.request.basket.applied_offers()
  397. is_move = False
  398. for form in formset:
  399. if form.cleaned_data.get('move_to_basket', False):
  400. is_move = True
  401. msg = render_to_string(
  402. 'basket/messages/line_restored.html',
  403. {'line': form.instance})
  404. messages.info(self.request, msg, extra_tags='safe noicon')
  405. real_basket = self.request.basket
  406. real_basket.merge_line(form.instance)
  407. if is_move:
  408. # As we're changing the basket, we need to check if it qualifies
  409. # for any new offers.
  410. apply_messages(self.request, offers_before)
  411. response = HttpResponseRedirect(self.get_success_url())
  412. else:
  413. response = super(SavedView, self).formset_valid(formset)
  414. return response
  415. def formset_invalid(self, formset):
  416. messages.error(
  417. self.request,
  418. '\n'.join(
  419. error for ed in formset.errors for el
  420. in ed.values() for error in el))
  421. return HttpResponseRedirect(
  422. self.request.META.get('HTTP_REFERER', reverse('basket:summary')))