├── 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 | | Product |
12 | Quantity |
13 | Price |
14 |
15 | {% for item in cart.items %}
16 |
17 | | {{ item.product }} |
18 | {{ item.quantity }} |
19 | {{ item.subtotal }} |
20 |
21 | {% endfor %}
22 |
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 |
--------------------------------------------------------------------------------