odoo13学习---14 Web服务器开发

使路径可从网络访问

我们将了解如何让用户可以访问http://yourserver/path1/path2表单的URL。这可以是一个web页面,也可以是返回要被其他程序使用的任意数据的路径。在后一种情况下,通常使用JSON格式使用参数并提供数据。

准备

我们希望允许任何用户查询完整的图书列表。此外,我们希望通过JSON请求向程序提供相同的信息。

怎么做呢?

我们需要添加控制器,按照惯例将其放入名为controllers的文件夹中:

1. 添加一个controller /main.py文件与HTML版本的页面,如下所示:

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

2. 添加一个函数以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'])

3.添加controllers/__init__.py文件,如下所示: 

from . import main

4. 将控制器导入到my_library/__init__.py文件中,如下所示:

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 decorator允许我们告诉Odoo一个方法首先应该是web可访问的,而第一个参数决定了它在哪个路径上可访问。如果使用同一个函数服务多个路径,还可以传递字符串列表,而不是字符串。type参数默认为http,并确定要服务的请求类型。严格地说,JSON是HTTP,将第二个函数声明为type=' JSON '会使我们的工作变得容易得多,因为Odoo会为我们处理类型转换。

现在不用担心auth参数;这将在本章的限制访问web可访问路径配方中讨论。 

返回值

Odoo对函数返回值的处理是由路由装饰器的类型参数决定的。对于type='http',我们通常想要传递一些HTML,所以第一个函数只是返回一个包含它的字符串。另一种方法是使用request.make_response(),它允许您控制要在响应中发送的头。因此,为了指示页面最后更新的时间,我们可以将books()中的最后一行更改为以下代码:

return 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)
),
])

这段代码将发送一个last -modified头以及我们生成的HTML,告诉浏览器最后一次修改列表的时间。我们可以从库的write_date字段中提取这些信息。本模型。为了让前面的代码片段发挥作用,你需要在文件的顶部添加一些导入,如下所示:

import email
import datetime
from odoo import fields

您还可以手动创建werkzeug的响应对象并返回该对象,但是这样做不会带来什么好处。

手工生成HTML非常适合用于演示,但是千万不要在生产代码中这样做。始终使用模板,如第16章“Web客户端开发”中的“创建或修改模板—QWeb”中所示,并通过调用request.render()返回模板。

这将免费为您提供本地化,并通过将业务逻辑与表示层分离使您的代码更好。此外,模板还提供了在输出HTML之前转义数据的函数。前面的代码容易受到跨站点脚本攻击(例如,如果用户设法将脚本标记插入到图书名称中)。

对于一个JSON请求,只需返回你想要移交给客户端的数据结构;Odoo负责序列化。要做到这一点,您应该将自己限制在JSON可序列化的数据类型上,这些数据类型大致是字典、列表、字符串、浮点数和整数。

odoo.http.request

请求对象是一个静态对象,它引用当前处理的请求,其中包含您采取操作所需的所有内容。这里最重要的方面是request.env属性,它包含一个与模型的self.env相同的环境对象。这个环境被绑定到当前用户(在前面的示例中没有),因为我们使用了auth='none'。缺少用户也是我们必须在示例代码中使用sudo()来调用所有模型方法的原因。

如果您习惯了web开发,那么您会期望会话处理,这是完全正确的。 在OpenERPSession对象,使用request.session(这是相当薄包装werkzeug的会话对象),和request.session.sid访问会话ID。存储会话值,只是把request.session当作字典,如以下示例所示:

request_session['你好']=‘世界’

request.session_get(“你好”)

注意,在会话中存储数据与使用全局变量没有区别。如果有必要的话,请使用它。这通常是多请求操作的情况,比如website_sale模块中的签出。在这种情况下,tiy sgiykd处理控制器中所有与会话有关的功能,而不是模型中。

有更多的…

route decorator可以有一些额外的参数,以便进一步定制其行为。默认情况下,所有的HTTP方法都是允许的,Odoo将传递的参数混合在一起。

使用methods参数,您可以传递一个要接受的方法列表,通常是['GET']或['POST']中的一个。

允许cross-origin请求(浏览器阻止AJAX和一些其他类型的请求域脚本从何处加载以外,安全与隐私原因),设置cors参数为 *,允许请求从所有来源,或限制请求URI的来自这个URI。如果这个参数未设置(这是默认值),那么Access-Control-Allow-Origin头文件就没有设置,剩下的就是浏览器的标准行为了。在我们的示例中,我们可能希望将其设置为/my_module/books/json,以便允许从其他网站提取的脚本访问图书列表。

默认情况下,Odoo通过在每个请求上传递一个令牌,来保护某些类型的请求免受称为“跨站点请求伪造”的攻击。如果您想关闭它,可以将csrf参数设置为false,但请注意,通常这不是一个好主意。

请参阅以下要点以了解更多有关HTTP路由的信息:

 如果在同一个实例上托管多个Odoo数据库,那么不同的数据库可能运行在不同的域上。在这种情况下,您可以使用--db-filter选项,也可以使用https://github.com/OCA/server-tools中的dbfilter_from_header模块,它可以帮助您基于域过滤数据库。在撰写本书时,这个模块还没有迁移到版本12,但在出版时,它可能已经迁移到了版本12。


 

限制访问web可访问路径

 

我们将探讨Odoo为路由提供的三种身份验证机制。我们将使用不同的身份验证机制定义路由,以显示它们的差异。

 

准备

 

在扩展上一个菜谱中的代码时,我们还将依赖于库。书的第5章模型,应用程序模型,所以你应该得到它的代码,以便继续。

怎么做呢?

在controller /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

2. 添加一个路径,以显示所有图书,并指出当前用户编写了哪些图书(如果有的话)。如下例所示:

    @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

3.添加一个显示当前用户的图书的路径,如下所示:

    @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/标记-mine路径看起来是相同的,而登录用户在后一个路径上可以看到他们的图书以粗体显示。未经身份验证的用户根本无法访问/my_library/allbooks/ mine路径。如果您尝试在没有经过身份验证的情况下访问它,那么您将被重定向到登录屏幕。

它是如何工作的…

身份验证方法之间的区别基本上可以从request.env.user的内容中看出。

对于auth='none',用户记录总是空的,即使通过身份验证的用户正在访问该路径。如果希望提供对用户没有依赖关系的内容,或者希望在服务器范围的模块中提供与数据库无关的功能,请使用此功能。

auth='public'将未经身份验证的用户的用户记录设置为XML ID base.public_user的特殊用户,将经过身份验证的用户的用户记录设置为用户的记录。如果您希望同时为未经过身份验证的用户和经过身份验证的用户提供功能,而经过身份验证的用户可以获得一些额外功能,这是正确的选择,如前面的代码所示。

使用auth='user'来确保只有经过身份验证的用户才能访问您提供的内容。使用此方法,您可以确保request.env.user指向一个现有用户。

有更多的…

身份验证方法的魔力发生在基本外接程序的ir.http模型中。不管你传递给路径中的auth参数的是什么值,Odoo都会搜索一个名为_auth_method_<yourvalue>的函数。因此,您可以通过继承它并声明一个负责所选身份验证方法的方法来轻松地对其进行定制。

作为示例,我们将提供一种名为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()

现在您可以在decorator中说auth='base_group_user',并确保运行此路由处理程序的用户是该组的成员。通过一个小技巧,您可以将其扩展到auth='groups(xmlid1,…)';它的实现留给读者作为练习,但是包含在GitHub存储库示例代码Chapter14/r2_paths_auth/my_library/models/sample_auth_http.py中。

from odoo import exceptions, models
from odoo.http import request


class IrHttp(models.AbstractModel):
    _inherit = 'ir.http'

    @classmethod
    def _auth_method_base_group_user(cls):
        cls._auth_method_user()
        if not request.env.user.has_group('base.group_user'):
            raise exceptions.AccessDenied()

    # this is for the exercise
    @classmethod
    def _auth_method_groups(cls, group_xmlids=None):
        cls._auth_method_user()
        if not any(map(request.env.user.has_group, group_xmlids or [])):
            raise exceptions.AccessDenied()


    # the controller will be like this add this in main.py

    @http.route('/my_module/all-books/group_user', type='http',
                auth='base_group_user')
    def all_books_mine_base_group_user(self):
        # your code
        return ...

    # this is for the exercise
    @http.route('/my_module/all-books/groups', type='http',
                auth='groups(base.group_no_one)')
    def all_books_mine_groups(self):
        # your code
        return ...

 


使用传递给处理程序的参数

能够显示内容很好,但最好是显示用户输入的结果。接收此输入并对其作出反应的不同方法。与前面的一样,我们将使用library_book模型。

 怎么做呢?

首先,我们将添加一个路由,它需要一个带有图书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', )

如果你把浏览器指向/my_module/book_details?book_id=1,您应该会看到ID为1的书的详细页面。如果不存在,您将收到一个错误页面。

 

 

2. 添加一个路径,我们可以在路径中传递图书的ID,如下所示:

    @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/1并查看相同的页面。

它是如何工作的…

默认情况下,Odoo(实际上是werkzeug)混合了GET和POST参数,并将它们作为关键字参数传递给处理程序。

因此,只需简单地将函数声明为期望一个名为book_id的参数,就可以将这个参数引入为GET (URL中的参数)或POST(通常通过<form>传递)。并将处理程序作为操作属性)。

如果我们没有为该参数添加默认值,那么如果您尝试在没有设置该参数的情况下访问该路径,运行时将引发错误。

第二个例子利用了这样一个事实:在werkzeug环境中,大多数路径都是虚拟的。因此,我们可以简单地将路径定义为包含一些输入。

在本例中,我们说希望将library.book的ID作为路径的最后一个组件。冒号后的名称是关键字参数的名称。我们的函数将使用此参数作为关键字参数被调用。在这里,Odoo负责查找这个ID并提供一个浏览记录,当然,只有当访问这个路径的用户具有适当的权限时,它才工作。假设图书是一个浏览记录,我们可以简单地通过将book.id作为book_id参数传递来循环第一个示例的函数,从而给出相同的内容。

 

有更多的…

 

在路径中定义参数是werkzeug提供的一项功能,称为转换器。Odoo添加了模型转换器,它还定义了接受逗号分隔的id列表的转换器模型,并将包含这些id的记录集传递给处理程序。

转换器的美妙之处在于,运行时将参数强制为期望的类型,而您只能使用正常的关键字参数。它们是作为字符串交付的,您必须自己处理必要的类型转换,如第一个示例所示。

内置的werkzeug转换器包括int、float和string,但也包括更复杂的转换器,如path、any或uuid。您可以在http://werkzeug.pocoo.org/docs/0.11/routing/#builtin-converters上查找它们的语义。

另请参阅

如果你想了解更多关于HTTP路由,请参考以下几点:Odoo的自定义转换器在ir_http.py的基本模块中定义,并在ir.http的_get_converters类方法中注册。作为练习,您可以创建自己的转换器,它允许您访问/my_library/book_details/Odoo+cookbook页面来接收这本书的详细信息(如果您之前将它添加到您的库中)。


 

修改现有的处理程序

 

当你安装网站模块时,/website/info路径显示一些关于你的Odoo实例的信息。在此菜谱中,我们将覆盖此内容,以更改此信息页面的布局,并更改所显示的内容。

 

准备

 

安装网站模块并检查/website/info路径。在此菜谱中,我们将更新/website/info路径以提供更多信息。

 

怎么做呢?

 

我们必须修改现有的模板并覆盖现有的处理程序。我们可以这样做:

 

1. 在一个名为views/templates的文件中覆盖qweb模板。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>

 

 

2. 重写一个名为controllers/main的文件中的处理程序。,如下例所示:

# -*- coding: utf-8 -*-
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

 

 

 

 现在,在访问info页面时,我们只会在表中看到经过筛选的已安装应用程序列表,而不是原始的定义列表。

 

  它是如何工作的… 

  在第一步中,我们覆盖了现有的QWeb模板。为了找出它是哪一个,您必须查阅原始处理程序的代码。通常,这将以类似于下面的代码结束,它告诉您需要重写template_name:

return request_render('template_name', values)

  在我们的例子中,处理程序使用了一个名为website_info的模板,但是这个模板立即被另一个名为website_show_website_info的模板扩展,因此覆盖这个模板更方便。这里,我们用一个表替换了显示已安装应用程序的定义列表。有关QWeb继承如何工作的详细信息,请参阅第16章Web客户端开发。

  为了覆盖处理程序方法,我们必须标识定义处理程序的类,在本例中是odoo_addons_website_controllers_main_Website我们需要导入类以便能够从它继承。现在,我们可以覆盖该方法并更改传递给响应的数据。注意,为了简洁起见,这里被覆盖的处理程序返回的是一个响应对象,而不是前面菜谱所返回的HTML字符串。此对象包含对要使用的模板的引用和模板可访问的值,但仅在请求的最后才计算它。

  通常,有三种方法来改变一个现有的处理程序:

  如果它使用QWeb模板,最简单的更改方法是覆盖模板。这是布局变化和小逻辑变化的正确选择。

  QWeb模板获得传递的上下文,该上下文在响应中作为qcontext成员可用。这通常是一个字典,您可以根据需要在其中添加或删除值。在前面的例子中,我们只过滤了网站上的应用程序列表。

  如果处理程序接收参数,您还可以对这些参数进行预处理,以便覆盖的处理程序按您希望的方式运行。

 

  有更多的…

  如前所述,控制器的继承与模型继承的工作方式略有不同;实际上,您需要对基类进行引用,并在其上使用Python继承。

  不要忘记用@http_route装饰器装饰你的新处理器;Odoo使用它作为一个标记,用于将哪些方法暴露给网络层。如果省略了decorator,则实际上使处理程序的路径不可访问。@http_route装饰器本身的行为类似于字段声明:您没有设置的每个值都将派生自您正在重写的函数的装饰器,因此我们不必重复我们不想更改的值。

  在从你覆盖的函数接收到一个响应对象后,你可以做的不仅仅是改变QWeb上下文:您可以通过操作response_headers来添加或删除HTTP headers 

  如果您想呈现一个完全不同的模板,您可以覆盖response_template,要首先检测响应是否基于QWeb,请查询response_is_qweb,通过调用response_render()可以得到结果HTML代码

 另请参阅 

  QWeb模板的细节将在第16章Web客户端开发中解释。

 

 

 

 

 

 

 

 

posted @ 2020-10-27 15:37  YongL  阅读(318)  评论(0)    收藏  举报