django by example 实践 myshop 项目(一)


点我查看本文集的说明及目录。


本项目相关内容包括:

实现过程

CH7 创建在线商店

CH8 管理支付和订单

CH9 扩展商店


CH7 创建在线商店


上一章,我们创建了关注系统和用户活动流,我们还学习了如何使用 Django signal ,以及在项目中集成 Redis 来对视图进行计数。本章,我们将学习如何创建基本的在线商店,创建商品目录和使用 Django sessions 将商品放入购物车。我们还将学习如何创建自定义内容处理器和使用 Celery 加载异步任务。

本章将包含以下内容:

  • 创建产品目录
  • 使用 Django session 创建购物车
  • 管理用户订单
  • 使用 Celery 为用户发送异步通知

创建一个在线商店项目


我们将创建一个在线商店项目。我们的用户将能够通过商品目录浏览商品并将商品放入购物车。最后,检查购物车并下单。本章将包括在线商店的以下功能:

  • 创建商品目录模型,将其添加到 admin网站,并且创建展示商品目录的基本视图;
  • 使用 Django session 创建购物车系统帮助用户浏览网站时保存选择的商品;
  • 创建表单和下单功能;
  • 用户下单成功后为用户发送同步邮件。

首先,打开 teminal 并使用以下命令来为新项目创建虚拟环境并激活:

mkdir env
virtualenv env/myshop
source env/myshop/bin/activate

笔者注:

可以将虚拟环境放到前两个项目 ( blog、bookmarks ) 虚拟环境所在的文件夹下,这时, mkdir env 改为 cd env

笔者这里仍然使用第一章中用到的 PyCharm 创建虚拟环境。

在虚拟环境中使用以下命令安装 Django :

 pip install django

运行以下命令并创建名为 myshop 的新项目,并在项目中创建名为 shop 的新应用:

django-admin startporject myshop
cd myshop/
django-admin startapp shop

然后在项目 settings.py 文件的 INSTALLED_APPS 中添加应用名称:

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

现在,shop 应用已经激活,我们来为商品目录定义模型。

from django.db import models


# Create your models here.

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

这是 Category 和 Product 模型。 Category 模型包含 name 字段和 slug 唯一字段。Product 模型字段如下:

  • category: Category 模型的外键。这是一个多对一关系,一个商品属于一个目录,一个目录下包含多个商品。
  • name: 商品名称。
  • slug:商品 slug,用于生成漂亮的 URLs 。
  • image:商品图片,可选。
  • description:商品描述,可选。
  • price:DecimalField,该字段使用 Python 的 decimal.Decimal 类型来存储固定精度的decimal。max_digit 属性设置数字的最大值(包括 decimal 位),decimal_places 设置 decimal 位。
  • stock:PositiveIntegerField ,用于保存商品库存。
  • available:布尔值,表示是否可以获得商品。它可以帮助我们控制商品是否出现在商品目录中。
  • created:商品创建时间。
  • updated:商品更新时间。

对于 price 字段,我们使用 DecimalField 代替 FloatField 以防止小数位数问题。

注意:

一定要使用 Decimal 保存钱数。 FloatField 使用 Python float 类型, DecimalField 使用 Python 的 decimal.Decimal 类型。通过使用 DecimalField,可以防止小数位数问题。

在 Product 模型的 Meta 类中,由于我们计划使用 id 和 slug 进行索引,这里使用 index_together 选项指定使用 id 和 slug 进行索引。两个索引组合可以改善两个字段的查询性能。

由于模型需要处理图片,打开 shell 并使用以下命令安装 Pillow :

  pip install Pillow

现在,运行另一个命令来为项目创建初始迁移文件:

python manage.py makemigrations

现在可以看到以下输出:

Migrations for 'shop':
  shop/migrations/0001_initial.py
- Create model Category
- Create model Product
- Alter index_together for product (1 constraint(s))

运行以下命令同步数据库:

python manage.py migrate

可以看到以下输出:

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, shop
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying sessions.0001_initial... OK
  Applying shop.0001_initial... OK

现在,数据库与模型同步了。

将产品目录模型注册到 admin网站

将产品目录模型注册到 admin网站可以帮助我们管理目录和产品。编辑 shop 应用的 admin.py 文件并添加以下代码:

from django.contrib import admin

from .models import Category, Product


# Register your models here.

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)

prepopulated_fields 属性用来指定使用其它字段的值自动生成值的字段。正如我们前面看到的,这是生成 slug 的简便方法。在 ProductAdmin 类中使用 list_editable 属性设置 admin网站的列表展示页面可以更改的字段。这样可以同时编辑多行,由于只有展示的内容才能进行编辑,list_editable 的任何字段都必须在 list_display 中。

笔者注:

可以采用第五章中的方法, 分别重写 Category 和 Product 的 save 方法自动生成 slug :

from django.utils.text import slugify


class Category(models.Model):
    ...

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super(Category, self).save(*args, **kwargs)
        
        
class Product(models.Model):
  ...
  
  def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super(Product, self).save(*args, **kwargs)

现在,使用以下命令为网站创建超级用户:

python manage.py createsuperuser

使用 python manage.py runserver 命令启动开发服务器,在浏览器中打开 http://127.0.0.1:8000/admin/shop/product/add/并使用刚刚创建的账号登录。使用 admin网站添加一个新的商品目录和一个新的商品,admin网站的商品更改列表页面看起来是这样的:

CH7_1.png

创建产品目录视图


为了展示产品目录,我们需要创建一个视图列出所有产品或者通过给定类别对产品进行过滤。编辑 shop 应用的views.py 文件并添加以下代码:

from django.shortcuts import render, get_object_or_404

from .models import Category, Product


# Create your views here.



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

使用 available=True 过滤 QuerySet 来获取可以得到的商品。我们将使用可选的 category_slug 参数来获得给定类别的商品。

我们还需要一个视图来获取和展示单个产品。在 views.py 文件中添加以下代码:

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

product_detail 视图需要id 和 slug 参数来检索 Product 实例。由于 id 的唯一属性,我们只通过 id 就可以获得实例。然而我们包含URL 中的 slug 来为商品创建SEO-友好的 URL。

创建完产品列表和详情视图,我们需要为它们定义 URL 模式。在 shop 应用目录下创建名为 urls.py 新文件,并添加以下文件:

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

这是产品目录的 URL模式。我们为 product_list 视图设置了两个不同的 URL模式:一个模式为 product_list ,可以在不输入任何参数的情况下的调用 product_list 视图,另一个模式为 product_list_by_category ,需要向视图提供 category_slug 参数来告诉视图通过给定类别对进行过滤。我们为 product_detail 视图设置了一个模式,该视图需要提供 id 和 slug 参数获得指定的产品。

编辑 myshop 项目的 urls.py 文件:

from django.conf.urls import url, include
from django.contrib import admin

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

在项目的主 URLs模式中,使用自定义命名空间 shop 包含 shop 应用的 URLs。

现在,编辑 shop 应用的 models.py 文件,导入 reverse() 函数,并向 Category 和 Product 模型添加 get_absolute_url() 方法:

from django.urls 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])

笔者注:

这里使用 from django.urls import reverse 代替了原文的 from django.core.urlresolvers import reverse

我们已经知道,get_absolute_url() 是获得指定对象 url 的简便方法,这里,我们将使用刚刚在 urls.py 文件中定义的 URLs模式。

创建产品目录模板

现在,我们需要为商品列表和详情视图创建模板。在 shop 应用目录下创建下面的目录和文件结构:

CH7_2.png

我们需要定义基础模板,然后在产品列表和详情模板中对其进行扩展。编辑 shop/base.html 模板并添加以下代码:

{% load static %}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>{% block title %}My shop{% endblock %}</title>
    <link href="{% static "shop/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>

这是商店的基础模板。为了包含模板使用的 CSS 文件和图片,我们需要将本章 shop 应用的 static/ 目录下的静态文件拷贝到相同的路径下。

编辑 shop/product/list.html 模板并添加以下代码:

{% 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 "shop/img/no_image.png" %}{% endif %}">
                    
                    
                </a>
                
                <a href="{{ product.get_absolute_url }}">{{ product.name }}</a><br>
                ${{ product.price }}
            </div>
        {% endfor %}
    </div>
{% endblock %}

笔者注:

如果想统一商品图片的大小以及节约空间,这里可以使用缩略图,我们在第五章学习了缩略图的用法。

使用 pip install sorl-thumbnail 安装,在项目 settings.py 的 INSTALLED_APPS 中添加 'sorl.thumbnail'。

使用 python manage.py migrate 同步数据库。

然后将 <img src="{% if product.image %}{{ product.image.url }}{% else %}{% static "img/no_image.png" %}{% endif %}">

更改为:

{% load thumbnail %}

{% if product.image %}
    {% thumbnail product.image '300x200' as im %}
        <img src="{{ im.url }}">
    {% endthumbnail %}

{% else %}
    <img src="{% static 'shop/img/no_image.png' %}">
{% endif %}

这是产品列表模板。它扩展 shop/base.html 模板并在边栏中使用 categories 变量展示所有分类,使用 products 展示当前页面的产品。这个模板适用于两种情况:列出所有可获得的产品和通过类别过滤到的产品。由于 Product 模型的 image 字段可以为空,我们需要为没有图片的产品设置默认图片。默认图片位于静态文件中的 img/no_image.png 。

由于使用 ImageField 存储产品图片,我们需要开发服务器提供上传图片文件服务。编辑 myshop 的 settings.py 文件并添加以下设置:

# Media files

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/') 

MEDIA_URL 是用户上传文件的基础 URL 。MEDIA_ROOT 是这些文件的本地位置,通过 BASE_DIR 进行动态创建。

Django 使用开发服务器上传文件需要编辑 myshop 的urls.py 文件并添加以下代码:

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

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

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

只在开发过程中这样处理静态文件,在生产过程中,不要使用 Django 处理静态文件。

使用 admin网站为商店添加几个商品并在浏览器中打开 http://127.0.0.1:8000/。你将看到商品列表页面:

CH7_3.png

如果你使用 admin 网站创建了商品但是没有上传图片,那么将看到 no_image.png:

CH7_4.png

编辑 shop/product/detail.html 来编辑产品详情模板,模板并添加以下代码:

{% 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 'shop/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 %}

我们将调用 category 的 get_absolute_url() 方法来获得同类的产品列表。现在,在浏览器中打开 http://127.0.0.1:8000/,点击任意产品来查看产品详情。看起来是这样的:

CH7_5.png

创建购物车


创建完商品目录之后,下一步是创建保存用户选择的商品的购物车。购物车帮助用户选择想要的商品并在用户浏览网站时暂时保存选中的商品直到用户下单。购物车应该放在 session 中以便用户浏览时将商品放入购物车。

我们将使用 Django session 框架来存放购物车。 用户结账或者退出登录之前购物车将保留在 session 中。我们需要创建额外的 Django 模型来保存购物车中的商品。

使用 Django session


Django 提供 session 框架来支持匿名和用户会话。 session 框架帮助我们为每位浏览者保存任何数据。 除非使用基于 cookie 的 session 引擎,Session 数据一般存储在服务端, cookie 则存储 session ID。 session 中间件负责管理发送和接收 cookies 。默认的 session 引擎在数据库中保存 session 数据,当然,也可以选择其它的 session 引擎。

为了使用 session,我们需要项目设置的 MIDDLEWARE_CLASSES 中包含django.contrib.sessions.middleware.SessionMiddleware。这个引擎用于管理 sessions ,如果使用startproject 命令创建新项目时默认添加。

session 中间件可以实现从 request 对象中访问当前 session 。我们可以通过 request.session 得到当前 session ,可以像使用 Python 字典一样保存和获得 session 数据。session 字典接收任何可以序列化为 JSON 的 Python 对象,我们可以这样设置 session 变量:

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

获得 session 的值:

request.session.get('foo')

删除 session 中保存的一个值:

del request.session['foo']

我们可以看到,可以像操作 Python 字典一样处理 request.session 。

注意:

当用户登录网站时,将丢弃他们的匿名会话并为有权限的用户创建一个新的 session 。如果需要保存一个登录后可用的匿名 session ,那么需要将旧的 session 数据拷贝到新的 session 数据中。

session 设置


可以使用几种方法为配置项目 sessions 。最重要的是 SESSION_ENGINE。这个设置允许用户设置 session 存储位置。默认情况下,Django 使用 django.contrib.sessions 应用的 Session 模型将数据保存到数据库中。

Django 提供以下存储 session 数据的选项:

  • Database sessions: Session 数据保存在数据库中,默认的 session 引擎。
  • File-based sessions: Session 数据保存在文件系统中。
  • Cached sessions: Session 数据保存在缓存后端,可以使用 CACHES 设置指定缓存后端,将 session 数据保存在缓存后端可以实现最好的性能。
  • Cached database sessions: Session 数据保存在 write-through 缓存和数据库。数据不在缓存中时才读取数据库。
  • Cookie-based sessions: Session 数据保存在发送到浏览器的 cookies 中。

注意:

为了获得更好的性能可以使用 cache-based session 引擎。Django 支持 Memcached 和其它 Redis 第三方缓存后端以及其他缓存系统。

你可以使用其它设置自定义 sessions 。这里有一些非常重要的 session 设置:

SESSION_COOKIE_AGE: session cookie 保存时间(秒为单位)。默认值为 1209600 (2 周)。

SESSION_COOKIE_DOMAIN: session cookies 使用的域,将其设置为 .mydomain.com 可以实现跨域 cookies 。

SESSION_COOKIE_SECURE : 布尔值,是否只有HTTPS连接才能发送cookie。

SESSION_EXPIRE_AT_BROWSER_CLOSE: 布尔值,关闭浏览器时 session 是否过期。

SESSION_SAVE_EVERY_REQUEST: 布尔值,如果为 True,每个request 都会将 session 保存到数据库,并且每次更新 session 到期时间。

所有的 session 设置参考 https://docs.djangoproject.com/en/1.11/ref/settings/#sessions

session 过期


你可以通过设置 SESSION_EXPIRE_AT_BROWSER_CLOSE 选择使用浏览器长度 session 或者持久 session 。这里的默认设置为 False ,session 的有效期将取决于 SESSION_COOKIE_AGE 设置的值。如果将 SESSION_EXPIRE_AT_BROWSER_CLOSE 设置为 True ,session 将在关闭浏览器时失效, SESSION_COOKIE_AGE 设置的值不会起作用。

你可以使用request.session 的 set_expiry() 方法重写当前 session 的有效时间。

在 sessions 中保存购物车


我们需要创建一个可以序列化为 JSON 的简单结构来在 session 中保存购物车内的商品 。 购物车中的每种商品需要包含以下数据:

  • Product 实例的 id ;

  • 商品的数量;

  • 这个商品的单价;

由于商品价格可能变动,当添加到购物车时,我们将商品价格和商品放在一起。这样,可以将价格保持在顾客将商品添加到购物车时的价格,即使价格随后可能发生改变也不会受到影响。

现在,我们需要创建购物车并将数据保存到 session 中。购物车需要这样工作:

  • 当需要购物车时,我们检查是否设置了一个自定义 session 键,如果 session 没有设置购物车,我们将创建新的购物车并将其保存到购物车 session 键中。

  • 对于成功请求,我们进行相同的检查并从 购物车 session 键中获得值。我们从 session 中获得购物车中的商品并从数据库中获取对应 Product 对象。

编辑项目的 settings.py 文件并添加以下设置:

# session settings
CART_SESSION_ID = 'cart'

这是我们在用户 session 中保存购物车的键。由于 Django session 是 pre-visitor,所有 session 都使用相同的购物车 session 键。

我们来创建一个管理购物车的应用,打开 teminal 并创建一个新的应用,在项目目录下运行以下命令:

python manage.py startapp cart

然后,然后在项目 settings.py 文件的 INSTALLED_APPS 中添加应用名称:

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

在 cart 应用的目录下创建一个 cart.py 的新文件,并添加以下代码:

from django.conf import settings

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

这是管理购物车的 Cart 类。使用 request 对象对 cart 进行初始化。使用 self.session = request.session 来保存当前 session,以便 Cart 类的其它方法可以访问它。首先,我们使用self.session.get(settings.CART_SESSION_ID) 从当前 session 中获得 cart,如果当前session 中没有 cart ,那么在 session 中设置一个空字典来设置一个空的 cart 。我们期望 cart 字典使用商品 id 作为键,由商品数量和价格组成的字典作为值。这样,可以保证 cart 不能多次添加同一商品,还便于访问 cart 中的任意商品数据。

我们来创建一个方法在购物车中添加商品或更新商品数量。向 Cart 类添加以下 add() 和 save() 方法:

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

add() 方法接收以下参数:

  • product: 购物车添加或者更改的 Product 实例;
  • quality: 商品数量,可选的整数,默认值为 1 ;
  • update_quality :布尔值,是否使用输入的数量对数量进行更新的标志位,如果为True,则根据输入的数量更新数量,如果为 False ,新值与原来的值相加。

我们使用商品 id 作为 cart 字典的键,由于Django 使用 JSON 进行序列化,而 JSON 只允许字符串键,因此这里将商品 id 转换为字符串。商品 id 为键,quality 和 price 组成的字典为值。由于序列化要求,商品的价格也由 Decimal 格式转换为 string 格式。最后,调用 save() 方法将 cart 保存到 session 中。

save() 方法在 session 中保存 cart 的所有变化,并通过 session.modified = True 将 session 标记为更改状态。这将告诉 django 发生了更改需要进行保存。

我们还需要一个从购物车中删除商品的方法,向 Cart 类添加以下方法:

def remove(self, product):
    """
    Remove a product from the cart
    :param product: 
    :return: 
    """
    product_id = str(product.id)
    if product_id in self.cart:
        del self.cart[product_id]
        self.save()

remove() 方法从购物车字典中删除指定商品并调用 save() 方法更新购物车。

我们还需要对购物车中的商品进行迭代来访问相关的 Product 实例。我们可以在类中定义__iter__()来实现该功能。向 Cart 类中添加以下方法:

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

__iter__()方法中,我们得到了购物车中所有商品的 Product 实例。然后对购物车中的商品进行遍历,将每一项的 price 的格式更改回 Decimal,并为每一项添加 total_price 属性。现在,我们可以很容易的遍历购物车中的商品了。

我们还需要返回购物车中的所有商品数量,当我们对一个对象执行 len() 函数时,Python 调用它的 __len__方法来获得长度。我们将定义自定义 __len__方法来返回购物车中商品的总数量。在 Cart 类中添加 __len__方法:

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

将返回购物车中所有商品的总数量。

添加以下方法来计算购物车商品的总价格:

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

最后,添加方法来清理购物车 session:

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

现在, Cart 类可以管理购物车了。

创建购物车视图


现在,我们已经有一个管理购物车的 Cart 类了,现在需要创建视图来添加、更新或者移除购物车中的商品。我们需要创建以下视图:

  • 可以添加或者更新购物车中的商品的视图,可以处理当前和更新的数量;
  • 删除购物车中商品的视图
  • 展示购物车中商品和总量的视图

向购物车添加商品


为了能够在购物车中添加商品,我们需要一个表单来实现选择数量的功能。在 cart 应用目录下创建一个 forms.py 的文件并添加以下代码:

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)

这个表单用来向购物车添加商品。CartAddProductForm 包含以下两个字段:

  • quantity: 值的范围为 1-20 。我们使用 TypedChoiceField 字段和 coerce=int 来将输入转换为整型;

  • update: 标志位,如果为False,则在购物车原数量的基础上增加quantity,如果为True,则将数量设置为 quantity。由于不想让用户看到,这个字段使用了 HiddenInput 小控件。

创建一个向购物车添加商品的视图。编辑 cart 应用的 views.py 视图:

from django.shortcuts import 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
# 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'],
                 update_quantity=cd['update'])
    return redirect('cart:cart_detail')

这是一个向购物车添加商品或更新商品数量的视图,由于视图将更改数据,视图使用 require_POST 装饰器只允许 POST 请求。视图以商品 ID 作为参数,我们获得特定 ID 的 Product 实例并验证 CartAddProductForm 表单。如果表单有效,将添加或更新购物车中的商品。视图重定向到 cart_detail URL 来展示购物车中的商品。我们稍后将创建 cart_detail 视图。

此外,还需要创建从购物车中移除商品的视图。将以下代码添加到 cart 应用的 views.py 文件中:

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

cart_remove 视图接收商品 id 。使用给定的 ID 获得 Product 实例并从购物车中移除该商品,然后重定向到 cart_detail URL。

最后,我们需要一个视图来展示购物车和购物车内的商品。在 views.py 中添加以下代码:

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

cart_detail 获取当前的购物车并进行展示。

我们已经创建了购物车添加商品、更新数量、删除商品、展示商品的视图。接下来我们为这些视图添加 URL 模式。 在 cart 应用目录下创建一个 urls.py 的新文件,并添加下面的内容:

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

编辑 myshop 项目的 urls.py 文件并添加 cart 的 URLs :

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

确保 cart.urls 在 shop.urls 之前,因为它比 shop.url 限制更多。

创建展示购物车的模板


cart_add 和 cart_delete 视图不需要渲染模板,但是我们需要为 cart_detail 视图创建模板来展示购物车中的商品和总数。

在 cart 应用目录下创建下面的文件结构:

编辑 cart/detail.html 模板并添加以下代码:

{% 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 "shop/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 %}  

这是展示购物车内容的模板。它包含一个当前购物车商品的表格。用户可以通过指向 cart_add 视图的表单更改选中产品的数量。我们还通过为每个商品提供删除链接来删除商品。

将商品添加到购物车


现在,我们需要为商品详情页面添加一个 Add to cart 按钮。编辑 shop 应用的 views.py 文件,并将 CartAddProductForm 添加到 product_detail 视图中:

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

编辑 shop 应用的 shop/product/detail.html 模板,并在产品价格后面添加下面的表单:

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

使用 python manage.py runserver 运行开发服务器。在浏览器中打开 http://127.0.0.1:8000/ 并点击某个商品到商品详情页面。现在页面在添加到购物车前面有一个选择数量的选项。页面看起来是这样的:

CH7_6.png

选择数量并点击 Add to cart 按钮。表单通过 POST 方法提交到 cart_add 视图。视图将商品(包括商品的价格和选择的数量)添加到 session 的购物车中。然后,重定向到购物车详情页面,看起来是这样的:

CH7_7.png

在购物车中更新产品数量


用户查看购物车时,他们在下单之前可能需要更改产品数量。下面将实现购物车详细页面更改商品数量的功能。

编辑 cart 应用的 views.py 文件并这样更改 cart_detail 视图:

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

我们为购物车的每一个商品创建了一个 CartAddProductForm 实例,这样就可以更改产品数量了。这里使用产品的当前数量并将 update 设置为 True 对实例进行初始化,这样我们可以将表单提交到 cart_add 视图,新的产品数量会代替当前产品数量。

现在,编辑 cart 应用的 cart/detail.html 模板并找到下面一行:

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

将其更改为:

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

在浏览器中打开 http://127.0.0.1:8000/cart/,可以看到每个商品都包含编辑数量的表单:

CH7_8.png

更改某个商品的数量,并点击 Update 按钮对新功能进行测试。

为当前购物车创建内容处理器


你可能已经注意到,页面头部还在显示 Your cart is empty 的信息。当我们开始向购物车添加商品时,我们应该可以看到购物车添加商品的数量和总金额。由于需要在所有页面展示这些信息,我们需要创建内容处理器将当前购物车包含在请求内容中,这些内容与被处理的视图无关。

内容处理器


内容处理器是一个 Python 函数,它的输入参数为 request 对象,返回添加到请求内容中的字典。它可以用来生成所有模板都需要的内容。

默认情况下,使用 startproject 命令创建一个新的项目,项目将包含下面的内容处理器(在 TEMPLATES 设置的 context_processors 选项中):

  • django.template.context_processors.debug:设置内容中表示请求执行的 SQL 查询列表的 debug 布尔值和 sql_queries 变量;

  • django.template.context_processor.request:设置内容中的 request 变量;

  • django.contrib.auth.context_processors.auth:设置请求中的用户变量;

  • django.contrib.messages.context_processors.messages:设置message 变量,message 变量包括消息框架中的所有消息;

Django 还将启用 django.template.context_processors.csrf 来避免跨网站请求伪造攻击。这个内容处理器没有出现在设置中,但是它一直处于启用状态,并且由于安全原因无法关闭。

我们可以在以下页面了解所有的内容内容处理器https://docs.djangoproject.com/en/1.11/ref/templates/api/#built-in-template-context-processors

在请求内容中设置购物车


我们创建内容处理器来将购物车放到模板的 request 内容中。这样任意模板都可以访问这个购物车。

在 cart 应用目录下新建名为 context_processors.py 的文件。内容处理器可以放在代码中的任何位置,但是放置在这里可以更好的组织代码,在文件中添加以下代码:

from .cart import Cart


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

正如我们看到的,内容处理器是一个函数,它接收 request 对象作为参数,返回一个任何模板都可以通过 RequestContext 渲染的字典对象。在我们的内容处理器中,我们使用 request 对象对购物车进行实例化,模板可以通过 cart 变量访问购物车。

编辑 项目的 settings.py 文件,并将 ‘ cart.context_processors.cart' 添加到 TEMPLATES 设置的 context_processors 选项中。

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        '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',
            ],
        },
    },
]

现在,每次使用 Django 的 RequestContext 渲染模板时都会执行我们刚刚创建的模板处理器,模板内容中将包含模板变量。

注意:

内容处理器在所有使用 RequestContext 的请求中执行。如果需要访问数据库,最好创建自定义模板标签,而不是使用内容处理器。

现在,编辑 shop 应用中的 shop/base.html 模板并找到以下内容:

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

使用以下代码代替上面的代码:

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

使用 python manage.py runserver 命令重新启动服务器,打开http://127.0.0.1:8000/并在购物车中添加一些商品,在页面的头部,你将看到商品的总数和总价格:

CH7_9.png

管理用户订单


结算时,我们需要将订单保存到数据库中,订单将包含用户信息以及他们购买的商品。

使用以下命令创建一个管理用户订单的新应用:

