Skip to main content

A simple session-backed shopping cart for modern Django.

Project description

GitHub Actions Workflow Status PyPI - Version

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

  • Session-linked cart backed by a lightweight DB record
  • Works with any product model via Django's generic foreign keys
  • add, remove, update, clear, checkout operations
  • count, summary, is_empty, cart_serializable helpers
  • Management command clean_carts with 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, defaults
  • ItemManager — filter/get by product instance
  • Item model — total_price, unique_together constraint
  • Cart class — all public methods (success, error, and edge cases)
  • clean_carts command — 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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_cart-2.4.1.tar.gz (24.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_cart-2.4.1-py3-none-any.whl (15.7 kB view details)

Uploaded Python 3

File details

Details for the file django_cart-2.4.1.tar.gz.

File metadata

  • Download URL: django_cart-2.4.1.tar.gz
  • Upload date:
  • Size: 24.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for django_cart-2.4.1.tar.gz
Algorithm Hash digest
SHA256 848f6ff6eeca5ea9723084efda28afd2bf634d112a5cdcdb0bc63ae628b6fad4
MD5 a48209aa8c5af59b6da152c8e118a188
BLAKE2b-256 496c51afc6f991a5b330d0432179ea4ca7b66ab20eff669d6a24520655a6c5d2

See more details on using hashes here.

File details

Details for the file django_cart-2.4.1-py3-none-any.whl.

File metadata

  • Download URL: django_cart-2.4.1-py3-none-any.whl
  • Upload date:
  • Size: 15.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for django_cart-2.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 917f4ae5a584ceae9bcb8cc09227f163ac88b640fe6ea4b79a60cc13a5a320d2
MD5 350495b7bf5ac97b76d0a15693345c4e
BLAKE2b-256 fe3e8ea99928effdf9258bc14163bd20f89ceeeb3166d9e24925bf9ef4b5bced

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page