【odoo14】【好书学习】第十五章、网站客户端开发

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

odoo的web客户端、后台是员工经常使用的地方。在第九章中,我们了解了如何使用后台提供的各种可能性。本章,我们将了解如何扩展这种可能性。其中web模块包含了我们在使用odoo中的各种交互行为。
本章将依赖于web模块。odoo有两个不同的版本(社区版、企业版)。社区版包含web模块,而企业版是对web的扩展模块web_enterprise模块。
企业版提供了定制的手机端自适应、可搜索的菜单及模块化设计。

重要提醒
与其他Odoo版本相比,odoo14对于后端web客户端来说有点独特。它包含两种管理odoo后台GUI的框架。第一个是传统基于小部件的框架,第二个是基于Odoo Web Library(OWL)的框架。OWL是odoo14的最新UI框架。两者都使用QWeb模板,但是在语法及运行原理方面有一些明显的调整。
尽管odoo14有新的框架OWL,但是odoo并没有广泛的使用。大多数网页客户端依旧使用老的框架。本章,我们将了解如何通过小部件的框架调整网页客户端。下一章节,我们将介绍OWL。

本章我们将创建一个用于获取用户输入的小部件。我们还将从头创建一个新视图。读完本章后,你将能够在Odoo后端创建自己的UI元素。

小贴士
odoo的用户交互依赖于JavaScript。本章中,我们假设你已经具备JavaScript、JQuery、Underscore.js和SCSS的基础知识。

本章主要内容如下:

  1. 创建自定义控件
  2. 使用客户端侧的QWeb模板
  3. 通过RPC调用后端python方法
  4. 创建新的视图
  5. 调试用客户端侧的代码
  6. 通过引导提升交互感
  7. 手机端js

创建自定义控件

正如您在第9章后端视图中看到的,我们可以使用小部件以不同的格式显示特定的数据。例如,我们使用widget='image'以图像的形式显示一个二进制字段。为了演示如何创建自己的小部件,我们将编写一个小部件,它允许用户选择一个整数字段,但我们将以不同的方式显示它。代替输入框,我们将显示一个颜色选择器,以便我们可以选择一个颜色号。在这里,每个数字将被映射到其相关的颜色。

准备

在本教程中,我们将使用my_library模块和基本字段和视图。

步骤

我们将添加一个JavaScript文件,其中包含小部件的逻辑,并添加一个SCSS文件来执行一些样式化操作。然后,我们将向books表单添加一个整数字段,以使用我们的新小部件。执行以下步骤添加一个新的字段小部件:

  1. 添加一个static/src/js/field_widget.js文件。关于这里使用的语法,请参考《CMS网站开发》第14章中扩展CSS和JavaScript的内容:
odoo.define('my_field_widget', function (require) {
    "use strict";
    var AbstractField = require('web.AbstractField');
    var fieldRegistry = require('web.field_registry');
	...
})
  1. 创建widget:
var colorField = AbstractField.extend({
  1. 设置CSS类、根元素以及支持的字段类型:
className: 'o_int_colorpicker', 
tagName: 'span', 
supportedFieldTypes: ['integer'],
  1. 配置js事件
events: {
	'click .o_color_pill': 'clickPill',
},
  1. 重载构造函数
init: function(){
	this.totalColors = 10;
	this._super.apply(this, arguments);
}
  1. 重载DOM元素的_renderEdit和_renderReadonly函数
_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,
	}));
},
  1. 添加处理函数
	clickPill: function(ev){
		var $target = $(ev.currentTarget);
		var data = $target.data();
		this._setValue(data.val.toString());
	}
}); // close AbstractField
  1. 注册widget:
fieldRegistry.add('int_color', colorField);
  1. 对其他组件可见:
return {
	colorField: colorField,
};
}; // close 'my_field_widget' Namespace
  1. 添加SCSS,static/src/scss/field_widget.scss:
.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 14px/1 FontAwesome;
                    font-size: inherit;
                    color: #fff;
                    position: absolute;
                    padding: 4px;
                    font-size: 16px;
                }
            }
        }
    }
}
  1. 注册js及scss文件
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
    <template id="assets_end" inherit_id="web.assets_ backend">
        <xpath expr="." position="inside">
            <script src="/my_library /static/src/js/field_widget.js" type="text/javascript" />
            <link href="/my_library/static/src/scss/field_widget.scss" rel="stylesheet" type="text/scss" />
        </xpath>
    </template>
</odoo>
  1. 添加color字段
color = fields.Integer()
  1. 在form视图添加color字段
<group>
    <field name="date_release"/>
    <field name="color" widget="int_color"/>
</group>

更新后如下图所示:

原理

让我们来了解下widget的生命周期:

  • init(): 这是widget的构造函数。是widget初始化时最先被调用的函数。
  • willStart(): 当widget初始化之后调用,被添加到DOM时调用。可用于异步初始化widget数据。它还应该返回一个延迟对象,可以简单地从super()调用获得该对象。我们将在后续菜谱中使用此方法。
  • start(): 在widget渲染完成但尚未添加到DOM时调用。它对于后期渲染工作非常有用,并且应该返回一个延迟的对象。你可以在this.$el中访问一个已渲染的元素。
  • destory(): 当widget被摧毁时调用。一般用于基础的清理工作,比如事件解绑。

重要信息
widget的基础类是Widget(web.Widget)。如果你想进一步了解该类,可在/addons/web/static/src/js/core/widget.js中查看。

步骤1,我们引入了AbstractField和fieldRegistry。
步骤2,我们创建AbstractField的扩展类colorField。通过该类,colorField将获得AbstractField的所有属性及方法。
步骤3,我们添加了三个属性: className用于定义根元素的类;tagName是根元素的标签;supportedFieldTypes代表当前widget可作用于哪些类型的field字段。在我们的例子中,我们创建了可用于整型字段的widget。
步骤4,我们映射了widget支持的事件。通过key是"事件的名称 CSS选择器",两者之间是空格。value是函数名。所以,当事件被触发的时候,函数将自动执行。本节,当用户点击了颜色圆点,将会在color字段设置所对应的整数值。
步骤5,我们重写了init方法,并设置了this.totalColors的值。通过该变量,决定展示的颜色圆点的个数。
步骤6,我们添加了_renderEdit和_renderReadonly函数。_renderEdit在编辑模式下调用,_renderReadonly在只读模式下调用。在编辑模式下,我们添加了代表不同颜色的标签。通过点击标签,我们可设置该字段的值,并将添加到this.$el中。$el是widget的根元素,将被添加到form视图。在只读模式下,我们仅展示当前字段代表的颜色。当前,我们通过硬编码的方式添加了颜色圆点,下一章,我们将通过JavaScript QWeb模板渲染小圆点。注意,我们再编辑模式下使用了在init()函数中设置的tottalColors属性值。
步骤7,我们添加了clickPill函数管理颜色圆点的点击事件。为了设置字段值,我们使用了_setValue方法。这个方法是AbstractField中的方法。当我们设置了字段值,odoo将渲染widget并再次调用_renderEdit方法。
步骤8,在定义完widget后我们需将其注册到web.field_registry。注意,所有视图的widget都会通过web.field_registry查找widget。所以如果你创建一个在list视图下展示字段的widget,也同样需要将其注册到web.field_registry中。
最后,我们将widget暴露出来,以便其他的模块也可以扩展或者继承。

更多

web.mixins命名空间定义了一组非常有用的类mixin。本章中我们已经使用过mixin了。AbstractField继承自Widget类,Widget继承自两个mixin。第一个是EventDispatcherMixin,可提供触发事件及捕获事件的接口。第二个是ServicesMixin,提供了RPC和动作所需的函数。

重要小贴士
当我们重载函数的时候,我们需要了解原始函数的返回值。一个常见的BUG是忘记返回父函数所需的对象,而引起报错。
Widgets可用于数据验证。通过isValid函数实现我们这方面的需求。

使用客户端侧的QWeb模板

正如以编程方式在JavaScript中创建HTML代码是一个坏习惯一样,您应该在客户端JavaScript代码中只创建最少数量的DOM元素。幸运的是,客户端也有模板引擎可用,更幸运的是,客户端模板引擎具有与服务器端模板相同的语法。

准备

我们将把DOM元素的创建移动到QWeb,使其更加模块化。

步骤

我们需要将QWeb定义添加到清单中,并更改JavaScript代码,以便我们可以使用它。请按照以下步骤启动:

  1. 导入web.core并提取qweb引用,如下代码所示:
