全书完整目录请见:Odoo 14开发者指南(Cookbook)第四版
Odoo自带完整功能的内容管理系统(CMS)。通过拖拽终端用户可以在几分钟内设计出一个页面,但在Odoo的CMS系统中开发新功能或构建功能块就不是那么简单了。本章将会探讨Odoo前端部分。我们将学习如何创建网页。还会学习如何创建用户可拖放到页面的功能块。本章还会涉及到一些高阶内容,比如Urchin追踪模块 (UTM)、搜索引擎优化 (SEO)、多站点、GeoIP和网站地图。简言之,读者可以学习到所有开发互动式网站的知识。
📝重要信息:所有的Odoo CMS功能均由website和web_editor模块实现。如果想要学习CMS底层的运行原理,请查阅这两个模块。
本章中,我们将讲解如下小节:
- 管理静态资源
- 为网站扩展CSS和JavaScript
- 创建或更改模板 – QWeb
- 管理动态路由
- 为用户提供静态小组件
- 为用户提供动态小组件
- 获取网站用户的输入
- 管理搜索引擎优化(SEO)选项
- 管理网站的站点地图
- 获取访客的国家信息
- 追踪营销活动
- 管理多站点
- 重定向老URL
- 网站相关记录的发布管理
管理静态资源
现代网站包含大量的JavaScript和CSS文件。当页面在浏览器中加载时,这些静态文件对服务端发送单独的请求。请求数量越多,网站的速度就越慢。为避免这一问题,大部分网站通过合并多文件返回静态资源。市面上有很多工具用于进行此类管理,但是Odoo对于管理静态资源有其自己的实现方式。
什么是资源包以及Odoo中资源的区别?
在Odoo中,静态资源的管理和在其它应用中的管理一样简单。Odoo有大量不同的应用和代码。不同的Odoo应用有不同的作用和 UI 界面。这些应用并不使用相同的代码,因此有时候我们希望加载一部分资源,但不并所有的时候都加载。在页面中加载不必要的静态资源并不是个良好实践。为避免在应用中加载额外的资源,Odoo采用了资源包的概念。资源包的任务是将所有JavaScript和CSS合并为一个文件,通过最小化减小其大小。在Odoo 的代码中存在多个资源包,不同的代码集拥有不同的资源包。
以下是在Odoo中使用的各种资源包:
- web.assets_common:这个资源包包含对所有应用通用的基本工具文件,如JQurey, Underscore.js, FontAwesome等等。此资源包用于前台(网站)、后台、销售点(POS)和报表等处。这一通用资源在Odoo的几乎所有地方加载。它也包含用于Odoo模块系统的boot.js文件。
- web.assets_backend:这一资源包在Odoo的后台中使用(ERP部分)。它包含所有与web客户端、视图、字段微件、动作管理器等相关的代码。
- web.assets_frontend或website.assets_frontend::这一资源包用于Odoo的前台(网站部分)。它包含所有与网站端应用相关的代码,如电商、博客、线上活动、论坛和在线聊天等等。注意这个资源包不包含与网站编辑和拖拽功能(网站构造器)相关的代码。这背后的原因是我们不希望在公众使用网站时加载编辑器资源。
- web_editor.assets_editor和web_editor.summernote:这个资源包包含与网站编辑小组件选项及拖拽功能(网站构造器)相关的代码。它仅在用户对网站具有编辑权限时才进行加载。也用于批量邮件设计工具。
- web.report_assets_common: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文件,它们会自动编译并添加到资源包中。
自定义资源
如我们所见,Odoo不同的代码集拥有不同的资源。要获取适当的结果,我们需要选择正确的资源包并放入自定义JavaScript和CSS文件。例如,如果你在设计一个网站,则需将文件放入web.assets_frontend中。虽然很少见,但有时我们需要创建全新的资源包。在下一部分中就会讲到。
如何实现…
按照如下步骤来创建一个自定义资源包:
- 创建QWeb模板并添加JavaScript, CSS或SCSS文件如下:
12345<template id="my_custom_assets" name="My Custom Assets"><link rel="stylesheet" type="text/scss" href="/my_library/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> - 在想要加载这个包的QWeb模板中使用t-call-assets如下:
1234567<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>...
译者注:以上代码仅用于说明概念,读者可自行在 my_library 模块进行对照添加或在学习下一节时再进行实操
运行原理…
在第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非常的困难,因为AssetBundle将多个JavaScript文件合并成了单个文件并进行了最小化。通过启用带资源的开发者模式,可以跳过资源打包,页面中会独立加载各个静态资源,这样就可以轻松地进行调试了。
合并资源进行一次生成并存储在ir.attachment模型中。之后,从附件中进行调用。如果希望重新生成资源,可以通过如下图所示的调试功能完成:
图14.1 – 重新生成资源的选项
📝小贴士:如你所知,Odoo中资源仅会生成一次。这种行为对于开发阶段来说相当头疼,因为这时会需要进行频繁的服务端重启。要解决这一问题,我们可以在命令行中使用dev=xml,这样会直接加载资源,就无需再重启服务了。
下一节中我们将学习如何在已有资源包中包含自定义的CSS/JavaScript。
为网站扩展CSS和JavaScript
本节中,我们将讲解如何在网站中添加自定义样式表和JavaScript。
准备工作
本节我们将使用第三章 创建Odoo插件模块中的my_library模块。读者可以使用GitHub 仓库中的初始模块。我们会添加 CSS, SCSS和 JavaScript 文件,这些会修改前台网站。因为我们要修改网站,因此要在依赖中添加website。修改声明文件如下:
1 2 3 |
... 'depends': ['base', 'website'], ... |
如何实现…
重载主网站模板来注入我们的代码,如下:
- 添加文件views/templates.xml并添加一个空的视图重载,如下(别忘了在__manifest__.py中添加该文件):
1234567<odoo><template id="assets_frontend" inherit_id="web.assets_frontend"><xpath expr="." position="inside"><!-- 第2和第3步代码在这里添加 /--></xpath></template></odoo> - 添加对CSS和SCSS文件的引用如下:
12<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"/> - 添加对JavaScript文件的引用如下:
1<script src="/my_library/static/src/js/my_library.js" type="text/JavaScript" /> - 在static/src/css/my_library.css中添加一些CSS代码,如下:
123body main {background: #b9ced8;} - 在static/src/scss/my_library.scss中添加一些SCSS代码,如下:
12345678910111213$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;} - 在static/src/js/my_library.js文件中添加一些JavaScript代码,如下:
1234567odoo.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消息,如下图所示:
运行原理…
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,仅仅是在页面加载时显示alert信息。为避免JavaScript的排序问题,Odoo使用了类似RequireJS的机制。在我们的JavaScript文件中,调用了odoo.define(),需要两个参数:希望定义的命名空间和包含实际实现的函数。如果开发大量使用JavaScript的复杂产品,可以按逻辑将代码分割成不同部分并在不同的函数中进行定义。这非常有用,因为可以通过require进行导入有复用这些函数。同时,可定义模块的命令空间、添加插件名,将其加到前面、用点号分隔以避免未来的命名冲突。这是web模块的作用,它定义了web.core和web.data等。
通过第二个参数,定义函数仅接收一个参数require,它是用于获取对其它模块中定义的JavaScript命名空间引用的函数 。使用它进行Odoo的所有交互 ,并且从不依赖于全局odoo对象。
你自己的函数可以返回指向想要在其它模块中可引用的对象或者在没有引用时不返回任何内容。如果从你的函数返回了一些引用,可以像下例这样在另一个函数中使用它们:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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导入必要的对象。
扩展知识…
为改善性能,Odoo仅对前端加载最小化的JavaScript。所有资源中的其它JavaScript会页面完全加载后进行懒加载,最小化可用资源具有web. assets_frontend_minimal_js ID。
创建或更改模板 – QWeb
我们将向第四章 应用模型中所开发的my_library插件添加网站功能。我们所想要做的是允许浏览图书,如果用户使用相应的权限进行了登录,让用户可在网站界面中编辑图书详情。
准备工作
本节我们将使用配套GitHub仓库中的my_library插件模块。
如何实现…
我们需要定义如下几个控制器和视图:
- 在controllers/main.py中添加控制器提供图书列表服务,如下:
1234567891011from odoo import httpfrom odoo.http import requestclass 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([]),}) - 在views/templates.xml中添加最小化模板如下(请确保在 manifest 文件中添加了views/templates.xml):
12345678<?xml version="1.0" encoding="utf-8"?><odoo><template id="books"><t t-call="website.layout"><!-- Add page elements here --></t></template></odoo> - 在website.layout中,通过oe_structure类添加可拖放元素,如下:
1234567<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> - 将该代码块加入到website.layout中以显示图书的信息,如下:
123456789101112131415161718<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> - 在website.layout中添加一个不可编辑元素如下:
123<section class="container mt16" contenteditable="False">This is a non-editable text after the list of books.</section>
在浏览器中打开http://your-server-url:8069/books,将会看到一个带有作者的图书列表。通过这段代码,用户可以看到图书列表及详情。授予相应权限的话,用户还能修改图书详情及一些其它文本。
运行原理…
第1步中,我们创建了一个传递自定义值的控制器。这些自定义值将通过控制器传递给QWeb模板。
接下来的步骤(2,3,4,5)中,我们创建了一个名为books的模板,用于生成需显示图书列表HTML代码。所有的代码在带有t-call属性集的t元素之中,它会让Odoo渲染带有website.layout模板的页面并在模板内部插入内容,website.layout包含所有需要的工具,如Bootstrap, jQuery, FontAwesome等等。这些工具用于设计网页。website.layout还包含默认的头部、底部、代码片断和页面编辑功能。这样,我们获取带有菜单、底部和页面编辑功能的完整Odoo网页,无需在所有页面中重复这段代码。如果不使用t-call=”website.layout”,则不会获取到默认的头部、底部和网站编辑功能。
在第3,4,5步中,我们在website.layout内部添加了带有一些QWeb模板属性的HTML。这段 HTML 代码用于显示一个图书列表。现在,我们来了解不同的QWeb属性及它们的用法。
循环
为操作记录集或可迭代数据类型,需要一个遍历列表的结构。在QWeb模板中,可使用t-foreach元素来实现。迭代发生在t元素中,这时对每个 t-foreach属性中传递的可迭代成员重复内容,如下:
1 2 3 |
<t t-foreach="[1, 2, 3, 4, 5]" t-as="num"> <p><t t-esc="num"/></p> </t> |
渲染如下:
1 2 3 4 5 |
<p>1</p> <p>2</p> <p>3</p> <p>4</p> <p>5</p> |
也可以在其它元素中放置t-foreach及t-as属性,此时这个元素及其内容会对可迭代内容的每项进行重复。看一下下面的代码块。它会生成与前例完全相同的结果。
1 2 3 |
<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-$attr_name。在模板渲染时,会创建一个$attr_name属性;它的值可以为任意有效的Python表达式。通过当前上下文进行计算并且结果被设置为该属性的值,如下:
1 |
<div t-att-total="10 + 5 + 5"/> |
渲染结果如下:
1 |
<div total="20"></div> |
第二种方式是通过t-attf-$attr_name。与前一种类似,唯一的区别是仅运行{{ ..}}和#{..} 之间的字符串。在值中混合了字符串时会很有用。它最常用于运行样式类,如下例这样:
1 2 3 4 5 |
<t t-foreach="['info', 'danger', 'warning']" t-as="color"> <div t-attf-class="alert alert-#{color}"> Simple bootstrap alert </div> </t> |
渲染成如下这样:
1 2 3 4 5 6 7 8 9 |
<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选项。该选项在模板渲染字典数据转化为属性和值后接收这个字典。参见如下示例:
1 |
<div t-att="{'id': 'my_el_id', 'class': 'alert alert-danger'}"/> |
在渲染这个模板之后,它会被转化为如下这样:
1 |
<div id="my_el_id" class="alert alert-danger"/> |
在我们的示例中,使用了t-attf-class来获取基于索引值的动态背景。
字段
h3和div标签使用t-field属性。t-field属性的值必须与长度为1的记录集一起使用,这会允许用户在编辑模式打开网站时修改网页的内容。在保存页面时,更新的值会存储到数据库中。当然,它也遵照权限检查,仅在当前用户对所显示记录有写权限时才允许操作。通过可选的t-options属性,我们可以给出一个字典参数并传递给字段渲染器,包含要使用的组件。当前在后台中并没有太多的微件,因此这里的选择是有限的。例如,如果你希望通过二进制字段显示图像,那么可以像这样使用图像微件:
1 |
<span t-field="author.image_small" t-options="{'widget': 'image'}"/> |
t-field存在一些限制。它仅能用于记录集且不能用于<t>元素。为此我们需要使用一些HTML元素,如<span>或<div>。t-field属性有一个替代属性t-esc。t-esc属性并不只限于记录集,还可用于任意数据类型,但在网站中不可编辑。
t-esc和t-field之间的另一个区别是t-field显示基于用户语言的值,而t-esc显示数据库中的原始值。如对于在首选项中设置语言为英语的用户,在设置datetime字段使用t-field时,结果会以12/15/2018 17:12:13格式进行渲染。而相应的如果使用t-esc,那么结果渲染的格式会是2018-12-15 16:12:13。
译者注:此处的时区应为 UTC+1,因 Odoo创始地为比利时,位于东一区
条件语句
注意显示出版日期的分区由带有t-if属性集的t元素包裹。这个属性以Python代码运行,并且该元素仅在结果为真值时进行渲染。在下例中,我们仅在有实际出版日期集合时才显示div类。但是,在复杂用例中,我们可以像下例中这样使用t-elif和t-else:
1 2 3 4 5 6 7 8 9 |
<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模板也能够在模板自身中定义变量。在定义该模板后,我们可以在随后的模板中使用该变量。可以像下面这样设置变量:
1 2 |
<t t-set="my_var" t-value="5 + 1"/> <t t-esc="my_var"/> |
子模板
如果在开发一个大型应用,管理大模板会很困难。QWeb模板支持子模板,这时可以将大模板划分为更小的子模板并可以在多个模板中进行使用用。子模板中可以使用t-call属性,如下例所示:
1 2 3 4 5 6 7 |
<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将启用编辑和小组件拖拽的支持。
📝注:为了让页面兼容多站点,在编辑页面/浏览网站编辑器时,Odoo会为该网站创建一份单独的页面拷贝。也就是说随后的代码更新不会在已编辑网页中体现。为同时能轻松地使用行内编辑,又能在后台的发布中更新HTML代码,创建一个包含HTML语法元素的视图以及一个注入可编辑元素的视图。然后,仅后一个视图会进行拷贝,仍可获取父级视图中的更新。
对于这里使用的其它CSS类,请参见本节其它内容中Bootstrap文档的链接。
在第1步中,我们声明了渲染模板的路由。如果留心的话,会发现我们在route()中使用了website=True参数,它会在模板中传递一些额外的上下文,如菜单、用户语言和公司等。这将会在website.layout中用于渲染菜单和底部。website=True参数还会在网站中启用多语言支持。也以更好地方式显示异常。
在函数的最后,我们通过渲染模板返回了结果,然后我们传递了的所有在模板中使用的图书的记录集。
扩展知识…
要修改已有模板,我们可以在模板中使用inherit_id属性,然后使用像视图继承那样的xpath元素。例如,我们希望通过继承books模板来在Authors标签旁边显示作者的数量。可以通过如下的方式进行实现:
1 2 3 4 5 |
<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字段。
下一节中,我们会学习管理动态路由处理动态URL。
其它内容
参见如下各点来有效设计QWeb模板:
- Odoo整体大量地使用了Bootstrap,应当使用它来毫不费力地实现可适配的设计。
- 有关视图继承的详细内容,请参见第九章 后端视图中的修改已有视图 – 视图继承一节。
- 更多有关控制器的深入讨论,可参见第十三章 Web服务端开发中的让路径在网络中可访问和限制线上路径的访问两小节。
- 更多有关更新现有路由的知识,请参见第十三章 Web服务端开发中修改已有handler一节。
管理动态路由
在网站开发项目中,我们经常需要创建带有动态URL的页面。例如,在电商中,每个产品有一个具有不同URL的详情页。本节中,我们将创建一个显示图书详情的网页。
准备工作
我们使用前一节中的my_library模块。要让图书详情页看起来美观,我们需要添加一些新字段。请像下面这样在library.book中添加两个新字段:
1 2 3 4 5 6 7 8 |
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() |
可以将这些字段加到表单视图中。不过,还是可以在网页中进行字段编辑。
如何实现…
按照这些步骤来生成一个图书详情页面:
- 在main.py中为图书详情添加一个新路径,如下:
123456@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,}) - 在templates.xml中为图书详情添加一个新模板,如下:
1234567891011121314151617181920212223242526<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> - 在图书列表模板中添加一个按钮如下。这个按钮会重定向到图书详情网页:
12345678910111213141516171819...<div t-attf-class="card mt-3 #{'bg-info' 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>...
更新my_library模块应用修改。升级后就会在图书卡片中看到图书详情页的链接。点击链接就会打开图书详情页。
运行原理…
在第1步中,我们为图书详情页创建了一个动态路由。在这个路由中添加了<model(“library.book”):book>。它接收带有整数的URL,如/books/1。Odoo会把这个整数看作library.book模型的ID,并在访问这个URL时,Odoo获取一个记录集并将其作为参数传递给函数。因此,在浏览器中访问/books/1时,library_book_detail()函数中的book参数会有一个ID为1的library.book模型的记录集。我们传递了这个图书记录集并渲染了一个名为my_library.book2_detail的新模板。
在第2步中,我们新建了一个名为book_detail的QWeb模板来渲染图书详情页。这很简单,通过Bootstrap结构创建。查看页面,我们在详情页中添加了html_description。html_description字段的字段类型为HTML,因此可在该字段中存储HTML数据。Odoo对HTML类型的字段自动添加小组件拖拽的支持。因此,现在我们可以在图书详情页中使用小组件了。拖拽到HTML字段中的小组件存储于图书记录中,因此可以为不同图书设计不同的内容。
最后一步中,我们添加了一个带有a标签的链接,这样可以将访客重定向到图书详情页中。
📝注:模型路由还支持域的过滤。例如,如果希望根据条件限制对某些图书的访问,可以通过在路由中传递作用域如下:
/books/<model(“library.book”, “[(name’,’!=’, ‘Book 1’)]”):team>/submit
这会限制对名称为Book 1的图书的访问。
扩展知识…
Odoo使用werkzeug来处理HTTP请求。Odoo对werkzeug添加了轻量封装来易于路由的处理。在最一个示例的<model(“library.book”):book>路由中可以看到。这是Odoo自己的实现,但它还支持werkzeug路由中的所有其它功能。因此,可以使用这样的路由:
- /page/<int:page>接收一个整数值
- /page/<any(about, help):page_name>接收给定的值
- /pages/<page>接收字符串
- /pages/<category>/<int:page>接收多个值
对于路由有大量可用的变体,可以参阅http://werkzeug.pocoo.org/docs/0.14/routing/。
为用户提供静态小组件
Odoo的网站编辑器提供了一些编辑区块,可拖拽到页面上根据需求进行编辑。本节讲解如何提供自建区块。这些区块称为小组件(snippet)。有几种类型小组件,但总的来说可以分为两种类型:静态的和动态的。静态小组件是固定的,在用户修改前不会改变。动态小组件依赖于数据库记录,根据记录的值进行变化。本节中我们学习如何创建静态小组件。
准备工作
本节我们将使用前一节中的my_library模块。
如何实现…
小组件实际上只是一个注入到Insert blocks栏中的一个QWeb视图。我们将创建一个显示封面图和书名和小组件。可以拖拽小组件到页面中并可编辑图片和文本。按照如下步骤添加新的静态小组件:
- 添加文件views/snippets.xml,如下(别忘了在声明文件中注册该文件):
1234<?xml version="1.0" encoding="UTF-8"?><odoo><!-- 第2和第3步代码在这里添加 --></odoo> - 在views/snippets.xml中添加小组件如下:
1234567891011121314151617<template id="snippet_book_cover" name="Book Cover"><section class="pt-3 pb-3"><div class="container"><div class="row align-items-center"><div class="col-lg-6 pt16 pb16"><h1>Odoo 14 Development Cookbook</h1><p>Learn with Odoo development quickly with examples</p><a class="btn btn-primary" href="#">Book Details</a></div><div class="col-lg-6 pt16 pb16"><img src="/my_library/static/src/img/cover.jpeg"class="mx-auto img-thumbnail w-50 img img-fluid shadow" alt=""/></div></div></div></section></template> - 在小组件列表中列出模板如下:
12345<template id="book_snippets" inherit_id="website.snippets"><xpath expr="//div[@id='snippet_structure']/div[hasclass('o_panel_body')]" position="inside"><t t-snippet="my_library.snippet_book_cover" t-thumbnail="/my_library/static/src/img/s_book_thumb.png"/></xpath></template> - 将封面图和小组件缩略图加入到/my_library/static/src/img 目录中。
重启服务、更新my_library模块来应用修改。在编辑模式下打开网页,就可以在小组件区块面板中看到我们新添加的小组件了。
运行原理…
静态小组件不过是一个HTML代码块。第1步中,我们创建了具有图书区块HTML的QWeb模板。在这个HTML中,我们只是使用到了Bootstrap列式布局,但读者可以使用任何HTML代码。注意在小组件QWeb模板中添加的HTML会在拖拽时添加至页面中。通常使用section元素并对小组件使用Bootstrap类会是好的做法,因为Odoo的编辑器默认对它们提供了编辑、背景和改变大小的控制。
第2步中,我们在小组件列表中注册了我们的小组件。需要继承website.snippets来注册小组件。在网站编辑器图形界面中,小组件根据用途分成不同版块。本例中,我们通过xpath在Structure版块中注册了我们的小组件。显示小组件,需要使用带有 t-snippet属性的<t>标签。t-snippet会拥有QWeb模板的XML ID,本例中为my_library.snippet_book_cover。我们还需要使用t-thumbnail 属性,用于在网站编辑器显示图片小组件。
📝注:website.snippets模板包含所有默认的小组件,可以通过查看/addons/website/views/snippets/snippets.xml 文件来了解到更多。
在使用了恰当的Bootstrap结构时Odoo会对我们的小组件添加一些默认选项。例如,在我们的小组件中,可以设置背景色、背景图片、宽度和高度等等。查看/addons/website/views/snippets/snippets.xml 文件在了解所有的小组件选项。下一节中,我们将学习如何添加自己的选项。
第3步中,我们在structure区块中列出了我们的小组件。更新该模块后,我们就可以拖拽该小组件了。第4步中,我们添加了一张图片用作小组件的缩略图。
扩展知识…
在这种用例中,无需用到额外的JavaScript。Odoo的编辑器默认提供了大量的选项和控制,对于静态小组件远超足够使用的量了。我们可在website/views/snippets.xml中查看所有已有小组件和选项。
小组件选项还支持data-exclude、data-drop-near和data-drop-in属性,它们决定在拖出小组件栏时在哪里放置它。还有jQuery选择器,本节的第3步中我们没有用到它们,因为我们允许在内容所处的任意位置放置该小组件。
为用户提供动态小组件
本节中,我们将学习如何为Odoo创建动态小组件。我们会根据数据库中的值生成内容。
准备工作
本节我们将使用前一节中的my_library模块。
如何实现…
执行如下步骤来添加显示图书列表的动态小组件:
- 在views/snippets.xml中添加小组件的给定QWeb模板:
12345678910111213<template id="snippet_book_dynamic" name="Latest Books"><section class="book_list"><div class="container"><h2>Latest books</h2><table class="table book_snippet table-striped" data-number-of-books="5"><tr><th>Name</th><th>Release date</th></tr></table></div></section></template> - 注册小组件并添加选项来修改小组件的行为:
123456789<template id="book_snippets_options" inherit_id="website.snippets"><xpath expr="//div[@id='snippet_structure']/div[hasclass('o_panel_body')]" position="inside"><t t-snippet="my_library.snippet_book_dynamic" t-thumbnail="/my_library/static/src/img/s_book_list.png"/></xpath><xpath expr="//div[@id='snippet_options']" position="inside"><!—- 在这里添加第3步中的代码 --></xpath></template> - 然后在图书小组件中添加组件选项:
123456789101112<div data-selector=".book_snippet"><we-select string="Table Style"><we-button data-select-class="table-striped">Striped</we-button><we-button data-select-class="table-dark">Dark</we-button><we-button data-select-class="table-bordered">Bordered</we-button></we-select><we-button-group string="No of Books" data-attribute-name="numberOfBooks"><we-button data-select-data-attribute="5" title="5 Books">5</we-button><we-button data-select-data-attribute="10" title="10 Books">10</we-button><we-button data-select-data-attribute="15" title="15 Books">15</we-button></we-button-group></div> - 新增文件/static/src/js/snippets.js并添加代码来渲染动态小组件:
12345odoo.define('book.dynamic.snippet', function (require) {'use strict';var publicWidget = require('web.public.widget');// 在此处添加第5步中的代码}); - 添加public微件来动态渲染图书小组件:
12345678910111213141516171819202122232425publicWidget.registry.books = publicWidget.Widget.extend({selector: '.book_snippet',disabledInEditableMode: false,start: function () {var self = this;var rows = this.$el[0].dataset.numberOfBooks || '5';this.$el.find('td').parents('tr').remove();this._rpc({model: 'library.book',method: 'search_read',domain: [],fields: ['name', 'date_release'],orderBy: [{name: 'date_release', asc: false}],limit: parseInt(rows)}).then(function (data) {_.each(data, function (book) {self.$el.append($('<tr />').append($('<td />').text(book.name),$('<td />').text(book.date_release)));});});},}); - 在模块中添加以上JavaScript文件:
12345<template id="assets_frontend" inherit_id="website.assets_frontend"><xpath expr="." position="inside"><script src="/my_library/static/src/js/snippets.js" type="text/javascript"/></xpath></template>
在更新该模块后,我们将获得一个名为Latest books的新组件,它具有修改近期添加图书数量的功能。我们还添加了修改表格设计的功能,在点击表格时会进行显示。
运行原理…
在第1步中,我们为新组件添加了一个QWeb模板(和前一小节中一样)。注意我们对表格添加了一个基础结构。在表格中会动态地添加一行行图书。
在第2步和第3中,注册了我们的动态小组件,我们还添加了自定义选项来修改动态小组件的行为。所添加的第一个选项为Table Style,用于修改表格的样式。添加的第二个选项为No of Books。我们对选项使用了<we-select> 和 <we-button-group>标签。这些标签对小组件选项提供了不同的图形界面。<we-select>标签会将选项显示为下拉列表,而<we-button-group>标签会将选项显示为按钮组。还有其它的图形选项,如<we-checkbox> 和 <we-colorpicker>。可以在/addons/website/views/snippets/snippets.xml中查看更多的图形选项。
如果仔细观察选项,会看到对于选项按钮有data-select-class 和 data-select-data-attribute属性。这让Odoo知道在用户选取选项时修改哪一属性。data-select-class会在用户选取时设置元素的class属性,而data-select-data-attribute则设置元素的自定义属性和值。注意它会使用data-attribute-name的值设置该属性。
现在,我们已添加小组件及选项。如果此时拖拽小组件的话,只会看到表头和选项。修改小组件的选项会改变表格的样式,但其中没有图书数据。为此,我们需要编写一些获取数据的JavaScript代码并在表格中显示。第4步中,我们添加了在表格中渲染图书数据的JavaScript代码。Odoo使用PublicWidget映射JavaScript对象为HTML元素。通过require(‘web.public.widget’) 模块让PublicWidget可供使用。使用PublicWidget的关键属性是selector属性。在selector属性中,我们需要使用元素的CSS选择器,并且Odoo会自定绑定PublicWidget和元素。我们可以在$el 属性中访问相关元素。其余的代码除_rpc外都是基本的JavaScript和jQuery。_rpc方法用于做网络请求、获取数据。我们会在第十五章 网页客户端开发的向服务端做RPC调用一节中学习_rpc方法。
最后一步中,我们对资源添加了一个JavaScript文件。
扩展知识…
如果想要创建自己的小组件选项,可以对小组件使用t-js 选项。然后,需要在JavaScript代码中定义我们自己的选项。查看addons/website/static/src/js/editor/snippets.options.js文件学习更多有关小组件选项的知识。
获取网站用户的输入
在网站开发中,经常会需要创建表单来从网站用户(访客)接收输入的数据。本节中,我们将在页面中创建一个HTML表单来报告与图书相关的勘误。
准备工作
本节中我们使用前一小节中的my_library模块。我们需要新建一个模型来存储用户所提交的勘误。
因此,在开始本节之前,修改此前的代码:
- 在 library.book模型中添加一个字段及新增book.issues模型,如下:
1234567891011121314151617class 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')issue_description = fields.Text() - 在图书的表单视图中添加book_issues_id字段,如下:
1234567891011...<group string="Book Issues"><field name="book_issue_id" nolabel="1"><tree name="Book issues"><field name="create_date"/><field name="submitted_by"/><field name="isuue_description"/></tree></field></group>...
- 在ir.model.access.csv文件中为新的 book.issue模型添加访问权限,如下:
1acl_book_issues,library.book_issue,model_book_issue,group_librarian,1,1,1,1
我们为图书勘误添加了一个新模型,现在我们将添加一个带有HTML表单的新模板。
如何实现…
按照如下步骤来为勘误页面创建一个新路由及模板页面:
- 在main.py中添加一个新路由,如下:
12345678910111213141516@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)}) - 在其中添加一个HTML表单模板,如下:
1234567<template id="books_issue_form" name="Book Issues Form"><t t-call="website.layout"><div class="container mt32"><!-- 此处添加页面元素 --></div></t></template> - 为该页面添加条件头部,如下:
12345678910<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> - 添加<form>来提交勘误,如下:
123456789101112131415161718192021222324252627<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个字段的<form>:csrf_token、图书选取和问题描述。后两个字段用于从网站用户获取输入内容。但csrf_token用于避免跨站请求伪造(CSRF)攻击。如果不在表单中使用它,用户将无法提交表单。在提交表单时,将会在第1步books_issues()方法中的**post参数中获取所提交的数据。
📝小贴士:在某些情况下,如果希望禁用csrf验证,可以在路由中使用csrf=False,类似这样:@http.route(‘/url’, type=’http’,auth=”user”, website=True, csrf=False )。
扩展知识…
如果需要,可以使用单独的路由页面,而对于post数据可以在表单中添加动作如下:
1 2 3 |
... <form action="/my_url" method="post"> ... |
此外,可以通过在路由中添加method参数来禁用get请求,如下:
1 |
@http.route('/my_url', type='http', method='POST' auth="user", website=True) |
管理搜索引擎优化(SEO)选项
Odoo对模板(页面)提供了内置的SEO支持。但是,有些模板在多个URL中使用。例如,在线商店中,产品页面使用相同模板和不同产品数据来进行渲染。对于这种情况,我们希望每个URL有单独的SEO选项。
准备工作
本节中我们使用前一小节中的my_library模块。我们将对每个图书详情页面分别存储SEO数据。在进行本节的开发之前,应当在不同的图书页面中测试SEO选项。可以从顶部的Promote下拉菜单中获取SEO对话框,如以下图片所示:
图14.3 – 打开页面的SEO配置
如果在不同的图书详情页中测试SEO选项,会注意到在一个图书页面中修改SEO数据会反映到所有的图书页面中。我们将在本节中修复这一问题。
如何实现…
按照如下步骤来管理每本书各自的SEO选项:
- 在library.book模型中继承website.seo.metadata元数据mixin ,如下:
12345678...class LibraryBook(models.Model):_name = 'library.book'_inherit = ['website.seo.metadata']name = fields.Char('Title', required=True)date_release = fields.Date('Release Date')... - 在图书详情路由中以main_object传递图书对象,如下:
12345678910...@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。它可以通过Promote>Optimize SEO选项来进行修改。下面,你就能够按书管理各个的SEO详情了。
译者注:SEO 是一个单独的话题,传统认为是对 TDK(Title, Description, Keywords)的优化,以上还包含有对 url的自定义,以及实时预览效果和社会化分享图标的自定义,常用的基本已经都有了。
运行原理…
要对模型的每条记录启用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中,我们可以对Open Graph(译者注:Facebook发起的一项标准)和Twitter分享(常称之为 Twitter Card)添加元标签。 如果希望对一个页面添加自定义元标签,可以在添加SEO mixin之后重载_default_website_meta() 。例如,如果希望使用图书封面作为社交分享的图片,可以在图书模型中使用如下代码:
1 2 3 4 5 |
def _default_website_meta(self): res = super(LibraryBook, self)._default_website_meta() res['default_opengraph']['og:image'] = self.env['website'].image_url(self, 'image') res['default_twitter']['twitter:image'] = self.env['website'].image_url(self, 'image') return res |
之后,在分享图书链接时会在社交媒体中显示图书封面。此外,也可以通过同样的方法设置页面标题和描述。
管理网站的站点地图
一个网站的站点地图(sitemap)对于任何网站都非常关键。搜索引擎会使用网站的站点地图来索引网站的页面。在本节中,我们将在站点地图中添加所有的图书详情页。
准备工作
本节中我们将使用前一节的my_library模块。如果你想要查看当前Odoo中的站点地图,可在浏览器中打开<your_odoo_server_url>/sitemap.xml进行查看。其中没有图书的 URL。
如何实现…
按照如下步骤在sitemap.xml中添加图书页面:
- 在main.py中导入方法如下:
12from odoo.addons.http_routing.models.ir_http import slugfrom odoo.addons.website.models.ir_http import sitemap_qs2dom - 在 main.py中添加sitemap_books方法如下:
123456789class 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} - 在图书的详情路由中添加sitemap_books函数引用如下:
123456...@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()的函数引用给带有sitemap关键词的路由。
扩展知识…
本节中,我们学习了如何使用自定义方法生成站点地图中的URL。便如果不希望过滤图书而是在站点地图中列出所有图书,那么就不需要传递函数,只要使用 True就好了:
1 2 3 |
... @http.route('/books/<model("library.book"):book>', type='http', auth="user", website=True, sitemap=True) ... |
获取访客的国家信息
Odoo CMS有对GeoIP的内置支持。在线上环境中,可以根据IP来追踪访客的国家。本节中我们将根据访客的IP地址来获取访客的国家。
准备工作
本节我们使用前一节的my_library模块。本节中,我们将根据访客的国家来在网页中隐藏一些图书。完成本节读者需要下载GeoIP数据库。然后需要通过cli选项传递数据库的位置,如下:
1 |
./odoo-bin -c config_file --geoip-db=location_of_geoip_DB |
如使用GeoLite2免费库。
1 |
./odoo-bin -c main.cfg --geoip-db=~/GeoLite2-Country.mmdb |
如何实现…
按照以下步骤来根据国家进行图书限制:
- 在library.book模型中添加restrict_country_ids多对多字段,如下:
1234567class LibraryBook(models.Model):_name = 'library.book'_inherit = ['website.seo.metadata']...restrict_country_ids = fields.Many2many('res.country')... - 在library.books模型的表单视图中添加restrict_country_ids字段,如下:
123456...<group><field name="date_release"/><field name="restrict_country_ids" widget="many2many_tags"/></group>... - 更新/books控制器来根据国家限制图书,如下:
12345678910111213@http.route('/books', type='http', auth="user", website=True)def library_books(self):country_id = Falsecountry_code = request.session.geoip and request.session.geoip.get('country_code') or Falseif country_code:country_ids = request.env['res.country'].sudo().search([('code', '=', country_code)])if country_ids:country_id = country_ids[0].iddomain = ['|', ('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 ‘CN’
GeoIP数据会经常进行更新,因此需要更新为最新版的国家信息。
追踪营销活动
在任何业务或服务中,熟悉投资回报率(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:
- 在manifest.py的depends中添加utm 模块,如下:
1'depends': ['base', 'website', 'utm'], - 在book.issue模型中继承utm.mixin如下:
1234567class 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() - 在book_issue_ids字段的树状视图中添加campaign_id字段,如下:
123456789101112...<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是其中之一。如果想要创建一个新的活动、媒介或来源,可以通过在开发模式下访问Link Tracker > UTMs菜单来实现。
管理多站点
在版本12中,Odoo添加了对多网站的支持。这意味着相同的Odoo实例可在多个域名上运行,并且可显示不同的记录。
准备工作
本节我们将使用前一小节中的my_library模块。在这一节中,我们将根据网站来隐藏图书。
如何实现…
按照如下步骤来兼容多网站:
- 在library.book模型中添加website.multi.mixin如下:
1234class LibraryBook(models.Model):_name = 'library.book'_inherit = ['website.seo.metadata', 'website.multi.mixin']... - 在图书的表单视图中添加website_id,如下:
123456...<group><field name="author_ids" widget="many2many_tags"/><field name="website_id"/></group>... - 在/books 控制器修改作用域如下:
1234567891011@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),})... - 导入werkzeug并修改图书详情控制器来在另一个网站中限制图书的访问,如下::
12345678910111213import 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并查看图书的列表。然后,修改网站并查看图书列表。为进行测试,我们可以通过网站的下拉菜单切换器来更改网站。参见下图来实现:
图14.4 – 网站切换菜单
也可以尝试直接通过URL访问图书详情,如 /books/1。如果书不是来自该网站的话,它会显示为404。
运行原理…
在第1步中,我们添加了website.multi.mixin。这个mixin添加了一个基本工具来在模型中处理多网站。这个mixin在模型中添加了website_id字段。该字段用于决定记录是针对哪个网站的。
在第2步中,我们在图书的表单视图中添加了website_id字段,因此该书可以根据网站来进行过滤。
在第3步中,我们修改了用于查找图书列表的作用域,request.website.website_domain()将返回作用域过滤出不是来自该网站的图书。
📝重要信息:注意有未设置website_id的记录。这些记录会在所有网站中显示。这表示如果在特定图书中没有website_id字段,那么这本书会在所有网站中显示。
然后我们在网页搜索中添加了该作用域,如下:
- 在第4步中,我们限制了图书的访问。如果图书不是针对当前网站的话,那么会抛出not found错误。如果图书记录针对当前网站的话can_access_from_current_website方法会返回值True,而针对其它网站时返回False。
- 如果留意的话,我们在两个控制器中都添加了**post。这是因为没有它,**post /books 和/books/<model:library.book:book>就不会接收查询参数。它们会在通过网站切换菜单切换网站时报错,因此我们作了添加。正常在每个控制器中都添加**post是一种良好实践,这样它可以处理查询参数。
重定向老URL
从已有系统或网站迁移为Odoo网站时,就必须要处理老URL到新URL的跳转。通过适当的跳转,所有SEO排名都会迁移到新页面上。本节中,我们将学习如何在 Odoo 中将老URL跳转到新URL。
准备工作
本节我们不用管前一小节中的my_library模块。本节假定你有一个老网站,刚刚迁移到Odoo。
如何实现..
假设在老网站中图书列表页为/library,我们知道my_library模块通过/books 这一URL访问列表。因此就需要在Odoo中添加重定向,将老的/library跳转到新的/books链接上。执行如下步骤来实现重定向规则:
- 开启开发者模式。
- 打开 Website > Configuration > Redirects。
- 点击Create 新建一条规则。
- 按下图中那样输入值。在URL from中输入/library, 在URL to中输入enter /books。
- 对Action选取值301 Moved permanently。
- 保存记录。填写完数据后,表格如下所示:
添加好规则后,打开/library页面,会发现页面自动重定向到了/books页面。
运行原理…
页面重定向很简单,它是一种HTTP协议。本例中,我们将/library跳转到了/books。使用了301 Moved permanently重定向。以下是Odoo中可以使用的一些重定向选项:
- 404 Not Found: 该选项在希望访问页面响应404 Not Found 时使用。注意Odoo对这类请求会显示默认的404页面。
- 301 Moved permanently: 该选项将老的URL永久重定向为新的。这类跳转会将SEO排名迁移到新页面。
- 302 Moved temporarily: 该选项临时重定向老URL至新链接。在需要在有限时间内进行重定向时使用该选项。这类重定向不会将SEO权重迁移到新页面。
- 308 Redirect/Rewrite: 一个很意思的选项 – 使用它,可以改变/重写Odoo中已有的URL为新 URL。本节中,它可以将老的 /library URL重写为新的/books。因此,我们无需对 /library使用301 Moved permanently规则重定向老URL。
重定向规则表单中还有其它字段。其中一个是Active字段,用于启用或禁用规则。第二个重要的字段是Website。该Website字段在使用了多站点功能时使用,用于限定跳转规则仅针对某个站点。但默认规则会作用于所有站点。
网站相关记录的发布管理
业务流中,有时需要对外部用户放开或收回页面的访问权。一个例子是电商中的商品,需要根据库存来发布或取消商品的发布。本节中,我们将学习如何对公众发布或取消发布图书记录。
准备工作
本节中我们继续使用前面小节中的my_library模块。
📝重要信息:细心的读者会发现/books /books/ <model(“library.book”):book>路由中我们使用了auth=’user’ 。请修改为auth=’public’来让公众可访问到这些链接。
如何实现..
执行如下步骤对图书详情面启用发布/取消发布选项:
- 在library.book 模型中添加website.published.mixin如下:
12345class LibraryBook(models.Model):_name = 'library.book'_description = 'Library Book'_inherit = ['website.seo.metadata', 'website.published.mixin']... - 新增文件my_library/security/rules.xml ,对图书添加一条记录规则如下(不要忘记在声明文件中进行注册):
12345678910<?xml version="1.0" encoding="UTF-8" ?><odoo noupdate="1"><record id="books_rule_portal_public" model="ir.rule"><field name="name">Portal/Public user: read published books</field><field name="model_id" ref="my_library.model_library_book"/><field name="groups" eval="[(4, ref('base.group_portal')), (4, ref('base.group_public'))]"/><field name="domain_force">[('website_published','=', True)]</field><field name="perm_read" eval="True"/></record></odoo> - 更新my_library模块使用更改生效。接下来就可以发布或取消发布图书页面了:
图14.6 – 发布/取消发布按钮
发布或取消发布图书,我们可以在图书详情页通过上图的滑块进行切换。
运行原理…
Odoo提供了一个完备的处理记录发布管理的mixin。它完成了大部分的任务。我们要做的只是在模型中加上website.published.mixin。第1步中,我们在图书模型中添加了website.published.mixin。这会添加上所有用于发布和取消发布图书的字段和方法。在图书模型中添加了这一mixin后,就可心在图书详情页上看到切换状态的按钮,参见前图。
📝注:我们在图书详情路由中以main_object发送图书记录。没有它就无法在图书详情页中看到发布/取消发布按钮。
添加该mixin后会在图书详情页上出现发布/取消发布按钮,但公众用户仍可以访问到。需要添加记录规则加以限制。第2步中,我们添加了禁止访问未发布图书的记录规则。如果想要了解更多有关记录规则的知识,请参见第十章 权限安全。
扩展知识…
发布mixin会在网站上启用发布/未发布按钮。但如果希望在后台表单视图中显示一个重定向按钮,发布mixin也能实现。以下是在图书表单视图中显示重定向按钮的步骤:
- 在library.book 模型中添加方法计算图书的 URL:
1234@api.depends('name')def _compute_website_url(self):for book in self:book.website_url = '/books/%s' % (slug(book)) - 在表单视图中添加按钮跳转至网站:
1234567...<sheet><div class="oe_button_box" name="button_box"><field name="is_published"widget="website_redirect_button"/></div>...
添加按钮后,就可以在图书表单视图中年到该按钮,点击后会跳转到图书详情页。