测试开发之Django实战示例 第八章 管理支付与订单

news2024/12/24 11:41:28

第八章 管理支付与订单

上一章制作了一个带有商品品类展示和购物车功能的电商网站雏形,同时也学到了如何使用Celery给项目增加异步任务。本章将学习为网站集成支付网关以让用户通过信用卡付款,还将为管理后台扩展两项功能:将数据导出为CSV以及生成PDF发票。

本章的主要内容有:

  • 集成支付网关到项目中

  • 将订单数据导出成CSV文件

  • 为管理后台创建自定义视图

  • 动态生成PDF发票

1集成支付网关

支付网关是一种处理在线支付的网站或者程序,使用支付网关,就可以管理用户的订单,然后将支付过程交给一个可信赖且安全的第三方,而无需在我们自己的站点上处理支付信息。

支付网关有很多可供选择,我们将要集成的是叫做"Braintree"的支付网关。Braintree使用较为广泛,是Uber和Airbnb的支付服务提供商。Braintree提供了一套API用于支持信用卡,PayPal,Android Pay和Apple Pay等支付方式,官方网站在https://www.braintreepayments.com/。

Braintree提供了很多集成的方法,最简单的集成方式就是Drop-in集成,包含一个预先建立好的支付表单。但是为了自定义一些支付过程中的内容,这里选择使用高级的Hosted Field(字段托管)方式进行集成。在https://developers.braintreepayments.com/guides/hosted-fields/overview/javascript/v3可以看到详细的帮助文档。

支付表单中包含的信用卡号,CVV码,过期日期等信息必须要得到安全处理,Hosted Field集成方式将这些字段展示给用户的时候,在页面中渲染的是一个iframe框架。我们可以来自定义该字段的外观,但必须要遵循Payment Card Industry (PCI)安全支付的要求。由于可以修改外观,用户并不会注意到页面使用了iframe。

译者注:原书在这里说的不是很清晰。Hosted Fields的意思是敏感字段由我们页面中的Braintree JavaScript客户端通过Braintree服务器生成并填入到页面中,而不是在模板中直接编写input字段。简单的说就是信用卡等敏感信息的字段是由Braintree托管生成,而不是我们自行编写。

1.1注册Braintree沙盒测试账户

需要注册一个Braintree账户。才能使用集成支付功能。我们先注册一个Braintree沙盒账户用于开发和测试。打开https://www.braintreepayments.com/sandbox,如下图所示:

填写表单创建用户,之后会收到电子邮件验证,验证通过之后在https://sandbox.braintreegateway.com/login进行登录。可以得到自己的商户ID和私有/公开密钥如下图所示:

这些信息与使用Braintree API进行验证交易有关,注意保存好私钥,不要泄露给他人。

1.2安装Braintree的Python模块

Braintree为Python提供了一个模块操作其API,源代码地址在https://github.com/braintree/braintree_python。我们将把这个braintree模块集成到站点中。

使用命令行安装braintree模块:

Copypip install braintree==3.45.0

之后在settings.py里配置:

Copy# Braintree支付网关设置
BRAINTREE_MERCHANT_ID = 'XXX'# 商户ID
BRAINTREE_PUBLIC_KEY = 'XXX'# 公钥
BRAINTREE_PRIVATE_KEY = 'XXX'# 私钥from braintree import Configuration, Environment

Configuration.configure(
    Environment.Sandbox,
    BRAINTREE_MERCHANT_ID,
    BRAINTREE_PUBLIC_KEY,
    BRAINTREE_PRIVATE_KEY
)

将BRAINTREE_MERCHANT_ID,BRAINTREE_PUBLIC_KEY,BRAINTREE_PRIVATE_KEY的值替换成你自己的实际信息。

注意此处的设置 Environment.Sandbox,,表示我们当前集成的是沙盒环境。如果站点正式上线并且获取了正式的Braintree账户,必须修改成Environment.Production。Braintree对于正式账号会有新的商户ID和公钥/私钥。

Braintree的基础设置结束了,下一步是将支付网关和支付过程结合起来。

1.3集成支付网关

结账过程是这样的:

  1. 将商品加入到购物车

  1. 从购物车中选择结账

  1. 输入信用卡信息并且支付

针对支付功能,我们建立一个新的应用叫做payment:

Copypython manage.py startapp payment

编辑settings.py文件,激活该应用:

CopyINSTALLED_APPS = [
    # ...'payment.apps.PaymentConfig',
]

payment现在已经被激活。

客户成功提交订单后,必须将该页面重定向到一个支付过程页面(目前是重定向到一个简单的成功页面)。编辑orders应用中的views.py,增加如下导入:

Copyfrom django.urls import reverse
from django.shortcuts import render, redirect

在同一个文件内,将order_create视图的如下部分:

Copy# 启动异步任务
order_created.delay(order.id)
return render(request, 'orders/order/created.html', locals())

替换成:

Copy# 启动异步任务
order_created.delay(order.id)
# 在session中加入订单id
request.session['order_id'] = order.id# 重定向到支付页面return redirect(reverse('payment:process'))

这样修改后,在成功创建订单之后,session中就保存了订单ID的变量order_id,然后用户被重定向至payment:process URL,这个URL稍后会编写。

注意必须为order_created视图启动Celery。

每次我们向Braintree中发送一个交易请求的时候,会生成一个唯一的交易ID号。因此我们在Order模型中增加一个字段用于存储这个交易ID号,这样可以将订单与Braintree交易联系起来。

编辑orders应用的models.py文件,为Order模型新增一行:

CopyclassOrder(models.Model):
    # ...
    braintree_id = models.CharField(max_length=150, blank=True)

之后执行数据迁移程序,每一个订单都会保存与其关联的交易ID。目前准备工作都已经做完,剩下就是在支付过程中使用支付网关。

1.3.1使用Hosted Fields进行支付

Hosted Fields方式允许我们创建自定义的支付表单,使用自定义样式和表现形式。Braintree JavaScript SDK会在页面中动态的添加iframe框体用于展示Host Fields支付字段。当用户提交表单的时候,Hosted Fields会安全地提取用户的信用卡等信息,生成一个特征字符串(tokenize,令牌化)。如果令牌化过程成功,就可以使用这个特征字符串(token),通过视图中的braintree模块发起一个支付申请。

为此需要建立一个支付视图。这个视图的工作流程如下:

  1. 用户提交订单时,视图通过braintree模块生成一个token,这个token用于Braintree JavaScript 客户端生成支付表单,并不是最终发送给支付网关的token。为了方便以下把这个token称为临时token,把最终提交给Braintree网站的token叫做交易token。

  1. 视图渲染支付表单所在的模板。页面中的Braintree JavaScript 客户端使用临时token来生成页面中的支付表单。

  1. 用户输入信用卡信息并且提交支付表单后,Braintree JavaScript 客户端会生成交易token,将这个交易token通过POST请求发送到视图

  1. 视图获取交易token之后,通过braintree模块向网站提交交易请求。

了解了工作流程之后,来编写相关视图,编辑payment应用中的views.py文件,添加下列代码:

Copyimport braintree
from django.shortcuts import render, redirect, get_object_or_404
from orders.models import Order

defpayment_process(request):
    order_id = request.session.get('order_id')
    order = get_object_or_404(Order, id=order_id)

    if request.method == "POST":
        # 获得交易token
        nonce = request.POST.get('payment_method_nonce', None)
        # 使用交易token和附加信息,创建并提交交易信息
        result = braintree.Transaction.sale(
            {
                'amount': '{:2f}'.format(order.get_total_cost()),
                'payment_method_nonce': nonce,
                'options': {
                    'submit_for_settlement': True,
                }
            }
        )
        if result.is_success:
            # 标记订单状态为已支付
            order.paid = True# 保存交易ID
            order.braintree_id = result.transaction.id
            order.save()
            return redirect('payment:done')
        else:
            return redirect('payment:canceled')

    else:
        # 生成临时token交给页面上的JS程序
        client_token = braintree.ClientToken.generate()
        return render(request,
                      'payment/process.html',
                      {'order': order,
                       'client_token': client_token})

这个payment_process视图管理支付过程,工作流程如下 :

  1. 从session中取出由order_create视图设置的order_id变量。

  1. 获取Order对象,如果没找到,返回404 Not Found错误

  1. 如果接收到POST请求,获取交易token payment_method_nonce,使用交易token和braintree.Transaction.sale()方法生成新的交易,该方法的几个参数解释如下:

  1. amount:总收款金额

  1. payment_method_nonce:交易token,由页面中的Braintree JavaScript 客户端生成。

  1. options:其他选项,submit_for_settlement设置为True表示生成交易信息完毕的时候就立刻提交。

  1. 如果交易成功,通过设置paid属性为True,将订单标记为已支付,将交易ID存储到braintree_id属性中,之后重定向至payment:done,如果交易失败就重定向至payment:canceled。

  1. 如果视图接收到GET请求,生成临时token交给页面中的Braintree JavaScript 客户端。

下边建立支付成功和失败时的处理视图,在payment应用的views.py中添加下列代码:

Copydefpayment_done(request):
    return render(request, 'payment/done.html')
defpayment_canceled(request):
    return render(request, 'payment/canceled.html')

然后在payment目录下建立urls.py,为上述视图配置路由:

Copyfrom django.urls import path
from . import views

app_name = 'payment'

urlpatterns = [
    path('process/', views.payment_process, name='process'),
    path('done/', views.payment_done, name='done'),
    path('canceled/', views.payment_canceled, name='canceled'),
]

这是支付流程的路由,配置了如下URL模式:

  • process:处理支付的视图

  • done:支付成功的视图

  • canceled:支付未成功的视图

编辑myshop项目的根urls.py文件,为payment应用配置二级路由:

Copyurlpatterns = [
    # ...
    path('payment/', include('payment.urls', namespace='payment')),
    path('', include('shop.urls', namespace='shop')),
]

