电竞比分网-中国电竞赛事及体育赛事平台

分享

第八章 管理支付和訂單

 ly88 2018-05-16

在上一章中,你創(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)管理用戶訂單,以及通過可靠的,安全的第三方代理處理支付。這意味著你不用考慮在自己的系統(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)站見過。這個(gè)按鈕把顧客重定向到PayPal來處理支付。我們將在網(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)。如果你還沒有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提供的,通過Instant Payment Notification(IPN)集成PayPal Payments Standard。我們之后會(huì)處理支付通知。

myshopsettings.py文件添加以下設(shè)置來配置django-paypal:

# django-paypal settingsPAYPAL_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é)賬過程中。

8.1.3 添加支付網(wǎng)關(guān)

結(jié)賬流程是這樣的:

  1. 用戶添加商品到購(gòu)物車中。
  2. 用戶結(jié)賬購(gòu)物車。
  3. 重定向用戶到PayPal進(jìn)行支付。
  4. PayPal發(fā)送支付通知到我們的服務(wù)器。
  5. 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, redirectfrom django.core.urlresolvers import reverse

找到order_create視圖中的以下代碼:

# launch asynchronous taskorder_created.delay(order.id)return render(request, 'orders/order/created.html', {'order': order})

替換為下面的代碼:

# launch asynchronous taskorder_created.delay(order.id)request.session['order_id'] = order.idreturn redirect(reverse('payment:process'))

創(chuàng)建訂單成功之后,我們用order_id會(huì)話鍵在當(dāng)前會(huì)話中設(shè)置訂單ID。然后我們把用戶重定向到接下來會(huì)創(chuàng)建的payment:process URL。

編輯payment應(yīng)用的views.py文件,并添加以下代碼:

from decimal import Decimalfrom django.conf import settingsfrom django.core.urlresolvers import reversefrom django.shortcuts import render, get_object_or_404from paypal.standard.forms import PayPalPaymentsFormfrom orders.models import Orderdef 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è)置過。我們獲得指定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:如果支付取消,或者遇到其它問題,重定向用戶到這個(gè)URL。我們使用之后會(huì)創(chuàng)建的payment:canceled URL。

PayPalPaymentForm會(huì)被渲染為帶隱藏字典的標(biāo)準(zhǔn)表單,用戶只能看到Buy now按鈕。點(diǎn)用戶點(diǎn)擊這個(gè)按鈕,表單會(huì)通過POST提交到PayPal。

讓我們創(chuàng)建一個(gè)簡(jiǎn)單的視圖,當(dāng)支付完成,或者因?yàn)槟承┰蛉∠Ц?,讓PayPal重定向用戶。在同一個(gè)views.py文件中添加以下代碼:

from django.views.decorators.csrf import csrf_exempt@csrf_exemptdef payment_done(request): return render(request, 'payment/done.html')@csrf_exemptdef payment_canceled(request): return render(request, 'payment/canceled.html')

因?yàn)镻ayPal可以通過POST重定向用戶到這些視圖的任何一個(gè),所以我們用csrf_exempt裝飾器避免Django期望的CSRF令牌。在payment應(yīng)用目錄中創(chuàng)建urls.py文件,并添加以下代碼:

from django.conf.urls import urlfrom . import viewsurlpatterns = [ 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 %}

Pay using PayPal

{{ form.render }}{% endblock content %}

這個(gè)模板用于渲染PayPalPaymentForm和顯示Buy now按鈕。

編輯payment/done.html模板,添加以下代碼:

{% extends 'shop/base.html' %}{% block content %}

Your payment was successful

Your payment has been successfully received.

{% endblock content %}

用戶支付成功后,會(huì)重定向到這個(gè)模板頁面。

編輯payment/canceled.html模板,并添加以下代碼:

{% extends 'shop/base.html' %}{% block content %}

Your payment has not been processed

There was a problem processing your payment.

{% endblock content %}

處理支付遇到問題,或者用戶取消支付時(shí),會(huì)重定向到這個(gè)模板頁面。

讓我們嘗試完整的支付流程。

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)擊列表中TypePERSONAL的賬戶,然后點(diǎn)擊Pofile鏈接。你會(huì)看到測(cè)試賬戶的信息,包括郵箱地址和個(gè)人資料信息,如下圖所示:

Funding標(biāo)簽頁中,你會(huì)看到銀行賬戶,信用卡數(shù)據(jù),以及PayPal貸方余額。

當(dāng)你的網(wǎng)站使用sandbox環(huán)境時(shí),測(cè)試賬戶可以用來處理支付。導(dǎo)航到Profile標(biāo)簽頁,然后點(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ì)重定向到支付處理頁面。這個(gè)頁面從會(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ì)被重定向到以下頁面:

譯者注:即之前修改過密碼的個(gè)人賬戶。

現(xiàn)在點(diǎn)擊立即付款按鈕。最后,你會(huì)看到一個(gè)包括交易ID的確認(rèn)頁面,如下圖所示:

點(diǎn)擊返回商家按鈕。你會(huì)被重定向到PayPalPaymentFormreturn_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)通知。我們接下來學(xué)習(xí)如何讓我們的網(wǎng)站可以從Internet訪問,從而接收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)通知來源的支付簽名。這個(gè)通知作為獨(dú)立的HTTP請(qǐng)求發(fā)送到你的服務(wù)器。出現(xià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接收的消息包括無效數(shù)據(jù)或者格式不對(duì)時(shí)觸發(fā)

我們將創(chuàng)建一個(gè)自定義接收函數(shù),并把它連接到valid_ipn_received信號(hào)來確認(rèn)支付。

payment應(yīng)用目錄中創(chuàng)建signals.py文件,并添加以下代碼:

from django.shortcuts import get_object_or_404from paypal.standard.models import ST_PP_COMPLETEDfrom paypal.standard.ipn.signals import valid_ipn_receivedfrom orders.models import Orderdef 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ù)是這樣工作的:

  1. 我們接收sender對(duì)象,它是在paypal.standard.ipn.models中定義的PayPalPN模型的一個(gè)實(shí)例。
  2. 我們檢查paypal_status屬性,確保它等于django-paypal的完成狀態(tài)。這個(gè)狀態(tài)表示支付處理成功。
  3. 接著我們用get_object_or_404快捷函數(shù)獲得訂單,這個(gè)訂單的ID必須匹配我們提供給PayPal的invoice參數(shù)。
  4. 我們?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)??梢酝ㄟ^定義一個(gè)自定義的應(yīng)用配置來實(shí)現(xiàn),我們會(huì)在下一節(jié)中講解。

8.1.6 配置我們的應(yīng)用

你已經(jīng)在第六章學(xué)習(xí)了應(yīng)用配置。我們將為payment應(yīng)用定義一個(gè)自定義配置,用來加載我們的信號(hào)接收函數(shù)。

payment應(yīng)用目錄中創(chuàng)建apps.py文件,并添加以下代碼:

from django.apps import AppConfigclass 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ǎng)站。有幾個(gè)應(yīng)用程序可以讓開發(fā)環(huán)境通過Internet訪問。我們將使用Ngrok,是最流行的之一。

這里下載你的操作系統(tǒng)版本的Ngrok,并使用以下命令運(yùn)行:

./ngrok http 8000

這個(gè)命令告訴Ngrok在8000端口為你的本地主機(jī)創(chuàng)建一個(gè)鏈路,并為它分配一個(gè)Internet可訪問的主機(jī)名。你可以看到類似這樣的輸入:

Session Status onlineAccount lakerszhy (Plan: Free)Update update available (version 2.2.4, Ctrl-U to update)Version 2.1.18Region United States (us)Web Interface http://127.0.0.1:4040Forwarding http://c0f17d7c. -> localhost:8000Forwarding https://c0f17d7c. -> localhost:8000Connections 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)在可以通過http://c0f17d7c.https://c0f17d7c.(分別對(duì)應(yīng)HTTP和HTTPS協(xié)議)在Internet上訪問。Ngrok還提供了一個(gè)網(wǎng)頁URL,這個(gè)網(wǎng)頁顯示發(fā)送到這個(gè)服務(wù)器的信息。在瀏覽器中打開Ngrok提供的URL,比如http://c0f17d7c.。在購(gòu)物車中添加一些商品,下單,然后用PayPal測(cè)試賬戶支付。此時(shí),PayPal可以訪問payment_process視圖中PayPalPaymentFormnotify_url字段生成的URL。如果你查看渲染的表單,你會(huì)看類似這樣的HTML表單:

完成支付處理后,在瀏覽器中打開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。

譯者注:如果通過http://c0f17d7c.訪問在線商店,則需要在項(xiàng)目的settings.py文件的ALLOWED_HOSTS設(shè)置中添加c0f17d7c.。

譯者注:我在后臺(tái)看到的一直都是Pending狀態(tà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ì)象列表頁面用復(fù)選框選擇對(duì)象,然后選擇一個(gè)在所有選中選項(xiàng)上執(zhí)行的操作,最后執(zhí)行操作。下圖顯示了操作位于管理站點(diǎn)的哪個(gè)位置:

創(chuàng)建自定義管理操作允許工作人員一次在多個(gè)元素上進(jìn)行操作。

你可以編寫一個(gè)常規(guī)函數(shù)來創(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è)自定義管理操作,來下載一組訂單的CSV文件。編輯orders應(yīng)用的admin.py文件,在OrderAdmin類之前添加以下代碼:

import csvimport datetimefrom django.http import HttpResponsedef 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 responseexport_to_csv.short_description = 'Export to CSV'

在這段代碼中執(zhí)行了以下任務(wù):

  1. 我們創(chuàng)建了一個(gè)HttpResponse實(shí)例,其中包括定制的text/csv內(nèi)容類型,告訴瀏覽器該響應(yīng)看成一個(gè)CSV文件。我們還添加了Content-Disposition頭部,表示HTTP響應(yīng)包括一個(gè)附件。
  2. 我們創(chuàng)建了CSV的writer對(duì)象,用于向response對(duì)象中寫入數(shù)據(jù)。
  3. 我們用模型的_meta選項(xiàng)的get_fields()方法動(dòng)態(tài)獲得模型的字段。我們派出了對(duì)多對(duì)和一對(duì)多關(guān)系。
  4. 我們用字段名寫入標(biāo)題行。
  5. 我們迭代給定的QuerySet,并為QuerySet返回的每個(gè)對(duì)象寫入一行數(shù)據(jù)。因?yàn)镃SV的輸出值必須為字符串,所以我們格式化datetime對(duì)象。
  6. 我們?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,paid1,allen,iverson,lakerszhy@gmail.com,北京市朝陽區(qū),100012,北京市,11/05/2017,11/05/2017,False2,allen,kobe,lakerszhy@gmail.com,北京市朝陽區(qū),100012,北京市,11/05/2017,11/05/2017,False

正如你所看到的,創(chuàng)建管理操作非常簡(jiǎn)單。

8.3 用自定義視圖擴(kuò)展管理站點(diǎn)

有時(shí),你可能希望通過配置ModelAdmin,創(chuàng)建管理操作和覆寫管理目標(biāo)來定制管理站點(diǎn)。這種情況下,你需要?jiǎng)?chuàng)建自定義的管理視圖。使用自定義視圖,可以創(chuàng)建任何你需要的功能。你只需要確保只有工作人員能訪問你的視圖,以及讓你的模板繼承自管理模板來維持管理站點(diǎn)的外觀。

讓我們創(chuàng)建一個(gè)自定義視圖,顯示訂單的相關(guān)信息。編輯orders應(yīng)用的views.py文件,并添加以下代碼:

from django.contrib.admin.views.decorators import staff_member_requiredfrom django.shortcuts import get_object_or_404from .models import Order@staff_member_requireddef 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è)頁面的用戶的is_activeis_staff字段是否為True。這個(gè)視圖中,我們用給定的ID獲得Order對(duì)象,然后渲染一個(gè)模板顯示訂單。

現(xiàn)在編輯orders應(yīng)用的urls.py文件,添加以下URL模式:

url(r'^admin/order/(?P\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 %} {% endblock extrastyle %}{% block title %} Order {{ order.id }} {{ block.super }}{% endblock title %}{% block breadcrumbs %} {% endblock breadcrumbs %}{% block content %}

Order {{ order.id }}

Created {{ order.created }}
Customer {{ order.first_name }} {{ order.last_name }}
E-mail {{ order.email }}
Address {{ order.address }}, {{ order.postal_code }} {{ order.city }}
Total amount ${{ order.get_total_cost }}
Status {% if order.paid %}Paid{% else %}Pending payment{% endif %}
{% 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)文件,我們可以從本章的示例代碼中獲得它們??截?code>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)的列表顯示頁中每個(gè)Order對(duì)象添加一個(gè)鏈接。編輯orders應(yīng)用的amdin.py文件,在OrderAdmin類之前添加以下代碼:

from django.core.urlresolvers import reversedef order_detail(obj): return 'View'.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類來顯示鏈接:

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ì)加載自定義的訂單詳情頁面,如下圖所示:

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問這里

然后用以下命令安裝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文件,并添加以下代碼:

My Shop

Invoice no. {{ order.id }}
{{ order.created|date:'M d, Y' }}

Bill to

{{ order.first_name }} {{ order.last_name }}
{{ order.email }}
{{ order.address }}
{{ order.postal_code }}, {{ order.city }}

Items bought

{% for item in order.items.all %} {% endfor %}
Product Price Quantity Cost
{{ item.product.name }} ${{ item.price }} {{ item.quantity }} ${{ item.get_cost }}
Total ${{ order.get_total_cost }}
{% if order.paid %}Paid{% else %}Pending payment{% endif %}

這是PDF單據(jù)的模板。在這個(gè)模板中,我們顯示所有訂單詳情和一個(gè)包括商品的HTML的

元素。我們還包括一個(gè)消息,顯示訂單是否支付。

8.4.3 渲染PDF文件

我們將創(chuàng)建一個(gè)視圖,在管理站點(diǎn)中生成已存在訂單的PDF單據(jù)。編輯orders應(yīng)用的views.py文件,并添加以下代碼:

from django.conf import settingsfrom django.http import HttpResponsefrom django.template.loader import render_to_stringimport weasyprint@staff_member_requireddef 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裝飾器確保只有工作人員可以訪問這個(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 destinationlocation as specified in your settings: /Users/lakerszhy/Documents/GitHub/Django-By-Example/code/Chapter 8/myshop/staticThis 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\d+)/pdf/$', views.admin_order_pdf, name='admin_order_pdf'),

現(xiàn)在,我們可以編輯管理列表顯示頁面,為Order模型的每條記錄添加一個(gè)PDF文件鏈接。編輯orders應(yīng)用的admin.py文件,在OrderAdmin類之前添加以下代碼:

def order_pdf(obj): return 'PDF'.format(reverse('orders:admin_order_pdf', args=[obj.id]))order_pdf.allow_tags = Trueorder_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 通過郵件發(fā)送PDF文件

當(dāng)收到支付時(shí),讓我們給顧客發(fā)送一封包括PDF單據(jù)的郵件。編輯payment應(yīng)用的signals.py文件,并添加以下導(dǎo)入:

from django.template.loader import render_to_stringfrom django.core.mail import EmailMessagefrom django.conf import settingsimport weasyprintfrom io import BytesIO

然后在order.save()行之后添加以下代碼,保持相同的縮進(jìn):

# create invoice e-mailsubject = '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 PDFhtml = 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 fileemail.attach('order_{}.pdf'.format(order.id), out.getvalue(), 'application/pdf')# send e-mailemail.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)和商品推薦引擎。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多