我们将在本章中介绍Odoo网页服务端部分的基础知识。注意本章中所讲解的为基础部分,有关更高阶的功能,请参见第十四章 CMS网站开发。
所有的Odoo网页请求都是由Python库werkzeug来进行处理的。虽然werkzeug的复杂部分多隐藏在Odoo便捷的封装器中,学习其底层的运行机制也会非常的有帮助。
本章中,我们将讲解如下内容:
- 让路径在网络中可访问
- 限制线上路径的访问
- 使用传递给handler的参数
- 修改已有handler
- 提供对静态资源的访问
技术准备
学习本章要求安装有在线Odoo平台。
本章中使用的所有代码可通过GitHub仓库进行下载:https://github.com/alanhou/odoo14-cookbook/tree/main/Chapter13。
让路径在网络中可访问
本节中我们学习如何让http://yourserver/path1/path2这样的URL可由用户访问。这既有可能是一个网页,也有可能是返回供其它程序使用数据的路径。后一种情况中,我们通常会使用JSON格式来接收参数并提供数据。
准备工作
我们将使用library.book 模型,可参见第四章 应用模型,因此如果你还没有按该章进行操作,请通过GitHub仓库获取相关代码以便能按照本章示例进行操作。
我们希望允许任何用户查询完整的图书列表。此外,我们希望通过JSON请求对程序提供同样的信息。
如何实现…
我们需要添加控制器,按惯例放在一个名为controllers的文件夹中:
-
添加带有我们页面HTML内容的controllers/main.py文件,如下:
from odoo import http from odoo.http import request class 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.name html_result += '</ul></body></html>' return html_result -
添加一个函数来以JSON格式提供相同的信息,如下例所示:
@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文件,如下:
from . import main -
在my_library/init.py文件中导入controllers,如下:
from . import controllers
在重启服务之后,可以在浏览器中访问/my_library/books并获得书名的列表。要进行JSON-RPC的测试,需要构造一个JSON请求。简单的实现方式是通过使用如下命令在命令行中接收输出:
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路由系统注册该控制器,与继承odoo.models.Model注册模型的方式相似。同时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()中的修改最后一行为如下代码:
return request.make_response(
html_result, headers=[
('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)),
])
代码发送一个Last-modified头及所生成的 HTML,告诉浏览器列表最后一次修改的时间。我们可以从library.book 模型的write_date字段中提取这一信息。
为让前面代码段可以运行,我们需要在文件的顶部添加一些导入语句,如下:
import email
import datetime
from odoo import fields
也可以手动创建一个werkzeug的Response对象并返回,但费这番功夫收获甚微。
📝重要信息:手动生成HTML对于演示非常好,但在生产环境的代码中则不应这么做。保持使用模板,如我们在第十五章 网页客户端开发中的创建或更改模板 - QWeb一节中所演示的,并通过调用request.render()返回。这样我们可以从容地进行本地化并让代码通过将展示层与业务逻辑分离而更优雅。同时,模板为我们提供函数在输出 HTML 之前转义数据。前面的代码会容易遭受跨站脚本攻击(比如用户可能会把脚本标签放到书名中)。
对于JSON请求,只需返回希望交给客户端的数据结构,Odoo会做序列化。这时,应限定所返回的数据类型可进行JSON序列化,通常意味着要使用字典、列表、字符串、浮点型和整型。
odoo.http.request
request对象是引用当前处理请求的静态对像,包含所有执行需要的内容。这里最重要的是request.env属性,包含一个与模型中self.env相同的Environment对象。环境与当前用户绑定,在前例中并不存在,因为我们使用了auth=‘none’。缺少用户也是我们使用sudo()来在示例代码中调用模型方法的原因。
如果习惯于web开发,则会倾向进行会话处理,这是绝对正确的。使用OpenERPSession对象(是对werkzeug的Session对象的轻微封装)的request.session,以及用request.session.sid来访问会话ID。存储会话值,只需将request.session作为字典处理,如以下示例代码所示:
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模块,它有助于按域名过滤数据库。在写本书时这个模块还没有迁移到版本14,但在本书出版时应该已经迁移了。
- 要学习如何使用模板来实现模块化,请参见本章中的修改已有handler一节。
限制线上路径的访问
我们将在本节中探讨Odoo为路由所提供的三种验证机制。并使用不同的验证机制来定义路由,展示它们之间的不同之处。
准备工作
我们将对前一节的代码继续进行扩展,还会依赖第四章 应用模型中的library.book模型,所以请准备好相关代码再继续下面的学习。
如何实现…
在controllers/main.py中定义handler:
-
添加显示所有图书的路径,如下例所示:
@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.name html_result += '</ul></body></html>' return html_result -
添加一个显示所有图书的路径并表明哪个是由当前用户所著的。参见如下示例代码:
@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 in book.author_ids.ids: html_result += "<li> <b>%s</b> </li>" % book.name else: html_result += "<li> %s </li>" % book.name html_result += '</ul></body></html>' return html_result -
添加显示当前用户图书的路径,如下:
@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.name html_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组的当前登录用户,如下例所示:
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仓库中的示例代码Chapter13/r2_paths_auth/my_library/models/sample_auth_http.py。
使用传递给handler的参数
能够显示内容自然很棒,但能够根据用户输入显示内容则更佳。本节将演示接收输入和做出响应的不同方式。如同前一小节,我们将使用library.book模型。
如何实现…
首先,我们将添加一个接收传统参数图书 ID来显示其详情的路由。然后,我们使用将参数嵌入路径内的方式来实现同样的功能:
-
添加一个接收图书ID参数的路径,如下例所示:
@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的路径,如下:
@http.route("/my_library/book_details/<model('library.book'):book>", type='http', auth='public') def book_details_in_path(self, book): return self.book_details(book.id)
如果在浏览器中访问 /my_library/book_details?book_id=1,应该会看到ID为1的图书的详情页。如不存在,会收到一个报错页面。
报错内容:The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.
第二个handler允许我们访问/my_library/book_details/1并浏览到相同的内容。
译者注: 请注意原书中为auth=‘none’,这会出现psycopg2.ProgrammingError: can’t adapt type ‘RequestUID’报错,因为这里我们用到了模型数据,同时也请检查代码是否为最新的稳定版。
运行原理…
默认,Odoo(实际上是werkzeug)合并了GET和POST参数并将它们通过关键词参数传递给handler。因此,仅需声明接收参数book_id的函数,我们以GET(URL中的参数)或POST(通过以是action属性指定handler的