В этом документе рассказывается о фреймворке Odoo Javascript. Данный фреймворк не представляет собой какое-то крупное приложение с точки зрения объема кода, но, не смотря на это, он довольно универсален, потому что это в основном машина, превращающая декларативное описание интерфейса в живое приложение, способное взаимодействовать с любой моделью и записями в базе данных. Можно даже использовать веб-клиент для изменения интерфейса веб-клиента.
Общее описание
Данный JS фреймворк создан для работы с тремя основными сценариями:
- веб-клиент: это веб-приложение, в котором можно просматривать и редактировать бизнес-данные. Приложение защищает доступ к этим данным. С технической стороны то Single Page Application (SPA) (сама страница не перезагружается, а новые данные запрашиваются с сервера каждый раз, когда это необходимо)
- вебсайт: это часть Odoo доступная всем. Это позволяет любому пользователю просматривать доступный контент, делать покупки или выполнять множество действий в качестве клиента. Технически - это классический сайт: используются контроллеры для обработки url маршрутов и генерации html страниц, плюс немного javascript для их работы.
- рабочее место кассира: это интерфейс для работы автоматизированных касс. Технически это специальное SPA.
Часть JS кода используется всеми этими сценариями и объединен в один бандл (см. ниже в разделе ассетов). В этом документе основное внимание будет уделено дизайну веб-клиента.
Web client
SPA
In short, the webClient, instance of WebClient is the root component of the whole user interface. Its responsability is to orchestrate all various subcomponents, and to provide services, such as rpcs, local storage and more.
Во время работы веб-клиент представляет собой SPA приложение. Оно не запрашивает полную страницу с сервера каждый раз, когда пользователь выполняет действие. Вместо этого оно запрашивает только те данные, которые ему нужны, а затем заменяет/обновляет представление. Кроме того, оно управляет URL-адресом: синхронизирует его с состоянием веб-клиента.
Это означает, что, пока пользователь работает в Odoo, класс веб-клиента (и менеджер действий) фактически создает и уничтожает многие дочерние компоненты. Состояние постоянно меняется, и каждый виджет может быть уничтожен в любое время.
Обзор JS кода веб-клиента
Здесь мы даем очень краткое описание кода веб-клиента в содержащегося в модуле web/static/src/js. Обратите внимание, что это намеренно и мы не ставим перед собой цель детально все описать. Мы пробежимся только по самым важным файлам/папкам.
- boot.js: это файл, который определяет систему модулей. Его нужно загружать первым.
- core/: это коллекция строительных блоков нижнего уровня. В частности, он содержит систему классов, систему виджетов, инструменты для асинхронной работы и многие другие классы/функции.
- chrome/: в этой папке у нас находятся самые большие виджеты, которые составляют большую часть пользовательского интерфейса.
- chrome/abstract_web_client.js и chrome/web_client.js: вместе эти файлы определяют виджет WebClient, который является корневым виджетом для веб-клиента.
- chrome/action_manager.js: это код, который преобразует действие в виджет (например, Kanban или представление From)
- chrome/search_X.js все эти файлы определяют представление Search (это не представление с точки зрения веб-клиента, только с точки зрения сервера)
- fields: все виджеты поля основного представления определены здесь
- views: это место в котором расположены представления
Управление ассетами
Управление ассетами организовано в Odoo не так просто, как в других приложениях. Одна из причин заключается в том, что может возникнуть множество ситуаций, когда требуются не все ассеты, а только конкретные. Например, потребности веб-клиента, рабочего места кассира, веб-сайта или даже мобильного приложения различны. Кроме того, некоторые ассеты могут быть достаточно объемными и при этом они редко нужны. В этих случаях мы хотим, чтобы они загружались в фоне.
The main idea is that we define a set of bundles in xml. A bundle is here defined as a collection of files (javascript, css, less). In Odoo, the most important bundles are defined in the file addons/web/views/webclient_templates.xml. It looks like this:
<template id="web.assets_common" name="Common Assets (used in backend interface and website)">
<link rel="stylesheet" type="text/css" href="/web/static/lib/jquery.ui/jquery-ui.css"/>
...
<script type="text/javascript" src="/web/static/src/js/boot.js"></script>
...
</template>
Файлы в бандл можно вставить в шаблон с помощью директивы t-call-assets:
<t t-call-assets="web.assets_common" t-js="false"/>
<t t-call-assets="web.assets_common" t-css="false"/>
Вот что происходит, когда шаблон рендерится сервером с этими директивами:
- all the less files described in the bundle are compiled into css files. A file named file.less will be compiled in a file named file.less.css.
- если мы находимся в режиме debug=assets,
- директива t-call-assets с атрибутом t-css установленном в
false
будет заменен списком тегов таблицы стилей, указывающих на файлы CSS - директива t-call-assets с атрибутом t-js, установленным в
false
, будет заменена списком тегов скриптов, указывающих на файлы js
- директива t-call-assets с атрибутом t-css установленном в
- если мы не находимся в режиме debug=assets,
- the css files will be concatenated and minified, then splits into files with no more than 4096 rules (to get around an old limitation of IE9). Then, we generate as many stylesheet tags as necessary
- js-файлы будут объединены и минифицированы, а затем будет создан тег javascript
Обратите внимание, что файлы ассетов кэшируются, поэтому теоретически браузер должен загружать их только один раз.
Основные бандлы
Когда сервер Odoo запускается, он проверяет временную метку каждого файла в бандле и при необходимости создает/переделвыет соответствующие бандлы.
Вот несколько важных бандлов, которые большинству разработчиков нужно знать:
- web.assets_common: этот бандл содержит большинство ресурсов необходимых для веб-клиента, веб-сайта, а также рабочего места кассира. Предполагается, что он содержит строительные блоки более низкого уровня для платформы odoo. Обратите внимание, что он содержит файл boot.js, который определяет систему модулей odoo.
- web.assets_backend: этот бандл содержит код для веб-клиента (в частности, веб-клиент/менеджер действий/представления)
- web.assets_frontend: этот бандл содержит все что нужно для работы веб-сайта: интернет-магазин, форум, блог, управление событиями, …
Добавление файла в качестве ассета в бандл
Правильный способ добавить файл, расположенный в addons/web, в бандл очень прост: достаточно просто добавить тег script или stylesheet в бандл в файле webclient_templates.xml. Но когда мы работаем в другом модуле, нам нужно добавить файл из этого самого модуля. Вам нужно выполнить три шага:
- добавьте фалй assets.xml в каталог views/
- добавьте строку views/assets.xml в ключ data в файле манифеста
- создайте представление для наследования желаемого бандла и добавьте файл(ы) с выражением xpath. Например,
<template id="assets_backend" name="helpdesk assets" inherit_id="web.assets_backend">
<xpath expr="//script[last()]" position="after">
<link rel="stylesheet" href="/helpdesk/static/src/less/helpdesk.less"/>
<script type="text/javascript" src="/helpdesk/static/src/js/helpdesk_dashboard.js"></script>
</xpath>
</template>
Примечание
Обратите внимание, что файлы в бандле загружаются сразу же, когда пользователь загружает веб-клиент odoo. Это означает, что файлы передаются по сети каждый раз (кроме тех случаев, когда кеш браузера активен). Иногда может быть лучше загрузить часть ассетов в фоне. Например, если для работы виджета требуется большая библиотека, и этот виджет не является основной частью работы пользователя, тогда хорошей идеей может стать загрузка библиотеки только в момент создания виджета. Класс виджета имеет встроенную поддержку для такого варианта использования. (см. раздел Генератор шаблонов QWeb)
Что делать если файл не загружается/обновляется
Причин такого поведения может быть много. Вот несколько советов, которые могу помочь решить эту проблему:
- когда сервер уже запущен он не знает, был ли изменен файл ассета. Поэтому, вы можете просто перезапустить сервер, чтобы ассеты были пересобраны.
- проверьте консоль браузера (в инструментах разработчика, обычно открываемых с помощью F12), чтобы убедиться в отсутствии явных ошибок
- попробуйте добавить console.log в начале вашего файла (до определения любого модуля), чтобы вы могли увидеть, был файл загружен или нет
- в пользовательском интерфейсе в режиме отладки (ВСТАВЬТЕ ССЫЛКУ ЗДЕСЬ ДЛЯ ПЕРЕХОДА РЕЖИМ ОТЛАДКИ) есть опция, заставляющая сервер обновлять свои файлы ассетов.
- используйте режим debug=assets. Это фактически напрямую загрузит файлы ассетов объявленных в бандлах (обратите внимание, что это на самом деле не решает проблему. Сервер все еще использует устаревшие бандлы)
- наконец, наиболее удобный способ сделать это для разработчика - запустить сервер с параметром - dev=all. Это активирует параметры средства просмотра файлов, которые автоматически аннулируют ресурсы при необходимости. Обратите внимание, что это не очень хорошо работает на ОС Windows.
- и главное не забывайте обновлять страницу (прим.пер. Желательно со сбросом кэша)!
- или просто сохраните ваш файл…
Примечание
После того, как файл ассета был заново создан, вам необходимо обновить страницу, чтобы перезагрузить нужные файлы (не забываем про то, что файлы могут быть кэшированы и перезагружаем со сбросом кэша).
Система модулей JavaScript
Как только мы смогли загрузить наши файлы javascript в браузер, мы должны убедиться, что они загружены в правильном порядке. Для этого в Odoo определена небольшая модульная система (находится в файле*addons/web/static/src/js/boot.js*, который должен быть загружен первым).
Система модулей Odoo, вдохновленная AMD, работает путем определения функции define для глобального объекта odoo. Затем мы определяем каждый модуль javascript, вызывая эту функцию. В платформе Odoo модуль представляет собой фрагмент кода, который будет выполнен в максимально короткие сроки. У него есть имя и, возможно, зависимости. Когда его зависимости загружены, модуль также будет загружен. Значением модуля это возвращаемое значение функции, определяющей модуль.
Вот небольшой пример:
// in file a.js
odoo.define('module.A', function (require) {
"use strict";
var A = ...;
return A;
});
// in file b.js
odoo.define('module.B', function (require) {
"use strict";
var A = require('module.A');
var B = ...; // something that involves A
return B;
});
An alternative way to define a module is to give explicitely a list of dependencies in the second argument.
odoo.define('module.Something', ['module.A', 'module.B'], function (require) {
"use strict";
var A = require('module.A');
var B = require('module.B');
// some code
});
Если некоторые зависимости отсутствуют/не готовы, модуль просто не будет загружен. Через несколько секунд в консоли браузера появится предупреждение.
Циклические зависимости не поддерживаются. Это имеет смысл, но это означает, что нужно быть осторожным.
Определение модуля
Метод odoo.define предоставляет три аргумента:
moduleName: имя модуля JavaScript. Это должна быть уникальная строка. По соглашение имя модуля javascript должно иметь иметь имя модуля odoo, сопровожденное определенным описанием. Например, «web.Widget» описывает модуль, определенный в модуле web, который экспортирует класс Widget (потому что первая буква заглавная)
Если имя не уникальное, то вы увидите возникшее исключение в консоли браузера.
- dependencies: the second argument is optional. If given, it should be a list of strings, each corresponding to a javascript module. This describes the dependencies that are required to be loaded before the module is executed. If the dependencies are not explicitely given here, then the module system will extract them from the function by calling toString on it, then using a regexp to find all require statements.
- наконец, последний аргумент - это функция, которая определяет модуль. Его возвращаемое значение - это значение модуля, которое может быть передано другим модулям, зависящим от него. Обратите внимание, что существует исключение для асинхронных модулей, см. Следующий раздел.
Если произойдет ошибка, она будет добавлена в лог (в режиме отладки) в консоль браузера:
Missing dependencies
: эти модули не отображаются на странице. Возможно, что файл JavaScript отсутствует на странице или что имя модуля указано неверноFailed modules
: Обнаружена ошибка javascriptRejected modules
: The module returns a rejected deferred. It (and its dependent modules) is not loaded.Rejected linked modules
: Модули который зависят от сброшенного модуляNon loaded modules
: Модули которые зависят от пропущенного или не загрузившегося модуля
Асинхронные модули
It can happen that a module needs to perform some work before it is ready. For example, it could do a rpc to load some data. In that case, the module can simply return a deferred (promise). In that case, the module system will simply wait for the deferred to complete before registering the module.
odoo.define('module.Something', ['web.ajax'], function (require) {
"use strict";
var ajax = require('web.ajax');
return ajax.rpc(...).then(function (result) {
// some code here
return something;
});
});
Best practices
- помните соглашение для имени модуля: имя модуля odoo с суффиксом имя модуля javascript.
- объявляйте все свои зависимости в верхней части модуля. Кроме того, они должны быть отсортированы в алфавитном порядке по названию модуля. Это упрощает понимание вашего модуля.
- объявляйте весь экспорт в конце.
- try to avoid exporting too much things from one module. It is usually better to simply export one thing in one (small/smallish) module.
- asynchronous modules can be used to simplify some use cases. For example, the web.dom_ready module returns a deferred which will be resolved when the dom is actually ready. So, another module that needs the DOM could simply have a require(„web.dom_ready“) statement somewhere, and the code will only be executed when the DOM is ready.
- Старайтесь избегать определения более одного модуля в одном файле. Это может быть удобно в краткосрочной перспективе, но на деле это сложнее поддерживать.
Система классов
Odoo was developped before ECMAScript 6 classes were available. In Ecmascript 5, the standard way to define a class is to define a function and to add methods on its prototype object. This is fine, but it is slightly complex when we want to use inheritance, mixins.
По этим причинам, мы в Odoo решили использовать свою систему классов вдохновленную John Resig. Базовый класс находится в модуле web.Class, файла class.js.
Создание подкласса
Давайте обсудим, как создаются классы. Основной механизм это использование метода extended (что очень похоже на extension в классах ES6).
var Class = require('web.Class');
var Animal = Class.extend({
init: function () {
this.x = 0;
this.hunger = 0;
},
move: function () {
this.x = this.x + 1;
this.hunger = this.hunger + 1;
},
eat: function () {
this.hunger = 0;
},
});
В этом примере функция init является конструктором. Она будет вызвана при создании экземпляра. Создание экземпляра выполняется с помощью ключевого слова new.
Наследование.
Удобно иметь возможность наследовать существующий класс. Это делается с помощью метода extended в родительском классе. Когда метод вызывается, фреймворк тайно перепривязывает специальный метод: _super к вызываемому в настоящее время методу. Это позволяет нам использовать this._super всякий раз, когда нам нужно вызвать родительский метод.
var Animal = require('web.Animal');
var Dog = Animal.extend({
move: function () {
this.bark();
this._super.apply(this, arguments);
},
bark: function () {
console.log('woof');
},
});
var dog = new Dog();
dog.move()
Миксины
Система классов odoo не поддерживает множественное наследование, но для тех случаев, когда нам нужно поделиться каким либо функционалом, у нас есть система миксинов: метод extended может фактически принимать произвольное количество аргументов и объединит их все в новый класс.
var Animal = require('web.Animal');
var DanceMixin = {
dance: function () {
console.log('dancing...');
},
};
var Hamster = Animal.extend(DanceMixin, {
sleep: function () {
console.log('sleeping');
},
});
Вот пример, класс Hamster является дочерним классом Animal, к которому так же «подмешан» DanceMixin.
Изменение существующего класса
Не часто, но иногда нужно изменить другой класс на месте. Цель - иметь механизм для изменения класса и всех будущих/действующих экземпляров. Это делается с помощью метода include:
var Hamster = require('web.Hamster');
Hamster.include({
sleep: function () {
this._super.apply(this, arguments);
console.log('zzzz');
},
});
Очевидно это опасная операция, и ее следует выполнять с осторожностью. Но в связи тем, как устроена Odoo быывает, что иногда необходимо изменить поведение виджета/класса, определенного в другом модуле. Обратите внимание, что изменятся все экземпляры класса, даже если они уже были созданы.
Виджеты
Класс Widget один из наиболее важных строительных блоков пользовательского интерфейса. Практически все в пользовательском интерфейсе находится под контролем виджета. Класс Widget определен в модуле web.Widget файла widget.js.
Если вкратце то вот что включают в себя функции, предоставляемые классом Widget:
- следит за отношениями родитель/наследник между виджетами (PropertiesMixin)
- расширенное управление жизненным циклом с функциями безопасности (например
- автоматическое уничтожение дочерние виджеты при уничтожении родителя)
- автоматический рендеринг с помощью qweb
- различные вспомогательные функции, помогающие взаимодействовать с внешним окружением.
Вот простой пример виджета счетчика:
var Widget = require('web.Widget');
var Counter = Widget.extend({
template: 'some.template',
events: {
'click button': '_onClick',
},
init: function (parent, value) {
this._super(parent);
this.count = value;
},
_onClick: function () {
this.count++;
this.$('.val').text(this.count);
},
});
В этом примере мы предполагаем, что шаблон some.template (загружен правильно: шаблон находится в файле, который правильно определен в ключе qweb в манифесте модуля) определяется как:
<div t-name="some.template">
<span class="val"><t t-esc="widget.count"/></span>
<button>Increment</button>
</div>
Этот пример виджета можно использовать следующим образом:
// Create the instance
var counter = new Counter(this, 4);
// Render and insert into DOM
counter.appendTo(".some-div");
Этот пример иллюстрирует несколько функций класса Widget, включая систему событий, систему шаблонов, конструктор с начальным аргументом parent.
Жизненный цикл виджета
Как и во многих системах компонентов, класс виджетов имеет четко определенный жизненный цикл. Обычный жизненный цикл следующий: вызывается init, затем willStart, затем выполняется рендеринг, затем start и наконец destroy.
Widget.init(parent)
это конструктор. Предполагается, что метод init инициализирует базовое состояние виджета. Он работает синхронно и может быть переопределен так, чтобы получить больше параметров от создателя/родителя виджета
- parent (
Widget()
) – родитель нового виджета, используемый для обеспечения автоматического уничтожения и исполнения событий. Может бытьnull
, если мы не желаем, чтобы виджет не имел родителя.
Widget.willStart()
this method will be called once by the framework when a widget is created and in the process of being appended to the DOM. The willStart method is a hook that should return a deferred. The JS framework will wait for this deferred to complete before moving on to the rendering step. Note that at this point, the widget does not have a DOM root element. The willStart hook is mostly useful to perfom some asynchronous work, such as fetching data from the server
[Rendering]()
Этот шаг автоматически выполняется фреймворком. Он проверяет, определен ли ключ шаблона в виджете. Если да, то отреднерит этот шаблон с ключом widget, связанным с виджетом в контексте рендеринга (см. Пример выше: мы используем widget.count в шаблоне QWeb для чтения значения из виджета ). Если шаблон не определен, мы читаем ключ tagName и создаем соответствующий элемент DOM. Когда рендеринг завершен, мы устанавливаем результат как свойство $el для этого виджета. После этого мы автоматически связываем все события в событиях и ключах custom_events.
Widget.start()
когда рендеринг будет завершен, фреймворк автоматически вызовет метод start. Это полезно для выполнения специализированных задач. Например, настройка библиотеки.
Must return a deferred to indicate when its work is done.
Widget.destroy()
Это последний шаг в жизни виджета. Когда виджет уничтожается, мы выполняем все необходимые операции очистки: удаление виджета из дерева компонентов, открепление всех событий, …
Вызывается автоматически, когда родительский элемент виджета уничтожается, должен вызываться явно, если у виджета нет родительского элемента или он удален, но его родительский элемент остается.
Обратите внимание, что willStart и метод start не обязательно вызываются. Виджет можно создать (если вызвать метод init), а затем уничтожить (метод destroy), даже не добавив его в DOM. Если это так, willStart и start даже не будут вызваны.
API Виджетов
Widget.tagName
Используется, если виджет не имеет определенного шаблона. По умолчанию div
, будет использоваться в качестве имени тега для создания элемента DOM, который будет установлен как корень DOM виджета. Можно дополнительно настроить этот сгенерированный корень DOM со следующими атрибутами:
Widget.Widget.id
Используется для генерации атрибута id
в корне сгенерированного DOM. Обратите внимание, что это редко требуется и потенциально узким местом будет, если виджет можно использовать более одного раза.
Widget.className
Используется для генерации атрибута class
в корне сгенерированного DOM. Обратите внимание, что на самом деле он может содержать более одного класса css: „some-class other-class“
Widget.attributes
Сопоставление (строковый объект) имен атрибутов с значениями атрибутов. Каждая из пар ключ: значение будет установлена как атрибут DOM в корне сгенерированного DOM.
Widget.el
raw DOM element set as root to the widget (only available after the start lifecyle method)
Widget.$el
jQuery wrapper around el
. (only available after the start
lifecyle method)
Widget.template
Должно быть установлено имя QWeb шаблона. Если установлено, шаблон будет рендериться после инициализации виджета, но до его запуска. Корневой элемент, сгенерированный шаблоном, будет установлен как корневой DOM виджета.
xmlDependencies
List of paths to xml files that need to be loaded before the widget can be rendered. This will not induce loading anything that has already been loaded.
events
События - это сопоставление селектора событий (имя события и необязательный селектор CSS, разделенные пробелом) и колбэка. Колбэк может быть именем метода виджета или функционального объекта. В любом случае, this
будет установлен в widget
events: {
'click p.oe_some_class a': 'some_method',
'change input': function (e) {
e.stopPropagation();
}
},
Селектор используется для передачи событий фрейворку jQuery, данный колбэк будет инициирован только для потомков элемента DOM, соответствующих селектору. Если селектор не указан (указывается только имя события), событие будет установлено непосредственно в корне DOM виджета.
Примечание: использование встроенной функции не рекомендуется, и, вероятно, будет в будущем будет удалено.
custom_events
это почти то же самое, что и атрибут events, но ключами являются произвольные строкам. Они представляют бизнес-события, запускаемые вспомогательными виджетами. Когда событие инициируется, оно «всплывает» в дереве виджетов (подробности в разделе «Связь с компонентами»).
Widget.isDestroyed()
true
если виджет собирается или будет уничтожен, false
в остальных случаяхWidget.$(selector)
Применяет CSS-селектор, указанный в качестве параметра к корню DOM виджета:
this.$(selector);
функционально идентичен:
this.$el.find(selector);
- selector (
String
) – Селектор CSS
Примечание
Этот вспомогательный метод похож на Backbone.View.$
Widget.setElement(element)
Переустанавливает корень DOM виджета на предоставленный элемент, также обрабатывает повторную установку различных алиасов корня DOM, а также отмену и переустановку делегированных событий.
- element (
Element
) – Элемент DOM или объект jQuery для установки в качестве корня DOM виджета
Добавление виджета в DOM
Widget.appendTo(element)
Рендерит виджет и вставляет его в качестве последнего потомка целевого объекта, использует .appendTo()
Widget.prependTo(element)
Рендерит виджет и вставляет его в качестве первого потомка цели, использует .prependTo()
Widget.insertAfter(element)
Рендерит виджет и вставляет его зразу целевым элементом, использует .insertAfter()
Widget.insertBefore(element)
Рендерит виджет и вставляет его перед целевым элементом, использует`.insertBefore()`_
All of these methods accept whatever the corresponding jQuery method accepts (CSS selectors, DOM nodes or jQuery objects). They all return a deferred and are charged with three tasks:
- Рендеринг корневого элемента виджета с помощью
renderElement()
- вставка корневого элемента виджета в DOM с использованием любого подходящего jQuery
- метода, которому они соответствуют
- запуск виджета и возврат результата его запуска
Правила оформления виджетов
- Старайтесь избегать идентификаторов (атрибут
id
) Старайтесь избегать идентификаторов (атрибут
id
). В общих приложениях и модуляхid
ограничивает возможность повторного использования компонентов и делает код более хрупким. В большинстве случаев они могут быть просто удалены, заменены классами или содержать ссылку на узел DOM или элемент jQuery.Если
id
вам абсолютно необходим (например потому что этого требует сторонняя библиотека), то id должен быть частично сгенерирован с помощью_uniqueId()
, например:this.id = _.uniqueId('my-widget-');
- Старайтесь избегать идентификаторов (атрибут
- Избегайте предсказуемых/общих имен классов CSS. Имена классов, такие как «content» или «navigation», могут соответствовать желаемому смыслу/семантике, но вполне вероятно что другой разработчик будет точно так же, создавая конфликт имен и непонятное поведение. Имена универсальных классов должны начинаться с префикса, например, именем компонента, к которому они принадлежат (создание «неформальных» пространств имен, как в C или Objective-C).
- Следует избегать глобальных селекторов. Так как компонент может использоваться несколько раз на одной странице (пример в Dashboard), запросы должны быть ограничены областью заданного компонента. Непродуманные варианты, такие как
$(selector)
илиdocument.querySelectorAll(selector)
, обычно приводят к непредсказуемому поведению. КлассWidget()
имеет атрибут, предоставляющий свой корень DOM ($el
), и ссылку для выбора узлов напрямую ($()
). - В целом исходите из того, что ваши компоненты не владеют и не контролируют что-либо за пределами своего личного
$el
(поэтому избегайте использования ссылки на родительский виджет) - Html шаблонизатор/рендеринг должен использовать QWeb, если он не является очень простым.
- Все интерактивные компоненты (компоненты, отображающие информацию на экране или перехватывающие события DOM) должны наследовать
Widget()
и правильно реализовывать и использовать свой API и жизненный цикл.
Генератор шаблонов QWeb
Веб-клиент использует генератор шаблонов QWeb для рендерирга виджетов (если они не переопределяют метод renderElement для выполнения чего-то еще). Движок Qweb JS основан на XML и в большинстве случаев совместим с реализацией на Python.
Теперь давайте объясним, как загружаются шаблоны. Всякий раз, когда запускается веб-клиент, выполняется RPC запрос к url-маршруту /web/webclient/qweb. Затем сервер вернет список всех шаблонов, определенных в файлах данных для каждого установленного модуля. Нужные файлы перечислены по ключу qweb в манифесте каждого модуля.
Веб-клиент будет ждать загрузки этого списка шаблона, прежде чем запускать свой первый виджет.
Этот механизм работает в целом неплохо, но иногда мы хотим загрузить шаблон в фоне. Например, представьте, что у нас есть виджет, который используется редко. В этом случае, вероятно, мы предпочитаем не загружать его шаблон в основной файл, чтобы сделать веб-клиент немного легче. В этом случае мы можем использовать ключ виджета xmlDependencies:
var Widget = require('web.Widget');
var Counter = Widget.extend({
template: 'some.template',
xmlDependencies: ['/myaddon/path/to/my/file.xml'],
...
});
При этом виджет Counter загрузит файлы указанные в xmlDependencies при работе метода willStart, поэтому шаблон будет готов к выполнению рендеринга.
Система событий
В настоящее время Odoo поддерживает две системы событий: простую систему, которая позволяет добавлять слушателей и инициировать события, и более совершенную систему, которая заставляет события «всплывать».
Обе эти системы событий реализованы в EventDispatcherMixin, в файле mixins.js. Этот миксин входит в класс Widget.
Базовая система обмена сообщениями
Эта система событий была исторически первой. Он реализует простой шаблон шины. У нас есть 4 основных метода:
- on: используется для регистрации слушателя на событии.
- off: полезна открепления слушателя от события.
- once: используется для регистрации слушателя, который будет вызываться только один раз.
- trigger: инициировать событие. Вызывает каждого слушателя.
Вот пример того, как можно использовать эту систему событий:
var Widget = require('web.Widget');
var Counter = require('myModule.Counter');
var MyWidget = Widget.extend({
start: function () {
this.counter = new Counter(this);
this.counter.on('valuechange', this, this._onValueChange);
var def = this.counter.appendTo(this.$el);
return $.when(def, this._super.apply(this, arguments);
},
_onValueChange: function (val) {
// do something with val
},
});
// in Counter widget, we need to call the trigger method:
... this.trigger('valuechange', someValue);
Предупреждение
использование этой системы событий не рекомендуется, мы планируем заменить каждый метод trigger на метод trigger_up из расширенной системы событий
Расширенная система событий
Пользовательские события виджетов - это более продвинутая система, которая имитирует API событий DOM. Всякий раз, когда событие инициируется, оно «всплывает» в дереве компонентов, пока не достигнет корневого виджета или не будет остановлено.
- trigger_up: это метод, который создаст OdooEvent и отправит(задиспатчит) его в дерево компонентов. Обратите внимание, что это начнется с компонента, который вызвал событие
- custom_events: это эквивалент словаря event, но для событий odoo.
Класс OdooEvent очень прост. Он имеет три публичных атрибута: target (виджет, который вызвал событие), name (имя события) и data (полезная нагрузка). Он также имеет 2 метода: stopPropagation и is_stopped.
Предыдущий пример можно обновить, чтобы использовать пользовательскую систему событий:
var Widget = require('web.Widget');
var Counter = require('myModule.Counter');
var MyWidget = Widget.extend({
custom_events: {
valuechange: '_onValueChange'
},
start: function () {
this.counter = new Counter(this);
var def = this.counter.appendTo(this.$el);
return $.when(def, this._super.apply(this, arguments);
},
_onValueChange: function(event) {
// do something with event.data.val
},
});
// in Counter widget, we need to call the trigger_up method:
... this.trigger_up('valuechange', {value: someValue});
Реестр
Основной необходимостью в экосистеме Odoo является расширение/изменение поведения базовой системы извне (путем установки приложения, то есть другого модуля). Например, может потребоваться добавить новый тип виджета для какого-то представления. Как правило, вы создаете нужный компонент, а затем добавление его в реестр (этап регистрации), чтобы остальная часть веб-клиента знала о его существовании.
В системе доступно несколько реестров:
- реестр полей (экспортируется через
web.field_registry
). Поле реестра содержит все виджеты полей известные веб-клиенту. Всякий раз, когда представлению (чаще всего это Form, List или Kanban) нужен виджет поля, именно здесь клиент и будет его искать. Типичный пример использования выглядит следующим образом:
var fieldRegistry = require('web.field_registry'); var FieldPad = ...; fieldRegistry.add('pad', FieldPad);
Обратите внимание, что каждое значение должно быть подклассом AbstractField
- реестр полей (экспортируется через
- реестр представлений: этот реестр содержит все представления JS, известные веб-клиенту
- (и в том числе менеджер представлений). Каждое значение этого реестра должно быть подклассом AbstractView
- Реестр действий: мы отслеживаем все действия клиента в этом реестре.
- Именно здесь менеджер действий ищет каждый раз, когда ему нужно создать клиентское действие. В версии 11 каждое значение должно быть просто подклассом Widget. Однако в версии 12 значения должны быть AbstractAction.
Коммуникация между виджетами
Существует множество путей для коммуникации между компонентами.
- От родительского компонента к дочернему:
это простой случай. Родительский виджет может просто вызвать метод своего потомка:
this.someWidget.update(someInfo);
- От виджета к его родителю/более далекому предку:
в этом случае работа виджета состоит в том, чтобы просто уведомить его окружение о том, что что-то произошло. Поскольку мы не хотим, чтобы виджет имел ссылку на своего родителя (это связывало бы виджет с реализацией его родителя), наилучшим способом для таких случаев является запуск события, которое будет «всплывать» в дереве компонентов, используя метод
trigger_up
:this.trigger_up('open_record', { record: record, id: id});
Это событие будет вызвано в виджете, затем «всплывет» и будет в конечном итоге перехвачено кем-то из его предков:
var SomeAncestor = Widget.extend({ custom_events: { 'open_record': '_onOpenRecord', }, _onOpenRecord: function (event) { var record = event.data.record; var id = event.data.id; // do something with the event. }, });
- Межкомпнентное взаимодействие:
Межкомпонентное взаимодействие достигается с помощью шины. Это не самый корректный способ общения, т.к. он усложненяет поддержку кода. Тем не менее, он имеет преимущество, которое закючается в том что он позволяет взаимодействовать несвязанным компонентам. Вы просто прослушиваете событие на шине. Например:
// in WidgetA var core = require('web.core'); var WidgetA = Widget.extend({ ... start: function () { core.bus.on('barcode_scanned', this, this._onBarcodeScanned); }, }); // in WidgetB var WidgetB = Widget.extend({ ... someFunction: function (barcode) { core.bus.trigger('barcode_scanned', barcode); }, });
В этом примере мы используем шину, экспортированную из web.core, но это не обязательно. Шина может быть создана для определенной цели.
Сервисы
В версии 11.0 мы ввели понятие [UNKNOWN NODE problematic]service [UNKNOWN NODE problematic]. Основная идея состоит в том, чтобы предоставить субкомпонентам контролируемый способ доступа к их окружению, таким образом, чтобы фреймворк был контролируемым и тестируемым.
Система серсисов организована вокруг трех идей: сервисы, провайдеры сервисов и виджеты. Это работает так: виджеты инициируют (с помощью trigger_up) события, эти события всплывают перед провайдером, который попросит сервис выполнить задачу, а затем, возможно, вернет ответ.
Сервис
Сервис является экземпляром класса AbstractService. В основном это имя и несколько методов. Его задача - выполнять какую-то работу, обычно это зависит от конкретного окружения.
Например, у нас есть сервис ajax (работа заключается в выполнении запросов rpc), localStorage (взаимодействие с локальным хранилищем браузера) и многие другие.
Вот упрощенный пример того, как реализован сервис ajax:
var AbstractService = require('web.AbstractService');
var AjaxService = AbstractService.extend({
name: 'ajax',
rpc: function (...) {
return ...;
},
});
Этот сервис называется ajax и определяет один метод, rpc.
Провайдер сервиса
Чтобы сервисы работали, необходимо, чтобы у нас был провайдер, готовый отправлять пользовательские события. В backend (веб-клиент) это делается основным экземпляром веб-клиента. Обратите внимание, что код для провайдера взят из ServiceProviderMixin.
Виджет
Виджет - это та часть, которая запрашивает сервис. Для этого он просто запускает событие call_service (обычно с помощью вспомогательной функции call). Это событие всплывет и передаст интент остальной части системы.
На практике некоторые функции вызываются так часто, что у нас есть несколько вспомогательных функций, облегчающих их использование. Например, метод [UNKNOWN NODE problematic]_rpc [UNKNOWN NODE problematic], который помогает осуществлять запросы rpc.
var SomeWidget = Widget.extend({
_getActivityModelViewID: function (model) {
return this._rpc({
model: model,
method: 'get_activity_view_id'
});
},
});
Предупреждение
Если виджет уничтожен, он будет отделен от основного дерева компонентов и не будет иметь родителя. В этом случае события не будут «всплывать», что означает, что работа не будет выполнена. Это как раз то поведение, которое мы ожидаем от уничтоженного виджета.
Запрсы RPC
Функциональность rpc запросов предоставляется службой ajax. Но большинство людей будет взаимодействовать только со вспомогательной функцией _rpc.
При работе с Odoo есть два варианта использования: одному может потребоваться вызвать метод в модели (python) (это происходит через контроллер call_kw), или может потребоваться непосредственный вызов контроллера (доступного по какому то url-маршруту).
- Вызов методе в модели python:
return this._rpc({
model: 'some.model',
method: 'some_method',
args: [some, args],
});
- Прямой вызов контроллера:
return this._rpc({
route: '/some/route/',
params: { some: kwargs},
});
Управление переводами
Часть переводов выполняются на стороне сервера (в основном это все текстовые строки, отображаемые или обрабатываемые сервером), но в статических файлах тоже есть строки, которые необходимо перевести. На данный момент это работает следующим образом:
- каждая переводимая строка помечается специальной функцией _t (доступна в модуле JS web.core)
- эти строки будут использоваться сервером для генерации правильных PO-файлов
- всякий раз, когда веб-клиент загружается, он вызывает url-маршрут /web/webclient/translations, который возвращает список всех переводимых терминов
- во время выполнения всякий раз, когда вызывается функция _t, она будет искать в этом списке термин, чтобы найти его перевод, и вернуть его или исходную строку, если ничего не найдено.
Обратите внимание, что система переводов более подробно, с точки зрения сервера, описана в документе Перевод Модулей.
В javascript есть две важные функции для перевода: _t и _lt. Разница в том, что _lt исполняется в фоне.
var core = require('web.core');
var _t = core._t;
var _lt = core._lt;
var SomeWidget = Widget.extend({
exampleString: _lt('this should be translated'),
...
someMethod: function () {
var str = _t('some text');
...
},
});
В этом примере _lt необходим, потому что переводы не готовы при загрузке модуля.
Обратите внимание, что функции перевода требуют внимательного обращения. Строка, указанная в аргументе, не должна быть динамической.
Представления
Слово «представление» имеет более одного значения. Этот раздел о дизайне представлений со стороны javascript кода, а не о структуре arch или о чем-то еще.
В 2017 году Odoo заменил предыдущий код представления новой архитектурой. Основной задачей было отделить логику рендеринга от логики модели.
Представления (в общем смысле) теперь описываются четырьмя частями: представление, контроллер, рендерер и модель. API этих 4 частей описан в классах AbstractView, AbstractController, AbstractRenderer и AbstractModel.
Представление это фабрика. Его работа состоит в том, чтобы получить набор полей, arch, context и некоторые другие параметры, а затем создать триплет Контроллер/Рендерер/Модель.
Роль представления заключается в правильной настройке каждого фрагмента шаблона MVC с правильной информацией. Обычно он должен обрабатывать строку arch и извлекать данные, необходимые для других частей представления.
Обратите внимание, что представление является классом, а не виджетом. Как только его работа будет выполнена, его можно выбросить.
у рендерера есть одно задание: представлять данные, в виде DOM элемента. Каждое представление может отображать данные по-своему. Кроме того, он должен прислушиваться к соответствующим действиям пользователя и при необходимости уведомлять своего родителя (Контроллер).
Рендерер - это V в шаблоне MVC.
- Модель: ее работа заключается в том, чтобы извлекать и хранить состояние представления. Обычно он представляет собой набор записей в базе данных. Модель является владельцем «бизнес-данных». Это М в шаблоне MVC.
Контроллер: его работа заключается в координации рендерера и модели. Кроме того, это основная точка входа для остальной части веб-клиента. Например, когда пользователь изменяет что-либо в окне поиска, будет вызван метод update контроллера с соответствующей информацией.
Это C в шаблоне MVC.
Примечание
Код JS для представлений был разработан для использования вне контекста менеджера представлений/менеджера действий. Они могут быть использованы в действиях клиента или отображаться на общедоступном веб-сайте (придется немного повозится с ассетами).
Виджеты поля
Большая часть работы с веб-клиентом связана с редактированием и созданием данных. Основная часть этой работы выполняется с помощью виджетов поля, которые осведомлены как о типе поля так и о конкретных деталях того, как значение должно отображаться и редактироваться.
AbstractField
Класс AbstractField является базовым классом для всех виджетов в представлении, которые его поддерживают (в настоящее время: Form, List, Kanban).
Существует много различий между виджетами полей v11 и предыдущими версиями. Отметим наиболее важные из них:
- виджеты распределяются между всеми представлениями (ну, Form/List/ Kanban). Больше не нужно дублировать реализацию. Обратите внимание, что можно иметь специализированную версию виджета для представления, добавив к имени префикса имя представления в реестре представлений: list.many2one будет выбран с для*many2one*.
- виджеты больше не являются владельцем значения поля. Они только представляют данные и связываются с остальной частью представления.
- виджетам больше не нужно переключаться между режимами редактирования и чтения. Теперь, когда такое изменение необходимо, виджет будет уничтожен и снова отрендерен. Это не проблема, так как они в любом случае не владеют значениями, указанными в них
- виджеты полей можно использовать за пределами представления. Их API немного неудобно, но они разработаны разработаны для того, чтобы быть автономными.
Нереляционные поля
Здесь мы документируем все нереляционные поля, доступные по умолчанию, в произвольном порядке.
- integer (FieldInteger)
Это тип поля по умолчанию для полей типа integer.
- Поддерживаемые типы полей: integer
- float (FieldFloat)
Это тип поля по умолчанию для полей типа float.
- Поддерживаемые типы полей: float
Атрибуты:
- digits: отображаемая точность
<field name="factor" digits="[42,5]"/>
- float_time (FieldFloatTime)
Цель этого виджета - правильно отобразить значение float , представляющее временной интервал (в часах). Так, например, 0.5 должно быть отформатировано как 0:30, или 4.75 соответствует 4:45.
- Поддерживаемые типы полей: float
- boolean (FieldBoolean)
Это тип поля по умолчанию для полей типа boolean.
- Поддерживаемые типы полей: boolean
- char (FieldChar)
Это тип поля по умолчанию для полей типа char.
- Поддерживаемые типы полей: char
- date (FieldDate)
Это тип поля по умолчанию для полей типа date. Обратите внимание, что он также работает с полями datetime. При форматировании дат использует часовой пояс текущего сеанса.
- Поддерживаемые типы полей: date, datetime
Параметры:
- datepicker: дополнительные настройки для виджета * datepicker widget*.
<field name="datefield" options='{"datepicker": {"daysOfWeekDisabled": [0, 6]}}'/>
- datetime (FieldDateTime)
Это тип поля по умолчанию для полей типа datetime.
- Поддерживаемые типы полей: date, datetime
Параметры:
- datepicker: дополнительные настройки для виджета * datepicker widget*.
<field name="datetimefield" options='{"datepicker": {"daysOfWeekDisabled": [0, 6]}}'/>
- monetary (FieldMonetary)
Это тип поля по умолчанию для полей типа monetary. Используется для отображения валюты. Если в параметре задано поле валюты, он будет использовать его, в противном случае он вернется к валюте по умолчанию (которая указана в текущем сеансе)
- Поддерживаемые типы полей: monetary, float
Параметры:
- currency_field: имя другого поля, которое должно быть ссылкой many2one на валюту.
<field name="value" widget="monetary" options="{'currency_field': 'currency_id'}"/>
- text (FieldText)
Это тип поля по умолчанию для полей типа text.
- Поддерживаемые типы полей: text
- handle (HandleWidget)
This field’s job is to be displayed as a handle in a list view, and allow reordering the various records by drag and dropping lines.
- Поддерживаемые типы полей: integer
- email (FieldEmail)
В этом поле отображается адрес электронной почты. Основная причина его использования заключается в том, что он рендерится как связующий тег с соответствующей ссылкой в режиме только для чтения.
- Поддерживаемые типы полей: char
- phone (FieldPhone)
В этом поле отображается номер телефона. Основание для его использования заключается в том, что он имеет правильно оформленный тег с нужным href в режиме только для чтения, и только в некоторых случаях: мы хотим сделать его кликабельным, если устройство может вызвать этот конкретный номер.
- Поддерживаемые типы полей: char
- url (UrlWidget)
В этом поле отображается URL (в режиме только для чтения). Основная причина его использования в том, что он отображается как правильно оформленный тег с соответствующими классами CSS и href.
- Поддерживаемые типы полей: char
Кроме того, текст тега можно настроить с помощью атрибута text (он не изменит значение href).
<field name="foo" widget="url" text="Some URL"/>
- domain (FieldDomain)
Поле «Domain» позволяет пользователю создать домен с техническим префиксом благодаря древовидному интерфейсу и просматривать выбранные записи в режиме реального времени. Также имеется вход в режиме отладки , чтобы иметь возможность напрямую вводить префиксный домен char (или создавать расширенные домены, которые не позволяет древовидный интерфейс).
Обратите внимание, что это ограничено «статическим» доменом (без динамического выражения или доступа к переменной context).
- Поддерживаемые типы полей: char
- link_button (LinkButton)
Виджет LinkButton фактически просто отображает тег span с иконкой и текстовым значением в качестве содержимого. Ссылка кликабельна и откроет новое окно браузера со значением в качестве URL.
- Поддерживаемые типы полей: char
- image (FieldBinaryImage)
Этот виджет используется для представления двоичного значения в изображение. В некоторых случаях сервер возвращает «bin_size» вместо реального изображения (bin_size - это строка, представляющая размер файла, например 6,5 КБ). В этом случае виджет создаст тег image с атрибутом src со ссылкой изображения на сервере.
- Поддерживаемые типы полей: binary
Параметры:
- preview_image: если изображение загружается только как «bin_size», тогда этот параметр полезен для того, чтобы сообщить веб-клиенту, что именем поля по умолчанию является не имя текущего поля, а имя другого поля.
<field name="image" widget='image' options='{"preview_image":"image_medium"}'/>
- binary (FieldBinaryFile)
Виджет, позволяющий сохранять/загружать двоичный файл.
- Поддерживаемые типы полей: binary
Атрибут:
- filename: при сохранении файла будет утеряно его имя, так как он сохраняет только двоичное значение. Имя файла может быть сохранено в другом поле. Для этого в качестве атрибута filename должно быть установлено поле, объявленное в представлении.
<field name="datas" filename="datas_fname"/>
- priority (PriorityWidget)
Этот виджет отображается в виде набора звездочек, он разрешает пользователю щелкнуть по нему, чтобы выбрать значение. Это может быть полезно, например, чтобы пометить задачу как приоритетную.
Обратите внимание, что этот виджет работает и в режиме «только чтение», что необычно.
- Поддерживаемые типы полей: selection
- attachment_image (AttachmentImage)
Виджет изображений для полей many2one. Если поле установлено, этот виджет будет отображаться как изображение с соответствующим src url. Этот виджет имеет одинаковое поведение в режиме редактирования или только для чтения, он применятеся только для просмотра изображения.
- Поддерживаемые типы полей: many2one
<field name="displayed_image_id" widget="attachment_image"/>
- image_selection (ImageSelection)
Разрешить пользователю выбирать значение, при нажатии на изображение.
- Поддерживаемые типы полей: selection
Параметры: словарь с сопоставлением значения выбора на с URL-адресом для изображения (image_link) и предварительным просмотром изображения (preview_link).
Обратите внимание, что этот парметр не является обязательным!
<field name="external_report_layout" widget="image_selection" options="{ 'background': { 'image_link': '/base/static/img/preview_background.png', 'preview_link': '/base/static/pdf/preview_background.pdf' }, 'standard': { 'image_link': '/base/static/img/preview_standard.png', 'preview_link': '/base/static/pdf/preview_standard.pdf' } }"/>
- label_selection (LabelSelection)
Этот виджет рендерит простой не редактируемый label. Это полезно только для отображения информации, а не для ее редактирования.
- Поддерживаемые типы полей: selection
Параметры:
- classes: сопоставление значения selection с классом CSS
<field name="state" widget="label_selection" options="{ 'classes': {'draft': 'default', 'cancel': 'default', 'none': 'danger'} }"/>
- state_selection (StateSelectionWidget)
Это специализированный виджет выбора. Предполагается, что запись имеет несколько жестко закодированных полей, присутствующих в представлении: stage_id, legend_normal, legend_blocked, legend_done. В основном он используется для отображения и изменения состояния задачи в проекте, а в раскрывающемся списке отображается дополнительная информация.
- Поддерживаемые типы полей: selection
<field name="kanban_state" widget="state_selection"/>
- kanban_state_selection (StateSelectionWidget)
Это точно такой же виджет, что и state_selection
- Поддерживаемые типы полей: selection
- boolean_favorite (FavoriteWidget)
Этот виджет отображается как пустая (или нет) звезда, в зависимости от логического значения. Обратите внимание, что он также может быть отредактирован в режиме только для чтения.
- Поддерживаемые типы полей: boolean
- boolean_button (FieldBooleanButton)
Виджет Boolean Button предназначен для использования на кнопке статистики в представлении Form. Цель состоит в том, чтобы отобразить симпатичную кнопку с текущим состоянием логического поля (например, «Активно») и разрешить пользователю изменять это поле при нажатии на него.
Обратите внимание, что он также может быть отредактирован в режиме только для чтения.
- Поддерживаемые типы полей: boolean
Параметры:
- terminology: он может быть active, archive, close или произвольным значением сопоставленным с ключами string_true, string_false, hover_true, hover_false
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
- boolean_toggle (BooleanToggle)
- Отображает тумблер для представления логического значения. Это подвид FieldBoolean и часто используется для альтернативного отображения.
- statinfo (StatInfo)
Этот виджет предназначен для представления статистической информации в виде кнопки stat button. Это в основном просто label с числом.
- Поддерживаемые типы полей: integer, float
Параметры:
- label_field: если определено, виджет будет использовать значение label_field в качестве текста.
<button name="%(act_payslip_lines)d" icon="fa-money" type="action"> <field name="payslip_count" widget="statinfo" string="Payslip" options="{'label_field': 'label_tasks'}"/> </button>
- percentpie (FieldPercentPie)
Этот виджет предназначен для представления статистической информации в виде кнопки * stat button*. Это похоже на виджет statinfo, но информация представлена в виде pie диаграммы (от пустого до полного). Обратите внимание, что значение интерпретируется как процент (число от 0 до 100).
- Поддерживаемые типы полей: integer, float
<field name="replied_ratio" string="Replied" widget="percentpie"/>
- progressbar (FieldProgressBar)
Представляет значение в виде индикатора выполнения (от 0 до какого-то значения)
- Поддерживаемые типы полей: integer, float
Параметры:
- title: title of the bar, displayed on top of the bar options
- editable: логическое значение, true если значение доступно для редактирования
- current_value: получить current_value из поля, которое должно присутствовать в представлении
- max_value: получить max_value из поля, которое должно присутствовать в представлении
- edit_max_value: логическое значение, true если max_value доступно для редактирования
- title: заголовок панели, отображаемый в верхней части панели если не переведен, используйте параметр (не опция) вместо «title»
<field name="absence_of_today" widget="progressbar" options="{'current_value': 'absence_of_today', 'max_value': 'total_employee', 'editable': false}"/>
- toggle_button (FieldToggleBoolean)
Этот виджет предназначен для использования в логических полях. Он переключает кнопку между зеленой и серой значениями. Также настраивается всплывающая подсказка, в зависимости от значения и некоторых параметров.
- Поддерживаемые типы полей: boolean
Параметры:
- active: строка для всплывающей подсказки, которая должна быть установлена, если логическое значение true
- inactive: всплывающая подсказка, которая должна быть установлена, когда логическое значение false
<field name="payslip_status" widget="toggle_button" options='{"active": "Reported in last payslips", "inactive": "To Report in Payslip"}' />
- dashboard_graph (JournalDashboardGraph)
Это более специализированный виджет, полезный для отображения графика, представляющего набор данных. Например, он используется в представлении kanban в модуле accounting.
Предполагается, что это поле представляет собой сериализацию набора данных в формате JSON.
- Поддерживаемые типы полей: char
Атрибут
- graph_type: строковое значение, может быть либо „line“ либо „bar“
<field name="dashboard_graph_data" widget="dashboard_graph" graph_type="line"/>
- ace (AceEditor)
Этот виджет предназначен для использования в текстовых полях. Он предоставляет Ace Editor для редактирования XML и Python.
- Поддерживаемые типы полей: char, text
Реляционные поля
class web.relational_fields.FieldSelection()
Поддерживаемые типы полей: selection
web.relational_fields.FieldSelection.placeholder
строка, которая используется для отображения информации, в случаях когда ни одно из значений не выбрано
<field name="tax_id" widget="selection" placeholder="Select a tax"/>
- radio (FieldRadio)
Это подвид поля FieldSelection, но оно специализировано для отображения всех допустимых вариантов выбора в качестве radio переключателей.
Обратите внимание, что при использовании many2one записей, будет сделано больше rpc запросов для получения name_gets связанных записей.
- Типы поддерживаемых полей: selection, many2one
Параметры:
- horizontal: if true, radio buttons will be diplayed horizontally.
<field name="recommended_activity_type_id" widget="radio" options="{'horizontal':true}"/>
- many2one (FieldMany2One)
Виджет по умолчанию для полей many2one.
- Поддерживаемые типы полей: many2one
Атрибуты:
- can_create: разрешить создание связанных записей (имеет приоритет над параметром no_create)
- can_write: разрешить редактирование связанных записей (по умолчанию: true)
Параметры:
- no_create: предотвращает создание связанных записей
- quick_create: разрешает быстрое создание связанных записей (по умолчанию: true)
- no_quick_create: предотвращает быстрое создание связанных записей (не спрашивайте меня)
- no_create_edit: тоже что и no_create, возможно…
- create_name_field: если эта опция установлена, то при создании связанной записи, значение create_name_field будет заполнено значением ввода (по умолчанию: name)
- always_reload: логическое значение, по умолчанию false. Если true, виджет всегда будет делать дополнительный name_get для получения значения имени. Это используется для ситуаций, когда метод name_get переопределяется (пожалуйста, не делайте этого)
- no_open: логическое значение, по умолчанию false. Если установлено значение true, many2one не будет перенаправлять на запись при нажатии на нее (в режиме только для чтения)
<field name="currency_id" options="{'no_create': True, 'no_open': True}"/>
- list.many2one (ListFieldMany2One)
Виджет по умолчания для полей many2one (в представлении List).
Специализация поля many2one для представления List. Основная причина в том, что нам нужно отрендерить many2one поля (в режиме только для чтения) в виде текста, что не позволяет открывать связанные записи.
- Поддерживаемые типы полей: many2one
- kanban.many2one (KanbanFieldMany2One)
Виджет по умолчанию для many2one (в представлении Kanban ). Нам нужно отключить все издания в представлениях Kanban .
- Поддерживаемые типы полей: many2one
- many2many (FieldMany2Many)
Defaut widget for many2many fields.
- Поддерживаемые типы полей: many2many
Атрибуты:
- mode: строковый параметр, представление по умолчанию для отображения
- domain: ограничить данные конкретным доменом
Параметры:
- create_text: разрешить настройку текста, отображаемого при добавлении новой записи
- many2many_binary (FieldMany2ManyBinaryMultiFiles)
Этот виджет помогает пользователю загружать или удалять один или несколько файлов одновременно.
Обратите внимание, что этот виджет специфичен для модели ir.attachment.
- Поддерживаемые типы полей: many2many
- many2many_tags (FieldMany2ManyTags)
Отображает many2many как список тегов.
- Поддерживаемые типы полей: many2many
Параметры:
- color_field: имя числового поля, которое должно присутствовать в представлении. Цвет будет выбран в зависимости от его значения.
<field name="category_id" widget="many2many_tags" options="{'color_field': 'color'}"/>
- form.many2many_tags (FormFieldMany2ManyTags)
Специализация виджета many2many_tags для представлений Form. Он имеет дополнительный код, позволяющий редактировать цвет тега.
- Поддерживаемые типы полей: many2many
- kanban.many2many_tags (KanbanFieldMany2ManyTags)
Специализация виджета many2many_tags для представлений Kanban.
- Поддерживаемые типы полей: many2many
- many2many_checkboxes (FieldMany2ManyCheckBoxes)
Это поле отображает список чекбоксов и позволяет пользователю выбирать подмножество вариантов.
- Поддерживаемые типы полей: many2many
- one2many (FieldOne2Many)
Defaut widget for one2many fields.
Обычно данные отображаются в виде вложенного представления List или в вложенного представления Kanban.
- Поддерживаемые типы полей: one2many
- statusbar (FieldStatus)
Это действительно специализированный виджет для представлений Form. Это панель поверх многих форм, которые представляют поток и позволяют выбрать конкретное состояние.
- Типы поддерживаемых полей: selection, many2one
- reference (FieldReference)
FieldReference является комбинацией select (для модели) и FieldMany2One (для его значения). Это позволяет выбрать запись на произвольной модели.
- Допустимые типы полей: char, reference
- one2many_list (FieldOne2Many)
- This widget is exactly the same as a FieldOne2Many. It is registered at this key only for backward compatibility reasons. Please avoid using this.