點我查看本文集的說明及目錄。
本項目相關內(nèi)容包括:
實現(xiàn)過程:
CH7 創(chuàng)建在線商店
上一章,我們創(chuàng)建了關注系統(tǒng)和用戶活動流,我們還學習了如何使用 Django signal ,以及在項目中集成 Redis 來對視圖進行計數(shù)。本章,我們將學習如何創(chuàng)建基本的在線商店,創(chuàng)建商品目錄和使用 Django sessions 將商品放入購物車。我們還將學習如何創(chuàng)建自定義內(nèi)容處理器和使用 Celery 加載異步任務。
本章將包含以下內(nèi)容:
- 創(chuàng)建產(chǎn)品目錄
- 使用 Django session 創(chuàng)建購物車
- 管理用戶訂單
- 使用 Celery 為用戶發(fā)送異步通知
創(chuàng)建一個在線商店項目
我們將創(chuàng)建一個在線商店項目。我們的用戶將能夠通過商品目錄瀏覽商品并將商品放入購物車。最后,檢查購物車并下單。本章將包括在線商店的以下功能:
- 創(chuàng)建商品目錄模型,將其添加到 admin網(wǎng)站,并且創(chuàng)建展示商品目錄的基本視圖;
- 使用 Django session 創(chuàng)建購物車系統(tǒng)幫助用戶瀏覽網(wǎng)站時保存選擇的商品;
- 創(chuàng)建表單和下單功能;
- 用戶下單成功后為用戶發(fā)送同步郵件。
首先,打開 teminal 并使用以下命令來為新項目創(chuàng)建虛擬環(huán)境并激活:
mkdir env
virtualenv env/myshop
source env/myshop/bin/activate
筆者注:
可以將虛擬環(huán)境放到前兩個項目 ( blog、bookmarks ) 虛擬環(huán)境所在的文件夾下,這時,
mkdir env改為cd env。筆者這里仍然使用第一章中用到的 PyCharm 創(chuàng)建虛擬環(huán)境。
在虛擬環(huán)境中使用以下命令安裝 Django :
pip install django
運行以下命令并創(chuàng)建名為 myshop 的新項目,并在項目中創(chuàng)建名為 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',
]
現(xiàn)在,shop 應用已經(jīng)激活,我們來為商品目錄定義模型。
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 屬性設置數(shù)字的最大值(包括 decimal 位),decimal_places 設置 decimal 位。
- stock:PositiveIntegerField ,用于保存商品庫存。
- available:布爾值,表示是否可以獲得商品。它可以幫助我們控制商品是否出現(xiàn)在商品目錄中。
- created:商品創(chuàng)建時間。
- updated:商品更新時間。
對于 price 字段,我們使用 DecimalField 代替 FloatField 以防止小數(shù)位數(shù)問題。
注意:
一定要使用 Decimal 保存錢數(shù)。 FloatField 使用 Python float 類型, DecimalField 使用 Python 的 decimal.Decimal 類型。通過使用 DecimalField,可以防止小數(shù)位數(shù)問題。
在 Product 模型的 Meta 類中,由于我們計劃使用 id 和 slug 進行索引,這里使用 index_together 選項指定使用 id 和 slug 進行索引。兩個索引組合可以改善兩個字段的查詢性能。
由于模型需要處理圖片,打開 shell 并使用以下命令安裝 Pillow :
pip install Pillow
現(xiàn)在,運行另一個命令來為項目創(chuàng)建初始遷移文件:
python manage.py makemigrations
現(xiàn)在可以看到以下輸出:
Migrations for 'shop':
shop/migrations/0001_initial.py
- Create model Category
- Create model Product
- Alter index_together for product (1 constraint(s))
運行以下命令同步數(shù)據(jù)庫:
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
現(xiàn)在,數(shù)據(jù)庫與模型同步了。
將產(chǎn)品目錄模型注冊到 admin網(wǎng)站
將產(chǎn)品目錄模型注冊到 admin網(wǎng)站可以幫助我們管理目錄和產(chǎn)品。編輯 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網(wǎng)站的列表展示頁面可以更改的字段。這樣可以同時編輯多行,由于只有展示的內(nèi)容才能進行編輯,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)
現(xiàn)在,使用以下命令為網(wǎng)站創(chuàng)建超級用戶:
python manage.py createsuperuser
使用 python manage.py runserver 命令啟動開發(fā)服務器,在瀏覽器中打開 http://127.0.0.1:8000/admin/shop/product/add/并使用剛剛創(chuàng)建的賬號登錄。使用 admin網(wǎng)站添加一個新的商品目錄和一個新的商品,admin網(wǎng)站的商品更改列表頁面看起來是這樣的:

