Django
Online Shop With Django Part II

Online Shop With Django Part II

In part II we will build a shopping cart. We will create a class for cart. We will build additional features so that user can add several quantities of different items to the cart. User can update quantities to cart or remove the item completely. We will also use a context processor to show total items in our cart and their total cost.

Table of Contents

Creating Cart

We need to create a simple structure that can be serialized to JSON for storing cart items in a session. The cart has to include the following data for each item contained in it.

  • The ID of a product instance
  • The quantity selected for the product
  • The unit price for the product

Next, we have to build functionality to create shopping carts and associate them with sessions. This has to work as follows.

  • When a cart is needed, we check a custom session key is set. If no cart is set in the session, we create a new cart and save it in the cart session keys.
  • For successive requests, we perform the same check and get the cart items from the cart session key. We retrieve the cart items from the session and their related Product objects from the databases.

Edit the settings.py file of your project and add the following setting to it.

..................
CART_SESSION_ID = "cart"
........................

This is the key that we are going to use to store the cart in the user session. Since the Django sessions are managed per visitor, we can use the same cart session key for all sessions.

Let’s create an application for managing shopping carts. Open the terminal and create a new application, running the following command from the project directory.

#terminal
python manage.py startapp cart

Now, add the app to the project.

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'myshop',
    'cart',
]

Create a new file inside the cart application directory and name it cart.py. Add the following code to it.

from decimal import Decimal
from django.conf import settings
from myshop.models import Product

class Cart:
    def __init__(self,request):
        """
        Initialize the cart
        """
        self.session = request.session
        cart = self.session.get(settings.CART_SESSION_ID)
        if not cart:
            #save an empty cart in the session
            cart = self.session[settings.CART_SESSION_ID]={}

        self.cart = cart

        

This is the Cart class that will allow us to manage the shopping cart. We require the cart to be initialized with a request object. We store the current session using self.session = request.session to make it accessible to the other methods of the Cart class.

First, we try to get the cart from the current session using self.session.get(settings.CART_SESSION_ID). If no cart is present in the session, we create an empty cart by setting an empty dictionary in the session.

We will build cart dictionary with product ID as keys, and for each product key, a dictionary will be a value that include quantity and price. By doing this, we can guarantee that a product will not be added more than once to the cart. This way, we can simplify retrieving cart items.

Let’s create a method to add products to the cart or update their quantity. Add the following add() and save() method to the Cart class.

....................................................  
  def add(self,product,quantity=1,override_quantity=False):
        """
        Add a product to the cart or update its quantity
        """
        product_id = str(product.id)
        if product_id not in self.cart:
            self.cart[product_id] = {'quantity':0,'price':str(product.price)}

        if override_quantity:
            self.cart[product_id]['quantity'] = quantity

        else:
            self.cart[product_id]['quantity'] += quantity

        self.save()


    def save(self):
        #mark the session as "modified" to make sure it  gets saved
        self.session.modified = True

The add() method takes the following parameters as input:

  • product: The product instance to add or update in the cart.
  • quantity: An optional integer with the product quantity. This defaults to 1.
  • override_quantity: This is a Boolean that indicates whether the quantity has to be added to the existing quantity (True), or whether the new quantity has to be added to the existing quantity (False).

We use the product ID as a key in the cart’s content dictionary. We convert the product ID into a string because Django uses JSON to serialize session data, and JSON only allows string key names. The product ID is the key, and the value that we persist is a dictionary with quantity and price figures for the product. The product’s price is converted from decimal into a string to serialize it. Finally, we call the save() method to save the cart in the session.

The save() method marks the session as modified using session.modified = True. This tells Django that the session has changed and needs to be saved.

Add the following method to remove the products from the cart.

..............................................
def remove(self,product):
        """
        Remove a product from the cart
        """
        product_id = str(product.id)

        if product_id in self.cart:
            del self.cart['product_id']
            self.save()

The remove() method removes a given product from the cart dictionary and calls the save() method to update the cart in the session.

We will have to iterate through the items contained in the cart and access the related Product instances. To do so, we can define an __iter__() method in our class.

def __iter__(self):
        """
        Iterate over the items in the cart and get the products
        from the database
        """
        product_ids = self.cart.keys()
        #get the product objects and add them to the cart
        products= Product.objects.filter(id__in=product_ids)
        cart = self.cart.copy()
        for product in products:
            cart[str(product.id)]['product'] = product

        for item in cart.values():
            item['price'] = Decimal(item['price'])
            item['total_price'] = item['price']*item['quantity']
            yield item

In the __item__() method, we retrieve the Product instances that are present in the cart to include them in the cart items. We copy the current cart in the cart variable and add the Product instances to it. Finally, we iterate over the cart items, converting each item’s price back into decimal, and adding a total_price attribute to each item. This __iter__() method will allow us to easily iterate over the items in the cart in views and templates.

We also need a way to return the number of total items in the cart.

def __len__(self):
        """
        count all items in the cart.
        """
        return sum(item['quantity'] for item in self.cart.values())
    

Add the following method to calculate the total cost of the items in the cart:

 def get_total_price(self):
        return sum(Decimal(item['price'])*item['quantity'] for item in self.cart.values())
   

Finally, add a method to clear the cart session:

 def clear(self):
        del self.session[settings.CART_SESSION_ID]
        self.save()

Adding items to the cart

To add items to the cart, you need a form that allows the user to select a quantity. Create a forms.py file inside the cart application directory and add the following code to it.

from django import forms

PRODUCT_QUANTITY_CHOICES = [(i,str(i)) for i in range(1,21)]
class CartAddProductForm(forms.Form):
    quantity = forms.TypedChoiceField(choices=PRODUCT_QUANTITY_CHOICES,coerce=int)
    override = forms.BooleanField(required=False,initial=False,widget=forms.HiddenInput)
    

We will use this form to add products to the cart. Our CartAddProductForm class contains the following two fields:

  • quantity: This allows the user to select a quantity between 1 and 20. We use a TypeChoiceField field with corece=int to convert the input into an integer.
  • override: This allows us to indicate whether the quantity has to be added to any existing quantity in the cart for this product (False), or whether the existing quantity has to be overridden with the given quantity (True). We use a HiddenInput widget for this filed, since we do not want to display it to the user.

Creating shopping cart views

Add the following code to cart/views.py to create a view for adding items to the cart.

from django.shortcuts import render,redirect,get_object_or_404
from django.views.decorators.http import require_POST
from myshop.models import Product
from .cart import Cart
from .forms import CartAddProductForm

# Create your views here.
@require_POST
def cart_add(request,product_id):
    cart = Cart(request)
    product = get_object_or_404(Product,id=product_id)
    form = CartAddProductForm(request.POST)

    if form.is_valid():
        cd = form.cleaned_data
        cart.add(product=product,
                 quantity=cd['quantity'],
                 override_quantity=cd['override'])
        return redirect('cart:cart_detail')

This is the view for adding products to the cart or updating quantities for existing products. We use the require_POST decorator to allow only POST requests. The view receives the product ID as a parameter. You retrieve the Product instance with the given ID and validate CartAddProductForm. If the form is valid, we either add or update the product in the cart. The view redirects to the cart_detail URL, which will display the contents of the cart.

We also need a view to remove items from the cart. Add the following code to the views.py file the cart application:

@require_POST
def cart_remove(request,product_id):
    cart = Cart(request)
    product = get_object_or_404(Product,id=product_id)
    cart.remove(product)
    return redirect("cart:cart_detail")

The cart_remove view receives the product ID as a parameter. We use the require_POST decorator to allow only POST requests. We retrieve the Product instance with the given ID and remove the product from the cart. Then, you redirect the user to the cart_detail URL.

Add the following functions to our views.py to display the cart and its items.

def cart_detail(request):
    cart = Cart(request)
    return render(request,'cart/detail.html',{'cart':cart})

The URLs

Create cart/urls.py and add the following code.

from django.urls import path
from . import views

app_name = "cart"

urlpatterns = [
    path("",views.cart_detail,name='cart_detail'),
    path("add/<int:product_id>",views.cart_add,name="cart_add"),
    path("remove/<int:product_id>/",views.cart_remove,name='cart_remove'),
    
]

Edit the main urls.py file of the project.


from django.contrib import admin
from django.urls import path,include

from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('cart/',include('cart.urls',namespace='cart'))
    path("",include('myshop.urls',namespace='myshop')),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)

Building a template to display the cart

Create the following file structure inside the cart application directory:

Edit the cart/detatil.html template and add the following code to it.

{% extends "shop/base.html" %}
{% load static %}

{% block title %}
    Your shopping cart

{% endblock %}

{% block content %}

    <h1>
        Your shopping cart
    </h1>

    <table class="cart">
        <thead>
            <tr>
                <th>
                    Image
                </th>
                <th>
                    Product
                </th>
                <th>
                    Quantity
                </th>
                <th>
                    Remove
                </th>
                <th>
                    Unit Price
                </th>
                <th>
                    Price
                </th>
            </tr>

        </thead>
        <tbody>
            {% for item in cart %}
                {% with product=item.product %}
                    <tr>
                        <td>
                            <a href="{{ product.get_absolute_url }}">
                                {% if product.image %}
                                <img src="{{ product.image.url }}">
                                {% else %}
                                {% static "img/no_image.png" %}
                                {% endif %}
                            </a>
                        </td>
                        <td>
                            {{ product.name }}
                        </td>
                        <td>
                            {{ item.quantity }}
                        </td>
                        <td>
                            <form action="{% url "cart:cart_remove" product.id %}" method="post">
                                <input type="submit" value="Remove">
                                {% csrf_token%}
                            </form>
                        </td>
                        <td class="num">${{ item.price}}</td>
                        <td class="num">$ {{ item.total_price }}</td>
                    </tr>
                    {% endwith %}
                {% endfor %}
                <tr class="total">
                    <td>Total</td>
                    <td colspan="4"></td>
                    <td class="num">
                        ${{ cart.get_total_price }}
                    </td>
                </tr>
        </tbody>
        
    </table>
    <p class="text-right">
        <a href="{% url "myshop:product_list"%}" class="button light">
            Continue shopping
        </a>
        <a href="#" class="button">
            Checkout
        </a>
    </p>
{% endblock %}

Adding products to the cart

We need to add an Add to Cart button to the product detail page. Edit the views.py file of the myshop application and add CartAddProductForm to the produt_detail view, as follow:

from django.shortcuts import render,get_object_or_404
from cart.forms import CartAddProductForm

from .models import Category,Product
..............................................

def product_detail(request,id,slug):
    product = get_object_or_404(Product,id=id,slug=slug,available=True)

    cart_product_form = CartAddProductForm()

    return render(request,'shop/product/detail.html',{'product':product,
                                                      'cart_product_form':cart_product_form})

Edit the shop/product/detail.html template of the shop application and add the following form to the product price as follows.


.................................................
            <a href="{{ product.category.get_absolute_url}}">
                {{ product.category }}
            </a>
        </h2>
        <p class="price">
            ${{ product.price }}
        </p>
        <form action="{% url "cart:cart_add" product.id %}" method="post">
            {{ cart_product_form }}
            {% csrf_token %}
            <input type="submit" value="Add to cart">
        </form>
        {{ product.description|linebreaks }}
    </div>
{% endblock %}

Run the server

Updating product quantities in the cart

When users see the cart, they might want to change product quantities before placing an order. Edit the views.py file of cart application and add the following code.

def cart_detail(request):
    cart = Cart(request)
    for item in cart:
        item['update_quantity_form'] = CartAddProductForm(initial={
            'quantity':item['quantity'],
            'override':True
        })
    return render(request,'cart/detail.html',{'cart':cart})

We create an instance of CartAddProductForm for each item in the cart to allow changing product quantities. We initialize the form with the current item quantity and set the override field to True so that when you submit the form to the cart_add view, the current quantity is replaced with the new one.

Now, edit cart/detail.html template of the cart application and find the following code.

<td> {{item.quantity}}</td>

Replace the above line with the following code.

<td>
                            
                            <form action="{% url "cart:cart_add" product.id %}" method="post">
                                {{ item.update_quantity_form.quantity }}
                                {{ item.update_quantity_form.override }}
                                <input type="submit" value="Update">
                                {% csrf_token %}
                            </form>
                        </td>

After update

Creating a context processor for the current cart

We see the message Your cart is empty is displayed in the header of the site, even when the cart contains items. We should display the total number of items in the cart and the total cost instead. Since this has to be displayed on all pages, we need to build a context processor to include the current cart in the request context, regardless of the view that processes the request.

Context processors

A context processors is a Python function that takes the request object as an argument and returns a dictionary that gets added to the request context. Context processors come in handy when we need to make something available globally to all templates.

Setting the cart into the request context

Let’s create a context processor to set the current cart into the request context. With it, we will be able to access the cart in any template.

Create cart/context_processors.py.

from .cart import Cart

def cart(request):
    return {'cart':Cart(request)}

Now add this context to our settings.

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'cart.context_processors.cart',
            ],
        },
    },
]

The cart context processor will be executed every time a template is rendered using Django’s RequestContext. The cart variable will be set in the context of our templates.

Now, open the shop/base.html and find the following lines.

<div class="cart">
            Your cart is empty
</div>

Replace above code with the following code.

<div class="cart">
            {% with total_items=cart|length %}
                {% if total_items > 0 %}
                    Your cart:
                    <a href="{% url "cart:cart_detail" %}">
                        {{ total_items }}item {{ total_items|pluralize }}
                        ${{ cart.get_total_price }}
                    </a>
                {% else %}
                    Your cart is empty 
                {% endif %}

            {% endwith %}
        </div>