In the previous chapter, you created a follower system and built a user activity stream. You also learned how Django signals work and integrated Redis into your project to count image views. In this chapter, you will learn how to build a basic online shop. You will create a catalog of products and implement a shopping cart using Django sessions. You will also learn how to create custom context processors and launch asynchronous tasks using Celery.
In this chapter, you will learn to:
We are going to start with a new Django project to build an online shop. Our users will be able to browse through a product catalog and add products to a shopping cart. Finally, they will be able to checkout the cart and place an order. This chapter will cover the following functionalities of an online shop:
First, create a virtual environment for your new project and activate it with the following commands:
mkdir env virtualenv env/myshop source env/myshop/bin/activate
Install Django in your virtual environment with the following command:
pip install Django==1.8.5
Start a new project called myshop
with an application called shop
by opening a shell and running the following commands:
django-admin startproject myshop cd myshop/ django-admin startapp shop
Edit the settings.py
file of your project and add your application to the INSTALLED_APPS
setting as follows:
INSTALLED_APPS = (
# ...
'shop',
)
Your application is now active for this project. Let's define the models for the product catalog.
The catalog of our shop will consist of products that are organized into different categories. Each product will have a name, optional description, optional image, a price, and an available stock. Edit the models.py
file of the shop
application that you just created and add the following code:
from django.db import models class Category(models.Model): name = models.CharField(max_length=200, db_index=True) slug = models.SlugField(max_length=200, db_index=True, unique=True) class Meta: ordering = ('name',) verbose_name = 'category' verbose_name_plural = 'categories' def __str__(self): return self.name class Product(models.Model): category = models.ForeignKey(Category, related_name='products') name = models.CharField(max_length=200, db_index=True) slug = models.SlugField(max_length=200, db_index=True) image = models.ImageField(upload_to='products/%Y/%m/%d', blank=True) description = models.TextField(blank=True) price = models.DecimalField(max_digits=10, decimal_places=2) stock = models.PositiveIntegerField() available = models.BooleanField(default=True) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) class Meta: ordering = ('name',) index_together = (('id', 'slug'),) def __str__(self): return self.name
These are our Category
and Product
models. The Category
model consists of a name
field and a slug
unique field. The Product
model fields are as follows:
category
: This is ForeignKey
to the Category
model. This is a many-to-one relationship: A product belongs to one category and a category contains multiple products.name
: This is the name of the product.slug
: This is the slug for this product to build beautiful URLs.image
: This is an optional product image.description
: This is an optional description of the product.price
: This is DecimalField
. This field uses Python's decimal.Decimal
type to store a fixed-precision decimal number. The maximum number of digits (including the decimal places) is set using the max_digits
attribute and decimal places with the decimal_places
attribute.stock
: This is PositiveIntegerField
to store the stock of this product.available
: This is a boolean that indicates whether the product is available or not. This allows us to enable/disable the product in the catalog.created
: This field stores when the object was created.updated
: This field stores when the object was last updated.For the price
field, we use DecimalField
instead of FloatField
to avoid rounding issues.
In the Meta
class of the Product
model, we use the index_together
meta option to specify an index for the id
and slug
fields together. We define this index, because we plan to query products by both, id
and slug
. Both fields are indexed together to improve performances for queries that utilize the two fields.
Since we are going to deal with images in our models, open the shell and install Pillow with the following command:
pip install Pillow==2.9.0
Now, run the next command to create initial migrations for your project:
python manage.py makemigrations
You will see the following output:
Migrations for 'shop': 0001_initial.py: - Create model Category - Create model Product - Alter index_together for product (1 constraint(s))
Run the next command to sync the database:
python manage.py migrate
You will see an output that includes the following line:
Applying shop.0001_initial... OK
Let's add our models to the administration site so that we can easily manage categories and products. Edit the admin.py
file of the shop
application and add the following code to it:
from django.contrib import admin from .models import Category, Product class CategoryAdmin(admin.ModelAdmin): list_display = ['name', 'slug'] prepopulated_fields = {'slug': ('name',)} admin.site.register(Category, CategoryAdmin) class ProductAdmin(admin.ModelAdmin): list_display = ['name', 'slug', 'price', 'stock', 'available', 'created', 'updated'] list_filter = ['available', 'created', 'updated']list_editable = ['price', 'stock', 'available'] prepopulated_fields = {'slug': ('name',)} admin.site.register(Product, ProductAdmin)
Remember that we use the prepopulated_fields
attribute to specify fields where the value is automatically set using the value of other fields. As you have seen before, this is convenient for generating slugs. We use the list_editable
attribute in the ProductAdmin
class to set the fields that can be edited from the list display page of the administration site. This will allow you to edit multiple rows at once. Any field in list_editable
must also be listed in the list_display
attribute, since only the fields displayed can be edited.
Now, create a superuser for your site using the following command:
python manage.py createsuperuser
Start the development server with the command python manage.py runserver
. Open http://127.0.0.1:8000/admin/shop/product/add/
in your browser and log in with the user that you just created. Add a new category and product using the administration interface. The product change list page of the administration page will then look like this:
In order to display the product catalog, we need to create a view to list all the products or filter products by a given category. Edit the views.py
file of the shop
application and add the following code to it:
from django.shortcuts import render, get_object_or_404 from .models import Category, Product def product_list(request, category_slug=None): category = None categories = Category.objects.all() products = Product.objects.filter(available=True) if category_slug: category = get_object_or_404(Category, slug=category_slug) products = products.filter(category=category) return render(request, 'shop/product/list.html', {'category': category, 'categories': categories, 'products': products})
We will filter the QuerySet with available=True
to retrieve only available products. We will use an optional category_slug
parameter to optionally filter products by a given category.
We also need a view to retrieve and display a single product. Add the following view to the views.py
file:
def product_detail(request, id, slug): product = get_object_or_404(Product, id=id, slug=slug, available=True) return render(request, 'shop/product/detail.html', {'product': product})
The product_detail
view expects the id
and slug
parameters in order to retrieve the Product
instance. We can get this instance by just the ID since it's a unique attribute. However, we include the slug in the URL to build SEO-friendly URLs for products.
After building the product list and detail views, we have to define URL patterns for them. Create a new file inside the shop
application directory and name it urls.py
. Add the following code to it:
from django.conf.urls import url from . import views urlpatterns = [ url(r'^$', views.product_list, name='product_list'), url(r'^(?P<category_slug>[-w]+)/$', views.product_list, name='product_list_by_category'), url(r'^(?P<id>d+)/(?P<slug>[-w]+)/$', views.product_detail, name='product_detail'), ]
These are the URL patterns for our product catalog. We have defined two different URL patterns for the product_list
view: a pattern named product_list
, which calls the product_list
view without any parameters; and a pattern named product_list_by_category
, which provides a category_slug
parameter to the view for filtering products by a given category. We added a pattern for the product_detail
view, which passes the id
and slug
parameters to the view in order to retrieve a specific product.
Edit the urls.py
file of the myshop
project to make it look like this:
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^', include('shop.urls', namespace='shop')),
]
In the main URLs patterns of the project, we will include URLs for the shop
application under a custom namespace named 'shop'
.
Now, edit the models.py
file of the shop
application, import the reverse()
function, and add a get_absolute_url()
method to the Category
and Product
models as follows:
from django.core.urlresolvers import reverse # ... class Category(models.Model): # ... def get_absolute_url(self): return reverse('shop:product_list_by_category', args=[self.slug]) class Product(models.Model): # ... def get_absolute_url(self): return reverse('shop:product_detail', args=[self.id, self.slug])
As you already know, get_absolute_url()
is the convention to retrieve URL for a given object. Here, we will use the URLs patterns that we just defined in the urls.py
file.
Now, we need to create templates for the product list and detail views. Create the following directory and file structure inside the shop
application directory:
templates/ shop/ base.html product/ list.html detail.html
We need to define a base template, and then extend it in the product list and detail templates. Edit the shop/base.html
template and add the following code to it:
{% load static %} <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>{% block title %}My shop{% endblock %}</title> <link href="{% static "css/base.css" %}" rel="stylesheet"> </head> <body> <div id="header"> <a href="/" class="logo">My shop</a> </div> <div id="subheader"> <div class="cart"> Your cart is empty. </div> </div> <div id="content"> {% block content %} {% endblock %} </div> </body> </html>
This is the base template that we will use for our shop. In order to include the required CSS styles and images that are used by the templates, you will need to copy the static files that come along with this chapter, located in the static/
directory of the shop
application. Copy them to the same location of your project.
Edit the shop/product/list.html
template and add the following code to it:
{% extends "shop/base.html" %} {% load static %} {% block title %} {% if category %}{{ category.name }}{% else %}Products{% endif %} {% endblock %} {% block content %} <div id="sidebar"> <h3>Categories</h3> <ul> <li {% if not category %}class="selected"{% endif %}> <a href="{% url "shop:product_list" %}">All</a> </li> {% for c in categories %} <li {% if category.slug == c.slug %}class="selected"{% endif %}> <a href="{{ c.get_absolute_url }}">{{ c.name }}</a> </li> {% endfor %} </ul> </div> <div id="main" class="product-list"> <h1>{% if category %}{{ category.name }}{% else %}Products{% endif %}</h1> {% for product in products %} <div class="item"> <a href="{{ product.get_absolute_url }}"> <img src="{% if product.image %}{{ product.image.url }}{% else %}{% static "img/no_image.png" %}{% endif %}"> </a> <a href="{{ product.get_absolute_url }}">{{ product.name }}</a><br> ${{ product.price }} </div> {% endfor %} </div> {% endblock %}
This is the product list template. It extends the shop/base.html
template and uses the categories
context variable to display all the categories in a sidebar and products
to display the products of the current page. The same template is used for both: listing all available products and listing products filtered by a category. Since the image
field of the Product
model can be blank, we need to provide a default image for the products that don't have an image. The image is located in our static files directory with the relative path img/no_image.png
.
Since we are using ImageField
to store product images, we need the development server to serve uploaded image files. Edit the settings.py
file of myshop
and add the following settings:
MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
MEDIA_URL
is the base URL that serves media files uploaded by users. MEDIA_ROOT
is the local path where these files reside, which we build dynamically prepending the BASE_DIR
variable.
For Django to serve the uploaded media files using the development server, edit the urls.py
file of myshop
and add make it look like this:
from django.conf import settings from django.conf.urls.static import static urlpatterns = [ # ... ] if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Remember that we only serve static files this way during the development. In a production environment, you should never serve static files with Django.
Add a couple of products to your shop using the administration site and open http://127.0.0.1:8000/
in your browser. You will see the product list page, which looks like this:
If you create a product using the administration site and don't upload any images, the default no_image.png
image will be displayed:
Let's edit the product detail template. Edit the shop/product/detail.html
template and add the following code to it:
{% extends "shop/base.html" %} {% load static %} {% block title %} {% if category %}{{ category.title }}{% else %}Products{% endif %} {% endblock %} {% block content %} <div class="product-detail"> <img src="{% if product.image %}{{ product.image.url }}{% else %}{% static "img/no_image.png" %}{% endif %}"> <h1>{{ product.name }}</h1> <h2><a href="{{ product.category.get_absolute_url }}">{{ product.category }}</a></h2> <p class="price">${{ product.price }}</p> {{ product.description|linebreaks }} </div> {% endblock %}
We call the get_absolute_url()
method on the related category object to display the available products that belong to the same category. Now, open http://127.0.0.1:8000/
in your browser and click on any product to see the product detail page. It will look as follows: