本文为最好用的免费ERP系统Odoo 12开发手册系列文章第十三篇。
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开发之部署和维护生产实例