本章中,我们将讲解如下小节:

  • 管理静态资源
  • 为网站扩展CSS和JavaScript
  • 创建或更改模板 - QWeb
  • 管理动态路由
  • 为用户提供小组件
  • 从网站用户获取输入
  • 管理搜索引擎优化(SEO)选项
  • 管理网站的站点地图
  • 获取访客的国家信息
  • 追踪营销活动
  • 管理多站点

引言

Odoo自带完整功能的内容管理系统(CMS)。通过拖拽功能,终端用户可以在几分钟内设计出一个页面,但在Odoo的CMS系统中开发新功能或构建版块则不是那么简单了。本章将给出在Odoo CMS系统中开始功能按步骤的指南。

ℹ️所有的Odoo CMS功能由website和web_editor模块实现。如果想要学习CMS底层的运行原理,请查阅这两个模块。

管理静态资源

现代网站包含大量的JavaScript和CSS文件。当页面在浏览器中加载时,这些静态文件对服务端发送单独的请求。请求数量越多,网站的速度就越慢。为避免这一问题,大部分网站通过合并多个文件来返回静态资源。市面上有很多工具用来进行此类管理,但是Odoo对于管理静态资源有其自己的实现方式。

什么是资源包以及Odoo中资源的区别?

在Odoo中,静态资源的管理和对其它应用的管理一样简单。Odoo有大量不同的应用和代码基。不同的Odoo应用有不同的作用和 UI 界面。这些代码并不使用相同的代码,因此有时情况下我们希望加载一部分资源,而不是所有资源。在页面中加载不必要的静态资源并不是个良好的实践。为避免在应用中加载额外的资源,Odoo对不同的代码基使用不同的资源包。

以下是在Odoo中使用的不同的资源包:

  • web.assets_common:这个资源包包含对所有应用通用的所有基本工具,如JQurey, Underscore.js, FontAwesome等等。此资源包用于前台(网站)、后台、销售点(POS)和报表等处。这一通用资源在Odoo的几乎所有地方加载。它也包含用于Odoo模块系统的boot.js文件。
  • web.assets_backend:这一资源包在Odoo的后台中使用(ERP部分)。它包含所有与web客户端、视图、字段组件、动作管理器等相关的代码。
  • web.assets_frontend:这一资源包用于Odoo的前台(网站部分)。它包含所有与网站端应用相关的代码,如电商、博客、线上活动、论坛和在线聊天等等。注意这个资源包不包含与网站编辑和拖拽功能(网站构造器)相关的代码。这背后的原因是我们不希望在对外网站使用时加载编辑器资源。
  • web_editor.assets_editor和web_editor.summernote:这个资源包包含与网站编辑组件选项及拖拽功能(网站构造器)相关的代码。它仅在用户具有网站编辑器访问权限时才进行加载。也用于批量邮件设计器。
  • web.report_assets_commo:QWeb仅为通过HTML生成的PDF文件。这一资源在报表布局中进行加载。

ℹ️有一些用于指定应用的资源包:point_of_sale.assets, survey.survey_assets, mass_mailing.layout和website_slides.slide_embed_assets。

Odoo通过AssetsBundle类管理其静态资源,位于/odoo/addons/base/models/assetsbundle.py。AssetBundle不仅合并多个文件,也打包了不同的功能。以下是其所提供的功能列表:

  • 它包含多个JavaScript和CSS文件。
  • 它通过从文件内容中删除注释、多余空格及回车换行来最小化JavaScript和CSS文件。删除这一额外资源会减小静态资源的大小并提升页面速度。
  • 它拥有对CSS预处理器的内置支持,如SASS和LESS。这表示我们可以添加SCSS和LESS文件,并且它们会自动编译并添加到资源包中。
  • 在达到4095的规则上限时它自动拆分样式表资源。

自定义资源

如我们所见,Odoo对不同的代码基拥有不同的资源。要获取适当的结果,我们将需要选择正确的资源包并放入自定义JavaScript和CSS文件。例如,如果你在设计一个网站,则需将文件放入web.assets_frontend中。在下一节中,我们将学习如何在已有资源包中添加自定义CSS/JavaScript。虽然很罕见,但有时我们需要创建全新的资源包。在下一部分中就会讲到我们可以他过自己的资源包。

如何实现…

按照如下步骤来创建一个自定义资源包:

  1. 创建QWeb模板并添加JavaScript, CSS或SCSS文件如下:

    <template id="my_custom_assets" name="My Custom Assets">
      <link rel="stylesheet" type="text/scss"
        href="/my_module/static/src/scss/my_scss.scss"/>
      <link rel="stylesheet" type="text/css"
        href="/my_module/static/src/scss/my_css.css"/>
      <script type="text/JavaScript"
        src="/my_module/static/src/js/widgets/my_JavaScript.js"/>
    </template>
    
  2. 在想要加载这个包的QWeb模板中使用t-call-assets如下:

    <template id="some_page">
    ...
      <head>
        <t t-call-assets="my_module.my_custom_assets" tjs="false"/>
        <t t-call-assets="my_module.my_custom_assets" tcss="false"/>
      </head>
    ...
    

运行原理…

在第1步中,我们新建了一个带有外部ID my_custom_assets的QWeb模板。在这个模板中,我们将需要列出所有的CSS, SCSS和JavaScript文件。首先,Odoo会将SCSS文件编译为CSS,然后Odoo会合并所有的CSS和JavaScript文件为单个CSS及JavaScript文件。

在声明资源后,我们需要将它们加载到QWeb模板(网页)中。第2步中,我们在模板中加载了CSS和JavaScript资源。t-css和 t-js属性公用于加载样式表或脚本。

ℹ️在大部分的网站开发中,我们会需要在已有资源包中添加自己的JavaScript和CSS文件。添加新资源包极其少见。仅有你想要开发不带有Odoo CMS功能的页面或应用时才要求这么做。在下一节中,我们将学习如何在已有资源包中添加CSS/JavaScript 文件。

扩展知识…

以下是一些在Odoo中使用资源时需要知道的内容。

在Odoo中调试JavaScript非常的困难,因为资源包将多个JavaScript文件合并成了单个文件并进行了最小化。通过启用带资源的开发者模式,可以跳过资源打包,页面中会独立加载各个静态资源,这样就可以轻松地进行调试了。

合并资源进行一次生成并存储在ir.attachment模型中。之后,从附件中进行调用。如果希望重新生成资源,可以通过如下图所示的调试功能完成:

TODO

**小贴士:**如你所知,Odoo仅会生成资源一次。这种行为对于开发阶段来说相当头疼,因为这时会需要进行频繁的重启。要解决这一问题,我们可以在命令行中使用dev=xml,这样会直接加载资源,就无需再重启服务了。

为网站扩展CSS和JavaScript

本节中,我们将讲解如何在网站中添加自定义样式表和JavaScript。

准备工作

本节我们将使用第四章 创建Odoo插件模块中的my_library模块。我们会添加 CSS, SCSS和 JavaScript 文件来修改网站。因为我们要修改网站,因此要在依赖于添加website。修改声明文件如下:

...
  'depends': ['base', 'website'],
...

如何实现…

重载主网站模板来注入我们的代码,如下:

  1. 添加一个名为views/templates.xml的文件并添加一个空的视图重载,如下(别忘了在__manifest__.py中添加该文件):

    <odoo>
      <template id="assets_frontend" inherit_id="web.assets_frontend">
        <xpath expr="." position="inside">
          <!-- points 2 & 3 go here /-->
        </xpath>
      </template>
    </odoo>
    
  2. 添加对CSS和SCSS文件的引用如下:

    <link href="/my_library/static/src/css/my_library.css" rel="stylesheet" type="text/css"/>
    <link href="/my_library/static/src/scss/my_library.scss" rel="stylesheet" type="text/scss"/>
    
  3. 添加对JavaScript文件的引用如下:

    <script src="/my_library/static/src/js/my_library.js" type="text/JavaScript" />
    
  4. 在static/src/css/my_library.css中添加一些CSS代码,如下:

    body main {
      background: #b9ced8;
    }
    
  5. 在static/src/scss/my_library.scss中添加一些SCSS代码,如下:

    $my-bg-color: #1C2529;
    $my-text-color: #D3F4FF;
    nav.navbar {
      background-color: $my-bg-color !important;
      .navbar-nav .nav-link span{
        color: darken($my-text-color, 15);
        font-weight: 600;
      }
    }
    footer.o_footer {
      background-color: $my-bg-color !important;
      color: $my-text-color;
    }
    
  6. 在static/src/js/my_library.js文件中添加一些JavaScript代码,如下:

    odoo.define('my_library', function (require) {
      var core = require('web.core');
      alert(core._t('Hello world'));
      return {
        // if you created functionality to export, add it here
      }
    });
    

在更新模块之后,我们应该可以看到Odoo网站的菜单、页面主体及询问都带有了自定义的颜色,并且在每个页面都会弹出一个颇为烦人的Hello world消息,如下图所示:

TODO

运行原理…

Odoo CMS的底层是名为QWeb的XML模板引擎,在下一节中会对它进行详细讨论。资源包只是由这些模板创建。在第1、2和第3步中,我们通过继承在web.assets_frontend中列出了样式表和JavaScript文件。我们选择了web.assets_frontend的原因是希望更新网站。这些资源在每个网页中都会进行加载。

在第4步中,我们添加了CSS,它设置了网站主体的背景色。

**小贴士:**对于CSS/SCSS文件来说,有时顺序很重要。因此,如果你需要重载另一个插件中定义的样式,需要注意将你的文件放在所需修改的原始文件之后加载。这通过调整视图的priority字段或直接继承向其中注入引用的CSS文件的插件视图。更多详情,请参见第十章 后端视图中的修改已有视图 - 视图继承一节。

在第5步中,我们添加了基本的SCSS。Odoo对SCSS预处理器具有内置支持。Odoo会自动编译SCSS文件为CSS。在本例中,我们使用了带有变量的基本SCSS以及函数darken来让$my-text-color的暗度降低15%。SCSS预处理器有大量的其它功能,如果想深入学习SCSS,请参见http://sass-lang.com/

ℹ️Odoo 12之前的版本中使用Bootstrap 3,使用的是LESS (http://lesscss.org)预处理器。Odoo 12使用最新的Bootstrap版本,即Bootstrap 4 (https:// getbootstrap.com/),使用的是SCSS。因此,如果你使用的是老版本的Odoo的话,需要编写LESS而非SCSS。

在第6步中,我们添加了基础的JavaScript,仅仅是在页面加载时显示警告信息。为避免JavaScript的排序问题,Odoo使用了类似RequireJS (http://requirejs.org)的机制。在我们的JavaScript文件中,调用了odoo.define(),需要两个参数,希望定义的命名空间有包含实际实现的函数。如果导出大量的功能逻辑不同的部分,在不同的函数中定义它们,前置插件名并由点击分开来防止未来的命令冲突。这正是web模块所实行的,它定义了web.core和web.data。

通过第二个参数,定义函数仅接收一个参数require,这是可用于获取对其它模块或core中定义的JavaScript命名空间引用的函数 。通过Odoo来使用它进行所有交互 ,并永不依赖于全局odoo对象。

你自己的函数可以返回指向想要在其它模块中可用引用的对象或者在没有引用时不返回任何内容。如果从你的函数返回了一些引用,可以像下例这样在另一个函数中使用它们:

odoo.define('my_module', function (require) {
  var test = {
    key1: 'value1',
    key2: 'value2'
  };
  var square = function(number) {
    return 2*2;
  };
  return {
    test: test,
    square: square
  }
});


// 在另一个文件中
odoo.define('another_module', function (require) {
  var my_module = require('my_module');
  
  console.log(my_module.test.key1);
  console.log('square of 5 is', my_module.square(5));

});

ℹ️这里所讨论的require机制在 Odoo 9.0中引入。在更老的版本中,插件处理需要以同名定义的JavaScript函数来作为openerp命名空间中的插件。这个函数接收一个以当前加载实例为参数的引用 ,通过它来访问API函数。因此,为升级已有代码,修改它为一个 odoo.define语句并通过require导入必要的对象。

创建或更改模板 - QWeb

我们将向第五章 应用模型中所开发的my_library插件添加网站功能。我们所想要做的是允许通过这个图书应用进行浏览,如果用户使用相应的权限进行了登录,启用让用户在网站界面中编辑图书详情。

准备工作

本节我们将使用第四章 创建Odoo插件模块中my_library插件模块的library.book模型。为方便起见,本节的代码中包含了一份它的拷贝。

如何实现…

我们将需要定义如下几个控制器和视图:

  1. 在views/templates.xml中添加最小化模板如下:

    <?xml version="1.0" encoding="utf-8"?>
    <odoo>
    
      <template id="books">
        <t t-call="website.layout">
        <!-- Add page elements here -->
        </t>
      </template>
    
    </odoo>
    
  2. 在website.layout中,通过oe_structure类添加可拖放元素,如下:

    <div class="oe_structure">
      <section class="pt32 pb32 bg-secondary oe_custom_bg">
        <div class="container text-center">
          <h1> Editable text and supports drag and drop.</h1>
        </div>
      </section>
    </div>
    
  3. 将该代码块加入到网站布局中并显示图书的信息,如下:

    <div class="container">
      <t t-foreach="books" t-as="book">
        <div t-attf-class="card mt24 #{'bg-light' if book_odd else ''}">
          <div class="card-body">
            <h3 t-field="book.name"/>
            <t t-if="book.date_release"
              <div t-field="book.date_release" class="text-muted"/>
            </t>
            <b class="mt8"> Authors </b>
            <ul>
              <li t-foreach="book.author_ids" tas="author">
                <span t-esc="author.name" />
              </li>
            </ul>
          </div>
        </div>
      </t>
    </div>
    
  4. 在website.layout中添加一个不可编辑元素如下:

    <section class="container mt16" contenteditable="False">
      This is a non-editable text after the list of books.
    </section>
    
  5. 添加在controllers/main.py中返回图书列表的控制器,如下:

    from odoo import http
    from odoo.http import request
    
    class Main(http.Controller):
      @http.route('/books', type='http', auth="user", website=True)
      def library_books(self):
        return request.render(
          'my_library.books', {
          'books':
          request.env['library.book'].search([]),
        })
    

在浏览器中打开http://your-server-url:8069/books,你将会看到一个带有作者的图书列表。通过这段代码,用户可mnyw 看到图书列表及详情。授予相应权限的话,用户还将能修改图书详情及一些其它文本。

运行原理…

首先,我们创建了一个用于生成需显示图书列表HTML名为books的模板。所有的代码在带有t-call属性集的t元素中封装,它会让Odoo渲染带有website.layout模板的页面并在模板内部插入内容,website.layout包含所有需要的工具,如Bootstrap, jQuery, FontAwesome等等。这些工具用于设计网页。默认,它包含所有必要的资源,如 Bootstrap, jQuery, FontAwesome等。website.layout还包含默认的头部、底部、代码片断和页面编辑功能。这样,我们获取带有菜单、底部和页面编辑功能的完整Odoo网页,无需在所有页面中重启这段代码。如果不使用t-call=“website.layout”,则不会获取到默认的头部、询问和网站编辑功能。

在website.layout内部,我们添加了带有一些QWeb模板属性的HTML来显示一个图书列表。现在,我们将来了解不同的QWeb属性及它们的用法。在这个模板中,可以访问通过main.py中控制器传递的参数。

循环

为操作记录集或可迭代数据类型,需要通过列表构造一个循环。在QWeb模板中,这可使用t-foreach元素来实现。迭代发生在t元素中,这时其内容对每个 t-foreach属性中传递的可迭代成员重复,如下:

<t t-foreach="[1, 2, 3, 4, 5]" t-as="num">
  <p><t t-esc="num"/></p>
</t>

它会渲染如下:

<p>1</p>
<p>2</p>
<p>3</p>
<p>4</p>
<p>5</p>

你也可以在一些其它元素中放置t-foreach及t-as属性,此时这个元素及其内容会对可迭代内容的每项进行重复。看一下下面的代码块。它会生成与前例完全相同的结果。

<p t-foreach="[1, 2, 3, 4, 5]" t-as="num">
  <t t-esc="num"/>
</p>

在我们的示例中,查看 t-call元素的内部,实现的内容生成都在这里发生。模板应通过上下文渲染,这个上下文件有一个名为books变量,它在t-foreach 元素中被遍历。-as 属性是必须要有的,会用作迭代器变量的名称,使用它来访问迭代数据。虽然这一构建的最常见用处是迭代记录集,但可以将其用于任意可迭代的Python对象。

在t-foreach循环中,我们获得了对一些其它变量的访问,它们的名称由相应t-as 属性获取。因前例中为book,我们可以访问的是在包含遍历时奇数索引对应值为True,偶数索引对应值为 False的book_odd变量。在本例中,我们使用它来在卡片中显示交替背景色。

其它可用的变量有:

  • book_index:返回遍历中的当前索引值(以0开始)
  • book_first和book_last:分别在遍历第一个和最后一个时为True
  • book_value,:如果我们所遍历的变量book是一个字典的话它会包含各项的值,此时book会通过字典的键进行遍历
  • book_size,:集合的大小(如有)
  • book_even和book_odd:根据遍历的索引获取true值
  • book_parity:在遍历的索引为偶数时包含even值、奇数时包含odd值

ℹ️给出的示例基于我们的示例。对应你自己的用例,需要将t-as 属性中的book替换为对应的值。

动态属性

QWeb模板可以动态地设置属性值。这可通过如下三种方式来实现。

第一种方式是通过t-att-attrname。在模板渲染时,会创建一个attr_name。在模板渲染时,会创建一个attr_name属性;它的值可以为任意有效的Python表达式。这通过当前上下文进行计算并且结果被设置为属性的值,如下:

<div t-att-total="10 + 5 + 5"/>

渲染结果如下:

<div total="20"></div>

第二种方式是通过t-attf-$attr_name。这与前一个选项相同,唯一的区别是仅运行{{ ..}}和#{..} 之间的字符串。这在值中混合了字符串时会很有用。它最常用于运行类,如下例这样:

<t t-foreach="['info', 'danger', 'warning']" t-as="color">
  <div t-attf-class="alert alert-#{color}">
    Simple bootstrap alert
  </div>
</t>

它会渲染成如下这样:

<div class="alert alert-info">
  Simple bootstrap alert
</div>
<div class="alert alert-danger">
  Simple bootstrap alert
</div>
<div class="alert alert-warning">
  Simple bootstrap alert
</div>

第三种方式是通过 t-att=mapping选项。该选项在模板渲染字典数据转化为属性和值后接收这个字典。参见如下示例:

<div t-att="{'id': 'my_el_id', 'class': 'alert alert-danger'}"/>

在渲染这个模板之后,它会被转化为如下这样:

<div id="my_el_id" class="alert alert-danger"/>

在我们的示例中,我们使用了t-attf-class来获取基于索引值的动态背景。

字段

h3和div标签使用t-field属性。t-field属性的值必须通过与长度为1的记录集一起使用,这会允许用户在编辑模式打开网站是修改网页的内容。在保存页面时,更新的值会存储在数据库中。当前,它也遵照权限检查,仅在当前用户对所显示记录有写权限时才允许操作。通过可选的t-options属性,我们可以给出一个字典参数并传递给字段渲染器,包含要使用的组件。当前在后台中并没有太多的组件,因此这里的选择是有限的。例如,如果你希望通过二进制字段显示图像,那么可以像这样使用图像组件:

<span t-field="author.image_small" t-options="{'widget': 'image'}"/>

t-field存在一些限制。它仅能用于记录集并不能用于元素。为此我们需要使用一些HTML元素,如

。t-field属性有一个替代属性t-esc。t-esc属性并不只限于记录集,它也可用于任意数据类型,但在网站中不可编辑。

t-esc和t-field之间的另一个区别是t-field显示基于用户语言的值,而t-esc显示数据库中的原始值。如对于在首选项中设置语言为英语的用户,在设置datetime字段使用t-field时,结果会以12/15/2018 17:12

。而相应的如果使用t-esc,那么结果会是一个已渲染的格式2018-12-15 16:12

**译者注:**此处的时区应为 UTC+1,因 Odoo 创始地为比利时,位于东一区

条件语句

注意显示出版日期的分区由带有t-if属性集的t元素包裹。这个属性以Python代码运行,并且该元素仅在结果为真值时进行渲染。在下例中,我们仅在有实现出版日期集合时才显示div类。但是,在复杂用例中,我们可以像下例中这样使用t-elif和t-else:

<div t-if="state == 'new'">
  Text will be added of state is new.
</div>
<div t-elif="state == 'progress'">
  Text will be added of state is progress.
</div>
<div t-else="">
  Text will be added for all other stages.
</div>

设置变量

QWeb模板也能够在模板本身中定义变量。在定义该模板后,我们可以在随后的模板中使用该变量。可以像下面这样设置变量:

<t t-set="my_var" t-value="5 + 1"/>
<t t-esc="my_var"/>

子模板

如果你在开发一个大型应用,管理大模板会很有困难。QWeb模板支持子模板,这时可以将大模板划分为子模板并可以在多个模板中进行利用。子模板中可以使用t-call属性,如下例所示:

<template id="first_template">
  <div> Test Template </div>
</template>

<template id="second_template">
  <t t-call="first_template"/>
</template>

行内编辑

用户可以直接在网站的编辑模式中修改记录。通过t-field节点加载的数据默认可进行编辑。如果用户在这个节点中修改值并保存页面,会在后台中进行值的更新。但不必担心,要进行记录的更新,用户需要有对记录的写权限。注意t-field仅可用于记录集。要显示其它类型的数据,可以使用t-esc。它和t-field的作用完全一致,唯一的不同是t-esc不可编辑并且可用于任意类型的数据。

如果你希望对页面启用组件拖拽功能,可以使用oe_structure类。在本例中,我们在模板的顶部进行了添加,使用oe_structure将启用编辑和组件拖拽的支持。

如果想要对某些版块禁用网站编辑功能,那么你可以使用contenteditable=False属性。这会让元素变成只读。在第1步中,我们在最后一个

标签中使用这一属性。

ℹ️注意通过网站编辑器编辑视图会在视图中设置noupdate标记。这表示后续的代码修改不会在客户的数据库中体现。为了能轻松使用行内编辑并在随后的发版中更新HTML代码,创建一个包含HTML语法元素的视图以及另一个注入可编辑元素的视图。然后,仅后一个视图为noupdate,我们仍可对前一个视图进行修改。

对于这里使用的CSS类,请参见本节其它内容中Bootstrap文档的链接。

在第2步中,我们声明了渲染模板的路由。如果留心的话,会发现我们在route()中使用了website=True参数,它会在模板中传递一些额外的上下文,如菜单、公司等。这将会在website.layout中用于渲染菜单和底部。website=True参数还会在网站中启用多语言支持。它也以更好地方式显示异常。

在函数的最后,我们通过渲染模板返回了结果,然后我们传递了的所有在模板中使用的图书的记录集。对于更多有关更新已有路由的知识,请参见第十四章 网页服务端开发修改已有handler一节。

扩展知识…

要修改已有模板,我们可以在模板中使用inherit_id属性,然后使用像视图继承这样的xpath元素。例如,我们希望通过继承books模板来在Authors标签旁边显示作者的数量。可以通过如下的方式进行实现:

<template id="books_ids_inh" inherit_id="my_library.books">
  <xpath expr="//div[@class='card-body']/b" position="replace">
    <b class="mt8"> Authors (<t t-esc="len(book.author_ids)"/>)</b>
  </xpath>
</template>

模板继承和视图继承完全一样,因为在内部QWeb模板是一种类型为qweb的普通视图template元素是一种对记录设置某些属性的record元素的简写。虽然毫无理由要去放弃使用方便的template元素,但你应该知道底层发生了什么:该元素创建了一个带有qweb类型 ir.ui.view模型的记录。然后,根据template的元素名和inherit_id属性,会在视图记录中设置inherit_id字段。

其它内容

参见如下各点来有效设计QWeb模板:

  • Odoo整体大量地使用了Bootstrap,应当使用它来毫不费力地实现可适配的设计。
  • 有关视图继承的详情,请参见第十章 后端视图中的修改已有视图 - 视图继承一节。
  • 更多有关控制器的深入讨论,可参见第十四章 网页服务端开发中的让路径在网络中可访问限制网络可访问路径的访问两小节。

管理动态路由

在网站开发项目中,我们经常需要创建带有动态URL的页面。例如,在电商中,每个产品有一个带有不同URL的详情页。本例中,我们将创建一个显示图书详情的网页。

准备工作

我们将使用前一节中的my_library模块。要让图书详情页看起来美观,我们需要添加一些新字段。请像下面这样在library.book中添加两个新字段并形成一个视图:

class LibraryBook(models.Model):
  _name = 'library.book'
  
  name = fields.Char('Title', required=True)
  date_release = fields.Date('Release Date')
  author_ids = fields.Many2many('res.partner', string='Authors')
  image = fields.Binary(attachment=True)
  html_description = fields.Html()

如何实现…

按照这些步骤来为图书生成一个详情页面:

  1. 在main.py中为图书详情添加一个新路径,如下:

    @http.route('/books/<model("library.book"):book>', type='http', auth="user", website=True)
    def library_book_detail(self, book):
      return request.render(
        'my_library.book_detail', {
          'book': book,
        })
    
  2. 在templates.xml中为图书详情添加一个新模板,如下:

    <template id="book_detail" name="Books Detail">
     <t t-call="website.layout">
      <div class="container">
        <div class="row mt16">
          <div class="col-5">
            <span t-field="book.image" t-options="{
              'widget': 'image',
              'class': 'mx-auto d-block imgthumbnail'}"/>
          </div>
          <div class="offset-1 col-6">
            <h1 t-field="book.name"/>
            <t t-if="book.date_release">
              <div t-field="book.date_release" class="text-muted"/>
            </t>
            <b class="mt8"> Authors </b>
            <ul>
              <li t-foreach="book.author_ids" tas="author">
                <span t-esc="author.name" />
              </li>
            </ul>
          </div>
        </div>
      </div>
      <div t-field="book.html_description"/>
     </t>
    </template>
    
  3. 在图书列表模板中添加一个按钮如下。这个按钮会重定向到图书详情网页:

    ...
    <div t-attf-class="card mt24 #{'bg-light' if book_odd else ''}">
      <div class="card-body">
        <h3 t-field="book.name"/>
        <t t-if="book.date_release">
          <div t-field="book.date_release" class="textmuted"/>
        </t>
        <b class="mt8"> Authors </b>
        <ul>
          <li t-foreach="book.author_ids" t-as="author">
            <span t-esc="author.name" />
          </li>
        </ul>
        <a t-attf-href="/books/#{book.id}" class="btn btnprimary btn-sm">
          <i class="fa fa-book"/> Book Detail
        </a>
      </div>
    </div>
    ...
    

运行原理…

在第1步中,我们为图书详情页创建了一个动态路由。在这个路由中添加了<model(“library.book”)

>。它接收带有整数的URL,如/books/1。Odoo会把这个整数看作library.book模型的ID,并在访问这个URL时,Odoo获取一个记录集并将其以参数传递给函数。因此,在浏览器中访问/books/1时,library_book_detail()函数中的book参数会有一个ID为1的library.book模型的记录集。我们传递了这个图书记录集并渲染了一个名为my_library.book_detail.</model(“library.book”)
>的新模板。

在第2步中,我们新建了一个名为book_detail的QWeb模板来渲染图书详情页。这很简单,通过Bootstrap结构来进行创建。查看页面,我们在详情页中添加了html_description。html_description字段的字段类型为HTML,因此可在该字段中存储HTML数据。Odoo对HTML类型的字段自动添加组件拖拽的支持。因此,现在我们可以在图书详情页中使用组件了。拖拽到HTML字段中的组件存储于图书记录中,因此可以为不同图书设计不同的内容。

最后一步中,我们添加了一个带有a 标签的链接,这样可以将访客重定向到图书详情页中。

扩展知识…

Odoo使werkzeug来处理HTTP请求。Odoo对werkzeug添加了很轻的封装来轻松地处理路由。在最一个示例的<model(“library.book”)

>路由中可以看到。这是Odoo自己的实现,但它还支持werkzeug路由中的所有其它功能。因此,你可以使用这样的路由:

  • /page/接收一个整数
  • /page/<any(about, help)
    ="">接收给定的值
  • /pages/接收字符串
  • /pages//int:page接收多个值

对于路由有大量可用的变体,可以参见http://werkzeug.pocoo.org/docs/0.14/routing/

为用户提供小组件

网站设计器在网站编辑模式中提供了构造板块,可拖拽至页面中。本节我们将讲解如何创建自己的板块,内部称之为组件(snippet)。

准备工作

本节我们将使用前一节中的my_library模块。

如何实现…

组件实际上只是一个注入到Insert板块栏中的一个QWeb视图,它由QWeb视图自己定义。按照如下步骤进行操作:

  1. 添加一个名为views/snippets.xml的文件,如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <odoo>
    
    <!-- Assets for JS file added step 4 -->
    <template id="assets_frontend" inherit_id="web.assets_frontend">
      <xpath expr="." position="inside">
        <script src="/my_library/static/src/js/snippets.js" type="text/JavaScript" />
      </xpath>
    </template>
    
    <!-- 第2、第3步代码 -->
    </odoo>
    
  2. 为你的组件添加模板,如下:

    <template id="book_snippet">
      <section class="book_list">
        <div class="container">
          <h2>Latest books</h2>
          <table class="table book_snippet">
            <tr>
              <th>Name</th>
              <th>Release date</th>
            </tr>
          </table>
        </div>
      </section>
    </template>
    
  3. 继承组件模板并添加组件和选项,如下:

    <template id="book_snippets_options" inherit_id="website.snippets">
      <xpath
        expr="//div[@id='snippet_feature']/div[hasclass('o_panel_body'
        )]" position="inside">
        <t t-snippet="my_library.book_snippet"
          tthumbnail="/my_library/static/description/icon.png"/>
      </xpath>
      <xpath expr="//div[@id='snippet_options']" position="inside">
        <!-- 在这里添加组件选项 -->
      </xpath>
    </template>
    
  4. 在已继承的组件模板中添加组件选项,如下:

    <div data-js="book_count" data-selector="section.book_list">
      <div class="dropdown-submenu">
        <a tabindex="-2" href="#" class="dropdown-item">
          <i class="fa fa-book"/> Number of books
        </a>
        <div class="dropdown-menu" role="menu">
          <a href="#" class="dropdown-item" data-selectcount="3"> 3 </a>
          <a href="#" class="dropdown-item" data-selectcount="5"> 5 </a>
          <a href="#" class="dropdown-item" data-selectcount="10"> 10 </a>
          <a href="#" class="dropdown-item" data-selectcount="15"> 15 </a>
        </div>
      </div>
    </div>
    
    <div data-selector=".book_snippet">
      <div class="dropdown-submenu">
        <a tabindex="-2" href="#" class="dropdown-item">
          <i class="fa fa-columns"/> Table Style
        </a>
        <div class="dropdown-menu" role="menu">
          <a href="#" class="dropdown-item" data-toggleclass="table-bordered">
            Bordered
          </a>
          <a href="#" class="dropdown-item" data-toggleclass="table-dark">
            Dark
          </a>
          <a href="#" class="dropdown-item" data-toggleclass="table-striped">
            Striped
          </a>
        </div>
      </div>
    </div>
    
  5. 新增文件/my_library/static/src/js/snippets.js并添加JavaScript代码来调用脚本,如下:

    odoo.define('my_library.snippets', function (require) {
      "use strict";
      var rpc = require('web.rpc');
      var Animation =
      require('website.content.snippets.animation');
      var options = require('web_editor.snippets.options');
      // 此处添加组件选项及avaScript动画
    });
    
  6. 添加如下选项来决定我们希望在组件中显示多少本书:

    options.registry.book_count = options.Class.extend({
      selectCount: function (previewMode, value, $opt) {
        var table = this.$target.find('table');
        var oldClass = table.attr('class');
        var newTable = $('<table><tr><th>Name</th><th>Release date</th></tr></table>');
        newTable.attr('class', oldClass);
        newTable.attr('data-rows', value);
        table.replaceWith(newTable);
        this._refreshAnimations();
      }
    });
    
  7. 添加动画来获取图书数据并在页面中显示,如下:

    Animation.registry.book_snippet = Animation.Class.extend({
      selector: '.book_snippet',
        start: function () {
        var self = this;
        var rows = this.$el.data().rows || 5;
        this.$el.find('td').parents('tr').remove();
        rpc.query({
          model: 'library.book', method: 'search_read',
          domain: [], fields: ['name', 'date_release'],
          orderBy: [{name: 'date_release', asc: false}],
          limit: rows,
        }).then(function (data) {
          _.each(data, function (book) {
            self.$el.append(
              $('<tr />').append(
              $('<td />').text(book.name),
              $('<td />').text(book.date_release)
            ));
          });
        });
      },
    });
    

在更新该模块后,我们将获得一个名为Latest books的新组件,它具有修改近期添加图书数量的功能。我们还添加了修改表格设计的功能,在点击表格时会进行显示。

运行原理…

在第1步中,我们添加了一个新的XML文件snipptes.xml,并在资源中添加一个JS文件。组件仅是不带有website.layout的QWeb模板。

在第2步中,我们为图书创建了book_snippet模板。总之使用section元素和Bootstrap类是一个很好的想法,因为对于它们Odoo的编辑器默认提供了编辑、背景和重置大小的控制。在组件体中,我们只是添加了一个标题和带有头部的表格。本例中我们希望显示最新的图书。最新图书的列表并不是固定的,它会在每次新图书发布时发生改变。因此,我们希望补时向表格中插入图书详情。在下面几步中,我们将添加JavaScript来获取最新的图书列表并将它们添加到表格中。

在第3步中,我们继承了website.snippet 模板来添加我们的组件及其功能。第一个xpath在主组件区中添加了book_snippet。我们需要使用t-snippet属性来添加该组件。还需要添加t-thumbnail属性,它是针对该组件的缩略图URL。position属性用于决定在哪个区中显示该组件。我们选择的是 //div[@id=‘snippet_feature’]/div[@class=‘o_panel_body’],将其放在主区域中。通过snippet_structure, snippet_content和snippet_effect的ID,我们可以在其它各自的区域中放置组件。

在第2个xpath中,我们添加了针对组件的功能。通过第4步在这个xpath中添加了两个选项。第一个选项是选择图书的数量来在表格中显示。组件的选择有不同的类型。在本例中,我们使用了带有data-js=“book_count”的自定义选项。在使用data-js选项时,我们需要在options.registry中使用属性值来注册它。在用户修改该选项时,会调用已注册选项中的函数并且然后我们需要对元素设置选项值。这些值在页面重新加载时由渲染函数(Odoo动画框架)使用。

在第6步中可进行检查,这里我们添加了options.registry.book_count这样的选项。在函数体中,我们在this.$target中获取了一个组件元素,函数体中的其它内容是基本JQuery语法进行编写。

第4步中我们还添加了另一个组件选项,用于修改表格样式。对这个选项我们使用了data-toggle-class属性。在用户点击带有data-toggle-class属性的选项时,Odoo会切的一个在属性值中指定了的类。对于这种类型的选项无需要使用JavaScript代码。Odoo中可以使用的另一种选项类型称为data-select-class,它一次仅启用一个类。本例中我们没有进行使用,读者可以自己进行测试。

如果留心,会发现data-selector attribute中包含一个决定显示选项中哪个元素的JQuery选择器。本例中,第一个选项列表在选取了整个容器时显示,表格样式在选择时表格时显示。

在第6和第7步中,我们在Animation.registry中添加了book_snippet,它会从数据库中获取图书数据并在组件体中添加表格的行。它使用组件动画框架来在每次加载组件时执行代码。我们使用它来查询展示给用户的当前图书列表。这里的关键属性是所定义的selector,它告诉框架在有元素匹配该选择器时运行我们的类。内部我们使用data-rows选项,从options.registry.book_count中添加来决定多少行需要被显示。

本节中的示例用于创建动态组件。如果你没有对动态组件的需要,仅仅是想要添加静态内容,那么可以直接在组件中添加所有内容。无需为静态组件添加JavaScript。

**小贴士:**在前例中,我们创建了一个动态组件。如果没有对如果你没有对动态组件的需要,仅仅是想要添加静态内容,那么可以直接在组件中添加所有内容。在这种情况下,没有必要添加额外的JavaScript代码。

扩展知识…

在这种情况下,无需额外的JavaScript代码。Odoo的编辑器默认提供大量的选项及控制,对于静态是远远足够用了。在website/views/snippets.xml中可以看到所有的已已有组件及选项。

组件选项还支持data-exclude , data-drop-near和data-drop-in属性,它们决定在从组件栏中拖拽时组件放在何处。它们也是JQuery选择器,在本节的第3步中,我们没有使用到它们,因为我们允许将该组件放在可以放内容的任何区域。

从网站用户获取输入

在网站开发中,你经常会需要创建表单来从网站用户(访客)接收输入的数据。本节中,我们将在页面中创建一个HTML表单来报告与图书相关的问题。

准备工作

本节中我们将使用前一小节中的my_library模块。我们需要新建一个模型来存储用户所提交的问题。

因此,在开始本节之前,修改之前的代码。在 library.book模型中添加一个字段及新的book.issues模型,如下:

class LibraryBook(models.Model):
  _name = 'library.book'

  name = fields.Char('Title', required=True)
  date_release = fields.Date('Release Date')
  author_ids = fields.Many2many('res.partner', string='Authors')
  image = fields.Binary(attachment=True)
  html_description = fields.Html()
  book_issue_id = fields.One2many('book.issue', 'book_id')


class LibraryBookIssues(models.Model):
  _name = 'book.issue'
  
  book_id = fields.Many2one('library.book', required=True)
  submitted_by = fields.Many2one('res.users')
  isuue_description = fields.Text()

在图书的表单视图中添加book_issues_id字段,如下:

...
<group string="Book Issues">
  <field name="book_issue_id" nolabel="1">
    <tree>
      <field name="create_date"/>
      <field name="submitted_by"/>
      <field name="isuue_description"/>
    </tree>
  </field>
</group>
...

在ir.model.access.csv文件中为新的 book.issue模型添加访问权限,如下:

acl_book_issues,library.book_issue,model_book_issue,group_librarian,1,1,1,1

我们为图书问题添加了一个新模型,现在我们将添加一个带有HTML表单的新模板。

如何实现…

按照如下步骤来为问题页面创建一个新路由及模板页面:

  1. 在main.py中添加一个新路由,如下:

    @http.route('/books/submit_issues', type='http', auth="user", website=True)
    def books_issues(self, **post):
      if post.get('book_id'):
        book_id = int(post.get('book_id'))
        issue_description = post.get('issue_description')
        request.env['book.issue'].sudo().create({
          'book_id': book_id,
          'issue_description': issue_description,
          'submitted_by': request.env.user.id
        })
        return request.redirect('/books/submit_issues?submitted=1')
    
      return request.render('my_library.books_issue_form', {
        'books': request.env['library.book'].search([]),
        'submitted': post.get('submitted', False)
      })
    
  2. 在其中添加一个带有HTML表单的模板,如下:

    <template id="books_issue_form" name="Book Issues Form">
      <t t-call="website.layout">
        <div class="container mt32">
          <!-- 此处添加页面元素 -->
        </div>
      </t>
    </template>
    
  3. 为该页面添加条件头部,如下:

    <t t-if="submitted">
      <h3 class="alert alert-success mt16 mb16">
        <i class="fa fa-thumbs-up"/>
        Book submitted successfully
      </h3>
      <h1> Report the another book issue </h1>
    </t>
    <t t-else="">
      <h1> Report the book issue </h1>
    </t>
    
  4. 来提交问题,如下:

    <div class="row mt16">
      <div class="col-6">
        <form method="post">
          <input type="hidden" name="csrf_token"
            t-att-value="request.csrf_token()"/>
          <div class="form-group">
            <label>Select Book</label>
            <select class="form-control" name="book_id">
              <t t-foreach="books" t-as="book">
                <option t-att-value="book.id">
                <t t-esc="book.name"/>
                </option>
              </t>
            </select>
          </div>
          <div class="form-group">
            <label>Issue Description</label>
            <textarea name="issue_description"
              class="form-control"
              placeholder="e.g. pages are missing"/>
          </div>
          <button type="submit" class="btn btn-primary">
            Submit
          </button>
        </form>
      </div>
     </div>
    

更新该模块并打开链接/books/submit_issues。在这个页面中,你将可以对图书提交问题。在提交之后,可以在后台中对应的图书表单视图中进行检查。

运行原理…

在本节的第1步中,我们创建了一个用于提交图书问题的路径。函数中的post参数将接收URL中的所有查询参数。你还将在post参数中获取到所提交的表单数据。本例中,我们使用了相同的控制器来显示页面及提交问题。如果我们在post中发现了数据,将会在book.issue模型中新建一条问题,然后使用所提交的query参数来重定向到问题页面,这样用户可以看到问题已提交并且可以提交其它问题。

注意我们使用了sudo()来创建图书问题记录,因为普通用户(访客)没有权限创建新的图书问题记录。在用户通过网页提交问题时需要创建图书问题记录。这是一个sudo()用法的实际示例。

在第2步中,我们创建为问题页面创建了一个模板。在第3步中,我们添加了条件头部。成功的头部会在提交问题之后显示。

在第4步中,我们添加带有3个字段的:csrf_token, book selection 和问题描述。后两个字段用于从网站用户获取输入。但csrf_token用于避免跨站请求伪造(CSRF)功能。如果你不在表单中使用它,用户将无法提交表单。在提交表单时,你将会在第1步books_issues()方法中的**post参数中获取所提交的数据。

ℹ️在某些情况下,如果你希望禁用csrf验证,可以在路由中使用csrf=False,类似这样:@http.route(‘/url’, type=‘http’,auth=“user”, website=True, csrf=False )。

扩展知识…

如果希望,你可以使用单独的路由页面并且对于post数据,你可以在表单中添加动作如下:

...
<form action="/my_url" method="post">
...

此外,你可以通过在路由中添加method参数来限制get请求,如下:

@http.route('/my_url', type='http', method='POST' auth="user", website=True)

