《Django By Example》第八章 中文翻譯

譯者夜夜月,已獲作者授權轉載。

原文鏈接:jianshu.com/p/e96e3fc94

為了獲得更好閱讀體驗,可點擊原文鏈接跳轉到簡書閱讀。鼓勵對譯者的辛勤付出進行打賞。

書籍出處:Django By Example

原作者:Antonio Melé

(譯者註:還有4章!還有4章全書就翻譯完成了!)

第八章

管理付款和訂單

在上一章,你創建了一個基礎的在線商店包含一個產品列表以及訂單系統。你還學習了如何執行非同步的任務通過使用Celery。在這一章中,你會學習到如何集成一個支付網關(譯者註:支付網關(Payment Gateway)是銀行金融網路系統和Internet網路之間的介面,是由銀行操作的將Internet上傳輸的數據轉換為金融機構內部數據的一組伺服器設備,或由指派的第三方處理商家支付信息和顧客的支付指令。以上是我百度的。)到你的站點中。你還會擴展管理平台站點來管理訂單和用不同的格式導出它們。

在這一章中,我們會覆蓋以下幾點:

  • 集成一個支付網關到你的站點中
  • 管理支付通知
  • 導出訂單為CSV格式
  • 創建定製視圖給管理頁面
  • 動態的生成PDF支票

集成一個支付網關

一個支付網關允許你在線處理支付。通過使用一個支付網關,你可以管理顧客的訂單以及委託一個可靠的,安全的第三方處理支付。這意味著你無需擔心存儲信用卡信息到你的系統中。

PayPal 提供了多種方法來集成它的網管到你的站點中。標準的集成由一個Buy now按鈕組成,這個按鈕你可以已經在別的網站見到過(譯者註:國內還是支付寶和微信比較多)。這個按鈕會重定向購買者到PayPal去處理支付。我們將要集成PayPal支付標準包含一個定製的Buy now按鈕到我們的站點中。PayPal將會處理支付並且發送一個消息通知給我們的服務指明該筆支付的狀態。

創建一個PayPal賬戶

你需要有一個PayPal商業賬戶來集成支付網關到你的站點中。如果你還沒有一個PayPal賬戶,去 Create a Business or Personal Account Now With PayPal 註冊。確保你選擇了一個Bussiness Account並且註冊成為PayPal支付標準解決方案,如下圖所示:

django-8-0

填寫你的詳情在註冊表單中並且完成註冊流程。PayPal會發送給你一封e-mail來核對你的賬戶。

安裝django-paypal

Django-paypal是一個第三方django應用,它可以簡化集成PayPal到Django項目中。我們將要使用它來集成PayPal支付標準解決方案到我們的商店中。你可以找到django-paypal的文檔,訪問 http://django-paypal.readthedocs.org/。

安裝django-paypal在shell中通過以下命令:

pip install django-paypal==0.2.5n

(譯者註:現在應該有最新版本,書上使用的是0.2.5版本)

編輯你的項目中的settings.py文件,添加paypal.standard.ipn到INSTALLED_APPS設置中,如下所示:

INSTALLED_APPS = (n # ...n paypal.standard.ipn,n)n

這個應用提供自django-paypal來集成PayPal支付標準通過Instant Payment Notification(IPN)。我們之後會操作支付通知。

添加以下設置到myshopsettings.py文件來配置django-paypal:

# django-paypal settingsnPAYPAL_RECEIVER_EMAIL = mypaypalemail@myshop.comnPAYPAL_TEST = Truen

以上兩個設置含義如下:

  • PAYPAL_RECEIVER_EMAIL:你的PayPal賬戶e-mail。使用你創建的PayPal賬戶e-mail替換mypaypalemail@myshop.com
  • PAYPAL_TEST:一個布爾類型指示是否PayPal的沙箱環境,該環境可以用來處理支付。這個沙箱允許你測試你的PayPal集成在遷移到一個正式生產的環境之前。

打開shell運行如下命令來同步django-paypal的模型(models)到資料庫中:

python manage.py migraten

你會看到如下類似的輸出:

Running migrations:n Rendering model states... DONEn Applying ipn.0001_initial... OKn Applying ipn.0002_paypalipn_mp_id... OKn Applying ipn.0003_auto_20141117_1647... OKn

django-paypal的模型(models)如今已經同步到了資料庫中。你還需要添加django-paypal的URL模式到你的項目中。編輯主的urls.py文件,該文件位於myshop目錄,然後添加以下的URL模式。記住粘貼該URL模式要在shop.urls模式之前為了避免錯誤的模式匹配:

url(r^paypal/, include(paypal.standard.ipn.urls)),n

讓我們添加支付網關到結賬流程中。

添加支付網關

結賬流程工作如下:

  • 1.用戶添加物品到他們的購物車中
  • 2.用戶結賬他們的購物車
  • 3.用戶被重定向到PayPal進行支付
  • 4.PayPal發送一個支付通知給我們的站點
  • 5.PayPal重定向用戶回到我們的網站

創建一個新的應用到你的項目中使用如下命令:

python manage.py startapp paymentn

我們將要使用這個應用去管理結賬過程和用戶支付。

編輯你的項目的settings.py文件,添加payment到INSTALLED_APPS設置中,如下所示:

INSTALLED_APPS = (n # ...n paypal.standard.ipn,n payment,n)n

payment應用現在已經在項目中激活。編輯orders應用的views.py文件並且確保包含以下導入:

from django.shortcuts import render, redirectnfrom django.core.urlresolvers import reversen

替換以下order_create視圖(view)的內容:

# launch asynchronous tasknorder_created.delay(order.id)nreturn render(request, orders/order/created.html, locals())n

新的內容為:

# launch asynchronous tasknorder_created.delay(order.id) # set the order in the sessionnrequest.session[order_id] = order.id # redirect to the paymentnreturn redirect(reverse(payment:process))n

在成功的創建一個新的訂單之後,我們設置這個訂單ID到當前的會話中使用order_id會話鍵(session key)。之後,我們重定向用戶到payment:processURL,這個我們下一步就是創建。

編輯payment應用的views.py文件然後添加如下代碼:

from decimal import Decimalnfrom django.conf import settingsnfrom django.core.urlresolvers import reversenfrom django.shortcuts import render, get_object_or_404nfrom paypal.standard.forms import PayPalPaymentsFormnfrom orders.models import Ordernndef payment_process(request):n order_id = request.session.get(order_id)n order = get_object_or_404(Order, id=order_id)n host = request.get_host()n paypal_dict = {n business: settings.PAYPAL_RECEIVER_EMAIL,n amount: %.2f % order.get_total_cost().quantize(n Decimal(.01)),n item_name: Order {}.format(order.id),n invoice: str(order.id),n currency_code: USD,n notify_url: http://{}{}.format(host,n reverse(paypal-ipn)),n return_url: http://{}{}.format(host,n reverse(payment:done)),n cancel_return: http://{}{}.format(host,n reverse(payment:canceled)),n }n form = PayPalPaymentsForm(initial=paypal_dict)n return render(request,n payment/process.html,n {order: order, form:form})n

在payment_process視圖(view)中,我們生成了一個PayPal的Buy now按鈕用來支付一個訂單。首先,我們拿到當前的訂單從order_id會話鍵中,這個鍵值被之前的order_create視圖(view)設置。我們拿到這個order對象通過給予的ID並且構建一個新的PayPalPaymentsForm,該表單表單包含以下欄位:

  • business:PayPal商業賬戶用來處理支付。我們使用e-mail賬戶,該賬戶定義在PAYPAL_RECEIVER_EMAIL設置那裡。
  • amount:向顧客索要的總價。
  • item_name:正在出售的商品名。我們使用訂單ID,因為訂單可能包含很多產品。
  • currency_code:本次支付的貨幣。我們設置這裡為USD使用U.S. Dollar(譯者註:傳說中的美金)。需要使用相同的貨幣,該貨幣被設置在你的PayPal賬戶中(例如:EUR 對應歐元)。
  • notify_url:這個URL PayPal將會發送IPN請求過去。我們使用django-paypal提供的paypal-ipn URL。這個視圖(view)與這個URL關聯來操作支付通知以及存儲它們到資料庫中。
  • return_url:這個URL用來重定向用戶當他的支付成功之後。我們使用URL payment:done,這個我們接下來會創建。
  • cancel_return:這個URL用來重定向用戶如果這個支付被取消或者有其他問題。我們使用URL payment:canceled,這個我們接下來會創建。

PayPalpaymentsForm將會被渲染成一個標準表單帶有隱藏的欄位,並且用戶將來只能看到Buy now按鈕。當用戶點擊該按鈕,這個表單將會提交到PayPal通過POST渠道。

讓我們創建簡單的視圖(views)給PayPal用來重定向用戶當支付成功,或者當支付被取消因為某些原因。添加以下代碼到相同的views.py文件:

from django.views.decorators.csrf import csrf_exemptnn@csrf_exemptndef payment_done(request):n return render(request, payment/done.html)nn@csrf_exemptndef payment_canceled(request):n return render(request, payment/canceled.html)n

我們使用csrf_exempt裝飾器來避免Django期待一個CSRF標記,因為PayPal能重定向用戶到以上兩個視圖(views)通過POST渠道。創建新的文件在payment應用目錄下並且命名為urls.py。添加以下代碼:

from django.conf.urls import urlnfrom . import viewsnurlpatterns = [n url(r^process/$, views.payment_process, name=process),n url(r^done/$, views.payment_done, name=done),n url(r^canceled/$, views.payment_canceled, name=canceled),n]n

這些URL是給支付工作流的。我們已經包含了以下URL模式:

  • process:給這個視圖(view)用來生成PayPal表單給Buy now按鈕。
  • done:給PayPal用來重定向用戶當支付成功的時候。
  • canceled:給PayPal用來重定向用戶當支付取消的時候。

編輯主的myshop項目的urls.py文件,包含URL模式給payment應用:

url(r^payment/, include(payment.urls,namespace=payment)),n

記住粘貼以上內容在shop.urls模式之前用來避免錯誤的模式匹配。

創建以下文件建構在payment應用目錄下:

templates/n payment/n process.htmln done.htmln canceled.htmln

編輯payment/process.html模板(template)並且添加以下代碼:

{% extends "shop/base.html" %}nn{% block title %}Pay using PayPal{% endblock %}nn{% block content %}n <h1>Pay using PayPal</h1>n {{ form.render }}n{% endblock %}n

這個模板(template)會渲染PayPalPaymentsForm並且展示Buy now按鈕。

編輯payment/done.html模板(template)並且添加如下代碼:

{% extends "shop/base.html" %}n{% block content %}n <h1>Your payment was successful</h1>n <p>Your payment has been successfully received.</p>n{% endblock %}n

這個模板(template)的頁面給用戶重定向當成功支付之後。

編輯payment/canceled.html模板(template)並且添加以下代碼:

{% extends "shop/base.html" %}n{% block content %}n <h1>Your payment has not been processed</h1>n <p>There was a problem processing your payment.</p>n{% endblock %}n

這個模板(template)的頁面給用戶重定向當有這個支付過程出現問題或者用戶取消了這次支付。

讓我們嘗試完成的支付過程。

使用PayPal的沙箱

打開 PayPal Developer 在你的瀏覽器中然後進行登錄使用你的PayPal商業賬戶。點擊Dashboard菜單項,在左方菜單點擊Accounts選項在Sandbox下方。你會看到你的沙箱測試賬戶列,如下所示:

django-8-1

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

點擊Personal Account在列中擴大它,之後點擊Profile鏈接。你會看到一些信息關於這個測試賬戶包含e-mail和profile信息,如下所示:

django-8-2

在Funding tab中,你會找到銀行賬戶,信用卡日期,以及PayPal信用餘額。

這些測試賬戶能夠被用來做支付在你的網站中當使用沙箱環境。跳轉到Profile tab然後點擊Change password鏈接。創建一個定製密碼給這個測試賬戶。

打開shell並且啟動開發伺服器使用命令python manage.py runserver。打開 http://127.0.0.1:8000 在你的瀏覽器中,添加一些產品到購物車中,並且填寫結賬表單。當你點擊Place order按鈕,這個訂單會被保存在資料庫中,這個訂單ID會被保存在當前的會話中,並且你會被重定向到支付處理頁面。這個頁面從會話中獲取訂單並且渲染PayPal表單顯示一個Buy now按鈕,如下所示:

django-8-3

你可以看下HTML源碼來看下生成的表單欄位。

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

django-8-4

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

django-8-5

現在,點擊Pay now按鈕。最後,你會看到批准頁面該頁面包含你的交易ID。這個頁面看上去如下所示:

django-8-6

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

django-8-7

這個支付已經成功了。然而,PayPal並沒有發送一個支付狀態通知給我們的應用,因為我們運行我們的項目在我們本地主機,IP是 127.0.0.1 這並不是一個公開地址。我們將要學習如何使我們的站點可以從Internet訪問並且接收IPN通知。

獲取支付通知

IPN是一個方法提供自大部分的支付網關用來跟蹤實時的購買。一個通知會立即發送到你的服務當這個網關處理了一個支付。這個通知包含所有支付詳情,包括狀態以及一個支付的簽名,該簽名可以用來確定這個消息的來源點。這個消息被發送通過一個單獨的HTTP請求給你的服務。在出現連接問題的情況下,PayPal將會多次企圖通知你的站點。

django-paypal應用內置兩種不同的信號給IPNs。如下:

  • valid_ipn_received:會被觸發當IPN信息獲取自PayPal是正確的並且不是一個已存在資料庫中的消息的複製。
  • invalid_ipn_received:這個信號會觸發當IPN獲取自PayPal包含無效的數據或者不是一個良好的形式。

我們將要創建一個定製的接受函數並且連接它給valid_ipn_received信號用來確定支付。

創建新的文件在payment應用目錄下,並且命名為signals.py,添加如下代碼:

from django.shortcuts import get_object_or_404nfrom paypal.standard.models import ST_PP_COMPLETEDnfrom paypal.standard.ipn.signals import valid_ipn_receivednfrom orders.models import Ordernndef payment_notification(sender, **kwargs):n ipn_obj = sendern if ipn_obj.payment_status == ST_PP_COMPLETED:n # payment was successfuln order = get_object_or_404(Order, id=ipn_obj.invoice)n # mark the order as paidn order.paid = Truen order.save()nnvalid_ipn_received.connect(payment_notification)n

我們連接payment_notification接收函數給django-paypal提供的valid_ipn_received信號。這個接收函數工作如下:

  • 1.我們獲取發送對象,該對象是一個PayPalIPN模型的實例,位於paypal.standard.ipn.models。
  • 2.我們檢查payment_status屬性來確保它和django-payapl的完整狀態相同。這個狀態指示這個支付已經成功處理。
  • 3.之後我們使用get_object_or_404()快捷函數來拿到訂單,該訂單的ID匹配invoice參數我們之前提供給PayPal。
  • 4.我們備註這個訂單已經支付通過設置它的paid屬性為True並且保存這個訂單對象到資料庫中。

你需要確保你的信號方法已經載入,這樣這個接收函數會被調用當valid_ipn_received信號被觸發的時候。The best practice is to load your signals when the application containing them is loaded. (譯者註:誰幫我翻一下,好拗口啊)。這能夠實現通過定義一個定製應用配置,這方面會在下一節進行解釋。

配置我們的應用

你已經學習了關於應用的配置在第六章 跟蹤用戶操作。我們將要定義一個定製配置給我們的payment應用為了載入我們的信號接收函數。

創建一個新的文件在payment應用目錄下命名為apps.py。添加如下代碼:

from django.apps import AppConfignnclass PaymentConfig(AppConfig):n name = paymentn verbose_name = Paymentnn def ready(self):n # import signal handlersn import payment.signalsn

在上述代碼中,我們定義了一個定製AppConfif類給payment應用。name參數是這個應用的名字,verbose_name包含可讀的樣式。我們導入信號方法在ready()方法中確保它們會被載入當這個應用初始化的時候。

編輯payment應用的init.py文件,添加以下行:

default_app_config = payment.apps.PaymentConfign

以上操作可以使Django動態載入你的定製應用配置類。你可以找到更進一步的信息關於應用配置,通過訪問 Applications | Django documentation | Django 。

測試支付通知

由於我們工作在本地環境中,我們需要確保我們的站點可以被PayPal獲得。有不少應用允許你使你的開發環境在Internet中可獲得。我們將要使用Ngrok,它就是其中一個最著名的。

./ngrok http 8000n

通過這條命名,你告訴Ngrok去創建一條隧道給你的本地主機在埠8000上並且分配一個Internet可訪問主機名給它。你可以看到如下類似輸出:

Tunnel Status onlinenVersion 2.0.17/2.0.17nWeb Interface http://127.0.0.1:4040nForwarding http://1a1b50f2.ngrok.io -> localhost:8000nForwarding https://1a1b50f2.ngrok.io -> localhost:8000nnConnnections ttl opn rt1 rt5 p50 p90n 0 0 0.00 0.00 0.00 0.00n

Ngrok告訴我們關於我們的站點,運行在本地8000埠使用Django開發伺服器,已經可以在Internet訪問到通過URLs http://1a1b50f2.ngrok.io 以及 https://1a1b50f2.ngrok.io ,前者是HTTP,後者是HTTPS。Ngrok還提供一個URL來訪問一個web介面用來顯示信息關於發送到這個服務的請求。

打開Ngrok提供的URL在瀏覽器中;例如,http://1a1b50f2.ngrok.io 。添加一些產品到購物車中,放置一個訂單,然後使用你的PayPal測試賬戶進行支付。這個時候,PayPal將能夠拿到這個URL,這個URL由PayPalPaymentsForm的notify_url欄位生成,在payment_process視圖(view)中。如果你看一下這個渲染過的表單,你會看到這個HTML表單欄位看上去如下所示:

<input id="id_notify_url" name="notify_url" type="hidden"nvalue="http://1a1b50f2.ngrok.io/paypal/">n

在結束支付過程之後,打開 127.0.0.1:8000/admin/ip 在你的瀏覽器中。你會看到一個IPN對象對應最新的支付狀態為Completed。這個對象包含所有的支付信息,該對象由PayPal發送給你提供給IPN通知的URL。IPN管理列展示頁面看上去如下所示:

django-8-8

你還可以啟動IPNs通過使用PayPal的IPN模擬器位於 IPN Simulator - PayPal Developer 。這個模擬器允許你指定欄位和發送的通知類型。

除了PayPal支付標準外,PayPal提供Website Payments Pro,它是一個訂購服務允許你接受支付在你的站點中而不需要重定向用戶到PayPal。你可以找到更多信息關於如何集成Website Payments Pro,通過訪問 http://django-paypal.readthedocs.org/en/v0.2.5/pro/index.html。

導出訂單為CSV文件

有時候,你可能想要導出包含在模型的信息到一個文件中,這樣你可以導入它到其他的系統中。其中一個範圍最廣的格式用來導出/導入數據就是Comma-Separated Values(CSV)。一個CSV文件就是一個純文本文件包含若干記錄。一個csv文件中,通常一行記錄為一個訂單,以及一些分割符(通常是逗號)。我們將要定製管理平台站點能夠導出訂單為CSV文件。

添加定製操作到管理平台站點中

Django提供你多種不同的選項來定製管理平台站點。我們將要修改對象列視圖(view)來包含一個定製的管理操作。

一個管理操作工作如下:一個用戶選擇對象從管理對象列頁面通過複選框,之後選擇一個操作去執行在所有被選擇的項上,然後執行該操作。以下

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

django-8-9

創建定製管理操作允許管理人員一次性應用操作多個元素。

你可以創建一個定製操作通過編寫一個經常性的函數獲取以下參數:

  • 當前展示的ModelAdmin
  • 當前請求對象,一個HttpRequest實例
  • 一個查詢集(QuerySet)給用戶所選擇的對象

這個函數將會被執行當這個操作被觸發在管理平台站點上。

我們將要創建一個定製管理操作來下載訂單列表的CSV文件。編輯orders應用的admin.py文件,添加如下代碼在OrderAdmin類之前:

import csvnimport datetimenfrom django.http import HttpResponsendef export_to_csv(modeladmin, request, queryset):nn opts = modeladmin.model._metan response = HttpResponse(content_type=text/csv)n response[Content-Disposition] = attachment; n filename={}.csv.format(opts.verbose_name)n writer = csv.writer(response)n fields = [field for field in opts.get_fields() if not field.many_to_many and not field.one_to_many]n # Write a first row with header informationn writer.writerow([field.verbose_name for field in fields])n # Write data rowsn for obj in queryset:n data_row = []n for field in fields:n value = getattr(obj, field.name)n if isinstance(value, datetime.datetime):n value = value.strftime(%d/%m/%Y)n data_row.append(value)n writer.writerow(data_row)n return responsenexport_to_csv.short_description = Export to CSVn

在這代碼中,我們執行以下任務:

  • 1.我們創建一個HttpResponse實例包含一個定製text/csv內容類型來告訴瀏覽器這個響應需要處理為一個CSV文件。我們還添加一個Content-Disposition頭來指示這個HTTP響應包含一個附件。
  • 2.我們創建一個CSV writer對象,該對象將會被寫入response對象。
  • 3.我們動態的獲取model欄位通過使用模型(moedl)_meta選項的get_fields()方法。我們排除多對多以及一對多的關係。
  • 4.我們編寫了一個頭行包含欄位名。
  • 5.我們迭代給予的查詢集(QuerySet)並且為每一個查詢集中返回的對象寫入行。我們注意格式化datetime對象因為這個輸出值給CSV必須是一個字元串。
  • 6.我們定製這個操作的顯示名在模板(template)中通過設置一個short_description屬性給這個函數。

我們已經創建了一個普通的管理操作可以添加到任意的ModelAdmin類。

最後,添加新的export_to_csv管理操作給OrderAdmin類如下所示:

class OrderAdmin(admin.ModelAdmin):n # ...n actions = [export_to_csv]n

打開 127.0.0.1:8000/admin/or 在你的瀏覽器中。管理操作看上去如下所示:

django-8-10

選擇一些訂單然後選擇Export to CSV操作從下拉選框中,之後點擊Go按鈕。你的瀏覽器會下載生成的CSV文件名為order.csv。打開下載的文件使用一個文本編輯器。你會看到的內容如以下的格式,包含一個頭行以及你之前選擇的每行訂單對象:

ID,first name,last name,email,address,postalncode,city,created,updated,paidn3,Antonio,Mele?,antonio.mele@gmail.com,Bank Street 33,WS J11,London,25/05/2015,25/05/2015,Falsen...n

如你所見,創建管理操作是非常簡單的。

擴展管理站點通過定製視圖(view)

有時候你可能想要定製管理平台站點,比如處理ModelAdmin的配置,管理操作的創建,以及覆蓋管理模板(templates)。在這樣的場景中,你需要創建一個定製的管理視圖(view)。通過一個定製的管理視圖(view),你可以構建任何你需要的功能。你只需要確保只有管理用戶能訪問你的視圖並且你維護這個管理的外觀和感覺通過你的模板(template)擴展自一個管理模板(template)。

讓我們創建一個定製視圖(view)來展示關於一個訂單的信息。編輯orders應用下的views.py文件,添加以下代碼:

from django.contrib.admin.views.decorators import staff_member_requirednfrom django.shortcuts import get_object_or_404nfrom .models import Ordernn@staff_member_requiredndef admin_order_detail(request, order_id):n order = get_object_or_404(Order, id=order_id)n return render(request,n admin/orders/order/detail.html,n {order: order})n

這個staff_member_required裝飾器檢查用戶請求這個頁面的is_active以及is_staff欄位是被設置為True。在這個視圖(view)中,我們獲取Order對象通過給予的id以及渲染一個模板來展示這個訂單。

現在,編輯orders應用中的urls.py文件並且添加以下URL模式:

url(r^admin/order/(?P<order_id>d+)/$,n views.admin_order_detail,n name=admin_order_detail),n

創建以下文件結構在orders應用的templates/目錄下:

admin/n orders/n order/n detail.htmln

編輯detail.html模板(template),添加以下內容:

{% extends "admin/base_site.html" %}n{% load static %}nn{% block extrastyle %}n <link rel="stylesheet" type="text/css" href="{% static "css/admin.css" %}" />n{% endblock %}nn{% block title %}n Order {{ order.id }} {{ block.super }}n{% endblock %}nn{% block breadcrumbs %}n <div class="breadcrumbs">n <a href="{% url "admin:index" %}">Home</a> ?n <a href="{% url "admin:orders_order_changelist" %}">Orders</a>n ?n <a href="{% url "admin:orders_order_change" order.id %}">Order {{ order.id }}</a>n ? Detailn </div>n{% endblock %}nn{% block content %}n <h1>Order {{ order.id }}</h1>n <ul class="object-tools">n <li>n <a href="#" onclick="window.print();">Print order</a>n </li> n </ul>n <table> n <tr>n <th>Created</th>n <td>{{ order.created }}</td>n </tr>n <tr>n <th>Customer</th>n <td>{{ order.first_name }} {{ order.last_name }}</td>n </tr> n <tr>n <th>E-mail</th>n <td><a href="mailto:{{ order.email }}">{{ order.email }}</a></td>n </tr>n <tr>n <th>Address</th>n <td>{{ order.address }}, {{ order.postal_code }} {{ order.cityn}}</td>n </tr> n <tr>n <th>Total amount</th>n <td>${{ order.get_total_cost }}</td>n </tr>n <tr>n <th>Status</th>n <td>{% if order.paid %}Paid{% else %}Pending payment{% endif %}</td> n </tr>n </table>nn <div class="module">n <div class="tabular inline-related last-related">n <table>n <h2>Items bought</h2>n <thead>n <tr>n <th>Product</th>n <th>Price</th>n <th>Quantity</th>n <th>Total</th>n </tr>n </thead>n <tbody>n {% for item in order.items.all %}n <tr class="row{% cycle "1" "2" %}">n <td>{{ item.product.name }}</td>n <td class="num">${{ item.price }}</td>n <td class="num">{{ item.quantity }}</td>n <td class="num">${{ item.get_cost }}</td>n </tr>n {% endfor %}n <tr class="total">n <td colspan="3">Total</td>n <td class="num">${{ order.get_total_cost }}</td>n </tr>n </tbody>n </table>n </div>n </div>n{% endblock %}n

這個模板(template)是用來顯示一個訂單詳情在管理平台站點中。這個模板(template)擴展Djnago的管理平台站點的admin/base_site.html模板,它包含管理的主要HTML結構和CSS樣式。我們載入定製的靜態文件css/admin.css

為了使用靜態文件,你需要拿到它們從這章教程的實例代碼中。複製位於orders應用的static/目錄下的靜態文件然後添加它們到你的項目的相同位置。

我們使用定義在父模板(template)的區塊包含我們自己的內容。我們展示信息關於訂單和購買的商品。

當你想要擴展一個管理模板(template),你需要知道它的結構以及確定存在的區塊。你可以找到所有管理模板(template),通過訪問 django/django 。

你也可以重寫一個管理模板(template)如果你需要的話。為了重寫一個管理模板(template),拷貝它到你的template目錄保持相同的相對路徑以及文件名。Django管理平台站點將會使用你的定製模板(template)替代默認的模板。

最後,讓我們添加一個鏈接給每個Order對象在管理平台站點的列展示頁面。編輯orders應用的admin.py文件然後添加以下代碼,在OrderAdmin類上面:

from django.core.urlresolvers import reversendef order_detail(obj):n return <a href="{}">View</a>.format(n reverse(orders:admin_order_detail, args=[obj.id]))norder_detail.allow_tags = Truen

這個函數需要一個Order對象作為參數並且返回一個HTML鏈接給admind_order_detailURL。Django會避開默認的HTML輸出。我們必須設置allow_tags屬性為True來避開auto-escaping。

設置allow_tags屬性為True來避免HTML-escaping在一些Model方法,ModelAdmin方法,以及任何其他的調用中。當你使用allow_tags的時候,能確保避開用戶輸入的跨域腳本。

之後,編輯OrderAdmin類來展示鏈接:

class OrderAdmin(admin.ModelAdmin):n list_display = [id,n first_name, n # ... n updated, n order_detail]n

打開 127.0.0.1:8000/admin/or 在你的瀏覽器中。每一行現在都會包含一個View鏈接如下所示:

django-8-11

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

django-8-12

生成動態的PDF發票

如今我們已經有了一個完整的結賬和支付系統,我們可以生成一張PDF發票給每個訂單。有幾個Python庫可以生成PDF文件。一個最流行的生成PDF的Python庫是Reportlab。你可以找到關於如何使用Reportlab輸出PDF文件的信息,通過訪問 Outputting PDFs with Django 。

在大部分的場景中,你還需要添加定製樣式和格式給你的PDF文件。你會發現渲染一個HTML模板(template)以及轉化該模板(template)為一個PDF文件更加的方便,保持Python遠離表現層。我們要遵循這個方法並且使用一個模塊來生成PDF文件通過Django。我們將要使用WeasyPrint,它是一個Python庫可以生成PDF文件從HTML模板中。

安裝WeasyPrint

首先,安裝WeasyPrint的依賴給你的OS,這些依賴你可以找到通過訪問 Redirecting… 。

之後,安裝WeasyPrint通過pip渠道使用如下命令:

pip install WeasyPrint==0.24n

創建一個PDF模板(template)

我們需要一個HTML文檔給WeasyPrint輸入。我們將要創建一個HTML模板(template),渲染它使用Django,並且傳遞它給WeasyPrint來生成PDF文件。

創建一個新的模板(template)文件在orders應用的templates/orders/order/目錄下命名為pdf.html*。添加如下內容:

<html>n<body>n <h1>My Shop</h1>n <p>n Invoice no. {{ order.id }}</br>n <span class="secondary">n {{ order.created|date:"M d, Y" }}n </span>n </p>nn <h3>Bill to</h3>n <p>n {{ order.first_name }} {{ order.last_name }}<br>n {{ order.email }}<br>n {{ order.address }}<br>n {{ order.postal_code }}, {{ order.city }}n </p>n <h3>Items bought</h3>n <table>n <thead> n <tr>n <th>Product</th>n <th>Price</th>n <th>Quantity</th>n <th>Cost</th>n </tr>n </thead>n <tbody>n {% for item in order.items.all %}n <tr class="row{% cycle "1" "2" %}">n <td>{{ item.product.name }}</td>n <td class="num">${{ item.price }}</td>n <td class="num">{{ item.quantity }}</td>n <td class="num">${{ item.get_cost }}</td>n </tr>n {% endfor %}n <tr class="total">n <td colspan="3">Total</td>n <td class="num">${{ order.get_total_cost }}</td>n </tr>n </tbody>n </table>nn <span class="{% if order.paid %}paid{% else %}pending{% endif %}">n {% if order.paid %}Paid{% else %}Pending payment{% endif %}n </span>n</body>n</html>n

這個模板(template)就是PDF發票。在這個模板(template)中,我們展示所有訂單詳情以及一個HTML <table> 元素包含所有商品。我們還包含了一條消息來展示如果該訂單已經支付或者支付還在進行中。

渲染PDF文件

我們將要創建一個視圖(view)來生成PDF發票給存在的訂單通過使用管理平台站點。編輯order應用的views.py文件添加如下代碼:

from django.conf import settingsnfrom django.http import HttpResponsenfrom django.template.loader import render_to_stringnimport weasyprintnn@staff_member_requiredndef admin_order_pdf(request, order_id):n order = get_object_or_404(Order, id=order_id)n html = render_to_string(orders/order/pdf.html,n {order: order})n response = HttpResponse(content_type=application/pdf)n response[Content-Disposition] = filename=n "order_{}.pdf".format(order.id)n weasyprint.HTML(string=html).write_pdf(response,n stylesheets=[weasyprint.CSS(n settings.STATIC_ROOT + css/pdf.css)])n return responsen

這個視圖(view)用來生成一個PDF發票給一個訂單。我們使用staff_member_required裝飾器來確保只有管理人員能夠訪問這個視圖(view)。我們獲取Order對象通過給予的ID並且我們使用rander_to_string()函數提供自Django來渲染orders/order/pdf.html。這個渲染過的HTML會被保存到html變數中。之後,我們生成一個新的HttpResponse對象指定application/pdf的內容類型並且包含Content-Disposition頭來指定這個文件名。我們使用WeasyPrint來生成一個PDF文件從渲染的HTML代碼中並且將該文件寫入HttpResponse對象中。我們載入它從本地路徑通過使用STATIC_ROOT設置。最後,我們返回這個生成的響應。

由於我們需要使用STATIC_ROOT設置,我們需要添加它到我們的項目中。這個項目將會是靜態文件的所在地。編輯myshop項目的settings.py文件,添加如下設置:

STATIC_ROOT = os.path.join(BASE_DIR, static/)n

之後,運行命令python manage.py collectstatic。你會在輸出末尾看到如下輸出:

You have requested to collect static files at the destinationnlocation as specified in your settings:nn code/myshop/staticnThis will overwrite existing files!nAre you sure you want to do this?n

輸入yes然後回車。你會得到一條消息,告知那個靜態文件已經複製到STATIC_ROOT目錄中。

collectstatic命令複製所有靜態文件從你的應用到定義在STATIC_ROOT設置的目錄中。這允許每個應用去提供它自己的靜態文件通過使用一個static/目錄來包含它們。你還可以提供額外的靜態文件來源在STATICFILES_DIRS設置。所有的目錄被指定在STATICFILED_DIRS列中的都將會被複制到STATIC_ROOT目錄中當collectstatic被執行的時候。

編輯orders應用目錄下的urls.py文件並且添加如下URL模式:

url(r^admin/order/(?P<order_id>d+)/pdf/$,n views.admin_order_pdf,n name=admin_order_pdf),n

現在,我們可以編輯管理列展示頁面給Order模型(model)來添加一個鏈接給PDF文件給每一個結果。編輯orders應用的admin.py文件並且添加以下代碼在OrderAdmin類上面:

def order_pdf(obj):n return <a href="{}">PDF</a>.format(n reverse(orders:admin_order_pdf, args=[obj.id]))norder_pdf.allow_tags = Truenorder_pdf.short_description = PDF billn

添加order_pdf給OrderAdmin類的list_display屬性:

class OrderAdmin(admin.ModelAdmin):n list_display = [id,n # ... n order_detail, n order_pdf]n

如果你指定一個short_description屬性給你的調用,Django將會使用它給這個列命名。

打開 127.0.0.1:8000/admin/or 在你的瀏覽器中。每一行現在都包含一個PDF鏈接,如下所示:

django-8-13

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

django-8-14

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

django-8-15

通過e-mail發送PDF文件

讓我們發送一封e-mail給我們的顧客包含生成的PDF發表但一個支付被接收的時候。編輯payment應用下的signals.py文件並且添加如下導入:

from django.template.loader import render_to_stringnfrom django.core.mail import EmailMessagenfrom django.conf import settingsnimport weasyprintnfrom io import BytesIOn

之後添加如下代碼在order.save()行之後,需要同樣的縮進等級:

# create invoice e-mailnsubject = My Shop - Invoice no. {}.format(order.id)nmessage = Please, find attached the invoice for your recentnpurchase.nemail = EmailMessage(subject,n message,n admin@myshop.com,n [order.email])n# generate PDFnhtml = render_to_string(orders/order/pdf.html, {order: order})nout = BytesIO()nstylesheets=[weasyprint.CSS(settings.STATIC_ROOT + css/pdf.css)]nweasyprint.HTML(string=html).write_pdf(out,n stylesheets=stylesheets)n# attach PDF filenemail.attach(order_{}.pdf.format(order.id),n out.getvalue(),n application/pdf)n# send e-mailnemail.send()n

在這個信號中,我們使用Django提供的EmailMessage類來創建一個e-mail對象。之後我們渲染這個模板(template)到html變數中。我們生成PDF文件從渲染的模板(template)中,並且我們輸出它到一個BytesIO實例中,該實例是一個內容位元組緩存。之後我們附加這個生成的PDF文件到EmailMessage對象通過使用它的attach()方法,包含這個out緩存的內容。

記住設置你的SMTP設置在項目的settings.py文件中來發送e-mail。你可以到第二章 通過高級特性擴展你的blog去看下一個SMTP配置的例子。

現在你可以打開Ngrok提供給你的應用的URL然後完成一個新的支付處理為了收到PDF發票到你的e-mail中。

總結

在這一章中,你集成了一個支付網關到你的項目中。你定製了Django管理平台頁面並且學習到了如何動態的生成CSV以及PDF文件。

在下一章中將會給你一個深刻理解關於國際化和本地化給Django項目。你還會學習到創建一個贈券系統已經構建一個產品推薦引擎。

譯者總結

不知不覺,第八章也翻譯完成了,還是渣翻,精校之前大家先湊合著看吧,有問題我會及時更新。目前全書翻譯已完成三分之二,離不開各位的支持,我們下章再見!對了,本章完成日是三八婦女(女神?)節,各位女看客們節日快樂!


推薦閱讀:

請問安裝完anaconda後在開始的菜單中沒有Anaconda文件夾怎麼辦?
Python · 神經網路(五)· Cost & Optimizer
黃哥Python 轉載的霸氣文章"Yes, Python is Slow, and I Don』t Care"
python中WindowsError: [Error 32] 錯誤處理
梯度下降法快速教程 | 第一章:Python簡易實現以及對學習率的探討

TAG:Django框架 | Python | Python入门 |