第十五章 关系字段
关系字段的视图组成相对于基础字段来说可能要复杂一些, 常见的关系字段如Many2one, One2many和Many2many都有自己的一套显示逻辑, 这些部件有相同的地方也会有不同的地方, 本章就来揭示这几种关系字段的视图渲染行为特性.
Many2one
Many2one字段在视图上的显示是一个下拉框式的选择器,用户可以选择相应的值,或者使用快速创建和编辑功能创建一条新的记录.
如果想要Many2one的行为类似Selection, 可以使用wiget=selection部件,将其Selection化.
如果想要禁用快速创建和编辑功能, 有两种实现方式, 一种是将该Many2one的宿主对象的创建和编辑权限去掉, 另外一种则是使用options属性,将快速编辑和创建去掉:
<field name="many2one_field" options="{'no_create_edit':True}">
- no_create: 禁止快速创建
- no_edit: 禁止快速编辑
- no_create_edit: 禁止快速创建和编辑
- no_open: 禁止打开
X2Many
X2Many实际上包含了两种字段类型,One2many和Many2many。这两种字段虽然大部分属性相同,但是也有些许不同。我们将在下面分别介绍到这两种类型的字段特性。现在,我们先来看一下X2Many字段,也就是One2many和Many2many的共同点。
定义及支持的事件列表
我们先来看X2Many字段的定义:
var FieldX2Many = AbstractField.extend({
tagName: 'div',
custom_events: _.extend({}, AbstractField.prototype.custom_events, {
add_record: '_onAddRecord',
discard_changes: '_onDiscardChanges',
edit_line: '_onEditLine',
field_changed: '_onFieldChanged',
open_record: '_onOpenRecord',
kanban_record_delete: '_onRemoveRecord',
list_record_remove: '_onRemoveRecord',
resequence_records: '_onResequenceRecords',
save_line: '_onSaveLine',
toggle_column_order: '_onToggleColumnOrder',
activate_next_widget: '_onActiveNextWidget',
navigation_move: '_onNavigationMove',
save_optional_fields: '_onSaveOrLoadOptionalFields',
load_optional_fields: '_onSaveOrLoadOptionalFields',
}),
...
})
从定义中,我们可以看出X2Many字段是有抽象字段扩展而来。并且支持一下自定义事件:
- add_record: 添加新记录
- discard_changes: 丢弃更改
- edit_line: 编辑行事件
- field_changed: 字段值发生更改
- open_record: 打开记录(以弹窗形式)事件
- kanban_record_delete: 看板记录删除事件
- list_record_remove: 列表记录删除事件
- resequence_records: 重新排序事件
- save_line: 保存单行事件
- toggle_column_order: 更改列顺序事件
- activate_next_widget:激活下一个widget事件
- navigation_move: 上一个/下一个事件
- save_optional_fields: 保存可选字段事件
- load_optional_fields: 加载可选字段事件
渲染形式
X2Many字段默认有两种渲染形式,列表形式(List)和看板形式(Kanban),使用哪种形式的渲染方式取决于我们在视图arch节点中使用的是tree还是kanban,当然,我们也可以通过重载_getRenderer方法来实现更多的渲染形式。
_getRenderer: function () {
if (this.view.arch.tag === 'tree') {
return ListRenderer;
}
if (this.view.arch.tag === 'kanban') {
return KanbanRenderer;
}
}
子视图
我们在使用X2Many字段的时候,如果想要查看关联模型的更多数据,通过点击列表模式下的行,会弹出一个新的子窗口,用来展示关联模型的页面。
useSubview: true
编辑模式
X2Many有两种编辑模式,编辑(edit)和只读(readonly)。编辑模式下,可以对数据进行修改,而只读模式则不能对数据进行编辑。
X2Many字段用属性mode来标识当前部件属于哪种模式,或者可以用isReadonly来判断是否为只读模式。
One2many与Many2many的互换
X2Many声明了一个属性,isMany2many,用来标识当前字段是否应该用Many2many部件来进行渲染。注意,部件虽然可能声明为Many2many,但实际上字段本身可能是one2many,它们之间并无绝对关系。换句话说,部件one2many和many2many是可以互换使用的。
新增按钮、删除按钮和行内编辑
默认情况下,X2Many的列表视图中会包含新增、删除按钮。如果想要禁止新增按钮,我们可以在视图的属性中声明一下变量:
- create: 新增
- delete: 删除
- editable: 是否允许行内编辑
快速编辑特性
odoo15.0中新增了一个"快速编辑特性", 该特定可以让用户在不点击编辑按钮的情况下切换到编辑模式. X2Many类型的字段的tree视图就是快速编辑的一个使用场景, 用户在X2Many的tree视图中直接点击即可以进行编辑.
该特性的实现基于新增的_canQuickEdit属性, 我们在X2Many的初始化属性中找到了如下代码:
if (arch) {
this.activeActions.create = arch.attrs.create ?
!!JSON.parse(arch.attrs.create) :
true;
this.activeActions.delete = arch.attrs.delete ?
!!JSON.parse(arch.attrs.delete) :
true;
this.editable = arch.attrs.editable;
this._canQuickEdit = arch.tag === 'tree';
} else {
this._canQuickEdit = false;
}
由此我们可以知道, X2ManyField的tree节点都会开始快速编辑特性,因此One2Many和Many2Many都会有此特性.
启动
当X2Many字段挂载到视图以后,视图会调用渲染器(Renderer)来初始化字段(部件),我们现在来看一下X2Many字段初始化都做了什么。
init: function (parent, name, record, options) {
this._super.apply(this, arguments);
this.nodeOptions = _.defaults(this.nodeOptions, {
create_text: _t('Add'),
});
this.operations = [];
this.isReadonly = this.mode === 'readonly';
this.view = this.attrs.views[this.attrs.mode];
this.isMany2Many = this.field.type === 'many2many' || this.attrs.widget === 'many2many';
this.activeActions = {};
this.recordParams = {fieldName: this.name, viewType: this.viewType};
var arch = this.view && this.view.arch;
if (arch) {
this.activeActions.create = arch.attrs.create ?
!!JSON.parse(arch.attrs.create) :
true;
this.activeActions.delete = arch.attrs.delete ?
!!JSON.parse(arch.attrs.delete) :
true;
this.editable = arch.attrs.editable;
}
if (this.attrs.columnInvisibleFields) {
this._processColumnInvisibleFields();
}
}
我们可以看出,初始化过程对X2Many字段的常规属性做了赋值。
数据保存
用过户编辑完数据,表单控制器会通知X2Many字段进行数据保存。X2Many会调用commitChanges方法将修改的字段值提交保存。
commitChanges: function () {
var self = this;
var inEditionRecordID =
this.renderer &&
this.renderer.viewType === "list" &&
this.renderer.getEditableRecordID();
if (inEditionRecordID) {
return this.renderer.commitChanges(inEditionRecordID).then(function () {
return self._saveLine(inEditionRecordID);
});
}
return this._super.apply(this, arguments);
}
如果当前字段是以列表形式渲染的,那么渲染器将会调用getEditableRecordID方法获取当前正在编辑的记录ID,并提交保存。然后X2Many会将这一行的数据使用_saveLine方法进行保存。
One2many
One2many在视图中默认的渲染结果是一个树形列表, 该树形列表类似与树形视图中的渲染效果但是又有所不同, 其区别点在于:
- 树形视图中的列表可以进行分组, 而One2many字段的列表不可以被分组
- 树形视图中的列表带有搜索控件, 而One2many字段的列表不可以被搜索
- 树形视图中的创建和编辑可以使用create和edit树形进行控制,而One2many字段的列表不可以被控制.
对于X2many类型的字段, 如果用户拥有创建权限, 那么默认情况下,在树形列表的最后一行会有一个"添加新的一行"的超链接, 用户点击该连接就可以快速创建一条新的记录.
创建的新记录既可以在行内直接编辑也可以以弹窗的方式在模态窗口中创建, 控制这两种显示形式的特性是tree节点的editable属性:
<tree editable="top">
...
</tree>
- top: 以模态方式(Form)显示
- bottom: 以行内编辑的模式显示
如果想要禁止"添加新的一行", 可以通过一下的方式:
- 该字段设置为readonly属性, 其缺点是所有记录都将不可创建不可编辑
- options属性中将create设置为false/0
- 该字段属性中设置can_create属性为false/0
如果想要禁止其编辑One2many字段, 可以使用下面的方式:
- readonly属性
- options中的edit属性设置为false/0
- 字段属性中设置can_write为false/0
原生X2Many字段并不支持no_open属性,为了实现禁止点击条目弹出窗口,因此,我们在欧姆基础模块中对此添加了支持,安装了该模块以后可以设置下面的属性:
<field name="x2many" options="{'no_open':1}">
<tree>
...
</tree>
</field>
Many2many
Many2many字段与One2many字段基本行为上类似, 只是个别地方的行为有差异.
如果想要禁用Many2many字段的"添加新的一行", 不能使用create属性,而应当使用link属性替代.
options中支持的操作有: create delete link unlink.
X2many的options属性从14.0开始支持.
Many2many的默认值设置
与我们常规思维不同的是, many2many字段的默认值设置不是使用[(4,id)]这种形式, 反而是直接赋值:
def default_get(self,fields):
res = super().default_get(fields)
res['x'] = x.ids
..
这里假定x是一个many2many字段。
Reference
Reference是相对比较特殊的一个字段, 乃至odoo在顶层的数据加载过程中都对它进行了特殊化的处理.
我们在第三章提到过, 数据加载过程中使用read方法读取到数据以后, 回对Reference类型的字段进行特殊化的处理. 这个特殊化的处理就是指,页面再渲染Reference类型的字段时,会调用它的rpc方法name_get来获取可读化的文本, 而非其数据库存储值: model_name,id
这个过程的载体是_fetchReference方法:
_fetchReference: function (record, fieldName) {
var self = this;
var def;
var value = record._changes && record._changes[fieldName] || record.data[fieldName];
var model = value && value.split(',')[0];
var resID = value && parseInt(value.split(',')[1]);
if (model && model !== 'False' && resID) {
def = self._rpc({
model: model,
method: 'name_get',
args: [resID],
context: record.getContext({fieldName: fieldName}),
}).then(function (result) {
return self._makeDataPoint({
data: {
id: result[0][0],
display_name: result[0][1],
},
modelName: model,
parentID: record.id,
});
});
}
return Promise.resolve(def);
},