forked from stephenmcd/cartridge
/
views.py
343 lines (314 loc) · 14.6 KB
/
views.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
from collections import defaultdict
from django.contrib.auth.decorators import login_required
from django.contrib.messages import info
from django.core.urlresolvers import get_callable, reverse
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.template import RequestContext
from django.template.defaultfilters import slugify
from django.template.loader import get_template
from django.utils import simplejson
from django.utils.translation import ugettext as _
from django.views.decorators.cache import never_cache
from mezzanine.conf import settings
from mezzanine.utils.importing import import_dotted_path
from mezzanine.utils.views import render, set_cookie, paginate
from cartridge.shop import checkout
from cartridge.shop.forms import AddProductForm, DiscountForm, CartItemFormSet
from cartridge.shop.models import Product, ProductVariation, Order, OrderItem
from cartridge.shop.models import DiscountCode
from cartridge.shop.utils import recalculate_discount, sign
# Set up checkout handlers.
handler = lambda s: import_dotted_path(s) if s else lambda *args: None
billship_handler = handler(settings.SHOP_HANDLER_BILLING_SHIPPING)
payment_handler = handler(settings.SHOP_HANDLER_PAYMENT)
order_handler = handler(settings.SHOP_HANDLER_ORDER)
def product(request, slug, template="shop/product.html"):
"""
Display a product - convert the product variations to JSON as well as
handling adding the product to either the cart or the wishlist.
"""
published_products = Product.objects.published(for_user=request.user)
product = get_object_or_404(published_products, slug=slug)
fields = [f.name for f in ProductVariation.option_fields()]
variations = product.variations.all()
variations_json = simplejson.dumps([dict([(f, getattr(v, f))
for f in fields + ["sku", "image_id"]])
for v in variations])
to_cart = (request.method == "POST" and
request.POST.get("add_wishlist") is None)
initial_data = {}
if variations:
initial_data = dict([(f, getattr(variations[0], f)) for f in fields])
initial_data["quantity"] = 1
add_product_form = AddProductForm(request.POST or None, product=product,
initial=initial_data, to_cart=to_cart)
if request.method == "POST":
if add_product_form.is_valid():
if to_cart:
quantity = add_product_form.cleaned_data["quantity"]
request.cart.add_item(add_product_form.variation, quantity)
recalculate_discount(request)
info(request, _("Item added to cart"))
return redirect("shop_cart")
else:
skus = request.wishlist
sku = add_product_form.variation.sku
if sku not in skus:
skus.append(sku)
info(request, _("Item added to wishlist"))
response = redirect("shop_wishlist")
set_cookie(response, "wishlist", ",".join(skus))
return response
context = {
"product": product,
"editable_obj": product,
"images": product.images.all(),
"variations": variations,
"variations_json": variations_json,
"has_available_variations": any([v.has_price() for v in variations]),
"related_products": product.related_products.published(
for_user=request.user),
"add_product_form": add_product_form
}
return render(request, template, context)
@never_cache
def wishlist(request, template="shop/wishlist.html"):
"""
Display the wishlist and handle removing items from the wishlist and
adding them to the cart.
"""
skus = request.wishlist
error = None
if request.method == "POST":
to_cart = request.POST.get("add_cart")
add_product_form = AddProductForm(request.POST or None,
to_cart=to_cart)
if to_cart:
if add_product_form.is_valid():
request.cart.add_item(add_product_form.variation, 1)
recalculate_discount(request)
message = _("Item added to cart")
url = "shop_cart"
else:
error = add_product_form.errors.values()[0]
else:
message = _("Item removed from wishlist")
url = "shop_wishlist"
sku = request.POST.get("sku")
if sku in skus:
skus.remove(sku)
if not error:
info(request, message)
response = redirect(url)
set_cookie(response, "wishlist", ",".join(skus))
return response
# Remove skus from the cookie that no longer exist.
published_products = Product.objects.published(for_user=request.user)
f = {"product__in": published_products, "sku__in": skus}
wishlist = ProductVariation.objects.filter(**f).select_related(depth=1)
wishlist = sorted(wishlist, key=lambda v: skus.index(v.sku))
context = {"wishlist_items": wishlist, "error": error}
response = render(request, template, context)
if len(wishlist) < len(skus):
skus = [variation.sku for variation in wishlist]
set_cookie(response, "wishlist", ",".join(skus))
return response
@never_cache
def cart(request, template="shop/cart.html"):
"""
Display cart and handle removing items from the cart.
"""
cart_formset = CartItemFormSet(instance=request.cart)
discount_form = DiscountForm(request, request.POST or None)
if request.method == "POST":
valid = True
if request.POST.get("update_cart"):
valid = request.cart.has_items()
if not valid:
# Session timed out.
info(request, _("Your cart has expired"))
else:
cart_formset = CartItemFormSet(request.POST,
instance=request.cart)
valid = cart_formset.is_valid()
if valid:
cart_formset.save()
recalculate_discount(request)
info(request, _("Cart updated"))
else:
valid = discount_form.is_valid()
if valid:
discount_form.set_discount()
if valid:
return redirect("shop_cart")
context = {"cart_formset": cart_formset}
settings.use_editable()
if (settings.SHOP_DISCOUNT_FIELD_IN_CART and
DiscountCode.objects.active().count() > 0):
context["discount_form"] = discount_form
return render(request, template, context)
@never_cache
def checkout_steps(request):
"""
Display the order form and handle processing of each step.
"""
# Do the authentication check here rather than using standard
# login_required decorator. This means we can check for a custom
# LOGIN_URL and fall back to our own login view.
authenticated = request.user.is_authenticated()
if settings.SHOP_CHECKOUT_ACCOUNT_REQUIRED and not authenticated:
url = "%s?next=%s" % (settings.LOGIN_URL, reverse("shop_checkout"))
return redirect(url)
# Determine the Form class to use during the checkout process
form_class = get_callable(settings.SHOP_CHECKOUT_FORM_CLASS)
step = int(request.POST.get("step", checkout.CHECKOUT_STEP_FIRST))
initial = checkout.initial_order_data(request)
form = form_class(request, step, initial=initial)
data = request.POST
checkout_errors = []
if request.POST.get("back") is not None:
# Back button in the form was pressed - load the order form
# for the previous step and maintain the field values entered.
step -= 1
form = form_class(request, step, initial=initial)
elif request.method == "POST" and request.cart.has_items():
form = form_class(request, step, initial=initial, data=data)
if form.is_valid():
# Copy the current form fields to the session so that
# they're maintained if the customer leaves the checkout
# process, but remove sensitive fields from the session
# such as the credit card fields so that they're never
# stored anywhere.
request.session["order"] = dict(form.cleaned_data)
sensitive_card_fields = ("card_number", "card_expiry_month",
"card_expiry_year", "card_ccv")
for field in sensitive_card_fields:
if field in request.session["order"]:
del request.session["order"][field]
# FIRST CHECKOUT STEP - handle shipping and discount code.
if step == checkout.CHECKOUT_STEP_FIRST:
try:
billship_handler(request, form)
except checkout.CheckoutError, e:
checkout_errors.append(e)
form.set_discount()
# FINAL CHECKOUT STEP - handle payment and process order.
if step == checkout.CHECKOUT_STEP_LAST and not checkout_errors:
# Create and save the inital order object so that
# the payment handler has access to all of the order
# fields. If there is a payment error then delete the
# order, otherwise remove the cart items from stock
# and send the order reciept email.
order = form.save(commit=False)
order.setup(request)
# Try payment.
try:
transaction_id = payment_handler(request, form, order)
except checkout.CheckoutError, e:
# Error in payment handler.
order.delete()
checkout_errors.append(e)
if settings.SHOP_CHECKOUT_STEPS_CONFIRMATION:
step -= 1
else:
# Finalize order - ``order.complete()`` performs
# final cleanup of session and cart.
# ``order_handler()`` can be defined by the
# developer to implement custom order processing.
# Then send the order email to the customer.
order.transaction_id = transaction_id
order.complete(request)
order_handler(request, form, order)
checkout.send_order_email(request, order)
# Set the cookie for remembering address details
# if the "remember" checkbox was checked.
response = redirect("shop_complete")
if form.cleaned_data.get("remember") is not None:
remembered = "%s:%s" % (sign(order.key), order.key)
set_cookie(response, "remember", remembered,
secure=request.is_secure())
else:
response.delete_cookie("remember")
return response
# Only step forward through the checkout process if the callback
# handlers didn't generate any errors. Otherwise, assign the
# errors to the form and stay on the same step.
if not checkout_errors:
step += 1
form = form_class(request, step, initial=initial, data=data,
errors=checkout_errors or None)
step_vars = checkout.CHECKOUT_STEPS[step - 1]
template = "shop/%s.html" % step_vars["template"]
CHECKOUT_STEP_FIRST = step == checkout.CHECKOUT_STEP_FIRST
context = {"form": form, "CHECKOUT_STEP_FIRST": CHECKOUT_STEP_FIRST,
"step_title": step_vars["title"], "step_url": step_vars["url"],
"steps": checkout.CHECKOUT_STEPS, "step": step}
return render(request, template, context)
@never_cache
def complete(request, template="shop/complete.html"):
"""
Redirected to once an order is complete - pass the order object
for tracking items via Google Anayltics, and displaying in
the template if required.
"""
try:
order = Order.objects.from_request(request)
except Order.DoesNotExist:
raise Http404
items = order.items.all()
# Assign product names to each of the items since they're not
# stored.
skus = [item.sku for item in items]
variations = ProductVariation.objects.filter(sku__in=skus)
names = {}
for variation in variations.select_related(depth=1):
names[variation.sku] = variation.product.title
for i, item in enumerate(items):
setattr(items[i], "name", names[item.sku])
context = {"order": order, "items": items,
"steps": checkout.CHECKOUT_STEPS}
return render(request, template, context)
def invoice(request, order_id, template="shop/order_invoice.html"):
"""
Display a plain text invoice for the given order. The order must
belong to the user which is checked via session or ID if
authenticated, or if the current user is staff.
"""
lookup = {"id": order_id}
if not request.user.is_authenticated():
lookup["key"] = request.session.session_key
elif not request.user.is_staff:
lookup["user_id"] = request.user.id
order = get_object_or_404(Order, **lookup)
context = {"order": order}
context.update(order.details_as_dict())
context = RequestContext(request, context)
if request.GET.get("format") == "pdf":
response = HttpResponse(mimetype="application/pdf")
name = slugify("%s-invoice-%s" % (settings.SITE_TITLE, order.id))
response["Content-Disposition"] = "attachment; filename=%s.pdf" % name
html = get_template(template).render(context)
import ho.pisa
ho.pisa.CreatePDF(html, response)
return response
return render(request, template, context)
@login_required
def order_history(request, template="shop/order_history.html"):
"""
Display a list of the currently logged-in user's past orders.
"""
all_orders = Order.objects.filter(user_id=request.user.id)
orders = paginate(all_orders.order_by('-time'),
request.GET.get("page", 1),
settings.SHOP_PER_PAGE_CATEGORY,
settings.MAX_PAGING_LINKS)
# Add the total quantity to each order - this can probably be
# replaced with fetch_related and Sum when we drop Django 1.3
order_quantities = defaultdict(int)
for item in OrderItem.objects.filter(order__user_id=request.user.id):
order_quantities[item.order_id] += item.quantity
for order in orders.object_list:
setattr(order, "quantity_total", order_quantities[order.id])
context = {"orders": orders}
return render(request, template, context)