├── LICENSE ├── carton ├── models.py ├── tests │ ├── __init__.py │ ├── models.py │ ├── urls.py │ ├── settings.py │ ├── views.py │ └── tests.py ├── templatetags │ ├── __init__.py │ └── carton_tags.py ├── __init__.py ├── settings.py ├── module_loading.py └── cart.py ├── .gitignore ├── example └── shopping │ ├── __init__.py │ ├── models.py │ ├── urls.py │ ├── templates │ └── shopping │ │ └── show-cart.html │ └── views.py ├── MANIFEST.in ├── setup.py ├── CHANGES └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /carton/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /carton/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/shopping/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/shopping/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /carton/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | include LICENSE 3 | include CHANGES 4 | -------------------------------------------------------------------------------- /carton/__init__.py: -------------------------------------------------------------------------------- 1 | """django-carton is a simple and lightweight application for shopping carts and wish lists.""" 2 | 3 | __version__ = '1.2.1' 4 | -------------------------------------------------------------------------------- /carton/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | CART_SESSION_KEY = getattr(settings, 'CART_SESSION_KEY', 'CART') 4 | 5 | CART_TEMPLATE_TAG_NAME = getattr(settings, 'CART_TEMPLATE_TAG_NAME', 'get_cart') 6 | -------------------------------------------------------------------------------- /carton/tests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Product(models.Model): 5 | custom_id = models.IntegerField(primary_key=True) 6 | name = models.CharField(max_length=255) 7 | price = models.FloatField() 8 | 9 | def __unicode__(self): 10 | return self.name 11 | -------------------------------------------------------------------------------- /example/shopping/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import url, patterns 2 | 3 | 4 | urlpatterns = patterns('shopping.views', 5 | url(r'^add/$', 'add', name='shopping-cart-add'), 6 | url(r'^remove/$', 'remove', name='shopping-cart-remove'), 7 | url(r'^show/$', 'show', name='shopping-cart-show'), 8 | ) 9 | 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | import carton 4 | 5 | 6 | setup( 7 | name='django-carton', 8 | version=carton.__version__, 9 | description=carton.__doc__, 10 | packages=find_packages(), 11 | url='http://github.com/lazybird/django-carton/', 12 | author='lazybird', 13 | long_description=open('README.md').read(), 14 | include_package_data=True, 15 | ) 16 | -------------------------------------------------------------------------------- /carton/module_loading.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | try: 4 | from importlib import import_module 5 | except ImportError: 6 | from django.utils.importlib import import_module 7 | 8 | 9 | def get_product_model(): 10 | """ 11 | Returns the product model that is used by this cart. 12 | """ 13 | package, module = settings.CART_PRODUCT_MODEL.rsplit('.', 1) 14 | return getattr(import_module(package), module) 15 | -------------------------------------------------------------------------------- /carton/tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url, patterns 2 | 3 | 4 | urlpatterns = patterns('carton.tests.views', 5 | url(r'^show/$', 'show', name='carton-tests-show'), 6 | url(r'^add/$', 'add', name='carton-tests-add'), 7 | url(r'^remove/$', 'remove', name='carton-tests-remove'), 8 | url(r'^remove-single/$', 'remove_single', name='carton-tests-remove-single'), 9 | url(r'^clear/$', 'clear', name='carton-tests-clear'), 10 | url(r'^set-quantity/$', 'set_quantity', name='carton-tests-set-quantity'), 11 | ) 12 | -------------------------------------------------------------------------------- /example/shopping/templates/shopping/show-cart.html: -------------------------------------------------------------------------------- 1 | {% load carton_tags %} 2 | 3 | 4 | 5 |

Shopping Cart

