Skip to main content

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

  • 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

Changelog

2.0.0

  • Dropped Python 2 / Django < 4.2 support
  • Replaced ugettext_lazygettext_lazy
  • Replaced __unicode____str__
  • Replaced import modelsfrom . import models (relative import)
  • Cart.new() → private Cart._new(); creation_date now uses timezone.now() instead of datetime.datetime.now()
  • Item.item_setItem.items (related_name="items")
  • Added unique_together constraint on (cart, content_type, object_id)
  • Added __contains__, __len__ to Cart
  • Added unique_count(), checkout() methods
  • cart_serializable() now includes unit_price
  • update() no longer silently ignores unit_price=None — only updates price when explicitly provided
  • Added InvalidQuantity exception; add() and update() now validate quantities
  • Added clean_carts management command
  • Full test suite added

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.0.0.tar.gz (19.7 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.0.0-py3-none-any.whl (15.7 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: django_cart-2.0.0.tar.gz
  • Upload date:
  • Size: 19.7 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.0.0.tar.gz
Algorithm Hash digest
SHA256 b828d5ed7acd9109be401ca0576b6725a13cdfffd499291ebe41763df5cc455d
MD5 f1f8fa1d57f36d7616c320075f2e6db3
BLAKE2b-256 b5d09119fa9296eb7af1a28b9d36c260c08d1106ae575dc058677da64811f914

See more details on using hashes here.

File details

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

File metadata

  • Download URL: django_cart-2.0.0-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.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6783a794169e7197fd3d8c013230341975e0a350f5fa9d05cabd2e39a4351672
MD5 6e809ec0db0176e1a45a78f09a227e96
BLAKE2b-256 6efa89ca4f3fa521c32b3fc3f75127583a64d1381b5605d1248e0730bcc8c01c

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