python manage.py startapp orders

编辑项目的 settings.py 文件并在 INSTALLED_APPS 设置中添加 'orders':

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

你已经激活了一个新应用。

创建订单模型


我们需要创建一个存储订单详情的模型以及一个存储订单购买的商品(包含价格和数量)的模型。编辑 orders 应用的 models.py 文件并添加以下代码:

from django.db import models
from shop.models import Product


# Create your models here.


class Order(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField()
    address = models.CharField(max_length=250)
    postal_code = models.CharField(max_length=20)
    city = models.CharField(max_length=100)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    paid = models.BooleanField(default=False)

    class Meta:
        ordering = ('-created',)

    def __str__(self):
        return 'Order {}'.format(self.id)

    def get_total_cost(self):
        return sum(item.get_cost() for item in self.items.all())


class OrderItem(models.Model):
    order = models.ForeignKey(Order, related_name='items')
    product = models.ForeignKey(Product, related_name='order_items')
    price = models.DecimalField(max_digits=10, decimal_places=2)
    quantity = models.PositiveIntegerField(default=1)

    def __str__(self):
        return '{}'.format(self.id)

    def get_cost(self):
        return self.price * self.quantity

Order 模型包含几个用户信息的字段和一个默认设置为 False 的布尔字段 paid 。后面我们将使用这个字段区分订单是否付款。此外,还定义了一个 get_total_cost() 方法来计算订单商品的总费用。

OrderItem 模型用来保存每个订单的商品、数量和价格,并使用 get_cost() 计算商品费用。

运行以下命令对 order 应用进行第一次迁移:

python manage.py makemigrations 

你将看到这样的输出:

Migrations for 'orders':
orders/migrations/0001_initial.py
- Create model Order
- Create model OrderItem

运行以下命令应用新的迁移文件:

python manage.py migrate

现在我们的订单模型已经同步到数据库了。

将订单模型注册到 admin网站


我们将订单模型添加到 admin网站。编辑 order 应用的 admin.py 文件并添加以下代码:


from django.contrib import admin

from .models import OrderItem, Order


# Register your models here.
class OrderItemInline(admin.TabularInline):
    model = OrderItem
    raw_id_fields = ['product']


class OrderAdmin(admin.ModelAdmin):
    list_display = ['id', 'first_name', 'last_name', 'email', 'address',
                    'postal_code', 'city', 'paid', 'created', 'updated']
    list_filter = ['paid', 'created', 'updated']
    inlines = [OrderItemInline]


admin.site.register(Order, OrderAdmin)

我们在 OrderAdmin 中对 OrderItem 模型使用 ModelInline 来将其包含在 OrderAdmin 类中。内联(inline)帮助我们将一个模型放入父模型的编辑页面中。

使用 python manage.py runserver 启动开发服务器。在浏览器中打开http://127.0.0.1:8000/admin/orders/order/add/ ,你将看到下面的页面:

CH7_10.png

创建用户订单


我们需要在用户下单时使用刚刚创建的订单模型来保存购物车中的商品。创建新订单需要完成以下工作:

  1. 为用户提供一个订单表单来填写用户数据;

  2. 使用用户输入的数据创建一个新的 Order 实例,然后为购物车的每件商品关联一个 OrderItem 实例;

  3. 清空购物车的内容并重定向到成功页面。

首先,我们需要一个表单来填写订单详情。在 order 应用目录下新建一个 forms.py 文件。并添加以下代码:

from django import forms

from .models import Order


class OrderCreateForm(forms.ModelForm):
    class Meta:
        model = Order
        fields = ['first_name', 'last_name', 'email', 'address', 'postal_code',
                  'city']


这是我们新建 Order 对象的表单。现在需要一个视图来处理这个表单并创建一个新订单。编辑 orders 应用的 views.py 文件并添加以下代码:

from cart.cart import Cart
from django.shortcuts import render

from .forms import OrderCreateForm
from .models import OrderItem


# Create your views here.


def order_create(request):
    cart = Cart(request)
    if request.method == 'POST':
        form = OrderCreateForm(request.POST)
        if form.is_valid():
            order = form.save()
            for item in cart:
                OrderItem.objects.create(order=order, product=item['product'],
                                         price=item['price'],
                                         quantity=item['quantity'])
                # clear the cart
            cart.clear()
            return render(request, 'orders/order/created.html',
                          {'order': order})
    else:
        form = OrderCreateForm()
    return render(request, 'orders/order/create.html',
                  {'cart': cart, 'form': form})

在 order_create 视图中,使用 cart = Cart(request) 获得当前购物车。对于不同的请求方法我们将完成以下任务:

  • Get 请求:实例化 OrderCreateForm 表单并渲染模板 orders/order/create.html;

  • Post 请求:验证 post 数据,如果数据有效,使用 order = form.save() 创建新的 Order 实例,然后将其保存到数据库以及 order 变量中。创建完 order 后,我们将对购物车中的商品进行迭代并为每个商品创建 OrderItem 。最后清空购物车。

现在,在 orders 应用中新建 urls.py 的文件并添加下面的代码:

from django.conf.urls import url

from . import views

urlpatterns = [url(r'^create/$', views.order_create, name='order_create'), ]

这是 order_create 视图的 URL模式。编辑 myshop 的 urls.py 并添加以下模式。记得将其放在 shop.urls 模式之前:

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

编辑 cart 应用的 cart/detail.html 模板并找到下面一行:

<a href="#" class="button">Checkout</a>

将其替换为:

<a href="{% url 'orders:order_create' %}" class="button">Checkout</a>

用户现在可以从购物车详情页面跳转到订单表单了。我们还需要为订单定义模板。在 orders 应用下创建如下结构:

CH7_11.png

编辑 orders/order/create.html ,并包含下面的代码:

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

{% block title %}
    Checkout
{% endblock %}

{% block content %}
    <h1>Checkout</h1>

    <div class="order-info">
        <h3>Your order</h3>
        <ul>
            {% for item in cart %}
                <li>
                    {{ item.quantity }}x {{ item.product.name }}
                    <span>${{ item.total_price }}</span>
                </li>
            {% endfor %}
        </ul>
        <p>Total: ${{ cart.get_total_price }}</p>
    </div>

    <form action="." method="post" class="order-form">
        {{ form.as_p }}
        <p><input type="submit" value="Place order"></p>
        {% csrf_token %}
    </form>
{% endblock %}

这个模板展示购物车商品、总消费和下单的表单。

编辑 orders/order/created.html 模板并添加以下代码:

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

{% block title %}
    Thank you
{% endblock %}

{% block content %}
    <h1>Thank you</h1>
    <p>Your order has been successfully completed. Your order number is
        <strong>{{ order.id }}</strong>.</p>
{% endblock %}

这是成功下单后使用的模板。启动开发服务器,在浏览器中打开 http://127.0.0.1:8000/,在购物车中添加一些商品,然后跳转到结算页面。你将看到下面的内容:

CH7_12.png

在表单中填写有效数据并点击 Place order 按钮,订单将被创建,你见看到一个下面这样的成功页面。


CH7_13.png

使用 Celery 加载异步任务


视图执行的所有内容都对响应时间有影响。我们希望尽快返回响应以及在服务器上异步执行一些进程。这对耗时的进程或需要重试策略的进程而言尤为重要。 比如,用户可以在视频共享平台上传视频,但对上传的视频进行转码需要很长时间。 网站可能向用户返回响应,告诉他很快开始转码,并开始进行异步转码。 另一个例子是给用户发送电子邮件。 如果您的站点使用视图发送电子邮件通知,SMTP 连接可能失败或者影响响应速度。 启动异步任务对避免阻塞执行非常重要。

Celery 是一个可以处理大量信息的分布式任务队列。它进行实时处理,同时支持任务计划。使用 Celery 不仅可以更轻松的创建异步任务,并且可以尽快执行,但是也可以为这些任务设定执行时间。

你可以在这里找到 Celery 文档 http://celery.readthedocs.io/en/latest/

安装 Celery


我们来安装 Celery 并集成到项目中。通过 pip 使用以下命令安装 Celery :

pip install celery

Celery 需要一个消息中间件来处理外部请求。 消息中间件负责将消息发送到 Celery ,Celery 收到消息时处理任务。 下面来安装一个消息中间件。

安装 RabbitMQ


Celery 可以使用几种消息中间件,包括键值对存储(如 redis )和实际消息系统( 如 RabbitMQ )。由于 Celery 推荐使用 RabbitMQ,这里使用 RabbitMQ 配置 Celery 。

如果使用 Linux,可以使用如下命令安装 RabbitMQ :

apt-get install rabbitmq

如果需要在 Mac OS X 或者 Windows 上安装 RabbitMQ,可以在 https://www.rabbitmq.com/download.html 中找到独立版本。

安装完后,使用以下命令加载 RabbitMQ :

rabbitmq-server

你将看到以下面内容结尾的输出:

Starting broker... completed with 6 plugins.

RabbitMQ 开始运行并准备接收消息了。

在项目中添加 Celery


我们需要为 Celery 实例提供配置文件。在 myshop 项目中(与 settings.py 同级)新建 celery_task.py 的文件,这个文件保存项目的 Celery 配置。添加以下代码:

import os

from django.conf import settings

from celery import Celery

# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myshop.settings')

app = Celery('myshop')

app.config_from_object('django.conf:settings')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)

