《Django By Example》第八章 中文 翻譯 (個人學(xué)習(xí),渣翻)

全文鏈接

第一章 創(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)解決方案,如下圖所示:

django-8-0

填寫你的詳情在注冊表單中并且完成注冊流程。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è)置到myshopsettings.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:processURL,這個我們下一步就是創(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下方。你會看到你的沙箱測試賬戶列,如下所示:

django-8-1

一開始,你將會看到一個商業(yè)以及一個個人測試賬戶由PayPal動態(tài)創(chuàng)建。你可以創(chuàng)建新的沙箱測試賬戶通過使用Create Account按鈕。

點擊Personal Account在列中擴(kuò)大它,之后點擊Profile鏈接。你會看到一些信息關(guān)于這個測試賬戶包含e-mail和profile信息,如下所示:

django-8-2

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按鈕,如下所示:

django-8-3

你可以看下HTML源碼來看下生成的表單字段。

點擊Buy now按鈕。你會被重定向到PayPal,并且你會看到如下頁面:

django-8-4

輸入購買者測試賬戶e-mail和密碼然后點擊Log In按鈕。你會被重定向到以下頁面:

django-8-5

現(xiàn)在,點擊Pay now按鈕。最后,你會看到批準(zhǔn)頁面該頁面包含你的交易ID。這個頁面看上去如下所示:

django-8-6

點擊Return to e-mail@domain.com按鈕。你會被重定向到的URL是你之前在PayPalPaymentsForm中的return_url字段中定義的。這個URL對應(yīng)payment_done視圖(view)。這個頁面看上去如下所示:

django-8-7

這個支付已經(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由PayPalPaymentsFormnotify_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管理列展示頁面看上去如下所示:

django-8-8

你還可以啟動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í)行該操作。以下

展示操作會位于管理頁面的哪個地方:

django-8-9

創(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/ 在你的瀏覽器中。管理操作看上去如下所示:

django-8-10

選擇一些訂單然后選擇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鏈接如下所示:

django-8-11

點擊某個訂單的View鏈接來加載定制訂單詳情頁面。你會看到一個頁面如下所示:

django-8-12

生成動態(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_pdfOrderAdmin類的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鏈接,如下所示:

django-8-13

點擊某一個訂單的PDF。你會看到一個生成的PDF文件,如下所示一個訂單還沒有支付完成:

django-8-14

對于支付完成的訂單,你會看到如下所示的PDF文件:

django-8-15

通過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é)日快樂!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內(nèi)容