Odoo 起初是一个后台系统,但很快就有了前端界面的需求。早期基于后台界面的门户界面不够灵活并且对移动端不友好。为解决这一问题,Odoo 引入了新的网站功能,为系统添加了 CMS(Content Management System)内容管理系统。这使得我们无需集成第三方 CMS 便可创建美观又高效的前端。本文中我们将学习如何利用 Odoo 自带的网站功能开发面向前端的插件模块。

本文主要内容有:

  • 学习项目 – 自助图书馆
  • 第一个网页
  • 创建网站

开发准备

我将用第十一章 Odoo 12开发之看板视图和用户端 QWeb中最后编辑的library_checkout插件模块,代码请见GitHub 仓库。本文完成后的代码也请参见GitHub 仓库

学习项目 – 自助图书馆

本文中我们将为图书会员添加一个自助服务功能。可供会员分别登录账号来访问他们的借阅请求列表。这样我们就可以学习网站开发的基本技术:创建动态页面、在页面间传递参数、创建表单以及处理表单数据验证。对这些新的图书网站功能,我们要新建一个插件模块library_website。

大家应该已经轻车熟路了,首先创建插件的声明文件ibrary_website/__manifest__.py,代码如下:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
{
    'name': 'Library Website',
    'description': 'Create and check book checkout requests.',
    'author': 'Alan Hou',
    'depends': [
        'library_checkout'
    ],
    'data': [
        'security/ir.model.access.csv',
        'security/library_security.xml',
        'views/library_member.xml',
    ],
}

网站功能将会依赖于library_checkout。我们并没有添加对website核心插件模块的依赖。website插件为创建完整功能的网站提供了有用的框架,但现在我们仅探讨核心框架自带的基础网站功能,尚无需使用website。我们想要图书会员通过登录信息在图书网站上访问自己的借阅请求。为此需要在图书会员模型中添加一个user_id字段,需要分别在模型和视图中添加,下面就开始进行网站的创建:

1、添加library_website/models/library_member.py文件

 
1
2
3
4
5
from odoo import fields, models
 
class Member(models.Model):
    _inherit = 'library.member'
    user_id = fields.Many2one('res.users')

2、添加library_website/models/__init__.py文件:

 
1
from . import library_member

3、添加library_website/__init__.py文件:

 
1
from . import models

4、添加library_website/views/library_member.xml文件:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0"?>
<odoo>
    <record id="view_form_member" model="ir.ui.view">
        <field name="name">Member Form</field>
        <field name="model">library.member</field>
        <field name="inherit_id" ref="library_member.view_form_member" />
        <field name="arch" type="xml">
            <field name="card_number" position="after">
                <field name="user_id" />
            </field>
        </field>
    </record>
</odoo>

访问这些网页的都是门户用户,无需访问后台菜单。我们需要为这个用户组设置安全访问权限,否则会在使用图书网站功能时报权限错误。

5、添加library_website/security/ir.model.access.csv文件,添加对图书模型的读权限:

 
1
2
3
4
5
6
7
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_book_portal,Book Portal Access,library_app.model_library_book,base.group_
portal,1,0,0,0
access_member_portal,Member Portal Access,library_member.model_library_member,ba
se.group_portal,1,0,0,0
access_checkout_portal,Checkout Portal Access,library_checkout.model_library_che
ckout,base.group_portal,1,0,0,0

6、在library_website/security/library_security.xml文件中添加记录规则来限制门户用户所能访问的记录:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0"?>
<odoo>
    <data noupdate="1">
        <record id="member_portal_rule" model="ir.rule">
            <field name="name">Library Member Portal Access</field>
            <field name="model_id" ref="library_member.model_library_member" />
            <field name="domain_force">
                [('user_id', '=', user.id)]
            </field>
            <field name="groups" eval="[(4,ref('base.group_portal'))]" />
        </record>
 
        <record id="checkout_portal_rule" model="ir.rule">
            <field name="name">Library Checkout Portal Access</field>
            <field name="model_id" ref="library_checkout.model_library_checkout" />
            <field name="domain_force">
                [('member_id.user_id', '=', user.id)]
            </field>
            <field name="groups" eval="[(4,ref('base.group_portal'))]" />
        </record>
    </data>
</odoo>

base.group_portal是门户用户组的标识符。在创建门户用户时,应设置他们的用户类型为 Portal,而不是Internal User。这会让他们属于门户用户组并继承我们上面定义的访问权限:

补充:以上内容需开启开发者模式才可见

一旦为图书会员创建了一个门户用户,就应在我们会员表单中的用户字段中使用。该登录信息将可以访问相应会员的借阅请求。

小贴士:在模型中使用 ACL 和记录规则来实现安全权限比使用控制器的逻辑要更为安全。这是因为攻击者有可能跳过网页控制器直接使用RPC 来访问模型 API 。

了解了这些,我们就可以开始实现图书网站的功能了。但首先我们来使用简单的Hello World网页简短地介绍下基本网站概念。

第一个网页

要开始了解 Odoo 网页开发的基础,我们将先实现一个Hello World网页来展示基本概念和技术。很有想象空间,是不是?

要创建第一个网页,我们需要一个控制器对象。首先来添加controllers/hello.py文件:

1、在library_website/__init__.py文件中添加如下行:

 
1
from . import controllers

2、在library_website/controllers/__init__.py文件中添加如下行:

 
1
from . import hello

3、添加实际的控制器文件 library_website/controllers/hello.py,代码如下:

 
1
2
3
4
5
6
from odoo import http
 
class Hello(http.Controller):
    @http.route('/helloworld', auth="public")
    def helloworld(self):
        return('<h1>Hello World!</h1>')

odoo.http模块提供 Odoo 网页相关的功能。我们用于渲染页面的控制器,应该是一个继承了odoo.http.Controller类的对象。实际使用的名称并不是太重要,这里选择了 Hello(),一个常用的选择是 Main()。

在控制器类中使用了匹配 URL 路由的方法。这些路由用于做一些处理并返回结果,通常是返回用户网页浏览器的 HTML 页面。odoo.http.route装饰器用于为 URL 路由绑定方法,本例中使用的是/helloworld 路由。

安装library_website模块(~/odoo-dev/odoo/odoo-bin -d dev12 -i library_website)就可以在浏览器中打开http://xxx:8069/helloworld,我们应该就可以看到Hello World问候语了。

本例中方法执行的处理非常简单,它返回一个带有 HTML 标记的文本字符串,Hello World。

ℹ️使用这里的简单 URL 访问按制器,如果同一 Odoo 实例有多个数据库时,在没有指定目标数据库的情况下将会失败。这可通过在启动配置中设置-d或–db-filter来解决,参见第二章 Odoo 12开发之开发环境准备

你可能注意到在路由装饰中使用了auth=’public’参数,对于无需登录的用户开放的页面就需要使用它。如果删除该参数,仅有登录用户方可浏览此页面。如果没有活跃会话(session)则会进入登录页面。

小贴士:auth=’public’参数实际表示如果访客未登录则使用public特殊用户运行网页控制器。如果登录了,则使用登录用户来代替public。

使用 QWeb 模板的 Hello World

使用 Python 字符串来创建 HTML 很快就会觉得乏味。QWeb可用来增添色彩,下面就使用模板来写一个改进版的Hello World网页。QWeb模板通过 XML 数据文件添加,技术层面上它是与表单、列表视图类似的一种视图类型。它们甚至存储在同一个技术模型ir.ui.view中。

老规矩,需要在声明文件中添加声明来加载文件,编辑library_website/__manifest__.py文件并添加内容如下:

 
1
2
3
4
'data': [
...
  'views/helloworld_template.xml',
],

然后添加实际的数据文件views/helloworld_template.xml,内容如下:

 
1
2
3
4
5
6
<?xml version="1.0"?>
<odoo>
    <template id="helloworld" name="Hello World Template">
        <h1>Hello again World!</h1>
    </template>
</odoo>

<template>实际上是一种简写形式,它声明<record>将数据以type=”qweb”类型加载到ir.ui.view模型中。现在,我们需要修改控制器方法来使用这个模板:

 
1
2
3
4
5
6
7
8
from odoo import http
from odoo.http import request
 
class Hello(http.Controller):
 
    @http.route('/helloworld', auth="public")
    def helloworld(self, **kwargs):
        return request.render('library_website.helloworld')

模板的渲染是通过render()函数的 request 对象来实现的。

小贴士:注意我们添加了**kwargs方法参数。使用该参数,HTTP 请求中的任意附加参数,如GET 或 POST 请求参数,可通过 kwargs 字典捕获。这会让我们的方法更加健壮,因为即便添加了未预期的参数也不会产生错误。

HelloCMS!

下面我们来增加点趣味性,创建我们自己的简单 CMS。为此我们可以通过 URL在路由中使用模板名(一个页面),然后对其进行渲染。然后就可以动态创建网页,通过我们的 CMS 来提供服务。实现方法很简单:

 
1
2
3
@http.route('/hellocms/<page>', auth='public')
def hello(self, page, **kwargs):
    return http.request.render(page)

以上page 参数应匹配一个模板的外部ID,如果在浏览器中打开http://xxx:8069/hellocms/library_website.helloworld,应该又可以看到熟悉的Hello World 页面了。实际上内置的website模块提供了CMS功能,在 /page路径(endpoint)下还包含更为健壮的实现。

ℹ️在werkzeug的行话中,endpoint是路由的别名,由其静态部分(不含占位符)来表示。比如,CMS 示例中的 endpoint为/hellocms。

大多数情况下,我们要将页面集成到 Odoo 网站中,因此接下来的示例将使用website插件模块。

创建网站

前面的示例并未集成到 Odoo 网站中,并有页面 footer 和网站菜单。Odoo 的website插件模板为方便大家提供这些功能。

要使用网站功能,我们需要在工作实例中安装website插件模块。应当在library_website插件模块中添加这一依赖,修改__manifest__.py的 depends 内容如下:

 
1
2
3
4
'depends': [
    'library_checkout',
    'website',
],

要使用网站功能,我们需要对控制器和 QWeb模板进行一些修改。控制器中可在路由上添加一个额外的website=True参数:

 
1
2
3
@http.route('/helloworld', auth="public", website=True)
def helloworld(self, **kwargs):
    return request.render('library_website.helloworld')

集成website模块并非严格要求website=True参数,不添加它也可以在模板视图中添加网站布局。但是通过添加可以让我们在网页控制器中使用一些功能:

  • 路由会自动变成支持多语言并且会从网站安装的语言中自动检测最接近的语言。需要说明这可能会导致重新路由和重定向。
  • 控制器抛出的任何异常都会由网站代码进行处理,这会将默认的错误码变成更友好的错误页面向访客展示。
  • 带有当前网站浏览记录的request.website变量,可在请求中进行使用。
  • auth=public路由的 public用户将是由后台网站配置中选择的用户。这可能会和本地区、时区等相关。

如果在网页控制器中无需使用上述功能,则可省略website=True参数。但大多数网站QWeb模板需要使用website=True开启一些数据,比如底部公司信息,所以最好还是添加上。

ℹ️传入QWeb运行上下文语言的网站数据由website/model/ir_ui_view.py文件中的_prepare_qcontext方法设定。

要在模板中添加网站的基本布局,应为QWeb/HTML包裹一个t-call=”website.layout”指令,如下所示:

 
1
2
3
4
5
<template id="helloworld" name="Hello World Template">
    <t t-call="website.layout">
        <h1>Hello World!</h1>
    </t>
</template>

t-call运行QWeb模板website.layout并向其传递 XML 内的tcall 节点。website.layout设计用于渲染带有菜单、头部和底部的完整网页,交将传入的内容放在对应的主区域内。这样,我们的Hello World!示例内容就会显示在 Odoo 网站页面中了。

添加 CSS 和 JavaScript 资源

我们的网站页面可能需要一些其它的 CSS 或JavaScript资源。这方面的网页由website 管理,因此需要一个方式来告诉它使用这些文件。我们将使用 CSS 来添加一个简单的删除线效果,创建library_website/static/src/css/library.css文件并添加如下内容:

 
1
2
3
.text-strikeout {
    text-decoration: line-through;
}

接下来需要在网站页面中包含该文件。通过在website.assets_frontend模板中添加来实现,该模板用于加载网站相关的资源。添加library_website/views/website_assets.xml数据文件来继承该模板:

 
1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0"?>
<odoo>
    <template id="assets_frontend"
        name="library_website_assets"
        inherit_id="website.assets_frontend">
        <xpath expr="." position="inside">
            <link rel="stylesheet" type="text/css"
                href="/library_website/static/src/css/library.css" />
        </xpath>
    </template>
</odoo>

很快我们就会使用text-strikeout这个新的样式类。当然,可以使用相似的方法来添加JavaScript资源。

借阅列表控制器

既然我们已经过了一遍基础知识,就来一起实现借阅列表吧。我们需要使用/checkout URL来显示借阅列表的网页。为此我们需要一个控制器方法来准备要展示的数据,以及一个QWeb模板来向用户进行展示。

在模块中添加library_website/controllers/main.py文件,代码如下:

 
1
2
3
4
5
6
7
8
9
10
11
from odoo import http
from odoo.http import request
 
class Main(http.Controller):
    @http.route('/checkouts', auth='user', website=True)
    def checkouts(self, **kwargs):
        Checkout = request.env['library.checkout']
        checkouts = Checkout.search([])
        return request.render(
            'library_website.index',
            {'docs': checkouts})

控制器获取要使用的数据并传给渲染的模板。本例中控制器需要一个登录了的会话,因为路由中有一个auth=’user’属性。这是默认行为,推荐明确指出需要用户会话。登录了的用户存储在环境对象中,通过 request.env来使用。search()语句使用它来过滤出相应的借阅记录。

对于无需登录即可访问的控制器,所能读取的数据也是非常有限的。这种情况下,我们经常需要对部分代码采用提权上下文运行。这时我们可使用sudo()模型方法,它将权限上下文权限修改为内部超级用户,突破大部分限制。权力越大,责任越大,我们要小心这种操作带来的安全风险。需要特别注意在提权时输入的参数以及执行的操作的有效性。建议将sudo() 记录集操作控制在最小范围内。

回到我们的代码,它以request.render()方法收尾。和之前一样,我们传入了QWeb模板渲染的标识符,和模板运行用到的上下文字典。本例中我们向模板传入 docs 变量,该变量包含要渲染借阅记录的记录集。

借阅 QWeb 模板

QWeb模板使用数据文件来添加,我们可以使用library_website/views/checkout_template.xml文件并添加如下代码:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0"?>
<odoo>
    <template id="index" name="Checkout List">
        <t t-call="website.layout">
            <div id="wrap" class="container">
                <h1>Checkouts</h1>
 
                <!-- List of Checkouts -->
                <t t-foreach="docs" t-as="doc">
                    <div class="row">
                        <input type="checkbox" disabled="True"
                            t-att-checked="'checked' if doc.stage_id.fold else None" />
                        <a t-attf-href="/checkout/{{slug(doc)}}">
                            <h3 t-field="doc.request_date"
                                t-att-class="'text-strikeout' if doc.stage_id.fold else ''" />
                        </a>
                    </div>
                </t>
            </div>
        </t>
    </template>
</odoo>

以上代码使用t-foreach指令来迭代 docs 记录集。我们使用了复选框 input 并在借阅完成时保持为已选状态。在 HTML 中,复选框是否被勾选取决于是否有 checked 属性。为此我们使用了t-att-NAME指定来根据表达式动态渲染 checked 属性。当表达式运行结果为 None(或任意其它 false 值)时,QWeb会忽略该属性,本例用它就非常方便了。

在渲染任务名时,t-attf指令用于动态创建打开每个指定任务的明细表单的URL。我们使用一个特殊函数slug()来为每条记录生成易于阅读的 URL。该链接目前尚无法使用,因为我们还没有创建对应的控制器。

在每条借阅记录上,我们还使用了t-att 指令来在借阅为最终状态时应用text-strikeout样式。

借阅明细页面

借阅列表中的每一项都有一个相应明细页面的链接。我们就为这些链接实现一个控制器,以及实现一个QWeb模板来用于展示。说到这里应该已经很明朗了。

在library_website/controllers/main.py文件中添加如下方法:

 
1
2
3
4
5
6
7
8
9
10
class Main(http.Controller):
...
 
    @http.route('/checkout/<model("library.checkout"):doc>',
        auth='user', # 默认值,但此处明确指定
        website=True)
    def checkout(self, doc, **kwargs):
        return http.request.render(
            'library_website.checkout',
            {'doc': doc})

注意这里路由使用了带有model(“library.checkout”)转换器的占位符,会映射到方法的 doc 变量中。它从 URL 中捕获借阅标识符,可以是简单的 ID 数值或链接别名,然后转换成相应的浏览记录对象。

对于QWeb模板,应在library_website/views/checkout_template.xml数据文件中添加如下代码:

 
1
2
3
4
5
6
7
8
9
    <template id="checkout" name="Checkout Form">
        <t t-call="website.layout">
            <div id="wrap" class="container">
                <h1 t-field="doc.request_date" />
                <h5>Member: <span t-field="doc.member_id" /></h5>
                <h5>Stage: <span t-field="doc.stage_id" /></h5>
            </div>
        </t>
    </template>

这里值得一提的是使用了<t t-field>元素。和在后台中一样,它处理字段值的相应展示。比如,它正确地展示日期值和many-to-one值。

补充:controllers/__init__.py和__mainfest__.py 中请自行添加控制器文件和数据文件的引用

总结

读者现在应该对网站功能的基础有了不错的掌握。我们学习了如何使用网页控制器和QWeb模板来动态渲染网页。然后学习了如何使用website插件并使用它来创建我们自己页面。最后,我们介绍了网站表单插件来帮助我们来创建网页表单。这些都是创建网站功能的核心能技巧。

我们已经学习了Odoo 主要构件的开发,是时候学习如何将Odoo 服务部署到生产环境了。

☞☞☞第十四章 Odoo 12开发之部署和维护生产实例

扩展阅读

Odoo 官方文档中有一些对本文讲解课题的补充参考材料:

第十三章 Odoo 12开发之创建网站前端功能的更多相关文章

  1. 第三章 Odoo 12 开发之创建第一个 Odoo 应用

    Odoo 开发通常都需要创建自己的插件模块.本文中我们将通过创建第一个应用来一步步学习如何在 Odoo 中开启和安装这个插件.我们将从基础的开发流学起,即创建和安装新插件,然后在开发迭代中更新代码来进 ...

  2. 第十二章 Odoo 12开发之报表和服务端 QWeb

    报表是业务应用非常有价值的功能,内置的 QWeb 引擎是报表的默认引擎.使用 QWeb 模板设计的报表可生成 HTML 文件并被转化成 PDF.也就是说我们可以很便捷地利用已学习的 QWeb 知识,应 ...

  3. 第九章 Odoo 12开发之外部 API - 集成第三方系统

    Odoo 服务器端带有外部 API,可供网页客户端和其它客户端应用使用.本文中我们将学习如何在我们的客户端程序中使用 Odoo 的外部 API.为避免引入大家所不熟悉的编程语言,此处我们将使用基于 P ...

  4. 第六章 Odoo 12开发之模型 - 结构化应用数据

    在本系列文章第三篇Odoo 12 开发之创建第一个 Odoo 应用中,我们概览了创建 Odoo 应用所需的所有组件.本文及接下来的一篇我们将深入到组成应用的每一层:模型层.视图层和业务逻辑层. 本文中 ...

  5. 第五章 Odoo 12开发之导入、导出以及模块数据

    大多数Odoo 模块的定义,如用户界面和安全规则,实际是存储在对应数据表中的数据记录.模块中的 XML 和 CSV 文件不是 Odoo 应用运行时使用,而是载入数据表的手段.正是因为这个原因,Odoo ...

  6. 第四章 Odoo 12 开发之模块继承

    Odoo 的一个强大功能是无需直接修改底层对象就可以添加功能.这是通过其继承机制来实现的,采取在已有对象之上修改层来完成.这种修改可以在不同层上进行-模型层.视图层和业务逻辑层.我们创建新的模块来做出 ...

  7. 第十一章 Odoo 12开发之看板视图和用户端 QWeb

    QWeb 是 Odoo 使用的模板引擎,它基于 XML 来生成 HTML 片断和页面.通过 QWeb可生成内容丰富的看板(Kankan)视图.报表和 CMS 网页.本文中我们将学习QWeb 语法以及如 ...

  8. 第二章 Odoo 12开发之开发环境准备

    在更深入了解 Odoo 开发之前,我们应配置好开发环境并学习相关的基础管理任务.本文中,我们将学习创建 Odoo 应用所需用到的工具和环境配置.这里采用 Ubuntu 系统来作为开发服务器实例的主机, ...

  9. 第十四章 Odoo 12开发之部署和维护生产实例

    本文中将学习将 Odoo 服务器作为生产环境的基本准备.安装和维护服务器是一个复杂的话题,应该由专业人员完成.本文中所学习的不足以保证普通用户创建应对包含敏感数据和服务的健壮.安全环境. 本文旨在介绍 ...

随机推荐

  1. 2019 IEEEXtreme 13.0 题解记录

    比赛时间 2019.10.19 8:00 - 2019.10.20 8:00 比赛网站 https://csacademy.com/ieeextreme13 // 连续24小时做题真的是极限体验 // ...

  2. Spring Boot集成Shiro实战

    Spring Boot集成Shiro权限验证框架,可参考: https://shiro.apache.org/spring-boot.html 引入依赖 <dependency> < ...

  3. 协方差及matlib绘制

    转自http://www.cnblogs.com/chaosimple/p/3182157.html 一.统计学的基本概念 统计学里最基本的概念就是样本的均值.方差.标准差.首先,我们给定一个含有n个 ...

  4. scikit-learn 应用

    首先是sklearn的官网:http://scikit-learn.org/stable/ 在官网网址上可以看到很多的demo,下边这张是一张非常有用的流程图,在这个流程图中,可以根据数据集的特征,选 ...

  5. Nginx软件模块说明

    Nginx软件模块说明 Nginx常用模块 注:以下只是列举Nginx常用模块,需要详细了解更多模块可以登录Nginx官方网站查看 功能模块 模块说明 ngx_http_core_module 包含一 ...

  6. nodejs . module.exports

    //utils.js let a = 100; console.log(module.exports); //能打印出结果为:{} console.log(exports); //能打印出结果为:{} ...

  7. Git 获取项目git clone

    git clone 克隆项目 git clone 实际上是一个封装了其他几个命令的命令. 它创建了一个新目录,切换到新的目录,然后 git init 来初始化一个空的 Git 仓库, 然后为你指定的 ...

  8. php 三种文件下载的实现

    第一种:直接添加文件下载的绝对路径连接 //如:我有一个文件在demo.xx.cn/demo.zip <button> <a href = "http://demo.xx. ...

  9. [JZOJ3400] 【GDOI2014模拟】旅行

    题目 题目大意 给你一个图,让你选择权值和最小的边,使得\(1\)和\(n\),\(2\)和\(n-1\),--,\(K\)和\(n-K+1\)联通. \(K\leq 4\) 思考历程 一看到这题就觉 ...

  10. [JZOJ3235] 数字八

    题目 题目大意 给你一个二维的图,其中.代表完好,*代表有缺陷. 现在要在图上刻一个数字\(8\),满足: 由两个矩形组成. 每个矩形中必须有空隙在内部,也就是说,至少为\(3*3\)的矩形. 上矩形 ...