創(chuàng)建產(chǎn)品目錄視圖
為了展示產(chǎn)品目錄,我們需要創(chuàng)建一個視圖列出所有產(chǎn)品或者通過給定類別對產(chǎn)品進行過濾。編輯 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 參數(shù)來獲得給定類別的商品。
我們還需要一個視圖來獲取和展示單個產(chǎn)品。在 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 參數(shù)來檢索 Product 實例。由于 id 的唯一屬性,我們只通過 id 就可以獲得實例。然而我們包含URL 中的 slug 來為商品創(chuàng)建SEO-友好的 URL。
創(chuàng)建完產(chǎn)品列表和詳情視圖,我們需要為它們定義 URL 模式。在 shop 應用目錄下創(chuàng)建名為 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'), ])
這是產(chǎn)品目錄的 URL模式。我們?yōu)?product_list 視圖設置了兩個不同的 URL模式:一個模式為 product_list ,可以在不輸入任何參數(shù)的情況下的調(diào)用 product_list 視圖,另一個模式為 product_list_by_category ,需要向視圖提供 category_slug 參數(shù)來告訴視圖通過給定類別對進行過濾。我們?yōu)?product_detail 視圖設置了一個模式,該視圖需要提供 id 和 slug 參數(shù)獲得指定的產(chǎn)品。
編輯 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。
現(xiàn)在,編輯 shop 應用的 models.py 文件,導入 reverse() 函數(shù),并向 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。
我們已經(jīng)知道,get_absolute_url() 是獲得指定對象 url 的簡便方法,這里,我們將使用剛剛在 urls.py 文件中定義的 URLs模式。
創(chuàng)建產(chǎn)品目錄模板
現(xiàn)在,我們需要為商品列表和詳情視圖創(chuàng)建模板。在 shop 應用目錄下創(chuàng)建下面的目錄和文件結(jié)構:

我們需要定義基礎模板,然后在產(chǎn)品列表和詳情模板中對其進行擴展。編輯 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/ 目錄下的靜態(tài)文件拷貝到相同的路徑下。
編輯 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 %}
筆者注:
如果想統(tǒng)一商品圖片的大小以及節(jié)約空間,這里可以使用縮略圖,我們在第五章學習了縮略圖的用法。
使用 pip install sorl-thumbnail 安裝,在項目 settings.py 的 INSTALLED_APPS 中添加 'sorl.thumbnail'。
使用 python manage.py migrate 同步數(shù)據(jù)庫。
然后將
<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 %}
這是產(chǎn)品列表模板。它擴展 shop/base.html 模板并在邊欄中使用 categories 變量展示所有分類,使用 products 展示當前頁面的產(chǎn)品。這個模板適用于兩種情況:列出所有可獲得的產(chǎn)品和通過類別過濾到的產(chǎn)品。由于 Product 模型的 image 字段可以為空,我們需要為沒有圖片的產(chǎn)品設置默認圖片。默認圖片位于靜態(tài)文件中的 img/no_image.png 。
由于使用 ImageField 存儲產(chǎn)品圖片,我們需要開發(fā)服務器提供上傳圖片文件服務。編輯 myshop 的 settings.py 文件并添加以下設置:
# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
MEDIA_URL 是用戶上傳文件的基礎 URL 。MEDIA_ROOT 是這些文件的本地位置,通過 BASE_DIR 進行動態(tài)創(chuàng)建。
Django 使用開發(fā)服務器上傳文件需要編輯 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)
只在開發(fā)過程中這樣處理靜態(tài)文件,在生產(chǎn)過程中,不要使用 Django 處理靜態(tài)文件。
使用 admin網(wǎng)站為商店添加幾個商品并在瀏覽器中打開 http://127.0.0.1:8000/。你將看到商品列表頁面:

