全文鏈接
第一章 創(chuàng)建一個blog應(yīng)用
第二章 使用高級特性來增強(qiáng)你的blog
第三章 擴(kuò)展你的blog應(yīng)用
第四章上 創(chuàng)建一個社交網(wǎng)站
第四章下 創(chuàng)建一個社交網(wǎng)站
第五章 在你的網(wǎng)站中分享內(nèi)容
第六章 跟蹤用戶動作
第七章 建立一個在線商店
第八章 管理付款和訂單
第九章上 擴(kuò)展你的商店
第九章下 擴(kuò)展你的商店
第十章上 創(chuàng)建一個在線學(xué)習(xí)平臺
第十章下 創(chuàng)建一個在線學(xué)習(xí)平臺
第十一章 緩存內(nèi)容
第十二章 構(gòu)建一個API
書籍出處:https://www.packtpub.com/web-development/django-example
原作者:Antonio Melé
(譯者注:還有4章!還有4章全書就翻譯完成了!)
第八章
管理付款和訂單
在上一章,你創(chuàng)建了一個基礎(chǔ)的在線商店包含一個產(chǎn)品列表以及訂單系統(tǒng)。你還學(xué)習(xí)了如何執(zhí)行異步的任務(wù)通過使用Celery。在這一章中,你會學(xué)習(xí)到如何集成一個支付網(wǎng)關(guān)(譯者注:支付網(wǎng)關(guān)(Payment Gateway)是銀行金融網(wǎng)絡(luò)系統(tǒng)和Internet網(wǎng)絡(luò)之間的接口,是由銀行操作的將Internet上傳輸?shù)臄?shù)據(jù)轉(zhuǎn)換為金融機(jī)構(gòu)內(nèi)部數(shù)據(jù)的一組服務(wù)器設(shè)備,或由指派的第三方處理商家支付信息和顧客的支付指令。以上是我百度的。)到你的站點中。你還會擴(kuò)展管理平臺站點來管理訂單和用不同的格式導(dǎo)出它們。
在這一章中,我們會覆蓋以下幾點:
- 集成一個支付網(wǎng)關(guān)到你的站點中
- 管理支付通知
- 導(dǎo)出訂單為CSV格式
- 創(chuàng)建定制視圖給管理頁面
- 動態(tài)的生成PDF支票
集成一個支付網(wǎng)關(guān)
一個支付網(wǎng)關(guān)允許你在線處理支付。通過使用一個支付網(wǎng)關(guān),你可以管理顧客的訂單以及委托一個可靠的,安全的第三方處理支付。這意味著你無需擔(dān)心存儲信用卡信息到你的系統(tǒng)中。
PayPal 提供了多種方法來集成它的網(wǎng)管到你的站點中。標(biāo)準(zhǔn)的集成由一個Buy now按鈕組成,這個按鈕你可以已經(jīng)在別的網(wǎng)站見到過(譯者注:國內(nèi)還是支付寶和微信比較多)。這個按鈕會重定向購買者到PayPal去處理支付。我們將要集成PayPal支付標(biāo)準(zhǔn)包含一個定制的Buy now按鈕到我們的站點中。PayPal將會處理支付并且發(fā)送一個消息通知給我們的服務(wù)指明該筆支付的狀態(tài)。
創(chuàng)建一個PayPal賬戶
你需要有一個PayPal商業(yè)賬戶來集成支付網(wǎng)關(guān)到你的站點中。如果你還沒有一個PayPal賬戶,去 https://www.paypal.com/signup/account 注冊。確保你選擇了一個Bussiness Account并且注冊成為PayPal支付標(biāo)準(zhǔn)解決方案,如下圖所示:
填寫你的詳情在注冊表單中并且完成注冊流程。PayPal會發(fā)送給你一封e-mail來核對你的賬戶。
安裝django-paypal
Django-paypal是一個第三方django應(yīng)用,它可以簡化集成PayPal到Django項目中。我們將要使用它來集成PayPal支付標(biāo)準(zhǔn)解決方案到我們的商店中。你可以找到django-paypal的文檔,訪問 http://django-paypal.readthedocs.org/。
安裝django-paypal在shell中通過以下命令:
pip install django-paypal==0.2.5
(譯者注:現(xiàn)在應(yīng)該有最新版本,書上使用的是0.2.5版本)
編輯你的項目中的settings.py文件,添加'paypal.standard.ipn'到INSTALLED_APPS設(shè)置中,如下所示:
INSTALLED_APPS = (
# ...
'paypal.standard.ipn',
)
這個應(yīng)用提供自django-paypal來集成PayPal支付標(biāo)準(zhǔn)通過Instant Payment Notification(IPN)。我們之后會操作支付通知。
添加以下設(shè)置到myshop的settings.py文件來配置django-paypal:
# django-paypal settings
PAYPAL_RECEIVER_EMAIL = 'mypaypalemail@myshop.com'
PAYPAL_TEST = True
以上兩個設(shè)置含義如下:
- PAYPAL_RECEIVER_EMAIL:你的PayPal賬戶e-mail。使用你創(chuàng)建的PayPal賬戶e-mail替換mypaypalemail@myshop.com。
- PAYPAL_TEST:一個布爾類型指示是否PayPal的沙箱環(huán)境,該環(huán)境可以用來處理支付。這個沙箱允許你測試你的PayPal集成在遷移到一個正式生產(chǎn)的環(huán)境之前。
打開shell運(yùn)行如下命令來同步django-paypal的模型(models)到數(shù)據(jù)庫中:
python manage.py migrate
你會看到如下類似的輸出:
Running migrations:
Rendering model states... DONE
Applying ipn.0001_initial... OK
Applying ipn.0002_paypalipn_mp_id... OK
Applying ipn.0003_auto_20141117_1647... OK
django-paypal的模型(models)如今已經(jīng)同步到了數(shù)據(jù)庫中。你還需要添加django-paypal的URL模式到你的項目中。編輯主的urls.py文件,該文件位于myshop目錄,然后添加以下的URL模式。記住粘貼該URL模式要在shop.urls模式之前為了避免錯誤的模式匹配:
url(r'^paypal/', include('paypal.standard.ipn.urls')),
讓我們添加支付網(wǎng)關(guān)到結(jié)賬流程中。
添加支付網(wǎng)關(guān)
結(jié)賬流程工作如下:
- 1.用戶添加物品到他們的購物車中
- 2.用戶結(jié)賬他們的購物車
- 3.用戶被重定向到PayPal進(jìn)行支付
- 4.PayPal發(fā)送一個支付通知給我們的站點
- 5.PayPal重定向用戶回到我們的網(wǎng)站
創(chuàng)建一個新的應(yīng)用到你的項目中使用如下命令:
python manage.py startapp payment
我們將要使用這個應(yīng)用去管理結(jié)賬過程和用戶支付。
編輯你的項目的settings.py文件,添加'payment'到INSTALLED_APPS設(shè)置中,如下所示:
INSTALLED_APPS = (
# ...
'paypal.standard.ipn',
'payment',
)
payment應(yīng)用現(xiàn)在已經(jīng)在項目中激活。編輯orders應(yīng)用的views.py文件并且確保包含以下導(dǎo)入:
from django.shortcuts import render, redirect
from django.core.urlresolvers import reverse
替換以下order_create視圖(view)的內(nèi)容:
# launch asynchronous task
order_created.delay(order.id)
return render(request, 'orders/order/created.html', locals())
新的內(nèi)容為:
# 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)建一個新的訂單之后,我們設(shè)置這個訂單ID到當(dāng)前的會話中使用order_id會話鍵(session key)。之后,我們重定向用戶到payment:process
URL,這個我們下一步就是創(chuàng)建。
編輯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
視圖(view)中,我們生成了一個PayPal的Buy now按鈕用來支付一個訂單。首先,我們拿到當(dāng)前的訂單從order_id
會話鍵中,這個鍵值被之前的order_create
視圖(view)設(shè)置。我們拿到這個order
對象通過給予的ID并且構(gòu)建一個新的PayPalPaymentsForm
,該表單表單包含以下字段:
- business:PayPal商業(yè)賬戶用來處理支付。我們使用e-mail賬戶,該賬戶定義在
PAYPAL_RECEIVER_EMAIL
設(shè)置那里。 - amount:向顧客索要的總價。
- item_name:正在出售的商品名。我們使用訂單ID,因為訂單可能包含很多產(chǎn)品。
- currency_code:本次支付的貨幣。我們設(shè)置這里為USD使用U.S. Dollar(譯者注:傳說中的美金)。需要使用相同的貨幣,該貨幣被設(shè)置在你的PayPal賬戶中(例如:EUR 對應(yīng)歐元)。
- notify_url:這個URL PayPal將會發(fā)送IPN請求過去。我們使用django-paypal提供的
paypal-ipn
URL。這個視圖(view)與這個URL關(guān)聯(lián)來操作支付通知以及存儲它們到數(shù)據(jù)庫中。 - return_url:這個URL用來重定向用戶當(dāng)他的支付成功之后。我們使用URL
payment:done
,這個我們接下來會創(chuàng)建。 - cancel_return:這個URL用來重定向用戶如果這個支付被取消或者有其他問題。我們使用URL
payment:canceled
,這個我們接下來會創(chuàng)建。
PayPalpaymentsForm
將會被渲染成一個標(biāo)準(zhǔn)表單帶有隱藏的字段,并且用戶將來只能看到Buy now按鈕。當(dāng)用戶點擊該按鈕,這個表單將會提交到PayPal通過POST渠道。
讓我們創(chuàng)建簡單的視圖(views)給PayPal用來重定向用戶當(dāng)支付成功,或者當(dāng)支付被取消因為某些原因。添加以下代碼到相同的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')
我們使用csrf_exempt
裝飾器來避免Django期待一個CSRF標(biāo)記,因為PayPal能重定向用戶到以上兩個視圖(views)通過POST渠道。創(chuàng)建新的文件在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'),
]
這些URL是給支付工作流的。我們已經(jīng)包含了以下URL模式:
- process:給這個視圖(view)用來生成PayPal表單給Buy now按鈕。
- done:給PayPal用來重定向用戶當(dāng)支付成功的時候。
- canceled:給PayPal用來重定向用戶當(dāng)支付取消的時候。
編輯主的myshop項目的urls.py文件,包含URL模式給payment應(yīng)用:
url(r'^payment/', include('payment.urls',namespace='payment')),
記住粘貼以上內(nèi)容在shop.urls
模式之前用來避免錯誤的模式匹配。
創(chuàng)建以下文件建構(gòu)在payment應(yīng)用目錄下:
templates/
payment/
process.html
done.html
canceled.html
編輯payment/process.html模板(template)并且添加以下代碼:
{% extends "shop/base.html" %}
{% block title %}Pay using PayPal{% endblock %}
{% block content %}
<h1>Pay using PayPal</h1>
{{ form.render }}
{% endblock %}
這個模板(template)會渲染PayPalPaymentsForm
并且展示Buy now按鈕。
編輯payment/done.html模板(template)并且添加如下代碼:
{% extends "shop/base.html" %}
{% block content %}
<h1>Your payment was successful</h1>
<p>Your payment has been successfully received.</p>
{% endblock %}
這個模板(template)的頁面給用戶重定向當(dāng)成功支付之后。
編輯payment/canceled.html模板(template)并且添加以下代碼:
{% extends "shop/base.html" %}
{% block content %}
<h1>Your payment has not been processed</h1>
<p>There was a problem processing your payment.</p>
{% endblock %}
這個模板(template)的頁面給用戶重定向當(dāng)有這個支付過程出現(xiàn)問題或者用戶取消了這次支付。
讓我們嘗試完成的支付過程。
使用PayPal的沙箱
打開 http://developer.paypal.com 在你的瀏覽器中然后進(jìn)行登錄使用你的PayPal商業(yè)賬戶。點擊Dashboard菜單項,在左方菜單點擊Accounts選項在Sandbox下方。你會看到你的沙箱測試賬戶列,如下所示:
一開始,你將會看到一個商業(yè)以及一個個人測試賬戶由PayPal動態(tài)創(chuàng)建。你可以創(chuàng)建新的沙箱測試賬戶通過使用Create Account按鈕。
點擊Personal Account在列中擴(kuò)大它,之后點擊Profile鏈接。你會看到一些信息關(guān)于這個測試賬戶包含e-mail和profile信息,如下所示:
在Funding tab中,你會找到銀行賬戶,信用卡日期,以及PayPal信用余額。
這些測試賬戶能夠被用來做支付在你的網(wǎng)站中當(dāng)使用沙箱環(huán)境。跳轉(zhuǎn)到Profile tab然后點擊Change password鏈接。創(chuàng)建一個定制密碼給這個測試賬戶。
打開shell并且啟動開發(fā)服務(wù)器使用命令python manage.py runserver
。打開 http://127.0.0.1:8000 在你的瀏覽器中,添加一些產(chǎn)品到購物車中,并且填寫結(jié)賬表單。當(dāng)你點擊Place order按鈕,這個訂單會被保存在數(shù)據(jù)庫中,這個訂單ID會被保存在當(dāng)前的會話中,并且你會被重定向到支付處理頁面。這個頁面從會話中獲取訂單并且渲染PayPal表單顯示一個Buy now按鈕,如下所示:
你可以看下HTML源碼來看下生成的表單字段。
點擊Buy now按鈕。你會被重定向到PayPal,并且你會看到如下頁面:
輸入購買者測試賬戶e-mail和密碼然后點擊Log In按鈕。你會被重定向到以下頁面:
現(xiàn)在,點擊Pay now按鈕。最后,你會看到批準(zhǔn)頁面該頁面包含你的交易ID。這個頁面看上去如下所示:
點擊Return to e-mail@domain.com按鈕。你會被重定向到的URL是你之前在PayPalPaymentsForm
中的return_url
字段中定義的。這個URL對應(yīng)payment_done
視圖(view)。這個頁面看上去如下所示:
這個支付已經(jīng)成功了。然而,PayPal并沒有發(fā)送一個支付狀態(tài)通知給我們的應(yīng)用,因為我們運(yùn)行我們的項目在我們本地主機(jī),IP是 127.0.0.1 這并不是一個公開地址。我們將要學(xué)習(xí)如何使我們的站點可以從Internet訪問并且接收IPN通知。
獲取支付通知
IPN是一個方法提供自大部分的支付網(wǎng)關(guān)用來跟蹤實時的購買。一個通知會立即發(fā)送到你的服務(wù)當(dāng)這個網(wǎng)關(guān)處理了一個支付。這個通知包含所有支付詳情,包括狀態(tài)以及一個支付的簽名,該簽名可以用來確定這個消息的來源點。這個消息被發(fā)送通過一個單獨的HTTP請求給你的服務(wù)。在出現(xiàn)連接問題的情況下,PayPal將會多次企圖通知你的站點。
django-paypal應(yīng)用內(nèi)置兩種不同的信號給IPNs。如下:
- valid_ipn_received:會被觸發(fā)當(dāng)IPN信息獲取自PayPal是正確的并且不是一個已存在數(shù)據(jù)庫中的消息的復(fù)制。
- invalid_ipn_received:這個信號會觸發(fā)當(dāng)IPN獲取自PayPal包含無效的數(shù)據(jù)或者不是一個良好的形式。
我們將要創(chuàng)建一個定制的接受函數(shù)并且連接它給valid_ipn_received
信號用來確定支付。
創(chuàng)建新的文件在payment應(yī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
信號。這個接收函數(shù)工作如下:
- 1.我們獲取發(fā)送對象,該對象是一個PayPalIPN模型的實例,位于
paypal.standard.ipn.models
。 - 2.我們檢查
payment_status
屬性來確保它和django-payapl的完整狀態(tài)相同。這個狀態(tài)指示這個支付已經(jīng)成功處理。 - 3.之后我們使用
get_object_or_404()
快捷函數(shù)來拿到訂單,該訂單的ID匹配invoice
參數(shù)我們之前提供給PayPal。 - 4.我們備注這個訂單已經(jīng)支付通過設(shè)置它的
paid
屬性為True
并且保存這個訂單對象到數(shù)據(jù)庫中。
你需要確保你的信號方法已經(jīng)加載,這樣這個接收函數(shù)會被調(diào)用當(dāng)valid_ipn_received
信號被觸發(fā)的時候。The best practice is to load your signals when the application containing them is loaded. (譯者注:誰幫我翻一下,好拗口啊)。這能夠?qū)崿F(xiàn)通過定義一個定制應(yīng)用配置,這方面會在下一節(jié)進(jìn)行解釋。
配置我們的應(yīng)用
你已經(jīng)學(xué)習(xí)了關(guān)于應(yīng)用的配置在第六章 跟蹤用戶操作。我們將要定義一個定制配置給我們的payment應(yīng)用為了加載我們的信號接收函數(shù)。
創(chuàng)建一個新的文件在payment應(yīng)用目錄下命名為apps.py。添加如下代碼:
from django.apps import AppConfig
class PaymentConfig(AppConfig):
name = 'payment'
verbose_name = 'Payment'
def ready(self):
# import signal handlers
import payment.signals
在上述代碼中,我們定義了一個定制AppConfif
類給payment應(yīng)用。name
參數(shù)是這個應(yīng)用的名字,verbose_name包含可讀的樣式。我們導(dǎo)入信號方法在ready()
方法中確保它們會被加載當(dāng)這個應(yīng)用初始化的時候。
編輯payment應(yīng)用的init.py文件,添加以下行:
default_app_config = 'payment.apps.PaymentConfig'
以上操作可以使Django動態(tài)加載你的定制應(yīng)用配置類。你可以找到更進(jìn)一步的信息關(guān)于應(yīng)用配置,通過訪問 https://docs.djangoproject.com/en/1.8/ref/applications/ 。
測試支付通知
由于我們工作在本地環(huán)境中,我們需要確保我們的站點可以被PayPal獲得。有不少應(yīng)用允許你使你的開發(fā)環(huán)境在Internet中可獲得。我們將要使用Ngrok,它就是其中一個最著名的。
./ngrok http 8000
通過這條命名,你告訴Ngrok去創(chuàng)建一條隧道給你的本地主機(jī)在端口8000上并且分配一個Internet可訪問主機(jī)名給它。你可以看到如下類似輸出:
Tunnel Status online
Version 2.0.17/2.0.17
Web Interface http://127.0.0.1:4040
Forwarding http://1a1b50f2.ngrok.io -> localhost:8000
Forwarding https://1a1b50f2.ngrok.io -> localhost:8000
Connnections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
Ngrok告訴我們關(guān)于我們的站點,運(yùn)行在本地8000端口使用Django開發(fā)服務(wù)器,已經(jīng)可以在Internet訪問到通過URLs http://1a1b50f2.ngrok.io 以及 https://1a1b50f2.ngrok.io ,前者是HTTP,后者是HTTPS。Ngrok還提供一個URL來訪問一個web接口用來顯示信息關(guān)于發(fā)送到這個服務(wù)的請求。
打開Ngrok提供的URL在瀏覽器中;例如,http://1a1b50f2.ngrok.io 。添加一些產(chǎn)品到購物車中,放置一個訂單,然后使用你的PayPal測試賬戶進(jìn)行支付。這個時候,PayPal將能夠拿到這個URL,這個URL由PayPalPaymentsForm
的notify_url
字段生成,在payment_process
視圖(view)中。如果你看一下這個渲染過的表單,你會看到這個HTML表單字段看上去如下所示:
<input id="id_notify_url" name="notify_url" type="hidden"
value="http://1a1b50f2.ngrok.io/paypal/">
在結(jié)束支付過程之后,打開 http://127.0.0.1:8000/admin/ipn/paypalipn/ 在你的瀏覽器中。你會看到一個IPN對象對應(yīng)最新的支付狀態(tài)為Completed。這個對象包含所有的支付信息,該對象由PayPal發(fā)送給你提供給IPN通知的URL。IPN管理列展示頁面看上去如下所示:
你還可以啟動IPNs通過使用PayPal的IPN模擬器位于 https://developer.paypal.com/developer/ipnSimulator/ 。這個模擬器允許你指定字段和發(fā)送的通知類型。
除了PayPal支付標(biāo)準(zhǔn)外,PayPal提供Website Payments Pro,它是一個訂購服務(wù)允許你接受支付在你的站點中而不需要重定向用戶到PayPal。你可以找到更多信息關(guān)于如何集成Website Payments Pro,通過訪問 http://django-paypal.readthedocs.org/en/v0.2.5/pro/index.html。
導(dǎo)出訂單為CSV文件
有時候,你可能想要導(dǎo)出包含在模型的信息到一個文件中,這樣你可以導(dǎo)入它到其他的系統(tǒng)中。其中一個范圍最廣的格式用來導(dǎo)出/導(dǎo)入數(shù)據(jù)就是Comma-Separated Values(CSV)。一個CSV文件就是一個純文本文件包含若干記錄。一個csv文件中,通常一行記錄為一個訂單,以及一些分割符(通常是逗號) 。我們將要定制管理平臺站點能夠?qū)С鲇唵螢镃SV文件。
添加定制操作到管理平臺站點中
Django提供你多種不同的選項來定制管理平臺站點。我們將要修改對象列視圖(view)來包含一個定制的管理操作。
一個管理操作工作如下:一個用戶選擇對象從管理對象列頁面通過復(fù)選框,之后選擇一個操作去執(zhí)行在所有被選擇的項上,然后執(zhí)行該操作。以下
展示操作會位于管理頁面的哪個地方:
創(chuàng)建定制管理操作允許管理人員一次性應(yīng)用操作多個元素。
你可以創(chuàng)建一個定制操作通過編寫一個經(jīng)常性的函數(shù)獲取以下參數(shù):
- 當(dāng)前展示的ModelAdmin
- 當(dāng)前請求對象,一個HttpRequest實例
- 一個查詢集(QuerySet)給用戶所選擇的對象
這個函數(shù)將會被執(zhí)行當(dāng)這個操作被觸發(fā)在管理平臺站點上。
我們將要創(chuàng)建一個定制管理操作來下載訂單列表的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ù):
- 1.我們創(chuàng)建一個
HttpResponse
實例包含一個定制text/csv
內(nèi)容類型來告訴瀏覽器這個響應(yīng)需要處理為一個CSV文件。我們還添加一個Content-Disposition
頭來指示這個HTTP響應(yīng)包含一個附件。 - 2.我們創(chuàng)建一個CSV
writer
對象,該對象將會被寫入response
對象。 - 3.我們動態(tài)的獲取
model
字段通過使用模型(moedl)_meta
選項的get_fields()
方法。我們排除多對多以及一對多的關(guān)系。 - 4.我們編寫了一個頭行包含字段名。
- 5.我們迭代給予的查詢集(QuerySet)并且為每一個查詢集中返回的對象寫入行。我們注意格式化
datetime
對象因為這個輸出值給CSV必須是一個字符串。 - 6.我們定制這個操作的顯示名在模板(template)中通過設(shè)置一個
short_description
屬性給這個函數(shù)。
我們已經(jīng)創(chuàng)建了一個普通的管理操作可以添加到任意的ModelAdmin類。
最后,添加新的export_to_csv
管理操作給OrderAdmin
類如下所示:
class OrderAdmin(admin.ModelAdmin):
# ...
actions = [export_to_csv]
打開 http://127.0.0.1:8000/admin/orders/order/ 在你的瀏覽器中。管理操作看上去如下所示:
選擇一些訂單然后選擇Export to CSV操作從下拉選框中,之后點擊Go按鈕。你的瀏覽器會下載生成的CSV文件名為order.csv。打開下載的文件使用一個文本編輯器。你會看到的內(nèi)容如以下的格式,包含一個頭行以及你之前選擇的每行訂單對象:
ID,first name,last name,email,address,postal
code,city,created,updated,paid
3,Antonio,Mele?,antonio.mele@gmail.com,Bank Street 33,WS J11,London,25/05/2015,25/05/2015,False
...
如你所見,創(chuàng)建管理操作是非常簡單的。
擴(kuò)展管理站點通過定制視圖(view)
有時候你可能想要定制管理平臺站點,比如處理ModelAdmin的配置,管理操作的創(chuàng)建,以及覆蓋管理模板(templates)。在這樣的場景中,你需要創(chuàng)建一個定制的管理視圖(view)。通過一個定制的管理視圖(view),你可以構(gòu)建任何你需要的功能。你只需要確保只有管理用戶能訪問你的視圖并且你維護(hù)這個管理的外觀和感覺通過你的模板(template)擴(kuò)展自一個管理模板(template)。
讓我們創(chuàng)建一個定制視圖(view)來展示關(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
裝飾器檢查用戶請求這個頁面的is_active
以及is_staff
字段是被設(shè)置為True
。在這個視圖(view)中,我們獲取Order
對象通過給予的id以及渲染一個模板來展示這個訂單。
現(xiàn)在,編輯orders應(yīng)用中的urls.py文件并且添加以下URL模式:
url(r'^admin/order/(?P<order_id>\d+)/$',
views.admin_order_detail,
name='admin_order_detail'),
創(chuàng)建以下文件結(jié)構(gòu)在orders應(yīng)用的templates/目錄下:
admin/
orders/
order/
detail.html
編輯detail.html模板(template),添加以下內(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 %}
這個模板(template)是用來顯示一個訂單詳情在管理平臺站點中。這個模板(template)擴(kuò)展Djnago的管理平臺站點的admin/base_site.html模板,它包含管理的主要HTML結(jié)構(gòu)和CSS樣式。我們加載定制的靜態(tài)文件css/admin.css。
為了使用靜態(tài)文件,你需要拿到它們從這章教程的實例代碼中。復(fù)制位于orders應(yīng)用的static/目錄下的靜態(tài)文件然后添加它們到你的項目的相同位置。
我們使用定義在父模板(template)的區(qū)塊包含我們自己的內(nèi)容。我們展示信息關(guān)于訂單和購買的商品。
當(dāng)你想要擴(kuò)展一個管理模板(template),你需要知道它的結(jié)構(gòu)以及確定存在的區(qū)塊。你可以找到所有管理模板(template),通過訪問 https://github.com/django/django/tree/1.8.6/django/contrib/admin/templates/admin 。
你也可以重寫一個管理模板(template)如果你需要的話。為了重寫一個管理模板(template),拷貝它到你的template目錄保持相同的相對路徑以及文件名。Django管理平臺站點將會使用你的定制模板(template)替代默認(rèn)的模板。
最后,讓我們添加一個鏈接給每個Order對象在管理平臺站點的列展示頁面。編輯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
這個函數(shù)需要一個Order對象作為參數(shù)并且返回一個HTML鏈接給admind_order_detail
URL。Django會避開默認(rèn)的HTML輸出。我們必須設(shè)置allow_tags
屬性為True
來避開auto-escaping。
設(shè)置
allow_tags
屬性為True
來避免HTML-escaping在一些Model方法,ModelAdmin方法,以及任何其他的調(diào)用中。當(dāng)你使用allow_tags
的時候,能確保避開用戶輸入的跨域腳本。
之后,編輯OrderAdmin
類來展示鏈接:
class OrderAdmin(admin.ModelAdmin):
list_display = ['id',
'first_name',
# ...
'updated',
order_detail]
打開 http://127.0.0.1:8000/admin/orders/order/ 在你的瀏覽器中。每一行現(xiàn)在都會包含一個View鏈接如下所示:
點擊某個訂單的View鏈接來加載定制訂單詳情頁面。你會看到一個頁面如下所示:
生成動態(tài)的PDF發(fā)票
如今我們已經(jīng)有了一個完整的結(jié)賬和支付系統(tǒng),我們可以生成一張PDF發(fā)票給每個訂單。有幾個Python庫可以生成PDF文件。一個最流行的生成PDF的Python庫是Reportlab。你可以找到關(guān)于如何使用Reportlab輸出PDF文件的信息,通過訪問 https://docs.djangoproject.com/en/1.8/howto/outputting-pdf/ 。
在大部分的場景中,你還需要添加定制樣式和格式給你的PDF文件。你會發(fā)現(xiàn)渲染一個HTML模板(template)以及轉(zhuǎn)化該模板(template)為一個PDF文件更加的方便,保持Python遠(yuǎn)離表現(xiàn)層。我們要遵循這個方法并且使用一個模塊來生成PDF文件通過Django。我們將要使用WeasyPrint,它是一個Python庫可以生成PDF文件從HTML模板中。
安裝WeasyPrint
首先,安裝WeasyPrint的依賴給你的OS,這些依賴你可以找到通過訪問 http://weasyprint.org/docs/install/#platforms 。
之后,安裝WeasyPrint通過pip渠道使用如下命令:
pip install WeasyPrint==0.24
創(chuàng)建一個PDF模板(template)
我們需要一個HTML文檔給WeasyPrint輸入。我們將要創(chuàng)建一個HTML模板(template),渲染它使用Django,并且傳遞它給WeasyPrint來生成PDF文件。
創(chuàng)建一個新的模板(template)文件在orders應(yīng)用的templates/orders/order/目錄下命名為pdf.html*。添加如下內(nèi)容:
<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>
這個模板(template)就是PDF發(fā)票。在這個模板(template)中,我們展示所有訂單詳情以及一個HTML <table>
元素包含所有商品。我們還包含了一條消息來展示如果該訂單已經(jīng)支付或者支付還在進(jìn)行中。
渲染PDF文件
我們將要創(chuàng)建一個視圖(view)來生成PDF發(fā)票給存在的訂單通過使用管理平臺站點。編輯order應(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
這個視圖(view)用來生成一個PDF發(fā)票給一個訂單。我們使用staff_member_required
裝飾器來確保只有管理人員能夠訪問這個視圖(view)。我們獲取Order對象通過給予的ID并且我們使用rander_to_string()
函數(shù)提供自Django來渲染orders/order/pdf.html。這個渲染過的HTML會被保存到html
變量中。之后,我們生成一個新的HttpResponse
對象指定application/pdf
的內(nèi)容類型并且包含Content-Disposition
頭來指定這個文件名。我們使用WeasyPrint來生成一個PDF文件從渲染的HTML代碼中并且將該文件寫入HttpResponse
對象中。我們加載它從本地路徑通過使用STATIC_ROOT
設(shè)置。最后,我們返回這個生成的響應(yīng)。
由于我們需要使用STATIC_ROOT
設(shè)置,我們需要添加它到我們的項目中。這個項目將會是靜態(tài)文件的所在地。編輯myshop項目的settings.py文件,添加如下設(shè)置:
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
之后,運(yùn)行命令python manage.py collectstatic
。你會在輸出末尾看到如下輸出:
You have requested to collect static files at the destination
location as specified in your settings:
code/myshop/static
This will overwrite existing files!
Are you sure you want to do this?
輸入yes然后回車。你會得到一條消息,告知那個靜態(tài)文件已經(jīng)復(fù)制到STATIC_ROOT
目錄中。
collectstatic命令復(fù)制所有靜態(tài)文件從你的應(yīng)用到定義在STATIC_ROOT
設(shè)置的目錄中。這允許每個應(yīng)用去提供它自己的靜態(tài)文件通過使用一個static/目錄來包含它們。你還可以提供額外的靜態(tài)文件來源在STATICFILES_DIRS
設(shè)置。所有的目錄被指定在STATICFILED_DIRS
列中的都將會被復(fù)制到STATIC_ROOT
目錄中當(dāng)collectstatic被執(zhí)行的時候。
編輯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模型(model)來添加一個鏈接給PDF文件給每一個結(jié)果。編輯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 = ['id',
# ...
order_detail,
order_pdf]
如果你指定一個short_description
屬性給你的調(diào)用,Django將會使用它給這個列命名。
打開 http://127.0.0.1:8000/admin/orders/order/ 在你的瀏覽器中。每一行現(xiàn)在都包含一個PDF鏈接,如下所示:
點擊某一個訂單的PDF。你會看到一個生成的PDF文件,如下所示一個訂單還沒有支付完成:
對于支付完成的訂單,你會看到如下所示的PDF文件:
通過e-mail發(fā)送PDF文件
讓我們發(fā)送一封e-mail給我們的顧客包含生成的PDF發(fā)表但一個支付被接收的時候。編輯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()
在這個信號中,我們使用Django提供的EmailMessage
類來創(chuàng)建一個e-mail對象。之后我們渲染這個模板(template)到html
變量中。我們生成PDF文件從渲染的模板(template)中,并且我們輸出它到一個BytesIO
實例中,該實例是一個內(nèi)容字節(jié)緩存。之后我們附加這個生成的PDF文件到EmailMessage
對象通過使用它的attach()
方法,包含這個out
緩存的內(nèi)容。
記住設(shè)置你的SMTP設(shè)置在項目的settings.py文件中來發(fā)送e-mail。你可以到第二章 通過高級特性擴(kuò)展你的blog去看下一個SMTP配置的例子。
現(xiàn)在你可以打開Ngrok提供給你的應(yīng)用的URL然后完成一個新的支付處理為了收到PDF發(fā)票到你的e-mail中。
總結(jié)
在這一章中,你集成了一個支付網(wǎng)關(guān)到你的項目中。你定制了Django管理平臺頁面并且學(xué)習(xí)到了如何動態(tài)的生成CSV以及PDF文件。
在下一章中將會給你一個深刻理解關(guān)于國際化和本地化給Django項目。你還會學(xué)習(xí)到創(chuàng)建一個贈券系統(tǒng)已經(jīng)構(gòu)建一個產(chǎn)品推薦引擎。
譯者總結(jié)
不知不覺,第八章也翻譯完成了,還是渣翻,精校之前大家先湊合著看吧,有問題我會及時更新。目前全書翻譯已完成三分之二,離不開各位的支持,我們下章再見!對了,本章完成日是三八婦女(女神?)節(jié),各位女看客們節(jié)日快樂!