Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

views.py 19KB

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