Odoo入门(九)—— QWeb模板之看板视图

QWeb是Odoo使用的模板引擎。它是基于xml语言编写的,用来自动生成Html网页页面。下面就让我们来学习如何使用QWeb的语法。使用QWeb来创建我们的看板视图及用户报告。

看板视图

  • 看板是让工作队列可视化的一个工具。这个视图构造了一个个的列,每列代表一个工作状态阶段。工作根据目前的状态像一张张卡片一样放在相对应的每列中。一个新的工作的创建就是从看板视图的最左边一步步的向右边移动,最后到达看板视图的最右侧的列中,代表着一个工作的完成。

  • 看板视图的可视化跟简单性让它们能够完美的支持简单的业务逻辑流程。最简单的例子就是: 看板视图拥有3列。To Do, Doing, Done.分别代表工作的准备阶段,执行阶段,完成阶段。

    简单的看板视图例子

  • 对于许多的业务使用实例,看板视图在管理协作流程时比传统的工作流更加的有效率。

  • 在看板视图中,只有2个标签能够使用<field><button>.

  • HTML页面是通过QWeb模板动态生成的。QWeb引擎处理特殊的XML标签跟属性来生成web界面所需要的HTML语言。 这为如何在前端web界面传递后台数据提供了更好的控制,但也带来了缺点,视图的设计可能会相当复杂。

  • 看板视图的设计相当灵活,所以我们会使用最直接的描述来让我们能够快速的建立看板视图。一个很好的方法是找到已有的与我们的需求比较相近的看板视图。

  • 我们能够看到有两种使用看板视图的方式,第一种是卡片列表格式。通常在联系人,产品,员工名单中。 例子,联系人看板视图:

    联系人看板视图

    其实这不是真正的看板视图。一个真正意义上的看板视图是通过列来进行卡片的存放的。例子,Sales | My Pipeline.

    真正的看板视图

  • 这两个例子的最显著的区别在于第二个例子中的看板视图把每个工作小卡片通过列来组织管理起来。这是通过分组功能来实现的,通过每个卡片(代表work实例)的stage字段的值来进行分组,然后让这些卡片被置入代表不同分组的列中。在Sales | My Pipeline我们还能发现这些列中的卡片能够自由的拖动。其实在拖动过程中,每个卡片的stage字段已经发生了改变。

  • 我们使用第二个例子中的看板视图来进行对Todo 看板视图的编写

设计看板视图

  • 新建一个kanban模块,来搭建Todo task的看板视图

  • 新建todo_kanban/__manifest__.py.添加下面的模块参数

    {
        'name': 'To-Do Kanban',
        'description': 'Kanban board for todo-task',
        'author': 'xer',
        'depends': ['todo_ui'],
        'data': ['views/todo_view.xml',
    	     ],
    }
    

别忘记todo_kanban/__init__.py的创建

  • 下面使用xml编写的我们的看板视图并把它设置为默认视图.
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
    <!--add kanban mode to the menu actions-->
    <act_window id="todo_app.action_todo_task" name="To-Do Tasks"
                res_model="todo.task" view_mode="kanban,tree,form,calendar,graph,pivot"
                context="{'search_default_filter_my_tasks': True}" />
    <!--add kanban view-->
    <record id="To-do Task Kanban" model="ir.ui.view">
        <field name="model">todo.task</field>
        <field name="arch" type="xml">
            <kanban>
         <!-- Empty for now, but the Kanban will go here !-->
            </kanban>
        </field>
    </record>
</odoo>

现在我们已经有了基本的模块骨架。 在开始创建看板视图前,我们需要对to-do 任务添加一些新的字段。

优先级,看板状态,颜色

  • 除了stages,还有一些在看板视图中经常使用的字段
    • priority :让用户组织起他们自己的工作项目,标明首要处理项。

    • kanban_state : 标明了一个任务是已经可以转入到下一个阶段,还是被阻塞的状态。

    • color : 用来存储看板视图中卡片的显示颜色。能够通过看板视图中的颜色选择菜单来进行设置。 为了能添加这些字段,我们添加models/todo_task_model.py来进行task模型的新增字典操作.

      from odoo import fields, models
      class TodoTask(models.Model):
          _inherit = 'todo.task'
          color = fields.Integer('Color Index')
          priority = fields.Selection(
      	[('0', 'Low'),
      	 ('1', 'Normal'),
      	 ('2', 'High')],
      	'Priority', default='1')
          kanban_state = fields.Selection(
      	[('normal', 'In Progress'),
      	 ('blocked', 'Blocked'),
      	 ('done', 'Ready for next stage')],
      	'Kanban State', default='normal')
      

看板卡片元素

  • 看板视图结构是以<kanban>作为顶部标签。在其中添加相应的元素。

    <kanban default_group_by="stage_id" class="o_kanban_small_column">
    	<!-- Fields to use in expressions -->
    	<field name="stage_id"/>
    	<field name="color"/>
    	<field name="kanban_state"/>
    	<field name="priority"/>
    	<field name="is_done"/>
    	<field name="message_partner_ids"/>
    	<!--...add other used fields-->
    	<templates>
    	<t t-name="kanban-box">
    	<!--HTML QWeb template-->
    	</t>
    	</templates>
    </kanban>
       ```
    

注意到在kanban标签中有这么一个属性 default_group_by=“stage_id” .这个属性的作用显而易见,就是把看板卡片按照stage的值进行分组. 而在联系人模块中,我们没有设置这个属性,所以它的视图表现中并没有列来作为分隔. <kanban>标签支持的属性:

  • default_group_by : 默认列分组所使用的字段
  • default_order : 默认的排序格式
  • quick_create=“false” : 关闭快速建立列功能。
  • class : 为看板视图添加默认的css样式 在kanban标签中,我们把要在模板中使用的字段全部添加了进来。确保能够在服务端获取到这些字段中保存的数据。 接下来,我们使用<templates>这个标签元素,包含了一个或者多个QWeb模板来生成HTML语言框架。我们必须使用一个名为kanban-box的模板,这个模板是用来渲染我们的看板卡片。此外,那些被重复使用的HTML框架也能通过QWeb模板添加在标签内。 这些模板使用了标准化的HTML语言跟QWeb模板语言,QWeb提供了特殊的指令,在动态的创建最终HTML语言中起了重要作用。

看板卡片布局

  • 看板的主要内容是由kanban-box这个模板定义的.看板卡片的主要内容区域还能包含一个页脚子容器。

  • 对于单独的页脚,我们可以使用使用<div>标签,添加属性为oe_kanban_footer的css样式.这个属性会自动的留出合适的间隔来分割它的内部元素。对多余的空间进行准确的左右分割。

  • 在看板卡片中我们可以使用Bootstrap提供的 pull-leftpull-right 属性来指定添加的元素位于看板卡片的左边还是右边。在oe_kanban_footer 这个div中可以直接使用。 来看下我们的QWeb模板的代码:

     <t t-name="kanban-box">
       <!--set the kanban color-->
        <div t-attf-class="#{kanban_color(record.color.raw_value)} oe_kanban_global_click">
         <div class="o_dropdown_kanban dropdown">
          <!--top-right drop down menu here-->
    	</div>
    	  <div class="oe_kanban_content">
    	     <div class="oe_kanban_footer">
    		<div>
    		    <!-- Left hand footer.. -->
    		</div>
    		 <div>
    		    <!-- Right hand footer...-->
    		  </div>
    	      </div>
    	  </div> <!-- oe_kanban_content -->
    	      <div class="oe_clear"/>
          </div> <! --kanban color -->
      </t>
    

这个布局展示了看板卡片的全部结构。能够注意到color字段在顶部<div>标签中就被动态的指定了.

  • 接着在看板卡片的主要内容区域添加要放置的元素
<div>
      <field name="tag_ids"/>
 </div>
 <div>
      <strong>
      <a type="open"><field name="name"/></a>
      </strong>
  </div>
<ul>
     <li><field name="user_id"/></li>
     
      <li><field name="date_deadline"/></li>
</ul>
  • 在页脚的左边部分,我们插入一个优先级小部件(类似一个打星的控件)
<div>
    <!-- Left hand footer...-->
    <field name="priority" widget="priority"/>
</div>
  • 在页脚的右边部分,我们放置看板状态控件以及to-do task 的拥有者的头像小图标
<div>
  <!-- Right hand footer... -->
  <field name="kanban_state" widget="kanban_state_selection"/>
  ![](kanban_image('res.users', 'image_small', record.user_id.raw_value))
</div>

看板状态跟普通表单视图一样,使用<field>标签添加.而用户的小头像图片使用了HTML语言中的<img> 标签.我们把图片的地址使用QWeb的t-att-指令来进行动态生成。 有时候,我们需要一个头像来放在看板卡片中显示(例如联系人应用中的类似名片一样的存在).我们可以添加这样的图片链接代码

![](kanban_image('res.partner', 'image_medium',record.id.value))

添加看板卡片的可供选择菜单

  • 看板卡片中能够使用可选菜单,放置在右上角。通常放置在菜单里的动作为编辑或者删除记录。其实,这个菜单中可以放置很多其他的动作,我们就把一个设置卡片颜色的可选动作放置在其中。 下面,就在oe_kanban_content这个数据的div标签添加我们的可选菜单.

注意到如果我们的代码中不引入`<field name="is_done"/>`,
那么我们在可选菜单中引入的`server-side Model` 中的`do_toggle_done`是无法起作用的。
所以我们在定义`<template>`标签内容之前就引入了`is_done`字段。
![引入is_done字段](http://upload-images.jianshu.io/upload_images/6865906-addcdeaaedd0ee6f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)。

上面的代码中,我们还通过t-if这个QWeb指令来执行判断操作。如满足条件,就用HTML中a标签来超链接相应的 编辑,删除动作。

看板视图中的动作

  • 在QWeb模板中,a标签有一个type属性,它设置了动作的种类,能够在让一个链接作为一个动作来使用。所以,除了<button>元素,<a>标签也能用来执行Odoo中的动作. 就像在表单视图中一样,动作的种类也被分为actionobject.动作通过name这个属性来进行关联.a标签中添加name属性与动作的name相同即可调用.下面几个动作的类型也可以被使用:
    • open : 打开一个相应的表单视图
    • edit : 使用编辑模式直接打开一个相应的表单视图
    • delete : 从看板视图中删除该条记录。

QWeb 模板语言

  • QWeb 解析器在模板中找到特殊的QWeb指令,然后使用HTML代码来替代它们,这就是动态生成HTML页面的原理。这些特殊的QWeb指令是XML元素属性,能够在任何类型的标签中使用。例如<div>, <span>,<field>.中

  • 有些时候我们想要使用QWeb指令但是不想把它置于XML元素中.这时候,我们可以使用<t>这个特殊标签.它只是为了调用QWeb指令存在,在实际的XML跟HTML中并没有实际意义.所有不用担心最后会在XML/HTML被输出.

  • QWeb指令通过评估表达式,能够根据当前记录的具体值来得到不同的结果。有两种不同的QWeb实施方式:客户端的JavaScript 跟服务端的Python。

  • 报告跟网页是使用了服务端python代码实现。在另一方面,看板视图使用了客户端的JavaScript语言实现方式。这意味着在看板视图中使用的QWeb指令需要需要使用js形式来编写,而不是使用python。

    在展示看板视图时,内部的步骤可以概括如下:

    1. 得到模板中的xml内容作为渲染
    2. 调用服务端的read()方法来获取模板中的字段的数据记录.
    3. 定位模板中的kanbao-box位置,使用QWeb来对最终的HTML架构进行解析输出.
    4. 把最后的HTML置入浏览器中显示给用户

js环境下的QWeb

  • 在客户端使用的QWeb是用js编写的。一个record对象代表了当前需要被渲染的数据记录,使用时候一般会带上需要请求获取数据的字段。获取字段值的属性有2个
  • raw_value : 通过客户端read()方法返回的值.一般可用作判断的条件
  • value : 是根据用户的设置进行格式化后的数据,一般在用来客户界面.在date/datetimefloat/monetary 字段上很常用. QWeb的取值内容也能够与js网站客户端实例相关联。我们可以通过以下的标识来使用相关的js实例对象。
  • widget : 代表着当前的KanbanRecord()控件对象。
  • record : 其实是widget.records这个对象的缩写.使用了.操作符.
  • read_only_mode : 指明了当前视图是只读模式. widget.view.options.read_only_mode的缩写.
  • instance : 代表了整个网站客户端实例. 另外需要注意的是,在表达式中有些符号是不能使用的.例如>, <这样的大于小于符号,因为它们会与xml语言中的标签符号冲突.所以判断中的比较符号需要进行转义:
    • lt : <
    • lte : <=
    • gt : >
    • gte: >=

使用 t-attf 来对属性字符串进行替代

  • 我的看板卡片使用了 t-attf QWeb指令来动态的对顶层<div>标签设置class属性。目的是为了能够让卡片的颜色根据color这个字段进行动态的变化.

    <div t-attf-class="#{kanban_color(record.color.raw_value)} oe_kanban_global_click">
    
  • t-attf- 这个指令使用字符串替代来动态的生成标签属性。这对于URL,CSS属性的自动生成起很大的作用。替换的位置被{{ }}或者#{ }所包含起来.里面的具体内容可以是任何符合规范的js代码或者QWeb中定义过的表达式,例如record或者widget.

  • 在我们的代码中,我们使用了kanban_color()这个js方法,来把记录的color的原始int值转换为CSS样式中的具体颜色。

  • 另外再举一个例子,我们能够动态的改变 Deadline Date 这个字段的颜色,把已经过期的日期用红色展示.

  • 把看板卡片xml中的 date_deadline 字段进行改写

<li t-attf-class="oe_kanban_text_{{record.date_deadline.raw_value and  !(record.date_deadline.raw_value > (new Date()))= ? 'red': 'black'}}">
     <field name="date_deadline"/>
   </li>

使用t-att 来动态生成属性

  • t-att- 这个QWeb指令通过执行表达式来动态的生成一个属性.我们的看板卡片使用它来动态的设置<img>标签中名为src的属性。

    t-att-src="kanban_image('res.users', 'image_small', record.user_id.raw_value)"
    

这里使用了js中的kanban_image()方法,参数依次代表了需要读取图片的模型,读取的数据字段,最后使用.raw_value来读取用户的实际数据库ID. 总的来说,t-att-NAMEt-attf-NAME能够渲染任何属性.属性的名称由最后的NAME来决定.

使用t-foreach 来进行循环遍历

  • 一个HTML代码块能够通过循环来进行反复写入。我们能够使用它在看板卡片中添加任务跟随者的小头像。

    <t t-foreach="record.message_partner_ids.raw_value.slice(0, arg_max)" t-as="rec">
      <t t-esc="rec"/>
      </t>
    
  • t-foreach指令接收一个js表达式来获取一个可供遍历的集合。很多情况下,都是to-many关系型字段的名字。我们使用t-as来为循环中的对象进行命名.

  • t-esc指令在这里只是用来把rec变量渲染成HTML语言显示在页面上.

  • 在实际的使用中,例如我们遍历任务的跟随人数,由于跟随人数存放在message_parter_ids字段中,随意遍历这个字段. 看板卡片的内容有限,我们只显示前三个跟随人数.这里需要使用js的slice()方法.代码如下

t-foreach="record.message_partner_ids.raw_value.slice(0,3)

rec遍历用来存放遍历得到的值,例如获取Partner的ID.可以这样改写代码

<t t-foreach="record.message_parter_ids.raw_value.slice(0,3)" t-as="rec">
![](kanban_image('res.partner', 'image_small', rec))
</t>

在进行遍历时,rec存储的当前遍历获取的对象可以有以下几个变量作为辅助功能来使用: - rec_index: 当前迭代对象的角标。从0开始。 - rec_size : 循环集合的总的元素个数。 - rec_first : 第一个迭代对象. - rec_last : 最后一个迭代对象. - rec_even : 偶数角标 - rec_odd : 奇数角标 - rec_parity : 偶数或者奇数角标的任意一个,有当前角标决定。 - rec_all : 被遍历的所有对象 - rec_value : 当遍历的集合是字典时,获取字典中的值. 举例来说,

	```xml
	<t t-foreach="record.message_parter_ids.raw_value.slice(0,3)" t-as="rec">
	<t t-esc="rec"/>
	<t t-if="!rec_last">;</t>
	</t>
	```

上面的代码表示对每个遍历的值后面加上;符号.

使用t-if 来进行条件判断.

  • 这个很好理解,举例

    <t t-if="!record.is_done.value">
           <li><a name="do_toggle_done" type="object">Set as Done</a></li>
           </t>
    

上面的xml表示当当前记录的is_done字段为false时才会显示名字为Set as Done的这个动作按钮.

使用t-esct-raw 来进行值的渲染

  • 在视图中,我们使用<field>标签来对字段内容进行渲染.值得注意的是,我们也可直接使用t-esc来进行字段值的HTML页面渲染.
<t t-esc="record.message_parter_ids.raw_value" />

在某些情况下,如果源数据的格式确保正确,我们可以直接使用t-raw来直接渲染字段的源数据。

```xml
<t t-raw="record.message_parter_ids.raw_value" />
```

注意:考虑到安全问题,我们还是避免直接使用t-raw.

使用t-set来为变量设值

  • 在更为复杂的逻辑中,我们需要预先设置一个变量,以便在接下来的模板中使用.这时候就需要来使用t-set指令.t-set指令通常需要联合t-value一起使用.例子:

    <t t-set="red_or_black" t-value="record.date_deadline.raw_value and
    record.date_deadline.raw_value lte (new Date())
     ? 'oe_kanban_text_red': ' ' " / >
    <li t-att-class="red_or_black">
     <field name="date_deadline"/>
      </li>
    

可以看到,我们可以设置一个名为 red_or_black的变量,然后在li标签中直接使用该变量作为css样式。 还可以直接把HTML语句作为变量来进行传递

```xml
<t t-set="calendar_sign">
    <span class="oe_e">📅</span>
