Налаштування веб-клієнта

Небезпека

Цей підручник застарів.

У цьому посібнику йдеться про створення модулів для веб-клієнта Odoo.

Щоб створити веб-сайти за допомогою Odoo, перегляньте Створення веб-сайту; щоб додати бізнес-можливості або розширити існуючі бізнес-системи Odoo, перегляньте Створення модуля.

Попередження

Цей посібник передбачає знання:

Також потрібні встановлений Odoo та Git.

Простий модуль

Почнемо з простого модуля Odoo, який містить базову конфігурацію веб-компонентів і дозволяє тестувати веб-фреймворк.

Зразок модуля доступний онлайн і його можна завантажити за допомогою такої команди:

$ git clone http://github.com/odoo/petstore

Це створить папку petstore, де б ви не виконали команду. Потім вам потрібно додати цю папку до шляху addons Odoo, створити нову базу даних і встановити модуль oepetstore.

Якщо ви переглядаєте папку petstore, ви повинні побачити такий вміст:

oepetstore
|-- images
|   |-- alligator.jpg
|   |-- ball.jpg
|   |-- crazy_circle.jpg
|   |-- fish.jpg
|   `-- mice.jpg
|-- __init__.py
|-- oepetstore.message_of_the_day.csv
|-- __manifest__.py
|-- petstore_data.xml
|-- petstore.py
|-- petstore.xml
`-- static
    `-- src
        |-- css
        |   `-- petstore.css
        |-- js
        |   `-- petstore.js
        `-- xml
            `-- petstore.xml

Модуль вже містить різні настройки сервера. Ми повернемося до них пізніше, а зараз зосередимося на веб-вмісті в папці static.

Файли, які використовуються на «веб-сторінці» модуля Odoo, мають бути розміщені в папці static, щоб вони були доступні для веб-браузера, файли за межами цієї папки не можуть бути отримані браузерами. Під-папки src/css, src/js і src/xml є звичайними і не є суворо необхідними.

oepetstore/static/css/petstore.css

Наразі порожній, зберігатиме CSS для вмісту зоомагазину

oepetstore/static/xml/petstore.xml

Здебільшого порожній, містить шаблони Шаблони QWeb

oepetstore/static/js/petstore.js

Найважливіша (і цікава) частина містить логіку додатку (або принаймні її сторони веб-браузера) у вигляді JavaScript. Зараз це має виглядати так:

odoo.oepetstore = function(instance, local) {
    var _t = instance.web._t,
        _lt = instance.web._lt;
    var QWeb = instance.web.qweb;

    local.HomePage = instance.Widget.extend({
        start: function() {
            console.log("pet store home page loaded");
        },
    });

    instance.web.client_actions.add(
        'petstore.homepage', 'instance.oepetstore.HomePage');
}

Який лише друкує невелике повідомлення в консолі браузера.

Файли в папці static потрібно визначити в модулі, щоб вони завантажувалися правильно. Усе в src/xml визначено в __manifest__.py, тоді як вміст src/css і src/js визначено в petstore.xml, або подібний файл.

Попередження

Усі файли JavaScript об’єднані та minified для покращення часу завантаження додатку.

Одним із недоліків є те, що налагодження стає важчим, оскільки окремі файли зникають, а код стає значно менш читабельним. Можна вимкнути цей процес, увімкнувши «режим розробника»: увійдіть у свій екземпляр Odoo (користувач admin пароль admin за замовчуванням), відкрийте меню користувача (у верхньому правому куті екрана Odoo) і виберіть Про Odoo, потім Активуйте режим розробника:

../../_images/about_odoo.png ../../_images/devmode.png

Це перезавантажить веб-клієнт із вимкненою оптимізацією, що зробить розробку та налагодження значно зручнішими.

Модуль Odoo JavaScript

Javascript не має вбудованих модулів. У результаті змінні, визначені в різних файлах, змішуються разом і можуть конфліктувати. Це призвело до появи різних шаблонів модулів, які використовуються для побудови чистих просторів назв і обмеження ризиків конфліктів назв.

Фреймворк Odoo використовує один такий шаблон для визначення модулів у веб-додатках, щоб створити код простору назв і правильно впорядкувати його завантаження.

oepetstore/static/js/petstore.js містить оголошення модуля:

odoo.oepetstore = function(instance, local) {
    local.xxx = ...;
}

У мережі Odoo модулі оголошуються як функції, встановлені в глобальній змінній odoo. Назва функції має збігатися з назвою додатку (у цьому випадку oepetstore), щоб фреймворк міг знайти її та автоматично ініціалізувати.

Коли веб-клієнт завантажує ваш модуль, він викличе кореневу функцію та надасть два параметри:

  • перший параметр - це поточний екземпляр веб-клієнта Odoo, він надає доступ до різних можливостей, визначених Odoo (переклади, мережеві служби), а також об’єктів, визначених ядром або іншими модулями.

  • другий параметр - ваш власний локальний простір назв, автоматично створений веб-клієнтом. Об’єкти та змінні, які мають бути доступні ззовні вашого модуля (або через те, що веб-клієнт Odoo має викликати їх, або тому, що інші можуть захотіти їх налаштувати), мають бути встановлені в цьому просторі назв.

Класи

Як і модулі, і на відміну від більшості об’єктно-орієнтованих мов, javascript не вбудовується в класи1, хоча він забезпечує приблизно еквівалентні (якщо нижчого рівня та більш докладні) механізми.

Для простоти та зручності для розробників Odoo web надає систему класів на основі Simple JavaScript Inheritance John Resig’s.

Нові класи визначаються викликом методу extend() odoo.web.Class():

var MyClass = instance.web.Class.extend({
    say_hello: function() {
        console.log("hello");
    },
});

Метод extend() приймає словник, що описує вміст нового класу (методи та статичні атрибути). У цьому випадку він матиме лише метод say_hello, який не приймає параметрів.

Класи створюються за допомогою оператора new:

var my_object = new MyClass();
my_object.say_hello();
// print "hello" in the console

А атрибути екземпляра можна отримати через this:

var MyClass = instance.web.Class.extend({
    say_hello: function() {
        console.log("hello", this.name);
    },
});

var my_object = new MyClass();
my_object.name = "Bob";
my_object.say_hello();
// print "hello Bob" in the console

Класи можуть надати ініціалізатор для виконання початкового налаштування екземпляра, визначивши метод init(). Ініціалізатор отримує параметри, передані під час використання оператора new:

var MyClass = instance.web.Class.extend({
    init: function(name) {
        this.name = name;
    },
    say_hello: function() {
        console.log("hello", this.name);
    },
});

var my_object = new MyClass("Bob");
my_object.say_hello();
// print "hello Bob" in the console

Також можна створювати підкласи з існуючих (визначених у використанні) класів, викликаючи extend() у батьківському класі, як це робиться для підкласу Class():

var MySpanishClass = MyClass.extend({
    say_hello: function() {
        console.log("hola", this.name);
    },
});

var my_object = new MySpanishClass("Bob");
my_object.say_hello();
// print "hola Bob" in the console

Перевизначаючи метод за допомогою успадкування, ви можете використовувати this._super() для виклику оригінального методу:

var MySpanishClass = MyClass.extend({
    say_hello: function() {
        this._super();
        console.log("translation in Spanish: hola", this.name);
    },
});

var my_object = new MySpanishClass("Bob");
my_object.say_hello();
// print "hello Bob \n translation in Spanish: hola Bob" in the console

Попередження

_super не є стандартним методом, він встановлюється на льоту до наступного методу в поточному ланцюжку успадкування, якщо такий є. Він визначається лише під час синхронної частини виклику методу, для використання в асинхронних обробниках (після мережевих викликів або зворотних викликів setTimeout) слід зберегти посилання на його значення, до нього не можна отримати доступ через `` це „“:

// broken, will generate an error
say_hello: function () {
    setTimeout(function () {
        this._super();
    }.bind(this), 0);
}

// correct
say_hello: function () {
    // don't forget .bind()
    var _super = this._super.bind(this);
    setTimeout(function () {
        _super();
    }.bind(this), 0);
}

Основи віджетів

Веб-клієнт Odoo включає jQuery для легкого маніпулювання DOM. Він корисний і надає кращий API, ніж стандартний W3C DOM2, але недостатній для структурування складних додатків, що ускладнює обслуговування.

Подібно до об’єктно-орієнтованих інструментів інтерфейсу користувача робочого столу (наприклад, Qt, Cocoa або GTK), Odoo Web робить конкретні компоненти відповідальними за розділи сторінки. В Odoo web основою для таких компонентів є клас Widget(), компонент, що спеціалізується на обробці розділу сторінки та відображенні інформації для користувача.

Ваш перший віджет

Початковий демонстраційний модуль уже містить базовий віджет:

local.HomePage = instance.Widget.extend({
    start: function() {
        console.log("pet store home page loaded");
    },
});

Він розширює Widget() і замінює стандартний метод start(), який, як і попередній MyClass, наразі мало робить.

Цей рядок у кінці файлу:

instance.web.client_actions.add(
    'petstore.homepage', 'instance.oepetstore.HomePage');

реєструє наш базовий віджет як дію клієнта. Дії клієнта буде пояснено пізніше, наразі це саме те, що дозволяє викликати та відображати наш віджет, коли ми вибираємо меню Pet Store ‣ Pet Store ‣ Home Page.

Попередження

оскільки віджет буде викликатися ззовні нашого модуля, веб-клієнту потрібна його «повна» назва, а не локальна версія.

Показати вміст

Віджети мають низку методів і функцій, але основи прості:

  • налаштувати віджет

  • відформатувати дані віджета

  • відобразити віджет

Віджет HomePage вже має метод start(). Цей метод є частиною звичайного життєвого циклу віджета та автоматично викликається, коли віджет вставлено на сторінку. Ми можемо використовувати його для відображення певного вмісту.

Усі віджети мають $el, який представляє розділ сторінки, за який вони відповідають (як об’єкт jQuery). Туди слід вставити вміст віджета. За замовчуванням $el є порожнім елементом <div>.

Елемент <div> зазвичай невидимий для користувача, якщо він не має вмісту (або без певних стилів, що визначають його розмір), тому нічого не відображається на сторінці, коли запускається HomePage.

Давайте додамо трохи вмісту до кореневого елемента віджета за допомогою jQuery:

local.HomePage = instance.Widget.extend({
    start: function() {
        this.$el.append("<div>Hello dear Odoo user!</div>");
    },
});

Тепер це повідомлення з’являтиметься, коли ви відкриватимете Pet Store ‣ Pet Store ‣ Home Page

Примітка

щоб оновити код javascript, завантажений в Odoo Web, вам потрібно буде перезавантажити сторінку. Немає необхідності перезапускати сервер Odoo.

Віджет HomePage використовується Odoo Web і керується ним автоматично. Щоб навчитися використовувати віджет «з чистого аркуша», давайте створимо новий:

local.GreetingsWidget = instance.Widget.extend({
    start: function() {
        this.$el.append("<div>We are so happy to see you again in this menu!</div>");
    },
});

Тепер ми можемо додати наш GreetingsWidget до HomePage за допомогою методу GreetingsWidget appendTo():

local.HomePage = instance.Widget.extend({
    start: function() {
        this.$el.append("<div>Hello dear Odoo user!</div>");
        var greeting = new local.GreetingsWidget(this);
        return greeting.appendTo(this.$el);
    },
});
  • HomePage спочатку додає свій власний вміст до свого кореня DOM

  • HomePage потім створює екземпляр GreetingsWidget

  • Нарешті, він повідомляє GreetingsWidget, куди вставити себе, делегуючи частину свого $el GreetingsWidget.

Коли викликається метод appendTo(), він просить віджет вставити себе у вказане місце та відобразити його вміст. Метод start() буде викликано під час виклику appendTo().

Щоб побачити, що відбувається під інтерфейсом, який відображається, ми скористаємося DOM Explorer браузера. Але спочатку давайте трохи змінимо наші віджети, щоб нам було легше знаходити, де вони знаходяться, додавши клас до їхніх кореневих елементів:

local.HomePage = instance.Widget.extend({
    className: 'oe_petstore_homepage',
    ...
});
local.GreetingsWidget = instance.Widget.extend({
    className: 'oe_petstore_greetings',
    ...
});

Якщо ви можете знайти відповідний розділ DOM (клацніть правою кнопкою миші на тексті, а потім Перевірити елемент), він має виглядати так:

<div class="oe_petstore_homepage">
    <div>Hello dear Odoo user!</div>
    <div class="oe_petstore_greetings">
        <div>We are so happy to see you again in this menu!</div>
    </div>
</div>

На якому чітко видно два елементи <div>, автоматично створені Widget(), оскільки ми додали до них кілька класів.

Ми також можемо побачити два блоки div для зберігання повідомлень, які ми додали самі

Насамкінець зауважте, що елемент <div class="oe_petstore_greetings">, який представляє екземпляр GreetingsWidget, знаходиться всередині <div class="oe_petstore_homepage">, який представляє HomePage, оскільки ми додали

Віджет Parents and Children

У попередній частині ми створили екземпляр віджета за допомогою такого синтаксису:

new local.GreetingsWidget(this);

Першим аргументом є this, який у цьому випадку був екземпляром HomePage. Це повідомляє віджету, який створюється, який інший віджет є його батьківським.

Як ми бачили, віджети зазвичай вставляються в DOM іншим віджетом і всередині кореневого елемента цього іншого віджета. Це означає, що більшість віджетів є «частиною» іншого віджета та існують від його імені. Ми називаємо контейнер батьком, а віджет, що міститься, дочірнім.

Через численні технічні та концептуальні причини віджету необхідно знати, хто є його батьком, а хто є його дочірніми елементами.

getParent()

можна використовувати для отримання батьківського елемента віджета:

local.GreetingsWidget = instance.Widget.extend({
    start: function() {
        console.log(this.getParent().$el );
        // will print "div.oe_petstore_homepage" in the console
    },
});
getChildren()

можна використовувати для отримання списку його дочірніх елементів:

local.HomePage = instance.Widget.extend({
    start: function() {
        var greeting = new local.GreetingsWidget(this);
        greeting.appendTo(this.$el);
        console.log(this.getChildren()[0].$el);
        // will print "div.oe_petstore_greetings" in the console
    },
});

Під час перевизначення методу init() віджета найважливіше передати батьківський елемент виклику this._super(), інакше відношення не буде встановлено правильно:

local.GreetingsWidget = instance.Widget.extend({
    init: function(parent, name) {
        this._super(parent);
        this.name = name;
    },
});

Нарешті, якщо віджет не має батьківського елемента (наприклад, тому що це кореневий віджет додатку), null можна надати як батьківський:

new local.GreetingsWidget(null);

Знищення віджетів

Якщо ви можете відображати вміст своїм користувачам, ви також повинні мати можливість його стерти. Це робиться за допомогою методу destroy():

greeting.destroy();

Коли віджет знищено, він спочатку викличе destroy() для всіх своїх дочірніх. Потім він видаляється з DOM. Якщо ви встановили постійні структури в init() або start(), які потрібно явно очистити (оскільки збирач сміття не оброблятиме їх), ви можете перевизначити destroy().

Небезпека

під час перевизначення destroy(), _super() завжди потрібно викликати, інакше віджет та його дочірні елементи не очищаються належним чином, що призводить до можливих витоків пам’яті та «фантомних подій», навіть якщо помилка не відображається

Механізм шаблонів QWeb

У попередньому розділі ми додали вміст до наших віджетів, безпосередньо маніпулюючи (та додаючи) їх DOM:

this.$el.append("<div>Hello dear Odoo user!</div>");

Це дозволяє генерувати та відображати будь-який тип вмісту, але стає громіздким, коли генерується значна кількість DOM (велика кількість дублікатів, проблеми з цитуванням, …)

Як і багато інших середовищ, рішення Odoo полягає у використанні системи шаблонів. Механізм шаблонів Odoo називається Шаблони QWeb.

QWeb - це мова шаблонів на основі XML, подібна до Genshi, Thymeleaf або Facelets. Він має такі характеристики:

  • Він повністю реалізований у JavaScript і відображається у браузері

  • Кожен файл шаблону (файли XML) містить кілька шаблонів

  • Він має спеціальну підтримку у Widget() Odoo Web, хоча його можна використовувати поза веб-клієнтом Odoo (і можна використовувати Widget(), не покладаючись на QWeb)

Примітка

Обґрунтуванням використання QWeb замість існуючих движків шаблонів javascript є розширюваність уже існуючих (сторонніх) шаблонів, схожих на Odoo представлення.

Більшість движків шаблонів javascript засновані на тексті, що перешкоджає легкому структурному розширюванню, коли рушій шаблонів на основі XML можна загальним чином змінити, використовуючи, наприклад, XPath або CSS і DSL із зміною дерева (або навіть просто XSLT). Ця гнучкість і розширюваність є основною характеристикою Odoo, і її втрата вважалася неприйнятною.

Використання QWeb

Спочатку давайте визначимо простий шаблон QWeb у майже порожньому файлі oepetstore/static/src/xml/petstore.xml:

<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
    <t t-name="HomePageTemplate">
        <div style="background-color: red;">This is some simple HTML</div>
    </t>
</templates>

Тепер ми можемо використовувати цей шаблон у віджеті HomePage. Використовуючи змінну завантажувача QWeb, визначену у верхній частині сторінки, ми можемо викликати шаблон, визначений у файлі XML:

local.HomePage = instance.Widget.extend({
    start: function() {
        this.$el.append(QWeb.render("HomePageTemplate"));
    },
});

QWeb.render() шукає вказаний шаблон, перетворює його на рядок і повертає результат.

Однак, оскільки Widget() має спеціальну інтеграцію для QWeb, шаблон можна встановити безпосередньо у віджеті через його атрибут template:

local.HomePage = instance.Widget.extend({
    template: "HomePageTemplate",
    start: function() {
        ...
    },
});

Хоча результат виглядає схожим, між цими використаннями є дві відмінності:

  • у другій версії шаблон відображається безпосередньо перед викликом start()

  • у першій версії вміст шаблону додається до кореневого елемента віджета, тоді як у другій версії кореневий елемент шаблону безпосередньо встановлюється як кореневий елемент віджета. Ось чому суб-віджет «вітання» також отримує червоний фон

Попередження

шаблони повинні мати один не-t кореневий елемент, особливо якщо вони встановлені як template віджета. Якщо є кілька «кореневих елементів», результати не визначені (зазвичай буде використано лише перший кореневий елемент, а інші ігноруватимуться)

Контекст QWeb

Шаблони QWeb можуть містити дані та містити базову логіку відображення.

Для явних викликів QWeb.render() дані шаблону передаються як другий параметр:

QWeb.render("HomePageTemplate", {name: "Klaus"});

зі зміненим шаблоном на:

<t t-name="HomePageTemplate">
    <div>Hello <t t-esc="name"/></div>
</t>

призведе до:

<div>Hello Klaus</div>

При використанні інтеграції Widget() неможливо надати додаткові дані до шаблону. Шаблону буде надано єдину контекстну змінну widget, що посилається на віджет, який відображається безпосередньо перед викликом start() (стан віджета буде по суті таким, який налаштував init()):

<t t-name="HomePageTemplate">
    <div>Hello <t t-esc="widget.name"/></div>
</t>
local.HomePage = instance.Widget.extend({
    template: "HomePageTemplate",
    init: function(parent) {
        this._super(parent);
        this.name = "Mordecai";
    },
    start: function() {
    },
});

Результат:

<div>Hello Mordecai</div>

Шаблон декларації

Ми бачили, як відображати шаблони QWeb, тепер давайте подивимося на синтаксис самих шаблонів.

Шаблон QWeb складається зі звичайного XML, змішаного з директивами QWeb. Директива QWeb оголошується з атрибутами XML, що починаються з t-.

Найпростішою директивою є t-name, яка використовується для оголошення нових шаблонів у файлі шаблону:

<templates>
    <t t-name="HomePageTemplate">
        <div>This is some simple HTML</div>
    </t>
</templates>

t-name бере назву шаблону, який визначається, і оголошує, що його можна викликати за допомогою QWeb.render(). Його можна використовувати лише на верхньому рівні файлу шаблону.

Екранування

Директиву t-esc можна використовувати для виведення тексту:

<div>Hello <t t-esc="name"/></div>

Він приймає вираз Javascript, який оцінюється, результат виразу потім екранується HTML і вставляється в документ. Оскільки це вираз, можна надати лише назву змінної, як зазначено вище, або більш складний вираз, як-от обчислення:

<div><t t-esc="3+5"/></div>

або виклики методу:

<div><t t-esc="name.toUpperCase()"/></div>

Виведення HTML

Щоб додати HTML на сторінку, що відображається, використовуйте t-raw. Як і t-esc, він приймає довільний вираз Javascript як параметр, але не виконує крок HTML-екранування.

<div><t t-raw="name.link(user_account)"/></div>

Небезпека

t-raw не можна використовувати для будь-яких даних, які можуть містити неекранований вміст, наданий користувачем, оскільки це призводить до уразливості міжсайтового сценарію

Умовні

QWeb може мати умовні блоки за допомогою t-if. Директива приймає довільний вираз, якщо вираз хибний (false, null, 0 або порожній рядок), весь блок пригнічується, інакше він відображається.

<div>
    <t t-if="true == true">
        true is true
    </t>
    <t t-if="true == false">
        true is not true
    </t>
</div>

Примітка

QWeb не має структури «else», використовуйте другий t-if з інвертованою вихідною умовою. Ви можете зберегти умову в локальній змінній, якщо це складний або дорогий вираз.

Ітерація

Для повторення списку використовуйте t-foreach і t-as. t-foreach приймає вираз, який повертає список для ітерації t-as бере назву змінної для прив’язки до кожного елемента під час ітерації.

<div>
    <t t-foreach="names" t-as="name">
        <div>
            Hello <t t-esc="name"/>
        </div>
    </t>
</div>

Примітка

t-foreach також можна використовувати з числами та об’єктами (словниками)

Визначення атрибутів

QWeb надає дві пов’язані директиви для визначення обчислених атрибутів: t-att-name і t-attf-name. У будь-якому випадку name - це назва атрибута, який потрібно створити (наприклад, t-att-id визначає атрибут id після візуалізації).

t-att- приймає вираз javascript, результат якого встановлюється як значення атрибута, це найбільш корисно, якщо обчислюється все значення атрибута:

<div>
    Input your name:
    <input type="text" t-att-value="defaultName"/>
</div>

t-attf- приймає рядок формату. Рядок формату - це літеральний текст із блоками інтерполяції всередині, блок інтерполяції - це вираз javascript між {{ і }}, який буде замінено результатом виразу. Це найбільш корисно для атрибутів, які є частково літеральними та частково обчисленими, наприклад клас:

<div t-attf-class="container {{ left ? 'text-left' : '' }} {{ extra_class }}">
    insert content here
</div>

Виклик інших шаблонів

Шаблони можна розділити на підшаблони (для простоти, зручності обслуговування, багаторазового використання або щоб уникнути надмірної вкладеності розмітки).

Це робиться за допомогою директиви t-call, яка приймає назву шаблону для відтворення:

<t t-name="A">
    <div class="i-am-a">
        <t t-call="B"/>
    </div>
</t>
<t t-name="B">
    <div class="i-am-b"/>
</t>

рендеринг шаблону A призведе до:

<div class="i-am-a">
    <div class="i-am-b"/>
</div>

Підшаблони успадковують контекст візуалізації свого абонента.

Щоб дізнатися більше про QWeb

Для довідки QWeb див. Шаблони QWeb.

Вправа

Exercise

Використання QWeb у віджетах

Створіть віджет, конструктор якого приймає два параметри, окрім parent: product_names і color.

  • product_names має бути масивом рядків, кожен з яких є назвою продукту

  • color - це рядок, що містить колір у форматі кольорів CSS (тобто: #000000 для чорного).

Віджет має відображати дані назви продуктів один під одним, кожне в окремому полі з кольором фону зі значенням color і рамкою. Ви повинні використовувати QWeb для відтворення HTML. Будь-який необхідний CSS має бути в oepetstore/static/src/css/petstore.css.

Використовуйте віджет на HomePage з півдюжиною продуктів.

Помічники віджетів

Селектор jQuery Widget

Вибір елементів DOM у віджеті можна виконати викликом методу find() у корені DOM віджета:

this.$el.find("input.my_input")...

Але оскільки це звичайна операція, Widget() надає еквівалентний ярлик через метод $():

local.MyWidget = instance.Widget.extend({
    start: function() {
        this.$("input.my_input")...
    },
});

Попередження

Глобальну функцію jQuery $() ніколи не слід використовувати, окрім випадків, коли це абсолютно необхідно: виділення в корені віджета поширюються на віджет і є локальними для нього, але виділення з $() є глобальними до сторінки/програми та може відповідати частинам інших віджетів і представлень, що призводить до дивних або небезпечних побічних ефектів. Оскільки віджет, як правило, повинен діяти лише в тому розділі DOM, який йому належить, немає підстав для глобального вибору.

Простіше прив’язування подій DOM

Раніше ми зв’язували події DOM за допомогою звичайних обробників подій jQuery (наприклад, .click() або .change()) для елементів віджета:

local.MyWidget = instance.Widget.extend({
    start: function() {
        var self = this;
        this.$(".my_button").click(function() {
            self.button_clicked();
        });
    },
    button_clicked: function() {
        ..
    },
});

Хоча це працює, у нього є кілька проблем:

  1. це досить багатослівно

  2. він не підтримує заміну кореневого елемента віджета під час виконання, оскільки зв’язування виконується лише під час запуску start() (під час ініціалізації віджета)

  3. це вимагає вирішення проблем, пов’язаних із this

Таким чином, віджети забезпечують ярлик для зв’язування подій DOM через events:

local.MyWidget = instance.Widget.extend({
    events: {
        "click .my_button": "button_clicked",
    },
    button_clicked: function() {
        ..
    }
});

events - це об’єкт (зв’язування) події з функцією або методом, який потрібно викликати, коли подія запускається:

  • ключ - це назва події, можливо, уточнене за допомогою селектора CSS, і в цьому випадку функція або метод буде запущено, лише якщо подія відбудеться у вибраному піделементі: click оброблятиме всі клацання у віджеті, але `` click .my_button оброблятиме лише клацання в елементах, які мають клас my_button

  • значення - це дія, яку потрібно виконати, коли спрацьовує подія

    Це може бути як функція:

    events: {
        'click': function (e) { /* code here */ }
    }
    

    або назва методу об’єкта (див. приклад вище).

    У будь-якому випадку this є екземпляром віджета, а обробнику надається єдиний параметр, об’єкт події jQuery для події.

Події та властивості віджетів

Події

Віджети забезпечують систему подій (окрему від системи подій DOM/jQuery, описаної вище): віджет може запускати події сам по собі, а інші віджети (або він сам) можуть прив’язувати себе та слухати ці події:

local.ConfirmWidget = instance.Widget.extend({
    events: {
        'click button.ok_button': function () {
            this.trigger('user_chose', true);
        },
        'click button.cancel_button': function () {
            this.trigger('user_chose', false);
        }
    },
    start: function() {
        this.$el.append("<div>Are you sure you want to perform this action?</div>" +
            "<button class='ok_button'>Ok</button>" +
            "<button class='cancel_button'>Cancel</button>");
    },
});

Цей віджет діє як фасад, перетворюючи введені користувачем дані (через події DOM) у документовану внутрішню подію, до якої можуть прив’язуватися батьківські віджети.

trigger() приймає назву події, яку потрібно активувати, як свій перший (обов’язковий) аргумент, будь-які подальші аргументи розглядаються як дані події та передаються безпосередньо слухачам.

Потім ми можемо налаштувати батьківську подію, створюючи екземпляр нашого загального віджета та прослуховуючи подію user_chose за допомогою on():

local.HomePage = instance.Widget.extend({
    start: function() {
        var widget = new local.ConfirmWidget(this);
        widget.on("user_chose", this, this.user_chose);
        widget.appendTo(this.$el);
    },
    user_chose: function(confirm) {
        if (confirm) {
            console.log("The user agreed to continue");
        } else {
            console.log("The user refused to continue");
        }
    },
});

on() прив’язує функцію, яку потрібно викликати, коли відбувається подія, визначена event_name. Аргумент func - це функція для виклику, а object - це об’єкт, з яким ця функція пов’язана, якщо це метод. Зв’язану функцію буде викликано з додатковими аргументами trigger(), якщо вони є. Приклад:

start: function() {
    var widget = ...
    widget.on("my_event", this, this.my_event_triggered);
    widget.trigger("my_event", 1, 2, 3);
},
my_event_triggered: function(a, b, c) {
    console.log(a, b, c);
    // will print "1 2 3"
}

Примітка

Ініціювання подій на іншому віджеті, як правило, погана ідея. Основним винятком із цього правила є odoo.web.bus, який існує спеціально для трансляцій, у яких може бути зацікавлений будь-який віджет у веб-додатку Odoo.

Властивості

Властивості дуже схожі на звичайні атрибути об’єктів, оскільки вони дозволяють зберігати дані в екземплярі віджета, однак вони мають додаткову функцію, яка полягає в тому, що вони запускають події, коли їх встановлено:

start: function() {
    this.widget = ...
    this.widget.on("change:name", this, this.name_changed);
    this.widget.set("name", "Nicolas");
},
name_changed: function() {
    console.log("The new value of the property 'name' is", this.widget.get("name"));
}
  • set() встановлює значення властивості та запускає change:propname (де propname - це назва властивості, передане як перший параметр у set()) і change

  • get() отримує значення властивості.

Вправа

Exercise

Властивості та події віджетів

Створіть віджет ColorInputWidget, який відображатиме 3 <input type="text">. Кожен із цих <input> призначений для введення шістнадцяткового числа від 00 до FF. Коли будь-який із цих <input> змінюється користувачем, віджет повинен запитати вміст трьох <input>, об’єднати їхні значення, щоб отримати повний колірний код CSS (тобто: #00FF00 ``) і помістити результат у властивість під назвою ``color. Зверніть увагу на подію jQuery change(), яку можна прив’язати до будь-якого елемента HTML <input>, і метод val(), який може запитувати поточне значення цього <input> може бути корисним для цієї вправи.

Потім змініть віджет HomePage, щоб створити примірник ColorInputWidget і відобразити його. Віджет HomePage також має відображати порожній прямокутник. Цей прямокутник повинен завжди, у будь-який момент, мати той самий колір фону, що й колір у властивості color екземпляра ColorInputWidget.

Використовуйте QWeb для створення всього HTML.

Змінити існуючі віджети та класи

Система класів веб-платформи Odoo дозволяє безпосередньо модифікувати існуючі класи за допомогою методу include():

var TestClass = instance.web.Class.extend({
    testMethod: function() {
        return "hello";
    },
});

TestClass.include({
    testMethod: function() {
        return this._super() + " world";
    },
});

console.log(new TestClass().testMethod());
// will print "hello world"

Ця система подібна до механізму успадкування, за винятком того, що вона змінює цільовий клас на місці замість створення нового класу.

У цьому випадку this._super() викличе оригінальну реалізацію методу, який замінюється/перевизначається. Якщо клас уже мав підкласи, усі виклики this._super() у підкласах викличуть нові реалізації, визначені у виклику include(). Це також спрацює, якщо деякі екземпляри класу (або будь-якого з його підкласів) були створені до виклику include().

Переклади

Процес перекладу тексту в коді Python і JavaScript дуже схожий. Ви могли помітити ці рядки на початку файлу petstore.js:

var _t = instance.web._t,
    _lt = instance.web._lt;

Ці рядки просто використовуються для імпорту функцій перекладу в поточний модуль JavaScript. Вони використовуються таким чином:

this.$el.text(_t("Hello user!"));

В Odoo файли перекладів автоматично генеруються шляхом сканування вихідного коду. Усі фрагменти коду, які викликають певну функцію, виявляються, і їх вміст додається до файлу перекладу, який потім надсилається перекладачам. У Python це функція _(). У JavaScript функція _t() (а також _lt()).

_t() поверне переклад, визначений для наданого тексту. Якщо для цього тексту не визначено переклад, він поверне оригінальний текст як є.

Примітка

Щоб додати значення, надані користувачем, у рядки, які можна перекладати, рекомендується використовувати _.str.sprintf з іменованими аргументами після переклад:

this.$el.text(_.str.sprintf(
    _t("Hello, %(user)s!"), {
    user: "Ed"
}));

Це робить рядки, які можна перекладати, більш зрозумілими для перекладачів і дає їм більше можливостей для зміни порядку або ігнорування параметрів.

_lt() («лінивий переклад») схожий, але дещо складніший: замість негайного перекладу свого параметра він повертає об’єкт, який після перетворення на рядок виконає переклад.

Він використовується для визначення термінів, які можна перекладати, до ініціалізації системи перекладів, наприклад, для атрибутів класу (оскільки модулі завантажуються до того, як буде налаштовано мову користувача та завантажено переклади).

Зв’язок із сервером Odoo

Зв’язок з моделями

Більшість операцій з Odoo передбачають спілкування з моделями, що реалізують бізнес-концерт, ці моделі потім (потенційно) взаємодіятимуть із деяким механізмом зберігання (зазвичай PostgreSQL).

Незважаючи на те, що jQuery надає функцію $.ajax для мережевих взаємодій, спілкування з Odoo вимагає додаткових метаданих, налаштування яких перед кожним викликом будуть багатослівними та схильними до помилок. Як наслідок, Odoo web надає комунікаційні примітиви вищого рівня.

Щоб продемонструвати це, файл petstore.py вже містить невелику модель із зразком методу:

class message_of_the_day(models.Model):
    _name = "oepetstore.message_of_the_day"

    @api.model
    def my_method(self):
        return {"hello": "world"}

    message = fields.Text(),
    color = fields.Char(size=20),

Це оголошує модель із двома полями та методом my_method(), який повертає літеральний словник.

Ось зразок віджета, який викликає my_method() і відображає результат:

local.HomePage = instance.Widget.extend({
    start: function() {
        var self = this;
        var model = new instance.web.Model("oepetstore.message_of_the_day");
        model.call("my_method", {context: new instance.web.CompoundContext()}).then(function(result) {
            self.$el.append("<div>Hello " + result["hello"] + "</div>");
            // will show "Hello world" to the user
        });
    },
});

Клас, який використовується для виклику моделей Odoo, це odoo.Model(). Його екземпляр створюється з назвою моделі Odoo як першим параметром (oepetstore.message_of_the_day тут).

call() можна використовувати для виклику будь-якого (публічного) методу моделі Odoo. Він приймає такі позиційні аргументи:

name

Назва методу для виклику, тут my_method

args

масив позиційних аргументів для надання методу. Оскільки в прикладі немає позиційного аргументу, параметр args не надається.

Ось інший приклад із позиційними аргументами:

@api.model
def my_method2(self, a, b, c): ...
model.call("my_method", [1, 2, 3], ...
// with this a=1, b=2 and c=3
kwargs

зіставлення аргументів ключового слова для передачі. У прикладі подано один іменований аргумент context.

@api.model
def my_method2(self, a, b, c): ...
model.call("my_method", [], {a: 1, b: 2, c: 3, ...
// with this a=1, b=2 and c=3

call() повертає відкладене вирішення зі значенням, яке повертає метод моделі як перший аргумент.

CompoundContext

У попередньому розділі використовувався аргумент context, який не було пояснено у виклику методу:

model.call("my_method", {context: new instance.web.CompoundContext()})

Контекст схожий на «magic» аргумент, який веб-клієнт завжди надаватиме серверу під час виклику методу. Контекст - це словник, що містить кілька ключів. Одним із найважливіших ключів є мова користувача, яка використовується сервером для перекладу всіх повідомлень програми. Ще один - це часовий пояс користувача, який використовується для правильного обчислення дат і часу, якщо Odoo використовують люди в різних країнах.

argument необхідний у всіх методах, інакше можуть статися погані речі (наприклад, додаток не буде правильно перекладений). Ось чому, коли ви викликаєте метод моделі, ви завжди повинні надавати цей аргумент. Рішення для досягнення цього полягає у використанні odoo.web.CompoundContext().

CompoundContext() - це клас, який використовується для передачі контексту користувача (з мовою, часовим поясом тощо) на сервер, а також для додавання нових ключів до контексту (методи деяких моделей використовують довільні ключі, додані до контексту). Він створюється шляхом надання його конструктору будь-якої кількості словників або інших екземплярів CompoundContext(). Він об’єднає всі ці контексти перед надсиланням їх на сервер.

model.call("my_method", {context: new instance.web.CompoundContext({'new_key': 'key_value'})})
@api.model
def my_method(self):
    print self.env.context
    // will print: {'lang': 'en_US', 'new_key': 'key_value', 'tz': 'Europe/Brussels', 'uid': 1}

Ви можете побачити, що словник в аргументі context містить деякі ключі, пов’язані з конфігурацією поточного користувача в Odoo, а також ключ new_key, який було додано під час створення екземпляра CompoundContext().

Запити

Хоча call() достатньо для будь-якої взаємодії з моделями Odoo, Odoo Web надає помічник для простішого та зрозумілішого запиту моделей (витягування записів на основі різних умов): query(), який діє як ярлик для загальної комбінації search() і :read(). Він забезпечує більш зрозумілий синтаксис для пошуку та читання моделей:

model.query(['name', 'login', 'user_email', 'signature'])
     .filter([['active', '=', true], ['company_id', '=', main_company]])
     .limit(15)
     .all().then(function (users) {
    // do work with users records
});

versus:

model.call('search', [['active', '=', true], ['company_id', '=', main_company]], {limit: 15})
    .then(function (ids) {
        return model.call('read', [ids, ['name', 'login', 'user_email', 'signature']]);
    })
    .then(function (users) {
        // do work with users records
    });
  • query() приймає необов’язковий список полів як параметр (якщо поля не надано, вибираються всі поля моделі). Він повертає odoo.web.Query(), який можна додатково налаштувати перед виконанням

  • Query() представляє створений запит. Він незмінний, методи налаштування запиту фактично повертають змінену копію, тому можна використовувати оригінальну та нову версії поруч. Перегляньте Query() його параметри налаштування.

Коли запит налаштовано як потрібно, просто викличте all(), щоб виконати його та повернути відкладений результат. Результат такий самий, як і read(), масив словників, де кожен словник є запитуваним записом, з кожним запитуваним полем ключем словника.

Вправи

Exercise

Повідомлення дня

Створіть віджет MessageOfTheDay, що відображає останній запис моделі oepetstore.message_of_the_day. Віджет має отримати свій запис, щойно він відобразиться.

Відобразити віджет на домашній сторінці Pet Store.

Exercise

Список іграшок для домашніх тварин

Створіть віджет PetToysList з відображенням 5 іграшок (використовуючи їх назви та зображення).

Іграшки для домашніх тварин не зберігаються в новій моделі, натомість вони зберігаються в product.product за допомогою спеціальної категорії Pet Toys. Ви можете переглянути попередньо створені іграшки та додати нові, перейшовши до Pet Store ‣ Pet Store ‣ Pet Toys. Можливо, вам знадобиться дослідити product.product, щоб створити правильний домен для вибору лише іграшок для домашніх тварин.

В Odoo зображення зазвичай зберігаються у звичайних полях, закодованих як base64, HTML підтримує відображення зображень безпосередньо з base64 за допомогою <img src="data:mime_type;base64,base64_image_data"/>

Віджет PetToysList має відображатися на головній сторінці праворуч від віджета MessageOfTheDay. Щоб досягти цього, вам потрібно буде створити макет за допомогою CSS.

Існуючі веб-компоненти

Менеджер дій

В Odoo багато операцій починаються з дія: відкриття пункту меню (у представленні), друк звіту, …

Дії - це фрагменти даних, які описують, як клієнт має відреагувати на активацію частини вмісту. Дії можна зберігати (і зчитувати через модель) або генерувати на льоту (локально для клієнта за допомогою коду JavaScript або віддалено методом моделі).

В Odoo Web компонентом, відповідальним за обробку цих дій і реагування на них, є Менеджер дій.

Використання Менеджера дій

Менеджер дій можна викликати явно з коду javascript, створивши словник, що описує дію правильного типу, і викликаючи з ним примірник менеджера дій.

do_action() - це ярлик Widget(), який шукає «поточний» менеджер дій і виконує дію:

instance.web.TestWidget = instance.Widget.extend({
    dispatch_to_new_action: function() {
        this.do_action({
            type: 'ir.actions.act_window',
            res_model: "product.product",
            res_id: 1,
            views: [[false, 'form']],
            target: 'current',
            context: {},
        });
    },
});

Найпоширенішим типом дії є ir.actions.act_window, який надає представлення моделі (відображає модель різними способами), його найпоширенішими атрибутами є:

res_model

Модель для відображення в представленнях

res_id (необов’язковий)

Для представлень форми попередньо вибраний запис у res_model

views

Перелічує представлення, доступні через дію. Список [view_id, view_type], view_id може бути або ідентифікатором бази даних представлення правильного типу, або false, щоб використовувати представлення за замовчуванням для вказаного типу. Типи представлення не можуть бути присутні кілька разів. Ця дія за замовчуванням відкриє перше представлення списку.

target

Або current (за замовчуванням), який замінює розділ «контент» веб-клієнта дією, або new, щоб відкрити дію в діалоговому вікні.

context

Додаткові контекстні дані для використання в дії.

Exercise

Перейти до продукту

Змініть компонент PetToysList, щоб клацання іграшки замінювало домашню сторінку представленням форми іграшки.

Дії клієнта

У цьому посібнику ми використовували простий віджет HomePage, який веб-клієнт автоматично запускає, коли ми вибираємо правильний пункт меню. Але як веб-сайт Odoo дізнався, що потрібно запустити цей віджет? Тому що віджет зареєстровано як дія клієнта.

Дія клієнта - це (як випливає з назви) тип дії, визначений майже повністю в клієнті, у JavaScript для Odoo web. Сервер просто надсилає тег дії (довільне ім’я) і за бажанням додає кілька параметрів, але крім цього все обробляється спеціальним кодом клієнта.

Наш віджет зареєстровано як обробник дії клієнта через це:

instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');

instance.web.client_actions - це Registry(), у якому менеджер дій шукає обробники дій клієнта, коли йому потрібно їх виконати. Перший параметр add() - це назва (тег) дії клієнта, а другий параметр - шлях до віджета з кореня веб-клієнта Odoo.

Коли дію клієнта потрібно виконати, менеджер дій шукає її тег у реєстрі, проходить вказаний шлях і відображає знайдений віджет у кінці.

Примітка

обробник дій клієнта також може бути звичайною функцією, у цьому випадку її буде викликано, а її результат (якщо такий є) буде інтерпретовано як наступну дію для виконання.

На стороні сервера ми просто визначили дію ir.actions.client:

<record id="action_home_page" model="ir.actions.client">
    <field name="tag">petstore.homepage</field>
</record>

і меню, що відкриває дію:

<menuitem id="home_page_petstore_menu" parent="petstore_menu"
          name="Home Page" action="action_home_page"/>

Архітектура Представлень

Велика частина корисності (і складності) веб-сайту Odoo полягає в представленнях. Кожен тип перегляду є способом відображення моделі в клієнті.

Менеджер представлення

Коли екземпляр ActionManager отримує дію типу ir.actions.act_window, він делегує синхронізацію та обробку самих представлень представлення менеджеру, який потім налаштує одне або кілька представлень залежно щодо вимог оригінальної дії:

../../_images/viewarchitecture.png

Представлення

Більшість представлень Odoo реалізовано через підклас odoo.web.View(), який надає трохи загальної базової структури для обробки подій і відображення інформації про модель.

представлення пошуку вважається типом представлення основною структурою Odoo, але обробляється окремо веб-клієнтом (оскільки це більш постійний елемент і може взаємодіяти з іншими представленнями, чого не роблять звичайні представлення).

Представлення відповідає за завантаження власного опису XML (за допомогою fields_view_get) і будь-якого іншого джерела даних, яке йому потрібно. З цією метою представленням надається необов’язковий ідентифікатор представлення, встановлений як атрибут view_id.

Представлення також забезпечені екземпляром DataSet(), який містить найбільш необхідну інформацію про модель (назва моделі та, можливо, різні ідентифікатори записів).

Представлення також можуть обробляти пошукові запити, перевизначаючи do_search() і оновлюючи свій DataSet() за потреби.

Поля представлення форми

Загальною потребою є розширення представлення веб-форми для додавання нових способів відображення полів.

Усі вбудовані поля мають реалізацію відображення за замовчуванням, новий віджет форми може знадобитися для правильної взаємодії з новим типом поля (наприклад, поле GIS) або для надання нових представлень і способів взаємодії з існуючими типами полів (наприклад, перевірте поля Char, які мають містити адреси ел. пошти та відображати їх як посилання на ел. листи).

Щоб явно вказати, який віджет форми слід використовувати для відображення поля, просто скористайтеся атрибутом widget в описі XML подання:

<field name="contact_mail" widget="email"/>

Примітка

  • той самий віджет використовується як у режимах «перегляду» (тільки для читання), так і в режимі «редагування» перегляду форми, неможливо використовувати віджет в одному та інший віджет в іншому

  • і задане поле (назва) не можна використовувати кілька разів в одній формі

  • віджет може ігнорувати поточний режим представлення форми та залишатися незмінним у режимах перегляду та редагування

Поля створюються у представленні форми після того, як воно прочитає свій XML-опис і побудує відповідний HTML, що представляє цей опис. Після цього представлення форми буде спілкуватися з об’єктами поля за допомогою деяких методів. Ці методи визначаються інтерфейсом FieldInterface. Майже всі поля успадковують абстрактний клас AbstractField. Цей клас визначає деякі механізми за замовчуванням, які мають бути реалізовані в більшості полів.

Ось деякі з обов’язків польового класу:

  • Клас поля повинен відображати та дозволяти користувачеві редагувати значення поля.

  • Він має правильно реалізувати 3 атрибути поля, доступні в усіх полях Odoo. Клас AbstractField вже реалізує алгоритм, який динамічно обчислює значення цих атрибутів (вони можуть змінитися в будь-який момент, оскільки їх значення змінюються відповідно до значення інших полів). Їх значення зберігаються у Властивості віджета (властивості віджета пояснювалися раніше в цьому посібнику). Кожен клас поля відповідає за перевірку цих властивостей віджетів і динамічну адаптацію залежно від їхніх значень. Ось опис кожного з цих атрибутів:

    • required: поле має мати значення перед збереженням. Якщо required має значення true, а поле не має значення, метод is_valid() поля має повернути false.

    • invisible: якщо це значення true, поле має бути невидимим. Клас AbstractField вже має базову реалізацію цієї поведінки, яка підходить для більшості полів.

    • readonly: якщо true, поле не має бути доступним для редагування користувачем. Більшість полів в Odoo мають зовсім іншу поведінку залежно від значення readonly. Наприклад, FieldChar відображає HTML <input>, коли його можна редагувати, і просто відображає текст, коли він доступний лише для читання. Це також означає, що він має набагато більше коду, який знадобиться для реалізації лише однієї поведінки, але це необхідно для забезпечення хорошої взаємодії з користувачем.

  • Поля мають два методи, set_value() і get_value(), які викликаються представленням форми, щоб надати йому значення для відображення та повернути нове значення, введене користувачем. Ці методи повинні мати можливість обробляти значення, надане сервером Odoo, коли read() виконується на моделі, і повертати дійсне значення для write(). Пам’ятайте, що типи даних JavaScript/Python, які використовуються для представлення значень, наданих read() і write(), не обов’язково є однаковими в Odoo. Наприклад, коли ви читаєте many2one, це завжди кортеж, першим значенням якого є ідентифікатор зазначеного запису, а другим є назвою get (тобто: (15, "Agrolait")). Але коли ви пишете many2one, це має бути одне ціле число, а не кортеж. AbstractField має реалізацію цих методів за замовчуванням, яка добре працює для простих типів даних і встановлює властивість віджета під назвою value.

Зауважте, що для кращого розуміння того, як реалізувати поля, вам наполегливо рекомендується переглянути визначення інтерфейсу FieldInterface і класу AbstractField безпосередньо в коді веб-клієнта Odoo.

Створення нового типу поля

У цій частині ми пояснимо, як створити новий тип поля. Прикладом тут буде повторна реалізація класу FieldChar і поступове пояснення кожної частини.

Просте поле лише для читання

Ось перша реалізація, яка відображатиме лише текст. Користувач не зможе змінити вміст поля.

local.FieldChar2 = instance.web.form.AbstractField.extend({
    init: function() {
        this._super.apply(this, arguments);
        this.set("value", "");
    },
    render_value: function() {
        this.$el.text(this.get("value"));
    },
});

instance.web.form.widgets.add('char2', 'instance.oepetstore.FieldChar2');

У цьому прикладі ми оголошуємо клас під назвою FieldChar2, який успадковує AbstractField. Ми також реєструємо цей клас у реєстрі instance.web.form.widgets під ключем char2. Це дозволить нам використовувати це нове поле в будь-якому представленні форми, вказавши widget="char2" у тегу <field/> в XML-декларації подання.

У цьому прикладі ми визначаємо один метод: render_value(). Все, що він робить, це відображає властивість віджета value. Це два інструменти, визначені класом AbstractField. Як пояснювалося раніше, представлення форми викличе метод set_value() поля, щоб встановити значення для відображення. Цей метод уже має реалізацію за замовчуванням у AbstractField, яка просто встановлює властивість віджета value. AbstractField також стежить за подією change:value на собі та викликає render_value(), коли це відбувається. Таким чином, render_value() є зручним методом для реалізації в дочірніх класах для виконання певної операції кожного разу, коли значення поля змінюється.

У методі init() ми також визначаємо значення поля за замовчуванням, якщо жодне не вказано у представленні форми (тут ми припускаємо, що значенням за замовчуванням поля char має бути порожній рядок).

Read-Write Field

Поля лише для читання, які лише відображають вміст і не дозволяють користувачеві змінювати його, можуть бути корисними, але більшість полів в Odoo також дозволяють редагувати. Це ускладнює класи полів, здебільшого через те, що поля повинні обробляти як редагований, так і нередагований режими, ці режими часто абсолютно різні (з метою дизайну та зручності використання), і поля повинні мати можливість перемикатися між режимами в будь-який момент.

Щоб знати, у якому режимі має бути поточне поле, клас AbstractField встановлює властивість віджета з назвою effective_readonly. Поле має стежити за змінами у цій властивості віджета та відповідно відображати правильний режим. Приклад:

local.FieldChar2 = instance.web.form.AbstractField.extend({
    init: function() {
        this._super.apply(this, arguments);
        this.set("value", "");
    },
    start: function() {
        this.on("change:effective_readonly", this, function() {
            this.display_field();
            this.render_value();
        });
        this.display_field();
        return this._super();
    },
    display_field: function() {
        var self = this;
        this.$el.html(QWeb.render("FieldChar2", {widget: this}));
        if (! this.get("effective_readonly")) {
            this.$("input").change(function() {
                self.internal_set_value(self.$("input").val());
            });
        }
    },
    render_value: function() {
        if (this.get("effective_readonly")) {
            this.$el.text(this.get("value"));
        } else {
            this.$("input").val(this.get("value"));
        }
    },
});

instance.web.form.widgets.add('char2', 'instance.oepetstore.FieldChar2');
<t t-name="FieldChar2">
    <div class="oe_field_char2">
        <t t-if="! widget.get('effective_readonly')">
            <input type="text"></input>
        </t>
    </div>
</t>

У методі start() (який викликається одразу після додавання віджета до DOM) ми прив’язуємось до події change:effective_readonly. Це дозволяє нам повторно відображати поле кожного разу, коли змінюється властивість віджета effective_readonly. Цей обробник подій викличе display_field(), який також викликається безпосередньо в start(). Це display_field() було створено спеціально для цього поля, це не метод, визначений у AbstractField чи будь-якому іншому класі. Ми можемо використовувати цей метод для відображення вмісту поля в залежності від поточного режиму.

Відтепер концепція цього поля типова, за винятком того, що існує багато перевірок, щоб дізнатися стан властивості effective_readonly:

  • У шаблоні QWeb, який використовується для відображення вмісту віджета, він відображає <input type="text" />, якщо ми перебуваємо в режимі читання-запису, і нічого особливого в режимі лише читання.

  • У методі display_field() ми повинні прив’язати подію change <input type="text" />, щоб знати, коли користувач змінив значення. Коли це відбувається, ми викликаємо метод internal_set_value() з новим значенням поля. Це зручний метод, наданий класом AbstractField. Цей метод встановить нове значення у властивості value, але не ініціюватиме виклик render_value() (це необов’язково, оскільки <input type="text" /> вже містить правильне значення).

  • У render_value() ми використовуємо зовсім інший код для відображення значення поля залежно від того, чи ми перебуваємо в режимі лише для читання чи в режимі читання-запису.

Exercise

Створіть кольорове поле

Створіть клас FieldColor. Значення цього поля має бути рядком, що містить код кольору, як той, що використовується в CSS (приклад: #FF0000 для червоного). У режимі лише для читання це колірне поле має відображати маленький блок, колір якого відповідає значенню поля. У режимі читання-запису ви маєте відобразити <input type="color" />. Цей тип <input /> є компонентом HTML5, який працює не в усіх браузерах, але добре працює в Google Chrome. Тому можна використовувати як вправу.

Ви можете використовувати цей віджет у вигляді форми моделі message_of_the_day для його поля під назвою color. Як бонус, ви можете змінити віджет MessageOfTheDay, створений у попередній частині цього посібника, щоб відображати повідомлення дня з кольором тла, вказаним у полі color.

Спеціальні віджети представлення форми

Поля форми використовуються для редагування одного поля та внутрішньо пов’язані з полем. Оскільки це може бути обмеженням, також можна створювати віджети форм, які не мають таких обмежень і мають менший зв’язок із певним життєвим циклом.

Настроювані віджети форми можна додати до представлення форми за допомогою тегу widget:

<widget type="xxx" />

Цей тип віджетів буде просто створено у представленні форми під час створення HTML відповідно до визначення XML. Вони мають властивості, спільні з полями (наприклад, властивість effective_readonly), але їм не призначено точне поле. І тому у них немає таких методів, як get_value() і set_value(). Вони повинні успадкувати від абстрактного класу FormWidget.

Віджети форми можуть взаємодіяти з полями форми, прослуховуючи їх зміни та вибираючи або змінюючи їхні значення. Вони можуть отримати доступ до полів форми через атрибут field_manager:

local.WidgetMultiplication = instance.web.form.FormWidget.extend({
    start: function() {
        this._super();
        this.field_manager.on("field_changed:integer_a", this, this.display_result);
        this.field_manager.on("field_changed:integer_b", this, this.display_result);
        this.display_result();
    },
    display_result: function() {
        var result = this.field_manager.get_field_value("integer_a") *
                     this.field_manager.get_field_value("integer_b");
        this.$el.text("a*b = " + result);
    }
});

instance.web.form.custom_widgets.add('multiplication', 'instance.oepetstore.WidgetMultiplication');

FormWidget, як правило, є самим FormView(), але функції, які використовуються з нього, мають бути обмежені тими, що визначені FieldManagerMixin(), найбільш корисними з яких є:

  • get_field_value(field_name)(), який повертає значення поля.

  • set_values(values)() встановлює кілька значень полів, приймає відображення {field_name: value_to_set}

  • Подія field_changed:field_name запускається щоразу, коли змінюється значення поля під назвою field_name

Exercise

Показати координати на карті Google

Додайте два поля до product.product, що зберігають широту та довготу, а потім створіть новий віджет форми для відображення широти та довготи місця походження продукту на карті

Щоб відобразити карту, скористайтеся вставкою Google Map:

<iframe width="400" height="300" src="https://maps.google.com/?ie=UTF8&amp;ll=XXX,YYY&amp;output=embed">
</iframe>

де XXX слід замінити на широту, а YYY на довготу.

Відобразіть два поля позиції та віджет карти, використовуючи їх, на новій сторінці блокнота в поданні форми продукту.

Exercise

Отримати поточні координати

Додайте кнопку скидання координат продукту до місця розташування користувача, ви можете отримати ці координати за допомогою javascript geolocation API.

Тепер ми хотіли б відобразити додаткову кнопку для автоматичного встановлення координат розташування поточного користувача.

Щоб отримати координати користувача, простим способом є використання API геолокації JavaScript. Дивіться онлайн-документацію, щоб дізнатися, як ним користуватися.

Будь ласка, також зауважте, що користувач не повинен мати можливість натиснути цю кнопку, коли вікно форми перебуває в режимі лише для читання. Отже, цей спеціальний віджет має правильно обробляти властивість effective_readonly, як і будь-яке інше поле. Одним із способів зробити це було б змусити кнопку зникнути, коли effective_readonly має значення true.

1

як окреме поняття від екземплярів. У багатьох мовах класи є повноцінними об’єктами, а самі екземпляри (метакласів), але між класами та екземплярами залишається дві досить окремі ієрархії

2

а також приховування відмінностей між браузерами, хоча з часом це стало менш потрібним