7 構(gòu)建在線商店
在上一章中衙解,你創(chuàng)建了關(guān)注系統(tǒng)和用戶活動流阳柔。你還學(xué)習(xí)了Django信號是如何工作的,并在項目中集成了Redis蚓峦,用于計算圖片的瀏覽次數(shù)舌剂。在這一章中,你會學(xué)習(xí)如何構(gòu)建一個基本的在線商店暑椰。你會創(chuàng)建商品目錄(catalog)霍转,并用Django會話(session)實現(xiàn)購物車。你還會學(xué)習(xí)如果創(chuàng)建自定義上下文管理器干茉,以及用Celery啟動異步任務(wù)谴忧。
在這一章中,你會學(xué)習(xí):
- 創(chuàng)建商品目錄
- 使用Django會話創(chuàng)建購物車
- 管理客戶訂單
- 用Celery給客戶發(fā)送異步通知
7.1 創(chuàng)建在線商店項目
我們將創(chuàng)建一個新的Django項目來構(gòu)建在線商店。用戶可以通過商品目錄瀏覽沾谓,并把商品添加到購物車中委造。最后,客戶結(jié)賬并下單均驶。本章將會覆蓋在線商店以下幾個功能:
- 創(chuàng)建商品目錄模型昏兆,把它們添加到管理站點,并創(chuàng)建一個基礎(chǔ)視圖妇穴,用于顯示目錄
- 使用Django會話構(gòu)建購物車系統(tǒng)爬虱,允許用戶瀏覽網(wǎng)站時保留選定的商品
- 創(chuàng)建用于下單的表單和功能
- 用戶下單后,發(fā)送一封異步確認郵件給用戶
首先腾它,我們?yōu)樾马椖縿?chuàng)建虛機環(huán)境跑筝,并用以下命令激活虛擬環(huán)境:
mkdiv env
virtualenv env/myshop
source env/myshop/bin/activate
使用以下命令在虛擬環(huán)境中安裝Django:
pip install Django
打開終端,執(zhí)行以下命令瞒滴,創(chuàng)建myshop
項目曲梗,以及shop
應(yīng)用:
django-admin startproject myshop
cd myshop
django-admin startapp shop
編輯項目的settings.py
文件,在INSTALLED_APPS
設(shè)置中添加shop
應(yīng)用:
INSTALLED_APPS = (
# ...
'shop',
)
現(xiàn)在項目中的應(yīng)用已經(jīng)激活妓忍。讓我們?yōu)樯唐纺夸浂x模型虏两。
7.1.1 創(chuàng)建商品目錄模型
商店的目錄由屬于不同類別的商品組成。每個商品有名字世剖,可選的描述定罢,可選的圖片,價格旁瘫,以及有效的庫存祖凫。編輯你剛創(chuàng)建的shop
應(yīng)用的models.py
文件,添加以下代碼:
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
這是我們的Category
和Product
模型境蜕。Category
模型由name
字段和唯一的slug
字段組成蝙场。Product
模型包括以下字段:
-
category
:這是指向Catetory
模型的ForeignKey
凌停。這是一個多對一的關(guān)系:一個商品屬于一個目錄粱年,而一個目錄包括多個商品。 -
name
:這是商品的名稱罚拟。 -
slug
:這是商品的別名台诗,用于構(gòu)建友好的URL。 -
image
:這是一張可選的商品圖片赐俗。 -
description
:這是商品的可選描述拉队。 -
price
:這是DecimalField
。這個字段用Python的decimal.Decimal
類型存儲固定精度的十進制數(shù)阻逮。使用max_digits
屬性設(shè)置最大的位數(shù)(包括小數(shù)位)粱快,使用decimal_places
屬性設(shè)置小數(shù)位。 -
stock
:這個PositiveIntegerField
存儲商品的庫存。 -
available
:這個布爾值表示商品是否有效事哭。這允許我們在目錄中啟用或禁用商品漫雷。 -
created
:對象創(chuàng)建時存儲該字段。 -
updated
:對象最后更新時存儲該字段鳍咱。
對于price
字段降盹,我們使用DecimalField
代替FloatField
,來避免四舍五入的問題谤辜。
總是使用
DecimalField
存儲貨幣值蓄坏。在Python內(nèi)部,FloatField
使用float
類型丑念,而DecimalField
使用Decimal
類型涡戳。使用Decimal
類型可以避免float
的四舍五入問題。
在Product
模型的Meta
類中脯倚,我們用index_together
元選項為id
和slug
字段指定共同索引妹蔽。這是因為我們計劃通過id
和slug
來查詢商品。兩個字段共同索引可以提升用這兩個字段查詢的性能挠将。
因為我們要在模型中處理圖片胳岂,打開終端,用以下命令安裝Pillow
:
pip install Pillow
現(xiàn)在舔稀,執(zhí)行以下命令乳丰,創(chuàng)建項目的初始數(shù)據(jù)庫遷移:
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))
執(zhí)行以下命令同步數(shù)據(jù):
python manage.py migrate
你會看到包括這一行的輸出:
Applying shop.0001_initial... OK
現(xiàn)在數(shù)據(jù)庫與模型已經(jīng)同步了。
7.1.2 在管理站點注冊目錄模型
讓我們把模型添加到管理站點内贮,從而可以方便的管理目錄和商品产园。編輯shop
應(yīng)用的admin.py
文件,添加以下代碼:
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)
記住夜郁,我們使用prepopulated_fields
屬性指定用其它字段的值自動填充的字段什燕。正如你前面看到的,這樣可以很容易的生成別名竞端。我們在ProductAdmin
類中使用list_editable
屬性設(shè)置可以在管理站點的列表顯示頁面編輯的字段屎即。這樣可以一次編輯多行。list_editable
屬性中的所有字段都必須列在list_display
屬性中事富,因為只有顯示的字段才可以編輯技俐。
現(xiàn)在使用以下命令為網(wǎng)站創(chuàng)建超級用戶:
python manage.py createsuperuser
執(zhí)行python manage.py runserver
命令啟動開服務(wù)器。在瀏覽器中打開http://127.0.0.1:8000/admin/shop/product/add/
统台,然后用剛創(chuàng)建的用戶登錄雕擂。使用管理界面添加一個新的目錄和商品。管理頁面的商品修改列表頁面看起來是這樣的:
7.1.3 構(gòu)建目錄視圖
為了顯示商品目錄贱勃,我們需要創(chuàng)建一個視圖列出所有商品井赌,或者通過制定的目錄過濾商品谤逼。編輯shop
應(yīng)用的views.py
文件,添加以下代碼:
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})
我們用available=True
過濾QuerySet
仇穗,只檢索有效地商品森缠。我們用可選的category_slug
參數(shù),過濾指定目錄的商品仪缸。
我們還需要一個查詢和顯示單個商品的視圖贵涵。添加以下代碼到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中包括別名,為商品構(gòu)建搜索引擎友好的URL拴还。
創(chuàng)建商品列表和詳情視圖后跨晴,我們需要為它們定義URL模式。在shop
應(yīng)用目錄中創(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'),
]
這些是商品目錄的URL模式端盆。我們?yōu)?code>product_list視圖定義了兩個不同的URL模式:product_list
模式不帶任何參數(shù)調(diào)用product_list
視圖;product_list_by_category
模式給視圖提供category_slug
參數(shù)费封,用于過濾指定目錄的商品焕妙。我們添加了product_detail
模式,傳遞id
和slug
參數(shù)給視圖弓摘,用于檢索特定商品焚鹊。
編輯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')),
]
我們在項目的主URL模式中引入了shop
應(yīng)用的URL韧献,并指定命名空間為shop
末患。
現(xiàn)在編輯shop
應(yīng)用的models.py
文件,導(dǎo)入reverse()
函數(shù)锤窑,并為Category
和Product
模型添加get_absolute_url()
方法璧针,如下所示:
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])
你已經(jīng)知道,get_absolute_url()
是檢索指定對象URL的約定成俗的方法渊啰。我們在這里使用之前在urls.py
文件中定義的URL模式探橱。
7.1.4 創(chuàng)建目錄模板
現(xiàn)在我們需要為商品列表和詳情視圖創(chuàng)建模板。在shop
應(yīng)用目錄中創(chuàng)建以下目錄和文件結(jié)構(gòu):
templates/
shop/
base.html
product/
list.html
detail.html
我們需要定義一個基礎(chǔ)模板虽抄,并在商品列表和詳情模板中繼承它走搁。編輯shop/base.html
模板,添加以下代碼:
{% 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>
這是商店的基礎(chǔ)模板迈窟。為了引入模板使用的CSS樣式表和圖片,你需要拷貝本章實例中的靜態(tài)文件忌栅,它們位于shop
應(yīng)用的static/
目錄车酣。把它們拷貝到你的項目中的相同位置曲稼。
編輯shop/product/list.html
模板,添加以下代碼:
{% extends "shop/base.html" %}
{% load static %}
{% block title %}
{% if category %}{{ category.name }}{% else %}Products{% endif %}
{% endblock title %}
{% 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 catetory %}{{ category.name }}{% else %}Products{% endif %}</h1>
{% for product in products %}
<div class="item">
<a href="{{ product.get_absolute_url }}">

</a>
<a href="{{ product.get_absolute_url }}">{{ product.name }}</a><br/>
${{ product.price }}
</div>
{% endfor %}
</div>
{% endblock content %}
這是商品列表目錄湖员。它繼承自shop/base.html
目錄贫悄,用categories
上下文變量在側(cè)邊欄顯示所有目錄,用products
顯示當前頁商品娘摔。用同一個模板列出所有有效商品和通過目錄過濾的所有商品窄坦。因為Product
模型的image
字段可以為空,所以如果商品沒有圖片時凳寺,我們需要提供一張默認圖片鸭津。圖片位于靜態(tài)文件目錄,相對路徑為img/no_image.png
肠缨。
因為我們用ImageField
存儲商品圖片逆趋,所以需要開發(fā)服務(wù)器管理上傳的圖片文件。編輯myshop
的settings.py
文件晒奕,添加以下設(shè)置:
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
MEDIA_URL
是管理用戶上傳的多媒體文件的基礎(chǔ)URL闻书。MEDIA_ROOT
是這些文件的本地路徑,我們在前面添加BASE_DIR
變量脑慧,動態(tài)生成該路徑魄眉。
要讓Django管理通過開發(fā)服務(wù)器上傳的多媒體文件,需要編輯myshop
項目的urls.py
文件闷袒,如下所示:
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)
記住杆融,我們只在開發(fā)階段這么做。在生產(chǎn)環(huán)境霜运,你不應(yīng)該用Django管理靜態(tài)文件脾歇。
使用管理站點添加一些商品,然后在瀏覽器中打開http://127.0.0.1:8000/
淘捡。你會看到商品列表頁面藕各,如下圖所示:
如果你用管理站點創(chuàng)建了一個商品,但是沒有上傳圖片焦除,則會顯示默認圖片:
讓我們編輯商品詳情模板激况。編輯shop/product/detail.html
模板,添加以下代碼:
{% extends "shop/base.html" %}
{% load static %}
{% block titie %}
{% if category %}{{ category.title }}{% else %}Products{% endif %}
{% endblock titie %}
{% block content %}
<div class="product-detail">

