目錄
Django2實戰示例 第一章 創建博客應用
Django2實戰示例 第二章 增強博客功能
Django2實戰示例 第三章 擴展博客功能
Django2實戰示例 第四章 創建社交網站
Django2實戰示例 第五章 內容分享功能
Django2實戰示例 第六章 追蹤用戶行為
Django2實戰示例 第七章 創建電商網站
Django2實戰示例 第八章 管理支付與訂單
Django2實戰示例 第九章 擴展商店功能
Django2實戰示例 第十章 創建在線教育平台
Django2實戰示例 第十一章 渲染和緩存課程內容
Django2實戰示例 第十二章 創建API
Django2實戰示例 第十三章 上線
第七章 創建電商網站
在上一章里,創建了用戶關注系統和行為流應用,還學習了使用Django的信號功能與使用Redis數據庫存儲圖片瀏覽次數和排名。這一章將學習如何創建一個基礎的電商網站。本章將學習創建商品品類目錄,通過session實現購物車功能。還將學習創建自定義上下文管理器和使用Celery執行異步任務。
本章的要點有:
- 創建商品品類目錄
- 使用session創建購物車
- 管理客戶訂單
- 使用Celery異步向用戶發送郵件通知
1創建電商網站項目
我們要創建一個電商網站項目。用戶能夠瀏覽商品品類目錄,然后將具體商品加入購物車,最后還可以通過購物車生成訂單。本章電商網站的如下功能:
- 創建商品品類模型並加入管理后台,創建視圖展示商品品類
- 創建購物車系統,用戶瀏覽網站的時購物車中一直保存着用戶的商品
- 創建提交訂單的頁面
- 訂單提交成功后異步發送郵件給用戶
打開系統命令行窗口,為新項目配置一個新的虛擬環境並激活:
mkdir env
virtualenv env/myshop
source env/myshop/bin/activate
然后在虛擬環境中安裝Django:
pip install Django==2.0.5
新創建一個項目叫做myshop
,之后創建新應用叫shop
:
django-admin startproject myshop
cd myshop/
django-admin startapp shop
編輯settings.py
文件,激活shop
應用:
INSTALLED_APPS = [
# ...
'shop.apps.ShopConfig',
]
現在應用已經激活,下一步是設計數據模型。
1.1創建商品品類模型
我們的商品品類模型包含一系列商品大類,每個商品大類中包含一系列商品。每一個商品都有一個名稱,可選的描述,可選的圖片,價格和是否可用屬性。編輯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='category', on_delete=models.CASCADE)
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)
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
字段(unique
同時也意味着創建索引)。Product
模型的字段如下:
category
:關聯到Category
模型的外鍵。這是一個多對一關系,一個商品必定屬於一個品類,一個品類包含多個商品。name
:商品名稱。slug
:商品簡稱,用於創建規范化URL。image
:可選的商品圖片。description
:可選的商品圖片。price
:該字段使用了Python的decimal.Decimal
類,用於存儲商品的金額,通過max_digits
設置總位數,decimal_places=2
設置小數位數。availble
:布爾值,表示商品是否可用,可以用於切換該商品是否可以購買。created
:記錄商品對象創建的時間。updated
:記錄商品對象最后更新的時間。
這里需要特別說明的是price
字段,使用DecimalField
,而不是FloatField
,以避免小數尾差。
凡是涉及到金額相關的數值,使用DecimalField
字段。FloatField
的后台使用Python的float
類型,而DecimalField
字段后台使用Python的Decimal
類,可以避免出現浮點數的尾差。
在Product
模型的Meta
類中,使用index_together
設置id
和slug
字段建立聯合索引,這樣在同時使用兩個字段的索引時會提高效率。
由於使用了ImageField
,還需要安裝Pillow
庫:
pip install Pillow==5.1.0
之后執行數據遷移程序,創建數據表。
1.2將模型注冊到管理后台
將我們的模型都添加到管理后台中,編輯shop
應用的admin.py
文件:
from django.contrib import admin
from .models import Category, Product
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'slug']
prepopulated_fields = {'slug': ('name',)}
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'price', 'available', 'created', 'updated']
list_filter = ['available', 'created', 'updated']
list_editable = ['price', 'available']
prepopulated_fields = {'slug': ('name',)}
我們使用了prepopulated_fields
用於讓slug
字段通過name
字段自動生成,在之前的項目中可以看到這么做很簡便。在ProductAdmin
中使用list_editable
設置了可以編輯的字段,這樣可以一次性編輯多行而不用點開每一個對象。注意所有在list_editable
中的字段必須出現在list_display
中。
之后創建超級用戶。打開http://127.0.0.1:8000/admin/shop/product/add/,使用管理后台添加一個新的商品品類和該品類中的一些商品,頁面如下:
譯者注:這里圖片上有一個stock
字段,這是上一版的程序使用的字段。在本書內程序已經修改,但圖片依然使用了上一版的圖片。本項目中后續並沒有使用stock
字段。
1.3創建商品品類視圖
為了展示商品,我們創建一個視圖,用於列出所有商品,或者根據品類顯示某一品類商品,編輯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(categories, 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
參數用於選出特定的品類。
還需要一個展示單個商品詳情的視圖,繼續編輯views.py
文件:
def product_detail(request, id, slug):
product = get_object_or_404(Product, id=id, slug=slug, availbable=True)
return render(request, 'shop/product/detail.html', {'product': product})
product_detail
視圖需要id
和slug
兩個參數來獲取商品對象。只通過ID可以獲得商品對象,因為ID是唯一的,這里增加了slug
字段是為了對搜索引擎優化。
在創建了上述視圖之后,需要為其配置URL,在shop
應用內創建urls.py
文件並添加如下內容:
from django.urls import path
from . import views
app_name = 'shop'
urlpatterns = [
path('', views.product_list, name='product_list'),
path('<slug:category_slug>/', views.product_list, name='product_list_by_category'),
path('<int:id>/<slug:slug>/', views.product_detail, name='product_detail'),
]
我們為product_list
視圖定義了兩個不同的URL,一個名稱是product_list
,不帶任何參數,表示展示全部品類的全部商品;一個名稱是product_list_by_category
,帶參數,用於顯示指定品類的商品。還為product_detail
視圖配置了傳入id
和slug
參數的URL。
這里要解釋的就是product_list視圖帶一個默認值參數,所以默認路徑進來后就是展示全部品類的頁面。加上了具體某個品類,就展示那個品類的商品。詳情頁的URL使用id和slug來進行參數傳遞。
還需要編寫項目的一級路由,編輯myshop
項目的根urls.py
文件:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('shop.urls', namespace='shop')),
]
我們為shop
應用配置了名為shop
的二級路由。
由於URL中有參數,就需要配置URL反向解析,編輯shop
應用的models.py
文件,導入reverse()
函數,然后為Category
和Product
模型編寫get_absolute_url()
方法:
from django.urls import reverse
class Category(models.Model):
# ......
def get_absolute_url(self):
return reverse('shop:product_list_by_category',args=[self.slug])
class Product(models.Model):
# ......
def get_absolute_url(self):
return reverse('shop:product_detail',args=[self.id,self.slug])
這樣就為模型的對象配置好了用於反向解析URL的方法,我們已經知道,get_absolute_url()
是很好的獲取具體對象規范化URL的方法。
1.4創建商品品類模板
現在需要創建模板,在shop
應用下建立如下目錄和文件結構:
templates/
shop/
base.html
product/
list.html
detail.html
像以前的項目一樣,base.html
是母版,讓其他的模板繼承母版。編輯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>
這是這個項目的母版。其中使用的CSS文件可以從隨書源代碼中復制到shop
應用的static/
目錄下。
然后編輯shop/product/list.html
:
{% extends "shop/base.html" %}
{% load static %}
{% block title %}
{% if category %}{{ category.name }}{% else %}Products{% endif %}
{% endblock %}
{% block content %}
<div id="sidebar">
<h3>Categories</h3>
<ul>
<li {% if not category %}class="selected"{% endif %}>
<a href="{% url "shop:product_list" %}">All</a>
</li>
{% for c in categories %}
<li {% if category.slug == c.slug %}class="selected"
{% endif %}>
<a href="{{ c.get_absolute_url }}">{{ c.name }}</a>
</li>
{% endfor %}
</ul>
</div>
<div id="main" class="product-list">
<h1>{% if category %}{{ category.name }}{% else %}Products
{% endif %}</h1>
{% for product in products %}
<div class="item">
<a href="{{ product.get_absolute_url }}">
<img src="
{% if product.image %}{{ product.image.url }}{% else %}{% static "img/no_image.png" %}{% endif %}">
</a>
<a href="{{ product.get_absolute_url }}">{{ product.name }}</a>
<br>
${{ product.price }}
</div>
{% endfor %}
</div>
{% endblock %}
這是展示商品列表的模板,繼承了base.html
,使用categories
變量在側邊欄顯示品類的列表,在頁面主體部分通過products
變量展示商品清單。展示所有商品和具體某一類商品都采用這個模板。如果Product
對象的image
字段為空,我們顯示一張默認的圖片,可以在隨書源碼中找到img/no_image.png
,將其拷貝到對應的目錄。
由於使用了Imagefield,還需要對媒體文件進行一些設置,編輯settings.py文件加入下列內容:
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
MEDIA_URL
是保存用戶上傳的媒體文件的目錄,MEDIA_ROOT
是存放媒體文件的目錄,通過BASE_DIR
變量動態建立該目錄。
為了讓Django提供靜態文件服務,還必須修改shop
應用的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_image.png
,如下圖:
然后編寫商品詳情頁shop/product/detail.html
:
{% extends "shop/base.html" %}
{% load static %}
{% block title %}
{{ product.name }}
{% endblock %}
{% block content %}
<div class="product-detail">
<img src="{% if product.image %}{{ product.image.url }}{% else %}
{% static "img/no_image.png" %}{% endif %}">
<h1>{{ product.name }}</h1>
<h2><a href="{{ product.category.get_absolute_url }}">{{ product.category }}</a></h2>
<p class="price">${{ product.price }}</p>
{{ product.description|linebreaks }}
</div>
{% endblock %}
在模板中調用get_absolute_url()
方法用於展示對應類的商品,打開http://127.0.0.1:8000/,然后點擊任意一個商品,詳情頁如下:
現在已經將商品品類和展示功能創建完畢。
2創建購物車功能
在建立商品品類之后,下一步是創建一個購物車,讓用戶可以將指定的商品及數量加入購物車,而且在瀏覽整個網站並且下訂單之前,購物車都會維持其中的信息。為此,我們需要將購物車數據存儲在當前用戶的session中。
由於session通用翻譯成會話,而在本章中很多時候session指的是Django的session模塊或者session對象,所以不再進行翻譯。
我們將使用Django的session框架來存儲購物車數據。直到用戶生成訂單,商品信息都存儲在購session中,為此我們還需要為購物車和其中的商品創建一個模型。
2.1使用Django的session模塊
Django 提供了一個session模塊,用於進行匿名或登錄用戶會話,可以為每個用戶保存獨立的數據。session數據存儲在服務端,通過在cookie中包含session ID就可以獲取到session,除非將session存儲在cookie中。session中間件管理具體的cookie信息,默認的session引擎將session保存在數據庫內,也可以切換不同的session引擎。
要使用session,需要在settings.py
文件的MIDDLEWARE
設置中啟用'django.contrib.sessions.middleware.SessionMiddleware'
,這個管理session中間件在使用startproject
命令創建項目時默認已經被啟用。
這個中間件在request
對象中設置了session
屬性用於訪問session數據,類似於一個字典一樣,可以存儲任何可以被序列化為JSON的Python數據類型。可以像這樣存入數據:
request.session['foo'] = 'bar'
獲取鍵對應的值:
request.session.get('foo')
刪除一個鍵值對:
del request.session['foo']
可以將request.session
當成字典來操作。
當用戶登錄到一個網站的時候,服務器會創建一個新的用於登錄用戶的session信息替代原來的匿名用戶session信息,這意味着原session信息會丟失。如果想保存原session信息,需要在登錄的時候將原session信息存為一個新的session數據。
2.2session設置
Django中可以配置session模塊的一些參數,其中最重要的是SESSION_ENGINE
設置,即設置session數據具體存儲在何處。默認情況下,Django通過django.contrib.session
應用的Session
模型,將session數據保存在數據庫中的django_session
數據表中。
Django提供了如下幾種存儲session數據的方法:
- Database sessions:session數據存放於數據庫中,為默認設置,即將session數據存放到settings.py中的DATABASES設置中的數據庫內。
- File-based sessions:保存在一個具體的文件中
- Cached sessions:基於緩存的session存儲,使用Django的緩存系統,可以通過CACHES設置緩存后端。這種情況下效率最高。
- Cached database sessions:先存到緩存再持久化到數據庫中。取數據時如果緩存內無數據,再從數據庫中取。
- Cookie-based sessions:基於cookie的方式,session數據存放在cookie中。
為了提高性能,使用基於緩存的session是好的選擇。Django直接支持基於Memcached的緩存和如Redis的第三方緩存后端。
還有其他一系列的session設置,以下是一些主要的設置:
SESSION_COOKIE_AGE
:session過期時間,為秒數,默認為1209600
秒,即兩個星期。SESSION_COOKIE_DOMAIN
:默認為None
,設置為某個域名可以啟用跨域cookie。SESSION_COOKIE_SECURE
:布爾值,默認為False
,表示是否只允許HTTPS連接下使用sessionSESSION_EXPIRE_AT_BROWSER_CLOSE
:布爾值,默認為False
,表示是否一旦瀏覽器關閉,session就失效SESSION_SAVE_EVERY_REQUEST
:布爾值,默認為False
,設置為True
表示每次HTTP請求都會更新session,其中的過期時間相關設置也會一起更新。
可以在https://docs.djangoproject.com/en/2.0/ref/settings/#sessions查看所有的session設置和默認值。
2.3session過期
特別需要提的是SESSION_EXPIRE_AT_BROWSER_CLOSE
設置。該設置默認為False
,此時session有效時間采用SESSION_COOKIE_AGE
中的設置。
如果將SESSION_EXPIRE_AT_BROWSER_CLOSE
設置為True
,則session在瀏覽器關閉后就失效,SESSION_COOKIE_AGE
設置不起作用。
還可以使用request.session.set_expiry()
方法設置過期時間。
2.4在session中存儲購物車數據
我們需要創建一個簡單的數據結構,可以被JSON序列化,用於存放購物車數據。購物車中必須包含如下內容:
Product
對象的ID- 商品的數量
- 商品的單位價格
由於商品的價格會變化,我們在將商品加入購物車的同時存儲當時商品的價格,如果商品價格之后再變動,也不進行處理。
現在需要實現創建購物車和為session添加購物車的功能,購物車按照如下方式工作:
- 當需要創建一個購物車的時候,先檢查session中是否存在自定義的購物車鍵,如果存在說明當前用戶已經使用了購物車,如果不存在,就新建一個購物車鍵。
- 對於接下來的HTTP請求,都要重復第一步,並且從購物車中保存的商品ID到數據庫中取得對應的
Product
對象數據。
編輯settings.py
里新增一行:
CART_SESSION_ID = 'cart'
這就是我們的購物車鍵名稱,由於session對於每個用戶都通過中間件管理,所以可以在所有用戶的session里都使用統一的這個名稱。
然后新建一個應用來管理購物車,啟動系統命令行並創建新應用cart
:
python manage.py startapp cart
然后在settings.py
中激活該應用:
INSTALLED_APPS = [
# ...
'shop.apps.ShopConfig',
'cart.apps.CartConfig',
]
在cart
應用中創建cart.py
,添加如下代碼:
from decimal import Decimal
from django.conf import settings
from shop.models import Product
class Cart:
def __init__(self):
"""
初始化購物車對象
"""
self.session = request.session
cart = self.session.get(settings.CART_SESSION_ID)
if not cart:
# 向session中存入空白購物車數據
cart = self.session[settings.CART_SESSION_ID] = {}
self.cart =cart
這是我們用於管理購物車的Cart類,使用request對象進行初始化,使用self.session = request.session
讓類中的其他方法可以訪問session數據。首先,使用self.session.get(settings.CART_SESSION_ID)
嘗試獲取購物車對象。如果不存在購物車對象,通過為購物車鍵設置一個空白字段對象從而新建一個購物車對象。我們將使用商品ID作為字典中的鍵,其值又是一個由數量和價格構成的字典,這樣可以保證不會重復生成同一個商品的購物車數據,也簡化了取出購物車數據的方式。
創建將商品添加到購物車和更新數量的方法,為Cart類添加add()
和save()
方法:
class Cart:
# ......
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):
# 設置session.modified的值為True,中間件在看到這個屬性的時候,就會保存session
self.session.modified = True
add()
方法接受以下參數:
product
:要向購物車內添加或更新的product
對象quantity
:商品數量,為整數,默認值為1update_quantity
:布爾值,為True
表示要將商品數量更新為quantity
參數的值,為False
表示將當前數量增加quantity
參數的值。
我們把商品的ID轉換成字符串形式然后作為購物車中商品鍵名,這是因為Django使用JSON序列化session數據,而JSON只允許字符串作為鍵名。商品價格也被從decimal
類型轉換為字符串,同樣是為了序列化。最后,使用save()
方法把購物車數據保存進session。
save()
方法中修改了session.modified = True
,中間件通過這個判斷session已經改變然后存儲session數據。
我們還需要從購物車中刪除商品的方法,為Cart
類添加以下方法:
class Cart:
# ......
def remove(self, product):
"""
從購物車中刪除商品
"""
product_id = str(product.id)
if product_id in self.cart:
del self.cart[product_id]
self.save()
remove()
根據id從購物車中移除對應的商品,然后調用save()
方法保存session數據。
為了使用方便,我們會需要遍歷購物車內的所有商品,用於展示等操作。為此需要在Cart
類內定義__iter()__
方法,生成迭代器,供將for循環使用。
class Cart:
# ......
def __iter__(self):
"""
遍歷所有購物車中的商品並從數據庫中取得商品對象
"""
product_ids = self.cart.keys()
# 獲取購物車內的所有商品對象
products = Product.objects.filter(id__in=product_ids)
cart = self.cart.copy()
for product in products:
cart[str(product.id)]['product'] = product
for item in cart.values():
item['price'] = Decimal(item['price'])
item['total_price'] = item['price'] * item['quantity']
yield item
在__iter()__
方法中,獲取了當前購物車中所有商品的Product對象。然后淺拷貝了一份cart
購物車數據,並為其中的每個商品添加了鍵為product
,值為商品對象的鍵值對。最后迭代所有的值,為把其中的價格轉換為decimal
類,增加一個total_price
鍵來保存總價。這樣我們就可以迭代購物車對象了。
還需要顯示購物車中有幾件商品。當執行len()
方法的時候,Python會調用對象的__len__()
方法,為Cart
類添加如下的__len__()
方法:
class Cart:
# ......
def __len__(self):
"""
購物車內一共有幾種商品
"""
return sum(item['quantity'] for item in self.cart.values())
這個方法返回所有商品的數量的合計。
再編寫一個計算購物車商品總價的方法:
class Cart:
# ......
def get_total_price(self):
return sum(Decimal(item['price']*item['quantity']) for item in self.cart.values())
最后,再編寫一個清空購物車的方法:
class Cart:
# ......
def clear(self):
del self.session[settings.CART_SESSION_ID]
self.save()
現在就編寫完了用於管理購物車的Cart
類。
譯者注,原書的代碼采用class Cart(object)
的寫法,譯者將其修改為Python 3的新式類編寫方法。
2.5創建購物車視圖
現在我們擁有了管理購物車的Cart類,需要創建如下的視圖來添加、更新和刪除購物車中的商品
- 添加商品的視圖,可以控制增加或者更新商品數量
- 刪除商品的視圖
- 詳情視圖,顯示購物車中的商品和總金額等信息
2.5.1購物車相關視圖
為了向購物車內增加商品,顯然需要一個表單讓用戶選擇數量並按下添加到購物車的按鈕。在cart
應用中創建forms.py
文件並添加如下內容:
from django import forms
PRODUCT_QUANTITY_CHOICES = [(i, str(i)) for i in range(1, 21)]
class CartAddProductForm(forms.Form):
quantity = forms.TypedChoiceField(choices=PRODUCT_QUANTITY_CHOICES, coerce=int)
update = forms.BooleanField(required=False, initial=False, widget=forms.HiddenInput)
使用該表單添加商品到購物車,這個CartAddProductForm表單包含如下兩個字段:
quantity
:限制用戶選擇的數量為1-20個。使用TypedChoiceField
字段,並且設置coerce=int
,將輸入轉換為整型字段。update
:用於指定當前數量是增加到原有數量(False
)上還是替代原有數量(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 .form 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取得商品對象之后驗證表單。表單驗證通過后,將商品添加到購物車,然后跳轉到購物車詳情頁面對應的cart_detail
URL,稍后我們會來編寫cart_detail
URL。
再來編寫刪除商品的視圖,在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')
刪除商品視圖同樣接受商品ID作為參數,通過ID獲取Product
對象,刪除成功之后跳轉到cart_detail
URL。
還需要一個展示購物車詳情的視圖,繼續在cart
應用的views.py
文件中添加下列代碼:
def cart_detail(request):
cart = Cart(request)
return render(request, 'cart/detail.html', {'cart': cart})
cart_detail
視圖用來展示當前購物車中的詳情。現在已經創建了添加、更新、刪除及展示的視圖,需要配置URL,在cart
應用里新建urls.py
:
from django.urls import path
from . import views
app_name = 'cart'
urlpatterns = [
path('', views.cart_detail, name='cart_detail'),
path('add/<int:product_id>/', views.cart_add, name='cart_add'),
path('remove/<int:product_id>/', views.cart_remove, name='cart_remove'),
]
然后編輯項目的根urls.py
,配置URL:
urlpatterns = [
path('admin/', admin.site.urls),
path('cart/', include('cart.urls', namespace='cart')),
path('', include('shop.urls', namespace='shop')),
]
注意這一條路由需要增加在shop.urls
路徑之前,因為這一條比下一條的匹配路徑更加嚴格。
2.5.2創建展示購物車的模板
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 }}">
<img src="
{% if product.image %}{{ product.image.url }}{% else %}{% static 'img/no_image.png' %}{% endif %}"
alt="">
</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
視圖。還提供了一個刪除鏈接供用戶刪除商品。
2.5.3添加商品至購物車
需要修改商品詳情頁,增加一個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/templates/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>
{{ product.description|linebreaks }}
啟動站點,到http://127.0.0.1:8000/,進入任意一個商品的詳情頁,可以看到商品詳情頁內增加了按鈕,如下圖:
選擇一個數量,然后點擊Add to cart按鈕,即可購物車詳情界面,如下圖:
2.5.4更新商品數量
當用戶在瀏覽購物車詳情時,在下訂單前很可能會修改購物車的中商品的數量,我們必須允許用戶在購物車詳情頁修改數量。
編輯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
應用的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按鈕來測試新的功能,還可以嘗試從購物車中刪除商品。
2.6創建購物車上下文處理器
你可能在實際的電商網站中會注意到,購物車的詳細情況一直顯示在頁面上方的導航部分,在購物車為空的時候顯示特殊的為空的字樣,如果購物車中有商品,則會顯示數量或者其他內容。這種展示購物車的方法與之前編寫的處理購物車的視圖沒有關系,因此我們可以通過創建一個上下文處理器,將購物車對象作為request
對象的一個屬性,而不用去管是不是通過視圖操作。
2.6.1上下文處理器
Django中的上下文管理器,就是能夠接受一個request
請求對象作為參數,返回一個要添加到request
上下文的字典的Python函數。
當默認通過startproject
啟動一個項目的時候,settings.py
中的TEMPLATES
設置中的conetext_processors
部分,就是給模板附加上下文的上下文處理器,有這么幾個:
django.template.context_processors.debug
:這個上下文處理器附加了布爾類型的debug
變量,以及sql_queries
變量,表示請求中執行的SQL查詢django.template.context_processors.request
:這個上下文處理器設置了request
變量django.contrib.auth.context_processors.auth
:這個上下文處理器設置了user
變量django.contrib.messages.context_processors.messages
:這個上下文處理器設置了messages
變量,用於使用消息框架
除此之外,django還啟用了django.template.context_processors.csrf
來防止跨站請求攻擊。這個組件沒有寫在settings.py
里,強制啟用,無法進行設置和關閉。有關所有上下文管理器的詳情請參見https://docs.djangoproject.com/en/2.0/ref/templates/api/#built-in-template-context-processors。
2.6.2將購物車設置到request上下文中
現在我們就來設置一個自定義上下文處理器,以在所有模板內訪問購物車對象。
在cart
應用內新建一個context_processors.py
文件,同視圖,模板以及其他內容一樣,django內的程序可以寫在應用內的任何地方,但為了結構良好,將其單獨寫成一個文件:
from .cart import Cart
def cart(request):
return {'cart': Cart(request)}
Django規定的上下文處理器,就是一個函數,接受request
請求作為參數,然后返回一個字典。這個字典的鍵值對被RequestContext
設置為所有模板都可以使用的變量及對應的值。在我們的上下文處理器中,我們使用request
對象初始化了cart
對象
之后在settings.py里將我們的自定義上下文處理器加到TEMPLATES設置中:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')]
,
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
......
'cart.context_processors.cart'
],
},
},
]
定義了上下文管理器之后,只要一個模板被RequestContext
渲染,上下文處理器就會被執行然后附加上變量名cart
。
所有使用RequestContext
的請求過程中都會執行上下文處理器。對於不是每個模板都需要的變量,一般情況下首先考慮的是使用自定義模板標簽,特別是涉及到數據庫查詢的變量,否則會極大的影響網站的效率。
修改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>
啟動站點,到http://127.0.0.1:8000/,添加一些商品到購物車,在網站的標題部分可以顯示出購物車的信息:
3生成客戶訂單
當用戶准備對一個購物車內的商品進行結賬的時候,需要生成一個訂單數據保存到數據庫中。訂單必須保存用戶信息和用戶所購買的商品信息。
為了實現訂單功能,新創建一個訂單應用:
python manage.py startapp orders
然后在settings.py
中的INSTALLED_APPS
中進行激活:
INSTALLED_APPS = [
# ...
'orders.apps.OrdersConfig',
]
3.1創建訂單模型
我們用一個模型存儲訂單的詳情,然后再用一個模型保存訂單內的商品信息,包括價格和數量。編輯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', on_delete=models.CASCADE)
product = models.ForeignKey(Product, related_name='order_items', on_delete=models.CASCADE)
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
。稍后將在支付系統中使用該字段區分訂單是否已經付款。還定義了一個獲得總金額的方法get_total_cost()
,通過該方法可以獲得當前訂單的總金額。
OrderItem
存儲了生成訂單時候的價格和數量。然后定義了一個get_cost()
方法,返回當前商品的總價。
之后執行數據遷移,過程不再贅述。
3.2將訂單模型加入管理后台
編輯orders
應用的admin.py
文件:
from django.contrib import admin
from .models import Order, OrderItem
class OrderItemInline(admin.TabularInline):
model = OrderItem
raw_id_fields = ['product']
@admin.register(Order)
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]
我們讓OrderItem
類繼承了admin.TabularInline
類,然后在OrderAdmin
類中使用了inlines
參數指定OrderItemInline
,通過該設置,可以將一個模型顯示在相關聯的另外一個模型的編輯頁面中。
啟動站點到http://127.0.0.1:8000/admin/orders/order/add/
,可以看到如下的頁面:
3.3創建客戶訂單視圖和模板
在用戶提交訂單的時候,我們需要用剛創建的訂單模型來保存用戶當時購物車內的信息。創建一個新的訂單的步驟如下:
- 提供一個表單供用戶填寫
- 根據用戶填寫的內容生成一個新
Order
類實例,然后將購物車中的商品放入OrderItem
實例中並與Order
實例建立外鍵關系 - 清理全部購物車內容,然后重定向用戶到一個操作成功頁面。
首先利用內置表單功能建立訂單表單,在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'])
# 成功生成OrderItem之后清除購物車
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)
獲取當前購物車對象;之后根據HTTP請求種類的不同,視圖進行以下工作:
- GET請求:初始化空白的
OrderCreateForm
,並且渲染orders/order/created.html
頁面。 - POST請求:通過POST請求中的數據生成表單並且驗證,驗證通過之后執行
order = form.save()
創建新訂單對象並寫入數據庫;然后遍歷購物車的所有商品,對每一種商品創建一個OrderItem
對象並存入數據庫。最后清空購物車,渲染orders/order/created.html
頁面。
在orders
應用里建立urls.py
作為二級路由:
from django.urls import path
from . import views
app_name = 'orders'
urlpatterns = [
path('create/', views.order_create, name='order_create'),
]
配置好了order_create
視圖的路由,再配置myshop
項目的根urls.py
文件,在shop.urls
之前增加下邊這條:
path('orders/',include('orders.urls', namespace='orders')),
編輯購物車詳情頁cart/detail.html
,找到下邊這行:
<a href="#" class="button">Checkout</a>
將這個結賬按鈕的鏈接修改為order_create
視圖的URL:
<a href="{% url 'orders:order_create' %}" class="button">Checkout</a>
用戶現在可以通過購物車詳情頁來提交訂單,我們要為訂單頁制作模板,在orders
應用下建立如下文件和目錄結構:
templates/
orders/
order/
create.html
created.html
編輯確認訂單的頁面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" novalidate>
{{ 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 %}
這是訂單成功頁面。啟動站點,添加一些商品到購物車中,然后在購物車詳情頁面中點擊CHECKOUT按鈕,之后可以看到如下頁面:
填寫表單然后點擊Place order按鈕,訂單被創建,然后重定向至創建成功頁面:
現在可以到管理后台去看一看相關的信息了。
4使用Celery啟動異步任務
在一個視圖內執行的所有操作,都會影響到響應時間。很多情況下,尤其視圖中有一些非常耗時或者可能會失敗,需要重試的操作,我們希望盡快給用戶先返回一個響應而不是等到執行結束,而讓服務器去繼續異步執行這些任務。例如:很多視頻分享網站允許用戶上傳視頻,在上傳成功之后服務器需花費一定時間轉碼,這個時候會先返回一個響應告知用戶視頻已經成功上傳,正在進行轉碼,然后異步進行轉碼。還一個例子是向用戶發送郵件。如果站點中有一個視圖的操作是發送郵件,SMTP連接很可能失敗或者速度比較慢,這個時候采用異步的方式就能有效的避免阻塞。
Celery是一個分布式任務隊列,采取異步的方式同時執行大量的操作,支持實施操作和計划任務,可以方便的批量創建異步任務並且執行,也可以設定為計划執行。Celery的文檔在http://docs.celeryproject.org/en/latest/index.html。
4.1安裝Celery
通過pip
安裝Celery:
pip install celery==4.1.0
Celery需要一個消息代理程序來處理外部的請求,這個代理把要處理的請求發送到Celery worker,也就是實際處理任務的模塊。所以還需要安裝一個消息代理程序:
4.2安裝RabbitMQ
Celery的消息代理程序有很多選擇,Redis數據庫也可以作為Celery的消息代理程序。這里我們使用RabbitMQ,因為它是Celery官方推薦的消息代理程序。
如果是Linux系統,通過如下命令安裝RabbitMQ:
apt-get install rabbitmq
如果使用macOS X或者Windows,可以在https://www.rabbitmq.com/download.html下載RabbitMQ。
安裝之后使用下列命令啟動RabbitMQ服務:
rabbitmq-server
之后會看到:
Starting broker... completed with 10 plugins.
就說明RabbitMQ已經就緒,等待接受消息。
譯者注:Windows下安裝RabbitMQ,必須先安裝Erlong OPT平台,然后安裝從官網下載回來的RabbitMQ windows installer。之后需要手工把Erlong安裝目錄下的bin目錄和RabbitMQ安裝目錄下的sbin目錄設置到PATH中。之后安裝參見這里。
4.3在項目中集成Celery
需要為項目使用的Celery實例進行一些配置,在settings.py
文件的相同目錄下創建celery.py
文件:
import os
from celery import Celery
# 為celery程序設置環境為當前項目的環境
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myshop.settings')
app = Celery('myshop')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
這段程序解釋如下:
- 導入
DJANGO_SETTINGS_MODULE
環境變量,為Celery命令行程序創造運行環境。 - 實例化一個
app
對象,是一個Celery程序實例 - 調用
config_from_object()
方法,從我們項目的設置文件中讀取環境設置。namespace
屬性指定了在我們的settings.py文件中,所有和Celery相關的配置都以CELERY
開頭,例如CELERY_BROKER_URL
。 - 調用
autodiscover_tasks()
,讓Celery自動發現所有的異步任務。Celery會在每個INSTALLED_APPS
中列出的應用中尋找task.py
文件,在里邊尋找定義好的異步任務然后執行。
還需要在項目的__init__.py
文件中導入celery
模塊,以讓項目啟動時Celery就運行,編輯myshop/__inti__.py
:
# import celery
from .celery import app as celery_app
現在就可以為應用啟動異步任務了。
CELERY_ALWAYS_EAGER
設置可以讓Celery在本地以同步的方式直接執行任務,而不會去把任務加到隊列中。這常用來進行測試或者檢查Celery的配置是否正確。
4.4為應用添加異步任務
我們准備在用戶提交訂單的時候異步發送郵件。一般的做法是在應用目錄下建立一個task
模塊專門用於編寫異步任務,在orders
應用下建立task.py
文件,添加如下代碼:
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 {}'.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, 'lee0709@vip.sina.com', [order.email])
print(mail_sent, type(mail_sent))
return mail_sent
將order_created
函數通過裝飾器@task
定義為異步任務,可以看到,只要用@task
裝飾就可以把一個函數變成Celery異步任務。這里我們給異步函數傳入order_id
,推薦僅傳入ID,讓異步任務啟動的時候再去檢索數據庫。最后拼接好標題和正文后使用send_mail()
發送郵件。
在第二章已經學習過如何發送郵件,如果沒有SMTP服務器,在settings.py
里將郵件配置為打印到控制台上:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
在實際應用中,除了耗時比較大的功能之外,還可以將其他容易失敗需要重試的功能,即使耗時較短,也推薦設置為異步任務。
設置好了異步任務之后,還需要修改原來的視圖order_created
,以便在訂單完成的時候,調用order_created
異步函數。編輯orders
應用的views.py
文件:
from .task import order_created
def order_create(request):
#......
if request.method == "POST":
#......
if form.is_valid():
#......
cart.clear()
# 啟動異步任務
order_created.delay(order.id)
#......
調用delay()
方法即表示異步執行該任務,任務會被加入隊列然后交給執行程序執行。
啟動另外一個shell(必須是導入了當前環境的命令行窗口,比如Pycharm中啟動的terminal),使用如下命令啟動Celery worker:
celery -A myshop worker -l info
現在Celery worker已經啟動並且准備處理任務。啟動站點,然后添加一些商品到購物車,提交訂單。在啟動了Celery worker的窗口應該能看到類似下邊的輸出:
[2017-12-17 17:43:11,462: INFO/MainProcess] Received task:
orders.tasks.order_created[e990ddae-2e30-4e36-b0e4-78bbd4f2738e]
[2017-12-17 17:43:11,685: INFO/ForkPoolWorker-4] Task
orders.tasks.order_created[e990ddae-2e30-4e36-b0e4-78bbd4f2738e] succeeded in
0.22019841300789267s: 1
表示任務已經被執行,應該可以收到郵件了。
譯者注:Windows平台下,在發送郵件的時候,有可能出現錯誤信息如下:
not enough values to unpack (expected 3, got 0)
這是因為Celery 4.x 在win10版本下運行存在問題,解決方案為:先安裝Python的eventlet
模塊:
pip install eventlet
然后在啟動Celery worker的時候,加上參數 -P eventlet,命令行如下:
celery -A myshop worker -l info -P eventlet
即可解決該錯誤。在linux下應該不會發生該錯誤。參考Celery項目在 Github 上的問題:Unable to run tasks under Windows #4081
4.5監控Celery
如果想要監控異步任務的執行情況,可以安裝Python的FLower模塊:
pip install flower==0.9.2
之后在新的終端窗口輸入:
celery -A myshop flower
之后在瀏覽器中打開http://localhost:5555/dashboard
,即可看到圖形化監控的Celery情況:
可以在https://flower.readthedocs.io/查看Flower的文檔。
總結
這一章里創建了一個基礎的電商網站。為網站創建了商品品類和詳情展示,通過session創建了購物車應用。實現了一個自定義的上下文處理器用於將購物車對象附加到所有模板上,還實現了創建訂單的功能。最后還學習了使用Celery啟動異步任務。
在下一章將學習集成支付網關,為管理后台增加自定義操作,將數據導出為CSV格式,以及動態的生成PDF文件。