odoo.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;
...
  1. 修改从widget继承的_renderEdit方法,渲染元素
_renderEdit: function () {
    this.$el.empty();
    var pills = qweb.render('FieldColorPills', {
        widget: this
    });
    this.$el.append(pills);
},
  1. 添加static/src/xml/qweb_template.xml:
<?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>
  1. 注册QWeb模板
"qweb": [ 'static/src/xml/qweb_template.xml',
],

原理

在CMS网站开发的第14章中,已经有了关于QWeb创建或修改模板基础的全面讨论,我们将在这里重点讨论它的不同之处。首先,在这我们处理的是JavaScript QWeb模板的实现,相对的是服务器侧python的实现。这就意味着,你不能访问数据集及上下文,你只可以访问传递给qweb.render函数的参数。
在我们的例子中,我们将当前对象赋值给widget。这就意味着你可以操作widget的JavaScript实现,并让模板访问相关属性及函数。由于我们可以访问小部件上可用的所有属性,我们可以通过检查totalColors属性来检查模板中的值。
由于客户端QWeb与QWeb视图无关,因此有一种不同的机制使web客户端知道这些模板,通过QWeb键将它们添加到相对于加载项根目录的文件名列表中的加载项清单中。

小贴士
如果不想在清单中列出QWeb模板,可以使用代码段上的xmlDependencies键来延迟加载模板。对于xmlDependencies,QWeb模板仅在小部件初始化时加载。

更多

在这里使用QWeb的原因是可扩展性,这是客户端和服务器端QWeb的第二大区别。在客户端,不能使用XPath表达式;需要使用jQuery选择器和操作。例如,如果我们想从另一个模块向小部件添加用户图标,我们将使用以下代码在每个小部件中添加一个图标:

<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元素中的内容追加目标元素内元素的最后、目标元素的前或后、替换目标元素的内容、替换目标元素及其内容。还有t-operation='attributes',可设置目标元素的属性值。
另一个不同之处是,客户端QWeb中的名称不是以模块名称命名的,因此您必须为模板选择名称,这些模板可能是安装的所有附加组件中唯一的,这就是为什么开发人员倾向于选择相当长的名称。

参考

如果您想了解有关QWeb模板的更多信息,请参阅以下几点:

  • 与Odoo的其他部分相比,客户端QWeb引擎的错误消息和处理不太方便。一个小错误可能并不会影响程序的运行,因此这也就加大了初学者发现问题的难度。
  • 幸运的是,odoo提供了一些客户端QWeb模板的调试模型。我们将在 “调试你的客户端代码”一节中学习。

通过RPC调用后端python方法

我们的widget需要从服务器查询数据。本节,我们将在颜色圆点上显示一个tooltip提醒。当我们鼠标悬停在小圆点上时,将展示那个颜色相关图书的数量。我们将通过RPC调用,获取特定颜色图书的数量。

准备

步骤

  1. 添加willStart函数并设置colorGroupData的值:
willStart: function(){
	var self = this;
	this.colorGroupData = {};
	var colorDataPromise = 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 Promise.all([this._super.apply(this, arguments), colorDataPromise]);
},
  1. 更新_renderEdit函数,并设置tooltip:
_renderEdit: function(){
	this.$el.empty();
	var pills = qweb.render('FieldColorPills', {widget: this});
	this.$el.append(pills);
	this.$el.find('[data-toggle="tooltip"]').tooltip();	
},
  1. 更新FieldColorPills模板并添加tooltip数据:
<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函数在渲染之前调用,并返回一个Promise对象,带对象需在渲染开始前生成。
我们依赖于ServiceMixin类的_rpc函数进行数据调用。该方法可实现调用模型的公开函数,如search、read、write等,在我们的例子中,我们使用了read_group函数。
步骤1,我们通过_rpc调用了read_group函数。我们以color分组获取每组颜色的数量。我们将color_count和color序号映射到colorGroupData中供QWeb模板使用。In the last line of the function, we resolved willStart with super and our RPC call using $.when. Because of this, rendering only occurs after the values are fetched and after any asynchronous action super that was busy earlier, has finished, too.
步骤2,初始化tooltip。
步骤3,我们通过colorGroupData设置tooltip的值。

小贴士
你可以在widget的任何地方调用_rpc函数。注意,这是一个异步调用函数。您需要正确地管理延迟对象,以获得所需的结果。

更多

The AbstractField class comes with a couple of interesting properties, one of which we just used. In our example, we used the this.model property, which holds the name of the current model (for example, library.book). Another property is this.field, which contains roughly the output of the model's fields_get() function for the field the widget is displaying. This will give all the information related to the current field. For example, for x2x fields, the fields_get() function gives you information about the co-model or the domain. You can also use this to query the field's string, size, or whatever other property you can set on the field during model definition.
Another helpful property is nodeOptions, which contains data passed via the options attribute in the

view definition. This is already JSON parsed, so you can
access it like any object. For more information on such properties, dig further into the abstract_field.js file.

参考

如果在管理异步操作方面存在问题,请参考以下文档:

  • Odoo's RPC returns JavaScript's native Promise object. You will get requested data once Promise is resolved. You can learn more about Promise here: https:// developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/ Global_Objects/Promise.

创建新的视图

本节,我们将创建一种新的视图。用于展示作者及其图书信息。

准备

本节,我们使用之前的my_library模块。因为视图有很复杂的结构,每个视图都有其实现的目标。本节的目标是让你理解MVC模型下的视图及如何创建简单的视图。我们将创建m2m_group的视图,目标是以组的形式展示记录。为了将记录分配到不同的组,视图使用的是many2many的数据。在my_library模块中,我们有author_ids字段。我们将以作者分组并展示图书。
此外,我们将在控制区新增一个按钮,可允许我们新增图书记录。同时,我们也会在作者的卡片上新增一个按钮,用于重定向到另一个视图。

步骤

  1. 添加新的视图类型
class View(models.Model):
	_inherit = 'ir.ui.view'

	type = fields.Selection(selection_add=[('m2m_group', 'M2m Group')])
  1. 新建视图模式
class ActWindowView(models.Model):
	_inherit = 'ir.actions.act_window.view'

	view_mode = fields.Selection(selection_add=[('m2m_group', 'M2m group')], ondelete={'m2m_group': 'cascade'})
  1. 继承base模型实现新的方法,该方法将从JavaScript调用
class Base(models.AbstractModel):
	_inherit = 'base'

	@api.model
	def 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
  1. 添加/static/src/js/m2m_group_model.js
odoo.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;
            }
        },
        _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;
})
  1. /static/src/js/m2m_group_controller.js
odoo.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;
});
  1. 添加/static/src/js/m2m_group_renderer.js
odoo.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;
});
  1. 添加/static/src/js/m2m_group_view.js
odoo.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: _.extend({}, AbstractView.prototype.config, {
            Model: M2mGroupModel,
            Controller: M2mGroupController,
            Renderer: M2mGroupRenderer,
        }),
        viewType: 'm2m_group',
        searchMenuTypes: ['filter', 'favorite'],
        accesskey: "a",
        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 define "m2m_field" attribute.');
            }

            // Model Parameters
            this.loadParams.m2m_field = attrs.m2m_field;
        },
    });
    view_registry.add('m2m_group', M2mGroupView);

    return M2mGroupView;
})
  1. 添加/static/src/xml/qweb_template.xml
<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-attf-src="/web/image/#{group_data.model}/#{group}/image_512"/>
                <div class="card-body">
                    <h5 class="card-title mt8">
                        <t t-esc="group_data['name']"/>
                    </h5>

                </div>
                <ul class="list-group list-group-flush">
                    <t t-foreach="group_data['children']" t-as="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>
  1. 添加所有的js文件
...
<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" />
...
  1. 最后,添加新视图实例
<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>
  1. 添加动作
...
<field name="view_mode">tree,m2m_group,form</field> 
...

更新视图后如下:

重要信息
Odoo视图非常易于使用,并且非常灵活。然而,通常情况下,简单和灵活的事物都有复杂的实现。odoojavascript视图也是如此:它们易于使用,但实现起来很复杂。它们由许多组件组成,包括模型、渲染器、控制器、视图和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的值是搜索视图domain及action domain的组合。m2m_field是我们计划分组的元素,将在视图中定义。
后面几步,我们新增了JavaScript文件。Odoo中的JavaScript视图是由view,model,renderer,及controller构成。由于历史原因,单次view在odoo中有其特殊的含义。因此,传统意义上的model、view、controller(MVC)变成了model、renderer、controller(MRC)。view与model、renderer、controller的关系如下:

Model、Renderer、Controller、View是组成视图的基本元素。

  • Model: model是视图的状态载体。负责发送RPC请求数据,并传递数据给controller及renderer。我们重写了__load和__reload方法。当视图在初始化的将调用__load()获取数据。当搜索条件变化或视图有一个新的状态的时候,__reload()将被调用。在我们的案例中,我们创建了用于RPC请求数据的_fetchData()方法,可实现调用服务器侧模型的方法get_m2m_group_data(步骤3中定义)。__get()是在controller中调用获取模型状态。
  • Controller: Controller负责将Model和Renderer串起来。当Renderer中的action被触发的时候,它将传递相关信息给controller并执行该action。有时,它也将调用Model中的方法。此外,它也负责管理控制面板上的按钮。在我们的例子中,我们创建了添加记录的按钮。为了实现此目标,我们需重写AbstractController中的renderButtons()函数。我们也需要注册custom_events,以便在点击作者卡片上的按钮时,能够触发相关动作(点击button->触发事件->controll捕获事件->响应)。
  • Renderer: renderer负责管理DOM元素。每种视图都有其独特的渲染方式。在渲染器中,你可以获取Model的状态,并通过render()渲染视图。在我们的例子中,我们渲染了ViewM2mGroup QWeb模板。并把JavaScript事件与动作进行关联。本节,我们绑定了卡片按钮的点击事件。当用户点击了按钮,将触发btn_clicked事件,并打开该作者的图书列表。

重要贴士
events和custom_events是不同的,events是JavaScript常规的事件,而custom_events源自odoo的JavaScript的框架。用户自定义事件可通过trigger_up触发。

  • View: View负责用于获取构成视图的基本元素,比如fields、context、view arch以及其他的元素。然后,View将初始化controller、renderer、model。通常,View还将为model、view、controller准备相关参数。在我们的例子中,Model是以m2m_field的值进行分组,因此我们设置了model的参数。同样,this.controllerParams和this.rendererParams将用于controller及renderer。

步骤8,我们添加了QWeb模板用于视图及控制面板。关于QWeb模板更多的内容,可学习本章中“使用用户侧QWeb模板”一节。

重要信息
Odoo视图有各种各样的函数,我们在本章中学了最常用的一些函数。如果你想要学习更多的内容,可查看/addons/web/static/src/js/views目录。该目录也包含abstract model、controller、renderer及view的内容。

步骤9,添加JavaScript文件。
最后,最后两步,我们添加了book.library模型的视图。步骤10,我们使用<m2m_group>标签及m2m_field属性。这将用于从服务器获取数据。

更多

如果你只是想对现有视图进行调整,你可以使用js_class。比如,如果我们想要创建一个类似于kanban的视图,可如下操作:

var CustomDashboardRenderer = KanbanRenderer.extend({...});
var CustomDashboardModel = KanbanModel.extend({...});
var CustomDashboardController = KanbanController.extend({...});
var CustomDashboardView = KanbanView.extend({
	config: _.extend({}, KanbanView.prototype.config, {
		Model: CustomDashboardModel,
		Renderer: CustomDashRenderer,
		Controller: CustomDashboardController,
	}),
});

var viewRegistry = require('web.view_registry');
viewRegistry.add('my_custom_view', CustomDashboardView);

然后我们调用带有js_class属性的kanban视图:

...
<field name="arch" type="xml">
	<kanban js_class="my_custom_view">
		...
	</kanban>
</field>
...

调试用客户端侧的代码

对于服务器侧的代码调试,我们在第七章进行了详细介绍。本节,我们将对客户端侧代码调试进行说明。

准备

步骤

调试客户端侧代码之所以麻烦是因为web客户端严重依赖于Jquery的异步事件。因为断点会中断代码执行,因此很有可能由于时间问题引起的BUG并不会被触发。

  1. 对于客户端调试,您需要使用资产激活调试模式。如果您不知道如何使用资产激活调试模式,请阅读第1章“安装Odoo开发环境”中的“激活Odoo开发工具”配方。
  2. 在你感兴趣的JavaScript函数中,调用调试器:
