點(diǎn)我查看本文集的說明及目錄朝抖。
本項(xiàng)目相關(guān)內(nèi)容包括:
實(shí)現(xiàn)過程:
CH8 支付和訂單管理
上一章娜谊,我們新建了一個(gè)包含商品目錄和訂單系統(tǒng)的基礎(chǔ)在線網(wǎng)站涧至。我們還學(xué)習(xí)了使用 Celery 加載異步任務(wù)。本章退个,我們將學(xué)習(xí)如何在網(wǎng)站中集成支付網(wǎng)站募壕,還將擴(kuò)展 admin網(wǎng)站來管理訂單并輸出不同格式的訂單。
本章包含以下內(nèi)容:
- 在項(xiàng)目中集成支付網(wǎng)關(guān)
- 管理支付通知
- 將訂單輸出到 CSV 文件
- 為 admin網(wǎng)站創(chuàng)建自定義視圖
- 自動(dòng)生成 PDF發(fā)貨單
集成支付網(wǎng)關(guān)
支付網(wǎng)關(guān)可以實(shí)現(xiàn)用戶在線支付语盈。支付網(wǎng)關(guān)可以用來管理用戶訂單并將付款過程委托給可靠司抱、安全的第三方。這意味著我們不需要在自己的系統(tǒng)中存儲(chǔ)信用卡黎烈。
我們可以選擇很多支付網(wǎng)關(guān)供應(yīng)商提供的服務(wù)。這里將集成最流行的支付網(wǎng)關(guān) Paypal匀谣。
PayPal 提供幾種集成網(wǎng)關(guān)的方法照棋。標(biāo)準(zhǔn)集成方法包括一個(gè) Buy now 按鈕(我們?cè)谄渌W(wǎng)站看到過),按鈕將用戶重定向到 PayPal 處理支付過程武翎。我們將在網(wǎng)站集成包含自定義 Buy now 按鈕的 Paypal 標(biāo)準(zhǔn)支付烈炭。Paypal 將處理支付過程并向我們的服務(wù)器發(fā)送支付狀態(tài)通知。
創(chuàng)建PayPal賬號(hào)
在網(wǎng)站中集成 Paypal 需要 Paypal 商家賬戶宝恶,如果你沒有 Paypal 賬戶符隙,請(qǐng)?jiān)?https://www.paypal.com/c2/home 創(chuàng)建商家賬戶,如下圖所示:
筆者注:
原文需要確保創(chuàng)建的是商家賬戶并選擇 Paypal 標(biāo)準(zhǔn)解決方案垫毙,但筆者注冊(cè)過程中沒有選擇 Paypal 標(biāo)準(zhǔn)方案的選項(xiàng)霹疫,因此只是進(jìn)行了商家賬戶注冊(cè)。
在注冊(cè)表單中填入詳細(xì)信息并完成注冊(cè)综芥, Paypal 將向你發(fā)送郵件來驗(yàn)證賬戶丽蝎。
安裝 django-paypal
Django-Paypal 是簡(jiǎn)化集成 Paypal 的第三方應(yīng)用。我們使用它在商店中集成 Paypal 支付標(biāo)準(zhǔn)解決方案膀藐。django-paypal 的文檔請(qǐng)查閱 http://django-paypal.readthedocs.io/en/stable/屠阻。
在 shell 中運(yùn)行以下命令來安裝 django-paypal:
pip install django-paypal
編輯項(xiàng)目的 settings.py 文件在 INSTALLED_APPS 中添加 'paypal.standard.ipn' :
INSTALLED_APPS = ['django.contrib.admin', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.staticfiles',
'shop', 'cart', 'orders', 'paypal.standard.ipn']
這是 django-paypal 提供的集成 Paypal 支付標(biāo)準(zhǔn)和即時(shí)支付通知(IPN) 的應(yīng)用红省。我們稍后再處理付款通知。
在 myshop 的settings.py 文件中為 django-paypal 進(jìn)行以下配置:
# django-paypal settings
PAYPAL_RECEIVER_EMAIL = 'mypaypalemail@myshop.com'
PAYPAL_TEST = True
這些設(shè)置包括:
PAYPAL_RECEIVER_EMAIL: 你的 Paypal 賬戶郵箱国觉。使用創(chuàng)建 Paypal 賬戶時(shí)的郵箱代替 'mypaypalemail@myshop.com 吧恃。
PAYPAL_TEST:表示是否使用 Paypal 的沙盒環(huán)境來處理付款的布爾值。沙盒允許用戶在遷移到生產(chǎn)環(huán)境之前測(cè)試 PayPal 集成麻诀。
打開 shell 并運(yùn)行以下命令來同步 django-paypal 的模型:
python manage.py migrate
你應(yīng)該可以看到這樣的輸出:
Applying ipn.0001_initial... OK
Applying ipn.0002_paypalipn_mp_id... OK
Applying ipn.0003_auto_20141117_1647... OK
Applying ipn.0004_auto_20150612_1826... OK
Applying ipn.0005_auto_20151217_0948... OK
Applying ipn.0006_auto_20160108_1112... OK
Applying ipn.0007_auto_20160219_1135... OK
Applying orders.0001_initial... OK
django-paypal 的模型現(xiàn)在已經(jīng)同步到數(shù)據(jù)庫了痕寓。我們還需要將 django-paypal 的 URL 模式添加到項(xiàng)目中。編輯 myshop 目錄下的 urls.py 文件并添加以下 URL模式针饥。記得將其放在 shop.urls 模式之前以避免錯(cuò)誤的模式匹配:
url(r'paypal/',include('paypal.standard.ipn.urls')),
接下來厂抽,我們將支付網(wǎng)關(guān)添加到結(jié)算過程。
添加支付網(wǎng)關(guān)
結(jié)算過程這樣工作:
將商品添加到購物車丁眼;
結(jié)算購物車中的商品筷凤;
重定向到 Paypal 進(jìn)行支付;
Paypal 向網(wǎng)站發(fā)送支付通知苞七;
Paypal 將用戶重定向到網(wǎng)站藐守。
使用以下命令在項(xiàng)目中新建一個(gè)應(yīng)用:
python manage.py startapp payment
我們使用這個(gè)應(yīng)用管理支付過程和用戶支付。
編輯項(xiàng)目的 settings.py 文件并將 payment 添加到INSTALLED_APPS中:
INSTALLED_APPS = ['django.contrib.admin', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.staticfiles',
'shop', 'cart', 'orders', 'paypal.standard.ipn', 'payment']
payment 應(yīng)用現(xiàn)在已經(jīng)激活了蹂风,編輯 orders 應(yīng)用的 views.py 文件并確認(rèn)包含下面的 import :
from django.urls import reverse
from django.shortcuts import render, redirect
將 order_create 視圖中的下列代碼:
# launch asynchronous task
order_created.delay(order.id)
return render(request, 'orders/order/created.html',
{'order': order})
修改為:
# launch asynchronous task
order_created.delay(order.id)
# set the order in the session
request.session['order_id'] = order.id
# redirect to the payment
return redirect(reverse('payment:process'))
成功創(chuàng)建一個(gè)訂單后卢厂,我們使用 order_id 會(huì)話關(guān)鍵詞將訂單 ID 設(shè)置到當(dāng)前會(huì)話中。然后惠啄,重定向到 ‘payment:process’ URL 慎恒。
編輯 payment 應(yīng)用的 views.py 文件并添加以下代碼:
from decimal import Decimal
from django.conf import settings
from django.core.urlresolvers import reverse
from django.shortcuts import render, get_object_or_404
from orders.models import Order
from paypal.standard.forms import PayPalPaymentsForm
# Create your views here.
def payment_process(request):
order_id = request.session.get('order_id')
order = get_object_or_404(Order, id=order_id)
host = request.get_host()
paypal_dict = {'business': settings.PAYPAL_RECEIVER_EMAIL,
'amount': '%.2f' % order.get_total_cost().quantize(Decimal('.01')),
'item_name': 'Order {}'.format(order.id), 'invoice': str(order.id),
'currency_code': 'USD',
'notify_url': 'http://{}{}'.format(host, reverse('paypal-ipn')),
'return_url': 'http://{}{}'.format(host, reverse('payment:done')),
'cancel_return': 'http://{}{}'.format(host,
reverse('payment:canceled')), }
form = PayPalPaymentsForm(initial=paypal_dict)
return render(request, 'payment/process.html',
{'order': order, 'form': form})
在 patment_process 視圖中,我們生成了一個(gè)自定義 Paypal Buy now 表單來支付訂單撵渡。首先融柬,根據(jù) order_id (剛剛在 order_create 視圖中添加的)從會(huì)話中獲得當(dāng)前訂單。獲得給定 ID 的 Order 對(duì)象并新建一個(gè)包含以下字段的 PayPalPaymentForm 表單:
- business:處理支付的 PayPal 商家賬號(hào)趋距。這里使用設(shè)置中定義的 PAYPAL_RECEIVER_EMAIL 郵箱賬戶粒氧。
- amount:用戶需要支付的總費(fèi)用。
- item_name:將要銷售的商品名稱节腐,由于訂單可能包含多種商品外盯,這里使用訂單 ID 。
- invoice: 發(fā)貨單 ID 翼雀。要求每筆支付的 invoice 唯一饱苟。這里我們使用訂單 ID 。
- currency_code:支付所用貨幣代碼锅纺。我們將其設(shè)置為表示美元的 USD 掷空。 需要與用戶賬戶中設(shè)置的貨幣相同。
- notify_url : Paypal 發(fā)送 IPN 請(qǐng)求的 URL 。我們使用 django-paypal 提供的 paypal-ipn URL 坦弟。 該 URL 對(duì)應(yīng)的視圖處理支付通知并保存到數(shù)據(jù)庫护锤。
- return_url:支付成功后,用戶跳轉(zhuǎn)到的頁面酿傍。我們使用后續(xù)將創(chuàng)建的 payment:done URL 烙懦。
- cancel_return: 支付取消時(shí),用戶跳轉(zhuǎn)到的頁面赤炒。我們使用后續(xù)將創(chuàng)建的 payment:cancel URL 氯析。
PayPalPaymentForm 渲染為包含隱藏字段的標(biāo)準(zhǔn)表單,用戶僅能看到 Buy Now 按鈕莺褒。當(dāng)用戶點(diǎn)擊按鈕時(shí)掩缓,表單將通過 POST 方式提交到 PayPal 。
接下來創(chuàng)建支付成功遵岩、支付取消時(shí)用戶跳轉(zhuǎn)到的簡(jiǎn)單視圖你辣。向同一個(gè) views.py 文件添加以下代碼:
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def payment_done(request):
return render(request, 'payment/done.html')
@csrf_exempt
def payment_canceled(request):
return render(request, 'payment/canceled.html')
由于 PayPal 通過 POST 方式將用戶重定向到上面的任意一個(gè)視圖,這里使用 csrf_exempt 裝飾器避免 Django 檢驗(yàn) CSRF 令牌尘执。在 payment 應(yīng)用目錄下新建名為 urls.py 的文件并添加以下代碼:
from django.conf.urls import url
from . import views
urlpatterns = [url(r'^process/$', views.payment_process, name='process'),
url(r'^done/$', views.payment_done, name='done'),
url(r'^canceled/$', views.payment_canceled, name='canceled'), ]
這是支付流程的 URLs舍哄。包含下面的 URL模式:
- process:為 Paypal 表單生成 Buy Now 按鈕的視圖;
- done:支付成功后誊锭,用戶跳轉(zhuǎn)到的頁面表悬;
- canceled:支付取消后,用戶跳轉(zhuǎn)到的頁面丧靡。
編輯 myshop 項(xiàng)目的 urls.py 文件并在 payment 應(yīng)用中包含下面的 URL 模式:
url(r'payment/',include('payment.urls')),
記得將其放在 shop.urls 模式之前以避免錯(cuò)誤的模式匹配蟆沫。
在 payment 應(yīng)用目錄下創(chuàng)建下面的文件結(jié)構(gòu):
編輯 payment/process.html 模板并添加以下代碼:
{% extends "shop/base.html" %}
{% block title %}Pay using PayPal{% endblock %}
{% block content %}
<h1>Pay using PayPal</h1>
{{ form.render }}
{% endblock %}
這是渲染 PayPalPaymentForm 并展示 Buy Now 按鈕的模板。
編輯 payment/done.html 模板并添加以下代碼:
{% extends "shop/base.html" %}
{% block content %}
<h1>Your payment was successful</h1>
<p>Your payment has been successfully received.</p>
{% endblock %}
這是用戶支付成功后跳轉(zhuǎn)到的頁面的模板温治。
編輯 payment/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 %}
這是用戶由于某些原因取消支付后跳轉(zhuǎn)到的頁面的模板饥追。
使用 PayPal 沙盒
在瀏覽器中打開 https://developer.paypal.com/ 并使用 Paypal 商家賬戶登錄。點(diǎn)擊 Dashboard 目錄項(xiàng)罐盔,并點(diǎn)擊左側(cè)目錄中 Sandbox 下的 accounts 選項(xiàng),應(yīng)該可以看到沙盒測(cè)試用戶列表:
我們可以看到 Paypal 自動(dòng)創(chuàng)建的商業(yè)賬戶和個(gè)人賬戶救崔。我們還可以使用 Create Accounts 按鈕創(chuàng)建新的沙盒測(cè)試賬戶惶看。
點(diǎn)擊列表中個(gè)人賬戶 Email Address ,會(huì)出現(xiàn) Profile 和 notifications 鏈接六孵,點(diǎn)擊 Profile 鏈接纬黎。你將看到測(cè)試賬戶的 e-mail 和用戶信息等消息:
在 Funding 選項(xiàng)中,我們可以找到銀行賬戶劫窒、信用卡信息和 Paypal 信用賬單本今。
測(cè)試賬戶可以使用沙盒環(huán)境在網(wǎng)站上進(jìn)行支付。回到 Profile 選項(xiàng)并點(diǎn)擊 Change password 鏈接冠息,為兩個(gè)測(cè)試賬戶創(chuàng)建自定義密碼挪凑。
打開 shell 使用 python manage.py runserver 命令啟動(dòng)開發(fā)服務(wù)器。在瀏覽器中打開 http://127.0.0.1:8000逛艰,向購物車中添加一些商品躏碳,并填寫結(jié)賬表單。當(dāng)你點(diǎn)擊 Place order 按鈕散怖,訂單將保存到數(shù)據(jù)庫菇绵,訂單 ID 將保存到當(dāng)前會(huì)話( session )中,你將重定向到支付頁面镇眷。這個(gè)頁面從會(huì)話中獲取訂單并將 Paypal 表單渲染為 Buy Now 按鈕:
我們可以通過 HTML 源碼查看生成的表單字段咬最。
筆者注:
這里一定要確保第七章中的 rabbitmq-server 和 celery 處于運(yùn)行狀態(tài)。
點(diǎn)擊 Buy Now 按鈕欠动,你將重定向到 PayPal 永乌,你將看到下面的頁面:
輸入沙盒中 buyer 賬戶的 e-mail 和密碼并點(diǎn)擊 Log In 按鈕,你將重定向到下面的頁面:
現(xiàn)在翁垂,點(diǎn)擊立即付款按鈕铆遭。最終,你將看到包含交易 ID 的確認(rèn)頁面沿猜,頁面看起來是這樣的:
點(diǎn)擊返回商家按鈕枚荣。你將重定向 PayPalPaymentsForm 表單的 return_url 字段指定的 URL。這是 payment_done 視圖的 URL啼肩。頁面看起來這樣:
支付已經(jīng)成功了橄妆。然而, PayPal 無法將支付狀態(tài)通知發(fā)送到我們的應(yīng)用祈坠。這是由于我們的項(xiàng)目運(yùn)行在外部無法訪問的 127.0.0.1 上害碾。下面我們將學(xué)習(xí)如何在互聯(lián)網(wǎng)上訪問我們的網(wǎng)站并能獲得 IPN 通知。
獲得支付通知
許多支付網(wǎng)關(guān)使用 IPN 提供的實(shí)時(shí)追蹤支付情況赦拘。網(wǎng)關(guān)處理完一個(gè)付款后會(huì)實(shí)時(shí)向網(wǎng)站服務(wù)器發(fā)送一個(gè)通知慌随。這個(gè)通知包含狀態(tài)、支付簽名(幫助我們確認(rèn)通知是否是原始通知)等所有支付細(xì)節(jié)躺同。網(wǎng)關(guān)使用獨(dú)立的 HTTP 請(qǐng)求向服務(wù)器發(fā)送這條通知阁猜。至于連接問題,Paypal 將多次嘗試通知網(wǎng)站蹋艺。
django-paypal 應(yīng)用包含兩種 IPNs 信號(hào)剃袍,這些信號(hào)包括:
valid_ipn_received: 從 Paypal 接收到的消息正確并且尚未保存到數(shù)據(jù)庫時(shí)觸發(fā)的信息
invalid_ipn_received: 從 Paypal 接收到無效數(shù)據(jù)或者格式不正確時(shí)觸發(fā)的消息
我們將創(chuàng)建一個(gè)自定義接收函數(shù)并將其連接到 valid_ipn_received 信號(hào)來確定支付。
在 payment 應(yīng)用目錄下新建一個(gè) signals 的文件夾捎谨,文件夾內(nèi)新建 __init__.py
和 handlers.py 的文件民效,并在 handlers.py 中包含以下代碼:
from django.shortcuts import get_object_or_404
from paypal.standard.ipn.signals import valid_ipn_received
from paypal.standard.models import ST_PP_COMPLETED
from orders.models import Order
def payment_notification(sender, **kwargs):
ipn_obj = sender
if ipn_obj.payment_status == ST_PP_COMPLETED:
# payment was successful
order = get_object_or_404(Order, id=ipn_obj.invoice)
# mark the order as paid
order.paid = True
order.save()
valid_ipn_received.connect(payment_notification)
這里將 payment_notification 接收函數(shù)連接到 django-paypal 提供的 valid_ipn_received 信號(hào)憔维,接收函數(shù)的工作原理為:
接收發(fā)送的對(duì)象,該對(duì)象是 paypal.standard.ipn.models 中定義的 PayPalIPN 模型的實(shí)例畏邢;
檢查 payment_status 屬性是否與 django-paypal 的完成屬性一致业扒。 django-paypal 的完成屬性表示支付成功;
使用快捷函數(shù) get_object_or_404() 獲得發(fā)送給 Paypal 的訂單對(duì)象棵红。
將訂單對(duì)象的 paid 屬性設(shè)置為 True 凶赁,表示支付成功。
我們需要確保加載了信號(hào)逆甜,這樣 valid_ipn_received 觸發(fā)時(shí)才能調(diào)用接收函數(shù)虱肄。加載信息的最佳實(shí)踐是加載包含信號(hào)的應(yīng)用。我們可以通過下一節(jié)講到的定義自定義應(yīng)用配置來實(shí)現(xiàn)上述功能交煞。
筆者注:
與第六章的配置 signals 時(shí)的原因相同咏窿。我們這里使用了 signals 文件夾。
配置應(yīng)用
我們已經(jīng)在第六章了解了如何配置應(yīng)用素征,現(xiàn)在為 payment 應(yīng)用設(shè)置自定義配置來加載我們的信息接收函數(shù)集嵌。
在我們的 payment 應(yīng)用目錄的 apps.py 中添加以下代碼:
from django.apps import AppConfig
class PaymentConfig(AppConfig):
name = 'payment'
def ready(self):
# import signal handlers
from .signals import handler
我們?cè)?ready 方法中加載信號(hào)模塊,從而在應(yīng)用初始化的過程中加載信號(hào)模塊御毅。
編輯 payment 應(yīng)用的__init__.py
文件并添加以下代碼:
default_app_config = 'payment.apps.AppConfig'
筆者注:
python 3.3 以上版本的模塊可以不使用
__init__.py
文件根欧,因此,最好在 INSTALLED_APPS 中使用 'payment.apps.AppConfig'端蛆,而不是在__init__.py
文件中定義凤粗。
這樣捕发,Django 將自動(dòng)加載自定義應(yīng)用配置類廊驼。關(guān)于應(yīng)用配置的更多信息詳見 https://docs.djangoproject.com/en/1.11/ref/applications/。
測(cè)試支付通知
由于在本地環(huán)境工作唧喉,我們需要確保網(wǎng)站可以被 Paypal 訪問呆躲。有一些應(yīng)用可以實(shí)現(xiàn)因特網(wǎng)訪問開發(fā)環(huán)境异逐,我們將使用最流行的 Ngrok 。
從 https://ngrok.com/ 為操作系統(tǒng)下載 Ngrok 并在 shell 中(Ngrok 所在的文件夾中)使用以下命令運(yùn)行:
./ngrok http 8000
這個(gè)命令告訴 Ngrok 在 8000 端口為本地主機(jī)創(chuàng)建一個(gè)通道并為其設(shè)置一個(gè)網(wǎng)絡(luò)可以訪問的主機(jī)名稱插掂。你看到的輸出應(yīng)該與下面的輸出類似:
Session Status online
Session Expires 7 hours, 59 minutes
Version 2.2.8
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding http://4c94fca6.ngrok.io -> localhost:8000
Forwarding https://4c94fca6.ngrok.io -> localhost:8000
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
Ngrok 告訴我們使用開發(fā)服務(wù)器運(yùn)行在本地 8000 端口的網(wǎng)站可以分別通過 http://4c94fca6.ngrok.io 和 https://4c94fca6.ngrok.io 在網(wǎng)絡(luò)上進(jìn)行訪問灰瞻。Ngrok 還提供一個(gè) URL 來訪問 web 接口(展示發(fā)送到服務(wù)器的請(qǐng)求消息)。
使用瀏覽器打開 Ngrok 提供的 URL(測(cè)試使用的是上面的 http://4c94fca6.ngrok.io )辅甥,向購物車添加幾件商品箩祥,下單,并使用 Paypal 測(cè)試賬戶進(jìn)行支付肆氓。這次,Paypal 將能夠訪問為 payment_process 視圖的 PaypalPaymentForm 的notify_url 字段生成的 URL 底瓣。如果你看下渲染的表單谢揪,你將看到HTML 表單字段是這樣的:
<input id="id_notify_url" name="notify_url" type="hidden" value="http://4c94fca6.ngrok.io/paypal/">
筆者注:
使用 http://4c94fca6.ngrok.io 需要將 ‘4c94fca6.ngrok.io’ 和 ‘127.0.0.1 加入到項(xiàng)目 settings.py 文件中的ALLOWED_HOSTS 中蕉陋。
完成支付過程后,在瀏覽器中打開http://127.0.0.1:8000/admin/ipn/paypalipn/拨扶。你將會(huì)看到一個(gè)上次支付的 IPN 對(duì)象凳鬓,它的狀態(tài)為 Completed 。 這個(gè)對(duì)象包含支付的所有信息患民,這些信息由 PayPal 發(fā)送你提供的接收 IPN 通知的 URL 缩举。IPN admin 列表頁面看起來是這樣的:
我們也可以使用 Paypal 的 IPN 仿真器加載 IPN ,仿真器地址為https://developer.paypal.com/developer/ipnSimulator/ 匹颤。仿真器可以指定字段和發(fā)送的通知類型仅孩。
除了 PayPal Payment Standard,PayPal 提供訂閱服務(wù) Website Payments Pro印蓖,它可以接受網(wǎng)站付款而無需將用戶重定向到PayPal辽慕。你可以在http://django-paypal.readthedocs.io/en/v0.4.1/pro/index.html了解如何集成 Website Payments Pro。
將訂單導(dǎo)出到 CSV 文件
有時(shí)赦肃,我們希望將模型中的數(shù)據(jù)導(dǎo)出到文件中溅蛉,以便于在其它系統(tǒng)中使用。導(dǎo)出/導(dǎo)入數(shù)據(jù)最常用的格式為CSV他宛。 CSV 文件是包含記錄的純文本文件船侧。文件中通常一行為一條記錄,記錄的字段通過一些分隔字符(通常使用逗號(hào))進(jìn)行分隔厅各。
向 admin網(wǎng)站添加自定義動(dòng)作
Django提供自定義 admin網(wǎng)站的一些選項(xiàng)镜撩。我們將為對(duì)象列表視圖增加一些自定義 admin動(dòng)作。
admin動(dòng)作的工作方式如下:用戶在 admin 對(duì)象列表頁面使用選擇框選擇對(duì)象讯检,然后選擇對(duì)選中對(duì)象執(zhí)行的操作琐鲁,最后執(zhí)行操作。下面的截圖展示了 admin 網(wǎng)站中動(dòng)作的位置人灼。
注意:
創(chuàng)建自定義 admin動(dòng)作可以幫助用戶同時(shí)對(duì)多個(gè)對(duì)象進(jìn)行操作围段。
我們可以通過創(chuàng)建接收以下參數(shù)的常規(guī)函數(shù)來創(chuàng)建自定義動(dòng)作:
- 需要展示的當(dāng)前 ModelAdmin;
- 當(dāng)前請(qǐng)求對(duì)象( HttpRequest 實(shí)例);
- 表示用戶選擇的對(duì)象的 QuerySet 投放;
我們將創(chuàng)建一個(gè)自定義 admin動(dòng)作來下載訂單列表奈泪。編輯 orders 應(yīng)用的 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]
# Write a first row with header information
writer.writerow([field.verbose_name for field in fields])
# Write data rows
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'
在上面的代碼中,我們實(shí)現(xiàn)了以下工作:
- 創(chuàng)建了一個(gè)包含自定義 text/csv 內(nèi)容類型的 HttpResponse 實(shí)例通知瀏覽器響應(yīng)是一個(gè) CSV文件灸芳。我們還添加了Content-Disposition 標(biāo)頭表示 HTTP 響應(yīng)包含一個(gè)附加文件涝桅;
- 創(chuàng)建了一個(gè) CSV writer 對(duì)象寫入 response 對(duì)象;
- 使用模型 _meta 選項(xiàng)的 get_fields() 方法動(dòng)態(tài)獲取 model 字段烙样。這里排除了多對(duì)多和一對(duì)多關(guān)系冯遂。
- 將字段名稱作為標(biāo)題行寫入文件;
- 對(duì)指定的 QuerySet 進(jìn)行迭代谒获,QuerySet 返回的每個(gè)對(duì)象為一行蛤肌;這里處理 datetime 格式是由于 CSV 的輸出值必須是字符串格式壁却。
- 通過設(shè)置函數(shù)的 short_description 屬性自定義模板中操作動(dòng)作的名稱。
我們已經(jīng)創(chuàng)建了一個(gè)可以添加到任意 ModelAdmin 類的通用 admin 動(dòng)作裸准。
最后展东,將新的 export_to_csv admin 動(dòng)作添加到 OrderAdmin 中,如下所示:
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]
actions = [export_to_csv]
在瀏覽器中打開http://127.0.0.1:8000/admin/orders/order/炒俱,將會(huì)看到以下頁面:
選中一些訂單并從動(dòng)作選擇框中選擇 Export to CSV 動(dòng)作并點(diǎn)擊 Go 按鈕盐肃,瀏覽器將下載名為 order.csv 的 CSV文件。使用文本編輯器打開下載的文件权悟。你應(yīng)該可以看到以下格式的內(nèi)容砸王,包含標(biāo)題行和選中的 Order 對(duì)象:
ID,first name,last name,email,address,postal code,city,created,updated,paid
1,***,***,***@163.com,*******,*****,****,24/02/2018,24/02/2018,True
我們可以發(fā)現(xiàn),創(chuàng)建 admin動(dòng)作非常簡(jiǎn)單僵芹。
使用自定義視圖擴(kuò)展 admin網(wǎng)站
有時(shí)处硬,我們需要自定義 admin網(wǎng)站的范圍會(huì)超出 ModelAdmin 配置的范圍,比如創(chuàng)建 admin動(dòng)作拇派、覆蓋 admin模板等荷辕。這種情況下,需要?jiǎng)?chuàng)建一個(gè)自定義 admin 視圖件豌〈剑可以使用自定義視圖來實(shí)現(xiàn)任何需要的功能。需要確認(rèn)的是只有員工才能訪問視圖以及通過擴(kuò)展 admin模塊維持 admin 的統(tǒng)一風(fēng)格茧彤。
我們創(chuàng)建自定義視圖來展示訂單的信息骡显。編輯 orders 應(yīng)用的 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 裝飾器檢查請(qǐng)求頁面的用戶的 is_active 和 is_staff 字段是否均為 True 。在這個(gè)視圖中曾掂,我們通過給定的 id 獲得 Order 對(duì)象并渲染一個(gè)模板來展示訂單惫谤。
現(xiàn)在,編輯 orders 應(yīng)用的 urls.py 文件并添加以下 URL 模式:
url(r'^admin/order/(?P<order_id>\d+)/$',
views.admin_order_detail, name='admin_order_detail'),
在 orders 應(yīng)用的 templates/ 目錄下創(chuàng)建如下的文件結(jié)構(gòu):
編輯 detail.html 模板并添加以下內(nèi)容:
{% 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>
<h2>Items bought</h2>
<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 網(wǎng)站展示訂單詳情的模板珠洗。模板擴(kuò)展 Django admin 網(wǎng)站 admin/base_site.html 模板溜歪,這個(gè)模板包含 admin 網(wǎng)站的 HTML 結(jié)構(gòu)和 CSS 樣式。我們加載了自定義靜態(tài)文件 css/admin.css许蓖。
為了使用靜態(tài)文件蝴猪,需要獲得本章代碼中的相應(yīng)靜態(tài)文件〔沧Γ拷貝 orders 應(yīng)用下的 static 目錄下的靜態(tài)文件并將它們添加到你的目錄中自阱。
我們使用父模板中定義的 blocks 來添加自定義內(nèi)容,包括展示訂單和訂單中的商品信息米酬。
擴(kuò)展 admin模板需要了解它的結(jié)構(gòu)并識(shí)別存在的 blocks 沛豌。我們可以從這里找到所有的 admin 模板https://github.com/django/django/tree/master/django/contrib/admin/templates/admin。
如果需要赃额,還可以覆蓋 admin模板加派。覆蓋 admin模板只需要將它拷貝到 templates 目錄下并保持相同的相對(duì)路徑和文件名阁簸。Django 的 admin網(wǎng)站將使用自定義模板覆蓋默認(rèn)模板。
最后哼丈,我們?yōu)?admin 網(wǎng)站列表頁面的每個(gè) Order 對(duì)象添加鏈接。編輯 orders 應(yīng)用的 admin.py 文件并在 OrderAdmin 類之前添加以下代碼:
from django.core.urlresolvers import reverse
def order_detail(obj):
return '<a href="{}">View</a>'.format(
reverse('orders:admin_order_detail', args=[obj.id]))
order_detail.allow_tags = True
這是輸入 Order 對(duì)象為參數(shù)并返回 admin_site_detail URL 鏈接的函數(shù)筛严。Django 默認(rèn)轉(zhuǎn)義 HTML 輸出醉旦。我們需要設(shè)置 這個(gè)可調(diào)用函數(shù)的 allow_tags 屬性為 True 來防止自動(dòng)轉(zhuǎn)義。
注意:
將 allow_tags 屬性設(shè)置為 True 將防止任意模型桨啃、ModelAdmin 方法及其他可調(diào)用函數(shù)的 HTML 轉(zhuǎn)義车胡。確保轉(zhuǎn)義用戶的輸入以防止跨網(wǎng)站腳本。
然后照瘾,編輯 Order Admin 網(wǎng)站展示鏈接:
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/匈棘,現(xiàn)在每行都增加了下面的 View 列:
點(diǎn)擊任意訂單的 View 鏈接加載自定義訂單詳情頁面。你應(yīng)該可以看到下面的頁面析命。
動(dòng)態(tài)生成PDF通知
現(xiàn)在已經(jīng)有了結(jié)算和支付系統(tǒng)主卫,我們還可以為每個(gè)訂單生成 PDF 發(fā)貨單。生成 PDF文件的 Python 庫有很多鹃愤,比較受歡迎的是 Reportlab 簇搅,我們可以從這里找到使用 Reportlab 輸出 PDF 文件的方法 https://docs.djangoproject.com/en/1.11/howto/outputting-pdf/。
大多數(shù)情況需要為 PDF 文件添加自定義樣式和格式软吐。我們可以發(fā)現(xiàn)渲染 HTML 模板然后將其轉(zhuǎn)換為 PDF更加方便瘩将,這樣可以使 Python 遠(yuǎn)離表示層 。我們將使用這個(gè)方法并使用一個(gè)模塊在 django 中生成 PDF 文件凹耙。我們將使用 WeasyPrint 姿现,WeasyPrint 是將 HTML 模板轉(zhuǎn)換為 PDF 文件的 Python 庫文件。
安裝 WeasyPrint
首先肖抱,從 http://weasyprint.readthedocs.io/en/latest/install.html 為系統(tǒng)安裝 WeasyPrint 依賴程序备典。
然后,使用 pip 安裝WeasyPrint:
pip install WeasyPrint
創(chuàng)建PDF模板
WeasyPrint 輸入一個(gè) HTML文件虐沥。我們將創(chuàng)建一個(gè) HTML 模板熊经,使用 Django 進(jìn)行渲染,然后將其傳入 WeasyPrint 生成 PDF 文件欲险。
在 orders 應(yīng)用的 templates/orders/order/ 目錄下新建一個(gè) 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>
這是 PDF 發(fā)貨單的模板。在模板中天试,我們展示了訂單詳情和一個(gè)訂單中的商品 <table>槐壳,以及訂單是否支付的信息。
渲染 PDF 文件
我們將使用 admin網(wǎng)站創(chuàng)建一個(gè)視圖來生成已有訂單的 PDF 發(fā)貨單喜每。編輯 orders 應(yīng)用的 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_{}.pdf"'.format(order.id)
weasyprint.HTML(string=html).write_pdf(response, stylesheets=[
weasyprint.CSS(settings.STATIC_ROOT + 'css/pdf.css')])
return response
這是生成訂單 PDF 發(fā)貨單的視圖务唐。 staff_member_required 裝飾器用來保證只有工作人員才能訪問這個(gè)視圖雳攘,通過給定的 ID 獲得 Order 對(duì)象,然后使用 Django 提供的 render_to_string() 函數(shù)渲染 orders/order/pdf.html 枫笛。渲染的 HTML 保存到 html 變量中吨灭,然后生成一個(gè) application/pdf 格式的 HttpResponse 對(duì)象,該對(duì)象包含指定文件名的 Content-Disposition 頭刑巧。我們使用 WeasyPrint 從渲染的 HTML 生成 PDF 文件并將文件寫到 HttpResponse 對(duì)象中喧兄。 css/pdf.css 為生成 PDF 文件添加 CSS 格式。這里使用 STATIC_ROOT 設(shè)置從本地路徑加載靜態(tài)文件啊楚。 最后吠冤,返回生成的響應(yīng)。
由于需要使用 STATIC_ROOT 設(shè)置恭理,我們將其添加到項(xiàng)目中拯辙,它是項(xiàng)目存放靜態(tài)文件的路徑。編輯 myshop 項(xiàng)目的 settings.py 文件并添加以下代碼:
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
然后颜价,運(yùn)行命令 python mange.py collectstatic 涯保。你應(yīng)該可以看到項(xiàng)目使用的所有的靜態(tài)文件都被拷貝到了 STATIC_ROOT 目錄下了。
collectstatic 命令將項(xiàng)目應(yīng)用的靜態(tài)文件拷貝到 STATIC_ROOT 設(shè)置的目錄下拍嵌。每個(gè)應(yīng)用都可以使用 static 目錄提供自己的靜態(tài)文件遭赂。此外,還可以在 STATICFILE_DIRS 設(shè)置的路徑中提供額外的靜態(tài)文件横辆。執(zhí)行 collectstatic 命令時(shí) STATICFILE_DIRS 指定的所有目錄都將被拷貝到 STATIC_ROOT 設(shè)置的目錄下撇他。
編輯 orders 應(yīng)用的 urls.py 文件并添加以下 URL 模式:
url(r'^admin/order/(?P<order_id>\d+)/pdf/$',
views.admin_order_pdf, name='admin_order_pdf'),
現(xiàn)在,我們可以編輯 Order 模型的admin 列表展示頁面來為條記錄添加生成 PDF 文件的鏈接狈蚤。編輯 orders 應(yīng)用的 admin.py 文件困肩,在 OrderAdmin 類之前添加以下代碼:
def order_pdf(obj):
return '<a href="{}">PDF</a>'.format(
reverse('orders:admin_order_pdf', args=[obj.id]))
order_pdf.allow_tags = True
order_pdf.short_description = 'PDF bill'
將 order_pdf 添加到 OrderAdmin 的 list_display 屬性中:
list_display = ['id', 'first_name', 'last_name', 'email', 'address',
'postal_code', 'city', 'paid', 'created', 'updated',
order_detail, order_pdf]
如果調(diào)用函數(shù)指定了 short_description,Django 將使用它作為列名脆侮,否則使用函數(shù)名锌畸。
在瀏覽器中打開 http://127.0.0.1:8000/admin/orders/order/,將會(huì)看到下面的頁面:
點(diǎn)擊任意訂單的 PDF 鏈接靖避,沒有付款的訂單將會(huì)顯示下面這樣的 PDF 文件:
已經(jīng)付款的訂單將會(huì)顯示下面這樣的 PDF 文件:
通過email 發(fā)送PDF文件
客戶付款后潭枣,我們向客戶發(fā)送包含 PDF 發(fā)貨單的郵件。編輯 payment 應(yīng)用 signals 中的 handlers.py 文件并導(dǎo)入下面庫文件:
from io import BytesIO
import weasyprint
from django.conf import settings
from django.core.mail import EmailMessage
from django.template.loader import render_to_string
然后幻捏,在 order.save() 后面添加下面的代碼:
# create invoice e-mail
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])
# generate 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)
# attach PDF file
email.attach('order_{}.pdf'.format(order.id), out.getvalue(),
'application/pdf')
# send e-mail
email.send()
這里盆犁,我們使用 Django 提供的 EmailMessage 類創(chuàng)建一個(gè) e-mail 對(duì)象,然后渲染模板并保存到 html 變量中篡九,并根據(jù)渲染的模板生成 PDF文件并輸出到內(nèi)存字節(jié)緩沖區(qū) BytesIO 實(shí)例中谐岁。這時(shí)我們可以使用 attach 方法將生成的 PDF文件以附件的形式放到 EmailMessage 對(duì)象中。
發(fā)送郵件需要在項(xiàng)目的 settings.py 文件中設(shè)置 SMTP, SMTP 配置在第二章中介紹過伊佃。
現(xiàn)在窜司,打開 Ngrok 為我們的應(yīng)用提供的 URL 并完成一個(gè)新的支付過程,從而接收 PDF 發(fā)貨單郵件航揉。
總結(jié)
本章塞祈,我們?cè)陧?xiàng)目中集成了支付網(wǎng)關(guān),自定義了 Django admin網(wǎng)站帅涂,并且學(xué)習(xí)了如何動(dòng)態(tài)生成 CSV 和 PDF 文件织咧。
下一章,我們將介紹 Django 項(xiàng)目的全球化和本地化漠秋,還將創(chuàng)建折扣系統(tǒng)和商品推薦引擎。