RELAX NG
在视图开发的过程中,我们会发现,几种视图的属性都是已经固定了的,如果擅自添加自定义的属性,odoo会弹窗提醒非法的视图定义,如下图:
这是因为odoo已经预定死了每种视图的属性,在展示视图的时候会有验证机制。
视图验证机制
那么,如果我们想要自定义视图的属性该怎么做呢?首先,我们要了解odoo视图的验证机制,知彼知己才能百战不殆。odoo的视图验证是通过一个叫做RELAX NG的技术实现的。
Relax NG是什么
Relax NG 是 REgular LAnguage for XML Next Generation的缩写,即“可扩展标记语言的下一代正规语言”,是一种基于语法的可扩展标记语言模式语言,可用于描述、定义和限制 可扩展标记语言(标准通用标记语言的子集)词汇表。简单地说 Relax NG是解释XML如何被定义的一套XML。Odoo就是通过定义了一套rng文件定义了自己一套xml框架结构,在模块被安装或者升级的时候将其解析成与之相对应的内置对象,存储在数据库中。关于Relax NG的语法规则,可以参考Relax NG的官网。。
来看一个,odoo中的xml中常见的data属性的例子:
<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方法:
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经常碰到的老朋友们:
...
<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可以出现的一级节点列表:
menuitem
- 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方法:
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源文件:
<?xml version="1.0"?>
<calendar date_start="date_deadline" string="Tasks" mode="month" color="user_id" draggable="false">
<field name="name"/>
</calendar>