書籍出處:https://www.packtpub.com/web-development/django-example
原作者:Antonio Melé
(譯者@ucag注:咳咳,第七章終於來了。其實在一月份就翻譯完了😂😂但是后來我回老家了,就沒發出來。各位久等了~)
(譯者@夜夜月注:真羡慕有寒假和暑假的人- -願你們的寒暑假作業越多越好,粗略的校對了下,精校版本請大家繼續等待)
第七章
建立一個在線商店
在上一章,你創建了一個用戶跟蹤系統和建立了一個用戶活躍流。你也學習了 Django 信號是如何工作的,並且把 Redis 融合進了項目中來為圖像視圖計數。在這一章中,你將學會如何建立一個最基本的在線商店。你將會為你的產品創建目錄和使用 Django sessions 實現一個購物車。你也將學習怎樣定制上下文處理器( context processors )以及用 Celery 來激活動態任務。
在這一章中,你將學會:
- 創建一個產品目錄
- 使用 Django sessions 建立購物車
- 管理顧客的訂單
- 用 Celery 發送異步通知
創建一個在線商店項目(project)
我們將從新建一個在線商店項目開始。我們的用戶可以瀏覽產品目錄並且可以向購物車中添加商品。最后,他們將清點購物車然后下單。這一章涵蓋了在線商店的以下幾個功能:
- 創建產品目錄模型(模型),將它們添加到管理站點,創建基本的視圖(view)來展示目錄
- 使用 Django sessions 建立一個購物車系統,使用戶可以在瀏覽網站的過程中保存他們選中的商品
- 創建下單表單和功能
- 發送一封異步的確認郵件在用戶下單的時候
首先,用以下命令來為你的新項目創建一個虛擬環境,然后激活它:
mkdir env
virtualenv env/myshop
source env/myshop/bin/activate
用以下命令在你的虛擬環境中安裝 Django :
pip install django==1.8.6
創建一個叫做 myshop
的新項目,再創建一個叫做 shop
的應用,命令如下:
django-admin startproject myshop
cd myshop
django-admin startapp shop
編輯你項目中的 settings.py
文件,像下面這樣將你的應用添加到 INSTALLED_APPS
中:
INSTALLED_APPS = [
# ...
'shop',
]
現在你的應用已經在項目中激活。接下來讓我們為產品目錄定義模型(models)。
創建產品目錄模型(models)
我們商店中的目錄將會由不同分類的產品組成。每一個產品會有一個名字,一段可選的描述,一張可選的圖片,價格,以及庫存。 編輯位於shop
應用中的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
模型(models)。Category
模型(models)由一個 name
字段和一個唯一的 slug
字段構成。Product
模型(model):
category
: 這是一個鏈接向Category
的ForeignKey
。這是個多對一(many-to-one)關系。一個產品可以屬於一個分類,一個分類也可包含多個產品。name
: 這是產品的名字slug
: 用來為這個產品建立 URL 的 slugimage
: 可選的產品圖片description
: 可選的產品描述price
: 這是個DecimalField
(譯者@ucag注:十進制字段)。這個字段使用 Python 的decimal.Decimal
元類來保存一個固定精度的十進制數。max_digits
屬性可用於設定數字的最大值,decimal_places
屬性用於設置小數位數。stock
: 這是個PositiveIntegerField
(譯者@ucag注:正整數字段) 來保存這個產品的庫存。available
: 這個布爾值用於展示產品是否可供購買。這使得我們可在目錄中使產品廢棄或生效。created
: 當對象被創建時這個字段被保存。update
: 當對象最后一次被更新時這個字段被保存。
對於 price
字段,我們使用 DecimalField
而不是 FloatField
來避免精度問題。
我們總是使用
DecimalField
來保存貨幣值。FloatField
在內部使用 Python 的float
類型。反之,DecimalField
使用的是 Python 中的Decimal
類型,使用Decimal
類型可以避免精度問題。
在 Product
模型(model)中的 Meta
類中,我們使用 index_together
元選項來指定 id
和 slug
字段的共同索引。我們定義這個索引,因為我們准備使用這兩個字段來查詢產品,兩個字段被索引在一起來提高使用雙字段查詢的效率。
由於我們會在模型(models)中和圖片打交道,打開 shell ,用下面的命令安裝 Pillow :
pip isntall Pillow==2.9.0
現在,運行下面的命令來為你的項目創建初始遷移:
python manage.py makemigrations
你將會看到以下輸出:
Migrations for 'shop':
0001_initial.py:
- Create model Category
- Create model Product
- Alter index_together for product (1 constraint(s))
用下面的命令來同步你的數據庫:
python mange.py migrate
你將會看到包含下面這一行的輸出:
Applying shop.0001_initial... OK
現在數據庫已經和你的模型(models)同步了。
注冊目錄模型(models)到管理站點
讓我們把模型(models)注冊到管理站點,這樣我們就可以輕松管理產品和產品分類了。編輯 shop
應用的 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
屬性來指定那些要使用其他字段來自動賦值的字段。正如你以前看到的那樣,這樣做可以很方便的生成 slugs 。我們在 ProductAdmin
類中使用 list_editable
屬性來設置可被編輯的字段,並且這些字段都在管理站點的列表頁被列出。這樣可以讓你一次編輯多行。任何在 list_editable
的字段也必須在 list_display
中,因為只有這樣被展示的字段才可以被編輯。
現在,使用如下命令為你的站點創建一個超級用戶:
python manage.py createsuperuser
使用命令 python manage.py runserver
啟動開發服務器。 訪問 http://127.0.0.1:8000/admin/shop/product/add ,登錄你剛才創建的超級用戶。在管理站點的交互界面添加一個新的品種和產品。 product 的更改頁面如下所示:
創建目錄視圖(views)
為了展示產品目錄, 我們需要創建一個視圖(view)來列出所有產品或者是給出的篩選后的產品。編輯 shop
應用中的 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
的查詢集來檢索可用的產品。我們使用一個可選參數 category_slug
通過所給產品類別來有選擇性的篩選產品。
我們也需要一個視圖來檢索和展示單一的產品。把下面的代碼添加進去:
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
視圖(view)接收 id
和 slug
參數來檢索 Product
實例。我們可以只用 ID 就可以得到這個實例,因為它是一個獨一無二的屬性。盡管,我們在 URL 中引入了 slug 來建立搜索引擎友好(SEO-friendly)的 URL。
在創建了產品列表和明細視圖(views)之后,我們該為它們定義 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
視圖(view)定義了兩個不同的 URL 模式。 命名為product_list
的模式不帶參數調用 product_list
視圖(view);命名為 product_list_bu_category
的模式向視圖(view)函數傳遞一個 category_slug
參數,以便通過給定的產品種類來篩選產品。我們為 product_detail
視圖(view)添加的模式傳遞了 id
和 slug
參數來檢索特定的產品。
像這樣編輯 myshop
項目中的 urls.py
文件:
from django.conf.urls import url, include
from django.contrib import admin
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^', include('shop.urls', namespace='shop')),
]
在項目的主要 URL 模式中,我們引入了 shop
應用的 URL 模式,並指定了一個命名空間,叫做 shop
。
現在,編輯 shop
應用中的 models.py
文件,導入 reverse()
函數,然后給 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])
正如你已經知道的那樣, get_absolute_url()
是檢索一個對象的 URL 約定俗成的方法。這里,我們將使用我們剛剛在 urls.py
文件中定義的 URL 模式。
創建目錄模板(templates)
現在,我們需要為產品列表和明細視圖創建模板(templates)。在 shop
應用的路徑下創建如下路徑和文件:
templates/
shop/
base.html
product/
list.html
detail.html
我們需要定義一個基礎模板(template),然后在產品列表和明細模板(templates)中繼承它。 編輯 shop/base.html
模板(template),添加如下代碼:
{% 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>
這就是我們將為我們的商店應用使用的基礎模板(template)。為了引入模板使用的 CSS 和圖像,你需要復制這一章示例代碼中的靜態文件,位於 shop
應用中的 static/
路徑下。把它們復制到你的項目中相同的地方。
編輯 shop/product/list.html
模板(template),然后添加如下代碼:
{% 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 }}">

</a>
<a href="{{ product.get_absolute_url }}">{{ product.name }}</a><br>
${{ product.price }}
</div>
{% endfor %}
</div>
{% endblock %}
這是產品列表模板(template)。它繼承了 shop/base.html
並且使用了 categories
上下文變量來展示所有在側邊欄里的產品種類,以及 products
上下文變量來展示當前頁面的產品。相同的模板用於展示所有的可用的產品以及經目錄分類篩選后的產品。由於Product
模型的 image
字段可以為空,我們需要為沒有圖片的產品提供一個默認圖像。這個圖片位於我們的靜態文件路徑下,相對路徑為 img/no_image.png
。
因為我們在使用 ImageField
來保存產品圖片,我們需要開發服務器來服務上傳圖片文件。編輯 myshop
項目的 settings.py
文件,添加以下設置:
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
urlpatterns = [
# ...
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)
記住,我們僅僅在開發中像這樣提供靜態文件服務。在生產環境下,你不應該用 Django 來服務靜態文件。
使用管理站點為你的商店添加幾個產品,然后訪問 http://127.0.0.1:8000/ 。你可以看到如下的產品列表頁:
如果你用管理站點創建了幾個產品,並且沒有上傳任何圖片的話,就會顯示默認的 no_img.png
。
讓我們編輯產品明細模板(template)。 編輯 shop/product/detail.html
模板(template),添加以下代碼:
{% extends "shop/base.html" %}
{% load static %}
{% block title %}
{% if category %}{{ category.title }}{% else %}Products{% endif %}
{% endblock %}
{% 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 %}
我們可以調用相關聯的產品類別的 get_absolute_url()
方法來展示有效的屬於同一目錄的產品。現在,訪問 http://127.0.0.1:8000 ,點擊任意產品,查看產品明細頁面。看起來像這樣:
創建購物車
在創建了產品目錄之后,下一步我們要創建一個購物車系統,這個購物車系統可以讓用戶選中他們想買的商品。購物車允許用戶在最終下單之前選中他們想要的物品並且可以在用戶瀏覽網站時暫時保存它們。購物車存在於會話中,所以購物車中的物品會在用戶訪問期間被保存。
我們將會使用 Django 的會話框架(seesion framework)來保存購物車。在購物車最終被完成或用戶下單之前,購物車將會保存在會話中。我們需要為購物車和購物車里的商品創建額外的 Django 模型(models)。
使用 Django 會話
Django 提供了一個會話框架,這個框架支持匿名會話和用戶會話。會話框架允許你為任意訪問對象保存任何數據。會話數據保存在服務端,並且如果你使用基於 cookies 的會話引擎的話, cookies 會包含 session ID 。會話中間件控制發送和接收 cookies 。默認的會話引擎把會話保存在數據庫中,但是正如你一會兒會看到的那樣,你也可以選擇不同的會話引擎。為了使用會話,你必須確認你項目的 MIDDLEWARE_CLASSES
設置中包含了 django.contrib.sessions.middleware.SessionMiddleware
。這個中間件負責控制會話,並且是在你使用命令startproject
創建項目時被默認添加的。
會話中間件使當前會話在 request
對象中可用。你可以用 request.seesion
連接當前會話,它的使用方式和 Python 的字典相似。會話字典接收任何默認的可被序列化為 JSON 的 Python 對象。你可以在會話中像這樣設置變量:
request.session['foo'] = 'bar'
檢索會話中的鍵:
request.session.get('foo')
刪除會話中已有鍵:
del request.session['foo']
正如你所見,我們像使用 Python 字典一樣使用 request.session
。
當用戶登錄時,他們的匿名會話將會丟失,然后新的會話將會為認證后的用戶創建。如果你在匿名會話中儲存了在登錄后依然需要被持有的數據,你需要從舊的會話中復制數據到新的會話。
會話設置
你可以使用幾種設置來為你的項目配置會話系統。最重要的部分是 SESSION_ENGINE
.這個設置讓你可以配置會話將會在哪里被儲存。默認地, Django 用 django.contrib.sessions
的 Sessions
模型把會話保存在數據庫中。
Django 提供了以下幾個選擇來保存會話數據:
- Database sessions(數據庫會話):會話數據將會被保存在數據庫中。這是默認的會話引擎。
- File-based sessions(基於文件的會話):會話數據保存在文件系統中。
- Cached sessions(緩存會話):會話數據保存在緩存后端中。你可以使用
CACHES
設置來指定一個緩存后端。在緩存系統中保存會話擁有最好的性能。 - Cached sessions(緩存會話):會話數據儲存於緩存后端。你可以使用
CACHES
設置來制定一個緩存后端。在緩存系統中儲存會話數據會有更好的性能表現。 - Cached database sessions(緩存於數據庫中的會話):會話數據保存於可高速寫入的緩存和數據庫中。只會在緩存中沒有數據時才會從數據庫中讀取數據。
- Cookie-based sessions(基於 cookie 的會話):會話數據儲存於發送向瀏覽器的 cookie 中。
為了得到更好的性能,使用基於緩存的會話引擎( cach-based session engine)吧。 Django 支持 Mercached ,以及 Redis 的第三方緩存后端和其他的緩存系統。
你可以用其他的設置來定制你的會話。這里有一些和會話有關的重要設置:
SESSION_COOKIE_AGE
:cookie 會話保持的時間。以秒為單位。默認值為 1209600 (2 周)。SESSION_COOKIE_DOMAIN
:這是為會話 cookie 使用的域名。把它的值設置為.mydomain.com
來使跨域名 cookie 生效。SESSION_COOKIE_SECURE
:這是一個布爾值。它表示只有在連接為 HTTPS 時 cookie 才會被發送。SESSION_EXPIRE_AT_BROWSER_CLOSE
:這是一個布爾值。它表示會話會在瀏覽器關閉時就過期。SESSION_SAVE_EVERY_REQUEST
:這是一個布爾值。如果為True
,每一次請求的 session 都將會被儲存進數據庫中。 session 的過期時間也會每次刷新。
在這個網站你可以看到所有的 session 設置:https://docs.djangoproject.com/en/1.8/ref/settings/#sessions
會話過期
你可以通過 SESSION_EXPIRE_AT_BROWSER_CLOSE
選擇使用 browser-length 會話或者持久會話。默認的設置是 False
,強制把會話的有效期設置為 SESSION_COOKIE_AGE
的值。如果你把 SESSION_EXPIRE_AT_BROWSER_CLOSE
的值設為 True
,會話將會在用戶關閉瀏覽器時過期,且 SESSION_COOKIE_AGE
將不會對此有任何影響。
你可以使用 request.session
的 set_expiry()
方法來覆寫當前會話的有效期。
在會話中保存購物車
我們需要創建一個能序列化為 JSON 的簡單結構,這樣就可以把購物車中的東西儲存在會話中。購物車必須包含以下數據,每個物品的數據都要包含在其中:
Product
實例的id
- 選擇的產品數量
- 產品的總價格
因為產品的價格可能會變化,我們采取當產品被添加進購物車時同時保存產品價格和產品本身的辦法。這樣做,我們就可以保持用戶在把商品添加進購物車時他們看到的商品價格不變了,即使產品的價格在之后有了變更。
現在,你需要把購物車和會話關聯起來。購物車像下面這樣工作:
- 當需要一個購物車時,我們檢查顧客是否已經設置了一個會話鍵( session key)。如果會話中沒有購物車,我們就創建一個新的購物車,然后把它保存在購物車的會話鍵中。
- 對於連續的請求,我們在會話鍵中執行相同的檢查和獲取購物車內物品的操作。我們在會話中檢索購物車的物品和他們在數據庫中相關聯的
Product
對象。
編輯你的項目中 settings.py
,把以下設置添加進去:
CART_SESSION_ID = 'cart'
添加的這個鍵將會用於我們的會話中來儲存購物車。因為 Django 的會話對於每個訪問者是獨立的(譯者@ucag注:原文為 per-visitor ,沒能想出一個和它對應的中文詞,根據上下文,我就把這個詞翻譯為了一個短語),我們可以在所有的會話中使用相同的會話鍵。
讓我們創建一個應用來管理我們的購物車。打開終端,然后創建一個新的應用,在項目路徑下運行以下命令:
python manage.py startapp cart
然后編輯你添加的項目中的 settings.py
,在 INSTALLED_APPS
中添加 cart
:
INSTALLED_APPS = (
# ...
'shop',
'cart',
)
在 cart
應用路徑內創建一個新的文件,命名為 cart.py
,把以下代碼添加進去:
from decimal import Decimal
from django.conf import settings
from shop.models import Product
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
對象一同初始化。我們使用 self.session = request.session
保存當前會話以便使其對 Cart
類的其他方法可用。首先,我們使用 self.session.get(settings.CART_SESSION_ID)
嘗試從當前會話中獲取購物車。如果當前會話中沒有購物車,我們就在會話中設置一個空字典,這樣就可以在會話中設置一個空的購物車。我們希望我們的購物車字典使用產品 ID 作為鍵,以數量和價格為鍵值對的字典為值。這樣做,我們就能保證一個產品在購物車當中不被重復添加;我們也能簡化獲取任意購物車物品數據的步驟。
讓我們寫一個方法來向購物車當中添加產品或者更新產品的數量。把 save()
和 add()
方法添加進 Cart
類當中:
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
對象quantity
:一個產品數量的可選參數。默認為 1update_quantity
:這是一個布爾值,它表示數量是否需要按照給定的數量參數更新(True
),不然新的數量必須要被加進已存在的數量中(False
)
我們在購物車字典中把產品 id
作為鍵。我們把產品 id
轉換為字符串,因為 Django 使用 JSON 來序列化會話數據,而 JSON 又只接受支字符串的鍵名。產品 id
為鍵,一個有 quantity
和 price
的字典作為值。產品的價格從十進制數轉換為了字符串,這樣才能將它序列化。最后,我們調用 save()
方法把購物車保存到會話中。
save()
方法會把購物車中所有的改動都保存到會話中,然后用 session.modified = True
標記改動了的會話。這是為了告訴 Django 會話已經被改動,需要將它保存起來。
我們也需要一個方法來從購物車當中刪除購物車。把下面的方法添加進 Cart
類當中:
def remove(self, product):
"""
Remove a product from the cart.
"""
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
轉換回十進制數,然后為每個添加一個 total_price
屬性。現在我們就可以很容易的在購物車當中迭代物品了。
我們還需要一個方法來返回購物車中物品的總數量。當 len()
方法在一個對象上執行時,Python 會調用對象的 __len__()
方法來檢索它的長度。我們將會定義一個定制的 __len__()
方法來返回保存在購物車中保存的所有物品數量。把下面這個 __len__()
方法添加進 Cart
類中:
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())
最后,添加一個方法來清空購物車會話:
def clear(self):
# remove cart from session
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 之間選擇產品的數量。我們使用了帶有coerce=int
的TypeChoiceField
字段來把輸入轉換為整數update
:讓你展示數量是否要被加進已當前的產品數量上(False
),否則如果當前數量必須被用給定的數量給更新(True
)。我們為這個字段使用了HiddenInput
控件,因為我們不想把它展示給用戶。
讓我們一個新的視圖來想購物車中添加物品。編輯 cart
應用的 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')
這個視圖是為了想購物車添加新的產品或者更新當前產品的數量。我們使用 require_POST
裝飾器來只響應 POST 請求,因為這個視圖將會變更數據。這個視圖接收產品 ID 作為參數。我們用給定的 ID 來檢索 Product
實例,然后驗證 CartAddProductForm
。如果表單是合法的,我們將在購物車中添加或者更新產品。我們將創建 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_detail
視圖接收產品 ID 作為參數。我們根據給定的產品 ID 檢索相應的 Product
實例,然后將它從購物車中刪除。然后,我們將用戶重定向到 cart_detail
URL。
最后,我們需要一個視圖來展示購物車和其中的物品。講一下代碼添加進 veiws.py
中:
def cart_detail(request):
cart = Cart(request)
return render(request, 'cart/detail.html', {'cart': cart})
cart_detail
視圖獲取當前購物車並展示它。
我們已經創建了視圖來向購物車中添加物品,或從購物車中更新數量,刪除物品,還有展示他們。然我們為這些視圖添加 URL 模式。在 cart
應用中創建一個新的文件,命名為 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
文件,添加以下 URL 模式來引用 cart
URLs:
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^cart/', include('cart.urls', namespace='cart')),
url(r'^', include('shop.urls', namespace='shop')),
]
確保你在 shop.urls
之前引用它,因為它比前者更加有限制性。
創建展示購物車的模板
cart_add
和 cart_remove
視圖沒有渲染任何模板,但是我們需要為 cart_detail
創建模板。
在 cart
應用路徑下創建以下文件結構:
templates/
cart/
detail.html
編輯 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 }}">

</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
表單來改變選中的產品數量。我們通過提供一個 Remove 鏈接來允許用戶從購物車中刪除物品。
向購物車中添加物品
現在,我們需要在產品詳情頁添加一個 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/,導航到產品詳情頁。現在它包含了一個表單來選擇數量在將產品添加進購物車之前。這個頁面看起來像這樣:
選擇一個數量,然后點擊 Add to cart 按鈕。表單將會通過 POST 方法提交到 cart_add
視圖。視圖會把產品添加進當前會話的購物車當中,包括當前產品的價格和選定的數量。然后,用戶將會被重定向到購物車詳情頁,它長得像這個樣子:
在購物車中更新產品數量
當用戶看到購物車時,他們可能想要在下單之前改變產品數量。我們將會允許用戶在詳情頁改變產品數量。
編輯 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/ 。你將會看到一個表單來編輯每個物品的數量,長得像下面這樣:
改變物品的數量,然后點擊 Update 按鈕來測試新的功能。
為當前購物車創建上下文處理器
你可能已經注意到我們在網站的頭部展示了 Your cart is empty 的信息。當我們開始向購物車添加物品時,我們將看到它已經替換為了購物車中物品的總數和總花費。由於這是個展示在整個頁面的東西,我們將創建一個上下文處理器來引用當前請求中的購物車,盡管我們的視圖函數已經處理了它。
上下文處理器
上下文處理器是一個接收 request
對象為參數並返回一個已經添加了請求上下文字典的 Python 函數。他們在你需要讓什么東西在所有模板都可用時遲早會派上用場。
一般的,當你用 startproject
命令創建一個新的項目時,你的項目將會包含下面的模板上下文處理器,他們位於 TEMPLATES
設置中的 context_processors
內:
django.template.context_processors.debug
:在上下文中設置debug
布爾值和sql_queries
變量,來表示在 request 中執行的 SQL 查詢語句表django.template.context_processors.request
:在上下文中設置 request 變量django.contrib.auth.context_processors.auth
:在請求中設置用戶變量django.contrib.messages.context_processors.messages
:在包含所有使用消息框架發送的信息的上下文中設置一個messages
變量
Django 也使用 django.template.context_processors.csrf
來避免跨站請求攻擊。這個上下文處理器不在設置中,但是它總是可用的並且由安全原因不可被關閉。
你可以在這個網站看到所有的內建上下文處理器:https://docs.djangoproject.com/en/1.8/ref/templates/api/#built-in-template-context-processors
把購物車添加進請求上下文中
讓我們創建一個上下文處理器來將當前購物車添加進模板請求上下文中。這樣我們就可以在任意模板中獲取任意購物車了。
在 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
添加進 TEMPLATE
內的 context_processors
選項中。改變后的設置如下:
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
渲染 模板時執行。 cart
變量將會被設置在模板上下文中。
上下文處理器會在所有的使用
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/ ,添加一些產品到購物車里。在網站頭里,你可以看到當前物品總數和總花費,就象這樣:
保存用戶訂單
當購物車已經結賬完畢時,你需要把訂單保存進數據庫中。訂單將要保存客戶信息和他們購買的產品信息。
使用下面的命令創建一個新的應用來管理用戶訂單:
python manage.py startapp orders
編輯項目中的 settings.py
,然后把 orders
添加進 INSTALLED_APPS
中:
INSTALLED_APPS = (
# ...
'orders',
)
現在你已經激活了你的新應用。
創建訂單模型
你需要一個模型來保存訂單的詳細信息,第二個模型用來保存購買的物品,包括物品的價格和數量。編輯 orders
應用的 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
模型包含幾個用戶信息的字段和一個 paid
布爾值字段,這個字段默認值為 False
。待會兒,我們將使用這個字段來區分支付和未支付訂單。我們也定義了一個 get_total_cost()
方法來得到訂單中購買物品的總花費。
OrderItem
模型讓我們可以保存物品,數量和每個物品的支付價格。我們引用 get_cost()
來返回物品的花費。
給 orders
應用下運行首次遷移:
python manage.py makemigrations
你將看到如下輸出:
Migrations for 'orders':
0001_initial.py:
- Create model Order
- Create model OrderItem
運行以下命令來應用新的遷移:
python manage.py migrate
你的訂單模型已經同步到了數據庫中
在管理站點引用訂單模型
讓我們把訂單模型添加到管理站點。編輯 orders
應用的 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)
我們在 OrderItem
使用 ModelInline
來把它引用為 OrderAdmin
類的內聯元素。一個內聯元素允許你在同一編輯頁引用模型,並且將這個模型作為父模型。
用 python manage.py runserver
命令打開開發服務器,訪問 http://127.0.1:8000/admin/orders/order/add/ 。你將會看到如下頁面:
創建顧客訂單
我們需要使用訂單模型來保存在用戶最終下單時在購物車中的物品,創建新的訂單的工作流程如下:
-
- 向用戶展示一個訂單表來讓他們填寫數據
-
- 我們用用戶輸入的數據創建一個新的
Order
實例,然后我們創建每個物品相關聯的OrderItem
實例。
- 我們用用戶輸入的數據創建一個新的
-
- 我們清空購物車,然后把用戶重定向到成功頁面
首先,我們需要一個表單來輸入訂單詳情。在orders
應用路徑內創建一個新的文件,命名為 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 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)
獲取到當前會話中的購物車。基於請求方法,我們將執行以下幾個任務:
- GET 請求:實例化
OrderCreateForm
表單然后渲染模板orders/order/create.html
- 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
模式之前:
url(r'^orders/', include('orders.urls', namespace='orders')),
編輯 cart
應用的 cart/detail.html
模板,找到下面這一行:
<a href="#" class="button">Checkout</a>
替換為:
<a href="{% url "orders:order_create" %}" class="button">
Checkout
</a>
用戶現在可以從購物車詳情頁導航到訂單表了。我們依然需要定義一個下單模板。在 orders
應用路徑下創建如下文件結構:
templates/
orders/
order/
create.html
created.html
編輯 ordrs/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/ ,在購物車當中添加幾個產品進去,然后結賬。
你就會看到下面這個頁面:
用合法的數據填寫表單,然后點擊 Place order 按鈕。訂單就會被創建,然后你將會看到成功頁面:
使用 Celery 執行異步操作
你在視圖執行的每個操作都會影響響應的時間。在很多場景下你可能想要盡快的給用戶返回響應,並且讓服務器異步地執行一些操作。這特別和耗時進程或從屬於失敗的進程時需要重新操作時有着密不可分的關系。比如,一個視頻分享平台允許用戶上傳視頻但是需要相當長的時間來轉碼上傳的視頻。這個網站可能會返回一個響應給用戶,告訴他們轉碼即將開始,然后開始異步轉碼。另一個例子是給用戶發送郵件。如果你的網站發送了通知郵件,SMTP 連接可能會失敗或者減慢響應的速度。執行異步操作來避免阻塞執行就變得必要起來。
Celery 是一個分發隊列,它可以處理大量的信息。它既可以執行實時操作也支持任務調度。使用 Celery 不僅可以讓你很輕松的創建異步任務還可以讓這些任務盡快執行,但是你需要在一個指定的時間調度他們執行。
你可以在這個網站找到 Celery 的官方文檔:http://celery.readthedocs.org/en/latest/
安裝 Celery
讓我們安裝 Celery 然后把它整合進你的項目中。用下面的命令安裝 Celery:
pip install celery==3.1.18
Celery 需要一個消息代理(message broker)來管理請求。這個代理負責向 Celery 的 worker 發送消息,當接收到消息時 worker 就會執行任務。讓我們安裝一個消息代理。
安裝 RabbitMQ
有幾個 Celery 的消息代理可供選擇,包括鍵值對儲存,比如 Redis 或者是實時消息系統,比如 RabbitMQ。我們會用 RabbitMQ 配置 Celery ,因為它是 Celery 推薦的 message worker。
如果你用的是 Linux,你可以用下面這個命令安裝 RabbitMQ :
apt-get install rabbitmg
(譯者@夜夜月注:這是debian系linux的安裝方式)
如果你需要在 Mac OSX 或者 Windows 上安裝 RabbitMQ,你可以在這個網站找到獨立的支持版本:
https://www.rabbitmq.com/download.html
在安裝它之后,使用下面的命令執行 RabbitMQ:
rabbitmg-server
你將會在最后一行看到這樣的輸出:
Starting broker... completed with 10 plugins
RabbitMQ 正在運行了,准備接收消息。
把 Celery 添加進你的項目
你必須為 Celery 實例提供配置。在 myshop
的 settings.py
文件的旁邊創建一個新的文件,命名為 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)
在這段代碼中,我們為 Celery 命令行程序設置了 DJANGO_SETTINGS_MODULE
變量。然后我們用 app=Celery('myshop')
創建了一個實例。我們用 config_from_object()
方法來加載項目設置中任意的定制化配置。最后,我們告訴 Celery 自動查找我們列舉在 INSTALLED_APPS
設置中的異步應用任務。Celery 將在每個應用路徑下查找 task.py
來加載定義在其中的異步任務。
你需要在你項目中的 __init__.py
文件中導入 celery
來確保在 Django 開始的時候就會被加載。編輯 myshop/__init__.py
然后添加以下代碼:
# import celery
from .celery import app as celery_app
現在,你可以為你的項目開始編寫異步任務了。
CELERY_ALWAYS_EAGER
設置允許你在本地用異步的方式執行任務而不是把他們發送向隊列中。這在不運行 Celery 的情況下, 運行單元測試或者是運行在本地環境中的項目是很有用的。
向你的應用中添加異步任務
我們將創建一個異步任務來發送消息郵件來讓用戶知道他們下單了。
約定俗成的一般用法是,在你的應用路徑下的 tasks
模型里引入你應用的異步任務。在 orders
應用內創建一個新的文件,並命名為 task.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_created
任務。如你所見,一個 Celery 任務 只是一個用 task
裝飾的 Python 函數。我們的 task
函數接收一個 order_id
參數。通常推薦的做法是只傳遞 ID 給任務函數然后在任務被執行的時候需找相關的對象,我們使用 Django 提供的 send_mail()
函數來發送一封提示郵件給用戶告訴他們下單了。如果你不想安裝郵件設置,你可以通過一下 settings
.py` 中的設置告訴 Django 把郵件傳給控制台:
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()
方法並異步地執行它。之后任務將會被添加進隊列中,將會盡快被一個 worker 執行。
打開另外一個 shell ,使用以下命令開啟 celery worker :
celery -A myshop worker -1 info
Celery worker 現在已經運行,准備好執行任務了。確保 Django 的開發服務器也在運行當中。訪問 http://127.0.0.1:8000/ ,在購物車中添加一些商品,然后完成一個訂單。在 shell 中,你已經打開過了 Celery worker 所以你可以看到以下的相似輸出:
[2015-09-14 19:43:47,526: INFO/MainProcess] Received task: orders.
tasks.order_created[933e383c-095e-4cbd-b909-70c07e6a2ddf]
[2015-09-14 19:43:50,851: INFO/MainProcess] Task orders.tasks.
order_created[933e383c-095e-4cbd-b909-70c07e6a2ddf] succeeded in
3.318835098994896s: 1
任務已經被執行了,你會接收到一封訂單通知郵件。
監控 Celery
你或許想要監控執行了的異步任務。下面就是一個基於 web 的監控 Celery 的工具。你可以用下面的命令安裝 Flower:
pip install flower
安裝之后,你可以在你的項目路徑下用以下命令啟動 Flower :
celery -A myshop flower
在你的瀏覽器中訪問 http://localhost:555/dashboard ,你可以看到激活了的 Celery worker 和正在執行的異步任務統計:
你可以在這個網站找到 Flower 的文檔:http://flower.readthedocs.org/en/latest/
總結
在這一章中,你創建了一個最基本的商店應用。你創建了產品目錄以及使用會話的購物車。你實現了定制化的上下文處理器來使購物車在你的模板中可用,實現了一個下單表格。你也學到了如何用 Celery 執行異步任務。
在下一章中,你將會學習在你的商店中整合一個支付網關,添加管理站點的定制化動作,以 CSV 的形式導出數據,以及動態的生成 PDF 文件。
(譯者@夜夜月注:接下來就是第八章了,請大家等待,正在進行中= =,不確定啥時候放出來= =,我是懶人)