A simple session-backed shopping cart for modern Django.
Project description
django-cart
A simple, session-backed shopping cart for modern Django (4.2+) and Python 3.10+.
django-cart uses Django's content-type framework so you can use any model as a product — no changes required to your existing code.
Table of Contents
- Features
- Requirements
- Installation
- Quick Start
- Cart API Reference
- Template Example
- Cleaning Old Carts
- Scheduling with Cron
- Running the Tests
- Changelog
Features
- Session-linked cart backed by a lightweight DB record
- Works with any product model via Django's generic foreign keys
add,remove,update,clear,checkoutoperationscount,summary,is_empty,cart_serializablehelpers- Management command
clean_cartswith configurable retention window - Full test suite covering success, error, and edge cases
Requirements
| Dependency | Version |
|---|---|
| Python | 3.10+ |
| Django | 4.2+ |
The django.contrib.contenttypes app must be in INSTALLED_APPS (it is by default).
Installation
pip install django-cart
Then add cart to INSTALLED_APPS in your settings.py:
INSTALLED_APPS = [
...
"django.contrib.contenttypes", # must be present
"cart",
]
Run the migrations:
python manage.py migrate cart
Quick Start
1. Add to your views
# views.py
from decimal import Decimal
from django.shortcuts import get_object_or_404, redirect, render
from cart.cart import Cart, ItemDoesNotExist, InvalidQuantity
from shop.models import Product # your own product model
def cart_add(request, product_id):
product = get_object_or_404(Product, pk=product_id)
quantity = int(request.POST.get("quantity", 1))
cart = Cart(request)
cart.add(product, unit_price=product.price, quantity=quantity)
return redirect("cart_detail")
def cart_remove(request, product_id):
product = get_object_or_404(Product, pk=product_id)
cart = Cart(request)
try:
cart.remove(product)
except ItemDoesNotExist:
pass # already gone — not an error in most UX flows
return redirect("cart_detail")
def cart_update(request, product_id):
product = get_object_or_404(Product, pk=product_id)
quantity = int(request.POST.get("quantity", 1))
cart = Cart(request)
try:
cart.update(product, quantity=quantity)
except InvalidQuantity:
pass
return redirect("cart_detail")
def cart_detail(request):
cart = Cart(request)
return render(request, "cart/detail.html", {"cart": cart})
def cart_checkout(request):
cart = Cart(request)
# … process payment …
cart.checkout()
return redirect("order_complete")
2. URL configuration
# urls.py
from django.urls import path
from shop import views
urlpatterns = [
path("cart/", views.cart_detail, name="cart_detail"),
path("cart/add/<int:product_id>/", views.cart_add, name="cart_add"),
path("cart/remove/<int:product_id>/", views.cart_remove, name="cart_remove"),
path("cart/update/<int:product_id>/", views.cart_update, name="cart_update"),
path("cart/checkout/", views.cart_checkout, name="cart_checkout"),
]
Cart API Reference
from cart.cart import Cart, ItemDoesNotExist, InvalidQuantity
cart = Cart(request)
| Method / Property | Description |
|---|---|
cart.add(product, unit_price, quantity=1) |
Add a product. If already present, increments quantity and updates price. Raises InvalidQuantity if quantity < 1. |
cart.remove(product) |
Remove a product entirely. Raises ItemDoesNotExist if not in cart. |
cart.update(product, quantity, unit_price=None) |
Set exact quantity (0 removes the item). Raises ItemDoesNotExist or InvalidQuantity. |
cart.count() |
Total number of units across all items. |
cart.unique_count() |
Number of distinct products. |
cart.summary() |
Grand total as Decimal. |
cart.is_empty() |
True if the cart has no items. |
cart.clear() |
Delete all items (keeps the cart record). |
cart.checkout() |
Mark the cart as checked out. |
cart.cart_serializable() |
Returns a JSON-safe dict keyed by object_id. |
product in cart |
True if the product is in the cart (__contains__). |
len(cart) |
Equivalent to cart.count(). |
for item in cart |
Iterate over Item instances. |
Each Item exposes:
item.product # the product instance (via generic FK)
item.quantity # int
item.unit_price # Decimal
item.total_price # Decimal (quantity × unit_price)
Template Example
{# templates/cart/detail.html #}
{% extends "base.html" %}
{% block content %}
<h1>Your Cart</h1>
{% if cart.is_empty %}
<p>Your cart is empty.</p>
{% else %}
<table>
<thead>
<tr>
<th>Product</th>
<th>Unit Price</th>
<th>Qty</th>
<th>Total</th>
<th></th>
</tr>
</thead>
<tbody>
{% for item in cart %}
<tr>
<td>{{ item.product.name }}</td>
<td>{{ item.unit_price }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.total_price }}</td>
<td>
<a href="{% url 'cart_remove' item.product.pk %}">Remove</a>
</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="3"><strong>Total</strong></td>
<td colspan="2"><strong>{{ cart.summary }}</strong></td>
</tr>
</tfoot>
</table>
<a href="{% url 'cart_checkout' %}">Proceed to Checkout</a>
{% endif %}
{% endblock %}
Cleaning Old Carts
Over time, abandoned sessions leave orphaned Cart rows in your database. The clean_carts management command removes them.
Basic usage
Delete all unchecked-out carts older than 90 days (the default):
python manage.py clean_carts
Custom retention window
Delete abandoned carts older than 30 days:
python manage.py clean_carts --days 30
Include checked-out carts
Remove all carts older than 60 days, including those that were checked out:
python manage.py clean_carts --days 60 --include-checked-out
Dry run
Preview what would be deleted without actually deleting anything:
python manage.py clean_carts --days 30 --dry-run
# [DRY RUN] Would delete 142 cart(s) older than 30 day(s).
All options
| Flag | Default | Description |
|---|---|---|
--days N |
90 |
Delete carts older than N days |
--include-checked-out |
off | Also delete checked-out carts |
--dry-run |
off | Preview only — no deletions |
Scheduling with Cron
Run clean_carts automatically so your database stays clean without manual intervention.
Standard crontab
Open your crontab:
crontab -e
Add a line. For example, to run every day at 2:00 AM and delete carts older than 30 days:
0 2 * * * /path/to/venv/bin/python /path/to/project/manage.py clean_carts --days 30 >> /var/log/clean_carts.log 2>&1
Replace /path/to/venv and /path/to/project with the actual paths on your server.
With environment variables
If your Django project needs environment variables (e.g. DATABASE_URL), load them before calling the command:
0 2 * * * /bin/bash -c 'source /etc/environment && /path/to/venv/bin/python /path/to/project/manage.py clean_carts --days 30' >> /var/log/clean_carts.log 2>&1
Using a Makefile target (optional convenience)
.PHONY: clean-carts
clean-carts:
python manage.py clean_carts --days 30
Django management commands from Celery (alternative)
If you already use Celery Beat for periodic tasks you can call the command from a task instead:
# tasks.py
from celery import shared_task
from django.core.management import call_command
@shared_task
def clean_old_carts():
call_command("clean_carts", days=30)
Running the Tests
Install the development dependencies:
pip install django
Run all tests with the standalone runner:
python runtests.py
Run a specific test class:
python runtests.py tests.test_cart.CartAddTest
The test suite covers:
CartModel— creation, ordering, defaultsItemManager— filter/get by product instanceItemmodel —total_price,unique_togetherconstraintCartclass — all public methods (success, error, and edge cases)clean_cartscommand — deletion, dry-run, boundary conditions, cascade behaviour
Running Code Coverage
Install coverage tool:
pip install coverage
Run tests with coverage:
coverage run runtests.py
Generate a coverage report in the terminal:
coverage report
Generate an HTML coverage report (results saved to htmlcov/):
coverage html
Open the HTML report in your browser:
open htmlcov/index.html # macOS
# or
xdg-open htmlcov/index.html # Linux
# or
start htmlcov/index.html # Windows
Changelog
2.0.0
- Dropped Python 2 / Django < 4.2 support
- Replaced
ugettext_lazy→gettext_lazy - Replaced
__unicode__→__str__ - Replaced
import models→from . import models(relative import) Cart.new()→ privateCart._new();creation_datenow usestimezone.now()instead ofdatetime.datetime.now()Item.item_set→Item.items(related_name="items")- Added
unique_togetherconstraint on(cart, content_type, object_id) - Added
__contains__,__len__toCart - Added
unique_count(),checkout()methods cart_serializable()now includesunit_priceupdate()no longer silently ignoresunit_price=None— only updates price when explicitly provided- Added
InvalidQuantityexception;add()andupdate()now validate quantities - Added
clean_cartsmanagement command - Full test suite added
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_cart-2.2.11.tar.gz.
File metadata
- Download URL: django_cart-2.2.11.tar.gz
- Upload date:
- Size: 21.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
33cfec12a89b89741101d1c6500a959ea49ebefcbd01ca8cc096fb4f3c701833
|
|
| MD5 |
a87299ee077bc4f99e15d8bdd44005da
|
|
| BLAKE2b-256 |
2019ba20ed29317c0bd206dcd29c6949dae6ecfbd92a6651ed82a3b5c549e0f9
|
File details
Details for the file django_cart-2.2.11-py3-none-any.whl.
File metadata
- Download URL: django_cart-2.2.11-py3-none-any.whl
- Upload date:
- Size: 14.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
de5cc4ca7bf10bba9db94d90624e7df00c027f67a36487ebf2d47b93d3ef9faf
|
|
| MD5 |
529b2c17bb3ffbbe989fc4e8fb34feda
|
|
| BLAKE2b-256 |
a1b64f87d1d19abbb105a19beb73607146405c22fd7792f9c3969767c13d6952
|