如果你使用 admin 網(wǎng)站創(chuàng)建了商品但是沒有上傳圖片,那么將看到 no_image.png:

編輯 shop/product/detail.html 來編輯產(chǎn)品詳情模板,模板并添加以下代碼:
{% 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 %}
我們將調(diào)用 category 的 get_absolute_url() 方法來獲得同類的產(chǎn)品列表?,F(xiàn)在,在瀏覽器中打開 http://127.0.0.1:8000/,點擊任意產(chǎn)品來查看產(chǎn)品詳情??雌饋硎沁@樣的:

創(chuàng)建購物車
創(chuàng)建完商品目錄之后,下一步是創(chuàng)建保存用戶選擇的商品的購物車。購物車幫助用戶選擇想要的商品并在用戶瀏覽網(wǎng)站時暫時保存選中的商品直到用戶下單。購物車應該放在 session 中以便用戶瀏覽時將商品放入購物車。
我們將使用 Django session 框架來存放購物車。 用戶結(jié)賬或者退出登錄之前購物車將保留在 session 中。我們需要創(chuàng)建額外的 Django 模型來保存購物車中的商品。
使用 Django session
Django 提供 session 框架來支持匿名和用戶會話。 session 框架幫助我們?yōu)槊课粸g覽者保存任何數(shù)據(jù)。 除非使用基于 cookie 的 session 引擎,Session 數(shù)據(jù)一般存儲在服務端, cookie 則存儲 session ID。 session 中間件負責管理發(fā)送和接收 cookies 。默認的 session 引擎在數(shù)據(jù)庫中保存 session 數(shù)據(jù),當然,也可以選擇其它的 session 引擎。
為了使用 session,我們需要項目設置的 MIDDLEWARE_CLASSES 中包含django.contrib.sessions.middleware.SessionMiddleware。這個引擎用于管理 sessions ,如果使用startproject 命令創(chuàng)建新項目時默認添加。
session 中間件可以實現(xiàn)從 request 對象中訪問當前 session 。我們可以通過 request.session 得到當前 session ,可以像使用 Python 字典一樣保存和獲得 session 數(shù)據(jù)。session 字典接收任何可以序列化為 JSON 的 Python 對象,我們可以這樣設置 session 變量:
request.session['foo'] = 'bar'
獲得 session 的值:
request.session.get('foo')
刪除 session 中保存的一個值:
del request.session['foo']
我們可以看到,可以像操作 Python 字典一樣處理 request.session 。
注意:
當用戶登錄網(wǎng)站時,將丟棄他們的匿名會話并為有權限的用戶創(chuàng)建一個新的 session 。如果需要保存一個登錄后可用的匿名 session ,那么需要將舊的 session 數(shù)據(jù)拷貝到新的 session 數(shù)據(jù)中。
session 設置
可以使用幾種方法為配置項目 sessions 。最重要的是 SESSION_ENGINE。這個設置允許用戶設置 session 存儲位置。默認情況下,Django 使用 django.contrib.sessions 應用的 Session 模型將數(shù)據(jù)保存到數(shù)據(jù)庫中。
Django 提供以下存儲 session 數(shù)據(jù)的選項:
- Database sessions: Session 數(shù)據(jù)保存在數(shù)據(jù)庫中,默認的 session 引擎。
- File-based sessions: Session 數(shù)據(jù)保存在文件系統(tǒng)中。
- Cached sessions: Session 數(shù)據(jù)保存在緩存后端,可以使用 CACHES 設置指定緩存后端,將 session 數(shù)據(jù)保存在緩存后端可以實現(xiàn)最好的性能。
- Cached database sessions: Session 數(shù)據(jù)保存在 write-through 緩存和數(shù)據(jù)庫。數(shù)據(jù)不在緩存中時才讀取數(shù)據(jù)庫。
- Cookie-based sessions: Session 數(shù)據(jù)保存在發(fā)送到瀏覽器的 cookies 中。
注意:
為了獲得更好的性能可以使用 cache-based session 引擎。Django 支持 Memcached 和其它 Redis 第三方緩存后端以及其他緩存系統(tǒng)。
你可以使用其它設置自定義 sessions 。這里有一些非常重要的 session 設置:
SESSION_COOKIE_AGE: session cookie 保存時間(秒為單位)。默認值為 1209600 (2 周)。
SESSION_COOKIE_DOMAIN: session cookies 使用的域,將其設置為 .mydomain.com 可以實現(xiàn)跨域 cookies 。
SESSION_COOKIE_SECURE : 布爾值,是否只有HTTPS連接才能發(fā)送cookie。
SESSION_EXPIRE_AT_BROWSER_CLOSE: 布爾值,關閉瀏覽器時 session 是否過期。
SESSION_SAVE_EVERY_REQUEST: 布爾值,如果為 True,每個request 都會將 session 保存到數(shù)據(jù)庫,并且每次更新 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 中保存購物車
我們需要創(chuàng)建一個可以序列化為 JSON 的簡單結(jié)構來在 session 中保存購物車內(nèi)的商品 。 購物車中的每種商品需要包含以下數(shù)據(jù):
Product 實例的 id ;
商品的數(shù)量;
這個商品的單價;
由于商品價格可能變動,當添加到購物車時,我們將商品價格和商品放在一起。這樣,可以將價格保持在顧客將商品添加到購物車時的價格,即使價格隨后可能發(fā)生改變也不會受到影響。
現(xiàn)在,我們需要創(chuàng)建購物車并將數(shù)據(jù)保存到 session 中。購物車需要這樣工作:
當需要購物車時,我們檢查是否設置了一個自定義 session 鍵,如果 session 沒有設置購物車,我們將創(chuàng)建新的購物車并將其保存到購物車 session 鍵中。
對于成功請求,我們進行相同的檢查并從 購物車 session 鍵中獲得值。我們從 session 中獲得購物車中的商品并從數(shù)據(jù)庫中獲取對應 Product 對象。
編輯項目的 settings.py 文件并添加以下設置:
# session settings
CART_SESSION_ID = 'cart'
這是我們在用戶 session 中保存購物車的鍵。由于 Django session 是 pre-visitor,所有 session 都使用相同的購物車 session 鍵。
我們來創(chuàng)建一個管理購物車的應用,打開 teminal 并創(chuàng)建一個新的應用,在項目目錄下運行以下命令:
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 應用的目錄下創(chuàng)建一個 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 作為鍵,由商品數(shù)量和價格組成的字典作為值。這樣,可以保證 cart 不能多次添加同一商品,還便于訪問 cart 中的任意商品數(shù)據(jù)。
我們來創(chuàng)建一個方法在購物車中添加商品或更新商品數(shù)量。向 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() 方法接收以下參數(shù):
- product: 購物車添加或者更改的 Product 實例;
- quality: 商品數(shù)量,可選的整數(shù),默認值為 1 ;
- update_quality :布爾值,是否使用輸入的數(shù)量對數(shù)量進行更新的標志位,如果為True,則根據(jù)輸入的數(shù)量更新數(shù)量,如果為 False ,新值與原來的值相加。
我們使用商品 id 作為 cart 字典的鍵,由于Django 使用 JSON 進行序列化,而 JSON 只允許字符串鍵,因此這里將商品 id 轉(zhuǎn)換為字符串。商品 id 為鍵,quality 和 price 組成的字典為值。由于序列化要求,商品的價格也由 Decimal 格式轉(zhuǎn)換為 string 格式。最后,調(diào)用 save() 方法將 cart 保存到 session 中。
save() 方法在 session 中保存 cart 的所有變化,并通過 session.modified = True 將 session 標記為更改狀態(tài)。這將告訴 django 發(fā)生了更改需要進行保存。
我們還需要一個從購物車中刪除商品的方法,向 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() 方法從購物車字典中刪除指定商品并調(diào)用 save() 方法更新購物車。
我們還需要對購物車中的商品進行迭代來訪問相關的 Product 實例。我們可以在類中定義__iter__()來實現(xiàn)該功能。向 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 屬性?,F(xiàn)在,我們可以很容易的遍歷購物車中的商品了。
我們還需要返回購物車中的所有商品數(shù)量,當我們對一個對象執(zhí)行 len() 函數(shù)時,Python 調(diào)用它的 __len__方法來獲得長度。我們將定義自定義 __len__方法來返回購物車中商品的總數(shù)量。在 Cart 類中添加 __len__方法:
def __len__(self):
"""
Count all items in the cart.
"""
return sum(item['quantity'] for item in self.cart.values())
將返回購物車中所有商品的總數(shù)量。
添加以下方法來計算購物車商品的總價格:
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
現(xiàn)在, Cart 類可以管理購物車了。
創(chuàng)建購物車視圖
現(xiàn)在,我們已經(jīng)有一個管理購物車的 Cart 類了,現(xiàn)在需要創(chuàng)建視圖來添加、更新或者移除購物車中的商品。我們需要創(chuàng)建以下視圖:
- 可以添加或者更新購物車中的商品的視圖,可以處理當前和更新的數(shù)量;
- 刪除購物車中商品的視圖
- 展示購物車中商品和總量的視圖
向購物車添加商品
為了能夠在購物車中添加商品,我們需要一個表單來實現(xiàn)選擇數(shù)量的功能。在 cart 應用目錄下創(chuàng)建一個 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 來將輸入轉(zhuǎn)換為整型;
update: 標志位,如果為False,則在購物車原數(shù)量的基礎上增加quantity,如果為True,則將數(shù)量設置為 quantity。由于不想讓用戶看到,這個字段使用了 HiddenInput 小控件。
創(chuàng)建一個向購物車添加商品的視圖。編輯 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')
這是一個向購物車添加商品或更新商品數(shù)量的視圖,由于視圖將更改數(shù)據(jù),視圖使用 require_POST 裝飾器只允許 POST 請求。視圖以商品 ID 作為參數(shù),我們獲得特定 ID 的 Product 實例并驗證 CartAddProductForm 表單。如果表單有效,將添加或更新購物車中的商品。視圖重定向到 cart_detail URL 來展示購物車中的商品。我們稍后將創(chuàng)建 cart_detail 視圖。
此外,還需要創(chuàng)建從購物車中移除商品的視圖。將以下代碼添加到 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。
最后,我們需要一個視圖來展示購物車和購物車內(nèi)的商品。在 views.py 中添加以下代碼:
def cart_detail(request):
cart = Cart(request)
return render(request, 'cart/detail.html', {'cart': cart})
cart_detail 獲取當前的購物車并進行展示。
我們已經(jīng)創(chuàng)建了購物車添加商品、更新數(shù)量、刪除商品、展示商品的視圖。接下來我們?yōu)檫@些視圖添加 URL 模式。 在 cart 應用目錄下創(chuàng)建一個 urls.py 的新文件,并添加下面的內(nèi)容:
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 限制更多。
創(chuàng)建展示購物車的模板
cart_add 和 cart_delete 視圖不需要渲染模板,但是我們需要為 cart_detail 視圖創(chuàng)建模板來展示購物車中的商品和總數(shù)。
在 cart 應用目錄下創(chuàng)建下面的文件結(jié)構:
編輯 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 %}
這是展示購物車內(nèi)容的模板。它包含一個當前購物車商品的表格。用戶可以通過指向 cart_add 視圖的表單更改選中產(chǎn)品的數(shù)量。我們還通過為每個商品提供刪除鏈接來刪除商品。
將商品添加到購物車
現(xiàn)在,我們需要為商品詳情頁面添加一個 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 模板,并在產(chǎn)品價格后面添加下面的表單:
<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 運行開發(fā)服務器。在瀏覽器中打開 http://127.0.0.1:8000/ 并點擊某個商品到商品詳情頁面?,F(xiàn)在頁面在添加到購物車前面有一個選擇數(shù)量的選項。頁面看起來是這樣的:

選擇數(shù)量并點擊 Add to cart 按鈕。表單通過 POST 方法提交到 cart_add 視圖。視圖將商品(包括商品的價格和選擇的數(shù)量)添加到 session 的購物車中。然后,重定向到購物車詳情頁面,看起來是這樣的:

在購物車中更新產(chǎn)品數(shù)量
用戶查看購物車時,他們在下單之前可能需要更改產(chǎn)品數(shù)量。下面將實現(xiàn)購物車詳細頁面更改商品數(shù)量的功能。
編輯 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})
我們?yōu)橘徫镘嚨拿恳粋€商品創(chuàng)建了一個 CartAddProductForm 實例,這樣就可以更改產(chǎn)品數(shù)量了。這里使用產(chǎn)品的當前數(shù)量并將 update 設置為 True 對實例進行初始化,這樣我們可以將表單提交到 cart_add 視圖,新的產(chǎn)品數(shù)量會代替當前產(chǎn)品數(shù)量。
現(xiàn)在,編輯 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/,可以看到每個商品都包含編輯數(shù)量的表單:

更改某個商品的數(shù)量,并點擊 Update 按鈕對新功能進行測試。
為當前購物車創(chuàng)建內(nèi)容處理器
你可能已經(jīng)注意到,頁面頭部還在顯示 Your cart is empty 的信息。當我們開始向購物車添加商品時,我們應該可以看到購物車添加商品的數(shù)量和總金額。由于需要在所有頁面展示這些信息,我們需要創(chuàng)建內(nèi)容處理器將當前購物車包含在請求內(nèi)容中,這些內(nèi)容與被處理的視圖無關。
內(nèi)容處理器
內(nèi)容處理器是一個 Python 函數(shù),它的輸入?yún)?shù)為 request 對象,返回添加到請求內(nèi)容中的字典。它可以用來生成所有模板都需要的內(nèi)容。
默認情況下,使用 startproject 命令創(chuàng)建一個新的項目,項目將包含下面的內(nèi)容處理器(在 TEMPLATES 設置的 context_processors 選項中):
django.template.context_processors.debug:設置內(nèi)容中表示請求執(zhí)行的 SQL 查詢列表的 debug 布爾值和 sql_queries 變量;
django.template.context_processor.request:設置內(nèi)容中的 request 變量;
django.contrib.auth.context_processors.auth:設置請求中的用戶變量;
django.contrib.messages.context_processors.messages:設置message 變量,message 變量包括消息框架中的所有消息;
Django 還將啟用 django.template.context_processors.csrf 來避免跨網(wǎng)站請求偽造攻擊。這個內(nèi)容處理器沒有出現(xiàn)在設置中,但是它一直處于啟用狀態(tài),并且由于安全原因無法關閉。
我們可以在以下頁面了解所有的內(nèi)容內(nèi)容處理器https://docs.djangoproject.com/en/1.11/ref/templates/api/#built-in-template-context-processors。
在請求內(nèi)容中設置購物車
我們創(chuàng)建內(nèi)容處理器來將購物車放到模板的 request 內(nèi)容中。這樣任意模板都可以訪問這個購物車。
在 cart 應用目錄下新建名為 context_processors.py 的文件。內(nèi)容處理器可以放在代碼中的任何位置,但是放置在這里可以更好的組織代碼,在文件中添加以下代碼:
from .cart import Cart
def cart(request):
return {'cart': Cart(request)}
正如我們看到的,內(nèi)容處理器是一個函數(shù),它接收 request 對象作為參數(shù),返回一個任何模板都可以通過 RequestContext 渲染的字典對象。在我們的內(nèi)容處理器中,我們使用 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',
],
},
},
]
現(xiàn)在,每次使用 Django 的 RequestContext 渲染模板時都會執(zhí)行我們剛剛創(chuàng)建的模板處理器,模板內(nèi)容中將包含模板變量。
注意:
內(nèi)容處理器在所有使用 RequestContext 的請求中執(zhí)行。如果需要訪問數(shù)據(jù)庫,最好創(chuàng)建自定義模板標簽,而不是使用內(nèi)容處理器。
現(xiàn)在,編輯 shop 應用中的 shop/base.html 模板并找到以下內(nèi)容:
<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/并在購物車中添加一些商品,在頁面的頭部,你將看到商品的總數(shù)和總價格:

管理用戶訂單
結(jié)算時,我們需要將訂單保存到數(shù)據(jù)庫中,訂單將包含用戶信息以及他們購買的商品。
使用以下命令創(chuàng)建一個管理用戶訂單的新應用:
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', ]
你已經(jīng)激活了一個新應用。
創(chuàng)建訂單模型
我們需要創(chuàng)建一個存儲訂單詳情的模型以及一個存儲訂單購買的商品(包含價格和數(shù)量)的模型。編輯 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 。后面我們將使用這個字段區(qū)分訂單是否付款。此外,還定義了一個 get_total_cost() 方法來計算訂單商品的總費用。
OrderItem 模型用來保存每個訂單的商品、數(shù)量和價格,并使用 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
現(xiàn)在我們的訂單模型已經(jīng)同步到數(shù)據(jù)庫了。
將訂單模型注冊到 admin網(wǎng)站
我們將訂單模型添加到 admin網(wǎng)站。編輯 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 類中。內(nèi)聯(lián)(inline)幫助我們將一個模型放入父模型的編輯頁面中。
使用 python manage.py runserver 啟動開發(fā)服務器。在瀏覽器中打開http://127.0.0.1:8000/admin/orders/order/add/ ,你將看到下面的頁面:

創(chuàng)建用戶訂單
我們需要在用戶下單時使用剛剛創(chuàng)建的訂單模型來保存購物車中的商品。創(chuàng)建新訂單需要完成以下工作:
為用戶提供一個訂單表單來填寫用戶數(shù)據(jù);
使用用戶輸入的數(shù)據(jù)創(chuàng)建一個新的 Order 實例,然后為購物車的每件商品關聯(lián)一個 OrderItem 實例;
-
清空購物車的內(nèi)容并重定向到成功頁面。
?
首先,我們需要一個表單來填寫訂單詳情。在 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 對象的表單?,F(xiàn)在需要一個視圖來處理這個表單并創(chuàng)建一個新訂單。編輯 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 數(shù)據(jù),如果數(shù)據(jù)有效,使用 order = form.save() 創(chuàng)建新的 Order 實例,然后將其保存到數(shù)據(jù)庫以及 order 變量中。創(chuàng)建完 order 后,我們將對購物車中的商品進行迭代并為每個商品創(chuàng)建 OrderItem 。最后清空購物車。
現(xiàn)在,在 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>
用戶現(xiàn)在可以從購物車詳情頁面跳轉(zhuǎn)到訂單表單了。我們還需要為訂單定義模板。在 orders 應用下創(chuàng)建如下結(jié)構:

編輯 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 %}
這是成功下單后使用的模板。啟動開發(fā)服務器,在瀏覽器中打開 http://127.0.0.1:8000/,在購物車中添加一些商品,然后跳轉(zhuǎn)到結(jié)算頁面。你將看到下面的內(nèi)容:

在表單中填寫有效數(shù)據(jù)并點擊 Place order 按鈕,訂單將被創(chuàng)建,你見看到一個下面這樣的成功頁面。

使用 Celery 加載異步任務
視圖執(zhí)行的所有內(nèi)容都對響應時間有影響。我們希望盡快返回響應以及在服務器上異步執(zhí)行一些進程。這對耗時的進程或需要重試策略的進程而言尤為重要。 比如,用戶可以在視頻共享平臺上傳視頻,但對上傳的視頻進行轉(zhuǎn)碼需要很長時間。 網(wǎng)站可能向用戶返回響應,告訴他很快開始轉(zhuǎn)碼,并開始進行異步轉(zhuǎn)碼。 另一個例子是給用戶發(fā)送電子郵件。 如果您的站點使用視圖發(fā)送電子郵件通知,SMTP 連接可能失敗或者影響響應速度。 啟動異步任務對避免阻塞執(zhí)行非常重要。
Celery 是一個可以處理大量信息的分布式任務隊列。它進行實時處理,同時支持任務計劃。使用 Celery 不僅可以更輕松的創(chuàng)建異步任務,并且可以盡快執(zhí)行,但是也可以為這些任務設定執(zhí)行時間。
你可以在這里找到 Celery 文檔 http://celery.readthedocs.io/en/latest/ 。
安裝 Celery
我們來安裝 Celery 并集成到項目中。通過 pip 使用以下命令安裝 Celery :
pip install celery
Celery 需要一個消息中間件來處理外部請求。 消息中間件負責將消息發(fā)送到 Celery ,Celery 收到消息時處理任務。 下面來安裝一個消息中間件。
安裝 RabbitMQ
Celery 可以使用幾種消息中間件,包括鍵值對存儲(如 redis )和實際消息系統(tǒng)( 如 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
你將看到以下面內(nèi)容結(jié)尾的輸出:
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)
在代碼中,我們?yōu)?Celery 命令行程序設置 DJANGO_SETTINGS_MODULE 變量,然后使用app = Celery('myshop') 創(chuàng)建應用實例。使用 config_from_object() 方法從項目設置加載自定義配置。最后告訴 Celery 自動發(fā)現(xiàn) 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 模塊的名稱相同會造成后續(xù)運行時出現(xiàn)以下錯誤:
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
現(xiàn)在我們可以開始為應用編寫異步程序了。
注意:
Celery_ALWAYS_EAGER 設置允許我們按照同步方式在本地執(zhí)行任務,而不是將其發(fā)送到隊列中。這對于單元測試或在本地環(huán)境不運行 Celery 的項目非常有幫助。
為應用添加異步任務
我們將創(chuàng)建異步任務,用戶下單后異步發(fā)送通知郵件。
我們只需將應用的異步任務放入應用目錄下的 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 函數(shù)。 task 函數(shù)接收 order_id 參數(shù)。推薦只將 ID 傳入任務函數(shù)并在執(zhí)行任務時執(zhí)行查詢。我們使用 Django 提供的 send_mail() 函數(shù)來通知用戶已經(jīng)下單。如果不希望啟動郵件設置,可以在 settings.py 文件中進行以下設置將郵件輸出到 console :
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
注意:
使用異步任務不僅可以節(jié)約處理時間,還可以用于可能失敗的過程,它們可能不需要很長的執(zhí)行時間,但是可能會出現(xiàn)連接失敗或者需要重試策略。
現(xiàn)在,我們需要將任務添加到 order_create 視圖中。打開 orders 應用的 views.py 文件并添加以下代碼:
from .tasks import order_created
然后,在清理完購物車后調(diào)用 order_created 異步任務:
# clear the cart
cart.clear()
# launch asynchronous task
order_created.delay(order.id)
我們調(diào)用 delay() 方法來異步執(zhí)行任務。任務將被添加到隊列并盡快執(zhí)行。
打開另一個 shell 并使用下面的命令啟動 celery 工作:
celery -A myshop.celery_task:app worker -l info
筆者注:
原文命令為:
celery -A myshop worker -l info由于前面將 celery.py 改為 celery_task ,因此執(zhí)行的命令指定了文件名。
celery 現(xiàn)在正在運行并可以處理任務。啟動 Django 開發(fā)服務器。在瀏覽器中打開 http://127.0.0.1:8000/ ,在購物車中添加一些商品,完成訂單,在 shell 中,已經(jīng)開啟了 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.
任務已經(jīng)執(zhí)行,我們可以接收到下單通知了。
監(jiān)控 Celery
我們可能希望監(jiān)控正在執(zhí)行的異步任務。Flower 是一個基于 web 的 Celery 監(jiān)控器。可以使用 pip install flower 安裝 Flower 。
一旦安裝完畢,我們可以從項目目錄運行以下命令加載 Flower :
celery -A myshop.celery_task:app flower
筆者注:
原文命令為:
celery -A myshop flower修改原因與上面相同。
在瀏覽器中打開 http://localhost:5555/dashboard ,你將看到激活的 celery 和異步任務:

可以從 http://flower.readthedocs.io/en/latest/ 找到 Flower 文檔。
總結(jié)
本章,我們創(chuàng)建了一個簡單的商店應用。創(chuàng)建了商品目錄并使用 sessions 創(chuàng)建了購物車,實現(xiàn)了自定義內(nèi)容處理器保證模板可以獲得內(nèi)容并創(chuàng)建表單來下單。此外,還學習了使用 Celery 加載異步任務。
下一章,我們將學習在商店中集成支付網(wǎng)關,在 admin 網(wǎng)站添加自定義動作,輸出 CSV 數(shù)據(jù)和動態(tài)生成 PDF 文件。