全书完整目录请见:Odoo 12开发者指南(Cookbook)第三版
本章中,我们将讲解如下小节:
- 创建自定义组件
- 使用客户端QWeb模板
- 向服务端做RPC调用
- 新建一个视图
- 调试客户端代码
- 通过导览提升客户引导
- 移动应用JavaScript
引言
Odoo的网页客户端或后台,是公司成员花费最多时间的地方。在第十章 后端视图中,我们学习了如何使用后台中所存在的可能性。这里我们将学习如何继承和自定义这些可能性。web模块包含有关Odoo用户界面的所有内容。
本章中的所有代码都依赖于web模块。你已经知道Odoo有两个版本(企业版和社区版)。社区版的用户界面使用web模块,而企业版使用一个继承社区版web模块的版本,即web_enterprise模块。
企业版在社区版web模块的基础上提供了一些其它功能,如移动端兼容、可搜索菜单、材料设计等等。这里我们使用社区版。但不必担心,社区版中所开发的模块与企业版可以完美兼容,因为,在内部web_enterprise依赖于社区版的web模块,只是对其添加了一些功能。
本章中,我们将学习如何创建新字段组件来从用户获取输入。我们还将从零开始新建视图。在阅读完本章后,你将能够在Odoo后台中创建自己的UI元素。
ℹ️注意:Odoo 的用户界面重度依赖于JavaScript。在本章中我们会假定你已具备JavaScript, jQuery, Underscore.js和SCSS的基础知识。
技术准备
本章的技术要求有在线Odoo平台。
本章中的所有代码可通过GitHub仓库进行下载:https://github.com/PacktPublishing/Odoo-12-Development-Cookbook-Third-Edition/tree/master/Chapter16。
观看如下视频来查看实时代码操作:
创建自定义组件
正如在第十章 后端视图中所看到的,我们可以使用组件来以不同格式展示某些数据。例如,我们使用了widget=’image’将二进制字段展示为图片。要演示如何创建你自己的组件,我们将编写一个允许用选择整型字段的组件,但还会进行不同的展示。使用的不是输入框,而是展示为颜色拾取器,这样我们我们可以选择颜色数值。
准备工作
本节中我们将使用第四章 创建Odoo插件模块中所创建的my_library模块。本节中我们会新建一个依赖于web模块的字段组件。确保在声明文件中添加了对web的依赖,如下所示:
1 2 3 |
... 'depends': ['base', 'web'], ... |
如何实现…
我们将添加包含组件逻辑的JavaScript文件,以及负责样式的SCSS文件。然后,我们将使用新的组件在图书表单中添加一个整型字段。按照如下步骤来新增一个字段组件:
- 添加一个static/src/js/field_widget.js让你说的。此处所使用的语法可参见第十五章 CMS网站开发中为网站扩展CSS和JavaScript一节:
1234odoo.define('my_field_widget', function (require) {"use strict";var AbstractField = require('web.AbstractField');var fieldRegistry = require('web.field_registry'); - 通过继承AbstractField来创建组件:
1var colorField = AbstractField.extend({ - 设置CSS类、根元素标签以及组件所支持的字段类型:
123className: 'o_int_colorpicker',tagName: 'span',supportedFieldTypes: ['integer'], - 捕获一些JavaScript事件:
123events: {'click .o_color_pill': 'clickPill',}, - 重载init来进行初始化:
1234init: function () {this.totalColors = 10;this._super.apply(this, arguments);}, - 重载_renderEdit和_renderReadonly来设置DOM元素:
12345678910111213141516171819_renderEdit: function () {this.$el.empty();for (var i = 0; i < this.totalColors; i++ ) {var className = "o_color_pill o_color_" + i;if (this.value === i ) {className += ' active';}this.$el.append($('<span>', {'class': className,'data-val': i,}));}},_renderReadonly: function () {var className = "o_color_pill active readonly o_color_" + this.value;this.$el.append($('<span>', {'class': className,}));}, - 定义我们在前面所引用的handler:
123456clickPill: function (ev) {var $target = $(ev.currentTarget);var data = $target.data();this._setValue(data.val.toString());}}); // closing AbstractField - 别忘了注册你的组件:
1fieldRegistry.add('int_color', colorField); - 让其在其它插件中可用:
1234return {colorField: colorField,};}); // closing 'my_field_widget' namespace - 在static/src/scss/field_widget.scss中添加一些SCSS:
123456789101112131415161718192021222324252627282930.o_int_colorpicker {.o_color_pill {display: inline-block;height: 25px;width: 25px;margin: 4px;border-radius: 25px;position: relative;@for $size from 1 through length($o-colors) {&.o_color_#{$size - 1} {background-color: nth($o-colors, $size);&:not(.readonly):hover {transform: scale(1.2);transition: 0.3s;cursor: pointer;}&.active:after{content: "\f00c";display: inline-block;font: normal normal normal 14px/1 FontAwesome;font-size: inherit;color: #fff;position: absolute;padding: 4px;font-size: 16px;}}}}} - 在views/templates.xml后台资源中注册这两个文件:
123456789<?xml version="1.0" encoding="UTF-8"?><odoo><template id="assets_end" inherit_id="web.assets_backend"><xpath expr="." position="inside"><script src="/my_field_widget/static/src/js/field_widget.js" type="text/javascript" /><link href="/my_field_widget/static/src/scss/field_widget.scss" rel="stylesheet" type="text/scss" /></xpath></template></odoo> - 最后在library.book模型中添加颜色整型字段:
1color = fields.Integer() - 在图书表单中添加颜色字段,同时添加widget=”int_color”:
123456...<group><field name="date_release"/><field name="color" widget="int_color"/></group>...
更新模块来应用修改。在更新完成后,打开图书表单视图,就可以看到下图中所示的颜色拾取器:
实现原理…
为便于读者掌握示例,我们来通过查看其组件来看一下组件的生命周期:
- init():这是组件构造函数。用于进行初始化。在初始化组件时,会先调用该方法。
- willStart():这个方法组件初始化以及在DOM中添加的过程中调用。它用于异步将数据初始化到组件中。它还会返回一个延迟对象,只需要通过super()调用即可获取。我们在后面的小节中将会使用到它。
- start():该方法在完成组件渲染且未添加到DOM中时调用。这非常有助于后渲染任务,将返回一个延迟对象。可以在this.$el中访问已渲染的对象。
- destroy():该方法在消灭组件时调用。它多用于基本的清理操作,如取消事件绑定。
小贴士:组件的基本 base 类是Widget(在web.Widget中定义)。如果想要做更深入的探讨,可以通过/addons/web/static/src/js/core/widget.js进行研究。
在第1步中,我们导入了AbstractField和fieldRegistry。
在第2步中,我们通过继承AbstractField创建了colorField。这样我们的colorField就会获取到AbstractField所有属性和方法。
第3步中,我们添加了3个属性 – className用于定义组件根元素的类,tagName用于根元素的类型,supportedFieldTypes用于决定该组件所支持的字段类型。在本例中,我们希望创建一个针对整型字段的组件。
第4步中,我们对组件的事件进行了映射。通常键为事件名和可选CSS选择器的组合。事件和CSS选择器由空格分割,值为组件方法的名称。因此,在执行事件时,指定的方法会自动调用。在本节中,当用户点击颜色块时,我们需要在字段中设置一个整型值。为管理点击事件,我们在事件的键中添加了一个CSS选择器和方法。
第5步中,我们重载了init方法并设置了totalColors属性的值。我们将使用该变量来决定颜色块的值。这里需要显示10个颜色块,因此将值设为了10.
第6步中,我们添加了两个方法 – _renderEdit和_renderReadonly。根据名称可以知道,_renderEdit在组件处于编辑模式时调用,而_renderReadonly在组件处于只读模式时调用。在编辑方法中,我们添加了一些<span>标签,每个都在组件中表示一种颜色,并会在表单视图中添加。在点击<span>标签时,会在字段中设置值。我们将它们添加到了 this.$el中。这里的$el是组件的根元素,它会在表单视图中进行添加。在只读模式下,我们仅需展示激活的颜色,因此通过_renderReadonly()方法添加了单个颜色块。就目前而言,我们以硬编码的方式添加了色块,但在下一节中,我们将使用JavaScript Qweb模板来渲染这些色块。注意在编辑模式下,我们使用了totalColors属性,它通过init()进行的设置。
第7步中,我们添加了clickPill处理方法来管理色块的点击。使用了_setValue方法来设置字段值。该方法通过AbstractField类添加。在设置字段值时,Odoo框架会重新渲染组件并再次调用_renderEdit方法,这样就可以通过更新后的值来渲染组件了。
第8步中,在定义了新组件之后,通过web.field_registry中的表单注册表来注册它非常关键。注意所有的视图类型都查找这个注册表,因此如果希望在列表视图中创建另一种展现字段的形式,也需要在这里添加组件并在视图定义中对该字段设置组件属性。
最后,我们导出了组件类,这样其它的插件可以对其进行扩展和继承。然后,我们在library.book模型中添加了一个新的名为color的整型字段。我们还通过widget=”int_color”属性在表单视图中添加了相同的字段。这会在表单视图中展示我们的组件,用于代替默认的整型组件。
扩展知识…
web.mixins命名空间定义了几个mixin帮助类,在开发表单组件时就记得使用。AbstractField通过继承Widget类来创建,而Widget继承了两个mixin。第一个为EventDispatcherMixin,它提供添加并触发事件handler的简单接口。第二个是ServicesMixin,提供RPC调用和动作的函数。
ℹ️在想要重载一个方法,应保持先研究其基类来了解它所返回的函数。很常见的一个错误是忘记返回超级用户的延迟对象,会导致异常操作时的问题。
组件需要负责验证。使用isValid函数来自定义自己的验证。
使用客户端QWeb模板
就像在JavaScript中创建HTML代码是一种不好的编程习惯一样,我们应在客户端JavaScript代码尽量少的创建DOM元素。幸好在客户端也有一个模板引擎,更幸运的是,客户端的模板引擎和服务端模板语法相同。
准备工作
本节中,我们将使用前一小节中的my_library模块。我们将通过把DOM元素创建移到QWeb来让其更加的模块化。
如何实现…
我们需要在声明文件中添加QWeb定义并修改JavaScript代码来进行使用。按照如下步骤来进行操作:
- 导入web.core并将对qweb的引用提取为一个变量,如以下代码所示:
123456789odoo.define('my_field_widget', function (require) {"use strict";var AbstractField = require('web.AbstractField');var fieldRegistry = require('web.field_registry');var core = require('web.core');var qweb = core.qweb;... - 修改_renderEdit函数来仅渲(继承自组件的)该元素:
12345_renderEdit: function () {this.$el.empty();var pills = qweb.render('FieldColorPills', {widget: this});this.$el.append(pills);}, - 向static/src/xml/qweb_template.xml中添加模板文件:
12345678910<?xml version="1.0" encoding="UTF-8"?><templates><t t-name="FieldColorPills"><t t-foreach="widget.totalColors" t-as='pill_no'><span t-attf-class="o_color_pill o_color_#{pill_no}#{widget.value === pill_no and 'active' or ''}"t-att-data-val="pill_no"/></t></t></templates> - 在声明文件中注册QWeb文件:
123"qweb": ['static/src/xml/qweb_template.xml',],
现在,通过其它插件,我们修改组件所使用的HTML代码更为容易了,因为它们可以通过QWeb模式来进行重写。
实现原理…
因为在第十五章 CMS网站开发中创建或更改模板 – QWeb一节已有对QWeb基础知识非常全面的讨论,这里会集中讲解不同的地方。首先,需要注意到我们处理的是JavaScript QWeb的实现,而不是服务端中的Python实现。就是说我们没有读取记录或环境的权限,仅能访问qweb.render函数所传递的参数。
在本例中,我们通过组件的键传递了当前对象。这表示应将所有的逻辑放在组件的JavaScript代码中,只让模板访问属性或一种可能用到的函数。既然我们能访问所有组件中可用的属性,可以通过检查totalColors属性来在对模板的值进行检测。
客户端QWeb和QWeb视图间并没有关系,所以存在不同的机制来让网页客户端识别模板 – 通过qweb键以相对插件根路径的文件名列表将它们添加到插件的声明中。
扩展知识…
这里使用QWeb的原因是其可扩展性,它是客户端与服务端QWeb的另一个很大的区别。在客户端无法使用XPath表达式,需要使用jQuery选择器和操作。例如我们想要在另一个模块的组件中添加用户图标,需要在每个色块中添加如下代码来获取图标:
1 2 3 4 5 |
<t t-extend=“FieldColorPills”> <t t-jquery=“span” t-operation=“prepend”> <i class=“fa fa-user” /> </t> </t> |
如果在这里同时给定一个t-name 属性的话,会产生原模板的一个副本并不会动到原模板。 t-operation属性的其它可用值有append, before, after, inner和replace,会让t元素的内容要么通过append将其添加到匹配内容之后,要么通过before或after将其放到匹配元素之前或之后,要么通过inner将其替换所配置的元素内容,又或者通过replace替换掉整个元素。还有t-operation=’attributes’,它让我们可以在匹配的元素上设置属性,规则 与服务端QWeb相同。‘
另一个不同是客户端QWeb中的名称不由模块名建立命名空间,因此需要选择模板的名称来让其在所安装的所有插件模块中保持唯一,这也是开发者常常使用很长名称的原因。
其它内容
如果想要学习Qweb模板的更多内容,可参照如下各点:
- 客户端QWeb引擎的氏误消息和处理较Odoo其它部分来得更为不方便。小错误常常并不代表有任何问题,让初学者很难往下使用。
- 幸而,本章稍后的调试客户端代码一节中会讲解客户端QWeb模板的一些调试语句。
向服务端做RPC调用
迟早你的组件会需要从服务端查找一些数据。本节中,我们将为颜色块添加一条提示信息。在用户将光标悬停在颜色块元素上时,提示框中会显示与该颜色相关的图书的数量。我们将会对服务端做一个RPC调用来获取指定颜色关联数据的图书数量。
准备工作
本节中,我们将使用上一节中的my_library模块。
如何实现…
执行如下步骤来对服务端做RPC调用并在提示框中显示结果:
- 添加willStart方法并在RPC调用中设置colorGroupData:
12345678910111213141516willStart: function () {var self = this;this.colorGroupData = {};var colorDataDef = this._rpc({model: this.model,method: 'read_group',domain: [],fields: ['color'],groupBy: ['color'],}).then(function (result) {_.each(result, function (r) {self.colorGroupData[r.color] = r.color_count;});});return $.when(this._super.apply(this, arguments), colorDataDef);}, - 更新_renderEdit并对颜色块设置初始提示框:
123456_renderEdit: function () {this.$el.empty();var pills = qweb.render('FieldColorPills', {widget: this});this.$el.append(pills);this.$el.find('[data-toggle="tooltip"]').tooltip();}, - 更新FieldColorPills模板并添加提示数据:
123456789101112<t t-name="FieldColorPills"><t t-foreach="widget.totalColors" t-as='pill_no'><span t-attf-class="o_color_pill o_color_#{pill_no}#{widget.value === pill_no and 'active' or ''}"t-att-data-val="pill_no"data-toggle="tooltip"data-placement="top"t-attf-title="This color is used in#{widget.colorGroupData[pill_no] or 0 } books."/></t></t>
更新模块来应用修改。在更新后,就可以在色块上看到如下图所示的提示信息:
实现原理…
willStart函数在渲染之前调用,更重要的是,它返回一个必须在渲染开始之前解析的延迟对象。因此,在像我们这样的用例中,因需要在渲染发生之前运行异步动作,这是应使用的正确函数。
在处理数据访问时,我们依赖于前面讲解的ServicesMixin类所提供的_rpc函数。该函数允许我们调用模型上的任意公有函数,如search, read, write或本例中的read_group。
第1步中,我们做了一个RPC调用并在当前模型本例中即library.book上调用了read_group方法。我们根据color字段进行了分组,因此RPC调用会返回由color分组的图书数据并在color_count键中添加了累加。我们还在colorGroupData中建立了color_count与color索引的映射,这样就可以在QWeb模板中使用了。在该函数的最后一行中,我们通过super来解析了willStart以使用$.when来进行了RPC调用。因此,渲染仅在获取值之后及super所执行的任意异步动作完成之后发生。
第2步并没有什么特别的内容。我们只是初始化了起始提示框。
第3步中我们使用了colorGroupData设置所需的属性来显示提示框。在willStart方法中,我们通过this.colorGroupData指定了color映射 ,因此通过widget.colorGroupData.在QWeb模板中访问它们。这是因此我们传递了组件引用,这就是qweb.render方法。
小贴士:可以在组件中的任意地方调用_rpc。注意这是一个异步调用,我们需要正确地管理延时对象来获取所要的结果。
扩展知识…
AbstractField类带有两个有趣的属性,其中一个我们刚刚使用过。在我们的示例中,我们使用了this.model属性,它存储了当前模型的名称(如library.book)。另一个属性是this.field,大概包含组件正在显示字段的模型的fields_get()函数的输出。它会给出所有与当前字段相关联的信息。例如,对于x2x字段,fields_get()函数会给出co-model或域的信息。也可以使用它来查询字段的string、size或其可在模型定义时为字段所设置的属性。
另一个很有帮助的属性是nodeOptions,它包含通过<form>视图定义的options属性所传递的数据。它已经过JSON解析,因此可以像其它对象一样访问它。了解更多有关这些属性的知识,可以进一步挖掘abstract_field.js文件。
其它内容
如果在管理异步运算时出现问题可参照如下文档:
- Odoo的RPC依赖于jQuery对象,因此它是一个异步函数。应当学习延时对象来彻底掌握JavaScript中的RPC调用。可以在jQuery的文档中学习更多有关延时对象的知识:https://api.jquery.com/jQuery.Deferred
新建一个视图
我们在第十章 后端视图中学习过有不同类型的视图,如form, list, kanban等。本节中,我们将创建一个全新的视图。这个视图会显示作者列表及他们所著的图书。
准备工作
本节中我们将使用前一小节的my_library模块。注意视图是非常复杂的结构,并且每种已有视图都有不同的目的和实现。本节旨在让读者了解MVC模式的视图及如何创建简单视图。本节中,我们将创建一个名为m2m_group的视图,目的是在组中展示记录。将记录分成不同组,视图将使用many2many字段数据。在my_library模块中,我们有author_ids字段。这里,我们将根据作者分组图书并在卡片中进行显示。
此外,我们将向控制面板添加一个新按钮。借助这个按钮,我们将能够添加一条图书的新记录。我们还将在作者的卡片中添加一个按钮,这样可以将用户重定向到另一个视图中。
如何实现…
按照如下步骤来添加一个名为m2m_group新视图:
- 在ir.ui.view中添加一个新视图类型:
123class View(models.Model):_inherit = 'ir.ui.view'type = fields.Selection(selection_add=[('m2m_group', 'M2m Group')]) - 在ir.actions.act_window.view中新增一个视图模式:
123class ActWindowView(models.Model):_inherit = 'ir.actions.act_window.view'view_mode = fields.Selection(selection_add=[('m2m_group', 'M2m group')]) - 通过从base模型继承来新增方法。该方法会通过JavaScript模型调用(参见第4步获取详情):
1234567891011121314151617181920class Base(models.AbstractModel):_inherit = 'base'@api.modeldef get_m2m_group_data(self, domain, m2m_field):records = self.search(domain)result_dict = {}for record in records:for m2m_record in record[m2m_field]:if m2m_record.id not in result_dict:result_dict[m2m_record.id] = {'name': m2m_record.display_name,'children': [],'model': m2m_record._name}result_dict[m2m_record.id]['children'].append({'name': record.display_name,'id': record.id,})return result_dict - 新增名为/static/src/js/m2m_group_model.js的文件,并添加如下内容:
123456789101112131415161718192021222324252627282930313233343536373839odoo.define('m2m_group.Model', function (require) {'use strict';var AbstractModel = require('web.AbstractModel');var M2mGroupModel = AbstractModel.extend({get: function () {return this.data;},load: function (params) {this.modelName = params.modelName;this.domain = params.domain;this.m2m_field = params.m2m_field;return this._fetchData();},reload: function (handle, params) {if ('domain' in params) {this.domain = params.domain;}return this._fetchData();},_fetchData: function () {var self = this;return this._rpc({model: this.modelName,method: 'get_m2m_group_data',kwargs: {domain: this.domain,m2m_field: this.m2m_field}}).then(function (result) {self.data = result;});},});return M2mGroupModel;}); - 新增一个名为/static/src/js/m2m_group_controller.js的文件,并添加如下内容:
123456789101112131415161718192021222324252627282930313233343536373839404142434445odoo.define('m2m_group.Controller', function (require) {'use strict';var AbstractController = require('web.AbstractController');var core = require('web.core');var qweb = core.qweb;var M2mGroupController = AbstractController.extend({custom_events: _.extend({},AbstractController.prototype.custom_events, {'btn_clicked': '_onBtnClicked',}),renderButtons: function ($node) {if ($node) {this.$buttons =$(qweb.render('ViewM2mGroup.buttons'));this.$buttons.appendTo($node);this.$buttons.on('click', 'button',this._onAddButtonClick.bind(this));}},_onBtnClicked: function (ev) {this.do_action({type: 'ir.actions.act_window',name: this.title,res_model: this.modelName,views: [[false, 'list'], [false, 'form']],domain: ev.data.domain,});},_onAddButtonClick: function (ev) {this.do_action({type: 'ir.actions.act_window',name: this.title,res_model: this.modelName,views: [[false, 'form']],target: 'new'});},});return M2mGroupController;}); - 新增名为/static/src/js/m2m_group_renderer.js的文件,并添加如下内容:
1234567891011121314151617181920212223242526272829303132333435363738odoo.define('m2m_group.Renderer', function (require) {'use strict';var AbstractRenderer = require('web.AbstractRenderer');var core = require('web.core');var qweb = core.qweb;var M2mGroupRenderer = AbstractRenderer.extend({events: _.extend({},AbstractRenderer.prototype.events, {'click .o_primay_button': '_onClickButton',}),_render: function () {var self = this;this.$el.empty();this.$el.append(qweb.render('ViewM2mGroup', {'groups': this.state,}));return this._super.apply(this, arguments);},_onClickButton: function (ev) {ev.preventDefault();var target = $(ev.currentTarget);var group_id = target.data('group');var children_ids =_.map(this.state[group_id].children, function (group_id) {return group_id.id;});this.trigger_up('btn_clicked', {'domain': [['id', 'in', children_ids]]});}});return M2mGroupRenderer;}); - 新增一个名为/static/src/js/m2m_group_view.js的文件,并添加如下内容:
12345678910111213141516171819202122232425262728293031323334353637odoo.define('m2m_group.View', function (require) {'use strict';var AbstractView = require('web.AbstractView');var view_registry = require('web.view_registry');var M2mGroupController = require('m2m_group.Controller');var M2mGroupModel = require('m2m_group.Model');var M2mGroupRenderer = require('m2m_group.Renderer');var M2mGroupView = AbstractView.extend({display_name: 'Author',icon: 'fa-id-card-o',config: {Model: M2mGroupModel,Controller: M2mGroupController,Renderer: M2mGroupRenderer,},viewType: 'm2m_group',groupable: false,init: function (viewInfo, params) {this._super.apply(this, arguments);var attrs = this.arch.attrs;if (!attrs.m2m_field) {throw new Error('M2m view has not defined"m2m_field" attribute.');}// Model Parametersthis.loadParams.m2m_field = attrs.m2m_field;},});view_registry.add('m2m_group', M2mGroupView);return M2mGroupView;}); - 在/static/src/xml/qweb_template.xml文件中为视图添加QWeb模板:
1234567891011121314151617181920212223242526<t t-name="ViewM2mGroup"><div class="row ml16 mr16"><div t-foreach="groups" t-as="group" class="col-3"><t t-set="group_data" t-value="groups[group]" /><div class="card mt16"><img class="card-img-top" t-attfsrc="/web/image/#{group_data.model}/#{group}/image"/><div class="card-body"><h5 class="card-title mt8"><t tesc="group_data['name']"/></h5></div><ul class="list-group list-group-flush"><t t-foreach="group_data['children']" tas="child"><li class="list-group-item"><i class="fa fa-book"/> <t t-esc="child.name"/></li></t></ul><div class="card-body"><a href="#" class="btn btn-sm btn-primary o_primay_button" t-att-data-group="group">View books</a></div></div></div></div></t><div t-name="ViewM2mGroup.buttons"><button type="button" class="btn btn-primary">Add Record</button></div> - 向后台资源中添加了所有的JavaScript文件:
123456...<script type="text/javascript" src="/my_library/static/src/js/m2m_group_view.js" /><script type="text/javascript" src="/my_library/static/src/js/m2m_group_model.js" /><script type="text/javascript" src="/my_library/static/src/js/m2m_group_controller.js" /><script type="text/javascript" src="/my_library/static/src/js/m2m_group_renderer.js" />... - 最后,为library.book模型添加我们的新视图:
12345678<record id="library_book_view_author" model="ir.ui.view"><field name="name">Library Book Author</field><field name="model">library.book</field><field name="arch" type="xml"><m2m_group m2m_field="author_ids" color_field="color"></m2m_group></field></record> - 在图书动作中添加m2m_group:
123...<field name="view_mode">tree,m2m_group,form</field>...
更新my_library模块来打开图书视图,然后,通过视图切换图标,打开我们刚刚添加的的新视图。会长成下面这样:
ℹ️Odoo视图非常易于使用也非常灵活。但是,通常容易和灵活的事物背后都是复杂的实现。对于Odoo的JavaScript视图正是如此:很容易使用,但实现很复杂。它包含大量的组成部分,如模型、渲染器、控制器、视图、QWeb模板等等。在接下来的部分中,我们添加了所有视图所需的构件并使用了library.book模型中的新视图。如果不想要手动做所有的工作,可从本书的GitHub仓库中获取模块示例文件。
实现原理…
在第1和第2步中,我们在 ir.ui.view和ir.actions.act_window.view中注册一个新的视图类型,名为m2m_group。
在第3步中,我们在base中添加了get_m2m_group_data方法。在base中添加该方法会让其在所有模型中都可以使用。这个方法会在JavaScript视图中通过RPC调用。该视图会传递两个参数 – domain和m2m_field。在domain参数中,domain值会是由搜索视图域和动作域拼接所生成的域。m2m_field是我们希望对记录分组的字段名。这个字段在视图定义中进行设置。
在接下来的几步中,我们添加了要求构成视图的JavaScript文件。Odoo的JavaScript视图由视图、模型、渲染器和控制器组成。视图在Odoo的代码中有一些历史含义,因此在Odoo中模型、视图、控制器(MVC)变成了模型、渲染器和控制器(MRC)。总的来说,视图设置模型、视图和控制器,并设置MVC等级来变成下面这样:
我们来看一下模型、渲染器、控制器和视图的角色。模型、渲染器、控制器和视图的抽象版本有构成视图所需的所有基础部分。因此,在我们的示例中通过继承它们创建了模型、渲染器、控制器和视图。
这里有一些用于创建视图的不同部分的深入讲解:
- Model:模型的角色是放置视图状态。向针对数据向服务器发送RPC请求,然后向控制器和渲染器传递数据。然后我们重载load和reload方法。在视图初始化时,它调用load()方法来获取数据,在搜索条件修改了或视图需要新状态时,会调用reload()方法。在本例中,我们创建了common _fetchData()方法来做RPC调用获取数据。注意我们使用了在第3步中添加的get_m2m_group_data方法。会从控制器调用get()方法来获取模式的状态。
- Controller:控制器的角色是管理模型和渲染器之间的调度、在渲染器中发生动作时,它传递该信息到控制器中并相应的执行动作。有时,它还会在模型中调用一些方法。此外,它管理控制面板中的按钮。在本例中,我们添加了一个新增记录的按钮。为进行实现,我们重载了AbstractController的renderButtons() 方法。我们还注册了custom_events,这样在点击了作者卡片中的按钮时,渲染器会控制器的事件来让其执行动作。
- Renderer:渲染器的角色是管理视图的DOM元素。每个视图可以按不同的方式渲染数据。在渲染器中,我们可以在变量state中获取模型的状态。它调用进行渲染的render() 方法。在本例中,我们通过显示视图的当前状态渲染了ViewM2mGroup QWeb模板。我们还将JavaScript的事件映射到了用户的动作。本节中我们还绑定了卡片中按钮的点击事件。在点击作者卡片按钮时,它会触发btn_clicked事件给控制器,并且会打开该作者是的图书列表。
ℹ️注意events和custom_events是不同的。事件是常规的JavaScript事件,而custom_events事件来自Odoo的JavaScript框架。自定义事件可以由trigger_up方法调用。
- View:视图的角色是获取所有要求构建视图的基本内容,如字段、上下文、视图结构和一些其它参数的集合。然后,视图会初始化控制器、渲染器和模型三剑客。它会在MVC等级中进行设置。通常,它设置模型、视图和控制器所需要的参数。在本例中,我们需要m2m_field名称来获取模型中适当的分组数据,因此在其中设置了模型参数。以同样的方式,this.controllerParams和this.rendererParams可用于设置控制器和渲染器中的参数。
在第8步中,我们添加了视图和控制面板按钮的QWeb模板。要了解有关QWeb模板更多的知识,请参见本章中的使用客户端QWeb模板一节。
小贴士:Odoo视图有大量的不同目的,我们在本节中涉及了一些最重要的部分。如果想要更深入地学习视图,可以进入/addons/web/static/src/js/views/目录做更进一步的探索。该目录还包含抽象模型、控制器、渲染器和视图的代码。
第9步中,我们在资源中添加JavaScript文件。
在最后的两步中,我们添加了针对book.library模型的视图定义。在第10步中,我们对视图使用了<m2m_group>标签,并且我们传递了m2m_field属性来作为一个选项。它会传递给模型来从服务端获取数据。
扩展知识…
如果你不想要引入新视图类型,而只是想要在视图中修改一些内容,可以在视图中使用js_class。例如,如果我们想要类似我们创建的看板的社图,可以像下面这样进行继承:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var CustomRenderer = KanbanRenderer.extend({ ... }); var CustomRendererModel = KanbanModel.extend({ ... }); var CustomRendererController = KanbanController.extend({ ... }); var CustomDashboardView = KanbanView.extend({ config: _.extend({}, KanbanView.prototype.config, { Model: CustomDashboardModel, Renderer: CustomDashboardRenderer, Controller: CustomDashboardController, }), }); var viewRegistry = require('web.view_registry'); viewRegistry.add('my_custom_view', CustomDashboardView); |
然后我们可以通过js_class使用看板视图(注意服务端仍会将其看作一种看板视图):
1 2 3 4 5 6 7 |
... <field name="arch" type="xml"> <kanban js_class="my_custom_view"> ... </kanban> </field> ... |
调试客户端代码
关于调试服务端的代码,本书中有单独的一整章第八章 调试进行讲解。而对于客户端,本节会做一些初步讲解。
准备工作
本节并不要求依赖于具体的代码,但如果你希望能够重现其中的内容,请使用前一小节中的代码。
如何实现…
客户端脚本试试困难的原因在于网页客户端重度依赖于jQuery的异步事件。既然断点会中断执行,在调试时由定时问题会引起的错误很大机率不会发生。稍后我们会讨论一些策略。
- 对于客户端调试,我们需要激活带资源的调试模式。如果不知道如何启用带资源的调试模式,请参见第一章 安装Odoo开发环境中的激活Odoo开发者工具一节。
- 在需要调试的JavaScript函数中调用调试器:
1debugger; - 如果存在定时问题,通过JavaScript函数登录控制台:
1console.log(“I'm in function X currently”); - 如果角希望在模板渲染时进行调试,在QWeb中调用调试器:
1<t t-debug=““ /> - 也可以让QWeb登录到控制台中,如下所示:
1<t t-log=“myvalue” />
所有这些依赖于浏览器为调试提供适当的功能。而所有主流浏览器都有相关支持,这里我们仅使用Chromium来作为演示。要使用调试工具,请点击右上角菜单按钮并选择More tools > Developer tools:
实现原理…
打开调试器时,应该会看到类似下图的内容:
这里我们在不同的标签页上访问了大量不同的工具。上书中当前开启的标签为JavaScript调试器,我们通过点击行号在第31行设置了断点。每次我们的组件获取到用户列表时,执行会停止在这一行,调试器让我们可以查看变量或修改它们的值。在右侧的查看列表中,我们还可以调用函数来尝试它们的效果而无需持续保存脚本文件并重载页面。
我们前面所提到的debugger语句在打开开发工具时会进行同样的中断。执行会停止,而浏览器会切换到Sources标签,打开所调试的文件并在有debugger的那一行高亮显示。
前面可能会打印日志的两处会出现在Console标签中。在遇到问题时这应该是第一个要去查看的标签,因为如果JavaScript因语法错误而完全没有加载或是类似的基本错误,都会在这个标签中看到说明情况的错误信息。
扩展知识…
使用Elements标签来检查浏览器当前显示页面的DOM结构。在对于熟悉已有组件生成的HTML代码非常有帮助,并且它还让我们可以调整类和CSS属性。它对于测试布局的调整非常有帮助。
Network标签给出一个当前页面请求及时长的概览。这对于调试页面加载速度慢时很常有用,因为在Network标签中我们可以看到请求的详情。如果选择一个具体的请求,可以查看其传递给服务器的负载及返回的结果,有助于在客户端弄清楚预期外行为的原因。还会看到所做请求的状态码,如因拼错文件名而找不到资源时显示404,
通过导览提升客户引导
在开发大量应用之后,向终端用户说明软件流程非常关键。Odoo框架包含一个内置的导览管理器。通过它,我们可以让用户通过了解具体的流程来得到指导。本节中,我们将创建一个在图书馆中创建图书的引导。
准备工作
我们将使用前面小节的my_library模块。导览仅在没有演示的数据库中显示,因此如果你使用的是带有demo 数据的数据库,请为本节创建一个没有演示数据的数据库。
如何实现…
按照如下步骤来为图书馆添加引导:
- 新增一个/static/src/js/my_library_tour.js文件并添加如下代码:
123456789101112131415161718192021222324252627odoo.define('my_library.tour', function (require) {"use strict";var core = require('web.core');var tour = require('web_tour.tour');var _t = core._t;tour.register('library_tour', {url: "/web",}, [tour.STEPS.SHOW_APPS_MENU_ITEM, {trigger: '.o_app[data-menuxmlid="my_library.library_base_menu"]',content: _t('Manage books and authors in<b>Library app</b>.'),position: 'right'}, {trigger: '.o_list_button_add',content: _t("Let's create new book."),position: 'bottom'}, {trigger: 'input[name="name"]',extra_trigger: '.o_form_editable',content: _t('Set the book title'),position: 'right',}, {trigger: '.o_form_button_save',content: _t('Save this book record'),position: 'bottom',}]);}); - 在后台资源中添加导览JavaScript文件:
123...<script type="text/javascript" src="/my_library/static/src/js/my_library_tour.js" />...
更新该模块并打开Odoo后台。此时就会看到一个如下图所示的引导:
实现原理…
引导管理器在web_tour.tour命名空间下。
第1步中,我们导入了web_tour.tour。然后就可以通过register()函数新增导览了。我们通过library_tour名注册了导览并传递了需要运行导览的URL。
下一个参数是一个导览步骤的列表。每个导览步骤需要3个值。触发器用于选择导览显示在哪趟元素上。这是一个JavaScript选择器。我们使用了菜单的XML ID, 因为它存在于DOM中。
第一步tour.STEPS.SHOW_APPS_MENU_ITEM是为主菜单预定义的导览步骤。下一个键是content,在用户鼠标悬停在导览图标上时显示该内容。我们使用了_t()函数,因为希望翻译该字符串,而position键用于决定导览图标的位置。可以选择的值有top, right, left和bottom。
ℹ️引导会提升用于使用体验以及对集成测试的管理。在内部以测试模式运行Odoo时,它也会运行导览并在导览未能完成时使得测试用例失败。
移动应用JavaScript
Odoo 10引入了Odoo移动应用。它提供执行移动端动作的小工具,如震动手机、显示弹出消息、扫描二维码等等。
准备工作
我们将使用前一节的my_library模块。我们将通过移动应用在修改了颜色字段值时显示弹出消息。
ℹ️警告:Odoo移动应用仅支持企业版,因此如果你没有企业版,则无法进行测试。
如何实现…
按照如下步骤来在Odoo移动应用中显示弹出消息:
- 在field_widget.js中导入web_mobile.rpc:
1var mobile = require('web_mobile.rpc'); - 修改clickPill方法来在用户在移动设备上修改颜色时显示弹出消息:
12345678910clickPill: function (ev) {var $target = $(ev.currentTarget);var data = $target.data();if (mobile.methods.showToast) {mobile.methods.showToast({'message': 'Color changed'});}this._setValue(data.val.toString());}
升级模块并在移动应用中打开library.book模型的表单视图。在修改颜色时,会看到如下所示的弹出消息:
实现原理…
web_mobile.rpc提供了移动设备与Odoo JavaScript之间的桥梁。它暴露一些基本移动工具。本例中,我们使用了showToast方法来在移动应用中显示弹窗信息。我们还需要检查该函数的可用性。背后的原因是一些手机可能不支持某些功能,例如在设备没有摄像头时,就无法使用scanBarcode()方法。在这种情况下,为避免错误,我们需要将代码包裹在 if 条件中。
扩展知识…
Odoo中的移动工具有如下这些:
- showToast(): 显示提示消息
- vibrate(): 手机震动
- showSnackBar(): 显示带有按钮的滑动栏
- showNotification(): 显示手机提示消息
- addContact(): 在电话薄中新增联系人
- scanBarcode(): 扫描二维码
- switchAccount(): 在Android中打开账号切换器
学习更多有关移动端的JavaScript知识,请参见:https://www.odoo.com/documentation/12.0/reference/mobile.html。