依然要注意这一行要放到shop.urls上边,否则无法被解析到。

之后是建立视图,在payment目录下建立templates/payment/目录,并在其中建立 process.html, done.html,canceled.html三个模板。先来编写process.html:

在payment应用内建立下列目录和文件结构:

Copytemplates/
    payment/
        process.html
        done.html
        canceled.html

编辑payment/process.html,添加下列代码:

Copy{% extends "shop/base.html" %}

{% block title %}Pay by credit card{% endblock %}

{% block content %}
  <h1>Pay by credit card</h1><formaction="."id="payment"method="post"><labelfor="card-number">Card Number</label><divid="card-number"class="field"></div><labelfor="cvv">CVV</label><divid="cvv"class="field"></div><labelfor="expiration-date">Expiration Date</label><divid="expiration-date"class="field"></div><inputtype="hidden"id="nonce"name="payment_method_nonce"value="">
    {% csrf_token %}
    <inputtype="submit"value="Pay"></form><!-- Load the required client component. --><scriptsrc="https://js.braintreegateway.com/web/3.29.0/js/client.min.js"></script><!-- Load Hosted Fields component. --><scriptsrc="https://js.braintreegateway.com/web/3.29.0/js/hosted-fields.min.js"></script><script>var form = document.querySelector('#payment');
    var submit = document.querySelector('input[type="submit"]');

    braintree.client.create({
        authorization: '{{ client_token }}'
    }, function (clientErr, clientInstance) {
        if (clientErr) {
            console.error(clientErr);
            return;
        }

        braintree.hostedFields.create({
            client: clientInstance,
            styles: {
                'input': {'font-size': '13px'},
                'input.invalid': {'color': 'red'},
                'input.valid': {'color': 'green'}
            },
            fields: {
                number: {selector: '#card-number'},
                cvv: {selector: '#cvv'},
                expirationDate: {selector: '#expiration-date'}
            }
        }, function (hostedFieldsErr, hostedFieldsInstance) {
            if (hostedFieldsErr) {
                console.error(hostedFieldsErr);
                return;
            }

            submit.removeAttribute('disabled');

            form.addEventListener('submit', function (event) {
                event.preventDefault();

                hostedFieldsInstance.tokenize(function (tokenizeErr, payload) {
                    if (tokenizeErr) {
                        console.error(tokenizeErr);
                        return;
                    }
                    // set nonce to send to the serverdocument.getElementById('nonce').value = payload.nonce;
                    // submit formdocument.getElementById('payment').submit();
                });
            }, false);
        });
    });
  </script>
{% endblock %}

这是用户填写信用卡信息并且提交支付的模板,我们用<div>替代<input>使用在信用卡号,CVV码和过期日期字段上。这些字段就是Braintree JavaScript客户端渲染的iframe字段。还使用了一个名称为payment_method_nonce的<input>元素用于提交交易ID到后端。

在模板中还导入了Braintree JavaScript SDK的client.min.js和Hosted Fields组件hosted-fields.min.js,然后执行了下列JS代码:

  1. 使用braintree.client.create()方法,传入client_token即payment_process视图里生成的临时token,实例化Braintree JavaScript 客户端。

  1. 使用braintree.hostedFields.create()实例化Hosted Field组件

  1. 给input字段应用自定义样式

  1. 给cardnumber,cvv, 和expiration-date字段设置id选择器

  1. 给表单的submit行为绑定一个事件,当表单被点击提交时,Braintree SDK 使用表单中的信息,生成交易token放入payment_method_nonce字段中,然后提交表单。

编辑payment/done.html文件,添加下列代码:

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

这是订单成功支付时用户被重定向的页面。

编辑canceled.html,添加下列代码:

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

这是订单未支付成功时用户被重定向的页面。之后我们来试验一下付款。

1.4测试支付

打开系统命令行窗口然后运行RabbitMQ:

Copyrabbitmq-server

再启动一个命令行窗口,启动Celery worker:

Copycelery -A myshop worker -l info

再启动一个命令行窗口,启动站点:

Copypython manage.py runserver

之后在浏览器中打开http://127.0.0.1:8000/加入一些商品到购物车,提交订单,当按下PLACE ORDER按钮后,订单信息被保存进数据库,订单ID被附加到session上,然后进入支付页面。

支付页面从session中取得订单id然后在iframe中渲染Hosted Fields,像下图所示:

可以看一下页面的HTML代码,从而理解什么是Hosted Fields。

针对沙盒测试环境,Braintree提供了一些测试用的信用卡资料,可以进行付款成功或失败的测试,可以在https://developers.braintreepayments.com/guides/credit-cards/testing-go-live/python找到,我们来使用4111 1111 1111 1111这个信用卡号,在CVV码中填入123,到期日期填入未来的某一天比如12/20:

之后点击Pay,应该可以看到成功页面:

说明付款已经成功。可以在https://sandbox.braintreegateway.com/login登录,然后在左侧菜单选Transaction里搜索最近的交易,可以看到如下信息:

译者注:Braintree网站在成书后有部分改版,读者看到的支付详情页面可能与上述图片有一些区别。

然后再查看管理站点http://127.0.0.1:8000/admin/orders/order/中的对应记录,该订单应该已经被标记为已支付,而且记录了交易ID,如下图所示:

我们现在就成功集成了支付功能。

1.5正式上线

在沙盒环境中测试通过之后,需要正式上线的话,需要到https://www.braintreepayments.com创建正式账户。

在部署到生产环境时,需要将settings.py中的商户ID和公钥私钥更新为正式账户的对应信息,然后将其中的Environment.Sandbox修改为Environment.Production。正式上线的具体步骤可以参考:https://developers.braintreepayments.com/start/go-live/python。

2导出订单为CSV文件

有时我们想将某个模型中的数据导出为一个文件,用于在其他系统导入。常用的一种数据交换格式是CSV(逗号分隔数据)文件。CSV文件是一个纯文本文件,包含很多条记录。通常一行是一条记录,用特定的分隔符(一般是逗号)分隔每个字段的值。我们准备自定义管理后台,增加导出CSV文件的功能。

2.1给管理后台增加自定义管理行为(actions)

Django允许对管理后台的很多内容进行自定义修改。我们准备在列出具体数据的视图内增加导出CSV文件的功能。

一个管理行为是指如下操作:用户从管理后台列出某个模型中具体记录的页面内,使用复选框选中要操作的记录,然后从下拉菜单中选择一项操作,之后就会针对所有选中的记录执行操作。这个action的位置如下图所示:

创建自定义管理行为可以让管理员批量对记录进行操作。

可以通过写一个符合要求的自定义函数作为一项管理行为,这个函数要接受如下参数:

  • 当前显示的ModelAdmin类

  • 当前的request对象,是一个HttpResponse实例

  • 用户选中的内容组成的QuerySet

在选中一个action选项然后点击旁边的Go按钮的时候,该函数就会被执行。

我们就准备在下拉action清单里增加一项导出CSV数据的功能,为此先来修改orders应用中的admin.py文件,将下列代码加在OrderAdmin类定义之前:

Copyimport csv
import datetime
from django.http import HttpResponse

defexport_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() ifnot field.many_to_many andnot field.one_to_many]
    writer.writerow(field.verbose_name for field in fields)

    for obj in queryset:
        data_row = []
        for field in fields:
            value = getattr(obj, field.name)
            ifisinstance(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'

在这个函数里我们做了如下事情:

  1. 创建一个HttpResponse对象,将其内容类型设置为text/csv,以告诉浏览器将其视为一个CSV文件。还为请求头附加了Content-Disposition头部信息,告诉浏览器这个请求带有一个附加文件。

  1. 创建一个CSV的writer对象,用于向Http响应对象response中写入CSV文件数据。

  1. 通过_meta的get_fields()方法获取所有字段名,动态获取model的字段,排除了所有一对多和多对多字段。

  1. 将字段名写入到响应的CSV数据中,作为第一行数据,即表头

  1. 迭代QuerySet,将其中每一个对象的数据写入一行中,注意特别对datetime采用了格式化功能,以转换成字符串。

  1. 最后设置了该函数对象的short_description属性,该属性的值为在action列表中显示的功能名称。

这样我们就创建了一个通用的管理功能,可以操作任何ModelAdmin对象。

之后在OrderAdmin类中增加这个新的export_to_csv功能:

CopyclassOrderAdmin(admin.ModelAdmin):
WeasyPrint    # ...
    actions = [export_to_csv]

在浏览器中打开http://127.0.0.1:8000/admin/orders/order/查看订单类,页面如下:

选择一些订单,然后选择上边的Export to CSV功能,然后点击Go按钮,浏览器就会下载一个order.csv文件。

译者注:此处下载的文件名可能不是order.csv,这是因为原书没有在orders应用的models.py中为Order类的meta类增加verbose_name属性,手工增加verboser_name的值为order,这样才能下载到和原书里写的名称一样的order.csv文件。

使用文本编辑器打开刚下载的CSV文件,可以看到里边的内容类似:

CopyID,first name,last name,email,address,postal
code,city,created,updated,paid,braintree id
3,Antonio,Melé,antonio.mele@gmail.com,Bank Street,WS
J11,London,25/02/2018,25/02/2018,True,2bwkx5b6

可以看到,实现管理功能的方法很直接。Django中将数据输出为CSV的说明可以参考https://docs.djangoproject.com/en/2.0/howto/outputting-csv/。

3用自定义视图扩展管理后台的功能

不仅仅是配置ModelAdmin,创建管理行为和覆盖内置模板,有时候可能需要对管理后台进行更多的自定义。这时你需要创建自定义的管理视图。使用管理视图,就可以实现自己想要的功能,要注意的只是自定义管理视图应该只允许管理员进行操作,同时继承内置模板以保持风格一致性。

我们这次来修改一下管理后台,增加一个自定义的功能用于显示一个订单的信息。修改orders应用中的views.py文件,增加如下内容:

Copyfrom django.contrib.admin.views.decorators import staff_member_required
from django.shortcuts import get_object_or_404
from .models import Order

@staff_member_requireddefadmin_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_staff和is_active字段同时为True的用户才能使用被装饰的视图。在这个视图中,通过传入的id取得对应的Order对象。

然后配置orders应用的urls.py文件,增加一条路由:

Copypath('admin/order/<int:order_id>/', views.admin_order_detail, name='admin_order_detail')

然后在order应用的templates/目录下创建如下文件目录结构:

Copyadmin/
    orders/
        order/
            detail.html

编辑这个detail.html,添加下列代码:

Copy{% extends "admin/base_site.html" %}
{% load static %}
{% block extrastyle %}
    <linkrel="stylesheet"type="text/css"href="{% static "css/admin.css" %}"/>
{% endblock %}
{% block title %}
    Order {{ order.id }} {{ block.super }}
{% endblock %}
{% block breadcrumbs %}
    <divclass="breadcrumbs"><ahref="{% url "admin:index" %}">Home</a> ›
        <ahref="{% url "admin:orders_order_changelist" %}">Orders</a>
        ›
        <ahref="{% url "admin:orders_order_change" order.id %}">Order {{ order.id }}</a>
        › Detail
    </div>
{% endblock %}
{% block content %}
    <h1>Order {{ order.id }}</h1><ulclass="object-tools"><li><ahref="#"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><ahref="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><divclass="module"><divclass="tabular inline-related last-related"><table><caption>Items bought</caption><thead><tr><th>Product</th><th>Price</th><th>Quantity</th><th>Total</th></tr></thead><tbody>
                {% for item in order.items.all %}
                    <trclass="row{% cycle "1" "2" %}"><td>{{ item.product.name }}</td><tdclass="num">${{ item.price }}</td><tdclass="num">{{ item.quantity }}</td><tdclass="num">${{ item.get_cost }}</td></tr>
                {% endfor %}
                <trclass="total"><tdcolspan="3">Total</td><tdclass="num">${{ order.get_total_cost }}</td></tr></tbody></table></div></div>
{% endblock %}

这个模板用于在管理后台显示订单详情。模板继承了admin/base_site.html母版,这个母版包含Django管理站点的基础结构和CSS类,然后还加载了自定义的样式表css/admin.css。

自定义CSS样式表在随书代码中,像之前的项目一样将其复制到对应目录。

我们使用的块名称都定义在母版中,在其中编写了展示订单详情的部分。

当你需要继承Django的内置模板时,必须了解内置模板的结构,在https://github.com/django/django/tree/2.1/django/contrib/admin/templates/admin可以找到内置模板的信息。

如果需要覆盖内置模板,需要将自己编写的模板命名为与原来模板相同,然后复制到templates下,设置与内置模板相同的相对路径和名称。管理后台就会优先使用当前项目下的模板。

最后,还需要再管理后台中为每个Order对象增加一个链接到我们自行编写的视图,编辑orders应用的admin.py文件,在OrderAdmin类之前增加如下代码:

Copyfrom django.urls import reverse
from django.utils.safestring import mark_safe

deforder_detail(obj):
    return mark_safe('<a href="{}">View</a>'.format(reverse('orders:admin_order_detail', args=[obj.id])))

这个函数接受一个Order对象作为参数,返回一个解析后的admin_order_detail名称对应的URL,由于Django默认会将HTML代码转义,所以加上mark_safe。

使用mark_safe可以不让HTML代码转义。使用mark_safe的时候,确保对于用户的输入依然要进行转义,以防止跨站脚本攻击。

然后编辑OrderAdmin类来显示链接:

Copy@admin.register(Order)classOrderAdmin(admin.ModelAdmin):
    list_display = ['id', 'first_name', 'last_name', 'email', 'address', 'postal_code', 'city', 'paid', 'created',
                    'updated', order_detail]

然后启动站点,访问http://127.0.0.1:8000/admin/orders/order/,可以看到新增了一列:

点击任意一个订单的View链接查看详情,会进入Django管理后台风格的订单详情页:

4动态生成PDF发票

我们现在已经实现了完整的结账和支付功能,可以为每个订单生成一个PDF发票。有很多Python库都可以用来生成PDF,常用的是Reportlab库,该库也是django 2.0官方推荐使用的库,可以在https://docs.djangoproject.com/en/2.0/howto/outputting-pdf/查看详情。

大部分情况下,PDF文件中都要包含一些样式和格式,这个时候通过渲染后的HTML模板生成PDF更加方便。我们在Django中集成一个模块来按照上述方法转换PDF。这里我们使用WeasyPrint库,这个库用来从HTML模板生成PDF文件。

4.1安装WeasyPrint

需要先按照http://weasyprint.org/docs/install/#platforms的指引安装相关依赖,之后通过pip安装WeasyPrint:

Copypip install WeasyPrint==0.42.3

译者注:WeasyPrint在Windows下的配置非常麻烦,还未必能够成功,推荐Windows用户使用Linux虚拟机。

4.2创建PDF模板

需要创建一个模板作为输入给WeasyPrint的数据。我们创建一个带有订单内容和CSS样式的模板,通过Django渲染,将最终生成的页面传给WeasyPrint。

在orders应用的templates/orders/order/目录下创建pdf.html文件,添加下列代码:

Copy<html><body><h1>My Shop</h1><p>
    Invoice no. {{ order.id }}<br><spanclass="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 %}
        <trclass="row{% cycle "1" "2" %}"><td>{{ item.product.name }}</td><tdclass="num">${{ item.price }}</td><tdclass="num">{{ item.quantity }}</td><tdclass="num">${{ item.get_cost }}</td></tr>
    {% endfor %}
    <trclass="total"><tdcolspan="3">Total</td><tdclass="num">${{ order.get_total_cost }}</td></tr></tbody></table><spanclass="{% if order.paid %}paid{% else %}pending{% endif %}">
{% if order.paid %}Paid{% else %}Pending payment{% endif %}
</span></body></html>

译者注:注意第五行标红的部分,原书错误的写成了</br>。

这个模板的内容很简单,使用一个<table>元素展示订单的用户信息和商品信息,还添加了消息显示订单是否已支付。

4.3创建渲染PDF的视图

我们来创建在管理后台内生成订单PDF文件的视图,在orders应用的views.py文件内增加下列代码:

Copyfrom django.conf import settings
from django.http import HttpResponse
from django.template.loader import render_to_string
import weasyprint

@staff_member_requireddefadmin_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_{}"'.format(order.id)
    weasyprint.HTML(string=html).write_pdf(response, stylesheets=[weasyprint.CSS(settings.STATIC_ROOT + 'css/pdf.css')])
    return response

这是生成PDF文件的视图,使用了@staff_member_required装饰器使该视图只能由管理员访问。通过ID获取Order对象,然后使用render_to_string()方法渲染orders/order/pdf.html,渲染后的模板以字符串形式保存在html变量中。然后创建一个新的HttpResponse对象,并为其附加application/pdf和Content-Disposition请求头信息。使用WeasyPrint从字符串形式的HTML中转换PDF文件并写入HttpResponse对象。这个生成的PDF会带有STATIC_ROOT路径下的css/pdf.css中的样式,最后返回响应。

如果发现文件缺少CSS样式,记得把CSS文件从随书目录中放入shop应用的static/目录下。

我们这里还没有配置STATIC_ROOT变量,这个变量规定了项目的静态文件存放的路径。编辑myshop项目的settings.py文件,添加下面这行:

CopySTATIC_ROOT = os.path.join(BASE_DIR, 'static/')

然后运行如下命令:

Copypython manage.py collectstatic

之后会看到:

Copy120 static files copied to 'code/myshop/static'.

这个命令会把所有已经激活的应用下的static/目录中的文件,复制到STATIC_ROOT指定的目录中。每个应用都可以有自己的static/目录存放静态文件,还可以在STATICFILES_DIRS中指定其他的静态文件路径,当执行collectstatic时,会把所有STATICFILES_DIRS目录内的文件都复制过来。如果再次执行collectstatic,会提示是否需要覆盖已经存在的静态文件。

译者注:虽然将静态文件分开存放在每个应用的static/下可以正常运行开发中的站点,但在正式上线的最好统一静态文件的存放地址,以方便配置Web服务程序。

编辑orders应用的urls.py文件,为视图配置URL:

Copyurlpatterns = [
    # ...
    path('admin/order/<int:order_id>/pdf/', views.admin_order_pdf, name='admin_order_pdf'),
]

像导出CSV一样,我们要在管理后台的展示页面中增加一个链接到这个视图的URL。打开orders应用的admin.py文件,在OrderAdmin类之前增加:

Copydeforder_pdf(obj):
    return mark_safe('<a href="{}">PDF</a>'.format(reverse('orders:admin_order_pdf', args=[obj.id])))
order_pdf.short_description = 'Invoice'

如果指定了short_description属性,Django就会用该属性的值作为列名。

为OrderAdmin的list_display增加这个新的字段order_pdf:

Copy@admin.register(Order)classOrderAdmin(admin.ModelAdmin):
    list_display = ['id', 'first_name', 'last_name', 'email', 'address', 'postal_code', 'city', 'paid', 'created',
                    'updated', order_detail, order_pdf]

在浏览器中打开http://127.0.0.1:8000/admin/orders/order/,可以看到新增了一列字段用于转换PDF:

点击PDF链接,浏览器应该会下载一个PDF文件,如果是尚未支付的订单,样式如下:

已经支付的订单,则类似这样:

4.4使用电子邮件发送PDF文件

当支付成功的时,我们发送带有PDF发票的邮件给用户。编辑payment应用中的views.py视图,添加如下导入语句:

Copyfrom django.template.loader import render_to_string
from django.core.mail import EmailMessage
from django.conf import settings
import weasyprint
from io import BytesIO

在payment_process视图中,order.save()这行之后,以相同的缩进添加下列代码:

Copydefpayment_process(request):
    # ......if request.method == "POST":
        # ......if result.is_success:
            # ......
            order.save()

            # 创建带有PDF发票的邮件
            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])

            # 生成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)

            # 附加PDF文件作为邮件附件
            email.attach('order_{}.pdf'.format(order.id), out.getvalue(), 'application/pdf')

            # 发送邮件
            email.send()

            return redirect('payment:done')
        else:
            return redirect('payment:canceled')
    else:
        # ......

