Building a shopping cart

After building the product catalog, the next step is to create a shopping cart that will allow users to select the products that they want to purchase. A shopping cart allows users to select the products they want and store them temporarily while they browse the site, until they eventually place an order. The cart has to be persisted in the session so that the cart items are kept during the user's visit.

We will use Django's session framework to persist the cart. The cart will be kept in the session until it finishes or the user checks out of the cart. We will also need to build additional Django models for the cart and its items.

Using Django sessions

Django provides a session framework that supports anonymous and user sessions. The session framework allows you to store arbitrary data for each visitor. Session data is stored on the server side, and cookies contain the session ID, unless you use the cookie-based session engine. The session middleware manages sending and receiving cookies. The default session engine stores session data in the database, but as you will see next, you can choose between different session engines.

To use sessions, you have to make sure that the MIDDLEWARE_CLASSES setting of your project contains 'django.contrib.sessions.middleware.SessionMiddleware'. This middleware manages sessions and is added by default when you create a new project using the startproject command.

The session middleware makes the current session available in the request object. You can access the current session using request.session, by utilizing it similar to a Python dictionary to store and retrieve session data. The session dictionary accepts any Python object by default that can be serialized into JSON. You can set a variable in the session like this:

request.session['foo'] = 'bar'

Retrieve a session key:

request.session.get('foo')

Delete a key you stored in the session:

del request.session['foo']

As you saw, we just treated request.session like a standard Python dictionary.

Note

When users log into the site, their anonymous session is lost and a new session is created for the authenticated users. If you store items in an anonymous session that you need to keep after users log in, you will have to copy the old session data into the new session.

Session settings

There are several settings you can use to configure sessions for your project. The most important is SESSION_ENGINE. This setting allows you to set the place where sessions are stored. By default, Django stores sessions in the database using the Session model of the django.contrib.sessions application.

Django offers the following options for storing session data:

  • Database sessions: Session data is stored in the database. This is the default session engine.
  • File-based sessions: Session data is stored in the file system.
  • Cached sessions: Session data is stored in a cache backend. You can specify cache backends using the CACHES setting. Storing session data in a cache system offers best performance.
  • Cached database sessions: Session data is stored in a write-through cache and database. Reads only use the database if the data is not already in the cache.
  • Cookie-based sessions: Session data is stored in the cookies that are sent to the browser.

Note

For better performance, use a cache-based session engine. Django supports Memcached and there are other third-party cache backends for Redis and other cache systems.

You can customize sessions with other settings. Here are some of the important session related settings:

  • SESSION_COOKIE_AGE: This is the duration of session cookies in seconds. The default value is 1209600 (2 weeks).
  • SESSION_COOKIE_DOMAIN: This domain is used for session cookies. Set this to .mydomain.com to enable cross-domain cookies.
  • SESSION_COOKIE_SECURE: This is a boolean indicating that the cookie should only be sent if the connection is an HTTPS connection.
  • SESSION_EXPIRE_AT_BROWSER_CLOSE: This is a boolean indicating that the session has to expire when the browser is closed.
  • SESSION_SAVE_EVERY_REQUEST: This is a boolean that, if True, will save the session to the database on every request. The session expiration is also updated each time.

You can see all the session settings at https://docs.djangoproject.com/en/1.8/ref/settings/#sessions.

Session expiration

You can choose to use browser-length sessions or persistent sessions using the SESSION_EXPIRE_AT_BROWSER_CLOSE setting. This is set to False by default, forcing the session duration to the value stored in the SESSION_COOKIE_AGE setting. If you set SESSION_EXPIRE_AT_BROWSER_CLOSE to True, the session will expire when the user closes the browser, and the SESSION_COOKIE_AGE setting will not have any effect.

You can use the set_expiry() method of request.session to overwrite the duration of the current session.

Storing shopping carts in sessions

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:

  • id of a Product instance
  • The quantity selected for this product
  • The unit price for this product

Since product prices may vary, we take the approach of storing the product's price along with the product itself when it's added to the cart. By doing so, we keep the same price that users saw when they added the item to the cart, even if the product's price is changed afterward.

Now, you have to manage creating carts and associate them with sessions. The shopping cart has to work as follows:

  • When a cart is needed, we check if 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 key.
  • 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 database.

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 Django sessions are 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:

python manage.py startapp cart

Then, edit the settings.py file of your project and add 'cart' to the INSTALLED_APPS setting as follows:

INSTALLED_APPS = (
    # ...
    'shop',
    '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 shop.models import Product

class Cart(object):

    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 set an empty cart just by setting an empty dictionary in the session. We expect our cart dictionary to use product IDs as keys and a dictionary with quantity and price as value for each key. By doing so, we can guarantee that a product is not added more than once in the cart; we can also simplify the way to access any cart item data.

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

def add(self, product, quantity=1, update_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 update_quantity:
        self.cart[product_id]['quantity'] = quantity
    else:
        self.cart[product_id]['quantity'] += quantity
    self.save()

def save(self):
    # update the session cart
    self.session[settings.CART_SESSION_ID] = self.cart
    # mark the session as "modified" to make sure it is saved
    self.session.modified = True

The add() method takes the following parameters:

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

We use the product id as a key in the cart contents 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 for the product. The product's price is converted from Decimal into string in order to serialize it. Finally, we call the save() method to save the cart in the session.

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

We also need a method for removing products from the cart. Add the following method to the Cart class:

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, you can define an __iter__() method in your class. Add the following method to the Cart 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)
    for product in products:
        self.cart[str(product.id)]['product'] = product

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

In the __iter__() method, we retrieve the Product instances that are present in the cart to include them in the cart items. Finally, we iterate over the cart items converting the item's price back into Decimal and add a total_price attribute to each item. Now, we can easily iterate over the items in the cart.

We also need a way to return the number of total items in the cart. When the len() function is executed on an object, Python calls its __len__() method to retrieve its length. We are going to define a custom __len__() method to return the total number of items stored in the cart. Add the following __len__() method to the Cart class:

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

We return the sum of the quantities of all the cart items.

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

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

And finally, add a method to clear the cart session:

def clear(self):
    # remove cart from session
   del self.session[settings.CART_SESSION_ID]
        self.session.modified = True

Our Cart class is now ready to manage shopping carts.

Creating shopping cart views

Now that we have a Cart class to manage the cart, we need to create the views to add, update, or remove items from it. We need to create the following views:

  • A view to add or update items in a cart, which can handle current and new quantities
  • A view to remove items from the cart
  • A view to display cart items and totals

Adding items to the cart

In order to add items to the cart, we 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)
    update = 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-20. We use a TypedChoiceField field with coerce=int to convert the input into an integer.
  • update: This allows you to indicate whether the quantity has to be added to any existing quantity in the cart for this product (False), or if the existing quantity has to be updated with the given quantity (True). We use a HiddenInput widget for this field, since we don't want to display it to the user.

Let's create a view for adding items to the cart. Edit the views.py file of the cart application and add the following code to it:

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

@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'],
                 update_quantity=cd['update'])
    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, since this view is going to change data. The view receives the product ID as parameter. We retrieve the Product instance with the given ID and validate CartAddProductForm. If the form is valid, we will either add or update the product in the cart. The view redirects to the cart_detail URL that will display the contents of the cart. We are going to create the cart_detail view shortly.

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

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 parameter. We retrieve the Product instance with the given id and remove the product from the cart. Then, we redirect the user to the cart_detail URL.

Finally, we need a view to display the cart and its items. Add the following view to the views.py file:

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

The cart_detail view gets the current cart to display it.

We have created views to add items to the cart, update quantities, remove items from the cart, and display the cart. Let's add URL patterns for these views. Create a new file inside the cart application directory and name it urls.py. Add the following URLs to it:

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^$', views.cart_detail, name='cart_detail'),
    url(r'^add/(?P<product_id>d+)/$',
        views.cart_add,
        name='cart_add'),
    url(r'^remove/(?P<product_id>d+)/$',
        views.cart_remove,
        name='cart_remove'),
]

Edit the main urls.py file of myshop and add the following URL pattern to include the cart URLs:

urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    url(r'^cart/', include('cart.urls', namespace='cart')),
    url(r'^', include('shop.urls', namespace='shop')),
]

Make sure that you include this URL pattern before the shop.urls pattern, since it's more restrictive than the latter.

Building a template to display the cart

The cart_add and cart_remove views don't render any templates, but we need to create a template for the cart_detail view to display cart items and totals.

Create the following file structure inside the cart application directory:

templates/
    cart/
        detail.html

Edit the cart/detail.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 }}">
                <img src="{% if product.image %}{{ product.image.url }}{% else %}{% static "img/no_image.png" %}{% endif %}">                    
              </a>
            </td>
            <td>{{ product.name }}</td>
            <td>{{ item.quantity }}</td>
            <td><a href="{% url "cart:cart_remove" product.id %}">Remove</a></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 "shop:product_list" %}" class="button light">Continue shopping</a>
    <a href="#" class="button">Checkout</a>
  </p>
{% endblock %}

This is the template that is used to display the cart contents. It contains a table with the items stored in the current cart. We allow users to change the quantity for the selected products using a form that is posted to the cart_add view. We also allow users to remove items from the cart by providing a Remove link for each of them.

Adding products to the cart

Now, we need to add an Add to cart button to the product detail page. Edit the views.py file of the shop application, and add CartAddProductForm to the product_detail view like this:

from cart.forms import CartAddProductForm

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 the product's price like this:

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

Make sure the development server is running with the command python manage.py runserver. Now, open http://127.0.0.1:8000/ in your browser and navigate to a product detail page. It now contains a form to choose a quantity before adding the product to the cart. The page will look like this:

Adding products to the cart

Choose a quantity and click on the Add to cart button. The form is submitted to the cart_add view via POST. The view adds the product to the cart in the session, including its current price and the selected quantity. Then, it redirects the user to the cart detail page, which will look like the following screenshot:

Adding products to the cart

Updating product quantities in the cart

When users see the cart, they might want to change product quantities before placing an order. We are going to allow users to change quantities from the cart detail page.

Edit the views.py file of the cart application and change the cart_detail view to this:

def cart_detail(request):
    cart = Cart(request)
    for item in cart:
        item['update_quantity_form'] = CartAddProductForm(
                          initial={'quantity': item['quantity'],
                          'update': 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 update field to True so that when we submit the form to the cart_add view, the current quantity is replaced with the new one.

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

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

Replace the previous 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.update }}
    <input type="submit" value="Update">
    {% csrf_token %}
  </form>
</td>

Open http://127.0.0.1:8000/cart/ in your browser. You will see a form to edit the quantity for each cart item, shown as follows:

Updating product quantities in the cart

Change the quantity of an item and click on the Update button to test the new functionality.

Creating a context processor for the current cart

You might have noticed that we are still showing the message Your cart is empty in the header of the site. When we start adding items to the cart, we will see the total number of items in the cart and the total cost instead. Since this is something that should be displayed in all the pages, we will build a context processor to include the current cart in the request context, regardless of the view that has been processed.

Context processors

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

By default, when you create a new project using the startproject command, your project will contain the following template context processors, in the context_processors option inside the TEMPLATES setting:

  • django.template.context_processors.debug: This sets the Boolean debug and sql_queries variables in the context representing the list of SQL queries executed in the request
  • django.template.context_processors.request: This sets the request variable in the context
  • django.contrib.auth.context_processors.auth: This sets the user variable in the request
  • django.contrib.messages.context_processors.messages: This sets a messages variable in the context containing all messages that have been sent using the messages framework.

Django also enables django.template.context_processors.csrf to avoid cross-site request forgery attacks. This context processor is not present in the settings, but it is always enabled and cannot be turned off for security reasons.

You can see the list of all built-in context processors at https://docs.djangoproject.com/en/1.8/ref/templates/api/#built-in-template-context-processors.

Setting the cart into the request context

Let's create a context processor to set the current cart into the request context for templates. We will be able to access this cart in any template.

Create a new file into the cart application directory and name it context_processors.py. Context processors can reside anywhere in your code, but creating them here will keep your code well organized. Add the following code to the file:

from .cart import Cart

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

As you can see, a context processor is a function that receives the request object as parameter, and returns a dictionary of objects that will be available to all the templates rendered using RequestContext. In our context processor, we instantiate the cart using the request object and make it available for the templates as a variable named cart.

Edit the settings.py file of your project and add 'cart.context_processors.cart' to the context_processors option inside the TEMPLATES setting. The setting will look as follows after the change:

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',
            ],
        },
    },
]

Your context processor will now be executed every time a template is rendered using Django's RequestContext. The cart variable will be set in the context for your templates.

Note

Context processors are executed in all the requests that use RequestContext. You might want to create a custom template tag instead of a context processor, if you are going to access the database.

Now, edit the shop/base.html template of the shop application and find the:

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

Replace the previous lines with the following code:

<div class="cart">
  {% with total_items=cart|length %}
    {% if cart|length > 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>

Reload your server using the command python manage.py runserver. Open http://127.0.0.1:8000/ in your browser and add some products to the cart. In the header of the website, you can see the total number of items in the current and the total cost like this:

Setting the cart into the request context
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset