From 41bc2bfc703207f4a0c37b146ff81700d8ab11f7 Mon Sep 17 00:00:00 2001 From: kristijan Date: Tue, 21 Oct 2014 13:56:29 +0200 Subject: [PATCH 01/11] Add generated files from eclipse into git ignore list. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 0d20b64..eabaffd 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ *.pyc +/.project +/.pydevproject From b32188f35fc62de671733e117073d12d4b4e1879 Mon Sep 17 00:00:00 2001 From: kristijan Date: Tue, 21 Oct 2014 14:05:20 +0200 Subject: [PATCH 02/11] Support for customized CartItem class. --- README.md | 7 +++++++ carton/cart.py | 6 ++++-- carton/settings.py | 2 ++ 3 files changed, 13 insertions(+), 2 deletions(-) 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..ab3b2d5 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 = eval(carton_settings.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/settings.py b/carton/settings.py index 20b8dc8..91e5575 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') From ff8d7e033a0be552d30e96f61593eab4e420e6db Mon Sep 17 00:00:00 2001 From: kristijan Date: Tue, 21 Oct 2014 14:29:21 +0200 Subject: [PATCH 03/11] Properly read cart item class from setting. --- carton/cart.py | 2 +- carton/module_loading.py | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/carton/cart.py b/carton/cart.py index ab3b2d5..9b83dd7 100644 --- a/carton/cart.py +++ b/carton/cart.py @@ -39,7 +39,7 @@ class Cart(object): A cart that lives in the session. """ def __init__(self, session, session_key=None): - self.cart_item_class = eval(carton_settings.CART_ITEM_CLASS) + 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 diff --git a/carton/module_loading.py b/carton/module_loading.py index 47aa4ef..d6eb5b4 100644 --- a/carton/module_loading.py +++ b/carton/module_loading.py @@ -2,9 +2,23 @@ from django.utils.importlib import import_module -def get_product_model(): +def _get_module(dotted_path): """ - Returns the product model that is used by this cart. + Returns the entity that is imported from dotted path. """ package, module = settings.CART_PRODUCT_MODEL.rsplit('.', 1) return getattr(import_module(package), module) + + +def get_product_model(): + """ + Returns the product model that is used by this cart. + """ + 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(settings.CART_ITEM_CLASS) From 0433d09403488105784af239e431c7f46ca23178 Mon Sep 17 00:00:00 2001 From: kristijan Date: Tue, 21 Oct 2014 14:36:01 +0200 Subject: [PATCH 04/11] Read CART_ITEM_CLASS from proper settings module. --- carton/module_loading.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/carton/module_loading.py b/carton/module_loading.py index d6eb5b4..a2d88ae 100644 --- a/carton/module_loading.py +++ b/carton/module_loading.py @@ -1,6 +1,8 @@ from django.conf import settings from django.utils.importlib import import_module +from carton import settings as carton_settings + def _get_module(dotted_path): """ @@ -21,4 +23,4 @@ def get_cart_item_class(): """ Returns the class that is used by this cart for its items. """ - return _get_module(settings.CART_ITEM_CLASS) + return _get_module(carton_settings.CART_ITEM_CLASS) From 61dd64eda81094a1c58c1e335f652585283f0011 Mon Sep 17 00:00:00 2001 From: kristijan Date: Tue, 21 Oct 2014 14:36:37 +0200 Subject: [PATCH 05/11] Use passed parameter in mogule loading method. --- carton/module_loading.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/carton/module_loading.py b/carton/module_loading.py index a2d88ae..5bb63e6 100644 --- a/carton/module_loading.py +++ b/carton/module_loading.py @@ -8,7 +8,7 @@ def _get_module(dotted_path): """ Returns the entity that is imported from dotted path. """ - package, module = settings.CART_PRODUCT_MODEL.rsplit('.', 1) + package, module = dotted_path.rsplit('.', 1) return getattr(import_module(package), module) From a9d77c0640f7acf2af7d5d418643eea1308d29d0 Mon Sep 17 00:00:00 2001 From: kristijan Date: Tue, 21 Oct 2014 15:05:30 +0200 Subject: [PATCH 06/11] Add unit test with custom item class. --- carton/tests/cart.py | 21 +++++++++++++++++++++ carton/tests/tests.py | 17 +++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 carton/tests/cart.py diff --git a/carton/tests/cart.py b/carton/tests/cart.py new file mode 100644 index 0000000..c6b8752 --- /dev/null +++ b/carton/tests/cart.py @@ -0,0 +1,21 @@ +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 super(TestCartItem, self).subtotal() + else: + return self.price + \ + self.price * self.quantity * (1 - self._DISCOUNT) diff --git a/carton/tests/tests.py b/carton/tests/tests.py index 6895911..558a594 100644 --- a/carton/tests/tests.py +++ b/carton/tests/tests.py @@ -115,3 +115,20 @@ def test_custom_product_filter_are_applied(self): response = self.client.get(self.url_show) self.assertNotContains(response, 'EXCLUDE') self.assertContains(response, 'deer') + + @override_settings(CART_ITEM_CLASS='carton.tests.cart.TestCartItem') + 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 From 608d4b915870489043c5b9a58d62838b5e19c146 Mon Sep 17 00:00:00 2001 From: kristijan Date: Tue, 21 Oct 2014 15:42:25 +0200 Subject: [PATCH 07/11] Remove not needed space. --- carton/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/carton/settings.py b/carton/settings.py index 91e5575..9c7518d 100644 --- a/carton/settings.py +++ b/carton/settings.py @@ -4,4 +4,4 @@ CART_TEMPLATE_TAG_NAME = getattr(settings, 'CART_TEMPLATE_TAG_NAME', 'get_cart') -CART_ITEM_CLASS = getattr(settings, 'CART_ITEM_CLASS', 'carton.cart.CartItem') +CART_ITEM_CLASS = getattr(settings, 'CART_ITEM_CLASS', 'carton.cart.CartItem') From ace75e687eee12b33416ba0bd04fd35b8af72c91 Mon Sep 17 00:00:00 2001 From: kristijan Date: Tue, 21 Oct 2014 15:43:16 +0200 Subject: [PATCH 08/11] Remove not needed empty line. --- carton/tests/cart.py | 1 - 1 file changed, 1 deletion(-) diff --git a/carton/tests/cart.py b/carton/tests/cart.py index c6b8752..382ca13 100644 --- a/carton/tests/cart.py +++ b/carton/tests/cart.py @@ -13,7 +13,6 @@ 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 super(TestCartItem, self).subtotal() else: From 3d3eb9f60bd174762ea85f73af252042ee709b90 Mon Sep 17 00:00:00 2001 From: kristijan Date: Tue, 21 Oct 2014 15:43:52 +0200 Subject: [PATCH 09/11] Fix unit test for custom cart item class. --- carton/tests/tests.py | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/carton/tests/tests.py b/carton/tests/tests.py index 558a594..b92d137 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,13 @@ def setUp(self): self.deer_data = {'product_id': self.deer.pk} self.moose_data = {'product_id': self.moose.pk} + def tearDown(self): + self.deer.delete() + 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) @@ -116,11 +126,26 @@ def test_custom_product_filter_are_applied(self): self.assertNotContains(response, 'EXCLUDE') self.assertContains(response, 'deer') - @override_settings(CART_ITEM_CLASS='carton.tests.cart.TestCartItem') + +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`: From aa13be55c14731abf1d30d4e2e7fcef4d6998a4c Mon Sep 17 00:00:00 2001 From: kristijan Date: Tue, 21 Oct 2014 15:52:57 +0200 Subject: [PATCH 10/11] Fix tests. --- carton/tests/tests.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/carton/tests/tests.py b/carton/tests/tests.py index b92d137..be48c20 100644 --- a/carton/tests/tests.py +++ b/carton/tests/tests.py @@ -26,8 +26,12 @@ def setUp(self): self.moose_data = {'product_id': self.moose.pk} def tearDown(self): - self.deer.delete() - self.moose.delete() + # 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): @@ -156,4 +160,4 @@ def test_custom_cart_item_is_used(self): 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 + self.assertContains(response, '2 deer for $18.0') # instead of $20.0 From 7b85f3c62011b546df645e15d605084eeb7008c1 Mon Sep 17 00:00:00 2001 From: kristijan Date: Tue, 21 Oct 2014 15:53:21 +0200 Subject: [PATCH 11/11] Fix test cart item class. --- carton/tests/cart.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/carton/tests/cart.py b/carton/tests/cart.py index 382ca13..cde4cc7 100644 --- a/carton/tests/cart.py +++ b/carton/tests/cart.py @@ -1,3 +1,5 @@ +from decimal import Decimal + from carton.cart import CartItem @@ -14,7 +16,7 @@ def subtotal(self): entity of the same item in the cart is lowered for some discount. """ if self.quantity == 1: - return super(TestCartItem, self).subtotal() + return self.price else: - return self.price + \ - self.price * self.quantity * (1 - self._DISCOUNT) + return self.price * Decimal(str( + 1 + (self.quantity - 1) * (1 - self._DISCOUNT)))