Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor product view to use class-based views #82

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions cartridge/shop/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class AddProductForm(forms.Form):
quantity = forms.IntegerField(label=_("Quantity"), min_value=1)
sku = forms.CharField(required=False, widget=forms.HiddenInput())

def __init__(self, *args, **kwargs):
def __init__(self, **kwargs):
"""
Handles adding a variation to the cart or wishlist.

Expand All @@ -59,9 +59,10 @@ def __init__(self, *args, **kwargs):
"""
self._product = kwargs.pop("product", None)
self._to_cart = kwargs.pop("to_cart")
super(AddProductForm, self).__init__(*args, **kwargs)
super(AddProductForm, self).__init__(**kwargs)
data = kwargs.get('data')
# Adding from the wishlist with a sku, bail out.
if args[0] is not None and args[0].get("sku", None):
if data is not None and data.get("sku", None):
return
# Adding from the product page, remove the sku field
# and build the choice fields for the variations.
Expand Down
110 changes: 107 additions & 3 deletions cartridge/shop/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
from operator import mul

from django.core.urlresolvers import reverse
from django.db.models import F
from django.test import TestCase
from django.test.client import RequestFactory
from mezzanine.conf import settings
from mezzanine.core.models import CONTENT_STATUS_DRAFT
from mezzanine.core.models import CONTENT_STATUS_PUBLISHED
from mezzanine.utils.tests import run_pyflakes_for_package
from mezzanine.utils.tests import run_pep8_for_package
Expand Down Expand Up @@ -41,13 +43,12 @@ def setUp(self):
def test_views(self):
"""
Test the main shop views for errors.

Product view tests are in the ProductViewTests class
"""
# Category.
response = self.client.get(self._category.get_absolute_url())
self.assertEqual(response.status_code, 200)
# Product.
response = self.client.get(self._product.get_absolute_url())
self.assertEqual(response.status_code, 200)
# Cart.
response = self.client.get(reverse("shop_cart"))
self.assertEqual(response.status_code, 200)
Expand Down Expand Up @@ -352,6 +353,109 @@ def test_syntax(self):
self.fail("Syntax warnings!\n\n%s" % "\n".join(warnings))


class ProductViewTests(TestCase):
"""
Test Product views
"""

def setUp(self):
"""
Set up test data - product, options and variations
"""
self._published = {"status": CONTENT_STATUS_PUBLISHED}
self._product = Product.objects.create(**self._published)
self._product.available = True
self._product.save()
ProductOption.objects.create(type=1, name="Small")
ProductOption.objects.create(type=1, name="Medium")
ProductOption.objects.create(type=2, name="Blue")
ProductOption.objects.create(type=2, name="Read")
product_options = ProductOption.objects.as_fields()
self._product.variations.create_from_options(product_options)
self._product.variations.manage_empty()
self._product.variations.update(unit_price=F("id") + "10000")
self._product.variations.update(unit_price=F("unit_price") / "1000.0")
self._product.variations.update(num_in_stock=TEST_STOCK)

product_variation = ProductVariation.objects.get(option1='Small',
option2='Blue')
product_variation.sku = "widget-small-blue"
product_variation.save()

def test_get(self):
"""
Test the product view
"""
# Product.
response = self.client.get(self._product.get_absolute_url())
self.assertEqual(response.status_code, 200)

# Check if quantity defaults correctly
self.assertContains(response,
'<input type="text" name="quantity" value="1" id="id_quantity">',
html=True)

def test_404_on_not_published(self):
"""
Test that we get a 404 if it's not published
"""
self._product.status = CONTENT_STATUS_DRAFT
self._product.save()

response = self.client.get(self._product.get_absolute_url())
self.assertEqual(response.status_code, 404)

def test_not_available(self):
"""
Test that we get the appropriate message when the product is not
available
"""
self._product.available = False
self._product.save()
response = self.client.get(self._product.get_absolute_url())
self.assertContains(response,
"This product is currently unavailable.")

def test_add_to_cart(self):
"""
Test that the item gets added to the cart when clicking the buy
button
"""

# Test initial cart.
cart = Cart.objects.from_request(self.client)
self.assertFalse(cart.has_items())
self.assertEqual(cart.total_quantity(), 0)

url = self._product.get_absolute_url()
response = self.client.get(url)
payload = {'quantity': '1',
'option1': 'Small',
'option2': 'Blue',
'add_cart': 'Buy'}
response = self.client.post(url, payload, follow=True)
self.assertRedirects(response, "/shop/cart/")
cart = Cart.objects.from_request(self.client)

self.assertEqual(cart.skus(), ["widget-small-blue"])

def test_add_to_wishlist(self):
"""
Test that the item gets add to the wishlist when clicking the wishlist
button
"""

url = self._product.get_absolute_url()
response = self.client.get(url)
payload = {'quantity': '1',
'option1': 'Small',
'option2': 'Blue',
'add_wishlist': 'Save for later'}
response = self.client.post(url, payload, follow=True)
self.assertRedirects(response, "/shop/wishlist/")
self.assertEqual(response._request.wishlist, ["widget-small-blue"])


class SaleTests(TestCase):

def setUp(self):
Expand Down
5 changes: 3 additions & 2 deletions cartridge/shop/urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@

from cartridge.shop.views import ProductDetailView
from django.conf.urls.defaults import patterns, url

urlpatterns = patterns("cartridge.shop.views",
url("^product/(?P<slug>.*)/$", "product", name="shop_product"),
url("^product/(?P<slug>.*)/$", ProductDetailView.as_view(),
name="shop_product"),
url("^wishlist/$", "wishlist", name="shop_wishlist"),
url("^cart/$", "cart", name="shop_cart"),
url("^checkout/$", "checkout_steps", name="shop_checkout"),
Expand Down
162 changes: 114 additions & 48 deletions cartridge/shop/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

from collections import defaultdict

from django.contrib.auth.decorators import login_required
Expand All @@ -12,6 +11,8 @@
from django.utils import simplejson
from django.utils.translation import ugettext as _
from django.views.decorators.cache import never_cache
from django.views.generic import DetailView
from django.views.generic.edit import BaseFormView

from mezzanine.conf import settings
from mezzanine.utils.importing import import_dotted_path
Expand All @@ -31,55 +32,120 @@
order_handler = handler(settings.SHOP_HANDLER_ORDER)


def product(request, slug, template="shop/product.html"):
class DetailViewWithForm(DetailView, BaseFormView):
"""
Display a product - convert the product variations to JSON as well as
handling adding the product to either the cart or the wishlist.
A detail view of an object, with form processing
"""
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)

def get(self, request, *args, **kwargs):
self.object = self.get_object()
return BaseFormView.get(self, request, *args, **kwargs)

def post(self, request, *args, **kwargs):
self.object = self.get_object()
return BaseFormView.post(self, request, *args, **kwargs)

def get_context_data(self, **kwargs):
return DetailView.get_context_data(self, **kwargs)


class ProductDetailView(DetailViewWithForm):
"""
Display a product

Handle adding the product to either the cart or the wishlist.
"""

context_object_name = "product"
model = Product
template_name = "shop/product.html"
form_class = AddProductForm

def get_initial(self):
"""
Returns the dict that will be passed to the AddProductForm
constructor as the `initial` parameter
"""
product = self.object
variations = product.variations.all()
fields = [f.name for f in ProductVariation.option_fields()]
initial_data = {}
if variations:
initial_data = dict([(f, getattr(variations[0], f))
for f in fields])
initial_data["quantity"] = 1
return initial_data

def get_form_kwargs(self):
"""
Return the argumentst that will be passed to AddProductForm
constructor, other than `initial` and `data`
"""
kwargs = super(ProductDetailView, self).get_form_kwargs()
kwargs['product'] = self.object
kwargs['to_cart'] = self.to_cart()
return kwargs

def get_queryset(self):
"""
Restrict viewable products to ones that have been published for
the user
"""
return Product.objects.published(for_user=self.request.user)

def to_cart(self):
"""
Return True if product should be added to cart, False if should be
added to wishlist
"""
return (self.request.method == "POST" and
self.request.POST.get("add_wishlist") is None)

def form_valid(self, form):
"""
Called after form has been validated.
"""
add_product_form = form
if self.to_cart():
quantity = add_product_form.cleaned_data["quantity"]
self.request.cart.add_item(add_product_form.variation, quantity)
recalculate_discount(self.request)
info(self.request, _("Item added to cart"))
return redirect("shop_cart")
else:
skus = self.request.wishlist
sku = add_product_form.variation.sku
if sku not in skus:
skus.append(sku)
info(self.request, _("Item added to wishlist"))
response = redirect("shop_wishlist")
set_cookie(response, "wishlist", ",".join(skus))
return response

def get_context_data(self, **kwargs):
"""
Return additional variables to be passed to the template
"""
context = super(ProductDetailView, self).get_context_data(**kwargs)
product = self.object
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])
context["editable_obj"] = product
context["images"] = product.images.all()
context["variations"] = variations
context["variations_json"] = variations_json
context["has_available_variations"] = any([v.has_price() for v in
variations])
context["related_products"] = product.related_products.published(
for_user=self.request.user)

# Since the existing template uses add_product_form, we switch
# the form key to add_product_form
context["add_product_form"] = context.pop("form")
return context


@never_cache
Expand Down