import json
from django import shortcuts
from django.contrib import messages
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.template import RequestContext
from django.core.urlresolvers import reverse
from django.http import HttpResponse
from django.views.generic import FormView, View
from django.utils.http import is_safe_url
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist
from extra_views import ModelFormSetView
from oscar.core import ajax
from oscar.core.utils import redirect_to_referrer, safe_referrer
from oscar.apps.basket import signals
from oscar.core.loading import get_class, get_classes, get_model
Applicator = get_class('offer.utils', 'Applicator')
(BasketLineFormSet, BasketLineForm, AddToBasketForm, BasketVoucherForm,
SavedLineFormSet, SavedLineForm) \
= get_classes('basket.forms', ('BasketLineFormSet', 'BasketLineForm',
'AddToBasketForm', 'BasketVoucherForm',
'SavedLineFormSet', 'SavedLineForm'))
Repository = get_class('shipping.repository', ('Repository'))
OrderTotalCalculator = get_class(
'checkout.calculators', 'OrderTotalCalculator')
[docs]def get_messages(basket, offers_before, offers_after,
include_buttons=True):
"""
Return the messages about offer changes
"""
# Look for changes in offers
offers_lost = set(offers_before.keys()).difference(
set(offers_after.keys()))
offers_gained = set(offers_after.keys()).difference(
set(offers_before.keys()))
# Build a list of (level, msg) tuples
offer_messages = []
for offer_id in offers_lost:
offer = offers_before[offer_id]
msg = render_to_string(
'basket/messages/offer_lost.html',
{'offer': offer})
offer_messages.append((
messages.WARNING, msg))
for offer_id in offers_gained:
offer = offers_after[offer_id]
msg = render_to_string(
'basket/messages/offer_gained.html',
{'offer': offer})
offer_messages.append((
messages.SUCCESS, msg))
# We use the 'include_buttons' parameter to determine whether to show the
# 'Checkout now' buttons. We don't want to show these on the basket page.
msg = render_to_string(
'basket/messages/new_total.html',
{'basket': basket,
'include_buttons': include_buttons})
offer_messages.append((
messages.INFO, msg))
return offer_messages
[docs]def apply_messages(request, offers_before):
"""
Set flash messages triggered by changes to the basket
"""
# Re-apply offers to see if any new ones are now available
request.basket.reset_offer_applications()
Applicator().apply(request.basket, request.user, request)
offers_after = request.basket.applied_offers()
for level, msg in get_messages(
request.basket, offers_before, offers_after):
messages.add_message(
request, level, msg, extra_tags='safe noicon')
class BasketView(ModelFormSetView):
model = get_model('basket', 'Line')
basket_model = get_model('basket', 'Basket')
formset_class = BasketLineFormSet
form_class = BasketLineForm
extra = 0
can_delete = True
template_name = 'basket/basket.html'
def get_formset_kwargs(self):
kwargs = super(BasketView, self).get_formset_kwargs()
kwargs['strategy'] = self.request.strategy
return kwargs
def get_queryset(self):
return self.request.basket.all_lines()
def get_shipping_methods(self, basket):
return Repository().get_shipping_methods(
basket=self.request.basket, user=self.request.user,
request=self.request)
def get_default_shipping_method(self, basket):
return Repository().get_default_shipping_method(
basket=self.request.basket, user=self.request.user,
request=self.request)
def get_basket_warnings(self, basket):
"""
Return a list of warnings that apply to this basket
"""
warnings = []
for line in basket.all_lines():
warning = line.get_warning()
if warning:
warnings.append(warning)
return warnings
def get_upsell_messages(self, basket):
offers = Applicator().get_offers(basket, self.request.user,
self.request)
applied_offers = list(basket.offer_applications.offers.values())
msgs = []
for offer in offers:
if offer.is_condition_partially_satisfied(basket) \
and offer not in applied_offers:
data = {
'message': offer.get_upsell_message(basket),
'offer': offer}
msgs.append(data)
return msgs
def get_basket_voucher_form(self):
"""
This is a separate method so that it's easy to e.g. not return a form
if there are no vouchers available.
"""
return BasketVoucherForm()
def get_context_data(self, **kwargs):
context = super(BasketView, self).get_context_data(**kwargs)
context['voucher_form'] = self.get_basket_voucher_form()
# Shipping information is included to give an idea of the total order
# cost. It is also important for PayPal Express where the customer
# gets redirected away from the basket page and needs to see what the
# estimated order total is beforehand.
context['shipping_methods'] = self.get_shipping_methods(
self.request.basket)
method = self.get_default_shipping_method(self.request.basket)
context['shipping_method'] = method
shipping_charge = method.calculate(self.request.basket)
context['shipping_charge'] = shipping_charge
if method.is_discounted:
excl_discount = method.calculate_excl_discount(self.request.basket)
context['shipping_charge_excl_discount'] = excl_discount
context['order_total'] = OrderTotalCalculator().calculate(
self.request.basket, shipping_charge)
context['basket_warnings'] = self.get_basket_warnings(
self.request.basket)
context['upsell_messages'] = self.get_upsell_messages(
self.request.basket)
if self.request.user.is_authenticated():
try:
saved_basket = self.basket_model.saved.get(
owner=self.request.user)
except self.basket_model.DoesNotExist:
pass
else:
saved_basket.strategy = self.request.basket.strategy
if not saved_basket.is_empty:
saved_queryset = saved_basket.all_lines()
formset = SavedLineFormSet(strategy=self.request.strategy,
basket=self.request.basket,
queryset=saved_queryset,
prefix='saved')
context['saved_formset'] = formset
return context
def get_success_url(self):
return safe_referrer(self.request, 'basket:summary')
def formset_valid(self, formset):
# Store offers before any changes are made so we can inform the user of
# any changes
offers_before = self.request.basket.applied_offers()
save_for_later = False
# Keep a list of messages - we don't immediately call
# django.contrib.messages as we may be returning an AJAX response in
# which case we pass the messages back in a JSON payload.
flash_messages = ajax.FlashMessages()
for form in formset:
if (hasattr(form, 'cleaned_data') and
form.cleaned_data['save_for_later']):
line = form.instance
if self.request.user.is_authenticated():
self.move_line_to_saved_basket(line)
msg = render_to_string(
'basket/messages/line_saved.html',
{'line': line})
flash_messages.info(msg)
save_for_later = True
else:
msg = _("You can't save an item for later if you're "
"not logged in!")
flash_messages.error(msg)
return redirect(self.get_success_url())
if save_for_later:
# No need to call super if we're moving lines to the saved basket
response = redirect(self.get_success_url())
else:
# Save changes to basket as per normal
response = super(BasketView, self).formset_valid(formset)
# If AJAX submission, don't redirect but reload the basket content HTML
if self.request.is_ajax():
# Reload basket and apply offers again
self.request.basket = get_model('basket', 'Basket').objects.get(
id=self.request.basket.id)
self.request.basket.strategy = self.request.strategy
Applicator().apply(self.request.basket, self.request.user,
self.request)
offers_after = self.request.basket.applied_offers()
for level, msg in get_messages(
self.request.basket, offers_before,
offers_after, include_buttons=False):
flash_messages.add_message(level, msg)
# Reload formset - we have to remove the POST fields from the
# kwargs as, if they are left in, the formset won't construct
# correctly as there will be a state mismatch between the
# management form and the database.
kwargs = self.get_formset_kwargs()
del kwargs['data']
del kwargs['files']
if 'queryset' in kwargs:
del kwargs['queryset']
formset = self.get_formset()(queryset=self.get_queryset(),
**kwargs)
ctx = self.get_context_data(formset=formset,
basket=self.request.basket)
return self.json_response(ctx, flash_messages)
apply_messages(self.request, offers_before)
return response
def json_response(self, ctx, flash_messages):
basket_html = render_to_string(
'basket/partials/basket_content.html',
RequestContext(self.request, ctx))
payload = {
'content_html': basket_html,
'messages': flash_messages.as_dict()}
return HttpResponse(json.dumps(payload),
content_type="application/json")
def move_line_to_saved_basket(self, line):
saved_basket, _ = get_model('basket', 'basket').saved.get_or_create(
owner=self.request.user)
saved_basket.merge_line(line)
def formset_invalid(self, formset):
flash_messages = ajax.FlashMessages()
flash_messages.warning(_(
"Your basket couldn't be updated. "
"Please correct any validation errors below."))
if self.request.is_ajax():
ctx = self.get_context_data(formset=formset,
basket=self.request.basket)
return self.json_response(ctx, flash_messages)
flash_messages.apply_to_request(self.request)
return super(BasketView, self).formset_invalid(formset)
[docs]class BasketAddView(FormView):
"""
Handles the add-to-basket submissions, which are triggered from various
parts of the site. The add-to-basket form is loaded into templates using
a templatetag from module basket_tags.py.
"""
form_class = AddToBasketForm
product_model = get_model('catalogue', 'product')
add_signal = signals.basket_addition
http_method_names = ['post']
def post(self, request, *args, **kwargs):
self.product = shortcuts.get_object_or_404(
self.product_model, pk=kwargs['pk'])
return super(BasketAddView, self).post(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super(BasketAddView, self).get_form_kwargs()
kwargs['basket'] = self.request.basket
kwargs['product'] = self.product
return kwargs
def form_invalid(self, form):
msgs = []
for error in form.errors.values():
msgs.append(error.as_text())
clean_msgs = [m.replace('* ', '') for m in msgs if m.startswith('* ')]
messages.error(self.request, ",".join(clean_msgs))
return redirect_to_referrer(self.request, 'basket:summary')
def form_valid(self, form):
offers_before = self.request.basket.applied_offers()
self.request.basket.add_product(
form.product, form.cleaned_data['quantity'],
form.cleaned_options())
messages.success(self.request, self.get_success_message(form),
extra_tags='safe noicon')
# Check for additional offer messages
apply_messages(self.request, offers_before)
# Send signal for basket addition
self.add_signal.send(
sender=self, product=form.product, user=self.request.user,
request=self.request)
return super(BasketAddView, self).form_valid(form)
def get_success_message(self, form):
return render_to_string(
'basket/messages/addition.html',
{'product': form.product,
'quantity': form.cleaned_data['quantity']})
def get_success_url(self):
post_url = self.request.POST.get('next')
if post_url and is_safe_url(post_url, self.request.get_host()):
return post_url
return safe_referrer(self.request, 'basket:summary')
class VoucherAddView(FormView):
form_class = BasketVoucherForm
voucher_model = get_model('voucher', 'voucher')
add_signal = signals.voucher_addition
def get(self, request, *args, **kwargs):
return redirect('basket:summary')
def apply_voucher_to_basket(self, voucher):
if not voucher.is_active():
messages.error(
self.request,
_("The '%(code)s' voucher has expired") % {
'code': voucher.code})
return
is_available, message = voucher.is_available_to_user(self.request.user)
if not is_available:
messages.error(self.request, message)
return
self.request.basket.vouchers.add(voucher)
# Raise signal
self.add_signal.send(
sender=self, basket=self.request.basket, voucher=voucher)
# Recalculate discounts to see if the voucher gives any
Applicator().apply(self.request.basket, self.request.user,
self.request)
discounts_after = self.request.basket.offer_applications
# Look for discounts from this new voucher
found_discount = False
for discount in discounts_after:
if discount['voucher'] and discount['voucher'] == voucher:
found_discount = True
break
if not found_discount:
messages.warning(
self.request,
_("Your basket does not qualify for a voucher discount"))
self.request.basket.vouchers.remove(voucher)
else:
messages.info(
self.request,
_("Voucher '%(code)s' added to basket") % {
'code': voucher.code})
def form_valid(self, form):
code = form.cleaned_data['code']
if not self.request.basket.id:
return redirect_to_referrer(self.request, 'basket:summary')
if self.request.basket.contains_voucher(code):
messages.error(
self.request,
_("You have already added the '%(code)s' voucher to "
"your basket") % {'code': code})
else:
try:
voucher = self.voucher_model._default_manager.get(code=code)
except self.voucher_model.DoesNotExist:
messages.error(
self.request,
_("No voucher found with code '%(code)s'") % {
'code': code})
else:
self.apply_voucher_to_basket(voucher)
return redirect_to_referrer(self.request, 'basket:summary')
def form_invalid(self, form):
messages.error(self.request, _("Please enter a voucher code"))
return redirect(reverse('basket:summary') + '#voucher')
class VoucherRemoveView(View):
voucher_model = get_model('voucher', 'voucher')
remove_signal = signals.voucher_removal
http_method_names = ['post']
def post(self, request, *args, **kwargs):
response = redirect('basket:summary')
voucher_id = kwargs['pk']
if not request.basket.id:
# Hacking attempt - the basket must be saved for it to have
# a voucher in it.
return response
try:
voucher = request.basket.vouchers.get(id=voucher_id)
except ObjectDoesNotExist:
messages.error(
request, _("No voucher found with id '%d'") % voucher_id)
else:
request.basket.vouchers.remove(voucher)
self.remove_signal.send(
sender=self, basket=request.basket, voucher=voucher)
messages.info(
request, _("Voucher '%s' removed from basket") % voucher.code)
return response
class SavedView(ModelFormSetView):
model = get_model('basket', 'line')
basket_model = get_model('basket', 'basket')
formset_class = SavedLineFormSet
form_class = SavedLineForm
extra = 0
can_delete = True
def get(self, request, *args, **kwargs):
return redirect('basket:summary')
def get_queryset(self):
try:
saved_basket = self.basket_model.saved.get(owner=self.request.user)
saved_basket.strategy = self.request.strategy
return saved_basket.all_lines()
except self.basket_model.DoesNotExist:
return []
def get_success_url(self):
return safe_referrer(self.request, 'basket:summary')
def get_formset_kwargs(self):
kwargs = super(SavedView, self).get_formset_kwargs()
kwargs['prefix'] = 'saved'
kwargs['basket'] = self.request.basket
kwargs['strategy'] = self.request.strategy
return kwargs
def formset_valid(self, formset):
offers_before = self.request.basket.applied_offers()
is_move = False
for form in formset:
if form.cleaned_data.get('move_to_basket', False):
is_move = True
msg = render_to_string(
'basket/messages/line_restored.html',
{'line': form.instance})
messages.info(self.request, msg, extra_tags='safe noicon')
real_basket = self.request.basket
real_basket.merge_line(form.instance)
if is_move:
# As we're changing the basket, we need to check if it qualifies
# for any new offers.
apply_messages(self.request, offers_before)
response = redirect(self.get_success_url())
else:
response = super(SavedView, self).formset_valid(formset)
return response
def formset_invalid(self, formset):
messages.error(
self.request,
'\n'.join(
error for ed in formset.errors for el
in ed.values() for error in el))
return redirect_to_referrer(self.request, 'basket:summary')