Chapter 7. Building an Online Shop

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:

  • Create a product catalog
  • Build a shopping cart using Django sessions
  • Manage customer orders
  • Send asynchronous notifications to customers using Celery

Creating an online shop project

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:

  • Creating the product catalog models, adding them to the administration site, and building the basic views to display the catalog
  • Building a shopping cart system using Django sessions to allow users to keep selected products while they browse the site
  • Creating the form and functionality to place orders
  • Sending an asynchronous email confirmation to users when they place an order

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.

Creating product catalog models

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.

Note

Always use DecimalField to store monetary amounts. FloatField uses Python's float type internally, whereas DecimalField uses Python's Decimal type. By using the Decimal type, you will avoid the float 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

The database is now synced with your models.

Registering catalog models in the admin site

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:

Registering catalog models in the admin site

Building catalog views

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.

Creating catalog templates

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:

Creating catalog templates

If you create a product using the administration site and don't upload any images, the default no_image.png image will be displayed:

Creating catalog templates

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:

Creating catalog templates

We have now created a basic product catalog.

..................Content has been hidden....................

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