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.
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.
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:
CACHES
setting. Storing session data in a cache system offers best performance.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.
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.
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
instanceSince 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:
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
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:
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.
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.
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:
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:
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:
Change the quantity of an item and click on the Update button to test the new functionality.
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.
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 requestdjango.template.context_processors.request
: This sets the request
variable in the contextdjango.contrib.auth.context_processors.auth
: This sets the user variable in the requestdjango.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.
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.
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: