Это руководство посвящено созданию модулей для веб-клиента Odoo.
Чтобы создать веб-сайты с помощью Odoo, см. Создание веб-сайта; того чтобы добавить новые бизнес возможности или расширить существующие в Odoo, см. Создание модуля.
Предупреждение
Данное руководство предполагает следующие знание:
Основы и рекомендации по использованию Javascript
jQuery
- Underscore.js
Так же требуетсяIt also requires установленная Odoo и Git.
Простой модуль
Начнем с простого модуля Odoo, содержащего базовую конфигурацию веб-компонентов и позволяющую протестировать веб-фреймворк.
Пример модуля доступен онлайн и может быть загружен с помощью следующей команды:
$ git clone http://github.com/odoo/petstore
This will create a petstore
folder wherever you executed the command.
You then need to add that folder to Odoo's
addons path
, create a new database and
install the oepetstore
module.
Если вы просмотрите папку petstore
, увидите следующие содержимое:
oepetstore
|-- images
| |-- alligator.jpg
| |-- ball.jpg
| |-- crazy_circle.jpg
| |-- fish.jpg
| `-- mice.jpg
|-- __init__.py
|-- oepetstore.message_of_the_day.csv
|-- __openerp__.py
|-- petstore_data.xml
|-- petstore.py
|-- petstore.xml
`-- static
`-- src
|-- css
| `-- petstore.css
|-- js
| `-- petstore.js
`-- xml
`-- petstore.xml
Модуль уже содержит некоторые изменения серверной части. Мы вернемся к ним позже, а пока давайте сосредоточимся на веб-содержимом в папке static
.
Файлы используемые в "web" части Odoo модуля должны быть помещены в каталог static
, для того, чтобы они стали доступны из веб-браузереа файлы, находящиеся вне этого каталога, не могут быть получены браузером напрямую. Подкаталоги src/css
, src/js
и src/xml
не обязаны иметь строго аналогичные названия и имеют такие имена исключительно для удобства разработки.
oepetstore/static/css/petstore.css
- currently empty, will hold the CSS for pet store content
oepetstore/static/xml/petstore.xml
Так же почти пуст, будет содержать QWeb шаблон
oepetstore/static/js/petstore.js
Наиболее важная (и интересная) часть содержит логику приложения (или, по крайней мере, его веб-браузерную часть) в виде javascript. Должна выглядеть следующим образом:
openerp.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'); }
Этот код выводит небольшое сообщение в консоль веб-браузера.
Предупреждение
Все JavaScript файлы объеденены и minified для снижения времени загрузки приложения.
Одним из недостатков такого подхода является то, что отладка становится более сложной отдельные файлы исчезают и код становится практически не читаемым. Есть возможность отключить данных процесс путем включения "режима разработчика": авторизируйтесь в вашей Odoo системе (пользователь admin пароль admin по умолчанию), откройте меню пользователя (в верхнем правом углу экрана Odoo) И выберите About Odoo, а затем Activate the developer mode:
Данный механизм перезагрузит клиент с отключенными механизмами оптимизации, что позволит вести разработку и отладку более комфортно.
Odoo JavaScript модуль
Чистый Javascript сам по себе не имеет встроенных модулей. В результате чего переменные определенные в разных файлах могут смешиваться и конфликтовать. Такая ситуация привела к тому, что стали появляться различные шаблоны модулей, использование которых позволяет создавать чистые пространства имен с минимальными рисками конфликтов.
Odoo использует такого рода шаблоны для определения модулей внутри своих веб расширений, для упорядочивания как пространства имен так и правильного порядка загрузки.
oepetstore/static/js/petstore.js
содержит объявление модуля:
openerp.oepetstore = function(instance, local) {
local.xxx = ...;
}
In Odoo web, modules are declared as functions set on the global openerp
variable. The function's name must be the same as the addon (in this case
oepetstore
) so the framework can find it, and automatically initialize it.
Когда веб-клиент загружает ваш модуль, он вызывает корневую функцию и предоставляет ей два параметра:
Первый параметр является текущим экземпляром веб-клиента Odoo, он предоставляет доступ к различным возможностям, определенным Odoo (переводы, сетевые службы), а также объекты, определенные ядром или другими модулями.
Вторым параметром является ваше собственное локальное пространство имен, автоматически созданное веб-клиентом. Объекты и переменные, которые должны быть доступны извне вашего модуля (либо из-за того что Odoo веб клиенту необходимо вызывать их, либо другие хотят кастомизировать их) должны быть установлены внутри этого пространства имен.
Классы
В отличие от модулей и вопреки большинству объектно-ориентированных языков, javascript не строит в классах [#classes] _ хотя он предоставляет примерно эквивалентные (более низкоуровневые и более многословные) механизмы.
Для простоты и удобства разработчиков веб часть Odoo предоставляет систему классов основанную на Simple JavaScript Inheritance.
New classes are defined by calling the extend()
method of openerp.web.Class()
:
var MyClass = instance.web.Class.extend({
say_hello: function() {
console.log("hello");
},
});
The extend()
method takes a dictionary describing
the new class's content (methods and static attributes). In this case, it will
only have a say_hello
method which takes no parameters.
Классы создаются с помощью оператора 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
It is also possible to create subclasses from existing (used-defined) classes
by calling extend()
on the parent class, as is done
to subclass 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
) ссылку на его значение следует сохранить, к нему нельзя получить доступ через this
:
// 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);
}
Основы работы с Widget
Web-клиент Odoo связывает jQuery для легкой манипуляции с DOM. Это полезно и обеспечивает лучший API, чем стандартный W3C DOM [#dombugs]_, которого не достаточно для структурирования комплексных приложений, что приводит к сложностям в обслуживании.
Much like object-oriented desktop UI toolkits (e.g. Qt, Cocoa or GTK),
Odoo Web makes specific components responsible for sections of a page. In
Odoo web, the base for such components is the Widget()
class, a component specialized in handling a page section and displaying
information for the user.
Ваш первый Widget
В начальном демонстрационном модуле уже есть базовый виджет:
local.HomePage = instance.Widget.extend({
start: function() {
console.log("pet store home page loaded");
},
});
It extends Widget()
and overrides the standard method
start()
, which — much like the previous MyClass
— does little for now.
Эта строка в конце файла:
instance.web.client_actions.add(
'petstore.homepage', 'instance.oepetstore.HomePage');
Регистрирует наш основной виджет как действие клиента. Действия клиента будут объяснены позже, на данный момент это именно то, что позволяет нашему виджету вызываться и отображаться, когда мы выбираем меню
.Предупреждение
Т.к. виджет виджет будет вызываться вне нашего модуля, веб клиенту необходимо его полное имя, а не локальная его версия.
Отображение контента
Виджеты имеют ряд методов и функций, но основы просты:
настройка виджета
формат данных виджета
отображение виджета
The HomePage
widget already has a start()
method. That method is part of the normal widget lifecycle and automatically
called once the widget is inserted in the page. We can use it to display some
content.
All widgets have a $el
which represents the
section of page they're in charge of (as a jQuery object). Widget content
should be inserted there. By default, $el
is an
empty <div>
element.
Элемент <div> `` обычно невидим для пользователя, если внутри нет контента (или нет определенных стилей, придающих ему размер), поэтому при запуске ``HomePage
пока на странице ничего не отобразиться.
Давайте добавим содержимое в корневой элемент виджета, используя jQuery:
local.HomePage = instance.Widget.extend({
start: function() {
this.$el.append("<div>Hello dear Odoo user!</div>");
},
});
Это сообщение появится, когда вы откроете
Примечание
Для обновления кода 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>");
},
});
We can now add our GreetingsWidget
to the HomePage
by using the
GreetingsWidget
's appendTo()
method:
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
- Finally it tells
GreetingsWidget
where to insert itself, delegating part of its$el
to theGreetingsWidget
.
When the appendTo()
method is called, it asks the
widget to insert itself at the specified position and to display its content.
The start()
method will be called during the call
to appendTo()
.
To see what happens under the displayed interface, we will use the browser's
DOM Explorer. But first let's alter our widgets slightly so we can more easily
find where they are, by adding a class to their root elements
:
local.HomePage = instance.Widget.extend({
className: 'oe_petstore_homepage',
...
});
local.GreetingsWidget = instance.Widget.extend({
className: 'oe_petstore_greetings',
...
});
Если вы можете найти соответствующий раздел DOM (щелкните правой кнопкой мыши на странице, затем Inspect Element), он должен выглядеть так:
<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>
Which clearly shows the two <div>
elements automatically created by
Widget()
, because we added some classes on them.
Мы также можем увидеть два прикрепленных к сообщению div'а, которые мы добавили сами.
Наконец, обратите внимание, что элемент <div class="oe_petstore_greetings">
, который представляет экземпляр GreetingsWidget
находящийся внутри <div class="oe_petstore_homepage">
предоставленного экземпляра HomePage
.
Родительские и дочерние объекты Widget
В предыдущей части мы создали экземпляр виджета, используя этот синтаксис:
new local.GreetingsWidget(this);
Первый аргумент - this`, который в этом случае является экземпляром ``HomePage
. Это говорит виджету созданному другим виджетом, что он является его родителем.
Как мы видели, виджеты, как правило, вставляются в DOM другим виджетами, которые будут являться корневым элементом вставляемого виджета. Это означает, что большинство виджетов являются «частью» другого виджета и существуют от его имени. Мы вызываем контейнер parent, а содержащий его виджет child.
Из-за многочисленных технических и концептуальных причин виджету необходимо знать, кто его родитель и кто его дети.
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 }, });
When overriding the init()
method of a widget it is
of the utmost importance to pass the parent to the this._super()
call,
otherwise the relation will not be set up correctly:
local.GreetingsWidget = instance.Widget.extend({
init: function(parent, name) {
this._super(parent);
this.name = name;
},
});
Наконец, если виджет не имеет родительского объекта (т.к. это корневой виджет приложения), в качестве родителя может быть указан null
:
new local.GreetingsWidget(null);
Уничтожение виджетов
If you can display content to your users, you should also be able to erase
it. This is done via the destroy()
method:
greeting.destroy();
When a widget is destroyed it will first call
destroy()
on all its children. Then it erases itself
from the DOM. If you have set up permanent structures in
init()
or start()
which
must be explicitly cleaned up (because the garbage collector will not handle
them), you can override destroy()
.
Опасно
when overriding destroy()
, _super()
must always be called otherwise the widget and its children are not
correctly cleaned up leaving possible memory leaks and "phantom events",
even if no error is displayed
Шаблонизатор QWeb
В предыдущем разделе мы добавляли контент к нашим виджетам, непосредственно манипулируя (и добавляя) ими в DOM:
this.$el.append("<div>Hello dear Odoo user!</div>");
Это позволяет генерировать и отображать любой тип контента, но становится громоздким при генерации значительных объемов DOM (много повторяющихся действий, проблемы с цитированием, ...)
Как и во многих других средах, решение Odoo заключается в использовании template engine. Шаблонизатор Odoo называется QWeb.
QWeb - это язык шаблонов на основе XML, подобный Genshi, Thymeleaf or Facelets. Он имеет следующие характеристики:
Он полностью написан на JavaScript и рендерится в браузере
Каждый файл шаблона (файлы XML) может содержать несколько шаблонов
- It has special support in Odoo Web's
Widget()
, though it can be used outside of Odoo's web client (and it's possible to useWidget()
without relying on QWeb)
Примечание
Обоснованием использования QWeb вместо существующих шаблонизаторов на javascript является расширяемость собственных (а так же созданных сторонними разработчиками) шаблонов представлений.
Большинство шаблонизаторов на javascript основаны на HTML, что исключает упрощенную структурную расширяемость, тогда как XML-шаблонный механизм шаблонов может быть изменен, например, с помощью XPath или CSS и DSL-преобразования дерева (или даже просто XSLT). Гибкость и расширяемость является основной характеристикой Odoo. При применении шаблонизатора Qweb такая гибкость и расширяемость не утрачивается..
Использование 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()
ищет определенный шаблон преобразовывает в строку и возвращает результат.
However, because Widget()
has special integration for QWeb
the template can be set directly on the widget via its
template
attribute:
local.HomePage = instance.Widget.extend({
template: "HomePageTemplate",
start: function() {
...
},
});
Хотя результат выглядит похожим, между этими подходами есть два различия:
- with the second version, the template is rendered right before
start()
is called в первом варианте содержимое шаблона добавляется к корневому элементу виджета, тогда как во втором варианте корневой элемент шаблона сразу устанавливается, как корневой элемент виджета. Вот почему вспомогательный виджет "greetings" sub-widget также получает красный фон
Предупреждение
templates should have a single non-t
root element, especially if
they're set as a widget's template
. If there are
multiple "root elements", results are undefined (usually only the first
root element will be used and the others will be ignored)
Работа с Context в 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>
When using Widget()
's integration it is not possible to
provide additional data to the template. The template will be given a single
widget
context variable, referencing the widget being rendered right
before start()
is called (the widget's state will
essentially be that set up by 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
не должен использоваться * для любых данных, которые могут содержать небезопасный пользовательский контент, поскольку это приводит к уязвимостям cross-site scripting
Условия
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 предоставляет две связанные директивы для определения атрибутов HTML: 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 в Widget
Создайте виджет, конструктор которого имеет два параметра помимо parent
: product_names
и color
.
product_names
должен быть массивом строк, каждая из которых - название продуктаcolor
строка содержащая цвет в CSS цветовом формате (например:#000000
для черного)
Виджет должен отображать названия продуктов друг под другом, каждый в отдельном поле с цветом фона и значением color
и границы. Вы должны использовать QWeb для визуализации HTML. Все что касается CSS должно находится в oepetstore/static/src/css/petstore.css
.
Используйте виджет в HomePage
с полдюжиной продуктов.
Помощники для Widget
Widget
's jQuery Selector
Выбор элементов DOM внутри виджетов можно выполнить, вызвав метод find()
в корне DOM виджета:
this.$el.find("input.my_input")...
But because it's a common operation, Widget()
provides an
equivalent shortcut through the $()
method:
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() {
..
},
});
Хотя это метод и работает, у него есть несколько проблем:
он довольно многословный
он не поддерживает замену корневого элемента виджета во время выполнения, поскольку привязка выполняется только при запуске
start()
(во время инициализации виджета)он требует решения
this
-связанных проблем
Widgets thus provide a shortcut to DOM event binding via
events
:
local.MyWidget = instance.Widget.extend({
events: {
"click .my_button": "button_clicked",
},
button_clicked: function() {
..
}
});
events
is an object (mapping) of an event to the
function or method to call when the event is triggered:
ключ - это имя события, которое может быть уточнено с помощью селектора CSS, и в этом случае, только если событие произойдет в выбранном подэлементе, будет выполняться функция или метод:
click
будет обрабатывать все клики внутри виджета, аclick .my_button
будет обрабатывать только клики в элементах с классомmy_button
Значение - это действие, которое должно выполняться при срабатывании события
Это может быть либо функция:
events: { 'click': function (e) { /* code here */ } }
Или имя метода объекта (см. Пример выше).
В любом случае
this
является экземпляром виджета, а обработчику задан единственный параметр - jQuery event object для события.
События и свойства Widget
События
Виджеты предоставляют систему событий (свою, отдельно от описанной выше систем событий 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()
takes the name of the event to trigger as
its first (mandatory) argument, any further arguments are treated as event
data and passed directly to listeners.
We can then set up a parent event instantiating our generic widget and
listening to the user_chose
event using 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()
binds a function to be called when the
event identified by event_name
is. The func
argument is the
function to call and object
is the object to which that function is
related if it is a method. The bound function will be called with the
additional arguments of trigger()
if it has
any. Example:
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"
}
Примечание
Triggering events on an other widget is generally a bad idea. The main
exception to that rule is openerp.web.bus
which exists specifically
to broadcasts evens in which any widget could be interested throughout
the Odoo web application.
Свойства
Свойства очень похожи на обычные атрибуты объектов, поскольку они позволяют хранить данные в экземпляре виджетов, однако у них есть дополнительная функция, при которой они запускают события при установке:
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()
sets the value of a property and triggerschange:propname
(where propname is the property name passed as first parameter toset()
) andchange
get()
retrieves the value of a property.
Упражнение
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.
Изменение существующих виджетов и классов
The class system of the Odoo web framework allows direct modification of
existing classes using the include()
method:
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"
Эта система похожа на механизм наследования, за исключением того, что она изменит целевой класс на месте вместо создания нового класса.
In that case, this._super()
will call the original implementation of a
method being replaced/redefined. If the class already had sub-classes, all
calls to this._super()
in sub-classes will call the new implementations
defined in the call to include()
. This will also work
if some instances of the class (or of any of its sub-classes) were created
prior to the call to include()
.
Система переводов на другие языки
Процесс преобразования текста в код Python и JavaScript очень похож. Вы могли заметить эти строки в начале файла petstore.js
:
var _t = instance.web._t,
_lt = instance.web._lt;
Эти строки просто используются для импорта функций перевода в текущий модуль JavaScript. Они используются следующим образом:
this.$el.text(_t("Hello user!"));
In Odoo, translations files are automatically generated by scanning the source
code. All piece of code that calls a certain function are detected and their
content is added to a translation file that will then be sent to the
translators. In Python, the function is _()
. In JavaScript the function is
_t()
(and also _lt()
).
Функция _t()
возвращает перевод, определенный для текста, который задан. Если для этого текста не определен перевод, то она вернет исходный текст как есть.
Примечание
Чтобы вводить пользовательские значения в переносимые строки, рекомендуется использовать _.str.sprintf с именованными аргументами после перевода:
this.$el.text(_.str.sprintf(
_t("Hello, %(user)s!"), {
user: "Ed"
}));
Это делает переводы более удобочитаемыми для переводчиков и дает им большую гибкость для изменения порядка или игнорирования параметров.
_lt()
("lazy translate") is similar but somewhat more
complex: instead of translating its parameter immediately, it returns
an object which, when converted to a string, will perform the translation.
Данный механизм используется для определения переводимых терминов до инициализации системы переводов, например, для атрибутов класса (поскольку модули загружаются до того, как язык пользователя настроен и загружены переводы).
Связь с сервером Odoo
Взаимодействие с моделями данных
Большинство операций с Odoo связаны со взаимодействием с моделями данных, реализующими бизнес-логику, и эти модели данных будут (потенциально) взаимодействовать с какой либо системой хранения (обычно PostgreSQL).
Хотя jQuery предоставляет функцию $.ajax для сетевых взаимодействий, для связи с Odoo требуются дополнительные метаданные, настройка которых перед каждым вызовом будет многословной и подверженной ошибкам. В результате, веб клиент Odoo предоставляет примитивы связей более высокого уровня.
Чтобы продемонстрировать это, файл 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
});
},
});
The class used to call Odoo models is openerp.Model()
. It is
instantiated with the Odoo model's name as first parameter
(oepetstore.message_of_the_day
here).
call()
can be used to call any (public) method of an
Odoo model. It takes the following positional arguments:
name
Имя вызываемого метода (в нашем случае
my_method
)args
массив positional arguments для предоставления метода. Поскольку пример не имеет позиционного аргумента, параметр
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
Отображение keyword arguments для передачи. В примере представлен одиночный аргумент
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()
returns a deferred resolved with the value
returned by the model's method as first argument.
CompoundContext - составной контекст
В предыдущем разделе использовался аргумент context
, который не был объяснен в вызове метода:
model.call("my_method", {context: new instance.web.CompoundContext()})
Контекст подобен "магическому" аргументу, который веб-клиент всегда будет выдавать серверу при вызове метода. Контекст - это словарь, содержащий несколько ключей. Одним из наиболее важных ключей является язык пользователя, используемый сервером для перевода всех сообщений приложения. Другой - часовой пояс пользователя, используемый для правильного вычисления дат и времени, для одновременного использования Odoo людьми в находящихся в разных странах.
The argument
is necessary in all methods, otherwise bad things could
happen (such as the application not being translated correctly). That's why,
when you call a model's method, you should always provide that argument. The
solution to achieve that is to use openerp.web.CompoundContext()
.
CompoundContext()
is a class used to pass the user's
context (with language, time zone, etc...) to the server as well as adding new
keys to the context (some models' methods use arbitrary keys added to the
context). It is created by giving to its constructor any number of
dictionaries or other CompoundContext()
instances. It will
merge all those contexts before sending them to the server.
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}
You can see the dictionary in the argument context
contains some keys that
are related to the configuration of the current user in Odoo plus the
new_key
key that was added when instantiating
CompoundContext()
.
Запросы
While call()
is sufficient for any interaction with Odoo
models, Odoo Web provides a helper for simpler and clearer querying of models
(fetching of records based on various conditions):
query()
which acts as a shortcut for the common
combination of search()
and
:read()
. It provides a clearer syntax to search
and read models:
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
});
против:
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()
takes an optional list of fields as parameter (if no field is provided, all fields of the model are fetched). It returns aopenerp.web.Query()
which can be further customized before being executedQuery()
represents the query being built. It is immutable, methods to customize the query actually return a modified copy, so it's possible to use the original and the new version side-by-side. SeeQuery()
for its customization options.
When the query is set up as desired, simply call
all()
to execute it and return a
deferred to its result. The result is the same as
read()
's, an array of dictionaries where each
dictionary is a requested record, with each requested field a dictionary key.
Упражнения
Exercise
Послание дня
Создайте виджет MessageOfTheDay
отображающий последнюю запись модели данных oepetstore.message_of_the_day
. Виджет должен получать свою запись сразу после ее отображения.
Отобразите виджет на домашней странице Pet Store.
Exercise
Список игрушек для домашних животных
Создайте виджет PetToysList
отображающий 5 игрушек (используя их имя и их изображения).
Игрушки для животных не хранятся в новой модели, вместо этого они хранятся в product.product
с использованием специальной категории Pet Toys. Вы можете увидеть заранее сгенерированные игрушки и добавить новые, зайдя в . Вам, вероятно, понадобится изучить product.product
чтобы создать правильный домен, чтобы выбрать только игрушки для домашних животных.
В Odoo, изображения обычно хранятся в обычных полях, закодированных как base64, HTML поддерживает вывод изображений прямо из base64 с помощью тега <img src="data:mime_type;base64,base64_image_data"/>
Виджет PetToysList
должен отображаться на домашней странице справа от виджета MessageOfTheDay
. Для этого вам понадобится сделать макет с CSS.
Существующие веб-компоненты
Менеджер действий
В Odoo многие операции начинаются с action: открытие элемента меню (для вида), печать отчета, ...
Действия - это фрагменты данных, описывающие реакцию клиента на активацию части содержимого. Действия могут быть сохранены (и считаны с помощью модели данных), или они могут быть сгенерированы «на лету» (локально для клиента кодом javascript или дистанционно с помощью метода модели данных).
В Odoo Web, компонентом, ответственным за обработку и реакцию на эти действия, является Action Manager.
Использование менеджера действий
Администратор действий может быть явно вызван из javascript-кода, создав словарь, описывающий действие нужного типа, и вызов экземпляра менеджера действий вместе с ним.
do_action()
is a shortcut of Widget()
looking up the "current" action manager and executing the action:
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: {},
});
},
});
Наиболее распространенный type
действия - ir.actions.act_window
, который предоставляет представления к модели (отображает модель различными способами), ее наиболее распространенными атрибутами являются:
res_model
Модель для отображения в представлениях
res_id
(необязательный параметр)Для представлений вида Form предварительно выбранная запись
res_model
views
Для представлений вида Form предварительно выбранная запись
[view_id, view_type]
,view_id
может быть либо идентификатором представления правильного типа в базе данных , либоfalse
чтобы использовать представление по умолчанию для указанного типа. Типы представлений не могут присутствовать несколько раз. Действие по умолчанию откроет первое представление из списка.target
Либо
current
(по умолчанию), который заменяет «содержимое» раздела веб-клиента действием, либоnew
, чтобы открыть действие в диалоговом окне.context
Дополнительные данные контекста для использования в действии.
Exercise
Перейти к продукту
Измените компонент PetToysList
чтобы щелкнуть игрушку, заменит домашнюю страницу представлением вида Form игрушки.
Действия клиента
В этом руководстве мы использовали простой виджет HomePage
, который автоматически запускается веб-клиентом, когда мы выбираем нужный пункт меню. Но как веб-клиент Odoo узнал о том, что нужно запустить именно этот виджет? Это потому, что виджет зарегистрирован как действие клиента.
Клиентское действие (как следует из его названия) является типом действия, определенным почти полностью в клиенте, в javascript для веб-клиента Odoo. Сервер просто отправляет тег действия (произвольное имя) и, при необходимости, добавляет несколько параметров, но помимо этого всё обрабатывается пользовательским кодом клиента.
Наш виджет зарегистрирован как обработчик для действия клиента следующим образом:
instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');
instance.web.client_actions
is a Registry()
in which
the action manager looks up client action handlers when it needs to execute
one. The first parameter of add()
is the name
(tag) of the client action, and the second parameter is the path to the widget
from the Odoo web client root.
Когда клиентское действие должно быть выполнено, менеджер действий ищет свой тег в реестре, проходит указанный путь и отображает виджет, который он находит в конце.
Примечание
обработчик действий клиента также может быть регулярной функцией, в случае, когда он будет вызван, и его результат (если он есть) будет интерпретироваться как следующее действие для выполнения.
На стороне сервера мы просто определили действие 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
, , делегирует синхронизацию и обработку самих представлений менеджеру представлений, который затем настраивает одно или несколько представлений в зависимости от требований исходного действия:
Представления
Most Odoo views are implemented through a subclass
of openerp.web.View()
which provides a bit of generic basic structure
for handling events and displaying model information.
Представление вида Search считается основным типом представления в Odoo фреймворке, но обрабатывается отдельно веб-клиентом (как правило оно является постоянным и может взаимодействовать с другими представлениями, чего не делают обычные представления).
A view is responsible for loading its own description XML (using
fields_view_get
) and any other data source
it needs. To that purpose, views are provided with an optional view
identifier set as the view_id
attribute.
Views are also provided with a DataSet()
instance which
holds most necessary model information (the model name and possibly various
record ids).
Views may also want to handle search queries by overriding
do_search()
, and updating their
DataSet()
as necessary.
Поля представлений вида Form
Обычной потребностью является расширение представления вида Form для добавления новых способов отображения полей.
All built-in fields have a default display implementation, a new
form widget may be necessary to correctly interact with a new field type
(e.g. a GIS field) or to provide new representations and ways to
interact with existing field types (e.g. validate
Char
fields which should contain email addresses
and display them as email links).
Чтобы явно указать, какой вид виджета следует использовать для отображения поля, просто используйте атрибут widget
в XML описании представления:
<field name="contact_mail" widget="email"/>
Примечание
один и тот же виджет представления вида Form используется как в режиме "просмотра" (только для чтения) и "редактирования", нельзя использовать один виджет для одного режима, а другой для другого
одно и тоже поле (имя поля) нельзя использовать в представлении вида Form
Виджет может игнорировать текущий режим представления вида Form и может оставаться неизменным как в режиме просмотра, так и в режиме редактирования
Поля создаются экземпляром представления вида Form после того, как оно прочитало свое XML описание и сформирован соответствующий HTML код, представляющий это описание. После этого представление вида Form будет взаимодействовать с объектами поля с использованием методов. Эти методы определены интерфейсом FieldInterface
. Почти все поля наследуют абстрактный класс AbstractField
. Этот класс определяет некоторые механизмы по умолчанию, которые должны быть реализованы большинством полей.
Вот некоторые из обязанностей класса поля:
Класс поля должен отображать и разрешать пользователю редактировать значение поля.
Он должен правильно реализовать 3 атрибута поля, доступных во всех полях Odoo. Класс
AbstractField
уже реализует алгоритм, который динамически вычисляет значение этих атрибутов (они могут измениться в любой момент, потому что их значение изменяется в соответствии со значением других полей). Эти значения хранятся в Widget Properties (свойства виджета были объяснены ранее в этом руководстве). Каждый класс поля несет ответственность за проверку свойств виджета и динамическую адаптацию в зависимости от их значений. Вот описание каждого из этих атрибутов:required
: поле должно иметь значение перед сохранением. Еслиrequired
имеет значениеtrue
и поле не имеет значения, метод поляis_valid()
должен возвращатьfalse
.invisible
: Когдаtrue
, поле должно быть невидимым. КлассAbstractField
уже имеет базовую реализацию этого поведения, которая подходит для большинства полей.readonly
: Когдаtrue
, поле нельзя редактировать пользователю. Большинство полей в Odoo имеют совершенно другое поведение в зависимости от значенияreadonly
. Например,FieldChar
отображает HTML<input>
когда он редактируемый и просто отображает текст, когда он доступен только для чтения. Это также означает, что требуется гораздо больше кода для реализации только этого поведения, но это необходимо для создания удобного пользовательского интерфейса.
Поля имеют два метода,
set_value()
иget_value()
, которые вызываются представлением вида Form, чтобы придать ему значение для отображения и возврата к новому значению, введенному пользователем. Эти методы должны уметь обрабатывать значение, заданное сервером Odoo, когда выполняется функцияread()
для модели данных и возвращать допустимое значение дляwrite()
. Помните, что типы данных JavaScript / Python, используемые для представления значений, заданныхread()
и отданныхwrite()
не обязательно должны быть одинаковы в Odoo. Например, когда вы читаете many2one, это всегда кортеж, первым значением которого является идентификатор указанной записи, а вторым является имя (например:(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
. Это позволит нам использовать это новое поле в любом представлении вида Form, указав widget="char2"
в теге <field/>
при создании XML описании представления.
В этом примере мы определяем единственный метод: render_value()
. Все, что он делает - так это отображает value``свойства виджета. Это два инструмента, определенные классом ``AbstractField
. Как объяснялось ранее, представление вида Form вызовет метод поля set_value()
, чтобы установить отображаемое значение. У этого метода уже есть реализация по умолчанию в AbstractField
, которая просто устанавливает value
. AbstractField
также смотрит событие change:value
направленное на себя и вызывает render_value()
когда это происходит. Таким образом, render_value()
это удобный метод для реализации в дочерних классах выполнения некоторых операции каждый раз, когда изменяется значение поля.
В методе init()
мы также определяем значение поля по умолчанию, если в представлении формы не указано ни одного (здесь мы предполагаем, что значение по умолчанию поля char
должно быть пустой строкой).
Поле для чтения-записи
Поля, доступные только для чтения, которые отображают только контент и не позволяют пользователю изменять его, могут быть полезны, однако большинство полей в 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
.
Пользовательские виджеты для представления вида Form
Поля представления вида Form используются для редактирования одного поля и в основном связаны с этим полем. Поскольку это может быть ограничением, также возможно создавать виджеты для представления вида Form, которые не являются настолько ограниченными и имеют меньше связей с конкретным жизненным циклом.
Пользовательские виджеты для представления вида Form могут быть добавлены через тег widget
:
<widget type="xxx" />
Этот тип виджета будет просто создан представлением вида Form во время генерации HTML в соответствии с определением в XML описании. Они имеют общие свойства с полями (например, свойство effective_readonly
) но им не присвоено точное поле. И поэтому они не имеют методов, таких как get_value()
и set_value()
. Они должны наследоваться от абстрактного класса FormWidget
.
Form widgets can interact with form fields by listening for their changes and
fetching or altering their values. They can access form fields through
their field_manager
attribute:
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
is generally the
FormView()
itself, but features used from it should
be limited to those defined by FieldManagerMixin()
,
the most useful being:
get_field_value(field_name)()
which returns the value of a field.set_values(values)()
sets multiple field values, takes a mapping of{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&ll=XXX,YYY&output=embed">
</iframe>
Где `` XXX`` следует заменить широтой и `` YYY`` - долготой.
Отобразите два поля позиции и виджет карты, используя их в новой вкладке в режиме notebook представления вида Form текущего товара.
Exercise
Получение текущих координат
Добавьте кнопку, переустановив координаты продукта в местоположение пользователя, вы можете получить эти координаты, используя интерфейс javascript geolocation API.
Теперь мы хотим отобразить дополнительную кнопку, чтобы автоматически устанавливать координаты текущего положение пользователя.
Чтобы получить координаты пользователя, простым способом является использование JavaScript API-интерфейс. Смотрите See the online documentation to know how to use it.
Также обратите внимание, что пользователь не должен нажимать на эту кнопку, когда вид формы находится в режиме только для чтения. Таким образом, этот пользовательский виджет должен корректно обрабатывать свойство effective_readonly
точно так же, как любое поле. Одним из способов сделать это было бы заставить кнопку исчезать, когда значение effective_readonly
истинно.
как отдельное понятие из экземпляра. Во многих языках классы являются полноценными объектами а так же сами экземпляры (метаклассов), но это два достаточно разных поняти класс и экземпляр и между ними соблюдается четко прописанная иерархия
А также с учетом различий между браузерами, хотя со временем это стало менее необходимым