Есть много способов решить проблему как в JavaScript, так и в Odoo. Тем не менее, фреймворк Odoo был разработан для того чтобы быть расширяемым (это довольно большое ограничение), и некоторые распространенные проблемы имеют хорошее стандартное решение. Стандартное решение имеет преимущество в том, что его легко понять разработчикам odoo, и, скорее всего, оно будет работать, когда Odoo изменится в процессе развития.
Этот документ пытается объяснить, как можно решить некоторые из этих проблем. Обратите внимание, что это не справка. Это просто случайная коллекция рецептов или объяснения того, как действовать в некоторых случаях.
Прежде всего, помните, что первое правило настройки odoo с помощью JS: попробуйте сделать это в python. Это может показаться странным, но среда Python достаточно расширяема, и многие варианты поведения могут быть реализованы просто с помощью xml или python. Как правило, такое решение имеет более низкую стоимость обслуживания, чем работа с JS:
- сам JS фреймворк имеет тенденцию меняться больше,что приводит к необходимости чаще менять JS код
- часто бывает сложнее реализовать настраиваемое поведение, если ему необходимо взаимодействовать с сервером и должным образом интегрироваться во фреймворк javascript. Фреймворк требует много мелких деталей, которые нужно реплицировать в коде. Например, отзывчивость, или обновление URL, или отображение данных без мерцания.
Примечание
Этот документ на самом деле не объясняет никаких концепций. Это больше поваренная книга. Для более подробной информации, пожалуйста, обратитесь к справочной странице по javascript (см. Javascript в Odoo)
Создание нового поля виджета
Скорее всего это очень распространенный сценарий использования: мы хотим отображать информацию в представлении Form действительно особенным(возможно, бизнес-зависимым) способом. Например, мы хотим изменить цвет текста в зависимости от бизнес-условия.
Это можно сделать в три этапа: создать новый виджет, зарегистрировать его в реестре полей, затем добавить виджет в поле в представления Form
- создание нового виджета:
Это можно сделать, расширив виджет:
var FieldChar = require('web.basic_fields').FieldChar; var CustomFieldChar = FieldChar.extend({ _renderReadonly: function () { // implement some custom logic here }, });
- регистрация в реестре полей:
Веб-клиент должен знать как сопоставить имя виджета и его класс. Это делается реестром:
var fieldRegistry = require('web.field_registry'); fieldRegistry.add('my-custom-field', CustomFieldChar);
- добавляем виджет в представление Form
<field name="somefield" widget="my-custom-field"/>
Обратите внимание, что только поля представлений Form, List и Kanban используют это поле реестра виджетов. Эти представления тесно интегрированы, поскольку представления List и Kanban могут отображаться внутри представления Form).
Изменение существующего виджета поля
Другой вариант использования - мы хотим изменить существующий виджет поля. Например, аддон voip в odoo должен изменить виджет FieldPhone, чтобы добавить возможность легко позвонить по указанному номеру в voip. Это делается путем including виджета FieldPhone, поэтому нет необходимости изменять какое-либо существующее представление Form.
Виджеты полей (экземпляры (подкласса) AbstractField), как и все остальные виджеты, могут быть «пропатчены по обезьяньи». Это выглядит так:
var basic_fields = require('web.basic_fields');
var Phone = basic_fields.FieldPhone;
Phone.include({
events: _.extend({}, Phone.prototype.events, {
'click': '_onClick',
}),
_onClick: function (e) {
if (this.mode === 'readonly') {
e.preventDefault();
var phoneNumber = this.value;
// call the number on voip...
}
},
});
Обратите внимание, что нет необходимости добавлять виджет в реестр, так как он уже зарегистрирован.
Модификация основного виджета из интерфейса
Другим распространенным вариантом использования является необходимость настройки некоторых элементов из пользовательского интерфейса. Например, добавить сообщение в домашнее меню. Обычный процесс в этом случае снова включает include виджета. Это единственный способ сделать это, поскольку для этих виджетов нет реестров.
Обычно это делается с помощью подобного кода:
var HomeMenu = require('web_enterprise.HomeMenu');
HomeMenu.include({
render: function () {
this._super();
// do something else here...
},
});
Создание нового представления (с нуля)
Создание нового представления - более сложная тема. Этот рецепт содержит только те шаги, которые нужно будет выполнить (порядок выполнения не обязательно такой же):
добавление нового типа представления в поле
type
в модель``ir.ui.view``:class View(models.Model): _inherit = 'ir.ui.view' type = fields.Selection(selection_add=[('map', "Map")])
добавление нового типа представления в поле
view_mode
моделиir.actions.act_window.view
:class ActWindowView(models.Model): _inherit = 'ir.actions.act_window.view' view_mode = fields.Selection(selection_add=[('map', "Map")])
- Создание четырех основных частей, из которых создается представление (в JavaScript):
нам нужно представление (субкласс
AbstractView
, это фабрика), рендерер (отAbstractRenderer
), контроллер (отAbstractController
) и модель (fromAbstractModel
). Я предлагаю начать с простого расширения суперклассов:var AbstractController = require('web.AbstractController'); var AbstractModel = require('web.AbstractModel'); var AbstractRenderer = require('web.AbstractRenderer'); var AbstractView = require('web.AbstractView'); var MapController = AbstractController.extend({}); var MapRenderer = AbstractRenderer.extend({}); var MapModel = AbstractModel.extend({}); var MapView = AbstractView.extend({ config: { Model: MapModel, Controller: MapController, Renderer: MapRenderer, }, });
- добавление представления в реестр:
Как обычно, сопоставление между типом представления и его классом должно быть обновлено:
var viewRegistry = require('web.view_registry'); viewRegistry.add('map', MapView);
- реализация четырех основных классов:
- Классу
View
необходимо проанализировать полеarch
и настроить остальные три класса.Renderer
отвечает за представление данных в пользовательском интерфейсе,Model
должен общаться с сервером, загружать данные и обрабатывать их. ИController
предназначен для координации, общения с веб-клиентом, …
- создание представлений в базе данных:
<record id="customer_map_view" model="ir.ui.view"> <field name="name">customer.map.view</field> <field name="model">res.partner</field> <field name="arch" type="xml"> <map latitude="partner_latitude" longitude="partner_longitude"> <field name="name"/> </map> </field> </record>
Кастомизация существующего представления
Предположим, нам нужно создать пользовательскую версию универсального представления. Например, представление Kanban с дополнительным * ribbon-like* виджетом сверху (для отображения пользовательской информации). В этом случае это можно сделать за 3 шага: расширить представление Kanban (что также, вероятно, означает расширение контроллеров/рендеров и/или моделей), затем зарегистрировать представление в реестре представлений и, наконец, использовать представление в атрибуте arch. (конкретный пример - панель инструментов службы поддержки).
- расширение представления:
Вот как это может выглядеть:
var HelpdeskDashboardRenderer = KanbanRenderer.extend({ ... }); var HelpdeskDashboardModel = KanbanModel.extend({ ... }); var HelpdeskDashboardController = KanbanController.extend({ ... }); var HelpdeskDashboardView = KanbanView.extend({ config: _.extend({}, KanbanView.prototype.config, { Model: HelpdeskDashboardModel, Renderer: HelpdeskDashboardRenderer, Controller: HelpdeskDashboardController, }), });
- добавление в реестр представлений:
как обычно, мы должны информировать веб-клиента о сопоставлении имени представления и его класса.
var viewRegistry = require('web.view_registry'); viewRegistry.add('helpdesk_dashboard', HelpdeskDashboardView);
- используем в действующем представлении:
Теперь нам нужно сообщить веб-клиенту, что конкретному
ir.ui.view
необходимо использовать наш новый класс. Обратите внимание, что это особая потребность веб-клиента. С точки зрения сервера у нас все еще есть представление Kanban. Правильный способ сделать это - использовать специальный атрибутjs_class
(который когда-нибудь будет переименован вwidget
, потому что это действительно не очень хорошее имя) в корневом узле arch:<record id="helpdesk_team_view_kanban" model="ir.ui.view" > ... <field name="arch" type="xml"> <kanban js_class="helpdesk_dashboard"> ... </kanban> </field> </record>
Примечание
Примечание: вы можете изменить способ, которым представление интерпретирует структуру arch. Однако, с точки зрения сервера, это все еще представление того же базового типа, подчиняющийся тем же правилам (например, валидация rng). Таким образом, ваши представления все еще должны иметь валидное поле arch.
Promise’ы и асинхронный код
Для очень хорошего и полного введения в Promise, пожалуйста, прочитайте эту превосходную статью https://github.com/getify/You-Dont-Know-JS/blob/1st-ed/async%20%26%20performance/ch3.md
Создание новых Promise
- объявите константу как Promise
В Promise есть две статические функции, которые создают разрешенное или отклоненное обещание на основе константы:
var p = Promise.resolve({blabla: '1'}); // creates a resolved promise p.then(function (result) { console.log(result); // --> {blabla: '1'}; }); var p2 = Promise.reject({error: 'error message'}); // creates a rejected promise p2.catch(function (reason) { console.log(reason); // --> {error: 'error message'); });
Примечание
Обратите внимание, что даже если Promise созданы уже разрешенными или отклоненными, обработчики
then
или``catch`` будут вызываться асинхронно.
- на основе уже асинхронного кода
Предположим, что в функции вы должны выполнить rpc запрос, и когда он будет завершен, установите результат на это событие.
this._rpc
- это функция, которая возвращаетPromise
.function callRpc() { var self = this; return this._rpc(...).then(function (result) { self.myValueFromRpc = result; }); }
- для функции обратного вызова
Предположим, что вы использовали функцию
this.close
, которая принимает в качестве параметра обратный вызов(колбэк), который вызывается по окончании закрытия. Теперь предположим, что вы делаете это в методе, который должен отправить Promise, которое разрешается после завершения закрытия.1 2 3 4 5 6
function waitForClose() { var self = this; return new Promise (function(resolve, reject) { self.close(resolve); }); }
- строка 2: мы сохраняем
this
в переменную, чтобы во внутренней функции мы могли получить доступ к области действия нашего компонента - строка 3: мы создаем и возвращаем новый Promise. Конструктор Promise принимает функцию в качестве параметра. Эта функция сама имеет 2 параметра, которые мы назвали здесь
resolve
иreject
resolve
это функция, которая при вызове переводит Promise в разрешенное состояние.reject
это функция, которая при вызове переводит обещание в отклоненное состояние. Мы не используем отклонение здесь, и это может быть опущено.
- строка 3: мы создаем и возвращаем новый Promise. Конструктор Promise принимает функцию в качестве параметра. Эта функция сама имеет 2 параметра, которые мы назвали здесь
- строка 4: мы вызываем функцию
close
для нашего объекта. Он принимает функцию в качестве параметра (обратный вызов), и получается, чтоresolve
уже является функцией, поэтому мы можем передать ее напрямую. Чтобы быть более понятным, мы могли бы написать:
return new Promise (function (resolve) { self.close(function () { resolve(); }); });
- строка 2: мы сохраняем
- создание генератора Promise (вызов одного Promise за другим в последовательности и ожидание последнего)
Предположим, что вам нужно зациклить массив, выполнить операцию в последовательности и разрешить Promise, когда последняя операция будет выполнена.
function doStuffOnArray(arr) { var done = Promise.resolve(); arr.forEach(function (item) { done = done.then(function () { return item.doSomethingAsynchronous(); }); }); return done; }
Таким образом, Promise, который вы возвращаете, фактически является последним Promise.
- создание Promise, а затем разрешение его вне рамок его определения (анти-шаблон)
Примечание
мы не рекомендуем использовать такое, но иногда это полезно. Сначала тщательно продумайте альтернативы …
... var resolver, rejecter; var prom = new Promise(function (resolve, reject){ resolver = resolve; rejecter = reject; }); ... resolver("done"); // will resolve the promise prom with the result "done" rejecter("error"); // will reject the promise prom with the reason "error"
Ожидание Promise
- ожидание некоторого количества Promise
если у вас есть несколько Promise, которые нужно ждать, вы можете преобразовать их в один Promise, которое будет разрешено, когда все Promise будут разрешены с помощью Promise.all(arrayOfPromises).
var prom1 = doSomethingThatReturnsAPromise(); var prom2 = Promise.resolve(true); var constant = true; var all = Promise.all([prom1, prom2, constant]); // all is a promise // results is an array, the individual results correspond to the index of their // promise as called in Promise.all() all.then(function (results) { var prom1Result = results[0]; var prom2Result = results[1]; var constantResult = results[2]; }); return all;
- ожидая часть цепочки Promise, но не другую часть
Если у вас есть асинхронный процесс, который вы хотите подождать, чтобы что-то сделать, но вы также хотите вернуться к вызывающей стороне, прежде чем что-то сделать.
function returnAsSoonAsAsyncProcessIsDone() { var prom = AsyncProcess(); prom.then(function (resultOfAsyncProcess) { return doSomething(); }); /* returns prom which will only wait for AsyncProcess(), and when it will be resolved, the result will be the one of AsyncProcess */ return prom; }
Обработка ошибок
- в целом в Promise
Общая идея заключается в том, что Promise не должен быт отклонен для потока управления, а должен быть отклонен только для ошибок. В этом случае у вас будет несколько разрешений вашего Promise, например, с кодами состояния, которые вы должны будете проверить в обработчиках
then
и одиночном обработчикеcatch
в конце цепочки Promise.function a() { x.y(); // <-- this is an error: x is undefined return Promise.resolve(1); } function b() { return Promise.reject(2); } a().catch(console.log); // will log the error in a a().then(b).catch(console.log); // will log the error in a, the then is not executed b().catch(console.log); // will log the rejected reason of b (2) Promise.resolve(1) .then(b) // the then is executed, it executes b .then(...) // this then is not executed .catch(console.log); // will log the rejected reason of b (2)
- только для Odoo
В Odoo случается, что мы используем отклонение Promise для потока управления, например в мьютексах и других примитивах параллелизма, определенных в модуле
web.concurrency
. Мы также хотим выполнить перехват по бизнес причинам , но не в случае ошибки при написании кода в определении Promise или обработчиков. Для этого мы ввели понятиеguardedCatch
. Он вызывается какcatch
, но не тогда, когда отклоненная причина является ошибкойfunction blabla() { if (someCondition) { return Promise.reject("someCondition is truthy"); } return Promise.resolve(); } // ... var promise = blabla(); promise.then(function (result) { console.log("everything went fine"); }) // this will be called if blabla returns a rejected promise, but not if it has an error promise.guardedCatch(function (reason) { console.log(reason); }); // ... var anotherPromise = blabla().then(function () { console.log("everything went fine"); }) // this will be called if blabla returns a rejected promise, // but not if it has an error .guardedCatch(console.log);
var promiseWithError = Promise.resolve().then(function () { x.y(); // <-- this is an error: x is undefined }); promiseWithError.guardedCatch(function (reason) {console.log(reason);}); // will not be called promiseWithError.catch(function (reason) {console.log(reason);}); // will be called
Тестирование асинхронного кода
- использование Promise в тестах
В коде тестов мы поддерживаем последнюю версию Javascript, включая такие примитивы, как
async
иawait
. Это делает использование и ожидание Promise очень легким. Большинство вспомогательных методов также возвращают Promise (либо помечая как «async», либо возвращая Promise напрямую.var testUtils = require('web.test_utils'); QUnit.test("My test", async function (assert) { // making the function async has 2 advantages: // 1) it always returns a promise so you don't need to define `var done = assert.async()` // 2) it allows you to use the `await` assert.expect(1); var form = await testUtils.createView({ ... }); await testUtils.form.clickEdit(form); await testUtils.form.click('jquery selector'); assert.containsOnce('jquery selector'); form.destroy(); }); QUnit.test("My test - no async - no done", function (assert) { // this function is not async, but it returns a promise. // QUnit will wait for for this promise to be resolved. assert.expect(1); return testUtils.createView({ ... }).then(function (form) { return testUtils.form.clickEdit(form).then(function () { return testUtils.form.click('jquery selector').then(function () { assert.containsOnce('jquery selector'); form.destroy(); }); }); }); }); QUnit.test("My test - no async", function (assert) { // this function is not async and does not return a promise. // we have to use the done function to signal QUnit that the test is async and will be finished inside an async callback assert.expect(1); var done = assert.async(); testUtils.createView({ ... }).then(function (form) { testUtils.form.clickEdit(form).then(function () { testUtils.form.click('jquery selector').then(function () { assert.containsOnce('jquery selector'); form.destroy(); done(); }); }); }); });
как вы можете видеть, более приятной формой является использование
async/await
, так как это проще и короче писать.