debugger;
  1. 如果你有计时问题,可以通过JavaScript函数登录控制台:
console.log("I'm in function X currently");
  1. 如果你想在模板渲染过程中进行调试,可以从QWeb调用调试器:
<t t-debug="" />
  1. 您也可以使用QWeb登录控制台,如下所示:
<t t-log="myvalue" />

所有这一切都依赖于您的浏览器提供适当的调试功能。虽然所有的主流浏览器都能做到这一点,但为了便于演示,我们在这里只讨论Chromium。为了能够使用调试工具,点击右上角的菜单按钮并选择更多工具|开发人员工具:

原理

当调试器打开时,你应该会看到类似下面的截图:

在这里,您可以在不同的选项卡中访问许多不同的工具。前面屏幕截图中当前活动的选项卡是JavaScript调试器,我们通过单击行号在第31行中设置断点。每当我们的小部件获取用户列表时,执行应该在这一行停止,调试器将允许您检查变量或更改它们的值。在右侧的观察列表中,您还可以调用函数来尝试它们的效果,而不必不断保存脚本文件并重新加载页面。
一旦打开了开发人员工具,我们前面描述的调试器语句的行为将是相同的。然后,执行将停止,浏览器将切换到Sources选项卡,打开有问题的文件,并突出显示debugger语句所在的行。
前面提到的两种日志记录的可能性将在控制台选项卡中结束。这是您在任何情况下都应该检查的第一个选项卡,因为如果一些JavaScript代码由于语法错误或类似的基本问题而根本无法加载,那么您将看到一条错误消息,解释发生了什么。

更多

使用Elements选项卡检查浏览器当前显示的页面的DOM表示。在熟悉现有小部件生成的HTML代码时,这将被证明是有帮助的,而且一般来说,它还允许您使用类和CSS属性。这是测试布局变化的一个很好的资源。
Network选项卡提供了当前页面发出的请求的概览,以及它花费了多长时间。在调试缓慢的页面加载时,这很有帮助,因为在Network选项卡中,您通常会找到请求的详细信息。如果您选择了一个请求,您可以检查传递给服务器的有效负载和返回的结果,这有助于您找出客户端意外行为的原因。您还将看到请求的状态码(例如,404),以防由于文件名拼写错误而找不到资源。

通过引导提升交互感

在开发了一个大型的应用程序之后,向最终用户解释软件流程是至关重要的。Odoo框架包括一个内置的向导管理器。有了这个导览器,您就可以通过学习特定流程来指导最终用户。在此菜谱中,我们将创建一个向导来引导用户创建一本书。

准备

步骤

  1. 添加/static/src/js/my_library_tour.js文件
odoo.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",
        rainbowManMessage: _t("Congrats, you have listed a book."),
        sequence: 5,
    }, [tour.stepUtils.showAppsMenuItem(), {
        trigger: '.o_app[data-menu-xmlid="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',
    }]);
});
  1. 将js文件添加到后台资源
<script type="text/javascript" src="/my_library/static/ src/js/my_library_tour.js" />

更新模块如下:

原理

向导管理器在web_tour.tour命名空间中。
步骤1,我们引入了web_tour.tour。随后我们通过registry()添加了向导。我们注册了名为library_tour的向导并配置URL(向导的作用URL)。
下一个参数是这些向导步骤的列表。一个浏览步骤需要三个值。触发器用于选择应该在其上显示游览的元素。这是一个JavaScript选择器。我们使用菜单的XML ID,因为它在DOM中可用。
第一步tour. steputils . showappsmenuitem()是tour中为主菜单预定义的步骤。下一个键是内容,当用户将鼠标悬停在tour drop上时将显示内容。我们使用_t()函数是因为我们想要转换字符串,而position键用于决定行程删除的位置。可能的值包括top、right、left或bottom。

重要信息
这些向导可改善了用户体验,以及管理集成测试。当您在内部以测试模式运行Odoo时,它也会运行向导。但是如果向导未完成,将导致测试用例失败。

手机端js(企业版可用)

posted @ 2021-03-11 21:08  老韩头的开发日常  阅读(1118)  评论(0编辑  收藏  举报