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

Support for custom cart item class #19

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
*.pyc
/.project
/.pydevproject
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,3 +350,10 @@ items. By default they are removed from the cart.
### Session Key

The `CART_SESSION_KEY` settings controls the name of the session key.

### Customized Cart Item class

The `CART_ITEM_CLASS` settings may contain a dotted path to your extension of
`carton.cart.CartItem` class (which is the default value). This might get handy
in case you need some specific pricing model for items (like some discounts for
ordering more than one entity of the same item).
6 changes: 4 additions & 2 deletions carton/cart.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class Cart(object):
A cart that lives in the session.
"""
def __init__(self, session, session_key=None):
self.cart_item_class = module_loading.get_cart_item_class()
self._items_dict = {}
self.session = session
self.session_key = session_key or carton_settings.CART_SESSION_KEY
Expand All @@ -50,7 +51,7 @@ def __init__(self, session, session_key=None):
products_queryset = self.get_queryset().filter(pk__in=ids_in_cart)
for product in products_queryset:
item = cart_representation[str(product.pk)]
self._items_dict[product.pk] = CartItem(
self._items_dict[product.pk] = self.cart_item_class(
product, item['quantity'], Decimal(item['price'])
)

Expand Down Expand Up @@ -98,7 +99,8 @@ def add(self, product, price=None, quantity=1):
else:
if price == None:
raise ValueError('Missing price when adding to cart')
self._items_dict[product.pk] = CartItem(product, quantity, price)
self._items_dict[product.pk] = self.cart_item_class(
product, quantity, price)
self.update_session()

def remove(self, product):
Expand Down
20 changes: 18 additions & 2 deletions carton/module_loading.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
from django.conf import settings
from django.utils.importlib import import_module

from carton import settings as carton_settings


def _get_module(dotted_path):
"""
Returns the entity that is imported from dotted path.
"""
package, module = dotted_path.rsplit('.', 1)
return getattr(import_module(package), module)


def get_product_model():
"""
Returns the product model that is used by this cart.
"""
package, module = settings.CART_PRODUCT_MODEL.rsplit('.', 1)
return getattr(import_module(package), module)
return _get_module(settings.CART_PRODUCT_MODEL)


def get_cart_item_class():
"""
Returns the class that is used by this cart for its items.
"""
return _get_module(carton_settings.CART_ITEM_CLASS)
2 changes: 2 additions & 0 deletions carton/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
CART_SESSION_KEY = getattr(settings, 'CART_SESSION_KEY', 'CART')

CART_TEMPLATE_TAG_NAME = getattr(settings, 'CART_TEMPLATE_TAG_NAME', 'get_cart')

CART_ITEM_CLASS = getattr(settings, 'CART_ITEM_CLASS', 'carton.cart.CartItem')
22 changes: 22 additions & 0 deletions carton/tests/cart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from decimal import Decimal

from carton.cart import CartItem


class TestCartItem(CartItem):
"""
Customized cart item for unit tests.
"""
_DISCOUNT = 0.2 # 20%

@property
def subtotal(self):
"""
The price for the first item is as earlier, but the price for each next
entity of the same item in the cart is lowered for some discount.
"""
if self.quantity == 1:
return self.price
else:
return self.price * Decimal(str(
1 + (self.quantity - 1) * (1 - self._DISCOUNT)))
54 changes: 50 additions & 4 deletions carton/tests/tests.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
from django.core.urlresolvers import reverse
from django.test import TestCase

from carton.tests.models import Product
from carton import settings as carton_settings

try:
from django.test import override_settings
except ImportError:
from django.test.utils import override_settings


class CartTests(TestCase):

class BaseTestCase(TestCase):
def setUp(self):
self.deer = Product.objects.create(name='deer', price=10.0, custom_id=1)
self.moose = Product.objects.create(name='moose', price=20.0, custom_id=2)
self.deer = Product.objects.create(
name='deer', price=10.0, custom_id=1)
self.moose = Product.objects.create(
name='moose', price=20.0, custom_id=2)
self.url_add = reverse('carton-tests-add')
self.url_show = reverse('carton-tests-show')
self.url_remove = reverse('carton-tests-remove')
Expand All @@ -22,6 +25,17 @@ def setUp(self):
self.deer_data = {'product_id': self.deer.pk}
self.moose_data = {'product_id': self.moose.pk}

def tearDown(self):
# Note that in some tests items might be deleted. Do not try to delete
# them twice.
if self.deer.pk is not None:
self.deer.delete()
if self.moose.pk is not None:
self.moose.delete()


class CartTests(BaseTestCase):

def test_product_is_added(self):
self.client.post(self.url_add, self.deer_data)
response = self.client.get(self.url_show)
Expand Down Expand Up @@ -115,3 +129,35 @@ def test_custom_product_filter_are_applied(self):
response = self.client.get(self.url_show)
self.assertNotContains(response, 'EXCLUDE')
self.assertContains(response, 'deer')


class CustomizedCartItemTests(BaseTestCase):
def setUp(self):
super(CustomizedCartItemTests, self).setUp()
# Note that changing the cart item class with @override_setting
# annotation will not work as it was already read in carton_settings
self.old_cart_item_class = carton_settings.CART_ITEM_CLASS
carton_settings.CART_ITEM_CLASS = 'carton.tests.cart.TestCartItem'


def tearDown(self):
carton_settings.CART_ITEM_CLASS = self.old_cart_item_class
super(CustomizedCartItemTests, self).tearDown()


def test_custom_cart_item_is_used(self):
# We use custom (test) cart item with specific method for total price
# calculation, and expect the price to differs for more than one item
# of the same type (according to the discount se in that test class).

self.client.post(self.url_add, self.deer_data)
response = self.client.get(self.url_show)
# This is still the same as in `test_product_is_added`:
self.assertContains(response, '1 deer for $10.0')

# Now add another one:
self.client.post(self.url_add, self.deer_data)
response = self.client.get(self.url_show)
# And for that one, we should have `TestCartItem.DISCOUNT` applied
# (which is 15%):
self.assertContains(response, '2 deer for $18.0') # instead of $20.0