6 | 7 | {% get_cart as cart %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% for item in cart.items %} 16 | 17 | 18 | 19 | 20 | 21 | {% endfor %} 22 |
ProductQuantityPrice
{{ item.product }}{{ item.quantity }}{{ item.subtotal }}
23 | 24 | Total: {{ cart.total }} 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /carton/tests/settings.py: -------------------------------------------------------------------------------- 1 | SITE_ID = 1 2 | 3 | DATABASES = { 4 | 'default': { 5 | 'ENGINE': 'django.db.backends.sqlite3', 6 | 'NAME': 'carton-tests.db', 7 | } 8 | } 9 | 10 | INSTALLED_APPS = ( 11 | 'django.contrib.sessions', 12 | 'django.contrib.sites', 13 | 'carton', 14 | 'carton.tests', 15 | ) 16 | 17 | MIDDLEWARE_CLASSES = ( 18 | 'django.contrib.sessions.middleware.SessionMiddleware', 19 | 'django.middleware.common.CommonMiddleware', 20 | ) 21 | 22 | ROOT_URLCONF = 'carton.tests.urls' 23 | 24 | SECRET_KEY = 'any-key' 25 | 26 | CART_PRODUCT_MODEL = 'carton.tests.models.Product' 27 | -------------------------------------------------------------------------------- /example/shopping/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.shortcuts import render 3 | 4 | from carton.cart import Cart 5 | from products.models import Product 6 | 7 | 8 | def add(request): 9 | cart = Cart(request.session) 10 | product = Product.objects.get(id=request.GET.get('id')) 11 | cart.add(product, price=product.price) 12 | return HttpResponse("Added") 13 | 14 | 15 | def remove(request): 16 | cart = Cart(request.session) 17 | product = Product.objects.get(id=request.GET.get('id')) 18 | cart.remove(product) 19 | return HttpResponse("Removed") 20 | 21 | 22 | def show(request): 23 | return render(request, 'shopping/show-cart.html') 24 | -------------------------------------------------------------------------------- /carton/templatetags/carton_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | from carton.cart import Cart 4 | from carton.settings import CART_TEMPLATE_TAG_NAME 5 | 6 | 7 | register = template.Library() 8 | 9 | 10 | def get_cart(context, session_key=None, cart_class=Cart): 11 | """ 12 | Make the cart object available in template. 13 | 14 | Sample usage:: 15 | 16 | {% load carton_tags %} 17 | {% get_cart as cart %} 18 | {% for product in cart.products %} 19 | {{ product }} 20 | {% endfor %} 21 | """ 22 | request = context['request'] 23 | return cart_class(request.session, session_key=session_key) 24 | 25 | 26 | register.simple_tag(takes_context=True, name=CART_TEMPLATE_TAG_NAME)(get_cart) -------------------------------------------------------------------------------- /carton/tests/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | from carton.cart import Cart 4 | from carton.tests.models import Product 5 | 6 | 7 | def show(request): 8 | cart = Cart(request.session) 9 | response = '' 10 | for item in cart.items: 11 | response += '%(quantity)s %(item)s for $%(price)s\n' % { 12 | 'quantity': item.quantity, 13 | 'item': item.product.name, 14 | 'price': item.subtotal, 15 | } 16 | response += 'items count: %s\n' % cart.count 17 | response += 'unique count: %s\n' % cart.unique_count 18 | return HttpResponse(response) 19 | 20 | 21 | def add(request): 22 | cart = Cart(request.session) 23 | product = Product.objects.get(pk=request.POST.get('product_id')) 24 | quantity = request.POST.get('quantity', 1) 25 | discount = request.POST.get('discount', 0) 26 | price = product.price - float(discount) 27 | cart.add(product, price, quantity) 28 | return HttpResponse() 29 | 30 | 31 | def remove(request): 32 | cart = Cart(request.session) 33 | product = Product.objects.get(pk=request.POST.get('product_id')) 34 | cart.remove(product) 35 | return HttpResponse() 36 | 37 | 38 | def remove_single(request): 39 | cart = Cart(request.session) 40 | product = Product.objects.get(pk=request.POST.get('product_id')) 41 | cart.remove_single(product) 42 | return HttpResponse() 43 | 44 | 45 | def clear(request): 46 | cart = Cart(request.session) 47 | cart.clear() 48 | return HttpResponse() 49 | 50 | 51 | def set_quantity(request): 52 | cart = Cart(request.session) 53 | product = Product.objects.get(pk=request.POST.get('product_id')) 54 | quantity = request.POST.get('quantity') 55 | cart.set_quantity(product, quantity) 56 | return HttpResponse() 57 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | django-carton-1.2.1 2 | =================== 3 | 4 | Date: Febuary, 2016. 5 | 6 | * Fix Django 1.9 compatibility issues with the import module 7 | 8 | 9 | * * * 10 | 11 | 12 | django-carton-1.2.0 13 | =================== 14 | 15 | Date: June 19, 2014. 16 | 17 | * #12: Improve the way product model and queryset are handled 18 | 19 | 20 | * * * 21 | 22 | 23 | django-carton-1.1.3 24 | =================== 25 | 26 | Date: March 22, 2014. 27 | 28 | * #13: KeyError when adding item to cart. 29 | 30 | 31 | * * * 32 | 33 | 34 | django-carton-1.1.2 35 | ------------------- 36 | 37 | Date: February 13, 2014. 38 | 39 | * Fixed a but in template tags. 40 | 41 | 42 | * * * 43 | 44 | 45 | django-carton-1.1.1 46 | ------------------- 47 | 48 | Date: February 13, 2014. 49 | 50 | * Released with wrong version in setup. 51 | 52 | * * * 53 | 54 | 55 | django-carton-1.1.0 56 | ------------------- 57 | 58 | Date: February 12, 2014. 59 | 60 | * #10: Changed so only Cart state is saved to session, not the whole cart object 61 | * #9: Doesn't work with django 1.6 out of the box 62 | 63 | 64 | * * * 65 | 66 | 67 | django-carton-1.0.2 68 | ------------------- 69 | 70 | Date: January 9, 2014. 71 | 72 | 73 | * #8: Ability to check if cart is empty 74 | * #7: Ability to serialize cart content 75 | 76 | 77 | * * * 78 | 79 | 80 | django-carton-1.0.1 81 | ------------------- 82 | 83 | Date: November 7, 2013. 84 | 85 | * #6: Django 1.6 compatibility - the `urls` module path have changed 86 | * #5: Support for products that use a custom primary key 87 | * #4: Add the ability to count cart items 88 | 89 | 90 | * * * 91 | 92 | 93 | django-carton-1.0.0 94 | ------------------- 95 | 96 | Date: August 15, 2013. 97 | 98 | * Updated multiple cart support in documentation (#2) 99 | * Improved stale items handling (#1) 100 | * Other minor documentation updates 101 | 102 | 103 | * * * 104 | 105 | 106 | django-carton-0.1.0 107 | ------------------- 108 | 109 | Date: April 17, 2013 110 | 111 | First release of django-carton. 112 | 113 | 114 | * * * 115 | -------------------------------------------------------------------------------- /carton/tests/tests.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.test import TestCase 3 | from carton.tests.models import Product 4 | 5 | try: 6 | from django.test import override_settings 7 | except ImportError: 8 | from django.test.utils import override_settings 9 | 10 | 11 | class CartTests(TestCase): 12 | 13 | def setUp(self): 14 | self.deer = Product.objects.create(name='deer', price=10.0, custom_id=1) 15 | self.moose = Product.objects.create(name='moose', price=20.0, custom_id=2) 16 | self.url_add = reverse('carton-tests-add') 17 | self.url_show = reverse('carton-tests-show') 18 | self.url_remove = reverse('carton-tests-remove') 19 | self.url_remove_single = reverse('carton-tests-remove-single') 20 | self.url_quantity = reverse('carton-tests-set-quantity') 21 | self.url_clear = reverse('carton-tests-clear') 22 | self.deer_data = {'product_id': self.deer.pk} 23 | self.moose_data = {'product_id': self.moose.pk} 24 | 25 | def test_product_is_added(self): 26 | self.client.post(self.url_add, self.deer_data) 27 | response = self.client.get(self.url_show) 28 | self.assertContains(response, '1 deer for $10.0') 29 | 30 | def test_multiple_products_are_added(self): 31 | self.client.post(self.url_add, self.deer_data) 32 | self.client.post(self.url_add, self.moose_data) 33 | response = self.client.get(self.url_show) 34 | self.assertContains(response, '1 deer for $10.0') 35 | self.assertContains(response, '1 moose for $20.0') 36 | 37 | def test_stale_item_is_removed_from_cart(self): 38 | # Items that are not anymore reference in the database should not be kept in cart. 39 | self.client.post(self.url_add, self.deer_data) 40 | self.client.post(self.url_add, self.moose_data) 41 | response = self.client.get(self.url_show) 42 | self.assertContains(response, 'deer') 43 | self.assertContains(response, 'moose') 44 | self.deer.delete() 45 | response = self.client.get(self.url_show) 46 | self.assertNotContains(response, 'deer') 47 | self.assertContains(response, 'moose') 48 | 49 | def test_quantity_increases(self): 50 | self.client.post(self.url_add, self.deer_data) 51 | self.deer_data['quantity'] = 2 52 | self.client.post(self.url_add, self.deer_data) 53 | response = self.client.get(self.url_show) 54 | self.assertContains(response, '3 deer') 55 | 56 | def test_items_are_counted_properly(self): 57 | self.deer_data['quantity'] = 2 58 | self.client.post(self.url_add, self.deer_data) 59 | self.client.post(self.url_add, self.moose_data) 60 | response = self.client.get(self.url_show) 61 | self.assertContains(response, 'items count: 3') 62 | self.assertContains(response, 'unique count: 2') 63 | 64 | def test_price_is_updated(self): 65 | # Let's give a discount: $1.5/product. That's handled on the test views. 66 | self.deer_data['quantity'] = 2 67 | self.deer_data['discount'] = 1.5 68 | self.client.post(self.url_add, self.deer_data) 69 | response = self.client.get(self.url_show) 70 | # subtotal = 10*2 - 1.5*2 71 | self.assertContains(response, '2 deer for $17.0') 72 | 73 | def test_products_are_removed_all_together(self): 74 | self.deer_data['quantity'] = 3 75 | self.client.post(self.url_add, self.deer_data) 76 | self.client.post(self.url_add, self.moose_data) 77 | remove_data = {'product_id': self.deer.pk} 78 | self.client.post(self.url_remove, remove_data) 79 | response = self.client.get(self.url_show) 80 | self.assertNotContains(response, 'deer') 81 | self.assertContains(response, 'moose') 82 | 83 | def test_single_product_is_removed(self): 84 | self.deer_data['quantity'] = 3 85 | self.client.post(self.url_add, self.deer_data) 86 | remove_data = {'product_id': self.deer.pk} 87 | self.client.post(self.url_remove_single, remove_data) 88 | response = self.client.get(self.url_show) 89 | self.assertContains(response, '2 deer') 90 | 91 | def test_quantity_is_overwritten(self): 92 | self.deer_data['quantity'] = 3 93 | self.client.post(self.url_add, self.deer_data) 94 | self.deer_data['quantity'] = 4 95 | self.client.post(self.url_quantity, self.deer_data) 96 | response = self.client.get(self.url_show) 97 | self.assertContains(response, '4 deer') 98 | 99 | def test_cart_items_are_cleared(self): 100 | self.client.post(self.url_add, self.deer_data) 101 | self.client.post(self.url_add, self.moose_data) 102 | self.client.post(self.url_clear) 103 | response = self.client.get(self.url_show) 104 | self.assertNotContains(response, 'deer') 105 | self.assertNotContains(response, 'moose') 106 | 107 | @override_settings(CART_PRODUCT_LOOKUP={'price__gt': 1}) 108 | def test_custom_product_filter_are_applied(self): 109 | # We modify the queryset to exclude some products. For these excluded 110 | # we should not be able to add them in the cart. 111 | exclude = Product.objects.create(name='EXCLUDE', price=0.99, custom_id=100) 112 | exclude_data = {'product_id': exclude.pk} 113 | self.client.post(self.url_add, self.deer_data) 114 | self.client.post(self.url_add, exclude_data) 115 | response = self.client.get(self.url_show) 116 | self.assertNotContains(response, 'EXCLUDE') 117 | self.assertContains(response, 'deer') 118 | -------------------------------------------------------------------------------- /carton/cart.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | from django.conf import settings 4 | 5 | from carton import module_loading 6 | from carton import settings as carton_settings 7 | 8 | 9 | class CartItem(object): 10 | """ 11 | A cart item, with the associated product, its quantity and its price. 12 | """ 13 | def __init__(self, product, quantity, price): 14 | self.product = product 15 | self.quantity = int(quantity) 16 | self.price = Decimal(str(price)) 17 | 18 | def __repr__(self): 19 | return u'CartItem Object (%s)' % self.product 20 | 21 | def to_dict(self): 22 | return { 23 | 'product_pk': self.product.pk, 24 | 'quantity': self.quantity, 25 | 'price': str(self.price), 26 | } 27 | 28 | @property 29 | def subtotal(self): 30 | """ 31 | Subtotal for the cart item. 32 | """ 33 | return self.price * self.quantity 34 | 35 | 36 | class Cart(object): 37 | 38 | """ 39 | A cart that lives in the session. 40 | """ 41 | def __init__(self, session, session_key=None): 42 | self._items_dict = {} 43 | self.session = session 44 | self.session_key = session_key or carton_settings.CART_SESSION_KEY 45 | # If a cart representation was previously stored in session, then we 46 | if self.session_key in self.session: 47 | # rebuild the cart object from that serialized representation. 48 | cart_representation = self.session[self.session_key] 49 | ids_in_cart = cart_representation.keys() 50 | products_queryset = self.get_queryset().filter(pk__in=ids_in_cart) 51 | for product in products_queryset: 52 | item = cart_representation[str(product.pk)] 53 | self._items_dict[product.pk] = CartItem( 54 | product, item['quantity'], Decimal(item['price']) 55 | ) 56 | 57 | def __contains__(self, product): 58 | """ 59 | Checks if the given product is in the cart. 60 | """ 61 | return product in self.products 62 | 63 | def get_product_model(self): 64 | return module_loading.get_product_model() 65 | 66 | def filter_products(self, queryset): 67 | """ 68 | Applies lookup parameters defined in settings. 69 | """ 70 | lookup_parameters = getattr(settings, 'CART_PRODUCT_LOOKUP', None) 71 | if lookup_parameters: 72 | queryset = queryset.filter(**lookup_parameters) 73 | return queryset 74 | 75 | def get_queryset(self): 76 | product_model = self.get_product_model() 77 | queryset = product_model._default_manager.all() 78 | queryset = self.filter_products(queryset) 79 | return queryset 80 | 81 | def update_session(self): 82 | """ 83 | Serializes the cart data, saves it to session and marks session as modified. 84 | """ 85 | self.session[self.session_key] = self.cart_serializable 86 | self.session.modified = True 87 | 88 | def add(self, product, price=None, quantity=1): 89 | """ 90 | Adds or creates products in cart. For an existing product, 91 | the quantity is increased and the price is ignored. 92 | """ 93 | quantity = int(quantity) 94 | if quantity < 1: 95 | raise ValueError('Quantity must be at least 1 when adding to cart') 96 | if product in self.products: 97 | self._items_dict[product.pk].quantity += quantity 98 | else: 99 | if price == None: 100 | raise ValueError('Missing price when adding to cart') 101 | self._items_dict[product.pk] = CartItem(product, quantity, price) 102 | self.update_session() 103 | 104 | def remove(self, product): 105 | """ 106 | Removes the product. 107 | """ 108 | if product in self.products: 109 | del self._items_dict[product.pk] 110 | self.update_session() 111 | 112 | def remove_single(self, product): 113 | """ 114 | Removes a single product by decreasing the quantity. 115 | """ 116 | if product in self.products: 117 | if self._items_dict[product.pk].quantity <= 1: 118 | # There's only 1 product left so we drop it 119 | del self._items_dict[product.pk] 120 | else: 121 | self._items_dict[product.pk].quantity -= 1 122 | self.update_session() 123 | 124 | def clear(self): 125 | """ 126 | Removes all items. 127 | """ 128 | self._items_dict = {} 129 | self.update_session() 130 | 131 | def set_quantity(self, product, quantity): 132 | """ 133 | Sets the product's quantity. 134 | """ 135 | quantity = int(quantity) 136 | if quantity < 0: 137 | raise ValueError('Quantity must be positive when updating cart') 138 | if product in self.products: 139 | self._items_dict[product.pk].quantity = quantity 140 | if self._items_dict[product.pk].quantity < 1: 141 | del self._items_dict[product.pk] 142 | self.update_session() 143 | 144 | @property 145 | def items(self): 146 | """ 147 | The list of cart items. 148 | """ 149 | return self._items_dict.values() 150 | 151 | @property 152 | def cart_serializable(self): 153 | """ 154 | The serializable representation of the cart. 155 | For instance: 156 | { 157 | '1': {'product_pk': 1, 'quantity': 2, price: '9.99'}, 158 | '2': {'product_pk': 2, 'quantity': 3, price: '29.99'}, 159 | } 160 | Note how the product pk servers as the dictionary key. 161 | """ 162 | cart_representation = {} 163 | for item in self.items: 164 | # JSON serialization: object attribute should be a string 165 | product_id = str(item.product.pk) 166 | cart_representation[product_id] = item.to_dict() 167 | return cart_representation 168 | 169 | 170 | @property 171 | def items_serializable(self): 172 | """ 173 | The list of items formatted for serialization. 174 | """ 175 | return self.cart_serializable.items() 176 | 177 | @property 178 | def count(self): 179 | """ 180 | The number of items in cart, that's the sum of quantities. 181 | """ 182 | return sum([item.quantity for item in self.items]) 183 | 184 | @property 185 | def unique_count(self): 186 | """ 187 | The number of unique items in cart, regardless of the quantity. 188 | """ 189 | return len(self._items_dict) 190 | 191 | @property 192 | def is_empty(self): 193 | return self.unique_count == 0 194 | 195 | @property 196 | def products(self): 197 | """ 198 | The list of associated products. 199 | """ 200 | return [item.product for item in self.items] 201 | 202 | @property 203 | def total(self): 204 | """ 205 | The total value of all items in the cart. 206 | """ 207 | return sum([item.subtotal for item in self.items]) 208 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Django Carton 3 | ============= 4 | 5 | 6 | +------+ 7 | /| /| 8 | +-+----+ | django-carton is a simple and lightweight application 9 | | | | | for shopping carts and wish lists. 10 | | +----+-+ 11 | |/ |/ 12 | +------+ 13 | 14 | 15 | 16 | * Simple: You decide how to implement the views, templates and payment 17 | processing. 18 | * Lightweight: The cart lives in the session. 19 | * Just a container: You define your product model the way you want. 20 | 21 | 22 | Usage Example 23 | ------------- 24 | 25 | View: 26 | 27 | from django.http import HttpResponse 28 | 29 | from carton.cart import Cart 30 | from products.models import Product 31 | 32 | def add(request): 33 | cart = Cart(request.session) 34 | product = Product.objects.get(id=request.GET.get('product_id')) 35 | cart.add(product, price=product.price) 36 | return HttpResponse("Added") 37 | 38 | def show(request): 39 | return render(request, 'shopping/show-cart.html') 40 | 41 | 42 | We are assuming here that your products are defined in an application 43 | called ``products``. 44 | 45 | Template: 46 | 47 | {% load carton_tags %} 48 | {% get_cart as cart %} 49 | 50 | {% for item in cart.items %} 51 | {{ item.product.name }} 52 | {{ item.quantity }} 53 | {{ item.subtotal }} 54 | {% endfor %} 55 | 56 | You can also use this convinent shortcut: 57 | {% for product in cart.products %} 58 | {{ product.name }} 59 | {% endfor %} 60 | 61 | Within the template you can access the product id with {{product.id}}. 62 | 63 | Settings: 64 | 65 | CART_PRODUCT_MODEL = 'products.models.Product' 66 | 67 | 68 | This project is shipped with an application example called ``shopping`` 69 | implementing basic add, remove, display features. 70 | To use it, you will need to install the ``shopping`` application and 71 | include the URLs in your project ``urls.py`` 72 | 73 | # settings.py 74 | INSTALLED_APPS = ( 75 | 'carton', 76 | 'shopping', 77 | 'products', 78 | ) 79 | 80 | # urls.py 81 | urlpatterns = patterns('', 82 | url(r'^shopping-cart/', include('shopping.urls')), 83 | ) 84 | 85 | 86 | Assuming you have some products defined, you should be able to 87 | add, show and remove products like this: 88 | 89 | /shopping-cart/add/?id=1 90 | /shopping-cart/show/ 91 | /shopping-cart/remove/?id=1 92 | 93 | 94 | Installation 95 | ------------ 96 | 97 | This application requires Django version 1.4; all versions above should be fine. 98 | 99 | Just install the package using something like pip and add ``carton`` to 100 | your ``INSTALLED_APPS`` setting. 101 | 102 | Add the `CART_PRODUCT_MODEL` setting, a dotted path to your product model. 103 | 104 | This is how you run tests: 105 | 106 | ./manage.py test carton.tests --settings=carton.tests.settings 107 | 108 | 109 | Abstract 110 | -------- 111 | 112 | The cart is an object that's stored in session. Products are associated 113 | to cart items. 114 | 115 | Cart 116 | |-- CartItem 117 | |----- product 118 | |----- price 119 | |----- quantity 120 | 121 | A cart item stores a price, a quantity and an arbitrary instance of 122 | a product model. 123 | 124 | 125 | You can access all your product's attributes, for instance it's name: 126 | 127 | {% for item in cart.items %} 128 | {{ item.price }} 129 | {{ item.quantity }} 130 | {{ item.product.name }} 131 | {% endfor %} 132 | 133 | 134 | 135 | Managing Cart Items 136 | ------------------- 137 | 138 | These are simple operations to add, remove and access cart items: 139 | 140 | >>> apple = Product.objects.all()[0] 141 | >>> cart.add(apple, price=1.5) 142 | >>> apple in cart 143 | True 144 | >>> cart.remove(apple) 145 | >>> apple in cart 146 | False 147 | 148 | >>> orange = Product.objects.all()[1] 149 | >>> cart.add(apple, price=1.5) 150 | >>> cart.total 151 | Decimal('1.5') 152 | >>> cart.add(orange, price=2.0) 153 | >>> cart.total 154 | Decimal('3.5') 155 | 156 | Note how we check weather the product is in the cart - The following 157 | statements are different ways to do the same thing: 158 | 159 | >>> apple in cart 160 | >>> apple in cart.products 161 | >>> apple in [item.product for item in cart.items] 162 | 163 | 164 | The "product" refers to the database object. The "cart item" is where 165 | we store a copy of the product, it's quantity and it's price. 166 | 167 | >>> cart.items 168 | [CartItem Object (apple), CartItem Object (orange)] 169 | 170 | >>> cart.products 171 | [, ] 172 | 173 | 174 | Clear all items: 175 | 176 | >>> cart.clear() 177 | >>> cart.total 178 | 0 179 | 180 | 181 | Increase the quantity by adding more products: 182 | 183 | >>> cart.add(apple, price=1.5) 184 | >>> cart.add(apple) # no need to repeat the price. 185 | >>> cart.total 186 | Decimal('3.0') 187 | 188 | 189 | Note that the price is only needed when you add a product for the first time. 190 | 191 | >>> cart.add(orange) 192 | *** ValueError: Missing price when adding a cart item. 193 | 194 | 195 | You can tell how many items are in your cart: 196 | 197 | >>> cart.clear() 198 | >>> cart.add(apple, price=1.5) 199 | >>> cart.add(orange, price=2.0, quantity=3) 200 | >>> cart.count 201 | 4 202 | >>> cart.unique_count # Regarless of product's quantity 203 | 2 204 | 205 | 206 | You can add several products at the same time: 207 | 208 | >>> cart.clear() 209 | >>> cart.add(orange, price=2.0, quantity=3) 210 | >>> cart.total 211 | Decimal('6') 212 | >>> cart.add(orange, quantity=2) 213 | >>> cart.total 214 | Decimal('10') 215 | 216 | 217 | The price is relevant only the first time you add a product: 218 | 219 | >>> cart.clear() 220 | >>> cart.add(orange, price=2.0) 221 | >>> cart.total 222 | Decimal('2') 223 | >>> cart.add(orange, price=100) # this price is ignored 224 | >>> cart.total 225 | Decimal('4') 226 | 227 | 228 | Note how the price is ignored on the second call. 229 | 230 | 231 | You can change the quantity of product that are already in the cart: 232 | 233 | >>> cart.add(orange, price=2.0) 234 | >>> cart.total 235 | Decimal('2') 236 | >>> cart.set_quantity(orange, quantity=3) 237 | >>> cart.total 238 | Decimal('6') 239 | >>> cart.set_quantity(orange, quantity=1) 240 | >>> cart.total 241 | Decimal('2') 242 | >>> cart.set_quantity(orange, quantity=0) 243 | >>> cart.total 244 | 0 245 | >>> cart.set_quantity(orange, quantity=-1) 246 | *** ValueError: Quantity must be positive when updating cart 247 | 248 | 249 | 250 | Removing all occurrence of a product: 251 | 252 | >>> cart.add(apple, price=1.5, quantity=4) 253 | >>> cart.total 254 | Decimal('6.0') 255 | >>> cart.remove(apple) 256 | >>> cart.total 257 | 0 258 | >>> apple in cart 259 | False 260 | 261 | 262 | Remove a single occurrence of a product: 263 | 264 | >>> cart.add(apple, price=1.5, quantity=4) 265 | >>> cart.remove_single(apple) 266 | >>> apple in cart 267 | True 268 | >>> cart.total 269 | Decimal('4.5') 270 | >>> cart.remove_single(apple) 271 | >>> cart.total 272 | Decimal('3.0') 273 | >>> cart.remove_single(apple) 274 | >>> cart.total 275 | Decimal('1.5') 276 | >>> cart.remove_single(apple) 277 | >>> cart.total 278 | 0 279 | 280 | 281 | Multiple carts 282 | -------------- 283 | 284 | Django Carton has support for using multiple carts in the same project. 285 | The carts would need to be stored in Django session using different session 286 | keys. 287 | 288 | from carton.cart import Cart 289 | 290 | cart_1 = Cart(session=request.session, session_key='CART-1') 291 | cart_2 = Cart(session=request.session, session_key='CART-2') 292 | 293 | 294 | Working With Product Model 295 | -------------------------- 296 | 297 | Django Carton needs to know how to list your product objects. 298 | 299 | The default behaviour is to get the product model using the 300 | `CART_PRODUCT_MODEL` setting and list all products. 301 | 302 | The default queryset manager is used and all products are 303 | retrieved. You can filter products by defining some lookup 304 | parameters in `CART_PRODUCT_LOOKUP` setting. 305 | 306 | # settings.py 307 | 308 | CART_PRODUCT_LOOKUP = { 309 | 'published': True, 310 | 'status': 'A', 311 | } 312 | 313 | 314 | If you need further customization of the way product model and queryset 315 | are retrieved, you can always sub-class the default `Cart` and overwrite 316 | the `get_queryset` method. In that case, you should take into account that: 317 | 318 | * You probably won't need `CART_PRODUCT_MODEL` and `CART_PRODUCT_LOOKUP` 319 | if you get a direct access to your product model and define the 320 | filtering directly on the cart sub-class. 321 | * You probably have to write your own template tag to retrieve the cart 322 | since the default `get_cart` template tag point on the `Cart` class 323 | defined by django-carton. 324 | 325 | 326 | Settings 327 | -------- 328 | 329 | ### Template Tag Name 330 | 331 | You can retrieve the cart in templates using 332 | `{% get_cart as my_cart %}`. 333 | 334 | You can change the name of this template tag using the 335 | `CART_TEMPLATE_TAG_NAME` setting. 336 | 337 | 338 | # In you project settings 339 | CART_TEMPLATE_TAG_NAME = 'get_basket' 340 | 341 | # In templates 342 | {% load carton_tags %} 343 | {% get_basket as my_basket %} 344 | 345 | 346 | ### Stale Items 347 | 348 | Cart items are associated to products in the database. Sometime a product can be found 349 | in the cart when its database instance has been removed. These items are called stale 350 | items. By default they are removed from the cart. 351 | 352 | ### Session Key 353 | 354 | The `CART_SESSION_KEY` settings controls the name of the session key. 355 | --------------------------------------------------------------------------------