8 管理支付和訂單
在上一章中邦马,你創(chuàng)建了一個(gè)包括商品目錄和訂單系統(tǒng)的在線商店贱鼻。你還學(xué)習(xí)了如何用Celery啟動(dòng)異步任務(wù)宴卖。在這一章中,你會(huì)學(xué)習(xí)如何在網(wǎng)站中集成支付網(wǎng)關(guān)邻悬。你還會(huì)擴(kuò)展管理站點(diǎn)症昏,用于管理訂單和導(dǎo)出不同格式的訂單。
我們會(huì)在本章覆蓋以下知識(shí)點(diǎn):
- 在項(xiàng)目中集成支付網(wǎng)關(guān)
- 管理支付通知
- 導(dǎo)出訂單到CSV文件中
- 為管理站點(diǎn)創(chuàng)建自定義視圖
- 動(dòng)態(tài)生成PDF單據(jù)
8.1 集成支付網(wǎng)關(guān)
支付網(wǎng)關(guān)允許你在線處理支付父丰。你可以使用支付網(wǎng)關(guān)管理用戶訂單肝谭,以及通過(guò)可靠的,安全的第三方代理處理支付蛾扇。這意味著你不用考慮在自己的系統(tǒng)中存儲(chǔ)信用卡攘烛。
有很多支付網(wǎng)關(guān)可供選擇。我們將集成PayPal镀首,它是最流行的支付網(wǎng)關(guān)之一医寿。
PayPal提供了幾種方法在網(wǎng)站中集成它的網(wǎng)關(guān)。標(biāo)準(zhǔn)集成包括一個(gè)Buy now
按鈕蘑斧,你可能在其它網(wǎng)站見過(guò)。這個(gè)按鈕把顧客重定向到PayPal來(lái)處理支付须眷。我們將在網(wǎng)站中集成包括一個(gè)自定義Buy now
按鈕的PayPal Payments Standard
竖瘾。PayPal會(huì)處理支付,并發(fā)送一條支付狀態(tài)的信息到我們的服務(wù)器花颗。
8.1.1 創(chuàng)建PayPal賬戶
你需要一個(gè)PayPal商家賬戶捕传,才能在網(wǎng)站中集成支付網(wǎng)關(guān)。如果你還沒(méi)有PayPal賬戶扩劝,在這里注冊(cè)庸论。確保你選擇了商家賬戶。
在注冊(cè)表單填寫詳細(xì)信息完成注冊(cè)棒呛。PayPal會(huì)給你發(fā)送一封郵件確認(rèn)賬戶聂示。
8.1.2 安裝django-paypal
django-paypal
是一個(gè)第三方Django應(yīng)用,可以簡(jiǎn)化在Django項(xiàng)目中集成PayPal簇秒。我們將用它在我們的商店中集成PayPal Payments Standard
鱼喉。你可以在這里查看django-paypal的文檔。
在終端使用以下命令安裝django-paypal:
pip install django-paypal
編輯項(xiàng)目的settings.py
文件趋观,在INSTALLED_APPS
設(shè)置中添加paypal.standard.ipn
:
INSTALLED_APPS = [
# ...
'paypal.standard.ipn',
]
這個(gè)應(yīng)用是django-paypal提供的扛禽,通過(guò)Instant Payment Notification(IPN)
集成PayPal Payments Standard
。我們之后會(huì)處理支付通知皱坛。
在myshop
的settings.py
文件添加以下設(shè)置來(lái)配置django-paypal:
# django-paypal settings
PAYPAL_RECEIVER_EMAIL = 'mypaypalemail@myshop.com'
PAYPAL_TEST = True
這些設(shè)置分別是:
-
PAYPAL_RECEIVER_EMAIL
:你PayPal賬戶的郵箱地址编曼。用你創(chuàng)建PayPal賬戶的郵箱替換mypaypalemail@myshop.com
。 -
PAYPAL_TEST
:一個(gè)布爾值剩辟,表示是否用PayPal的Sandbox環(huán)境處理支付掐场。在遷移到生產(chǎn)環(huán)境之前往扔,你可以用Sandbox測(cè)試PayPal集成。
打開終端執(zhí)行以下命令刻肄,同步django-paypal的模型到數(shù)據(jù)庫(kù)中:
python manage.py migrate
你會(huì)看到類似這樣結(jié)尾的輸出:
Running migrations:
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
現(xiàn)在django-paypal的模型已經(jīng)同步到數(shù)據(jù)庫(kù)中瓤球。你還需要添加django-paypal的URL模式到項(xiàng)目中。編輯myshop
項(xiàng)目的主urls.py
文件敏弃,并添加以下URL模式卦羡。記住,把它放在shop.urls
模式之前麦到,避免錯(cuò)誤的模式匹配:
url(r'^paypal/', include('paypal.standard.ipn.urls')),
讓我們把支付網(wǎng)關(guān)添加到結(jié)賬過(guò)程中绿饵。
8.1.3 添加支付網(wǎng)關(guān)
結(jié)賬流程是這樣的:
- 用戶添加商品到購(gòu)物車中。
- 用戶結(jié)賬購(gòu)物車瓶颠。
- 重定向用戶到PayPal進(jìn)行支付拟赊。
- PayPal發(fā)送支付通知到我們的服務(wù)器。
- PayPal重定向用戶返回我們的網(wǎng)站粹淋。
使用以下命令在項(xiàng)目中創(chuàng)建一個(gè)新應(yīng)用:
python manage.py startapp payment
我們將使用這個(gè)應(yīng)用管理結(jié)賬流程和用戶支付吸祟。
編輯項(xiàng)目的settings.py
文件,在INSTALLED_APP
設(shè)置中添加payment
:
INSTALLED_APPS = [
# ...
'paypal.standard.ipn',
'payment',
]
現(xiàn)在payment
應(yīng)用已經(jīng)在項(xiàng)目中激活了桃移。編輯orders
應(yīng)用的views.py
文件屋匕,添加以下導(dǎo)入:
from django.shortcuts import render, redirect
from django.core.urlresolvers import reverse
找到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)
request.session['order_id'] = order.id
return redirect(reverse('payment:process'))
創(chuàng)建訂單成功之后,我們用order_id
會(huì)話鍵在當(dāng)前會(huì)話中設(shè)置訂單ID借杰。然后我們把用戶重定向到接下來(lái)會(huì)創(chuàng)建的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 paypal.standard.forms import PayPalPaymentsForm
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)
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})
在payment_process
視圖中蔗衡,我們生成了一個(gè)自定義PayPal的Buy now
按鈕用于支付纤虽。首先我們從order_id
會(huì)話鍵中獲得當(dāng)前訂單,這個(gè)鍵值之前在order_create
視圖中設(shè)置過(guò)绞惦。我們獲得指定ID的Order
對(duì)象逼纸,并創(chuàng)建了包括以下字段的PayPalPaymentForm
:
-
business
:處理支付的PayPal商家賬戶。在這里我們使用PAYPAL_RECEIVER_EMAIL
設(shè)置中定義的郵箱賬戶济蝉。 -
amount
:向顧客收取的總價(jià)樊展。 -
item_name
:出售的商品名。我們使用商品ID堆生,因?yàn)橛唵卫锟赡馨ǘ鄠€(gè)商品专缠。 -
invoice
:?jiǎn)螕?jù)ID。每次支付對(duì)應(yīng)的這個(gè)ID應(yīng)用是唯一的淑仆。我們使用訂單ID涝婉。 -
currency_code
:這次支付的貨幣。我們?cè)O(shè)置為USD
使用美元蔗怠。使用與PayPal賬戶中設(shè)置的相同貨幣(EUR
對(duì)應(yīng)歐元)墩弯。 -
notify_url
:PayPal發(fā)送IPN請(qǐng)求到這個(gè)URL吩跋。我們使用django-paypal提供的paypal-ipn
URL。這個(gè)URL關(guān)聯(lián)的視圖處理負(fù)責(zé)支付通知和在數(shù)據(jù)庫(kù)中保存支付通知渔工。 -
return_url
:支付成功后重定向用戶到這個(gè)URL锌钮。我們使用之后會(huì)創(chuàng)建的payment:done
URL。 -
cancel_return
:如果支付取消引矩,或者遇到其它問(wèn)題梁丘,重定向用戶到這個(gè)URL。我們使用之后會(huì)創(chuàng)建的payment:canceled
URL旺韭。
PayPalPaymentForm
會(huì)被渲染為帶隱藏字典的標(biāo)準(zhǔn)表單氛谜,用戶只能看到Buy now
按鈕。點(diǎn)用戶點(diǎn)擊這個(gè)按鈕区端,表單會(huì)通過(guò)POST提交到PayPal值漫。
讓我們創(chuàng)建一個(gè)簡(jiǎn)單的視圖,當(dāng)支付完成织盼,或者因?yàn)槟承┰蛉∠Ц堆詈危孭ayPal重定向用戶。在同一個(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')
因?yàn)镻ayPal可以通過(guò)POST重定向用戶到這些視圖的任何一個(gè)沥邻,所以我們用csrf_exempt
裝飾器避免Django期望的CSRF令牌危虱。在payment
應(yīng)用目錄中創(chuà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'),
]
這些是支付流程的URL谋国。我們包括了以下URL模式:
-
process
:用于生成帶Buy now
按鈕的PayPal表單的視圖 -
done
:當(dāng)支付成功后,用于PayPal重定向用戶 -
canceled
:當(dāng)支付取消后迁沫,用于PayPal重定向用戶
編輯myshop
項(xiàng)目的主urls.py
文件芦瘾,引入payment
應(yīng)用的URL模式:
url(r'^payment/', include('payment.urls', namespace='payment')),
記住把它放在shop.urls
模式之前,避免錯(cuò)誤的模式匹配集畅。
在payment
應(yīng)用目錄中創(chuàng)建以下文件結(jié)構(gòu):
templates/
payment/
process.html
done.html
canceled.html
編輯payment/process.html
模板近弟,添加以下代碼:
{% extends "shop/base.html" %}
{% block title %}Pay using PayPal{% endblock title %}
{% block content %}
<h1>Pay using PayPal</h1>
{{ form.render }}
{% endblock content %}
這個(gè)模板用于渲染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 content %}
用戶支付成功后祷愉,會(huì)重定向到這個(gè)模板頁(yè)面。
編輯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 content %}
處理支付遇到問(wèn)題二鳄,或者用戶取消支付時(shí),會(huì)重定向到這個(gè)模板頁(yè)面媒怯。
讓我們嘗試完整的支付流程订讼。
8.1.4 使用PayPal的Sandbox
在瀏覽器中打開http://developer.paypal.com
,并用你的PayPal商家賬戶登錄扇苞。點(diǎn)擊Dashboard
菜單項(xiàng)欺殿,然后點(diǎn)擊Sandbox
下的Accounts
選項(xiàng)寄纵。你會(huì)看到你的sandbox測(cè)試賬戶列表,如下圖所示:
最初脖苏,你會(huì)看到一個(gè)商家賬戶和一個(gè)PayPal自動(dòng)生成的個(gè)人測(cè)試賬戶程拭。你可以點(diǎn)擊Create Account
按鈕創(chuàng)建新的sandbox測(cè)試賬戶。
點(diǎn)擊列表中Type
為PERSONAL
的賬戶棍潘,然后點(diǎn)擊Pofile
鏈接恃鞋。你會(huì)看到測(cè)試賬戶的信息,包括郵箱地址和個(gè)人資料信息蜒谤,如下圖所示:
在Funding
標(biāo)簽頁(yè)中山宾,你會(huì)看到銀行賬戶,信用卡數(shù)據(jù)鳍徽,以及PayPal貸方余額资锰。
當(dāng)你的網(wǎng)站使用sandbox環(huán)境時(shí),測(cè)試賬戶可以用來(lái)處理支付阶祭。導(dǎo)航到Profile
標(biāo)簽頁(yè)绷杜,然后點(diǎn)擊修改Change password
鏈接。為這個(gè)測(cè)試賬戶創(chuàng)建一個(gè)自定義密碼濒募。
在終端執(zhí)行python manage.py runserver
命令啟動(dòng)開發(fā)服務(wù)器鞭盟。在瀏覽器中打開http://127.0.0.1:8000/
,添加一些商品到購(gòu)物車中瑰剃,然后填寫結(jié)賬表單齿诉。當(dāng)你點(diǎn)擊Place order
按鈕時(shí),訂單會(huì)存儲(chǔ)到數(shù)據(jù)庫(kù)中晌姚,訂單ID會(huì)保存在當(dāng)前會(huì)話中粤剧,然后會(huì)重定向到支付處理頁(yè)面。這個(gè)頁(yè)面從會(huì)話中獲得訂單挥唠,并渲染帶Buy now
按鈕的PayPal表單抵恋,如下圖所示:
譯者注:啟動(dòng)開發(fā)服務(wù)器后,還需要啟動(dòng)RabbitMQ和Celery宝磨,因?yàn)槲覀円盟鼈儺惒桨l(fā)送郵件弧关,否則會(huì)拋出異常。
你可以看一眼HTML源碼唤锉,查看生成的表單字段世囊。
點(diǎn)擊Buy now
按鈕。你會(huì)被重定向到PayPal窿祥,如下圖所示:
輸入顧客測(cè)試賬號(hào)的郵箱地址和密碼茸习,然后點(diǎn)擊登錄按鈕。你會(huì)被重定向到以下頁(yè)面:
譯者注:即之前修改過(guò)密碼的個(gè)人賬戶壁肋。
現(xiàn)在點(diǎn)擊立即付款
按鈕号胚。最后籽慢,你會(huì)看到一個(gè)包括交易ID的確認(rèn)頁(yè)面,如下圖所示:
點(diǎn)擊返回商家
按鈕猫胁。你會(huì)被重定向到PayPalPaymentForm
的return_url
字段指定的URL箱亿。這是payment_done
視圖的URL,如下圖所示:
支付成功弃秆!但是因?yàn)槲覀冊(cè)诒镜剡\(yùn)行項(xiàng)目届惋,127.0.0.1不是一個(gè)公網(wǎng)IP,所以PayPal不能給我們的應(yīng)用發(fā)送支付狀態(tài)通知菠赚。我們接下來(lái)學(xué)習(xí)如何讓我們的網(wǎng)站可以從Internet訪問(wèn)脑豹,從而接收IPN通知。
8.1.5 獲得支付通知
IPN是大部分支付網(wǎng)關(guān)都會(huì)提供的方法衡查,用于實(shí)時(shí)跟蹤購(gòu)買瘩欺。當(dāng)網(wǎng)關(guān)處理完一個(gè)支付后,會(huì)立即給你的服務(wù)器發(fā)送一個(gè)通知拌牲。該通知包括所有支付細(xì)節(jié)俱饿,包括狀態(tài)和用于確認(rèn)通知來(lái)源的支付簽名。這個(gè)通知作為獨(dú)立的HTTP請(qǐng)求發(fā)送到你的服務(wù)器塌忽。出現(xiàn)問(wèn)題的時(shí)候拍埠,PayPal會(huì)多次嘗試發(fā)送通知。
django-payapl
自帶兩個(gè)不同的IPN信號(hào)土居,分別是:
-
valid_ipn_received
:當(dāng)從PayPal接收的IPN消息是正確的枣购,并且不會(huì)與數(shù)據(jù)庫(kù)中現(xiàn)在消息重復(fù)時(shí)觸發(fā) -
invalid_ipn_received
:當(dāng)從PayPal接收的消息包括無(wú)效數(shù)據(jù)或者格式不對(duì)時(shí)觸發(fā)
我們將創(chuàng)建一個(gè)自定義接收函數(shù),并把它連接到valid_ipn_received
信號(hào)來(lái)確認(rèn)支付擦耀。
在payment
應(yīng)用目錄中創(chuàng)建signals.py
文件棉圈,并添加以下代碼:
from django.shortcuts import get_object_or_404
from paypal.standard.models import ST_PP_COMPLETED
from paypal.standard.ipn.signals import valid_ipn_received
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ù)是這樣工作的:
- 我們接收
sender
對(duì)象埂奈,它是在paypal.standard.ipn.models
中定義的PayPalPN
模型的一個(gè)實(shí)例迄损。 - 我們檢查
paypal_status
屬性定躏,確保它等于django-paypal的完成狀態(tài)账磺。這個(gè)狀態(tài)表示支付處理成功。 - 接著我們用
get_object_or_404
快捷函數(shù)獲得訂單痊远,這個(gè)訂單的ID必須匹配我們提供給PayPal的invoice
參數(shù)垮抗。 - 我們?cè)O(shè)置訂單的
paid
屬性為True
,標(biāo)記訂單狀態(tài)為已支付碧聪,并把Order
對(duì)象保存到數(shù)據(jù)庫(kù)中冒版。
當(dāng)valid_ipn_received
信號(hào)觸發(fā)時(shí),你必須確保信號(hào)模塊已經(jīng)加載逞姿,這樣接收函數(shù)才會(huì)被調(diào)用辞嗡。最好的方式是在包括它們的應(yīng)用加載的時(shí)候捆等,加載你自己的信號(hào)⌒遥可以通過(guò)定義一個(gè)自定義的應(yīng)用配置來(lái)實(shí)現(xiàn)栋烤,我們會(huì)在下一節(jié)中講解。
8.1.6 配置我們的應(yīng)用
你已經(jīng)在第六章學(xué)習(xí)了應(yīng)用配置挺狰。我們將為payment
應(yīng)用定義一個(gè)自定義配置明郭,用來(lái)加載我們的信號(hào)接收函數(shù)。
在payment
應(yīng)用目錄中創(chuàng)建apps.py
文件丰泊,并添加以下代碼:
from django.apps import AppConfig
class PaymentConfig(AppConfig):
name = 'payment'
verbose_name = 'Payment'
def ready(self):
# improt signal handlers
import payment.signals
在這段代碼中薯定,我們?yōu)?code>payment應(yīng)用定義了一個(gè)AppConfig
類。name
參數(shù)是應(yīng)用的名字瞳购,verbose_name
是一個(gè)可讀的名字话侄。我們?cè)?code>ready()方法中導(dǎo)入信號(hào)模板,確保應(yīng)用初始化時(shí)會(huì)加載信號(hào)模塊苛败。
編輯payment
應(yīng)用的__init__.py
文件满葛,并添加這一行代碼:
default_app_config = 'payment.apps.PaymentConfig'
這會(huì)讓Django自動(dòng)加載你的自定義應(yīng)用配置類。你可以在這里閱讀更多關(guān)于應(yīng)用配置的信息罢屈。
8.1.7 測(cè)試支付通知
因?yàn)槲覀冊(cè)诒镜丨h(huán)境開發(fā)嘀韧,所以我們需要讓PayPal可以訪問(wèn)我們的網(wǎng)站。有幾個(gè)應(yīng)用程序可以讓開發(fā)環(huán)境通過(guò)Internet訪問(wèn)缠捌。我們將使用Ngrok锄贷,是最流行的之一。
從這里下載你的操作系統(tǒng)版本的Ngrok曼月,并使用以下命令運(yùn)行:
./ngrok http 8000
這個(gè)命令告訴Ngrok在8000端口為你的本地主機(jī)創(chuàng)建一個(gè)鏈路谊却,并為它分配一個(gè)Internet可訪問(wèn)的主機(jī)名。你可以看到類似這樣的輸入:
Session Status online
Account lakerszhy (Plan: Free)
Update update available (version 2.2.4, Ctrl-U to update)
Version 2.1.18
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding http://c0f17d7c.ngrok.io -> localhost:8000
Forwarding https://c0f17d7c.ngrok.io -> localhost:8000
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
Ngrok告訴我們哑芹,我們網(wǎng)站使用的Django開發(fā)服務(wù)器在本機(jī)的8000端口運(yùn)行炎辨,現(xiàn)在可以通過(guò)http://c0f17d7c.ngrok.io
和https://c0f17d7c.ngrok.io
(分別對(duì)應(yīng)HTTP和HTTPS協(xié)議)在Internet上訪問(wèn)。Ngrok還提供了一個(gè)網(wǎng)頁(yè)URL聪姿,這個(gè)網(wǎng)頁(yè)顯示發(fā)送到這個(gè)服務(wù)器的信息碴萧。在瀏覽器中打開Ngrok提供的URL,比如http://c0f17d7c.ngrok.io
末购。在購(gòu)物車中添加一些商品破喻,下單,然后用PayPal測(cè)試賬戶支付盟榴。此時(shí)曹质,PayPal可以訪問(wèn)payment_process
視圖中PayPalPaymentForm
的notify_url
字段生成的URL。如果你查看渲染的表單,你會(huì)看類似這樣的HTML表單:
<input id="id_notify_url" name="notify_url" type="hidden" value="http://c0f17d7c.ngrok.io/paypal/">
完成支付處理后羽德,在瀏覽器中打開http://127.0.0.1:8000/admin/ipn/paypalipn/
几莽。你會(huì)看到一個(gè)IPN
對(duì)象,對(duì)應(yīng)狀態(tài)是Completed
的最新一筆支付宅静。這個(gè)對(duì)象包括支付的所有信息银觅,它由PayPal發(fā)送到你提供給IPN通知的URL。
譯者注:如果通過(guò)
http://c0f17d7c.ngrok.io
訪問(wèn)在線商店坏为,則需要在項(xiàng)目的settings.py
文件的ALLOWED_HOSTS
設(shè)置中添加c0f17d7c.ngrok.io
究驴。
譯者注:我在后臺(tái)看到的一直都是
Pending
狀態(tài),一直沒(méi)有找出原因匀伏。哪位朋友知道的話洒忧,請(qǐng)給我留言左胞,謝謝碘举。
你也可以在這里使用PayPal的模擬器發(fā)送IPN。模擬器允許你指定通知的字段和類型柒室。
除了PayPal Payments Standard
履磨,PayPal還提供了Website Payments Pro
蛉抓,它是一個(gè)訂購(gòu)服務(wù),可以在你的網(wǎng)站接收支付剃诅,而不用重定向到PayPal巷送。你可以在這里查看如何集成Website Payments Pro
。
8.2 導(dǎo)出訂單到CSV文件
有時(shí)你可能希望把模型中的信息導(dǎo)出到文件中矛辕,然后把它導(dǎo)入到其它系統(tǒng)中笑跛。其中使用最廣泛的格式是Comma-Separated Values(CSV)
。CSV文件是一個(gè)由若干條記錄組成的普通文本文件聊品。通常一行包括一條記錄和一些定界符號(hào)飞蹂,一般是逗號(hào),用于分割記錄的字段翻屈。我們將自定義管理站點(diǎn)陈哑,讓它可以到處訂單到CSV文件。
8.2.1 在管理站點(diǎn)你添加自定義操作
Django提供了大量自定義管理站點(diǎn)的選項(xiàng)伸眶。我們將修改對(duì)象列表視圖惊窖,在其中包括一個(gè)自定義的管理操作。
一個(gè)管理操作是這樣工作的:用戶在管理站點(diǎn)的對(duì)象列表頁(yè)面用復(fù)選框選擇對(duì)象赚抡,然后選擇一個(gè)在所有選中選項(xiàng)上執(zhí)行的操作爬坑,最后執(zhí)行操作纠屋。下圖顯示了操作位于管理站點(diǎn)的哪個(gè)位置:
創(chuàng)建自定義管理操作允許工作人員一次在多個(gè)元素上進(jìn)行操作涂臣。
你可以編寫一個(gè)常規(guī)函數(shù)來(lái)創(chuàng)建自定義操作,該函數(shù)需要接收以下參數(shù):
- 當(dāng)前顯示的
ModelAdmin
- 當(dāng)前請(qǐng)求對(duì)象——一個(gè)
HttpRequest
實(shí)例 - 一個(gè)用戶選中對(duì)象的
QuerySet
當(dāng)在管理站點(diǎn)觸發(fā)操作時(shí),會(huì)執(zhí)行這個(gè)函數(shù)赁遗。
我們將創(chuàng)建一個(gè)自定義管理操作署辉,來(lái)下載一組訂單的CSV文件。編輯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'
在這段代碼中執(zhí)行了以下任務(wù):
- 我們創(chuàng)建了一個(gè)
HttpResponse
實(shí)例哭尝,其中包括定制的text/csv
內(nèi)容類型,告訴瀏覽器該響應(yīng)看成一個(gè)CSV文件剖煌。我們還添加了Content-Disposition
頭部材鹦,表示HTTP響應(yīng)包括一個(gè)附件。 - 我們創(chuàng)建了CSV的
writer
對(duì)象耕姊,用于向response
對(duì)象中寫入數(shù)據(jù)桶唐。 - 我們用模型的
_meta
選項(xiàng)的get_fields()
方法動(dòng)態(tài)獲得模型的字段。我們派出了對(duì)多對(duì)和一對(duì)多關(guān)系茉兰。 - 我們用字段名寫入標(biāo)題行尤泽。
- 我們迭代給定的
QuerySet
,并為QuerySet
返回的每個(gè)對(duì)象寫入一行數(shù)據(jù)规脸。因?yàn)镃SV的輸出值必須為字符串坯约,所以我們格式化datetime
對(duì)象。 - 我們?cè)O(shè)置函數(shù)的
short_description
屬性莫鸭,指定這個(gè)操作在模板中顯示的名字闹丐。
我們創(chuàng)建了一個(gè)通用的管理操作,可以添加到所有ModelAdmin
類上被因。
最后妇智,如下添加export_to_csv
管理操作到OrderAdmin
類上:
calss OrderAdmin(admin.ModelAdmin):
# ...
actions = [export_to_csv]
在瀏覽器中打開http://127.0.0.1:8000/admin/orders/order/
,管理操作如下圖所示:
選中幾條訂單氏身,然后在選擇框中選擇Export to CSV
操作巍棱,接著點(diǎn)擊Go
按鈕。你的瀏覽器會(huì)下載生成的order.csv
文件蛋欣。用文本編輯器打開下載的文件航徙。你會(huì)看到以下格式的內(nèi)容,其中包括標(biāo)題行陷虎,以及你選擇的每個(gè)Order
對(duì)象行:
ID,first name,last name,email,address,postal code,city,created,updated,paid
1,allen,iverson,lakerszhy@gmail.com,北京市朝陽(yáng)區(qū),100012,北京市,11/05/2017,11/05/2017,False
2,allen,kobe,lakerszhy@gmail.com,北京市朝陽(yáng)區(qū),100012,北京市,11/05/2017,11/05/2017,False
正如你所看到的到踏,創(chuàng)建管理操作非常簡(jiǎn)單。
8.3 用自定義視圖擴(kuò)展管理站點(diǎn)
有時(shí)尚猿,你可能希望通過(guò)配置ModelAdmin
窝稿,創(chuàng)建管理操作和覆寫管理目標(biāo)來(lái)定制管理站點(diǎn)。這種情況下凿掂,你需要?jiǎng)?chuàng)建自定義的管理視圖伴榔。使用自定義視圖纹蝴,可以創(chuàng)建任何你需要的功能。你只需要確保只有工作人員能訪問(wèn)你的視圖踪少,以及讓你的模板繼承自管理模板來(lái)維持管理站點(diǎn)的外觀塘安。
讓我們創(chuàng)建一個(gè)自定義視圖,顯示訂單的相關(guān)信息援奢。編輯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)求這個(gè)頁(yè)面的用戶的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):
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 extrastyle %}
{% block title %}
Order {{ order.id }} {{ block.super }}
{% endblock title %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url "admin:index" %}">Home</a> $rsaquo;
<a href="{% url "admin:orders_order_changelist" %}">Orders</a> $rsaquo;
<a href="{% url "admin:orders_order_change" order.id %}">Order {{ order.id }}</a>
$rsaquo; Detail
</div>
{% endblock breadcrumbs %}
{% 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 content %}
這個(gè)模板用于在管理站點(diǎn)顯示訂單詳情。模板擴(kuò)展自Django管理站點(diǎn)的admin/base_site.html
模板栽连,其中包括主HTML結(jié)構(gòu)和管理站的CSS樣式险领。我們加載自定義的靜態(tài)文件css/admin.css
。
為了使用靜態(tài)文件秒紧,我們可以從本章的示例代碼中獲得它們绢陌。拷貝orders
應(yīng)用的static/
目錄中的靜態(tài)文件熔恢,添加到你項(xiàng)目中的相同位置脐湾。
我們使用父模板中定義的塊引入自己的內(nèi)容。我們顯示訂單信息和購(gòu)買的商品叙淌。
當(dāng)你想要擴(kuò)展一個(gè)管理模板時(shí)秤掌,你需要了解它的結(jié)構(gòu),并確定它存在哪些塊鹰霍。你可以在這里查看所有管理模板闻鉴。
如果需要,你也可以覆蓋一個(gè)管理模板茂洒。把要覆蓋的模板拷貝到templates
目錄中孟岛,保留一樣的相對(duì)路徑和文件。Django的管理站點(diǎn)會(huì)使用你自定義的模板代替默認(rèn)模板督勺。
最后渠羞,讓我們?yōu)楣芾碚军c(diǎn)的列表顯示頁(yè)中每個(gè)Order
對(duì)象添加一個(gè)鏈接。編輯orders
應(yīng)用的amdin.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
這個(gè)函數(shù)接收一個(gè)Order
對(duì)象作為參數(shù)次询,并返回一個(gè)admin_order_detail
的HTML鏈接。默認(rèn)情況下瓷叫,Django會(huì)轉(zhuǎn)義HTML輸出屯吊。我們必須設(shè)置函數(shù)的allow_tags
屬性為True
送巡,從而避免自動(dòng)轉(zhuǎn)義。
在任何
Model
方法雌芽,ModelAdmin
方法,或者可調(diào)用函數(shù)中設(shè)置allow_tags
屬性為True
可以避免HTML轉(zhuǎn)義辨嗽。使用allow_tags
時(shí)世落,確保轉(zhuǎn)義用戶的輸入,以避免跨站點(diǎn)腳本糟需。
然后編輯OrderAdmin
類來(lái)顯示鏈接:
class OrderAdmin(admin.ModelAdmin):
list_display = [... order_detail]
在瀏覽器中打開http://127.0.0.1:8000/admin/orders/order/
屉佳,現(xiàn)在每行都包括一個(gè)View
鏈接,如下圖所示:
點(diǎn)擊任何一個(gè)訂單的View
鏈接洲押,會(huì)加載自定義的訂單詳情頁(yè)面武花,如下圖所示:
8.4 動(dòng)態(tài)生成PDF單據(jù)
我們現(xiàn)在已經(jīng)有了完成的結(jié)賬和支付系統(tǒng),可以為每個(gè)訂單生成PDF單據(jù)了杈帐。有幾個(gè)Python庫(kù)可以生成PDF文件体箕。一個(gè)流行的生成PDF文件的Python庫(kù)是Reportlab。你可以在這里查看如果使用Reportlab輸出PDF文件挑童。
大部分情況下累铅,你必須在PDF文件中添加自定義樣式和格式。你會(huì)發(fā)現(xiàn)站叼,讓Python遠(yuǎn)離表現(xiàn)層娃兽,渲染一個(gè)HTML模板,然后把它轉(zhuǎn)換為PDF文件更加方便尽楔。我們將采用這種方法投储,在Django中用模塊生成PDF文件。我們會(huì)使用WeasyPrint阔馋,它是一個(gè)Python庫(kù)玛荞,可以從HTML模板生成PDF文件。
8.4.1 安裝WeasyPrint
首先呕寝,為你的操作系統(tǒng)安裝WeasyPrint的依賴冲泥,請(qǐng)?jiān)L問(wèn)這里。
然后用以下命令安裝WeasyPrint:
pip install WeasyPrint
8.4.2 創(chuàng)建PDF模板
我們需要一個(gè)HTML文檔作為WeasyPrint的輸入壁涎。我們將創(chuàng)建一個(gè)HTML模板凡恍,用Django渲染它,然后把它傳遞給WeasyPrint生成PDF文件怔球。
在orders
應(yīng)用的templates/orders/order/
目錄中創(chuàng)建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單據(jù)的模板。在這個(gè)模板中竟坛,我們顯示所有訂單詳情和一個(gè)包括商品的HTML的<table>
元素闽巩。我們還包括一個(gè)消息钧舌,顯示訂單是否支付。
8.4.3 渲染PDF文件
我們將創(chuàng)建一個(gè)視圖涎跨,在管理站點(diǎn)中生成已存在訂單的PDF單據(jù)洼冻。編輯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
這個(gè)視圖用于生成訂單的PDF單據(jù)隅很。我們用staff_member_required
裝飾器確保只有工作人員可以訪問(wèn)這個(gè)視圖撞牢。我們用給定的ID獲得Order
對(duì)象,并用Django提供的render_to_string()
函數(shù)渲染orders/order/pdf.html
文件叔营。被渲染的HTML保存在html
變量中屋彪。然后,我們生成一個(gè)新的HttpResponse
對(duì)象绒尊,指定application/pdf
內(nèi)容類型畜挥,并用Content-Disposition
指定文件名。我們用WeasyPrint從被渲染的HTML代碼生成一個(gè)PDF文件婴谱,并把文件寫到HttpResponse
對(duì)象中蟹但。我們用css/pdf.css
靜態(tài)文件為生成的PDF文件添加CSS樣式。我們從STATIC_ROOT
設(shè)置中的本地路徑加載它谭羔。最后返回生成的響應(yīng)矮湘。
因?yàn)槲覀冃枰褂?code>STATIC_ROOT設(shè)置,所以需要把它添加到我們項(xiàng)目中口糕。這是項(xiàng)目的靜態(tài)文件存放的路徑缅阳。編輯myshop
項(xiàng)目的settings.py
文件,添加以下設(shè)置:
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
接著執(zhí)行python manage.py collectstatic
命令景描。你會(huì)看到這樣結(jié)尾的輸出:
You have requested to collect static files at the destination
location as specified in your settings:
/Users/lakerszhy/Documents/GitHub/Django-By-Example/code/Chapter 8/myshop/static
This will overwrite existing files!
Are you sure you want to do this?
輸入yes
并按下Enter
十办。你會(huì)看到一條消息,顯示靜態(tài)文件已經(jīng)拷貝到STATIC_ROOT
目錄中超棺。
collectstatic
命令拷貝應(yīng)用中所有靜態(tài)文件到STATIC_ROOT
設(shè)置中定義的目錄向族。這樣每個(gè)應(yīng)用可以在static/
目錄中包括靜態(tài)文件。你還可以在STATICFILES_DIRS
設(shè)置中提供其它靜態(tài)文件源棠绘。執(zhí)行collectstatic
命令時(shí)件相,STATICFILES_DIRS
中列出的所有目錄都會(huì)被拷貝到STATIC_ROOT
目錄中。
編輯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)在夜矗,我們可以編輯管理列表顯示頁(yè)面,為Order
模型的每條記錄添加一個(gè)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
屬性中,如下所示:
class OrderAdmin(admin.ModelAdmin):
list_display = [..., order_detail, order_pdf]
如果你為可調(diào)用對(duì)象指定了short_description
屬性赡突,Django將把它作為列名对扶。
在瀏覽器中打開http://127.0.0.1:8000/admin/orders/order
区赵。每行都會(huì)包括一個(gè)PDF鏈接,如下圖所示:
點(diǎn)擊任意一條訂單的PDF鏈接浪南。你會(huì)看到生成的PDF文件笼才,下圖是未支付的訂單:
已支付訂單如下圖所示:
8.4.4 通過(guò)郵件發(fā)送PDF文件
當(dāng)收到支付時(shí)络凿,讓我們給顧客發(fā)送一封包括PDF單據(jù)的郵件骡送。編輯payment
應(yīng)用的signals.py
文件了赵,并添加以下導(dǎo)入:
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
然后在order.save()
行之后添加以下代碼淹父,保持相同的縮進(jìn):
# 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()
在這個(gè)信號(hào)中,我們用Django提供的EmailMessage
類創(chuàng)建了一個(gè)郵件對(duì)象怎虫。然后把模板渲染到html
變量中暑认。我們從渲染的模板中生成PDF文件困介,并把它輸出到一個(gè)BytesIO
實(shí)例(內(nèi)存中的字節(jié)緩存)中。接著我們用EmailMessage
對(duì)象的attach()
方法蘸际,把生成的PDF文件和out
緩存中的內(nèi)容添加到EmailMessage
對(duì)象中座哩。
記得在項(xiàng)目settings.py
文件中設(shè)置發(fā)送郵件的SMTP
設(shè)置,你可以參考第二章粮彤。
現(xiàn)在打開Ngrok提供的應(yīng)用URL根穷,完成一筆新的支付,就能在郵件中收到PDF單據(jù)了导坟。
8.5 總結(jié)
在這一章中屿良,你在項(xiàng)目中集成了支付網(wǎng)關(guān)。你自定義了Django管理站點(diǎn)惫周,并學(xué)習(xí)了如果動(dòng)態(tài)生成CSV和PDF文件尘惧。
下一章會(huì)深入了解Django項(xiàng)目的國(guó)際化和本地化。你還會(huì)創(chuàng)建一個(gè)優(yōu)惠券系統(tǒng)和商品推薦引擎递递。