</t>
<t t-raw="calendar_sign"/>
```

使用t-call 来导入模板

  • QWeb 模板能够被重复使用.这就避免了编写大量重复的HTML代码块.

  • 重复使用QWeb的关键在于需要在模板标签<templates>下使用t-name来标识该模板.然后其他模板中就能通过t-call指令来调用. 举例,我们直接把跟随者显示模板作为需要导入的模板。

     <t t-name="follower_avatars">
         <div>
    	<t t-foreach="record.message_partner_ids.raw_value.slice(0, 3)" t-as="rec">
    	  ![](kanban_image('res.partner','image_small',rec))
    	  </t>
          </div>
        </t>
    

然后在我们的看板视图模板中直接使用

<t t-call="followe_avatars"/>

值得注意的是,我们导入的模板中的变量是可以通过 t-set 来进行设值的. 例如刚才的代码中,我们把需要显示的跟随者数目更改为一个变量arg_max

  <t t-foreach="record.message_partner_ids.raw_value.slice(0, arg_max)" t-as="rec">
.....

然后在实际的调用该模板时,对arg_max变量进行传值

<t t-call="follower_avatars">
  <t t-set="arg_max" t-value="3" />
</t>

额外的t-attf使用方法

  • 在对t-att-NAMEt-attf-NAME形式的动态属性值生成时.我们可以使用2种方式进行.
  1. 字典形式

    <p t-att="{'class': 'oe_bold'}" />
    

    ``

  2. 一对元素的列表形式

<p t-att="['class' , 'oe_bold']" />

这两种方式得到的结果都是一样的

<p class="oe_bold"/ >

继承看板视图

  • 看板或者报告视图同样可以使用第三章所讲的Odoo继承机制来进行扩展。
  • 一个很常见的例子就是使用<field>元素来作为选择器,使用Xpath进行定位后然后修改即可。这里需要注意的是因为我们在看板模板视图中会对field定义两次,(别忘记一开始我们首先需要声明模板使用的字段).所以要进行准确的Xpath定位,否则只能定位到第一个匹配的字段. 例如: 直接使用
<xpath expr="//t[@t-name='kanban=box']//filed[@name='display_name'" position="before">

这个xpath表达式就能准确的匹配到模板内部的field.

导入自定义的CSS跟js文件位置.

  • 在视图的编写中,我们肯定会进行很多css样式跟js代码的编写.通常的前端开发都是把这些代码放在外部文件中然后进行路径导入.Odoo中的导入方式在于创建一个xml文件,然后使用assets_backend模板来进行路径的编写. 在我们的例子中,创建todo_kanban/views/todo_kanban_assets.xml这个xml文件,加入下面的代码:

    <?xml version="1.0" encoding="UTF-8" ?>
    <odoo>
        <template id="assets_backend" inherit_id="web.assets_backend"
    	      name="Todo Kanban Assets">
    	<xpath expr="." position="inside">
    	    <link ref="stylesheet"
    		  href="/todo_kanban/static/src/css/todo_kanban.css"/>
    	    <script type="text/javascript"
    		    src="/todo_kanban/static/src/js/todo_kanban.js"></script>
    	</xpath>
        </template>
    </odoo>