Odoo中的Relax NG

很早之前,在博客园的文章中曾经介绍过Relax NG,如今odoo发展到了12.0,13.0时代,重新梳理一下有关Relax NG的知识点。

Relax NG是什么

Relax NG 是 REgular LAnguage for XML Next Generation的缩写,即“可扩展标记语言的下一代正规语言”,是一种基于语法的可扩展标记语言模式语言,可用于描述、定义和限制 可扩展标记语言(标准通用标记语言的子集)词汇表。简单地说 Relax NG是解释XML如何被定义的一套XML。Odoo就是通过定义了一套rng文件定义了自己一套xml框架结构,在模块被安装或者升级的时候将其解析成与之相对应的内置对象,存储在数据库中。关于Relax NG的语法规则,可以参考Relax NG的官网。

Relax NG语法

我们可以结合odoo中的一个Relax NG例子来看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<rng:define name="data">
<rng:element name="data">
<rng:zeroOrMore>
<rng:choice>
<rng:ref name="field"/>
<rng:ref name="label"/>
<rng:ref name="separator"/>
<rng:ref name="xpath"/>
<rng:ref name="button"/>
<rng:ref name="group"/>
<rng:ref name="filter"/>
<rng:ref name="html"/>
<rng:element name="newline"><rng:empty/></rng:element>
</rng:choice>
</rng:zeroOrMore>
</rng:element>
</rng:define>

这是我们常用的odoo视图文件中的data节点的结构。

  • define 定义一个节点,方便引用。
  • zeroOrMore 表示接受零个或多个节点。
  • choice 表示在列表中选择。

关于Relax NG更多介绍,请参见官方网站

odoo解析Relax NG

模块安装时

模块在安装时,odoo会解析xml文件是否符合既有的xml架构,解析方法使用的是odoo.tools.convert.py中的convert_xml_import方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def convert_xml_import(cr, module, xmlfile, idref=None, mode='init', noupdate=False, report=None):
doc = etree.parse(xmlfile)
relaxng = etree.RelaxNG(
etree.parse(os.path.join(config['root_path'],'import_xml.rng' )))
try:
relaxng.assert_(doc)
except Exception:
_logger.error('The XML file does not fit the required schema !')
_logger.error(misc.ustr(relaxng.error_log.last_error))
raise

if idref is None:
idref={}
obj = xml_import(cr, module, idref, mode, report=report, noupdate=noupdate)
obj.parse(doc.getroot(), mode=mode)
return True

从上面的代码中可以看出,解析的文件是import_xml.rng,由此文件我们可以看到我们xml经常碰到的老朋友们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
...
<rng:define name="field">
<rng:element name="field">
<rng:attribute name="name" />
<rng:choice>
<rng:group>
<rng:attribute name="type">
<rng:choice>
<rng:value>base64</rng:value>
<rng:value>char</rng:value>
<rng:value>file</rng:value>
</rng:choice>
</rng:attribute>
<rng:choice>
<rng:group>
<rng:attribute name="file"/>
<rng:empty/>
</rng:group>
<rng:text/>
</rng:choice>
</rng:group>
<rng:group>
<rng:attribute name="type"><rng:value>int</rng:value></rng:attribute>
<rng:choice>
<rng:data type="int"/>
<rng:value>None</rng:value>
</rng:choice>
</rng:group>
<rng:group>
<rng:attribute name="type"><rng:value>float</rng:value></rng:attribute>
<rng:data type="float"/>
</rng:group>
<rng:group>
<rng:attribute name="type">
<rng:choice>
<rng:value>list</rng:value>
<rng:value>tuple</rng:value>
</rng:choice>
</rng:attribute>
<rng:oneOrMore><rng:ref name="value"/></rng:oneOrMore>
</rng:group>
<rng:group>
<rng:attribute name="type">
<rng:choice>
<rng:value>html</rng:value>
<rng:value>xml</rng:value>
</rng:choice>
</rng:attribute>
<rng:oneOrMore>
<rng:ref name="any"/>
</rng:oneOrMore>
</rng:group>
<rng:group>
<rng:attribute name="ref"/>
<rng:empty/>
</rng:group>
<rng:group>
<rng:attribute name="eval"/>
<rng:optional><rng:attribute name="model"/></rng:optional>
<rng:empty/>
</rng:group>
<rng:group>
<rng:attribute name="search"/>
<rng:optional><rng:attribute name="model"/></rng:optional>
<rng:optional><rng:attribute name="use"/></rng:optional>
<rng:empty/>
</rng:group>
<rng:text/>
</rng:choice>
</rng:element>
</rng:define>


<rng:define name="record">
<rng:element name="record">
<rng:optional>
<rng:attribute name="id" />
<rng:optional>
<rng:attribute name="forcecreate" />
</rng:optional>
</rng:optional>
<rng:attribute name="model" />
<rng:optional><rng:attribute name="context"/></rng:optional>
<rng:zeroOrMore>
<rng:ref name="field" />
</rng:zeroOrMore>
</rng:element>
</rng:define>

<rng:define name="template">
<rng:element name="template">
<rng:optional><rng:attribute name="id"/></rng:optional>
<rng:optional><rng:attribute name="t-name"/></rng:optional>
<rng:optional><rng:attribute name="name"/></rng:optional>
<rng:optional><rng:attribute name="forcecreate"/></rng:optional>
<rng:optional><rng:attribute name="context"/></rng:optional>
<rng:optional><rng:attribute name="priority"/></rng:optional>
<rng:optional><rng:attribute name="key"/></rng:optional>
<rng:optional><rng:attribute name="website_id"/></rng:optional>
<rng:group>
<rng:optional>
<rng:attribute name="inherit_id"/>
<rng:optional>
<rng:attribute name="primary">
<rng:value>True</rng:value>
</rng:attribute>
</rng:optional>
</rng:optional>
<rng:optional><rng:attribute name="groups"/></rng:optional>
<rng:optional><rng:attribute name="active"></rng:attribute></rng:optional>
<rng:optional><rng:attribute name="customize_show"></rng:attribute></rng:optional>
</rng:group>
<rng:zeroOrMore>
<rng:choice>
<rng:text/>
<rng:ref name="any"/>
</rng:choice>
</rng:zeroOrMore>
</rng:element>
</rng:define>
...

由此文件,我们可以总结出xml可以出现的一级节点列表:

  • id(必填)
  • name
  • parent
  • action
  • sequence
  • groups
  • icon
  • web_icon
  • web_icon_hover
  • string

record

  • id(可选)
  • forcecreate(可选)
  • model
  • context(可选)
  • 子节点:field(可多个)

template

  • id
  • t-name
  • name
  • forecreate
  • context
  • priority
  • inherit_id
  • primary
  • groups
  • active
  • customzie_show
  • page

delete

  • model
  • id
  • search

act_window

  • id
  • name
  • res_model
  • domain
  • src_model
  • context
  • view_id
  • view_type
  • view_mode
  • multi
  • target
  • key2
  • groups
  • limit
  • usage
  • auto_refresh

url

  • id
  • name
  • url
  • target

assert

  • model
  • search
  • count
  • string
  • id
  • context
  • severity
  • test

report

  • id
  • string
  • model
  • name
  • report_type
  • multi
  • menu
  • keyword
  • rml
  • file
  • sxw
  • xml
  • xsl
  • parser
  • auto
  • header
  • webkit_header
  • attachment
  • attachment_use
  • groups
  • usage

workflow

  • model
  • action
  • uid
  • context
  • ref
  • value

function

  • model
  • name
  • id
  • context
  • eval

ir_set

  • 子节点:field

这就解释了,笔者曾经的困惑,xml中为什么可以出现act_window这样一个节点?menuitem是在哪里定义的?

视图渲染时

上面提到的只是对静态代码的约束,我们知道,odoo中非常有特点的功能就是可以在界面中编辑xml代码,那么,odoo又是如何确保用户手动填写的代码符合Relax NG架构的呢?

经过几番跟踪测试,我们发现,当odoo渲染页面视图时,会调用tools.view_validation中的relaxng方法:

1
2
3
4
5
6
7
8
9
10
11
def relaxng(view_type):
""" Return a validator for the given view type, or None. """
if view_type not in _relaxng_cache:
with tools.file_open(os.path.join('base', 'rng', '%s_view.rng' % view_type)) as frng:
try:
relaxng_doc = etree.parse(frng)
_relaxng_cache[view_type] = etree.RelaxNG(relaxng_doc)
except Exception:
_logger.exception('Failed to load RelaxNG XML schema for views validation')
_relaxng_cache[view_type] = None
return _relaxng_cache[view_type]

从上述方法中可以看出,每种视图都要经过验证,只有符合定义的属性才会通过。定义的文件位于base模块下的rng文件夹中。这里包含了我们经常用到的search、tree、pivot、graph、gantt、diagram、calendar等视图的结构文件。

自定义视图架构

了解了视图架构的验证机制,那么我们自然就想到了可以拓展视图的架构。最简单的方法莫过于直接修改RNG文件,但是这样污染了源代码。
想要避免修改源码的方法也很简单,重载relaxng方法,使之先查找自定义的RNG文件夹,如果找不到再去base模块查找。例如,笔者为了禁止日历视图拖拽,编写了一个calendar_draggle模块,其中就新增了一个draggable属性而没有修改RNG源文件:

1
2
3
4
<?xml version="1.0"?>
<calendar date_start="date_deadline" string="Tasks" mode="month" color="user_id" draggable="false">
<field name="name"/>
</calendar>
你的支持我的动力