<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 content %}
我們在關(guān)聯(lián)的目錄對象上調(diào)用get_absolute_url()
方法膘魄,來顯示屬于同一個目錄的有效商品∥谥穑現(xiàn)在在瀏覽器中打開http://127.0.0.1/8000/
,點擊某個商品查看詳情頁面创葡,如下圖所示:
我們現(xiàn)在已經(jīng)創(chuàng)建了一個基本的商品目錄浙踢。
7.2 構(gòu)建購物車
創(chuàng)建商品目錄之后,下一步是創(chuàng)建購物車灿渴,讓用戶選擇他們希望購買的商品洛波。當用戶瀏覽網(wǎng)站時胰舆,購物車允許用戶選擇并暫時存儲他們想要的商品,直到最后下單蹬挤。購物車存儲在會話中缚窿,所以在用戶訪問期間可以保存購物車里的商品。
我們將使用Django的會話框架保存購物車焰扳。購物車會一直保存在會話中倦零,直到完成購物或者用戶結(jié)賬離開。我們還需要為購物車和它的商品創(chuàng)建額外的Django模型吨悍。
7.2.1 使用Django會話
Django提供了一個會話框架扫茅,支持匿名和用戶會話。會話框架允許你為每個訪問者存儲任意數(shù)據(jù)畜份。會話數(shù)據(jù)保存在服務(wù)端诞帐,cookies包括會話ID,除非你使用基于cookie的會話引擎爆雹。會話中間件負責發(fā)送和接收cookies停蕉。默認的會話引擎在數(shù)據(jù)庫中存儲會話數(shù)據(jù),但是接下來你會看到钙态,可以選擇不同的會話引擎慧起。要使用會話,你必須確保項目的MIDDLEWARE_CLASSES
設(shè)置中包括django.contrib.sessions.middleware.SessionMiddleware
册倒。這個中間件負責管理會話蚓挤,當你用startproject
命令創(chuàng)建新項目時,會默認添加這個中間件驻子。
會話中間件讓當前會話在request
對象中生效灿意。你可以使用request.session
訪問當前會話,與使用Python字典類似的存儲和檢索會話數(shù)據(jù)崇呵。會話字典默認接收所有可以序列化為JSON的Python對象缤剧。你可以這樣在會話中設(shè)置變量:
request.session['foo'] = 'bar'
查詢一個會話的鍵:
request.session.get('foo')
刪除存儲在會話中的鍵:
del request.session['foo']
正如你所看到的,我們把request.session
當做標準的Python字典域慷。
當用戶登錄到網(wǎng)站時荒辕,他們的匿名會話丟失,并未認證用戶創(chuàng)建新的會話犹褒。如果你在匿名會話中存儲了數(shù)據(jù)抵窒,并想在用戶登錄后保留,你需要舊的會話數(shù)據(jù)拷貝到新的會話中叠骑。
7.2.2 會話設(shè)置
你可以使用幾種設(shè)置為項目配置會話李皇。其中最重要的是SESSION_ENGINE
。該設(shè)置允許你設(shè)置會話存儲的位置座云。默認情況下疙赠,Django使用django.contrib.sessions
應(yīng)用的Session
模型付材,把會話存儲在數(shù)據(jù)庫中朦拖。
Django為存儲會話數(shù)據(jù)提供了以下選項:
-
Database sessions
:會話數(shù)據(jù)存儲在數(shù)據(jù)庫中圃阳。這是默認的會話引擎。 -
File-based sessions
:會話數(shù)據(jù)存儲在文件系統(tǒng)中璧帝。 -
Cached sessions
:會話數(shù)據(jù)存儲在緩存后臺捍岳。你可以使用CACHES
設(shè)置指定婚車后臺。在緩存系統(tǒng)中存儲會話數(shù)據(jù)的性能最好睬隶。 -
Cached database sessions
:會話數(shù)據(jù)存儲在連續(xù)寫入的緩存(write-through cache)和數(shù)據(jù)庫中锣夹。只有在緩存中沒有數(shù)據(jù)時才讀取數(shù)據(jù)庫。 -
Cookie-based sessions
:會話數(shù)據(jù)存儲于發(fā)送到瀏覽器的cookies苏潜。
使用
cache-based
會話引擎有更好的性能银萍。Django支持Memcached,以及其它支持Redis的第三方緩存后臺和緩存系統(tǒng)恤左。
你可以只是用其它設(shè)置自定義會話贴唇。以下是一些重要的會話相關(guān)設(shè)置:
-
SESSION_COOKIE_AGE
:這是會話cookies的持續(xù)時間(單位是秒)。默認值是1209600(兩周)飞袋。 -
SESSION_COOKIE_DOMAIN
:會話cookies使用的域戳气。設(shè)置為.mydomain.com
可以啟用跨域cookies。 -
SESSION_EXPIRE_AT_BROWSER_CLOSE
:當瀏覽器關(guān)閉后巧鸭,表示會話是否過期的一個布爾值瓶您。 -
SESSION_SAVE_EVERY_REQUEST
:如果這個布爾值為True
,則會在每次請求時把會話保存到數(shù)據(jù)庫中纲仍。會話的過期時間也會每次更新呀袱。
你可以在這里查看所有會話設(shè)置。
7.2.3 會話過期
你可以使用SESSTION_EXPIRE_AT_BROWSER_CLOSE
設(shè)置選擇browser-length
會話或者持久會話郑叠。默認值為False
夜赵,強制把會話的有效期設(shè)置為SESSION_COOKIE_AGE
的值。如果設(shè)置SESSTION_EXPIRE_AT_BROWSER_CLOSE
為True
锻拘,當用戶關(guān)閉瀏覽器后油吭,會話會過期,而SESSION_COOKIE_AGE
不會起任何作用署拟。
你可以使用request.session
的set_expiry()
方法覆寫當前會話的有效期流济。
7.2.4 在會話中存儲購物車
我們需要創(chuàng)建一個簡單的可以序列號為JSON的結(jié)構(gòu)體父阻,在會話中存儲購物車商品。購物車的每一件商品必須包括以下數(shù)據(jù):
-
Product
實例的id
- 選擇該商品的數(shù)量
- 該商品的單價
因為商品價格可能變化,所以當商品添加到購物車時义起,我們把商品的價格和商品本身同事存入購物車。這樣的話,即使之后商品的價格發(fā)生變化,用戶看到的還是添加到購物車時的價格痕惋。
現(xiàn)在你需要創(chuàng)建購物車,并與會話關(guān)聯(lián)起來娃殖。購物車必須這樣工作:
- 需要購物車時值戳,我們檢查是否設(shè)置了自定義會話鍵。如果會話中沒有設(shè)置購物車炉爆,則創(chuàng)建一個新的購物車堕虹,并保存在購物車會話鍵中。
- 對于連續(xù)的請求芬首,我們執(zhí)行相同的檢查赴捞,并從購物車會話鍵中取出購物車的商品。我們從會話中檢索購物車商品郁稍,并從數(shù)據(jù)庫中檢索它們關(guān)聯(lián)的
Product
對象赦政。
編輯項目settings.py
文件,添加以下設(shè)置:
CART_SESSION_ID = 'cart'
我們在用戶會話用這個鍵存儲購物車耀怜。因為每個訪客的Django會話是獨立的恢着,所以我們可以為所有會話使用同一個購物車會話鍵。
讓我們創(chuàng)建一個管理購物車的應(yīng)用封寞。打開終端然评,執(zhí)行以下命令創(chuàng)建一個新應(yīng)用:
python manage.py startapp cart
然后編輯項目的settings.py
文件,把cart
添加到INSTALLED_APPS
:
INSTALLED_APPS = (
# ...
'cart',
)
在cart
應(yīng)用目錄中創(chuàng)建cart.py
文件狈究,并添加以下代碼:
from decimal import Decimal
from django.conf import settings
from shop.models import Product
class Cart:
def __init__(self, request):
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
對象初始化購物車。我們用self.session = request.session
存儲當前會話抖锥,以便在Cart
類的其它方法中可以訪問亿眠。首先,我們用self.session.get(settings.CART_SESSION_ID)
嘗試從當前會話中獲得購物車磅废。如果當前會話中沒有購物車纳像,通過在會話中設(shè)置一個空字典來設(shè)置一個空的購物車。我們希望購物車字典用商品ID做為鍵拯勉,一個帶數(shù)量和價格的字典作為值竟趾。這樣可以保證一個商品不會在購物車中添加多次;同時還可以簡化訪問購物車的數(shù)據(jù)宫峦。
讓我們創(chuàng)建一個方法岔帽,用于向購物車中添加商品,或者更新商品數(shù)量导绷。在Cart
類中添加add()
和save()
方法:
def add(self, product, quantity=1, update_quantity=False):
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 sessions as "modified" to make sure it is saved
self.session.modified = True
add()
方法接收以下參數(shù):
-
product
:在購物車中添加或更新的Product
實例犀勒。 -
quantity
:可選的商品數(shù)量。默認為1. -
update_quantity
:一個布爾值,表示使用給定的數(shù)量更新數(shù)量(True
)贾费,或者把新數(shù)量加到已有的數(shù)量上(False
)钦购。
我們用商品id
作為購物車內(nèi)容字典的鍵。因為Django使用JSON序列號會話數(shù)據(jù)褂萧,而JSON只允許字符串類型的鍵名押桃,所以我們把商品id
轉(zhuǎn)換為字符串。商品id
是鍵箱玷,保存的值是帶商品quantity
和price
的字典怨规。為了序列號陌宿,我們把商品價格轉(zhuǎn)換為字符串锡足。最后,我們調(diào)用save()
方法在會話中保存購物車壳坪。
save()
方法在會話中保存購物車的所有修改舶得,并使用session.modified = True
標記會話已修改。這告訴Django爽蝴,會話已經(jīng)修改沐批,需要保存。
我們還需要一個方法從購物車中移除商品蝎亚。在Cart
類中添加以下方法:
def remove(self, product):
product_id = str(product.id)
if product_id in self.cart:
del self.cart[product_id]
self.save()
remove()
方法從購物車字典中移除指定商品九孩,并調(diào)用save()
方法更新會話中的購物車。
我們將需要迭代購物車中的商品发框,并訪問關(guān)聯(lián)的Product
實例躺彬。因為需要在類中定義__iter__()
方法。在Cart
類中添加以下方法:
def __iter__(self):
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
轉(zhuǎn)換回Decimal
類型,并為每一項添加total_price
屬性『現(xiàn)在我們可以在購物車中方便的迭代商品缔刹。
我們還需要返回購物車中商品總數(shù)量。當在一個對象上調(diào)用len()
函數(shù)時劣针,Python會調(diào)用__len__()
方法返回對象的長度校镐。我們定義一個__len__()
方法,返回購物車中所有商品的總數(shù)量酿秸。在Cart
類中添加__len__()
方法:
def __len__(self):
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())
最后,添加一個清空購物車會話的方法:
def clear(self):
del self.session[settings.CART_SESSION_ID]
self.session.modified = True
我們的Cart
類已經(jīng)可以管理購物車了肝箱。
7.2.5 創(chuàng)建購物車視圖
現(xiàn)在我們已經(jīng)創(chuàng)建了Cart
類來管理購物車哄褒,我們需要創(chuàng)建添加,更新和移除購物車商品的視圖煌张。我們需要創(chuàng)建以下視圖:
- 一個添加或更新購物車商品的視圖呐赡,可以處理當前和新的數(shù)量
- 一個從購物車中移除商品的視圖
- 一個顯示購物車商品和總數(shù)的視圖
7.2.5.1 添加商品到購物車
要添加商品到購物車中,我們需要一個用戶可以選擇數(shù)量的表單骏融。在cart
應(yīng)用目錄中創(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之間的數(shù)量档玻。我們使用帶coerce=int
的TypedChoiceField
字段把輸入的值轉(zhuǎn)換為整數(shù)怀泊。 -
update
:允許你指定把數(shù)量累加到購物車中已存在的商品數(shù)量上(False
),還是用給定的數(shù)量更新已存在商品數(shù)量(True
)误趴。我們?yōu)樵撟侄问褂?code>HiddenInput組件霹琼,因為我們不想讓用戶看見它。
讓我們創(chuàng)建向購物車添加商品的視圖凉当。編輯cart
應(yīng)用的views.py
文件枣申,并添加以下代碼:
from django.shortcuts import render, redirect, get_object_or_404
from django.views.decorators.http import require_POST
from shop.models import Product
from .cart import Cart
from .forms import CartAddProductForm
@require_POST
def cart_add(request, product_id):
cart = Cart(request)
product = get_object_or_404(Product, id=product_id)
form = CartAddProductForm(request.POST)
if form.is_valid():
cd = form.cleaned_data
cart.add(product=product, quantity=cd['quantity'], update_quantity=cd['update'])
return redirect('cart:cart_detail')
這個視圖用于向購物車中添加商品或者更新已有商品的數(shù)量。因為這個視圖會修改數(shù)據(jù)看杭,所以我們只允許POST請求忠藤。視圖接收商品ID作為參數(shù)。我們用給定的商品ID檢索Product
實例楼雹,并驗證CartAddProductForm
模孩。如果表單有效,則添加或更新購物車中的商品烘豹。該視圖重定向到cart_detail
URL瓜贾,它會顯示購物車中的內(nèi)容。之后我們會創(chuàng)建cart_detail
視圖携悯。
我們還需要一個從購物車中移除商品的視圖祭芦。在cart
應(yīng)用的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作為參數(shù)。我們用給定的商品ID檢索Product
實例憔鬼,并從購物車中移除該商品龟劲。接著我們重定向到cart_detail
URL。
最后轴或,我們需要一個顯示購物車和其中的商品的視圖昌跌。在views.py
文件中添加以下代碼:
def cart_detail(request):
cart = Cart(request)
return render(request, 'cart/detail.html', {'cart': cart})
cart_detail
視圖獲得當前購物車,并顯示它照雁。
我們已經(jīng)創(chuàng)建了以下視圖:向購物車中添加商品蚕愤,更新數(shù)量,從購物車中移除商品,已經(jīng)顯示購物車萍诱。讓我們?yōu)檫@些視圖添加URL悬嗓。在cart
應(yīng)用目錄中創(chuàng)建urls.py
文件,并添加以下URL模式:
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
的URL模式:
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^cart/', include('cart.urls', namespace='cart')),
url(r'^', include('shop.urls', namespace='shop')),
]
確保在shop.urls
模式之前引入這個URL模式,因為它比前者更有限定性籍凝。
7.2.5.2 構(gòu)建顯示購物車的模板
cart_add
和cart_remove
視圖不需要渲染任何模板周瞎,但是我們需要為cart_detail
視圖創(chuàng)建顯示購物車和總數(shù)量的模板。
在cart
應(yīng)用目錄中創(chuàng)建以下文件結(jié)構(gòu):
templates/
cart/
detail.html
編輯cart/detail.html
目錄饵蒂,并添加以下代碼:
{% extends "shop/base.html" %}
{% load static %}
{% block title %}
Your shopping cart
{% endblock title %}
{% 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="{{ prouct.get_absolute_url }}">

</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 content %}
這個模板用于顯示購物車的內(nèi)容声诸。它包括一個當前購物車中商品的表格。用戶通過提交表單到cart_add
視圖苹享,來修改選中商品的數(shù)量双絮。我們?yōu)槊總€商品提供了Remove
鏈接,用戶可以從購物車移除商品得问。
7.2.5.3 添加商品到購物車
現(xiàn)在我們需要在商品詳情頁面添加Add to cart
按鈕。編輯shop
應(yīng)用的views.py
文件软免,修改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
應(yīng)用的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
命令啟動開發(fā)服務(wù)器漓骚。在瀏覽器中打開127.0.0.1/8000/
,然后導(dǎo)航到商品詳情頁面榛泛。它現(xiàn)在包括一個選擇數(shù)量的表單蝌蹂,如下圖所示:
選擇數(shù)量,然后點擊Add to cart
按鈕曹锨。表單通過POST提交到cart_add
視圖孤个。該視圖把商品添加到會話中的購物車,包括當前價格和選擇的數(shù)量沛简。然后重定義到購物車詳情頁面齐鲤,如下圖所示:
7.2.5.4 在購物車中更新商品數(shù)量
當用戶查看購物車時,他們可能希望在下單前修改商品數(shù)量椒楣。我們接下來實現(xiàn)在購物車詳情頁面修改數(shù)量给郊。
編輯cart
應(yīng)用的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
實例捧灰,允許用戶修改商品數(shù)量淆九。我們用當前商品數(shù)量初始化表單,并設(shè)置update
字段為True
。因此炭庙,當我們把表單提交到cart_add
視圖時跪另,會用新數(shù)量了代替當前數(shù)量。
現(xiàn)在編輯cart
應(yīng)用的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
按鈕嘲驾,測試一下新功能。
7.2.6 為當前購物車創(chuàng)建上下文處理器
你可能已經(jīng)注意到了迹卢,我們的網(wǎng)站頭部還是顯示Your cart is emtpy
辽故。當我們開始向購物車中添加商品,我們將看到它替換成購物車中商品的總數(shù)量和總價錢腐碱。因為這是需要在所有頁面顯示誊垢,所以我們將創(chuàng)建一個上下文處理器(context processor),將當前購物車包含在請求上下文中症见,而不管已經(jīng)處理的視圖喂走。
7.2.6.1 上下文處理器
上下文處理器是一個Python函數(shù),它將request
對象作為參數(shù)谋作,并返回一個添加到請求上下文中的字典芋肠。當你需要讓某些東西在所有模板都可用時,它會派上用場遵蚜。
默認情況下帖池,當你使用startproject
命令創(chuàng)建新項目時,項目中會包括以下模板上下文處理器吭净,它們位于TEMPLATES
設(shè)置的context_processors
選項中:
-
django.template.context_processors.debug
:在上下文中設(shè)置debug
布爾值和sql_queries
變量睡汹,表示請求中執(zhí)行的SQL查詢列表 -
django.template.context_processors.request
:在上下文中設(shè)置request
變量 -
django.contrib.auth.context_processors.auth
:在請求中設(shè)置user
變量 -
django.contrib.messages.context_processors.messages
:在上下文中設(shè)置message
變量,其中包括所有已經(jīng)用消息框架發(fā)送的消息寂殉。
Django還啟用了django.template.context_processors.csrf
來避免跨站點請求偽造攻擊囚巴。這個上下文處理器不在設(shè)置中,但它總是啟用的不撑,并且為了安全不能關(guān)閉文兢。
你可以在這里查看所有內(nèi)置的上下文處理器列表。
7.2.6.2 在請求上下文中設(shè)置購物車
讓我們創(chuàng)建一個上下文處理器焕檬,把當前購物車添加到模板的請求上下文中姆坚。我們可以在所有模板中訪問購物車。
在cart
應(yīng)用目錄中創(chuàng)建context_processors.py
文件实愚。上下文處理器可以位于代碼的任何地方兼呵,但是在這里創(chuàng)建他們將保持代碼組織良好兔辅。在文件中添加以下代碼:
from .cart import Cart
def cart(request):
return {'cart': Cart(request)}
正如你所看到的,上下文處理器是一個函數(shù)击喂,它將request
對象作為參數(shù)维苔,并返回一個對象的字典,這些對象可用于所有使用RequestContext
渲染的模板懂昂。在我們的上下文處理器中介时,我們用request
對象實例化購物車,模板可以通過cart
變量名訪問它凌彬。
編輯項目的settings.py
文件沸柔,在TEMPLATES
設(shè)置的context_processors
選項中添加cart.context_processors.cart
,如下所示:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'cart.context_processors.cart',
],
},
},
]
每次使用RequestContext
渲染模板時铲敛,會執(zhí)行你的上下文處理器褐澎。cart
變量會設(shè)置在模板的上下文中。
上下文處理器會在所有使用
RequestContext
的請求中執(zhí)行伐蒋。如果你想訪問數(shù)據(jù)庫的話工三,可能希望創(chuàng)建一個自定義模板標簽來代替上下文處理器。
現(xiàn)在編輯shop
應(yīng)用的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
重啟開發(fā)服務(wù)器俭正。在瀏覽器中打開http://127.0.0.1:8000/
,并添加一些商品到購物車中型型。在網(wǎng)站頭部段审,你會看到當前購物車總數(shù)量和總價錢,如下所示:
7.3 注冊用戶訂單
當購物車結(jié)賬后闹蒜,你需要在數(shù)據(jù)庫中保存訂單。訂單包括用戶信息和他們購買的商品抑淫。
使用以下命令創(chuàng)建一個新應(yīng)用绷落,來管理用戶訂單:
python manage.py startapp orders
編輯項目的settings.py
文件,在INSTALLED_APPS
設(shè)置中添加orders
:
INSTALLED_APPS = [
# ...
'orders',
]
你已經(jīng)激活了新應(yīng)用始苇。
7.3.1 創(chuàng)建訂單模型
你需要創(chuàng)建一個模型存儲訂單詳情砌烁,以及一個模型存儲購買的商品,包括價格和數(shù)量催式。編輯orders
應(yīng)用的models.py
文件函喉,添加以下代碼:
from django.db import models
from shop.models import Product
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()
返回商品價錢。
運行以下命令坷襟,為orders
應(yīng)用創(chuàng)建初始數(shù)據(jù)庫遷移:
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ù)庫中奸柬。
7.3.2 在管理站點引入訂單模型
讓我們在管理站點添加訂單模型。編輯orders
應(yīng)用的admin.py
文件婴程,添加以下代碼:
from django.contrib import admin
from .models import Order, OrderItem
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)
我們?yōu)?code>OrderItem模型使用ModeInline
廓奕,把它作為內(nèi)聯(lián)模型引入OrderAdmin
類。內(nèi)聯(lián)可以包含一個模型档叔,與父模型在同一個編輯頁面顯示桌粉。
使用python manage.py runserver
命令啟動開發(fā)服務(wù)器,然后在瀏覽器中打開http:127.0.0.1/8000/admin/order/add/
蹲蒲。你會看到以下界面:
7.3.3 創(chuàng)建用戶訂單
當用戶最終下單時番甩,我們需要使用剛創(chuàng)建的訂單模型來保存購物車中的商品。創(chuàng)建一個新訂單的工作流程是這樣的:
- 向用戶顯示一個填寫數(shù)據(jù)的訂單表單届搁。
- 用用戶輸入的數(shù)據(jù)創(chuàng)建一個新的
Order
實例缘薛,然后為購物車中的每件商品創(chuàng)建關(guān)聯(lián)的OrderItem
實例。 - 清空購物車中所有內(nèi)容卡睦,然后重定向到成功頁面宴胧。
首先,我們需要一個輸入訂單詳情的表單表锻。在orders
應(yīng)用目錄中創(chuàng)建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']
這是我們用于創(chuàng)建新Order
對象的表單。現(xiàn)在我們需要一個視圖處理表單和創(chuàng)建新表單瞬逊。編輯orders
應(yīng)用的views.py
文件显歧,并添加以下代碼:
from django.shortcuts import render
from .models import OrderItem
from .forms import OrderCreateForm
from cart.cart import Cart
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)
從會話中獲得當前購物車确镊。根據(jù)請求的方法士骤,我們執(zhí)行以下任務(wù):
-
GET
請求:實例化OrderCreateForm
表單,并渲染orders/order/create.html
模板蕾域。 -
POST
請求:驗證提交的數(shù)據(jù)拷肌。如果數(shù)據(jù)有效,則使用order = form.save()
創(chuàng)建一個新的Order
實例旨巷。然后我們會將它保存到數(shù)據(jù)庫中巨缘,并存儲在order
變量中。創(chuàng)建order
之后采呐,我們會迭代購物車中的商品若锁,并為每個商品創(chuàng)建OrderItem
。最后懈万,我們會清空購物車的內(nèi)容拴清。
現(xiàn)在靶病,在orders
應(yīng)用目錄中創(chuàng)建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
模式之前:
url(r'^orders/', include('orders.urls', namespace='orders')),
編輯cart
應(yīng)用的cart/detail.html
模板煤辨,找到這行代碼:
<a href="#" class="button">Checkout</a>
把這樣代碼替換為以下代碼:
<a href="{% url "orders:order_create" %}" class="button">Checkout</a>
現(xiàn)在用戶可以從購物車詳情頁面導(dǎo)航到訂單表單。我們還需要為下單定義模板木张。在orders
應(yīng)用目錄中創(chuàng)建以下文件結(jié)構(gòu):
templates/
orders/
order/
create.html
created.html
編輯orders/order/create.html
模板众辨,并添加以下代碼:
{% extends "shop/base.html" %}
{% block title %}
Checkout
{% endblock title %}
{% 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 content %}
這個模板顯示購物車中的商品,包括總數(shù)量和下單的表單舷礼。
編輯orders/order/created.html
模板鹃彻,并添加以下代碼:
{% extends "shop/base.html" %}
{% block title %}
Thank you
{% endblock title %}
{% block content %}
<h1>Thank you</h1>
<p>Your order has been successfully completed.
Your order number is <stong>{{ order.id }}</stong>
</p>
{% endblock content %}
成功創(chuàng)建訂單后,我們渲染這個模板妻献。啟動開發(fā)服務(wù)器蛛株,并在瀏覽器中打開http://127.0.0.1:8000/
。在購物車中添加一些商品育拨,然后跳轉(zhuǎn)到結(jié)賬界面谨履。如下圖所示:
用有效的數(shù)據(jù)填寫表單,然后點擊Place order
按鈕熬丧。訂單會被創(chuàng)建笋粟,你將看到成功頁面,如下圖所示:
7.4 使用Celery啟動異步任務(wù)
你在視圖中執(zhí)行的所有操作都會影響響應(yīng)時間析蝴。在很多場景中害捕,你可能希望盡快給用戶返回響應(yīng),并讓服務(wù)器執(zhí)行一些異步處理闷畸。對于費時處理吨艇,或者失敗后可能需要重試策略的處理尤其重要。例如腾啥,一個視頻分享平臺允許用戶上傳視頻,但轉(zhuǎn)碼上傳的視頻需要很長的時間冯吓。網(wǎng)站可能給用戶返回一個響應(yīng)倘待,告訴用戶馬上開始轉(zhuǎn)碼,然后開始異步轉(zhuǎn)碼组贺。另一個例子是給用戶發(fā)送郵件凸舵。如果網(wǎng)站在視圖中發(fā)送郵件通知,SMTP連接可能失敗失尖,或者減慢響應(yīng)時間啊奄。啟動異步任務(wù)避免阻塞操作是必不可少的渐苏。
Celery是一個可以處理大量消息的分布式任務(wù)隊列。它既可以實時處理菇夸,也支持任務(wù)調(diào)度琼富。使用Celery不僅可以很容易的創(chuàng)建異步任務(wù),還可以盡快執(zhí)行任務(wù)庄新,但也可以在一個指定時間執(zhí)行任務(wù)鞠眉。
你可以在這里查看Celery文檔。
7.4.1 安裝Celery
讓我們安裝Celery择诈,并在項目中集成它械蹋。使用以下pip
命令安裝Celery:
pip install celery
Celery必須有一個消息代理(message broker)處理外部請求。代理負責發(fā)送消息給Celery的worker
羞芍,worker
收到消息后處理任務(wù)哗戈。讓我們安裝一個消息代理。
7.4.2 安裝RabbitMQ
Celery有幾個消息代理可供選擇荷科,包括鍵值對存儲(比如Redis)唯咬,或者一個實際的消息系統(tǒng)(比如RabbitMQ)。我們將用RabbitMQ配置Celery步做,因為它是Celery的推薦消息worker副渴。
如果你使用的是Linux,可以在終端執(zhí)行以下命令安裝RabbitMQ:
apt-get install rabbitmq
如果你需要在Max OS X或者Windows上安裝RabbitMQ全度,你可以在這里找到獨立的版本煮剧。
安裝后,在終端執(zhí)行以下命令啟動RabbitMQ:
rabbitmq-server
你會看到以這一行結(jié)尾的輸出:
Starting broker... completed with 10 plugins.
7.4.3 在項目中添加Celery
你需要為Celery實例提供一個配置将鸵。在myshop
中創(chuàng)建celery.py
文件勉盅,該文件會包括項目的Celery配置,并添加以下代碼:
import os
from celery import Celery
from django.conf import settings
# 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)镃elery命令行程序設(shè)置DJANGO_SETINGS_MODULE
變量酪呻。然后用app = Celery('myshop.)
創(chuàng)建了一個應(yīng)用實例咐低。我們用config_from_object()
方法從項目設(shè)置中加載所有自定義設(shè)置。最后我們告訴Celery宰闰,為INSTALLED_APPS
設(shè)置中列出的應(yīng)用自動查找異步任務(wù)。Celery會在每個應(yīng)用目錄中查找tasks.py
文件簿透,并加載其中定義的異步任務(wù)移袍。
你需要在項目的__init__.py
文件中導(dǎo)入celery
模塊,確保Django啟動時會加載Celery老充。編輯myshop/__init__.py
文件葡盗,并添加以下代碼:
from .celery import app as celery_app
現(xiàn)在你可以開始為應(yīng)用編寫異步任務(wù)了。
CELERY_ALWAYS_EAGER
設(shè)置允許你以同步方式在本地執(zhí)行任務(wù)啡浊,而不是將其發(fā)送到隊列觅够。這對于運行單元測試胶背,或者在不運行Celery的情況下,運行本地環(huán)境中的項目時非常有用喘先。
7.4.4 在應(yīng)用中添加異步任務(wù)
當用戶下單后钳吟,我們將創(chuàng)建一個異步任務(wù),給用戶發(fā)送一封郵件通知苹祟。
一般的做法是在應(yīng)用目錄的tasks
模塊中包括應(yīng)用的異步任務(wù)砸抛。在orders
應(yīng)用目錄中創(chuàng)建tasks.py
文件。Celery會在這里查找異步任務(wù)树枫。在其中添加以下代碼:
from celery import task
from django.core.mail import send_mail
from .models import Order
@task
def order_created(order_id):
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_created
任務(wù)直焙。正如你所看到的,Celery任務(wù)就是一個用task
裝飾的Python函數(shù)砂轻。我們的task
函數(shù)接收order_id
作為參數(shù)奔誓。推薦只傳遞ID給任務(wù)函數(shù),并在任務(wù)執(zhí)行時查詢對象搔涝。我們用Django提供的send_mail()
函數(shù)厨喂,當用戶下單后發(fā)送郵件通知。如果你不想配置郵件選項庄呈,你可以在項目的settings.py
文件中添加以下設(shè)置蜕煌,讓Django在控制臺輸出郵件:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
異步任務(wù)不僅可以用于費時的操作,還可以用于可能失敗的操作诬留,這些操作不會執(zhí)行很長時間斜纪,但它們可能會連接失敗,或者需要重試策略文兑。
現(xiàn)在我們需要在order_create
視圖中添加任務(wù)盒刚。打開orders
應(yīng)用的views.py
文件,并導(dǎo)入任務(wù):
from .tasks import order_created
然后在清空購物車之后調(diào)用order_created
異步任務(wù):
# clear the cart
cart.clear()
# launch asynchronous task
order_created.delay(order.id)
我們調(diào)用任務(wù)的delay()
方法異步執(zhí)行任務(wù)绿贞。任務(wù)會被添加到隊列中因块,worker
會盡快執(zhí)行。
打開另一個終端籍铁,并使用以下命令啟動Celery的worker
:
celery -A myshop worker -l info
譯者注:必須在
myshop
項目目錄下執(zhí)行上面的命令涡上。
現(xiàn)在Celery的worker
已經(jīng)運行,準備好處理任務(wù)了拒名。確保Django開發(fā)服務(wù)器也在運行吓懈。在瀏覽器中打開http://127.0.0.1/8000/
,添加一些商品到購物車中靡狞,然后完成訂單。在終端隔嫡,你已經(jīng)啟動了Celery worker
甸怕,你會看到類似這樣的輸出:
[2017-05-11 06:40:27,416: INFO/MainProcess] Received task: orders.tasks.order_created[4d6f667b-7cc7-4310-82fc-8323810fae54]
[2017-05-11 06:40:27,825: INFO/PoolWorker-3] Task orders.tasks.order_created[4d6f667b-7cc7-4310-82fc-8323810fae54] succeeded in 0.12212000600266038s: 1
任務(wù)已經(jīng)執(zhí)行甘穿,你會收到一封訂單的郵件通知。
7.4.5 監(jiān)控Celery
你可能希望監(jiān)控已經(jīng)執(zhí)行的異步任務(wù)梢杭。Flower是一個基于網(wǎng)頁的監(jiān)控Celery工具温兼。你可以使用pip install flower
安裝Flower。
安裝后武契,你可以在項目目錄下執(zhí)行以下命令啟動Flower:
celery -A myshop flower
在瀏覽器中打開http://127.0.0.1:5555/dashboard
募判。你會看到活動的Celery worker
和異步任務(wù)統(tǒng)計:
你可以在這里查看Flower的文檔。
7.5 總結(jié)
在這章中咒唆,你創(chuàng)建了一個基礎(chǔ)的在線商店應(yīng)用届垫。你創(chuàng)建了商品目錄,并用會話構(gòu)建了購物車全释。你實現(xiàn)了自定義上下文處理器装处,讓模板可以訪問購物車,并創(chuàng)建了下單的表單浸船。你還學(xué)習(xí)了如何使用Celery啟動異步任務(wù)妄迁。
在下一章中,你會學(xué)習(xí)在商店中集成支付網(wǎng)關(guān)(payment gateway)李命,在管理站點添加用戶操作登淘,導(dǎo)出CVS格式數(shù)據(jù),以及動態(tài)生成PDF文件封字。