import json
from django import shortcuts
from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse
from django.http import HttpResponse
from django.shortcuts import redirect
from django.template import RequestContext
from django.template.loader import render_to_string
from django.utils.http import is_safe_url
from django.utils.translation import ugettext_lazy as _
from django.views.generic import FormView, View
from extra_views import ModelFormSetView
from oscar.apps.basket import signals
from oscar.core import ajax
from oscar.core.loading import get_class, get_classes, get_model
from oscar.core.utils import redirect_to_referrer, safe_referrer
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')
BasketMessageGenerator = get_class('basket.utils', 'BasketMessageGenerator')
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 BasketMessageGenerator().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)
BasketMessageGenerator().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
BasketMessageGenerator().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 voucher.is_expired():
messages.error(
self.request,
_("The '%(code)s' voucher has expired") % {
'code': voucher.code})
return
if not voucher.is_active():
messages.error(
self.request,
_("The '%(code)s' voucher is not active") % {
'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.
BasketMessageGenerator().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')