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可以出现的一级节点列表:

  • 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>

results matching ""

    No results matching ""