【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(企業版可用)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM