【odoo14】【好书学习】第十三章、网站开发(对外服务)

老韩头的开发日常【好书学习】系列

本章我们将介绍一些关于odoo web服务方面的基础知识。进阶的内容,将在第十四章介绍。
odoo中的web请求是由python的werkzeug库驱动的。odoo为了操作方便,对werkzeug进行了封装。
本章,将包含如下内容:

  • 配置url路径
  • 为url配置访问控制
  • 处理请求内容
  • 继承url的处理函数
  • 提供静态资源

配置url路径

准备

我们想所有的用户都可以获取所有的图书列表。此外,我们希望通过JSON请求向程序提供相同的信息。

步骤

  1. 添加controller/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
  1. 添加一个函数以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'])
  1. 添加controllers/init.py
from . import main
  1. 引入controller
from . import controllers

重启odoo服务后,我们可通过/my_library/books url访问页面,将展示图书列表。为了测试JSON-RPC部分,可通过如下:

curl -i -X POST -H "Content-Type: application/json" -d "{}" localhost:8069/my_library/books/json

如果报错(404),可能是因为我们有多个数据库。odoo并不知道需要使用哪个数据库。
可通过--db-filter='^yourdatabasename$'启动odoo实例。

原理

两个核心的要点,controller,衍生自odoo.http.Controller;方法,衍生自odoo.http.route。controller将注册到odoo的路由系统中,与模型的注册方式类似。
通常,由你的附加组件处理的路径应该以你的附加组件的名称开始,以避免名称冲突。当然,如果您扩展了该附加组件的一些功能,您将使用该附加组件的名称。

odoo.http.route

route装饰器将标识该方法是允许被web访问的,第一个参数是访问的地址。除了字符串形式,第一个参数也可以的字符串的列表,这可以使用同一个函数处理多个url的请求。
type参数默认是http,这将决定请求的支持的模式。严格来说,JSON也是HTTP,将type='json'将会让工作变的容易很多,因为odoo将会帮我们解决类型转化的问题。
auth变量将在随后的章节中介绍。

返回值

请求的返回值是由type参数决定的。对于type='http'的函数,通常返回HTML,所以第一个函数返回了HTML的字符串。另一种方式是通过request.make_response(),我们可以自定义返回header和body。因此,为了指明页面最后一次更新的时间,我们可以将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,告诉浏览器列表最后一次修改的时间。我们可以从库的write_date字段中提取这一信息。本模型。
为了让上面的代码片段工作,你必须在文件的顶部添加一些导入,如下所示:

import email
import datetime
from odoo import fields

您还可以手动创建werkzeug的响应对象并返回该对象,但是这样做效率比较低。

重要信息
为了演示的目的,手动生成HTML很好,但是在生产代码中绝不应该这样做,应该始终使用模板request.render()。
这将为你提供免费的本地化服务,并通过将业务逻辑与表示层分离使你的代码变得更好。此外,模板还提供了在输出HTML之前转义数据的函数。前面的代码容易受到跨站点脚本攻击(例如,如果用户设法将脚本标记插入到图书名中)。

对于JSON请求,只需返回您想要移交给客户端的数据结构;Odoo负责序列化。要实现这一点,您应该限制自己使用JSON可序列化的数据类型,这通常意味着字典、列表、字符串、浮点数和整数。

odoo.http.request

请求对象是一个静态对象,引用当前处理的请求,其中包含采取操作所需的一切。这里最重要的方面是request.env,它包含一个与self.env相同的环境对象。这个环境绑定到当前用户,而在前面的示例中并没有,因为我们使用了auth='none'。缺少用户也是为什么我们必须在示例代码中sudo()所有对模型方法的调用。
如果你习惯web开发,您将会面对会话处理。通过request.session,它是OpenERPSession对象(werkzeug的session的简单封装),和request.session.sid访问session的ID。要存储会话值,只需处理请求。会话作为字典,示例如下:

request.session['hello'] = 'world'
request.session.get('hello')

小贴士
注意,在会话中存储数据与使用全局变量没有什么不同。只有在必要的时候才使用它。这通常是多请求操作的情况,例如website_sale模块中的签出。

更多

route装饰器有额外的一些参数用于定制化请求行为。默认,所有的http请求都是可以的。methods参数,用于指定接收的请求类型,一般是['GET']或['POST']。
通过设置cors参数为"*",以允许跨域请求(处于安全考虑,浏览器将屏蔽跨域的AJAX以及其他的一些网络请求)。如果cors未设置,Access-Control-Allow-Origin头部将不会设置,浏览器将默认不允许跨域请求。在我们的例子中,我们将在/my_module/books/json设置cors参数,以允许所有的请求都可以访问该URL。
默认,odoo通过在请求中携带token保护请求免收跨站点伪造请求。如果想关掉该功能,可将csrf设置为False。

参考

关于HTTP路由的相关内容参考如下

  • 如果你在一个odoo实例中管理多个数据库,不同的数据库可能有不同的域。因此,你可以使用--db-filter,或者dbfilter_from_header模块,可帮助我们基于域过滤数据库。
  • 如何通过templates实现模块化,可通过 在后续章节中了解。

为url配置访问控制

本节,我们将探索三种权限验证。

准备

我们将利用第四章中的library.book模型。

步骤

定义controllers/main.py

  1. 添加显示所有图书的路由
@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
  1. 添加路由显示所有的图书以及被当前用户修改的图书。
@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
  1. 添加路由显示当前用户的图书
@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_libaray/all-books"和"/my_library/all-books/mark-mine"路由对于未授权用户而言是一样的,而登录用户在后面的路径上看到他们的书以粗体显示。"/my_library/all-books/mine"对于未授权用户是不可见的。如果您试图在没有经过身份验证的情况下访问它,您将被重定向到登录页面。

原理

不同权限验证方法之间的不同可在request.env.user中的上下文体现出来。
对于auth='none',用户记录总是空的,即便授权用户访问该URL。如果您希望提供不依赖于用户的内容,或者希望在服务器范围的模块中提供与数据库无关的功能,可以使用此选项。
对于auth='public',对于未授权用户,用户记录将被设置为base.public_user的XML ID,授权用户将设置为自己的ID。如果你计划为授权用户及未授权用户提供服务,只是授权用户将获得额外的内容时,可以使用此选项。
对于auth='user',可确保仅授权用户可访问。request.env.user将指向系统中的用户。

更多

身份验证方法定义在ir.http模型中。无论你在auth中传递何值,odoo将ir.http模型中查找_auth_method_方法,所以你可以通过继承ir.http模型自定义身份验证方法。
例如,我们提供了一个名为base_group_user的身份验证方法,该方法将仅运行登录账户属于base.group_user权限组的时候才对URL可见。

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'以确保只有base.group_user的用户可以访问该URL。With a little trickery, you can extend this to auth='groups(xmlid1,...)'; its implementation is left as an exercise to the reader but is included in the GitHub repository example code at Chapter13/ r2_paths_auth/my_library/models/sample_auth_http.py.

使用路由中传递的参数

本节将探讨几种针对用户输入进行响应的方式。

步骤

首先,我们添加一个接收图书ID并展示相应图书详细内容的路由。然后,我们将参数写入路由中。

  1. 添加接收图书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',)
  1. 添加接收图书ID作为URL一部分的路由
@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_library/book_details?book_id=1,你可以看到id=1的图书的详细页面。如果id不存在,将会报错。
第二个路由是允许我们通过/my_library/book_details/1展示id=1的详细页面。

原理

默认,odoo是混用GET和POST模式的。所以,我们定义了一个路由,其中包含Book_id的参数,那么该路由是支持GET(变量位于url中)或者POST(通过form提交)的。如果我们未传递该变量,且未设置该变量的默认值,那么将会报错。
第二个例子中,我们利用在werkzeug环境下,大部分路由都是虚拟的的现实情况。因此,我们可以方便在路由中包含变量。

更多

在路径中定义参数是由werkzeug提供的一种称为转换器的功能。模型转换器是由Odoo添加的,Odoo还定义了转换器模型,这些模型接受以逗号分隔的id列表,并将包含这些id的记录集传递给处理程序。
转换器的美妙之处在于运行时将参数强制转换为期望的类型,而您可以自己使用普通的关键字参数。它们是作为字符串传递的,您必须自己处理必要的类型转换,如第一个示例所示。
内置的werkzeug转换器包括int、float和string,但也包括更复杂的,如path、any和uuid。你可以在https:// werkzeug.palletsprojects.com/en/1.0.x/上查找它们的语义。

参考

如果您想了解更多关于HTTP路由的信息,请参考以下几点:

  • Odoo的自定义转换器定义在基本模块的ir_http.py中,并在ir.http的_get_converters类方法中注册。作为练习,您可以创建自己的转换器,它允许您访问/my_library/ book_details/Odoo+cookbook页面来接收该书的详细信息(如果您之前已将其添加到库中)。
  • 如果你想了解更多关于路径上的表单提交,请参考CMS网站开发第14章的“从用户获取输入”。

继承url的处理函数

当你安装website模块时,/website/info路径会显示你的Odoo实例的一些信息。在这个配方中,我们将重写它,以更改该信息页面的布局,并更改显示的内容。

准备

安装website模块

步骤

我们必须调整现有的模板并重写现有的处理程序。我们可以这样做:

  1. 重写qweb模板, views/templates.xml
<?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>
  1. 重写路由, controllers/main.py
from odoo import http
from odoo.addons.website.controllers.main import Website

class 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

现在,访问信息页面,我们仅能看到已安装的应用列表。

原理

步骤1,我们重写了QWeb模板,为了明确目标模板,需查阅源码。通常如下代码标识我们需要改写的模板名称:

return request.render('template.name', values)

在本案例中,处理程序使用的是名为website_info的模板,但它又被名为website.show_website_info的模板继承并改写,因此需要改写这个模板。我们替换了展示已安装应用的列表。对于QWeb的继承原理,可查阅第十五章,客户端开发。
为了重写处理程序,我们必须找到名为odoo.addons.website.controllers.main.website的处理程序。通过引入该类并继承实现改写。现在,通过改写我们将返回给内容进行了调整。注意,为简洁起见,此处重写的处理程序返回的是一个响应对象,而不是前面的方法所返回的HTML字符串。这个对象包含了对要使用的模板的引用以及模板可以访问的值,但是它只在请求的最后才被求值。
通常,有三种方法可以更改现有的处理程序:

  • 如果它使用QWeb模板,最简单的更改方法是重写模板。对于布局更改和小的逻辑更改,这是正确的选择。
  • QWeb模板获得传递的上下文,该上下文作为qcontext成员在响应中可用。这通常是一个可以根据需要添加或删除值的字典。在前面的例子中,我们只过滤了网站上的应用程序列表。
  • 如果处理程序接收到参数,您也可以对这些参数进行预处理,以使覆盖的处理程序按照您想要的方式运行。

更多

如前一节所述,控制器继承与模型继承的工作原理略有不同;实际上,您需要一个基类的引用,并在其上使用Python继承。
不要忘记用@http.route装饰新处理程序;Odoo使用它作为标记,将其方法暴露给网络层。如果忽略了装饰器,实际上会使处理程序的路径不可访问。
@http.route装饰器本身的行为类似于字段声明:你没有设置的每个值都将从你要重写的函数的装饰器派生,所以我们不必重复我们不想更改的值。
从你覆盖的函数接收到一个响应对象后,你可以做更多的事情,而不仅仅是改变QWeb上下文:

  • 您可以通过操作response.headers来添加或删除HTTP标头。
  • 如果您想呈现一个完全不同的模板,您可以覆盖response.template。
  • 要首先检测响应是否基于QWeb,请查询
    response.is_qweb。
  • 通过调用response.render()可以获得生成的HTML代码。

提供静态资源

Web页面包含几种类型的静态资源,比如图像、视频、CSS等等。在本教程中,我们将了解如何为模块管理这样的静态资源。

准备

对于这个配方,我们将在页面上显示一个图像。所以,准备一张图片。另外,从之前的章节中获取my_library模块。

步骤

按照以下步骤在页面上显示图像:

  1. 将图像添加到/my_library/static/src/img目录。
  2. 在controller中定义新的路由。在代码中,将图像URL替换为你的图像URL:
@http.route('/demo_page', type='http', auth='none')
def books(self):
    image_url = '/my_library/static/src/image/odoo.png'
    html_result = """<html>
        <body>
        <img src="%s"/>
        </body>
        </html>""" % image_url
    return html_result

重新启动服务器并更新模块以应用更改。现在访问/demo_page查看页面上的图像。

原理

位于/static文件夹下的所有文件都被认为是静态资源,可以公开访问。在我们的例子中,我们将图像放在/static/src/img目录中。你可以把静态资源放在静态目录下的任何位置,但是根据文件类型有一个推荐的目录结构:

  • /static/src/img是用于存放图像的目录。
  • /static/src/css是css文件的目录。
  • /static/src/scss是存放scss文件的目录。
  • /static/src/fonts是用于字体文件的目录。
  • /static/src/js是JavaScript文件的目录。
  • /static/src/xml是客户端QWeb模板的xml文件目录。
  • /static/lib是用于存放外部库文件的目录。
    在我们的示例中,我们在页面上显示了一个图像。您还可以访问该映像
    直接从/my_library/static/src/image/ odoo.png。
    在本节中,我们在页面上显示了一个静态资源(图像),并看到了不同静态资源的推荐目录。还有更简单的方法来表示页面内容和静态资源,我们将在下一章中看到。
posted @ 2021-03-07 07:40  老韩头的开发日常  阅读(1158)  评论(0编辑  收藏  举报