Skip to main content

A simple session-backed shopping cart for modern Django.

Project description

GitHub Actions Workflow Status PyPI - Version

django-cart

A lightweight, session-backed shopping cart for Django e-commerce applications. Built for developers who need a robust cart solution without the bloat of full e-commerce platforms.

django-cart uses Django's content-type framework to work with any product model — no modifications to your existing code required.


Table of Contents


Why django-cart?

Feature Benefit
Any Product Model Works with your existing models via generic foreign keys
Session-Backed Lightweight storage, scales to multiple servers
Atomic Operations Safe concurrent cart modifications
Type Hints Full IDE support and static analysis
Extensible Signals, hooks, and custom session adapters
Production-Ready 290 tests, database indexes, cache support

Quick Start

from cart.cart import Cart
from decimal import Decimal

# Add items to cart
cart = Cart(request)
cart.add(product, unit_price=product.price, quantity=2)

# Check cart status
cart.count()         # Total items (e.g., 5)
cart.summary()        # Total price (e.g., Decimal('99.99'))
cart.is_empty()       # Boolean

# Update quantities
cart.update(product, quantity=5)

# Remove items
cart.remove(product)

# Checkout
cart.checkout()

Installation

pip install django-cart

Add to INSTALLED_APPS in settings.py:

INSTALLED_APPS = [
    ...
    "django.contrib.contenttypes",  # Required (default in Django)
    "cart",
]

Run migrations:

python manage.py migrate cart

Basic Usage

Views

# views.py
from django.shortcuts import get_object_or_404, redirect, render
from cart.cart import Cart, ItemDoesNotExist, InvalidQuantity
from shop.models import Product


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 removed
    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):
    return render(request, "cart/detail.html", {"cart": Cart(request)})


def cart_checkout(request):
    cart = Cart(request)
    # Process payment...
    cart.checkout()
    return redirect("order_complete")

URLs

# 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"),
]

API Reference

Cart Class

from cart.cart import Cart, ItemDoesNotExist, InvalidQuantity

cart = Cart(request)

Core Methods

Method Description Returns
cart.add(product, unit_price, quantity=1) Add product to cart Item
cart.remove(product) Remove product None
cart.update(product, quantity, unit_price=None) Update quantity (0 = remove) None
cart.clear() Remove all items None
cart.checkout() Mark cart as checked out None

Query Methods

Method Description Returns
cart.count() Total units in cart int
cart.unique_count() Number of distinct products int
cart.summary() Grand total Decimal
cart.is_empty() Cart has no items bool
cart.contains(product) Product in cart bool

Utility Methods

Method Description Returns
cart.cart_serializable() JSON-safe dict for APIs dict
cart.merge(other_cart, strategy) Merge two carts None
cart.bind_to_user(user) Associate with user account None
cart.add_bulk(items) Add multiple items list[Item]
Cart.get_user_carts(user) Carts for a user (class method) QuerySet

Item Properties

Each item in the cart exposes:

item.product       # Your product model instance
item.quantity      # int
item.unit_price    # Decimal
item.total_price  # Decimal (quantity × unit_price)

Magic Methods

len(cart)              # Same as cart.count()
for item in cart:      # Iterate over items
cart[product]          # Access item by product
product in cart        # Same as cart.contains(product)

Exceptions

Exception When Raised
InvalidQuantity Quantity < 1 or exceeds maximum
ItemDoesNotExist Product not in cart

Advanced Features

Discounts

Apply discount codes to carts with support for percentage and fixed amount discounts:

from cart.cart import Cart, InvalidDiscountError

cart = Cart(request)

# Apply a discount code
cart.apply_discount("SAVE20")  # 20% off

# Check discount info
discount_amount = cart.discount_amount()  # Decimal("20.00")
discount_code = cart.discount_code()       # "SAVE20"

# Remove discount
cart.remove_discount()

# Validation with Discount model
discount = Discount.objects.get(code="SAVE20")
is_valid, message = discount.is_valid_for_cart(cart)

Create discounts with various restrictions:

from cart.models import Discount, DiscountType

discount = Discount.objects.create(
    code="SUMMER2024",
    discount_type=DiscountType.PERCENT,
    value=Decimal("15.00"),        # 15% off
    min_cart_value=Decimal("50.00"),  # Minimum order
    max_uses=100,                  # Limited uses
    valid_from=start_date,
    valid_until=end_date,
)

Tax Calculator

Customize tax calculation for your region:

# settings.py
CART_TAX_CALCULATOR = 'myapp.tax.USStateTaxCalculator'

# myapp/tax.py
from cart.tax import TaxCalculator
from decimal import Decimal

class USStateTax(TaxCalculator):
    def calculate(self, cart):
        subtotal = cart.summary()
        return subtotal * Decimal("0.0825")  # 8.25% tax

# Usage
tax = cart.tax()  # Returns Decimal

Shipping Calculator

Configure shipping options based on cart contents:

# settings.py
CART_SHIPPING_CALCULATOR = 'myapp.shipping.FlatRateShipping'

# myapp/shipping.py
from cart.shipping import ShippingCalculator, ShippingOption
from decimal import Decimal

class FlatRateShipping(ShippingCalculator):
    def calculate(self, cart):
        return Decimal("9.99")
    
    def get_options(self, cart):
        return [
            {"id": "standard", "name": "Standard Shipping", "price": "9.99"},
            {"id": "express", "name": "Express Shipping", "price": "19.99"},
        ]

# Usage
shipping_cost = cart.shipping()  # Decimal
options = cart.shipping_options()  # List of shipping options

Inventory Checker

Validate product availability before adding to cart:

# settings.py
CART_INVENTORY_CHECKER = 'myapp.inventory.StockInventoryChecker'

# myapp/inventory.py
from cart.inventory import InventoryChecker

class StockInventoryChecker(InventoryChecker):
    def is_available(self, product, quantity):
        return product.stock >= quantity
    
    def reserve(self, product, quantity):
        product.stock -= quantity
        product.save()
    
    def release(self, product, quantity):
        product.stock += quantity
        product.save()

# Usage
from cart.cart import InsufficientStock

cart = Cart(request)
try:
    cart.add(product, price, quantity=10, check_inventory=True)
except InsufficientStock:
    print("Not enough stock available")

Cart Merge

Merge guest carts into user carts upon login. Three strategies available:

Strategy Behavior
add Combine quantities (default)
replace Use other cart's quantities
keep_higher Keep maximum quantity
# Login flow example
def login_view(request):
    user = authenticate(request)
    if user:
        login(request, user)
        
        # Get guest cart
        guest_cart = Cart(request)
        
        # Get or create user cart
        user_carts = Cart.get_user_carts(user)
        if user_carts.exists():
            user_cart = Cart(request)
            user_cart.cart = user_carts.first()
        else:
            user_cart = Cart(request)
            user_cart.bind_to_user(user)
        
        # Merge with 'add' strategy (combines quantities)
        user_cart.merge(guest_cart, strategy='add')

User Binding

Persist carts to user accounts across sessions:

# Bind current cart to user
cart = Cart(request)
cart.bind_to_user(request.user)

# Retrieve all carts for a user
user_carts = Cart.get_user_carts(request.user)
for cart in user_carts:
    print(f"Cart {cart.id}: {cart.summary()}")

Bulk Operations

Add or update multiple items efficiently:

cart = Cart(request)

items = [
    {'product': product1, 'unit_price': Decimal("10.00"), 'quantity': 2},
    {'product': product2, 'unit_price': Decimal("20.00"), 'quantity': 1},
    {'product': product3, 'unit_price': Decimal("30.00"), 'quantity': 3},
]

cart.add_bulk(items)  # Atomic operation

Maximum Quantity Limits

Restrict how many units per item:

# settings.py
CART_MAX_QUANTITY_PER_ITEM = 10

# Usage
cart.add(product, price, quantity=5)  # OK
cart.add(product, price, quantity=15)  # Raises InvalidQuantity

Price Validation

Verify passed price matches product's actual price:

from cart.cart import PriceMismatchError

# Product with price attribute
product.price = Decimal("19.99")

# validate_price=True checks price matches
cart.add(product, unit_price=Decimal("19.99"), quantity=1, validate_price=True)  # OK
cart.add(product, unit_price=Decimal("9.99"), quantity=1, validate_price=True)   # Raises PriceMismatchError

Caching

Summary and count results are cached and automatically invalidated on changes:

cart = Cart(request)
cart.add(product, price, quantity=2)

# First call calculates, subsequent calls use cache
total = cart.summary()  # Calculated
total = cart.summary()  # Cached

# Cache invalidates automatically on:
cart.add(product, price, quantity=1)  # Invalidates
cart.update(product, quantity=5)      # Invalidates
cart.remove(product)                  # Invalidates
cart.clear()                          # Invalidates

Template Integration

Cart Template

{% load cart_tags %}

<h1>Your Cart</h1>

{% if cart.is_empty %}
    <p>Your cart is empty.</p>
{% else %}
    <table>
        <thead>
            <tr>
                <th>Product</th>
                <th>Price</th>
                <th>Qty</th>
                <th>Total</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>
            </tr>
            {% endfor %}
        </tbody>
        <tfoot>
            <tr>
                <td colspan="3"><strong>Total</strong></td>
                <td><strong>{{ cart.summary }}</strong></td>
            </tr>
        </tfoot>
    </table>
{% endif %}

Template Tags

{% load cart_tags %}

<p>Items: {% cart_item_count request %}</p>
<p>Total: {% cart_summary request %}</p>

{% if cart_is_empty request %}
    <p>Empty!</p>
{% endif %}

{% cart_link request "btn btn-primary" "View Cart" %}

Signals

Receive notifications on cart events for analytics, logging, or integrations:

Available Signals

Signal When Fired
cart_item_added Item added or quantity increased
cart_item_removed Item removed from cart
cart_item_updated Item quantity changed
cart_checked_out Checkout completed
cart_cleared Cart emptied

Example Handler

# signals.py
from django.dispatch import receiver
from cart.signals import cart_item_added, cart_checked_out


@receiver(cart_item_added)
def on_item_added(sender, cart, product, quantity, **kwargs):
    print(f"{quantity}x {product} added to cart {cart.id}")


@receiver(cart_checked_out)
def on_checkout(sender, cart, **kwargs):
    print(f"Cart {cart.id} checked out: {cart.summary}")

Connect in your app's ready() method:

# myapp/apps.py
class MyAppConfig(AppConfig):
    name = "myapp"

    def ready(self):
        import myapp.signals  # noqa: F401

Session Adapters

Control where the cart ID is stored:

Built-in Adapters

Adapter Use Case
DjangoSessionAdapter Default Django sessions
CookieSessionAdapter Cookie-based (no server sessions)

Using Cookie Storage

# settings.py
from cart.session import CookieSessionAdapter

CARTS_SESSION_ADAPTER_CLASS = CookieSessionAdapter

Custom Adapter

# myapp/session.py
from cart.session import CartSessionAdapter

class RedisSessionAdapter(CartSessionAdapter):
    def __init__(self, request):
        super().__init__(request)
        import redis
        self.redis = redis.Redis(host="localhost", port=6379)

    def _get_session_key(self):
        return f"cart:{self.request.session.session_key}"

    def _set_session_key(self, value):
        self.redis.set(self._get_session_key(), value)

    def _del_session_key(self):
        self.redis.delete(self._get_session_key())
# settings.py
CARTS_SESSION_ADAPTER_CLASS = "myapp.session.RedisSessionAdapter"

Database Optimization

Indexes

Cart automatically uses database indexes for efficient queries on common patterns (cart ID, content type, object ID).

Avoiding N+1 Queries

The Item.product property is cached to prevent repeated database hits:

# Efficient - single query per item
for item in cart:
    print(item.product.name)  # Cached after first access

Serialization for APIs

# JSON-safe dictionary for API responses
data = cart.cart_serializable()
# Returns: {'123': {'quantity': 2, 'unit_price': '9.99', 'total_price': '19.98'}, ...}

Maintenance

Cleaning Old Carts

Remove abandoned carts from your database:

# Delete unchecked-out carts older than 90 days (default)
python manage.py clean_carts

# Custom retention period
python manage.py clean_carts --days 30

# Include checked-out carts
python manage.py clean_carts --days 60 --include-checked-out

# Preview only
python manage.py clean_carts --days 30 --dry-run

Scheduling with Cron

# Run daily at 2 AM
0 2 * * * /path/to/venv/bin/python /path/to/project/manage.py clean_carts --days 30 >> /var/log/clean_carts.log 2>&1

Celery Alternative

# 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)

Testing

Run All Tests

python runtests.py

Run Specific Tests

python runtests.py tests.test_cart.CartAddTest

Test Coverage

pip install coverage
coverage run runtests.py
coverage report
coverage html  # HTML report in htmlcov/

What Gets Tested

  • All cart operations (add, remove, update, clear, checkout)
  • Error handling (InvalidQuantity, ItemDoesNotExist)
  • Edge cases (empty cart, concurrent modifications)
  • Signals and template tags
  • Session adapters
  • Integration with Django

Developer Setup

Pre-commit Hooks

pip install pre-commit
pre-commit install

Hooks: black (formatting), isort (imports), flake8 (linting), mypy (type checking)

Dependencies

pip install -e ".[dev]"

Project Structure

cart/
├── __init__.py
├── admin.py          # Django admin integration
├── apps.py           # App configuration
├── cart.py           # Main Cart class
├── models.py         # Cart and Item models
├── signals.py        # Cart event signals
├── session.py        # Session adapters
├── templatetags/
│   └── cart_tags.py  # Template tags
└── management/
    └── commands/
        └── clean_carts.py

Requirements

Dependency Version
Python 3.10+
Django 4.2+

License

LGPL-3.0

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-3.0.8.tar.gz (50.0 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-3.0.8-py3-none-any.whl (29.7 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for django_cart-3.0.8.tar.gz
Algorithm Hash digest
SHA256 d5d7810a5a387a4e748d366006e1363bef95b51cef3cbfa3c23d6531063b0874
MD5 efbd5ff240bae905f613ba5546c11eb5
BLAKE2b-256 94f55e204950a59607539a80823adef11c433a0062c0233119e43730a9f5dc84

See more details on using hashes here.

File details

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

File metadata

  • Download URL: django_cart-3.0.8-py3-none-any.whl
  • Upload date:
  • Size: 29.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-3.0.8-py3-none-any.whl
Algorithm Hash digest
SHA256 fcee12dbdd37680b0c240a0af33e3a549c4e6ba8bc982de03fe42271445bdcd6
MD5 977786bca311c93e259176365432f9d6
BLAKE2b-256 6ead7d5c8a690667851b2de5c24038735de0eb20996bee4ff76f5aff01c337cb

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