管理搜索引擎优化(SEO)选项

Odoo为模板(页面)提供了内置的SEO支持。但是,有些模板在多个URL中使用。例如,在线商店中,产品页面使用相同模板和不同产品数据来进行渲染。对于这种用例,我们希望为每个URL分离SEO选项。

准备工作

本节中我们使用前一小节中的my_library模块。我们将为每个图书详情页面分别存储SEO数据。在进行本节的开发之前,应当在不同的图书页面中测试SEO选项。你可以从顶部的Promote下拉菜单中获取SEO对话框,如以下图片所示:

TODO

如果你在不同的图书详情页中测试SEO选项,会注意到在一个图书页面中修改SEO数据会反映到所有的图书页面中。我们将在本节中修复这一问题。

如何实现…

按照如下步骤来管理每本书各自的SEO选项:

  1. 在library.book模型中继承SEO元数据mixin ,如下:

    ...
    class LibraryBook(models.Model):
      _name = 'library.book'
      _inherit = ['website.seo.metadata']
    
      name = fields.Char('Title', required=True)
      date_release = fields.Date('Release Date')
    ...
    
  2. 在图书详情路由中以main_object传递图书对象,如下:

    ...
    @http.route('/books/<model("library.book"):book>',
    type='http', auth="user", website=True)
    def library_book_detail(self, book):
      return request.render(
        'my_library.book_detail', {
          'book': book,
          'main_object': book
        })
    ...
    

更新模块并在不同的图书页面上修改SEO。它可以通过Optimize SEO选项来进行修改。下面,你就能够按书管理各个的SEO详情了。

运行原理…

要对模型的每条记录启用SEO,你需要在模型中继承website.seo.metadata mixin。这将在library.book模型中添加一些新字段和方法。这些在网站中字段和方法将用于为每本书存储各自的数据。

ℹ️如果你想要查看针对SEO mixin的字段和方法,在/addons/website/models/website.py文件中搜索website.seo.metadata模型。

所有SEO相关的代码在website.layout中编写,它从以main_object传递的数据集中获取所有SEO元信息。因此,在第2步中,我们传递了一个带有main_object键的图书对象,这样网站布局将从图书中获取所有的SEO的信息。如果不通过控制器传递main_object,那么模板记录集将以main_object进行传递,这就是你在所有图书中获取到了相同SEO数据的原因。

扩展知识…

Odoo 12添加了对Open Graph和Twitter分享meta标签的支持。如果你希望在页面中添加自己的自定义meta标签,可以在添加SEO mixin后重载_default_website_meta() 。

管理网站的站点地图

一个网站的站点地图对于任何网站都非常关键。搜索引擎将使用网站的站点地图来索引所有网站的页面。在本节中,我们将向站点地图添加所有的图书详情页。

准备工作

本节中我们将使用前一节的my_library模块。如果你想要查看当前Odoo中的站点地图,可在浏览器中打开/sitemap.xml。其中没有图书的 URL。

如何实现…

按照如下步骤来向sitemap.xml添加图书页面:

  1. 在main.py中导入给定的方法,如下:

    from odoo.addons.http_routing.models.ir_http import slug
    from odoo.addons.website.models.ir_http import sitemap_qs2dom
    
  2. 在 main.py中添加sitemap_books方法如下:

    class Main(http.Controller):
    ...
      def sitemap_books(env, rule, qs):
        Books = env['library.book']
        dom = sitemap_qs2dom(qs, '/books', Books._rec_name)
        for f in Books.search(dom):
          loc = '/books/%s' % slug(f)
          if not qs or qs.lower() in loc:
            yield {'loc': loc}
    
  3. 在图书的详情路由中添加sitemap_books函数引用如下:

    ...
    @http.route('/books/<model("library.book"):book>',
    type='http', auth="user", website=True,
    sitemap=sitemap_books)
      def library_book_detail(self, book):
    ...
    

更新模块来应用修改。sitemap.xml已生成并在附件中存储。然后每隔几小时就会重新生成一次。要查看我们的修改,需要从附件中删除掉站点地图文件。访问Settings > Technical > Database Structure > Attachments并搜索这个站点地图,并删除该文件。此时,在浏览器中访问链接/sitemap.xml,会在站点地图中看到图书的页面。

运行原理…

在第1步中,我们导入一些所需的函数。slug用于根据记录名生成整洁、用户友好的URL。sitemap_qs2dom用于根据路由和查询字符生成域。

在第2步中,我们创建了Python生成器函数sitemap_books()。这个函数在生成站点地图时调用。调用时它会接收3个参数 - env Odoo环境、rule路由规则和qs查询字符串。在该函数中,我们通过sitemap_qs2dom生成了一个域。然后我们使用所生成的域来搜索图书记录,用于通过slug()方法生成位置。通过slug,我们将获取到一个用户友好的URL,如/books/odoo-12-development-cookbook-1而非books/1。

在第3步中,我们传递了sitemap_books()的函数引用给带有关键词站点地图的路由。

获取访客的国家信息

Odoo CMS有对GeoIP的内置支持。在线上环境中,你可以根据IP来追踪访问的国家。本节中我们将根据访客的IP地址来获取访客的国家。

准备工作

本节我们将使用前一节的my_library模块。本节中,我们将根据访客的国家来在网页中隐藏一些图书。完成本节你需要下载GeoIP数据库。然后需要通过cli选项传递数据库的位置,如下:

./odoo-bin -c config_file --geoip-db=location_of_geoip_DB

如何实现…

按照以下步骤来根据国家进行图书限制:

  1. 在library.book模型中添加restrict_country_ids多对多字段,如下:

    class LibraryBook(models.Model):
      _name = 'library.book'
      _inherit = ['website.seo.metadata']  
      
      ...
      restrict_country_ids = fields.Many2many('res.country')
      ...
    
  2. 在library.books模型的表单视图中添加restrict_country_ids字段,如下:

    ...
    <group>
      <field name="date_release"/>
      <field name="restrict_country_ids"
        widget="many2many_tags"/>
    </group>
    ...
    
  3. 更新/books控制器来根据国家限制图书,如下:

    @http.route('/books', type='http', auth="user", website=True)
    def library_books(self):
        country_id = False
        country_code = request.session.geoip and request.session.geoip.get('country_code') or False
        if country_code:
            country_ids = request.env['res.country'].sudo().search([('code', '=', country_code)])
            if country_ids:
                country_id = country_ids[0].id
        domain = ['|', ('restrict_country_ids', '=', False), ('restrict_country_ids', 'not in', [country_id])]
        return request.render(
            'my_library.books', {
                'books': request.env['library.book'].search(domain),
            })
    

更新模块来应用修改。在图书的受限国家字段中添加国家并访问/book。此时不会在列表中显示受限的图书。

ℹ️**警告:**本节无法使用本地服务器。它要求一个托管的主机,因为在本地机器上,你将获取到本地IP,而它不与任何国家关联。

你将需要正确地配置Nginx。

运行原理…

在第1步中,我们在library.book模型中添加了一个新的restricted_country_ids多对多类型字段。如果网站访客来自受限国家的话将隐藏该图书。

在第2步中,我们在图书的表单视图中添加了restricted_country_ids字段。如果正确地配置了GeoIP和NGINX,Odoo会对request.session.geoip添加GeoIP信息,然后你可以通过它获取到国家代码。

在第3步中,我们从GeoIP获取到了国家代码,紧接着根据country_code获取到国家记录集。在获取到访客的国家信息后,我们根据一个受限国家通过域过滤了图书。

**小贴士:**如果你没有真实的服务器,又想要以这种方式进行测试,可以在控制器中添加一个默认国家代码,像这样:country_code = request.session.geoip and request.session.geoip.get(‘country_code’) or ‘IN’

追踪营销活动

在任何业务或服务中,熟悉投资回报率(ROI)都非常的重要。ROI用于评估投资的有效性。广告的花费可通过Urchin Tracking Module (UTM)代码进行追踪。UTM代码是一个可以添加到URL中的字符串。UTM代码可以帮助你追踪活动、来源和媒介。

准备工作

本节我们将使用前一节中的my_library模块。Odoo有一个针对UTM的内置支持。通过我们的图书馆应用,我们没有可以使用UTM的任意实际用例。但是在本节中,我们将在my_library的/books/submit_issues中所产生的问题内添加UTM。

如何实现…

按照这些步骤来在/books/submit_issues URL上的网页中生成的图书问题来链接UTM:

  1. 在manifest.py的depends小节中添加utm 模块,如下:

    'depends': ['base', 'website', 'utm'],
    
  2. 在book.issue模型中继承utm.mixin如下:

    class LibraryBookIssues(models.Model):
      _name = 'book.issue'
      _inherit = ['utm.mixin']
      
      book_id = fields.Many2one('library.book', required=True)
      submitted_by = fields.Many2one('res.users')
      issue_description = fields.Text()
    
  3. 在book_issue_ids字段的树状视图中添加campaign_id字段,如下:

    ...
    <group string="Book Issues">
      <field name="book_issue_ids" nolabel="1">
        <tree name="Book isuues">
          <field name="create_date"/>
          <field name="submitted_by"/>
          <field name="issue_description"/>
          <field name="campaign_id"/>
        </tree>
      </field>
    </group>
    ...
    

更新该模块来应用修改。我们需要执行如下步骤来测试UTM:

  • 在Odoo中,UTM根据cookie来进行处理,并且一些浏览器不支持localhost的cookie,因此如果你是通过localhost来进行测试的话,通过http://127.0.0.1:8069来访问该实例。
  • 默认,UTM追踪是屏蔽销售人员的。因此,要测试UTM功能,你需要使用门户用户来进行登录。
  • 现在,打开像http://127.0.0.1:8069/books/submit_issues?utm_campaign=sale这样的URL。
  • 提交图书问题并查看后台中的图书问题。它会在图书的表单视图中显示活动。

运行原理…

在第1步中,我们在book.issue模型中继承了utm.mixin。它会在book.issue模型中添加了如下字段:

  • campaign_id:utm.campaign模型的Many2one字段。它用于追踪不同的活动,如夏季和圣诞特价
  • source_id:utm.source model.的Many2one字段。它用于追踪不同的来源,如搜索引擎和其它域名。
  • medium_id:utm.medium 模型的Many2one字段。它用于追踪不同的媒介,如贺卡、邮件或横幅广告。

要追踪活动、媒介和来源,你需要在营销媒体中分享这样的URL:your_url?utm_campaign=campaign_name&utm_medium=medium_name&utm_source=source_name。

如果访客通过任意营销媒体访问你的网站,那么source_id和medium_id字段会在网页上创建记录时自动填写。

在示例中,我们追踪了campaign_id,但是你还可以添加source_id和medium_id。

ℹ️**注意:**在测试示例中,我们使用了campaign_id=sale。sale是在model utm.campaign中的记录名。默认utm模块添加一些活动、媒介和来源的记录。记录sale是其中之一。如果你想要创建一个新的活动、媒介或来源,可以通过在开发模式下的Tracker > UTMs菜单中访问该链接来实现。

管理多站点

在版本12中,Odoo添加了对多网站的支持。这意味着相同的Odoo实例可在多个域名上运行,并且可显示不同的记录。

准备工作

本节我们将使用前一小节中的my_library模块。在这一节中,我们将根据网站来隐藏图书。

如何实现…

按照如下步骤来让在线的网站 - 多网站兼容:

  1. 在library.book模型中添加website.multi.mixin如下:

    class LibraryBook(models.Model):
      _name = 'library.book'
      _inherit = ['website.seo.metadata', 'website.multi.mixin']
    ...
    
  2. 在图书的表单视图中添加website_id,如下:

    ...
    <group>
        <field name="author_ids" widget="many2many_tags"/>
        <field name="website_id"/>
    </group>
    ...
    
  3. 在/books 控制器修改域名下:

    @http.route('/books', type='http', auth="user", website=True)
    def library_books(self, **post):
    ...
      domain = ['|', ('restrict_country_ids', '=', False),
      ('restrict_country_ids', 'not in', [country_id])]
      domain += request.website.website_domain()
      return request.render(
        'my_library.books', {
          'books': request.env['library.book'].search(domain),
      })
    ...
    
  4. 导入werkzeug并修改一个图书详情控制器来从另一个网站中限制图书的访问,如下:

    import werkzeug
    ...
    @http.route('/books/<model("library.book"):book>',
    type='http', auth="user", website=True, sitemap=sitemap_books)
    def library_book_detail(self, book, **post):
      if not book.can_access_from_current_website():
        raise werkzeug.exceptions.NotFound()
      return request.render(
        'my_library.book_detail', {
          'book': book,
          'main_object': book
        })
    ...
    

更新模块来应用修改。要进行模块测试,需在某些书中设置不同的网站。现在,打开链接/books并查看图书的列表。然后,修改网站 并查看图书的列表。为进行测试,我们可以通过网站的下拉菜单切换器来更改网站。参见下图来实现:

TODO

你也可以尝试直接通过URL访问图书详情,如 /books/1。如果书不是来自该网站的话,它会显示为404。

运行原理…

在第1步中,我们添加了website.multi.mixin。这个mixin添加了一个基本工具来在模型中处理多网站。这个mixin在模型中添加了website_id字段。该字段用于决定记录是针对哪个网站的。

在第2步中,我们在图书的表单视图中添加了website_id字段,因此该书可以根据网站来进行过滤。

在第3步中,我们修改了用于查找图书列表的域名,equest.website.website_domain()将返回域名并过滤出不是来自该网站的图书。

ℹ️注意有未设置website_id的记录。这些记录会在所有网站中显示。这表示如果你在指定图书中没有website_id字段,那么这本书会在所有网站中显示。

然后我们在网页搜索中添加了该域名,如下:

  • 在第4步中,我们限制了图书的访问。如果图书不是针对当前网站的话,那么我们会抛出not found错误。如果图书记录针对当前活跃网站的话方法can_access_from_current_website会返回值True,而针对另一个网站时返回False。
  • 如果查看的话,我们在两个控制器中都添加了**post。这是因为没有它,post /books and /books/不会接收查询参数。它会在通过网站切换器切换网站时产生报错,因此我们进行了添加。正常在每个控制器中添加post是一个良好实践,这样它可以处理查询参数。