这里使用了EmailMessage类创建一个邮件对象email,然后将模板渲染到html变量中,然后通过WeasyPrint将其写入一个BytesIO二进制字节对象,之后使用attach方法,将这个字节对象的内容设置为EmailMessage的附件,同时设置文件类型为PDF,最后发送邮件。

记得在settings.py中设置SMTP服务器,可以参考第二章。

现在,可以尝试完成一个新的付款,并且在邮箱内接收PDF发票。

总结

在这一章中,我们集成了支付网关,自定义了Django管理后台,还学习了如何将数据以CSV文件格式导出和动态生成PDF文件。

下一章我们将深入了解Django项目的国际化和本地化设置,还将创建一个优惠码功能和商品推荐引擎。

如有不懂还要咨询下方小卡片,博主也希望和志同道合的测试人员一起学习进步

在适当的年龄,选择适当的岗位,尽量去发挥好自己的优势。

我的自动化测试开发之路,一路走来都离不每个阶段的计划,因为自己喜欢规划和总结,

测试开发视频教程、学习笔记领取传送门!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/342963.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

旋转框目标检测mmrotate v1.0.0rc1 之RTMDet训练DOTA(二)

1、模型rotated_rtmdet的论文链接与配置文件注意&#xff1a;我们按照 DOTA 评测服务器的最新指标&#xff0c;原来的 voc 格式 mAP 现在是 mAP50。IN表示ImageNet预训练&#xff0c;COCO表示COCO预训练。与报告不同的是&#xff0c;这里的推理速度是在 NVIDIA 2080Ti GPU 上测…

国外客户只想跟工厂合作?可以这样破解

1.客户是愿意和外贸公司合作还是更愿意和工厂合作&#xff1f;一个外贸公司的朋友说:“我去工厂接待过七八次外国人&#xff0c;基本上都是英国、德国、日本、加拿大、美国的。”贸易公司根本不避讳自己是贸易公司&#xff0c;外国人也不在乎。他们更关心的是贸易公司能否妥善安…

十五载厚积薄发,电信级分布式数据库是这样炼成

所在论坛&#xff1a;数据库技术创新&云原生论坛 分享时段&#xff1a;2.18 10:00-10:30 分享主题&#xff1a;大规模并行处理&#xff1a;AntDB分布式演进之路 分享嘉宾&#xff1a;沈夺&#xff0c;亚信科技AntDB数据库内核开发工程师 由中国开源软件推进联盟Postgre…

JFET(结型场效应管)

JFET的结构示意图 参考&#xff1a;https://blog.csdn.net/weixin_45882303/article/details/106008695 下图是实际结构图&#xff0c; 下面是原理图和符号表示&#xff08;参考连接中的图片&#xff09; 分析 VGS 对电压id的控制&#xff08;固定VDS&#xff09; 当让D和…

华为OD机试 - 箱子之字形摆放(Python)| 核心知识点 + 代码编写思路

箱子之字形摆放 题目 有一批箱子(形式为字符串,设为str), 要求将这批箱子按从上到下以之字形的顺序摆放在宽度为 n 的空地,请输出箱子的摆放位置。 例如:箱子ABCDEFG,空地宽度为3,摆放结果如图: 则输出结果为: AFG BE CD 输入 输入一行字符串,通过空格分隔,前面…

别具一格,原创唯美浪漫情人节表白专辑,(复制就可用)(html5,css3,svg)表白爱心代码(3)

别具一格&#xff0c;原创唯美浪漫情人节表白专辑&#xff0c; (复制就可用)&#xff08;html5,css3,svg)表白爱心代码(3) 目录 款式三&#xff1a;心形实时显示认识多长时间桃花飞舞&#xff08;猫咪&#xff09;款 1、拷贝完整源代码 2、拷贝完整js代码 3、修改时间 4、…

深入浅出带你学习shiro-550漏洞

//发点去年存货 前言 apache shiro是一个java安全框架&#xff0c;作用是提供身份验证&#xff0c;Apache Shiro框架提供了一个Rememberme的功能,存储在cookie里面的Key里面&#xff0c;攻击者可以使用Shiro的默认密钥构造恶意序列化对象进行编码来伪造用户的 Cookie&#xf…

一起学习用Verilog在FPGA上实现CNN----(七)全连接层设计

1 全连接层设计 1.1 Layer 进行线性计算的单元layer&#xff0c;原理图如图所示&#xff1a; 1.2 processingElement Layer中的线性计算单元processingElement&#xff0c;原理图如图所示&#xff1a; processingElement模块展开原理图&#xff0c;如图所示&#xff0c;包含…

Transformation(转换算子)

分布式代码的分析 启动spark程序的代码 在yarn中启动(没有配置环境变量) /export/server/spark/bin/spark-submit --master yarn --num-executors 6 /root/helloword.py # 配置环境变量 spark-submit --master yarn --num-executors 6 /root/helloword.py RDD的五大特征 1、…

ZYNQ-嵌入式学习(4)

GPIO之MIO中断GPIO的MIO中断功能实验&#xff1a;使用GPIO的MIO中断功能&#xff0c;实现按键控制LED的亮灭。GPIO的MIO中断功能 从MIO输入到GPIO的线路有一个通向中断检测模块的分支。 INT_TYPE寄存器表示中断类型。包括边沿和电平两种类型。 INT_POLARITY寄存器表示极性。包括…

基于 STM32+FPGA 的多轴运动控制器的设计

运动控制器是数控机床、高端机器人等自动化设备控制系统的核心。为保证控制器的实用性、实时性和稳定 性&#xff0c;提出一种以 STM32 为主控制器、FPGA 为辅助控制器的多轴运动控制器设计方案。给出了运动控制器的硬件电路设计&#xff0c; 将 S 形加减速算法融入运动控制器&…

【Git】合并多条 commit 注释信息

文章目录1、查看 commit 记录2、合并 commit 注释1、查看 commit 记录 # 3 指的是查看最近 3 次的 commit 记录&#xff0c;如果要查看多次的可以修改数字 # -3 不加&#xff0c;则表示查看所有 commit 记录&#xff0c;一般还是用数字去指定 git log -32、合并 commit 注释 …

【图像分类】基于PyTorch搭建LSTM实现MNIST手写数字体识别(单向LSTM,附完整代码和数据集)

写在前面: 首先感谢兄弟们的关注和订阅,让我有创作的动力,在创作过程我会尽最大能力,保证作品的质量,如果有问题,可以私信我,让我们携手共进,共创辉煌。 提起LSTM大家第一反应是在NLP的数据集上比较常见,不过在图片分类中,它同样也可以使用。我们以比较熟悉的 mnist…

软件测试面试自我介绍/项目介绍居然还有模板?我要是早点发现就好了

目录 1、自我介绍 2、项目介绍 2.1、最全电商项目介绍 2.2、电商项目介绍 2.3、在线教育项目介绍 2.4、互联网金融项目介绍 总结 1、自我介绍 以XXX简历来举例&#xff08;参照下面的案例&#xff0c;编写你的自我介绍&#xff0c;框架就是&#xff1a;我是谁&#xff0…

深入Kafka核心设计与实践原理读书笔记第三章消费者

消费者 消费者与消费组 消费者Consumer负责定于kafka中的主题Topic&#xff0c;并且从订阅的主题上拉取消息。与其他消息中间件不同的在于它有一个消费组。每个消费者对应一个消费组&#xff0c;当消息发布到主题后&#xff0c;只会被投递给订阅它的消费组的一个消费者。 如…

go gin学习记录1

环境&#xff1a; MAC M1&#xff0c;Go 1.17.2&#xff0c;GoLand 默认执行指令的终端&#xff0c;如果没有特别说明&#xff0c;指的都是goland->Terminal 创建项目 Goland中新建项目&#xff0c;在$GOPATH/src/目录下建立t_gin项目。 进入项目&#xff0c;在goland的T…

spark04-文件读取分区数据分配原理

接 https://blog.csdn.net/oracle8090/article/details/129013345?spm1001.2014.3001.5502通过上一节知道 总字节数为7 每个分区字节数为3代码val conf: SparkConf new SparkConf().setMaster("local").setAppName("wordcount")val sc: SparkContext ne…

日日顺供应链|想要看清供应链发展趋势,先回答这三个问题

技术变革如何支撑供应链及管理服务的发展&#xff1f; 数字化与科技化开始承托供应链管理能力的升级与变革&#xff1f; 如何从客户需求的纬度反推供应链及管理服务的模式变革&#xff1f;在过去的三年中&#xff0c;我国的供应链企业经受了最为极端的挑战&#xff0c;但当下&a…

论文写作——公式编辑器、latex表格、颜色搭配器

1、公式编辑器(网页版mathtype可用于latex公式编辑): MathType demo - For DevelopersLive demonstration about the features of Mathtype which allows edition equations and formulas (PNG, flash, SVG, PDF, EPS), based on MathML and compatible with LaTeX.https:/…

C++之可调用对象、bind绑定器和function包装器

可调用对象在C中&#xff0c;可以像函数一样调用的有&#xff1a;普通函数、类的静态成员函数、仿函数、lambda函数、类的非静态成员函数、可被转换为函数的类的对象&#xff0c;统称可调用对象或函数对象。可调用对象有类型&#xff0c;可以用指针存储它们的地址&#xff0c;可…