目錄
Django2實戰示例 第一章 創建博客應用
Django2實戰示例 第二章 增強博客功能
Django2實戰示例 第三章 擴展博客功能
Django2實戰示例 第四章 創建社交網站
Django2實戰示例 第五章 內容分享功能
Django2實戰示例 第六章 追蹤用戶行為
Django2實戰示例 第七章 創建電商網站
Django2實戰示例 第八章 管理支付與訂單
Django2實戰示例 第九章 擴展商店功能
Django2實戰示例 第十章 創建在線教育平台
Django2實戰示例 第十一章 渲染和緩存課程內容
Django2實戰示例 第十二章 創建API
Django2實戰示例 第十三章 上線
第八章 管理支付與訂單
上一章制作了一個帶有商品品類展示和購物車功能的電商網站雛形,同時也學到了如何使用Celery給項目增加異步任務。本章將學習為網站集成支付網關以讓用戶通過信用卡付款,還將為管理后台擴展兩項功能:將數據導出為CSV以及生成PDF發票。
本章的主要內容有:
- 集成支付網關到項目中
- 將訂單數據導出成CSV文件
- 為管理后台創建自定義視圖
- 動態生成PDF發票
1集成支付網關
支付網關是一種處理在線支付的網站或者程序,使用支付網關,就可以管理用戶的訂單,然后將支付過程交給一個可信賴且安全的第三方,而無需在我們自己的站點上處理支付信息。
支付網關有很多可供選擇,我們將要集成的是叫做"Braintree"的支付網關。Braintree使用較為廣泛,是Uber和Airbnb的支付服務提供商。Braintree提供了一套API用於支持信用卡,PayPal,Android Pay和Apple Pay等支付方式,官方網站在https://www.braintreepayments.com/。
Braintree提供了很多集成的方法,最簡單的集成方式就是Drop-in集成,包含一個預先建立好的支付表單。但是為了自定義一些支付過程中的內容,這里選擇使用高級的Hosted Field(字段托管)方式進行集成。在https://developers.braintreepayments.com/guides/hosted-fields/overview/javascript/v3可以看到詳細的幫助文檔。
支付表單中包含的信用卡號,CVV碼,過期日期等信息必須要得到安全處理,Hosted Field集成方式將這些字段展示給用戶的時候,在頁面中渲染的是一個iframe框架。我們可以來自定義該字段的外觀,但必須要遵循Payment Card Industry (PCI)安全支付的要求。由於可以修改外觀,用戶並不會注意到頁面使用了iframe。
譯者注:原書在這里說的不是很清晰。Hosted Fields的意思是敏感字段由我們頁面中的Braintree JavaScript客戶端通過Braintree服務器生成並填入到頁面中,而不是在模板中直接編寫input
字段。簡單的說就是信用卡等敏感信息的字段是由Braintree托管生成,而不是我們自行編寫。
1.1注冊Braintree沙盒測試賬戶
需要注冊一個Braintree賬戶。才能使用集成支付功能。我們先注冊一個Braintree沙盒賬戶用於開發和測試。打開https://www.braintreepayments.com/sandbox,如下圖所示:
填寫表單創建用戶,之后會收到電子郵件驗證,驗證通過之后在https://sandbox.braintreegateway.com/login進行登錄。可以得到自己的商戶ID和私有/公開密鑰如下圖所示:
這些信息與使用Braintree API進行驗證交易有關,注意保存好私鑰,不要泄露給他人。
1.2安裝Braintree的Python模塊
Braintree為Python提供了一個模塊操作其API,源代碼地址在https://github.com/braintree/braintree_python。我們將把這個braintree
模塊集成到站點中。
使用命令行安裝braintree
模塊:
pip install braintree==3.45.0
之后在settings.py
里配置:
# Braintree支付網關設置
BRAINTREE_MERCHANT_ID = 'XXX' # 商戶ID
BRAINTREE_PUBLIC_KEY = 'XXX' # 公鑰
BRAINTREE_PRIVATE_KEY = 'XXX' # 私鑰
from braintree import Configuration, Environment
Configuration.configure(
Environment.Sandbox,
BRAINTREE_MERCHANT_ID,
BRAINTREE_PUBLIC_KEY,
BRAINTREE_PRIVATE_KEY
)
將BRAINTREE_MERCHANT_ID
,BRAINTREE_PUBLIC_KEY
,BRAINTREE_PRIVATE_KEY
的值替換成你自己的實際信息。
注意此處的設置 Environment.Sandbox,
,表示我們當前集成的是沙盒環境。如果站點正式上線並且獲取了正式的Braintree賬戶,必須修改成Environment.Production
。Braintree對於正式賬號會有新的商戶ID和公鑰/私鑰。
Braintree的基礎設置結束了,下一步是將支付網關和支付過程結合起來。
1.3集成支付網關
結賬過程是這樣的:
- 將商品加入到購物車
- 從購物車中選擇結賬
- 輸入信用卡信息並且支付
針對支付功能,我們建立一個新的應用叫做payment
:
python manage.py startapp payment
編輯settings.py
文件,激活該應用:
INSTALLED_APPS = [
# ...
'payment.apps.PaymentConfig',
]
payment
現在已經被激活。
客戶成功提交訂單后,必須將該頁面重定向到一個支付過程頁面(目前是重定向到一個簡單的成功頁面)。編輯orders
應用中的views.py
,增加如下導入:
from django.urls import reverse
from django.shortcuts import render, redirect
在同一個文件內,將order_create
視圖的如下部分:
# 啟動異步任務
order_created.delay(order.id)
return render(request, 'orders/order/created.html', locals())
替換成:
# 啟動異步任務
order_created.delay(order.id)
# 在session中加入訂單id
request.session['order_id'] = order.id
# 重定向到支付頁面
return redirect(reverse('payment:process'))
這樣修改后,在成功創建訂單之后,session中就保存了訂單ID的變量order_id
,然后用戶被重定向至payment:process
URL,這個URL稍后會編寫。
注意必須為order_created
視圖啟動Celery。
每次我們向Braintree中發送一個交易請求的時候,會生成一個唯一的交易ID號。因此我們在Order
模型中增加一個字段用於存儲這個交易ID號,這樣可以將訂單與Braintree交易聯系起來。
編輯orders
應用的models.py
文件,為Order
模型新增一行:
class Order(models.Model):
# ...
braintree_id = models.CharField(max_length=150, blank=True)
之后執行數據遷移程序,每一個訂單都會保存與其關聯的交易ID。目前准備工作都已經做完,剩下就是在支付過程中使用支付網關。
1.3.1使用Hosted Fields進行支付
Hosted Fields方式允許我們創建自定義的支付表單,使用自定義樣式和表現形式。Braintree JavaScript SDK會在頁面中動態的添加iframe框體用於展示Host Fields支付字段。當用戶提交表單的時候,Hosted Fields會安全地提取用戶的信用卡等信息,生成一個特征字符串(tokenize,令牌化)。如果令牌化過程成功,就可以使用這個特征字符串(token),通過視圖中的braintree
模塊發起一個支付申請。
為此需要建立一個支付視圖。這個視圖的工作流程如下:
- 用戶提交訂單時,視圖通過
braintree
模塊生成一個token,這個token用於Braintree JavaScript 客戶端生成支付表單,並不是最終發送給支付網關的token。為了方便以下把這個token稱為臨時token,把最終提交給Braintree網站的token叫做交易token。 - 視圖渲染支付表單所在的模板。頁面中的Braintree JavaScript 客戶端使用臨時token來生成頁面中的支付表單。
- 用戶輸入信用卡信息並且提交支付表單后,Braintree JavaScript 客戶端會生成交易token,將這個交易token通過
POST
請求發送到視圖 - 視圖獲取交易token之后,通過
braintree
模塊向網站提交交易請求。
了解了工作流程之后,來編寫相關視圖,編輯payment
應用中的views.py
文件,添加下列代碼:
import braintree
from django.shortcuts import render, redirect, get_object_or_404
from orders.models import Order
def payment_process(request):
order_id = request.session.get('order_id')
order = get_object_or_404(Order, id=order_id)
if request.method == "POST":
# 獲得交易token
nonce = request.POST.get('payment_method_nonce', None)
# 使用交易token和附加信息,創建並提交交易信息
result = braintree.Transaction.sale(
{
'amount': '{:2f}'.format(order.get_total_cost()),
'payment_method_nonce': nonce,
'options': {
'submit_for_settlement': True,
}
}
)
if result.is_success:
# 標記訂單狀態為已支付
order.paid = True
# 保存交易ID
order.braintree_id = result.transaction.id
order.save()
return redirect('payment:done')
else:
return redirect('payment:canceled')
else:
# 生成臨時token交給頁面上的JS程序
client_token = braintree.ClientToken.generate()
return render(request,
'payment/process.html',
{'order': order,
'client_token': client_token})
這個payment_process
視圖管理支付過程,工作流程如下 :
- 從session中取出由
order_create
視圖設置的order_id
變量。 - 獲取
Order
對象,如果沒找到,返回404 Not Found
錯誤 - 如果接收到
POST
請求,獲取交易tokenpayment_method_nonce
,使用交易token和braintree.Transaction.sale()
方法生成新的交易,該方法的幾個參數解釋如下:amount
:總收款金額payment_method_nonce
:交易token,由頁面中的Braintree JavaScript 客戶端生成。options
:其他選項,submit_for_settlement
設置為True
表示生成交易信息完畢的時候就立刻提交。
- 如果交易成功,通過設置
paid
屬性為True
,將訂單標記為已支付,將交易ID存儲到braintree_id
屬性中,之后重定向至payment:done
,如果交易失敗就重定向至payment:canceled
。 - 如果視圖接收到
GET
請求,生成臨時token交給頁面中的Braintree JavaScript 客戶端。
下邊建立支付成功和失敗時的處理視圖,在payment
應用的views.py中添加下列代碼:
def payment_done(request):
return render(request, 'payment/done.html')
def payment_canceled(request):
return render(request, 'payment/canceled.html')
然后在payment
目錄下建立urls.py
,為上述視圖配置路由:
from django.urls import path
from . import views
app_name = 'payment'
urlpatterns = [
path('process/', views.payment_process, name='process'),
path('done/', views.payment_done, name='done'),
path('canceled/', views.payment_canceled, name='canceled'),
]
這是支付流程的路由,配置了如下URL模式:
process
:處理支付的視圖done
:支付成功的視圖canceled
:支付未成功的視圖
編輯myshop
項目的根urls.py
文件,為payment
應用配置二級路由:
urlpatterns = [
# ...
path('payment/', include('payment.urls', namespace='payment')),
path('', include('shop.urls', namespace='shop')),
]
依然要注意這一行要放到shop.urls
上邊,否則無法被解析到。
之后是建立視圖,在payment目錄下建立templates/payment/目錄,並在其中建立 process.html, done.html,canceled.html三個模板。先來編寫process.html:
在payment
應用內建立下列目錄和文件結構:
templates/
payment/
process.html
done.html
canceled.html
編輯payment/process.html
,添加下列代碼:
{% extends "shop/base.html" %}
{% block title %}Pay by credit card{% endblock %}
{% block content %}
<h1>Pay by credit card</h1>
<form action="." id="payment" method="post">
<label for="card-number">Card Number</label>
<div id="card-number" class="field"></div>
<label for="cvv">CVV</label>
<div id="cvv" class="field"></div>
<label for="expiration-date">Expiration Date</label>
<div id="expiration-date" class="field"></div>
<input type="hidden" id="nonce" name="payment_method_nonce" value="">
{% csrf_token %}
<input type="submit" value="Pay">
</form>
<!-- Load the required client component. -->
<script src="https://js.braintreegateway.com/web/3.29.0/js/client.min.js"></script>
<!-- Load Hosted Fields component. -->
<script src="https://js.braintreegateway.com/web/3.29.0/js/hosted-fields.min.js"></script>
<script>
var form = document.querySelector('#payment');
var submit = document.querySelector('input[type="submit"]');
braintree.client.create({
authorization: '{{ client_token }}'
}, function (clientErr, clientInstance) {
if (clientErr) {
console.error(clientErr);
return;
}
braintree.hostedFields.create({
client: clientInstance,
styles: {
'input': {'font-size': '13px'},
'input.invalid': {'color': 'red'},
'input.valid': {'color': 'green'}
},
fields: {
number: {selector: '#card-number'},
cvv: {selector: '#cvv'},
expirationDate: {selector: '#expiration-date'}
}
}, function (hostedFieldsErr, hostedFieldsInstance) {
if (hostedFieldsErr) {
console.error(hostedFieldsErr);
return;
}
submit.removeAttribute('disabled');
form.addEventListener('submit', function (event) {
event.preventDefault();
hostedFieldsInstance.tokenize(function (tokenizeErr, payload) {
if (tokenizeErr) {
console.error(tokenizeErr);
return;
}
// set nonce to send to the server
document.getElementById('nonce').value = payload.nonce;
// submit form
document.getElementById('payment').submit();
});
}, false);
});
});
</script>
{% endblock %}
這是用戶填寫信用卡信息並且提交支付的模板,我們用<div>
替代<input>
使用在信用卡號,CVV碼和過期日期字段上。這些字段就是Braintree JavaScript客戶端渲染的iframe字段。還使用了一個名稱為payment_method_nonce
的<input>
元素用於提交交易ID到后端。
在模板中還導入了Braintree JavaScript SDK的client.min.js
和Hosted Fields組件hosted-fields.min.js
,然后執行了下列JS代碼:
- 使用
braintree.client.create()
方法,傳入client_token
即payment_process
視圖里生成的臨時token,實例化Braintree JavaScript 客戶端。 - 使用
braintree.hostedFields.create()
實例化Hosted Field組件 - 給
input
字段應用自定義樣式 - 給
cardnumber
,cvv
, 和expiration-date
字段設置id
選擇器 - 給表單的
submit
行為綁定一個事件,當表單被點擊提交時,Braintree SDK 使用表單中的信息,生成交易token放入payment_method_nonce
字段中,然后提交表單。
編輯payment/done.html
文件,添加下列代碼:
{% extends "shop/base.html" %}
{% block content %}
<h1>Your payment was successful</h1>
<p>Your payment has been processed successfully.</p>
{% endblock %}
這是訂單成功支付時用戶被重定向的頁面。
編輯canceled.html
,添加下列代碼:
{% extends "shop/base.html" %}
{% block content %}
<h1>Your payment has not been processed</h1>
<p>There was a problem processing your payment.</p>
{% endblock %}
這是訂單未支付成功時用戶被重定向的頁面。之后我們來試驗一下付款。
1.4測試支付
打開系統命令行窗口然后運行RabbitMQ:
rabbitmq-server
再啟動一個命令行窗口,啟動Celery worker:
celery -A myshop worker -l info
再啟動一個命令行窗口,啟動站點:
python manage.py runserver
之后在瀏覽器中打開http://127.0.0.1:8000/加入一些商品到購物車,提交訂單,當按下PLACE ORDER按鈕后,訂單信息被保存進數據庫,訂單ID被附加到session上,然后進入支付頁面。
支付頁面從session中取得訂單id然后在iframe中渲染Hosted Fields,像下圖所示:
可以看一下頁面的HTML代碼,從而理解什么是Hosted Fields。
針對沙盒測試環境,Braintree提供了一些測試用的信用卡資料,可以進行付款成功或失敗的測試,可以在https://developers.braintreepayments.com/guides/credit-cards/testing-go-live/python找到,我們來使用4111 1111 1111 1111
這個信用卡號,在CVV碼中填入123
,到期日期填入未來的某一天比如12/20
:
之后點擊Pay,應該可以看到成功頁面:
說明付款已經成功。可以在https://sandbox.braintreegateway.com/login登錄,然后在左側菜單選Transaction里搜索最近的交易,可以看到如下信息:
譯者注:Braintree網站在成書后有部分改版,讀者看到的支付詳情頁面可能與上述圖片有一些區別。
然后再查看管理站點http://127.0.0.1:8000/admin/orders/order/中的對應記錄,該訂單應該已經被標記為已支付,而且記錄了交易ID,如下圖所示:
我們現在就成功集成了支付功能。
1.5正式上線
在沙盒環境中測試通過之后,需要正式上線的話,需要到https://www.braintreepayments.com創建正式賬戶。
在部署到生產環境時,需要將settings.py
中的商戶ID和公鑰私鑰更新為正式賬戶的對應信息,然后將其中的Environment.Sandbox
修改為Environment.Production
。正式上線的具體步驟可以參考:https://developers.braintreepayments.com/start/go-live/python。
2導出訂單為CSV文件
有時我們想將某個模型中的數據導出為一個文件,用於在其他系統導入。常用的一種數據交換格式是CSV(逗號分隔數據)文件。CSV文件是一個純文本文件,包含很多條記錄。通常一行是一條記錄,用特定的分隔符(一般是逗號)分隔每個字段的值。我們准備自定義管理后台,增加導出CSV文件的功能。
2.1給管理后台增加自定義管理行為(actions)
Django允許對管理后台的很多內容進行自定義修改。我們准備在列出具體數據的視圖內增加導出CSV文件的功能。
一個管理行為是指如下操作:用戶從管理后台列出某個模型中具體記錄的頁面內,使用復選框選中要操作的記錄,然后從下拉菜單中選擇一項操作,之后就會針對所有選中的記錄執行操作。這個action的位置如下圖所示:
創建自定義管理行為可以讓管理員批量對記錄進行操作。
可以通過寫一個符合要求的自定義函數作為一項管理行為,這個函數要接受如下參數:
- 當前顯示的
ModelAdmin
類 - 當前的request對象,是一個
HttpResponse
實例 - 用戶選中的內容組成的QuerySet
在選中一個action選項然后點擊旁邊的Go按鈕的時候,該函數就會被執行。
我們就准備在下拉action清單里增加一項導出CSV數據的功能,為此先來修改orders
應用中的admin.py
文件,將下列代碼加在OrderAdmin
類定義之前:
import csv
import datetime
from django.http import HttpResponse
def export_to_csv(modeladmin, request, queryset):
opts = modeladmin.model._meta
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename={}.csv'.format(opts.verbose_name)
writer = csv.writer(response)
fields = [field for field in opts.get_fields() if not field.many_to_many and not field.one_to_many]
writer.writerow(field.verbose_name for field in fields)
for obj in queryset:
data_row = []
for field in fields:
value = getattr(obj, field.name)
if isinstance(value, datetime.datetime):
value = value.strftime('%d/%m/%Y')
data_row.append(value)
writer.writerow(data_row)
return response
export_to_csv.short_description = 'Export to CSV'
在這個函數里我們做了如下事情:
- 創建一個
HttpResponse
對象,將其內容類型設置為text/csv
,以告訴瀏覽器將其視為一個CSV文件。還為請求頭附加了Content-Disposition
頭部信息,告訴瀏覽器這個請求帶有一個附加文件。 - 創建一個CSV的
writer
對象,用於向Http響應對象response
中寫入CSV文件數據。 - 通過
_meta
的get_fields()
方法獲取所有字段名,動態獲取model
的字段,排除了所有一對多和多對多字段。 - 將字段名寫入到響應的CSV數據中,作為第一行數據,即表頭
- 迭代QuerySet,將其中每一個對象的數據寫入一行中,注意特別對
datetime
采用了格式化功能,以轉換成字符串。 - 最后設置了該函數對象的
short_description
屬性,該屬性的值為在action列表中顯示的功能名稱。
這樣我們就創建了一個通用的管理功能,可以操作任何ModelAdmin
對象。
之后在OrderAdmin
類中增加這個新的export_to_csv
功能:
class OrderAdmin(admin.ModelAdmin):
WeasyPrint # ...
actions = [export_to_csv]
在瀏覽器中打開http://127.0.0.1:8000/admin/orders/order/查看訂單類,頁面如下:
選擇一些訂單,然后選擇上邊的Export to CSV功能,然后點擊Go按鈕,瀏覽器就會下載一個order.csv
文件。
譯者注:此處下載的文件名可能不是order.csv
,這是因為原書沒有在orders
應用的models.py
中為Order
類的meta
類增加verbose_name
屬性,手工增加verboser_name
的值為order
,這樣才能下載到和原書里寫的名稱一樣的order.csv
文件。
使用文本編輯器打開剛下載的CSV文件,可以看到里邊的內容類似:
ID,first name,last name,email,address,postal
code,city,created,updated,paid,braintree id
3,Antonio,Melé,antonio.mele@gmail.com,Bank Street,WS
J11,London,25/02/2018,25/02/2018,True,2bwkx5b6
可以看到,實現管理功能的方法很直接。Django中將數據輸出為CSV的說明可以參考https://docs.djangoproject.com/en/2.0/howto/outputting-csv/。
3用自定義視圖擴展管理后台的功能
不僅僅是配置ModelAdmin
,創建管理行為和覆蓋內置模板,有時候可能需要對管理后台進行更多的自定義。這時你需要創建自定義的管理視圖。使用管理視圖,就可以實現自己想要的功能,要注意的只是自定義管理視圖應該只允許管理員進行操作,同時繼承內置模板以保持風格一致性。
我們這次來修改一下管理后台,增加一個自定義的功能用於顯示一個訂單的信息。修改orders
應用中的views.py
文件,增加如下內容:
from django.contrib.admin.views.decorators import staff_member_required
from django.shortcuts import get_object_or_404
from .models import Order
@staff_member_required
def admin_order_detail(request, order_id):
order = get_object_or_404(Order, id=order_id)
return render(request, 'admin/orders/order/detail.html', {'order': order})
@staff_member_required
裝飾器只允許is_staff
和is_active
字段同時為True
的用戶才能使用被裝飾的視圖。在這個視圖中,通過傳入的id取得對應的Order
對象。
然后配置orders
應用的urls.py
文件,增加一條路由:
path('admin/order/<int:order_id>/', views.admin_order_detail, name='admin_order_detail')
然后在order應用的templates/目錄下創建如下文件目錄結構:
admin/
orders/
order/
detail.html
編輯這個detail.html
,添加下列代碼:
{% extends "admin/base_site.html" %}
{% load static %}
{% block extrastyle %}
<link rel="stylesheet" type="text/css" href="{% static "css/admin.css" %}"/>
{% endblock %}
{% block title %}
Order {{ order.id }} {{ block.super }}
{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url "admin:index" %}">Home</a> ›
<a href="{% url "admin:orders_order_changelist" %}">Orders</a>
›
<a href="{% url "admin:orders_order_change" order.id %}">Order {{ order.id }}</a>
› Detail
</div>
{% endblock %}
{% block content %}
<h1>Order {{ order.id }}</h1>
<ul class="object-tools">
<li>
<a href="#" onclick="window.print();">Print order</a>
</li>
</ul>
<table>
<tr>
<th>Created</th>
<td>{{ order.created }}</td>
</tr>
<tr>
<th>Customer</th>
<td>{{ order.first_name }} {{ order.last_name }}</td>
</tr>
<tr>
<th>E-mail</th>
<td><a href="mailto:{{ order.email }}">{{ order.email }}</a></td>
</tr>
<tr>
<th>Address</th>
<td>{{ order.address }}, {{ order.postal_code }} {{ order.city }}</td>
</tr>
<tr>
<th>Total amount</th>
<td>${{ order.get_total_cost }}</td>
</tr>
<tr>
<th>Status</th>
<td>{% if order.paid %}Paid{% else %}Pending payment{% endif %}</td>
</tr>
</table>
<div class="module">
<div class="tabular inline-related last-related">
<table>
<caption>Items bought</caption>
<thead>
<tr>
<th>Product</th>
<th>Price</th>
<th>Quantity</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{% for item in order.items.all %}
<tr class="row{% cycle "1" "2" %}">
<td>{{ item.product.name }}</td>
<td class="num">${{ item.price }}</td>
<td class="num">{{ item.quantity }}</td>
<td class="num">${{ item.get_cost }}</td>
</tr>
{% endfor %}
<tr class="total">
<td colspan="3">Total</td>
<td class="num">${{ order.get_total_cost }}</td>
</tr>
</tbody>
</table>
</div>
</div>
{% endblock %}
這個模板用於在管理后台顯示訂單詳情。模板繼承了admin/base_site.html
母版,這個母版包含Django管理站點的基礎結構和CSS類,然后還加載了自定義的樣式表css/admin.css
。
自定義CSS樣式表在隨書代碼中,像之前的項目一樣將其復制到對應目錄。
我們使用的塊名稱都定義在母版中,在其中編寫了展示訂單詳情的部分。
當你需要繼承Django的內置模板時,必須了解內置模板的結構,在https://github.com/django/django/tree/2.1/django/contrib/admin/templates/admin可以找到內置模板的信息。
如果需要覆蓋內置模板,需要將自己編寫的模板命名為與原來模板相同,然后復制到templates
下,設置與內置模板相同的相對路徑和名稱。管理后台就會優先使用當前項目下的模板。
最后,還需要再管理后台中為每個Order
對象增加一個鏈接到我們自行編寫的視圖,編輯orders
應用的admin.py
文件,在OrderAdmin
類之前增加如下代碼:
from django.urls import reverse
from django.utils.safestring import mark_safe
def order_detail(obj):
return mark_safe('<a href="{}">View</a>'.format(reverse('orders:admin_order_detail', args=[obj.id])))
這個函數接受一個Order對象作為參數,返回一個解析后的admin_order_detail
名稱對應的URL,由於Django默認會將HTML代碼轉義,所以加上mark_safe
。
使用mark_safe可以不讓HTML代碼轉義。使用mark_safe的時候,確保對於用戶的輸入依然要進行轉義,以防止跨站腳本攻擊。
然后編輯OrderAdmin
類來顯示鏈接:
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ['id', 'first_name', 'last_name', 'email', 'address', 'postal_code', 'city', 'paid', 'created',
'updated', order_detail]
然后啟動站點,訪問http://127.0.0.1:8000/admin/orders/order/,可以看到新增了一列:
點擊任意一個訂單的View鏈接查看詳情,會進入Django管理后台風格的訂單詳情頁:
4動態生成PDF發票
我們現在已經實現了完整的結賬和支付功能,可以為每個訂單生成一個PDF發票。有很多Python庫都可以用來生成PDF,常用的是Reportlab庫,該庫也是django 2.0官方推薦使用的庫,可以在https://docs.djangoproject.com/en/2.0/howto/outputting-pdf/查看詳情。
大部分情況下,PDF文件中都要包含一些樣式和格式,這個時候通過渲染后的HTML模板生成PDF更加方便。我們在Django中集成一個模塊來按照上述方法轉換PDF。這里我們使用WeasyPrint庫,這個庫用來從HTML模板生成PDF文件。
4.1安裝WeasyPrint
需要先按照http://weasyprint.org/docs/install/#platforms的指引安裝相關依賴,之后通過pip
安裝WeasyPrint:
pip install WeasyPrint==0.42.3
譯者注:WeasyPrint在Windows下的配置非常麻煩,還未必能夠成功,推薦Windows用戶使用Linux虛擬機。
4.2創建PDF模板
需要創建一個模板作為輸入給WeasyPrint的數據。我們創建一個帶有訂單內容和CSS樣式的模板,通過Django渲染,將最終生成的頁面傳給WeasyPrint。
在orders
應用的templates/orders/order/
目錄下創建pdf.html
文件,添加下列代碼:
<html>
<body>
<h1>My Shop</h1>
<p>
Invoice no. {{ order.id }}<br>
<span class="secondary">
{{ order.created|date:"M d, Y" }}
</span>
</p>
<h3>Bill to</h3>
<p>
{{ order.first_name }} {{ order.last_name }}<br>
{{ order.email }}<br>
{{ order.address }}<br>
{{ order.postal_code }}, {{ order.city }}
</p>
<h3>Items bought</h3>
<table>
<thead>
<tr>
<th>Product</th>
<th>Price</th>
<th>Quantity</th>
<th>Cost</th>
</tr>
</thead>
<tbody>
{% for item in order.items.all %}
<tr class="row{% cycle "1" "2" %}">
<td>{{ item.product.name }}</td>
<td class="num">${{ item.price }}</td>
<td class="num">{{ item.quantity }}</td>
<td class="num">${{ item.get_cost }}</td>
</tr>
{% endfor %}
<tr class="total">
<td colspan="3">Total</td>
<td class="num">${{ order.get_total_cost }}</td>
</tr>
</tbody>
</table>
<span class="{% if order.paid %}paid{% else %}pending{% endif %}">
{% if order.paid %}Paid{% else %}Pending payment{% endif %}
</span>
</body>
</html>
譯者注:注意第五行標紅的部分,原書錯誤的寫成了</br>
。
這個模板的內容很簡單,使用一個<table>
元素展示訂單的用戶信息和商品信息,還添加了消息顯示訂單是否已支付。
4.3創建渲染PDF的視圖
我們來創建在管理后台內生成訂單PDF文件的視圖,在orders
應用的views.py
文件內增加下列代碼:
from django.conf import settings
from django.http import HttpResponse
from django.template.loader import render_to_string
import weasyprint
@staff_member_required
def admin_order_pdf(request, order_id):
order = get_object_or_404(Order, id=order_id)
html = render_to_string('orders/order/pdf.html', {'order': order})
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'filename="order_{}"'.format(order.id)
weasyprint.HTML(string=html).write_pdf(response, stylesheets=[weasyprint.CSS(settings.STATIC_ROOT + 'css/pdf.css')])
return response
這是生成PDF文件的視圖,使用了@staff_member_required
裝飾器使該視圖只能由管理員訪問。通過ID獲取Order對象,然后使用render_to_string()
方法渲染orders/order/pdf.html
,渲染后的模板以字符串形式保存在html
變量中。然后創建一個新的HttpResponse
對象,並為其附加application/pdf
和Content-Disposition
請求頭信息。使用WeasyPrint從字符串形式的HTML中轉換PDF文件並寫入HttpResponse
對象。這個生成的PDF會帶有STATIC_ROOT
路徑下的css/pdf.css
中的樣式,最后返回響應。
如果發現文件缺少CSS樣式,記得把CSS文件從隨書目錄中放入shop
應用的static/
目錄下。
我們這里還沒有配置STATIC_ROOT
變量,這個變量規定了項目的靜態文件存放的路徑。編輯myshop
項目的settings.py
文件,添加下面這行:
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
然后運行如下命令:
python manage.py collectstatic
之后會看到:
120 static files copied to 'code/myshop/static'.
這個命令會把所有已經激活的應用下的static/
目錄中的文件,復制到STATIC_ROOT
指定的目錄中。每個應用都可以有自己的static/目錄存放靜態文件,還可以在STATICFILES_DIRS
中指定其他的靜態文件路徑,當執行collectstatic
時,會把所有STATICFILES_DIRS
目錄內的文件都復制過來。如果再次執行collectstatic
,會提示是否需要覆蓋已經存在的靜態文件。
譯者注:雖然將靜態文件分開存放在每個應用的static/
下可以正常運行開發中的站點,但在正式上線的最好統一靜態文件的存放地址,以方便配置Web服務程序。
編輯orders
應用的urls.py
文件,為視圖配置URL:
urlpatterns = [
# ...
path('admin/order/<int:order_id>/pdf/', views.admin_order_pdf, name='admin_order_pdf'),
]
像導出CSV一樣,我們要在管理后台的展示頁面中增加一個鏈接到這個視圖的URL。打開orders
應用的admin.py
文件,在OrderAdmin
類之前增加:
def order_pdf(obj):
return mark_safe('<a href="{}">PDF</a>'.format(reverse('orders:admin_order_pdf', args=[obj.id])))
order_pdf.short_description = 'Invoice'
如果指定了short_description
屬性,Django就會用該屬性的值作為列名。
為OrderAdmin
的list_display
增加這個新的字段order_pdf
:
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ['id', 'first_name', 'last_name', 'email', 'address', 'postal_code', 'city', 'paid', 'created',
'updated', order_detail, order_pdf]
在瀏覽器中打開http://127.0.0.1:8000/admin/orders/order/,可以看到新增了一列字段用於轉換PDF:
點擊PDF鏈接,瀏覽器應該會下載一個PDF文件,如果是尚未支付的訂單,樣式如下:
已經支付的訂單,則類似這樣:
4.4使用電子郵件發送PDF文件
當支付成功的時,我們發送帶有PDF發票的郵件給用戶。編輯payment
應用中的views.py
視圖,添加如下導入語句:
from django.template.loader import render_to_string
from django.core.mail import EmailMessage
from django.conf import settings
import weasyprint
from io import BytesIO
在payment_process
視圖中,order.save()
這行之后,以相同的縮進添加下列代碼:
def payment_process(request):
# ......
if request.method == "POST":
# ......
if result.is_success:
# ......
order.save()
# 創建帶有PDF發票的郵件
subject = 'My Shop - Invoice no. {}'.format(order.id)
message = 'Please, find attached the invoice for your recent purchase.'
email = EmailMessage(subject, message, 'admin@myshop.com', [order.email])
# 生成PDF文件
html = render_to_string('orders/order/pdf.html', {'order': order})
out = BytesIO()
stylesheets = [weasyprint.CSS(settings.STATIC_ROOT + 'css/pdf.css')]
weasyprint.HTML(string=html).write_pdf(out, stylesheets=stylesheets)
# 附加PDF文件作為郵件附件
email.attach('order_{}.pdf'.format(order.id), out.getvalue(), 'application/pdf')
# 發送郵件
email.send()
return redirect('payment:done')
else:
return redirect('payment:canceled')
else:
# ......
這里使用了EmailMessage
類創建一個郵件對象email
,然后將模板渲染到html
變量中,然后通過WeasyPrint將其寫入一個BytesIO
二進制字節對象,之后使用attach
方法,將這個字節對象的內容設置為EmailMessage
的附件,同時設置文件類型為PDF,最后發送郵件。
記得在settings.py
中設置SMTP服務器,可以參考第二章。
現在,可以嘗試完成一個新的付款,並且在郵箱內接收PDF發票。
總結
在這一章中,我們集成了支付網關,自定義了Django管理后台,還學習了如何將數據以CSV文件格式導出和動態生成PDF文件。
下一章我們將深入了解Django項目的國際化和本地化設置,還將創建一個優惠碼功能和商品推薦引擎。