diff --git a/.gitignore b/.gitignore index 0d20b64..eabaffd 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ *.pyc +/.project +/.pydevproject diff --git a/README.md b/README.md index 0150d5a..bdc3ba6 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/carton/cart.py b/carton/cart.py index e06ece0..9b83dd7 100644 --- a/carton/cart.py +++ b/carton/cart.py @@ -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 @@ -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']) ) @@ -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): diff --git a/carton/module_loading.py b/carton/module_loading.py index 47aa4ef..5bb63e6 100644 --- a/carton/module_loading.py +++ b/carton/module_loading.py @@ -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) diff --git a/carton/settings.py b/carton/settings.py index 20b8dc8..9c7518d 100644 --- a/carton/settings.py +++ b/carton/settings.py @@ -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') diff --git a/carton/tests/cart.py b/carton/tests/cart.py new file mode 100644 index 0000000..cde4cc7 --- /dev/null +++ b/carton/tests/cart.py @@ -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))) diff --git a/carton/tests/tests.py b/carton/tests/tests.py index 6895911..be48c20 100644 --- a/carton/tests/tests.py +++ b/carton/tests/tests.py @@ -1,6 +1,8 @@ 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 @@ -8,11 +10,12 @@ 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') @@ -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) @@ -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