在代码中,我们为 Celery 命令行程序设置 DJANGO_SETTINGS_MODULE 变量,然后使用app = Celery('myshop') 创建应用实例。使用 config_from_object() 方法从项目设置加载自定义配置。最后告诉 Celery 自动发现 INSTALLED_APPS 中的应用的异步任务。 Celery 将在每个应用目录中寻找 tasks.py 文件来加载文件定义的异步任务。

我们需要在项目的__init__文件中导入 celery 模块确保 Django 启动时加载该模块。

笔者注:

__init__.py文件的位置:

- proj/
  - manage.py
  - proj/
    - __init__.py
    - settings.py
    - urls.py

编辑 myshop/__init__.py 文件并添加以下代码:

# import celery
from .celery_task import app as celery_app

笔者注:

原文中,与celery_task.py 的文件名为 celery.py , celery.py 与 celery 模块的名称相同会造成后续运行时出现以下错误:

File "/Users/apple/profile/django_by_example/myshop/myshop/myshop/celery.py", line 5, in <module>

from celery import Celery

ImportError: cannot import name Celery

参考:

https://stackoverflow.com/questions/19577172/celery-worker-error-importerror-no-module-named-celery

现在我们可以开始为应用编写异步程序了。

注意:

Celery_ALWAYS_EAGER 设置允许我们按照同步方式在本地执行任务,而不是将其发送到队列中。这对于单元测试或在本地环境不运行 Celery 的项目非常有帮助。

为应用添加异步任务


我们将创建异步任务,用户下单后异步发送通知邮件。

我们只需将应用的异步任务放入应用目录下的 tasks 模块即可。在 orders 应用中新建 tasks.py 文件。这是 Celery 查找异步任务的位置,添加以下代码:

from celery import task
from django.core.mail import send_mail

from .models import Order


@task
def order_created(order_id):
    """
    Task to send an e-mail notification when an order is 
    successfully created.
    """
    order = Order.objects.get(id=order_id)
    subject = 'Order nr. {}'.format(order.id)
    message = 'Dear {},\n\nYou have successfully placed an order.\
                  Your order id is {}.'.format(order.first_name, order.id)
    mail_sent = send_mail(subject, message, 'admin@myshop.com', [order.email])
    return mail_sent

我们使用 task 装饰器定义 order_create 任务。你可以看到,一个 Celery 任务只是一个使用 task 装饰的 Python 函数。 task 函数接收 order_id 参数。推荐只将 ID 传入任务函数并在执行任务时执行查询。我们使用 Django 提供的 send_mail() 函数来通知用户已经下单。如果不希望启动邮件设置,可以在 settings.py 文件中进行以下设置将邮件输出到 console :

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

注意:

使用异步任务不仅可以节约处理时间,还可以用于可能失败的过程,它们可能不需要很长的执行时间,但是可能会出现连接失败或者需要重试策略。

现在,我们需要将任务添加到 order_create 视图中。打开 orders 应用的 views.py 文件并添加以下代码:

from .tasks import order_created

然后,在清理完购物车后调用 order_created 异步任务:

# clear the cart
cart.clear()
# launch asynchronous task
order_created.delay(order.id)

我们调用 delay() 方法来异步执行任务。任务将被添加到队列并尽快执行。

打开另一个 shell 并使用下面的命令启动 celery 工作:

celery -A myshop.celery_task:app worker -l info

笔者注:

原文命令为:

celery -A myshop worker -l info

由于前面将 celery.py 改为 celery_task ,因此执行的命令指定了文件名。

celery 现在正在运行并可以处理任务。启动 Django 开发服务器。在浏览器中打开 http://127.0.0.1:8000/ ,在购物车中添加一些商品,完成订单,在 shell 中,已经开启了 Celery 任务并将看到以下输出:

** -------------- celery@appledeMacBook.local v4.1.0 (latentcall)**

**---- \**** ----- **

**--- \* ***  * -- Darwin-15.6.0-x86_64-i386-64bit 2018-02-22 03:00:41**

**-- \* - **** --- **

**- \** ---------- [config]**

**- \** ---------- .> app:         myshop:0x1066daa90**

**- \** ---------- .> transport:   amqp://guest:**@localhost:5672//**

**- \** ---------- .> results:     disabled://**

**- \*** --- * --- .> concurrency: 2 (prefork)**

**-- \******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)**

**--- \***** ----- **

** -------------- [queues]**

**                .> celery           exchange=celery(direct) key=celery**

**                **

[tasks]

  . orders.tasks.order_created

[2018-02-22 03:00:41,705: INFO/MainProcess] Connected to amqp://guest:**@127.0.0.1:5672//

[2018-02-22 03:00:41,730: INFO/MainProcess] mingle: searching for neighbors

[2018-02-22 03:00:42,775: INFO/MainProcess] mingle: all alone

**[2018-02-22 03:00:42,832: WARNING/MainProcess] /Library/Python/2.7/site-packages/celery/fixups/django.py:202: UserWarning: Using settings.DEBUG leads to a memory leak, never use this setting in production environments!**

**  warnings.warn('Using settings.DEBUG leads to a memory leak, never '**

[2018-02-22 03:00:42,833: INFO/MainProcess] celery@appledeMacBook.local ready.

[2018-02-22 03:00:43,103: INFO/MainProcess] Events of group {task} enabled by remote.

任务已经执行,我们可以接收到下单通知了。

监控 Celery


我们可能希望监控正在执行的异步任务。Flower 是一个基于 web 的 Celery 监控器。可以使用 pip install flower 安装 Flower 。

一旦安装完毕,我们可以从项目目录运行以下命令加载 Flower :

celery -A myshop.celery_task:app flower

笔者注:

原文命令为:

celery -A myshop flower

修改原因与上面相同。

在浏览器中打开 http://localhost:5555/dashboard ,你将看到激活的 celery 和异步任务:

CH7_14.png

可以从 http://flower.readthedocs.io/en/latest/ 找到 Flower 文档。

总结


本章,我们创建了一个简单的商店应用。创建了商品目录并使用 sessions 创建了购物车,实现了自定义内容处理器保证模板可以获得内容并创建表单来下单。此外,还学习了使用 Celery 加载异步任务。

下一章,我们将学习在商店中集成支付网关,在 admin 网站添加自定义动作,输出 CSV 数据和动态生成 PDF 文件。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,603评论 5 465
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,422评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,478评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,116评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,037评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,832评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,075评论 3 389
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,685评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,976评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,028评论 2 315
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,830评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,584评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,101评论 3 301
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,229评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,521评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,121评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,314评论 2 339

推荐阅读更多精彩内容