控制器(controller)是处理页面请求的处理器,通常的 http 和 JSON 请求都是由 controller 负责处理和转发的,dtcloud 默认缺省的方式是 http。 dtcloud 内置的 Web 服务器 werkzeug 跟 Python 著名的 Web 框架 Flask 用的是同一个,因此,你在定义路由的时候会发现跟 Flask 的定义方式十分相像。
路由的定义
先来看一个 Web 路由的例子:
@http.route([
'/report/<converter>/<reportname>',
'/report/<converter>/<reportname>/<docids>',
], type='http', auth='user', website=True)
def report_routes(self, reportname, docids=None, converter=None, **data):
report = request.env['ir.actions.report']._get_report_from_name(reportname)
http.route 装饰器的参数:
- type: 指定请求的方式,可选值 http 和 JSON
- auth: 认证方式,user,public,none 其中 public 和 none 指明用户不需要登录即可访问改 URL。
- website: 是否是网站的 URL。
获取 GET 方法的参数
controller 获取 GET 方法传进来的参数,使用 reqest.params:
signature = request.params.get("signature",None)
timestamp = request.params.get("timestamp",None)
echostr = request.params.get("echostr",None)
nonce = request.params.get("nonce",None)
获取 POST 方法的数据
controller 获取 GET 方法传进来的原始参数,使用 request.httprequest.data:
request.httprequest.data.decode("utf-8")
对于图片等二进制文件,需要在前段 form 表单中添加属性 enctype="multipart/form-data",然后在 controller 中使用 request.httprequest.files 属性获取文件。
下面是一个例子:
<form class="oe_login_form" role="form" t-attf-action="/baybao_faceid/register/" method="post" enctype="multipart/form-data" onsubmit="this.action = this.action + location.hash">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="form-group field-community">
<input type="file" name="img" />
</div>
</form>
后端的接收代码:
img1 = request.httprequest.files.get('img')
...
"idnoimg1": b64encode(img1.read())
由于 dtcloud 存储二进制的方式为 base64 编码,因此,我们在拿到二进制文件以后需要使用 base64 编码存储到系统中。
获取 JSON 数据
当 API 接口使用 JSON 方式传输数据时,controller 中获取 JSON 数据的方式为:
data = request.jsonrequest.data
在 controller 中调用 model 对象
在 controller 中引用 model 对象的方法跟 model 中的方法类似:
request.env["sale.order"]
导出 Excel 的例子
controler 不仅可以处理日常的请求,也可以返回一个文件,这里以 Excel 文件为例:
wkbook = xlwt.Workbook()
for name in names:
order = request.env["sale.order"].sudo().search(
[('name', '=', name)], limit=1)
if order:
wksheet = wkbook.add_sheet(f"销售订单{order.name}")
wksheet.write(0, 0, "产品")
wksheet.write(0, 1, "订购数量")
wksheet.write(0, 2, "计量单位")
wksheet.write(0, 3, "单价")
wksheet.write(0, 4, "小计")
row = 1
for line in order.order_line:
wksheet.write(row, 0, line.product_id.name)
wksheet.write(row, 1, line.product_uom_qty)
wksheet.write(row, 2, line.product_uom.name)
wksheet.write(row, 3, line.price_unit)
wksheet.write(row, 4, line.price_subtotal)
row += 1
buffer = BytesIO()
wkbook.save(buffer)
data = buffer.getvalue()
headers = [
('Content-Type', 'application/vnd.ms-excel'),
('Content-Length', len(data)),
('Content-Disposition', 'attachment; filename="sale.order.xls"')
]
return request.make_response(data,
headers=headers)
获取用户的访问 IP
dtcloud 虽然用的是同 flask 一样的 werkzeug,但是获取用户的 ip 却不想 flask 那么简单,在 flask 中获取用户访问 ip 可以直接使用
request.remote_addr
而在 dtcloud 中要复杂一些,获取用户的 IP 代码如下:
request.httprequest.environ['REMOTE_ADDR']
对于前端挂在了 nignx 的服务器,Nignx 需要配置为转发用户的真实 IP:
proxy_set_header Host $host:80;
proxy_set_header Origin "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
然后在 dtcloud 中获取 nginx 的真实 IP:
request.httprequest.environ['HTTP_X_REAL_IP']
返回 404
controller 的 Web 服务器用的是 werkzeug,因此像 redirect、404 等返回对象,我们可以利用 werkzeug 提供的功能来实现。
@http.route('/book_store/raise_404/', auth='public')
def index(self, **kw):
return werkzeug.exceptions.NotFound()
返回 500
@http.route('/book_store/raise_500/', auth='public')
def list(self, **kw):
return werkzeug.exceptions.InternalServerError()
跳转其他网址
@http.route('/book_store/redirect_to_baidu/', auth='public')
def object(self,**kw):
return werkzeug.utils.redirect("http://www.baidu.com", code=301)
redirect 方法接受两个参数,第一个 code 指明要跳转的 URL,相对路径,如果要跳转站外路径需要补全路径。code 指明条状状态是 301 还是 302,默认是### 302 跳转。
也可以在 request 中直接调用 redirect 方法:
request.redirect("http://www.baidu.com")
Http Response
一般情况下,我们返回给前端的对象都是 HttpResponse,dtcloud 已经帮我们封装好了一个 make_response 方法,在 controller 中直接返回即可。
return request.make_response(content_base64, headers)
第一个参数为返回的数据对象,第二个参数为 headers。
JSON Response
如果要返回 JSON 对象,我们不需要封装,直接返回 dict 对象即可。由于 dtcloud 使用的是 jsonrpc,这里返回的结果会被自动包裹在result中。
{
"jsonrpc":"2.0",
"id":null,
"result":"result"
}
提示警告信息
警告信息通常用于提示用户缺少必要的输入条件或者当前环境不符合要求,我们可以通过在 controller 中返回一个包含 waring 的字典值来完成这个动作:
return {'warning': _('No picking or location corresponding to barcode %(barcode)s') % {'barcode': barcode}}
controller 的继承与重载
如果想要对已有的 controller 进行覆盖,可以通过指定模块路径的方式来实现。
例如,我们现有一个 controller
class DeliveryKdn(http.Controller):
@http.route('/delivery_kdn/eorder', auth='public')
def index(picking, id, **kw):
picking = request.env["stock.picking"].browse(int(id))
if not picking:
return
res = picking._create_eorder(
picking.partner_id, picking.env.user.partner_id)
if not res['Success']:
return request.make_response(f"打印电子面单失败:{res['Reason']}")
return request.make_response(res['PrintTemplate'])
现在,我们想要对这个 controler 的代码进行重构,根据不要动既有模块代码的原则,我们新建了一个模块,并继承该模块的 controller。
from dtcloud.addons.delivery_kdniao.controllers.controllers import DeliveryKdn
class controller(DeliveryKdn):
@http.route('/delivery_kdn/eorder', auth='public')
def index(picking, id, **kw):
pass
其实原理是利用的 python 自带的的子类继承父类,并将覆盖掉父类同名方法的特性。另外,这里需要将两个 route 的 URL 设置为一致。