全书完整目录请见:Odoo 12开发者指南(Cookbook)第三版
本章中,我们将讲解如下内容:
- 让路径在网络中可访问
- 限制网络可访问路径的访问
- 传递消耗参数到你的handler
- 修改已有handler
引言
我们将在本章中介绍Odoo网页服务端部分的基础知识。本章中所讲解的为基本的部分,有关更高阶的功能,请参见第十五章 CMS网站开发。
所有的Odoo网页请求都是由Python库werkzeug来进行处理的。虽然werkzeug的复杂部分多隐藏在Odoo便捷的封装器中,学习其底层的运行机制也会非常的有帮助。
技术准备
本章的要求包含一个在线的Odoo平台。
本章中使用的所有代码可通过GitHub仓库进行下载:https://github.com/PacktPublishing/Odoo-12-Development-Cookbook-Third-Edition/tree/master/Chapter14。
观看如下视频来查看代码实时操作:
让路径在网络中可访问
本节中我们来看如何http://yourserver/path1/path2这样的表单 URL 对用户可访问。这既有可能是一个网页,也有可能是返回供其它程序使用的数据的路径。后一种情况中,我们通常会使用JSON格式来接收参数并提供数据。
准备工作
我们将使用library.book 模型,可参见第五章 应用模型,因此如果你还没有按该章进行操作,请获取相关代码以便能按照后面的示例进行操作。
我们希望允许任何用户来查询完整的图书列表。此外,我们希望通过JSON请求向程序提供相同的信息。
如何实现…
我们需要添加控制器,按惯例它放在一个名为controllers的文件夹中:
- 添加带有我们页面HTML版本的controllers/main.py文件,如下:
1234567891011from odoo import httpfrom odoo.http import requestclass Main(http.Controller):@http.route('/my_library/books', type='http', auth='none')def books(self):books = request.env['library.book'].sudo().search([])html_result = '<html><body><ul>'for book in books:html_result += "<li> %s </li>" % book.namehtml_result += '</ul></body></html>'return html_result - 添加一个函数来以JSON格式提供相同的住下,如下例所示:
1234@http.route('/my_library/books/json', type='json', auth='none')def books_json(self):records = request.env['library.book'].sudo().search([])return records.read(['name']) - 添加controllers/__init__.py文件,如下:
1from . import main - 在my_library/__init__.py文件中导入controllers,如下:
1from . import controllers
在重启服务之后,可以在浏览器中访问/my_library/books并获得书名的列表。要进行JSON-RPC的测试,需要构造一个JSON请求。实现它的简单方式是通过使用如下命令来在命令行中接收输出:
1 |
curl -i -X POST -H "Content-Type: application/json" -d "{}" localhost:8069/my_library/books/json |
如果此时获取到404报错,可能是在实例中有不止一个数据库。这种情况下,Odoo无法决定使用哪个数据库对请求进行服务。
使用–db-filter=’^yourdatabasename$’参数来强制Odoo使用模块所安装的具体的数据库。现在该路径应该就可以访问了。
运行原理…
这里的两个关键部分是我们的控制器通过odoo.http.Controller获取,并且用于提供内容服务的方法由odoo.http.route进行装饰。继承 odoo.http.Controller以通过继承odoo.models.Model.注册模型相似的方式通过Odoo路由系统注册该控制器。同时Controller有一个处理这一注册的元类(Controller)。
通过由插件所处理的路径以插件名开头,以避免名称的冲突。当然,如果你继承一些插件功能,会使用这个插件名。
odoo.http.route
route装饰器让我们首先告诉Odoo某一方法可通过web访问,第一个参数决定可以访问哪个路径。除了传递一个字符串,也可以传递一个字符串列表,这样相同的函数可以为多个路径提供服务。
type参数默认为http,决定所提供服务对应的请求类型。严格意义上说,JSON是HTTP,声明第二个函数为type=’json’会让事情变得很轻松,因为接下来Odoo会处理类型转换。
现在先不用担心auth参数,它会在本章的限制网络可访问路径的访问一节中进行讲解。
返回值
Odoo中函数的返回值可由route装饰器的type参数来决定。对于type=’http’,我们通常希望发布一些HTML,因此第一个函数仅返回包含 HTML 的字符串。一种代替方案是使用request.make_response(),允许你控制在响应中发送的headers。因此要表明页面最后更新的时间,可以在books()中的修改最后一行为如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 |
eturn request.make_response( html_result, [ ('Last-modified', email.utils.formatdate( ( fields.Datetime.from_string( request.env['library.book'].sudo() .search([], order='write_date desc', limit=1) .write_date) - datetime.datetime(1970, 1, 1) ).total_seconds(), usegmt=True)), ]) |
代码与我们生成的HTML一起发送一个Last-modified头,告诉浏览器列表最后一次修改的时间。我们可以从library.book 模型的write_date字段中提取这一信息。
为让前面代码片断可以运行,我们需要在文件的顶部添加一些导入语句,如下:
1 2 3 |
import email import datetime from odoo import fields |
你也可以手动创建一个werkzeug的Response对象并返回,但费这番功夫收获甚微。
ℹ️手动生成HTML对于演示非常好,但在生产环境的代码中则不应这么做。保持使用模板,如我们在第十五章 CMS网站开发中的创建或更改模板 – QWeb一节中所演示的,并通过调用request.render()返回它们。
这会让你从容地进行本地化并让代码通过将展示层与业务逻辑分享而更优雅。同时,模板为你提供函数来在输出 HTML 之前转义数据。前面的代码会易于遭受跨站脚本攻击(比如用户可能会把脚本标签放到书名中)。
对于JSON请求,只需返回想要交给客户端的数据结构;Odoo会处理序列化。要让其可以动作,应将数据限制为JSON可序列化的类型,大概有字典、列表、字符串、浮点型和整型。
odoo.http.request
请求对象是引用当前处理的请求的静态对像,包含所有执行需要的内容。这里最重要的是request.env属性,包含一个与模型中self.env相同的Environment对象。环境与当前用户绑定,在前例中并不存在,因为我们使用了auth=’none’。缺少用户也是我们使用sudo()来在示例代码中调用模型方法的原因。
如果你习惯于web开发,则会预期进行会话处理,这是绝对正确的。使用OpenERPSession对象(是对werkzeug的Session对象的轻微封装)的request.session,以及request.session.sid来访问会话ID。要存储会话值,只需将request.session作为字典处理,如以下示例代码所示:
1 2 |
request.session['hello'] = 'world' request.session.get('hello') |
ℹ️注意在会话中存储数据与使用全局变量是相同的。仅有必须时才使用它。通常对于多请求动作是需要的,如website_sale模块中的结账。这种情况下, 在控制器中处理所有有关会话的函数,而不要在模型中处理。
扩展知识…
route装饰器可带有其它的参数来进一步自定义其行为。默认允许所有的HTTP方法,并且Odoo将所有传递的参数组装在一起。使用methods参数,我们可以传递一个可接受的方法列表,通常是[‘GET’] 或[‘POST’]。要允许跨域请求的话(出于安全和隐私考虑,浏览器阻止对脚本所加载域以外域的AJAX和其它类型的请求),可设置cors参数为 * 来允许来自所有域的请求,或设置一个URI来限制请求为来自该URI的请求。如果未设置这个参数,则为默认,即未设置Access-Control-Allow-Origin头,采取浏览器的默认行为。在本例中,我们可能会希望在/my_module/books/json中进行设置,来允许从其它网站拉取的脚本访问图书列表。
默认,Odoo通过对每个请求传递token来保护一些类型的请求免受称为跨站请求伪造(CSRF)的攻击。如果想要关闭它,设置csrf参数为False,但应注意这通常不是一个好的想法。
其它内容
参见以下各点来了解有关HTTP路由的更多知识:
- 如果在同一个实例中托管多个Odoo数据库,那么不同的数据库可能运行在不同的域中。这时我们可以使用–db-filter参数或使用https://github.com/OCA/server-tools,的dbfilter_from_header模块,它有助于按域过滤出数据库。在写本书时这个模块还没有迁移到版本12,但在本书出版时应该会进行迁移了(译者注:当前已可在 Odoo 12中使用)。
- 要学习如何使用模板来实现模块化,请参见本单中的修改已有handler一节。
限制网络可访问路径的访问
我们将在本节中探讨Odoo为路由所提供的三种验证机制。并使用不同的验证机制来定义路由,以展示它们之间的不同之处。
准备工作
我们将对前一节的代码继续进行扩展,还将依赖第五章 应用模型中的library.book模型,所以就准备好相关代码来继续下来的学习。
如何实现…
在controllers/main.py中定义handler:
- 添加显示所有图书的路径,如下例所示:
12345678@http.route('/my_library/all-books', type='http', auth='none')def all_books(self):books = request.env['library.book'].sudo().search([])html_result = '<html><body><ul>'for book in books:html_result += "<li> %s </li>" % book.namehtml_result += '</ul></body></html>'return html_result - 添加一个显示所有图书的路径并且若有则表明哪个是由当前用户所写入的。参见如下示例代码:
12345678910111213@http.route('/my_library/all-books/mark-mine', type='http', auth='public')def all_books_mark_mine(self):books = request.env['library.book'].sudo().search([])html_result = '<html><body><ul>'for book in books:if request.env.user.partner_id.id inbook.author_ids.ids:html_result += "<li> <b>%s</b> </li>" %book.nameelse:html_result += "<li> %s </li>" % book.namehtml_result += '</ul></body></html>'return html_result - 添加显示当前用户图书的路径,如下:
12345678910@http.route('/my_library/all-books/mine', type='http', auth='user')def all_books_mine(self):books = request.env['library.book'].search([('author_ids', 'in', request.env.user.partner_id.ids),])html_result = '<html><body><ul>'for book in books:html_result += "<li> %s </li>" % book.namehtml_result += '</ul></body></html>'return html_result
通过这段代码,/my_library/all-books和/my_library/allbooks/mark-mine路径与非验证用户所显示内容相同,但登录用户会在后面的路径中看到他们的图书以粗体显示。对非验证用户/my_library/allbooks/mine路径完全不可访问。如果未验证则深度访问该路径,会被重定向到登录页面来进行登录。
运行原理…
验证方法之间的不同基本上通过request.env.user的内容可判断到。
对于auth=’none’哪怕是经验证用户在访问路径用户记录也是空的。使用这一个验证的场景是所响应的内容对用户不存在依赖,或者是在服务端模块中提供与数据库无关的功能。
auth=’public’的值将用户记录对未验证用户设置为一个带有XML ID base.public_user的特殊用户,已验证用户设置为用户的记录。对于所于所提供的功能同时针对未验证和已验证用户而已验证用户又具体一些额外的功能时应选择它,前面的代码中已经演示。
使用auth=’user’来确保仅有已验证用户才能访问所提供的内容。通过这个方法,我们可以确保request.env.user指向已有用户。
扩展知识…
验证方法的逻辑发生在base插件的 ir.http模型中。不论在路由的auth参数中传递什么值,Odoo搜索该模型中名为_auth_method_的函数,这样可以通过继承并声明处理自己的验证方法来进行自定义。
作为示例,我们将提供一个名为base_group_user的验证方法,仅针对当前登录的是属于base.group_user组的用户,如下例所示:
1 2 3 4 5 6 7 8 |
from odoo import exceptions, http, models from odoo.http import request class IrHttp(models.Model): _inherit = 'ir.http' def _auth_method_base_group_user(self): self._auth_method_user() if not request.env.user.has_group('base.group_user'): raise exceptions.AccessDenied() |
现在可以在装饰器中使用auth=’base_group_user’,并确保运行这个路由handler的用户是该组的成员。使用一点技巧,我们还可以将其扩展为auth=’groups(xmlid1,…)’,这一实现留作读者练习,可参见GitHub仓库中的示例代码Chapter14/r2_paths_auth/my_library/models/sample_auth_http.py。
传递消耗参数到你的handler
能够显示内容自然很棒,但能够根据用户输入显示内容则更佳。本节将演示接收输入和做出响应的不同方法。如前一小节,我们将使用library.book模型。
如何实现…
首先,我们将添加一个接收带有图书 ID 来其显示详情的传统参数的路由。然后,我们使用将参数嵌入路径自身的方式来实现同样的功能:
- 添加一个接收图书ID来作为参数的路径,如下例所示:
1234567@http.route('/my_library/book_details', type='http', auth='none')def book_details(self, book_id):record = request.env['library.book'].sudo().browse(int(book_id))return u'<html><body><h1>%s</h1>Authors: %s' % (record.name,u', '.join(record.author_ids.mapped('name')) or 'none',) - 添加一个我们可以传递图书ID的路径,如下:
123@http.route("/my_library/book_details/<model('library.book'):book>", type='http', auth='none')def book_details_in_path(self, book):return self.book_details(book.id)
如果在浏览器中访问 /my_module/book_details?book_id=1,应该会看一个带有ID为1的图书的详情页。如不存在,会收到一个报错页面。
第二个handler允许我们访问/my_module/book_details/1并浏览到相同的内容。
运行原理…
默认,Odoo(实际上是werkzeug)合并了GET和POST参数并将它们通过关键词参数传递给handler。因此,仅需声明接收名为book_id参数的函数,我们以GET(URL中的参数)或POST(通过以是action属性指定handler的<form>元素来传递)来引入该参数 。如果没有对该参数添加默认值,运行时会在未设置参数就尝试访问时抛出错误。
第二个示例利用了werkzeug环境中大多数路径是虚拟的这一事实。因此我们可以定义路径为包含一些输入。在本例中,我们指定了library.book的ID为路径的最后一个组成部分。冒号后为关键词参数的名称。我们的函数会通过以关键词来传递参数并调用。这里,Odoo处理对这一ID的调用并分发一个浏览记录,当然要在访问路径的用户具有相应权限时才能使用。假定book是一条浏览记录,我们可以传递book.id 来作为book_id参数再利用第一个示例的函数,给出相同的内容。
扩展知识…
在路径中定义参数是由werkzeug所带的一个功能,称为converter。模型转换器由Odoo添加,它还定义接收一个逗号分隔的ID列表的转换器由模型,并传递包含这些 ID 的记录集给我们的handler。
转换器的优美之处在于runtime强制参数为所需类型,而使用普通关键字参数则需要自己处理。这些以字符串进行分发,而你需要像第一个示例一样自己处理必要的类型转换。
内置的werkzeug转换器包含 int, float和string,以及更为复杂的path, any或uuid等。可以在http://werkzeug.pocoo.org/docs/0.11/routing/#builtin-converters中查看它们的语法。
其它内容
如果你希望学习更多的HTTP路径,参见如下各点:
- Odoo的自定义转换器在base模块中的ir_http.py中定义并在ir.http的the _get_converters类方法中注册。作为练习,你可以创建自己的转换器,它允许你访问/my_library/book_details/Odoo+cookbook页面来接收图书的详情(如果之前在library中进行了添加的话)。
- 如果想要学习更多在该路径上的表单提交的话,请参见第十五章 CMS网站开发中的从网站用户获取输入一节。
修改已有handler
在安装website模块时,/website/info path显示有关Odoo实例的一些信息。本节中,我们将重载它来修改信息页面的布局,也修改其显示的内容。
准备工作
安装website模块并查看/website/info路径。本节中,我们将更新/website/info路由来提供更多的信息。
如何实现…
我们将需要适配已有模块并重载已有的handler。可以像这样实现:
- 在名为views/templates.xml的文件中重载qweb模板,如下:
1234567891011121314151617<?xml version="1.0" encoding="UTF-8"?><odoo><template id="show_website_info"inherit_id="website.show_website_info"><xpath expr="//dl[@t-foreach='apps']" position="replace"><table class="table"><tr t-foreach="apps" t-as="app"><th><a t-att-href="app.website"><t t-esc="app.name" /></a></th><td><t t-esc="app.summary" /></td></tr></table></xpath></template></odoo> - 在名为controllers/main.py的文件中重载handler,如下例所示:
123456789101112from odoo import httpfrom odoo.addons.website.controllers.main import Websiteclass WebsiteInfo(Website):@http.route()def website_info(self):result = super(WebsiteInfo, self).website_info()result.qcontext['apps'] =result.qcontext['apps'].filtered(lambda x: x.name != 'website')return result
此时在访问info页时,我们将只能看到一个在表中安装了应用的过滤列表,如原有定义列表对应。
运行原理…
在第一步中,我们重载了已有QWeb模板。为了找出它是哪一个,我们需要查看原有handler的代码。通常,这会产生一些与下面这行类似的内容,告诉我们需要重载template.name:
1 |
return request.render('template.name', values) |
在本例中,handler使用了名为website_info的模块,但它马上由另一个名为website.show_website_info的模板进行扩展,因此重载这个会非常方便。这里,我们将显示已安装应用的定义列表替换为表格。有关QWeb继承原理的更多详情,请参见第十六章 网页客户端开发。
为重载handler方法,我们必须识别出定义handler的类,本例中即为odoo.addons.website.controllers.main.Website。我们需要导入该类来从其进行继承。现在,我们可以重载方法并修改传递给response的数据。注意重载的handler在这里出于简洁考虑返回的是一个Response对象而不是像前一小节中那样的HTML字符串。这个对象包含一个对待使用模板的引用,并且该值对模板是可访问的,但它仅在请求的最终运行。
通常,有三种方式可修改已有handler:
- 如果它使用QWeb模板,最简单的方式是修改它来重载该模板。对于布局修改和小的逻辑变动可选择这种方法。
- QWeb获取一个传递的上下文,在响应中以qcontext成员存在。这通常是一个可以添加或删除值来满足你的需要的字典。在前例中,我们过滤了应用列表来仅针对website。
- 如果handler接收到参数,你也可以进行预处理,来让所重载的handler按照你所希望的方式运行。
扩展知识…
如前一部分中所了解的,控制器的继承与模型继承稍有不同,你实际上需要一个对base类的继承并对期使用Python继承。
不要忘记通过@http.route装饰器来装饰新的handler;Odoo使用它来作为一个标记,标记哪些方法对网络层暴露。如果你省掉了装饰器,实际上会让handler的路径不可访问。
@http.route装饰器本身与字段声明的行为相似,每个未设置的值会从你所重载的函数的装饰器中获取,这样对不希户修改的值就无需重复指定。
在从所重载的函数中接收response对象后,可以做比修改QWeb上下文以外的很多事:
- 你可以通过操作response.headers来添加或删除HTTP头部
- 如果你希望渲染整个不同的模板,可以覆盖response.template
- 要查看response首先是否是基于QWeb,使用response.is_qweb进行查询
- 通过调用esponse.render()可获取结果HTML代码
其它内容
- 有关QWeb的详情会在第十六章 网页客户端开发中进行讲解