A web kliens testreszabása¶
Veszély
Ez az útmutató elavult.
Ez az útmutató az Odoo web kliens moduljainak létrehozásáról szól.
To create websites with Odoo, see Build a website theme; to add business capabilities or extend existing business systems of Odoo, see Modul készítése.
Figyelem
Ez az útmutató feltételezi a következő ismereteket:
Javascript alapok és jó gyakorlatok
It also requires an installed Odoo, and Git.
Egy egyszerű modul¶
Kezdjünk egy egyszerű Odoo modullal, amely alapvető webkomponens-konfigurációt tartalmaz, és lehetővé teszi számunkra a webes keretrendszer tesztelését.
A példamodul online elérhető, és a következő parancs segítségével letölthető:
$ git clone http://github.com/odoo/petstore
Ez létrehoz egy petstore mappát ott, ahol a parancsot végrehajtottad. Ezután hozzá kell adnod ezt a mappát az Odoo addons path útvonalához, létre kell hoznod egy új adatbázist, és telepítened kell az oepetstore modult.
Ha megnyitod a petstore mappát, a következő tartalmat kell látnod:
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
A modul már tartalmaz különféle szerver testreszabásokat. Később visszatérünk ezekre, most koncentráljunk a webhez kapcsolódó tartalomra, a static mappában.
Files used in the „web” side of an Odoo module must be placed in a static
folder so they are available to a web browser, files outside that folder can
not be fetched by browsers. The src/css, src/js and src/xml
sub-folders are conventional and not strictly necessary.
oepetstore/static/css/petstore.cssJelenleg üres, a CSS-t fogja tartalmazni a kisállatbolt tartalmához
oepetstore/static/xml/petstore.xmlTöbbnyire üres, a QWeb sablonok sablonokat fogja tartalmazni
oepetstore/static/js/petstore.jsA legfontosabb (és legérdekesebb) rész, az alkalmazás logikáját tartalmazza (vagy legalábbis annak web-böngésző oldali részét) javascript formájában. Jelenleg így kell kinéznie:
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'); }
Amely csak egy kis üzenetet ír ki a böngésző konzoljába.
A static mappában lévő fájlokat a modulon belül kell definiálni, hogy helyesen betöltődjenek. Minden, ami a src/xml-ben van, a __manifest__.py-ben van definiálva, míg a src/css és src/js tartalma a petstore.xml-ben vagy egy hasonló fájlban van definiálva.
Figyelem
Minden JavaScript fájl össze van fűzve és minified, hogy javítsa az alkalmazás betöltési idejét.
One of the drawback is debugging becomes more difficult as individual files disappear and the code is made significantly less readable. It is possible to disable this process by enabling the „developer mode”: log into your Odoo instance (user admin password admin by default) open the user menu (in the top-right corner of the Odoo screen) and select About Odoo then Activate the developer mode:
Ez újratölti a webes klienst optimalizálások nélkül, ami jelentősen kényelmesebbé teszi a fejlesztést és a hibakeresést.
Odoo JavaScript Modul¶
A Javascript nem rendelkezik beépített modulokkal. Ennek eredményeként a különböző fájlokban definiált változók összekeverednek és ütközhetnek. Ez különböző modul minták kialakulásához vezetett, amelyek tiszta névterek létrehozására és a névütközések kockázatának csökkentésére szolgálnak.
Az Odoo keretrendszer egy ilyen mintát használ a modulok definiálására a webes bővítményekben, hogy mind a kód névterét biztosítsa, mind a betöltés sorrendjét helyesen állítsa be.
oepetstore/static/js/petstore.js tartalmaz egy modul deklarációt:
odoo.oepetstore = function(instance, local) {
local.xxx = ...;
}
Az Odoo webben a modulok a globális odoo változóra beállított függvényekként vannak deklarálva. A függvény neve meg kell egyezzen a bővítmény nevével (ebben az esetben oepetstore), hogy a keretrendszer megtalálhassa és automatikusan inicializálhassa azt.
Amikor a web kliens betölti a modulodat, meghívja a gyökér függvényt és két paramétert biztosít:
az első paraméter az Odoo web kliens aktuális példánya, amely hozzáférést biztosít az Odoo által definiált különböző képességekhez (fordítások, hálózati szolgáltatások), valamint a mag vagy más modulok által definiált objektumokhoz.
a második paraméter a saját helyi névtered, amelyet a web kliens automatikusan létrehoz. Azok az objektumok és változók, amelyeknek elérhetőnek kell lenniük a modulodon kívülről (akár azért, mert az Odoo web kliensnek hívnia kell őket, akár azért, mert mások testre szeretnék szabni őket), ebben a névtérben kell, hogy legyenek beállítva.
Osztályok¶
Hasonlóan a modulokhoz, és ellentétben a legtöbb objektum-orientált nyelvvel, a javascript nem épít be osztályokat1, bár biztosít hozzávetőlegesen egyenértékű (ha alacsonyabb szintű és bőbeszédűbb) mechanizmusokat.
Az egyszerűség és a fejlesztőbarát megközelítés érdekében az Odoo web egy osztályrendszert biztosít John Resig Simple JavaScript Inheritance alapján.
Új osztályok definiálhatók az extend() metódus meghívásával a odoo.web.Class() osztályból:
var MyClass = instance.web.Class.extend({
say_hello: function() {
console.log("hello");
},
});
Az extend() metódus egy szótárt vesz át, amely leírja az új osztály tartalmát (metódusok és statikus attribútumok). Ebben az esetben csak egy say_hello metódusa lesz, amely nem vesz át paramétereket.
Az osztályok példányosítása a new operátorral történik:
var my_object = new MyClass();
my_object.say_hello();
// print "hello" in the console
És az példány attribútumai elérhetők a this segítségével:
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
Az osztályok biztosíthatnak egy inicializálót az példány kezdeti beállításának elvégzéséhez, egy init() metódus definiálásával. Az inicializáló megkapja a new operátor használatakor átadott paramétereket:
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
Lehetőség van meglévő (felhasználó által definiált) osztályokból származtatott osztályok létrehozására is, azáltal, hogy a szülő osztályon meghívjuk az extend() metódust, ahogyan az Class() származtatásánál történik:
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
Amikor egy metódust felülírunk öröklés használatával, a this._super() segítségével hívhatjuk meg az eredeti metódust:
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
Figyelem
A _super nem egy szabványos metódus, az aktuális öröklési lánc következő metódusára van beállítva menet közben, ha van ilyen. Csak a metódushívás szinkron részében van definiálva, aszinkron kezelőkben való használathoz (hálózati hívások után vagy setTimeout visszahívásokban) meg kell őrizni az értékére való hivatkozást, nem szabad a this segítségével elérni:
// 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);
}
Widgetek alapjai¶
Az Odoo web kliens tartalmazza a jQuery-t a DOM egyszerű manipulálásához. Hasznos, és jobb API-t biztosít, mint a szabványos W3C DOM2, de nem elegendő a bonyolult alkalmazások struktúrájának kialakításához, ami nehéz karbantartáshoz vezet.
Hasonlóan az objektumorientált asztali UI eszközkészletekhez (pl. Qt, Cocoa vagy GTK), az Odoo Web meghatározott komponenseket tesz felelőssé az oldal egyes részeiért. Az Odoo webben az ilyen komponensek alapja a Widget() osztály, amely egy oldalrész kezelésére és a felhasználó számára történő információ megjelenítésére specializálódott komponens.
Az első widgeted¶
A kezdeti bemutató modul már biztosít egy alap widgetet:
local.HomePage = instance.Widget.extend({
start: function() {
console.log("pet store home page loaded");
},
});
Kiterjeszti a Widget()-et és felülírja a szabványos start() metódust, amely — hasonlóan az előző MyClass-hoz — jelenleg keveset tesz.
Ez a sor a fájl végén:
instance.web.client_actions.add(
'petstore.homepage', 'instance.oepetstore.HomePage');
regisztrálja az alap widgetünket kliens akcióként. A kliens akciók később kerülnek magyarázatra, jelenleg ez az, ami lehetővé teszi, hogy a widgetünket meghívjuk és megjelenítsük, amikor kiválasztjuk a menüt.
Figyelem
because the widget will be called from outside our module, the web client needs its „fully qualified” name, not the local version.
Tartalom megjelenítése¶
A widgetek számos metódussal és funkcióval rendelkeznek, de az alapok egyszerűek:
widget beállítása
a widget adatainak formázása
a widget megjelenítése
A HomePage widget már rendelkezik egy start() metódussal. Ez a metódus a normál widget életciklus része, és automatikusan meghívódik, amint a widget beillesztésre kerül az oldalra. Használhatjuk arra, hogy tartalmat jelenítsünk meg.
Minden widget rendelkezik egy $el attribútummal, amely az oldal azon részét képviseli, amelyért felelős (jQuery objektumként). A widget tartalmát ide kell beilleszteni. Alapértelmezés szerint a $el egy üres <div> elem.
Egy <div> elem általában láthatatlan a felhasználó számára, ha nincs tartalma (vagy nincsenek olyan speciális stílusok, amelyek méretet adnak neki), ezért nem jelenik meg semmi az oldalon, amikor a HomePage elindul.
Adjunk hozzá némi tartalmat a widget gyökéreleméhez, jQuery használatával:
local.HomePage = instance.Widget.extend({
start: function() {
this.$el.append("<div>Hello dear Odoo user!</div>");
},
});
Ez az üzenet most megjelenik, amikor megnyitja a
Megjegyzés
az Odoo Web-ben betöltött javascript kód frissítéséhez újra kell töltenie az oldalt. Nincs szükség az Odoo szerver újraindítására.
The HomePage widget is used by Odoo Web and managed automatically.
To learn how to use a widget „from scratch” let’s create a new one:
local.GreetingsWidget = instance.Widget.extend({
start: function() {
this.$el.append("<div>We are so happy to see you again in this menu!</div>");
},
});
Most hozzáadhatjuk a GreetingsWidget-et a HomePage-hez a GreetingsWidget appendTo() metódusának használatával:
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);
},
});
A
HomePageelőször hozzáadja saját tartalmát a DOM gyökeréhezA
HomePageezután példányosítja aGreetingsWidget-etVégül megmondja a
GreetingsWidget-nek, hogy hova illessze be magát, delegálva$elegy részét aGreetingsWidget-nek.
Amikor a appendTo() metódust meghívják, a widgetet arra kéri, hogy illessze be magát a megadott pozícióba és jelenítse meg a tartalmát. A start() metódus a appendTo() hívása során kerül meghívásra.
Ahhoz, hogy lássuk, mi történik a megjelenített felület alatt, a böngésző DOM Explorerét fogjuk használni. De először módosítsuk kissé a widgetjeinket, hogy könnyebben megtaláljuk őket, azáltal, hogy osztályt adunk a gyökérelemekhez:
local.HomePage = instance.Widget.extend({
className: 'oe_petstore_homepage',
...
});
local.GreetingsWidget = instance.Widget.extend({
className: 'oe_petstore_greetings',
...
});
Ha megtalálja a DOM releváns részét (kattintson jobb gombbal a szövegre, majd Elem vizsgálata), annak így kell kinéznie:
<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>
Ez egyértelműen megmutatja a két <div> elemet, amelyeket automatikusan létrehozott a Widget(), mert hozzáadtunk néhány osztályt rajtuk.
Láthatjuk a két üzenetet tartalmazó divet is, amelyeket mi magunk adtunk hozzá
Finally, note the <div class="oe_petstore_greetings"> element which
represents the GreetingsWidget instance is inside the
<div class="oe_petstore_homepage"> which represents the HomePage
instance, since we appended
Widget Szülők és Gyermekek¶
Az előző részben egy widgetet példányosítottunk ezzel a szintaxissal:
new local.GreetingsWidget(this);
Az első argumentum a this, amely ebben az esetben egy HomePage példány volt. Ez megmondja a létrehozott widgetnek, hogy melyik másik widget a szülője.
As we’ve seen, widgets are usually inserted in the DOM by another widget and inside that other widget’s root element. This means most widgets are „part” of another widget, and exist on behalf of it. We call the container the parent, and the contained widget the child.
Több technikai és koncepcionális okból kifolyólag szükséges, hogy egy widget tudja, ki a szülője és kik a gyermekei.
getParent()használható egy widget szülőjének lekérésére:
local.GreetingsWidget = instance.Widget.extend({ start: function() { console.log(this.getParent().$el ); // will print "div.oe_petstore_homepage" in the console }, });
getChildren()használható a gyermekei listájának lekérésére:
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 }, });
Amikor egy widget init() metódusát felülírjuk, rendkívül fontos, hogy a szülőt átadjuk a this._super() hívásnak, különben a kapcsolat nem lesz helyesen beállítva:
local.GreetingsWidget = instance.Widget.extend({
init: function(parent, name) {
this._super(parent);
this.name = name;
},
});
Végül, ha egy widgetnek nincs szülője (pl. mert az alkalmazás gyökér widgetje), null adható meg szülőként:
new local.GreetingsWidget(null);
Widgetek megsemmisítése¶
Ha képes tartalmat megjeleníteni a felhasználóinak, akkor azt is képesnek kell lennie törölni. Ezt a destroy() metódus segítségével lehet megtenni:
greeting.destroy();
Amikor egy widget megsemmisül, először meghívja a destroy() függvényt az összes gyermekén. Ezután törli magát a DOM-ból. Ha a init() vagy a start() során állandó struktúrákat hozott létre, amelyeket kifejezetten meg kell tisztítani (mert a szemétgyűjtő nem kezeli őket), felülírhatja a destroy() függvényt.
Veszély
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
A QWeb sablonmotor¶
Az előző részben úgy adtunk tartalmat a widgetjeinkhez, hogy közvetlenül manipuláltuk (és hozzáadtuk) a DOM-jukat:
this.$el.append("<div>Hello dear Odoo user!</div>");
Ez lehetővé teszi bármilyen típusú tartalom generálását és megjelenítését, de nehézkessé válik, amikor jelentős mennyiségű DOM-ot generálunk (sok ismétlés, idézési problémák, …)
Mint sok más környezetben, az Odoo megoldása egy sablonmotor használata. Az Odoo sablonmotorját QWeb sablonok-nek hívják.
A QWeb egy XML-alapú sablonnyelv, hasonló a Genshi, Thymeleaf vagy Facelets nyelvekhez. A következő jellemzőkkel rendelkezik:
Teljes mértékben JavaScript-ben van megvalósítva és a böngészőben renderelődik
Minden sablonfájl (XML fájlok) több sablont tartalmaz
Különleges támogatással rendelkezik az Odoo Web
Widget()-ben, bár az Odoo web kliensén kívül is használható (és lehetséges aWidget()használata QWeb nélkül is)
Megjegyzés
A QWeb használatának indoka a meglévő javascript sablonmotorok helyett a már meglévő (harmadik féltől származó) sablonok kiterjeszthetősége, hasonlóan az Odoo nézetekhez.
A legtöbb JavaScript sablonmotor szöveg alapú, ami kizárja az egyszerű strukturális kiterjeszthetőséget, ahol egy XML alapú sablonmotor általánosan módosítható például XPath vagy CSS és egy fa-módosító DSL (vagy akár csak XSLT) használatával. Ez a rugalmasság és kiterjeszthetőség az Odoo alapvető jellemzője, és ennek elvesztése elfogadhatatlannak minősült.
QWeb használata¶
Először definiáljunk egy egyszerű QWeb sablont a szinte üres oepetstore/static/src/xml/petstore.xml fájlban:
<?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>
Most használhatjuk ezt a sablont a HomePage widgetben. A lap tetején definiált QWeb loader változó használatával hivatkozhatunk az XML fájlban definiált sablonra:
local.HomePage = instance.Widget.extend({
start: function() {
this.$el.append(QWeb.render("HomePageTemplate"));
},
});
QWeb.render() megkeresi a megadott sablont, sztringgé rendereli, és visszaadja az eredményt.
Azonban, mivel a Widget() speciális integrációval rendelkezik a QWeb számára, a sablon közvetlenül beállítható a widgeten keresztül annak template attribútumán keresztül:
local.HomePage = instance.Widget.extend({
template: "HomePageTemplate",
start: function() {
...
},
});
Bár az eredmény hasonlónak tűnik, két különbség van ezek között a használatok között:
a második verzióval a sablon közvetlenül a
start()meghívása előtt renderelődikin the first version the template’s content is added to the widget’s root element, whereas in the second version the template’s root element is directly set as the widget’s root element. Which is why the „greetings” sub-widget also gets a red background
Figyelem
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)
QWeb kontextus¶
A QWeb sablonokhoz adatok adhatók, és tartalmazhatnak alapvető megjelenítési logikát.
Az QWeb.render() explicit hívásai esetén a sablon adatai második paraméterként kerülnek átadásra:
QWeb.render("HomePageTemplate", {name: "Klaus"});
a sablon módosításával:
<t t-name="HomePageTemplate">
<div>Hello <t t-esc="name"/></div>
</t>
az eredmény:
<div>Hello Klaus</div>
Amikor Widget() integrációját használjuk, nem lehetséges további adatokat biztosítani a sablon számára. A sablon egyetlen widget kontextus változót kap, amely a megjelenített widgetre hivatkozik közvetlenül azelőtt, hogy a start() meghívásra kerülne (a widget állapota lényegében az lesz, amit a init() állít be):
<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() {
},
});
Eredmény:
<div>Hello Mordecai</div>
Sablon deklaráció¶
Láttuk, hogyan lehet megjeleníteni QWeb sablonokat, most nézzük meg maguknak a sablonoknak a szintaxisát.
Egy QWeb sablon hagyományos XML-ből áll, amely QWeb direktívákkal van keverve. Egy QWeb direktíva XML attribútumokkal van deklarálva, amelyek t--vel kezdődnek.
A legegyszerűbb direktíva a t-name, amelyet új sablonok deklarálására használnak egy sablonfájlban:
<templates>
<t t-name="HomePageTemplate">
<div>This is some simple HTML</div>
</t>
</templates>
A t-name a definiált sablon nevét veszi fel, és deklarálja, hogy az QWeb.render() használatával hívható. Csak a sablonfájl legfelső szintjén használható.
Kódolás¶
A t-esc direktíva használható szöveg kiíratására:
<div>Hello <t t-esc="name"/></div>
Egy Javascript kifejezést vesz, amelyet kiértékel, a kifejezés eredménye pedig HTML-eszképelt formában kerül beillesztésre a dokumentumba. Mivel ez egy kifejezés, lehetőség van csak egy változónév megadására, mint fent, vagy egy összetettebb kifejezésre, mint például egy számítás:
<div><t t-esc="3+5"/></div>
vagy metódushívások:
<div><t t-esc="name.toUpperCase()"/></div>
HTML kiíratása¶
HTML beillesztéséhez a megjelenített oldalba használja a t-raw-t. A t-esc-hez hasonlóan tetszőleges Javascript kifejezést vesz paraméterként, de nem hajt végre HTML-eszkép lépést.
<div><t t-raw="name.link(user_account)"/></div>
Veszély
A t-raw nem használható olyan adatokon, amelyek nem eszképelt, felhasználó által megadott tartalmat tartalmazhatnak, mivel ez cross-site scripting sebezhetőségekhez vezet.
Feltételes utasítások¶
A QWeb rendelkezhet feltételes blokkokkal a t-if használatával. A direktíva tetszőleges kifejezést vesz, ha a kifejezés hamis (false, null, 0 vagy üres string), az egész blokk elnyomásra kerül, ellenkező esetben megjelenik.
<div>
<t t-if="true == true">
true is true
</t>
<t t-if="true == false">
true is not true
</t>
</div>
Megjegyzés
QWeb doesn’t have an „else” structure, use a second t-if with the
original condition inverted. You may want to store the condition in a
local variable if it’s a complex or expensive expression.
Iteráció¶
Lista bejárásához használja a t-foreach és t-as direktívákat. A t-foreach egy kifejezést vesz, amely egy bejárandó listát ad vissza, a t-as pedig egy változónevet vesz, amelyet az iteráció során minden elemhez köt.
<div>
<t t-foreach="names" t-as="name">
<div>
Hello <t t-esc="name"/>
</div>
</t>
</div>
Megjegyzés
A t-foreach számokkal és objektumokkal (szótárakkal) is használható.
Attribútumok meghatározása¶
A QWeb két kapcsolódó direktívát biztosít a számított attribútumok meghatározására: t-att-name és t-attf-name. Mindkét esetben a name az attribútum neve, amelyet létre kell hozni (pl. t-att-id az id attribútumot határozza meg a renderelés után).
A t-att- egy javascript kifejezést vesz, amelynek eredménye az attribútum értékeként kerül beállításra, ez a leghasznosabb, ha az attribútum értéke teljes egészében számított:
<div>
Input your name:
<input type="text" t-att-value="defaultName"/>
</div>
A t-attf- egy formátum stringet vesz. A formátum string egy szöveges rész interpolációs blokkokkal, az interpolációs blokk egy javascript kifejezés {{ és }} között, amelyet a kifejezés eredménye helyettesít. Ez a leghasznosabb olyan attribútumok esetén, amelyek részben szövegesek és részben számítottak, mint például egy osztály:
<div t-attf-class="container {{ left ? 'text-left' : '' }} {{ extra_class }}">
insert content here
</div>
Más sablonok meghívása¶
A sablonok al-sablonokra oszthatók (az egyszerűség, karbantarthatóság, újrafelhasználhatóság érdekében vagy a túlzott jelölés beágyazás elkerülése érdekében).
Ez a t-call direktíva használatával történik, amely a renderelendő sablon nevét veszi:
<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>
az A sablon renderelése az alábbi eredményt adja:
<div class="i-am-a">
<div class="i-am-b"/>
</div>
Az al-sablonok öröklik a hívójuk renderelési kontextusát.
További információk a QWeb-ről¶
A QWeb referenciához lásd: QWeb sablonok.
Gyakorlat¶
Exercise
QWeb használata Widgetekben
Hozzon létre egy widgetet, amelynek konstruktora két paramétert vesz fel a parent mellett: product_names és color.
A
product_namesegy karakterláncokból álló tömb legyen, mindegyik egy termék neve.A
coloregy karakterlánc, amely egy színt tartalmaz CSS színformátumban (pl.:#000000a feketéhez).
A widgetnek meg kell jelenítenie a megadott termékneveket egymás alatt, mindegyiket külön dobozban, amelynek háttérszíne a color értéke, és van egy kerete. A HTML megjelenítéséhez használja a QWeb-et. A szükséges CSS-nek az oepetstore/static/src/css/petstore.css fájlban kell lennie.
Használja a widgetet a HomePage-en fél tucat termékkel.
Widget Segítők¶
A Widget jQuery Szelektora¶
A DOM elemek kiválasztása egy widgeten belül a widget DOM gyökerén meghívott find() metódussal végezhető el:
this.$el.find("input.my_input")...
Mivel ez egy gyakori művelet, a Widget() biztosít egy ekvivalens rövidítést a $() metóduson keresztül:
local.MyWidget = instance.Widget.extend({
start: function() {
this.$("input.my_input")...
},
});
Figyelem
A globális jQuery függvényt $() soha nem szabad használni, hacsak nem feltétlenül szükséges: a widget gyökerén végzett kiválasztás a widgetre és annak helyi környezetére korlátozódik, míg a $()-val végzett kiválasztások globálisak az oldalra/alkalmazásra nézve, és más widgetek és nézetek részeit is érinthetik, ami furcsa vagy veszélyes mellékhatásokhoz vezethet. Mivel egy widgetnek általában csak a saját DOM szekcióján kellene működnie, nincs ok a globális kiválasztásra.
Egyszerűbb DOM események kötése¶
Korábban a DOM eseményeket normál jQuery eseménykezelőkkel kötöttük (pl. .click() vagy .change()) a widget elemein:
local.MyWidget = instance.Widget.extend({
start: function() {
var self = this;
this.$(".my_button").click(function() {
self.button_clicked();
});
},
button_clicked: function() {
..
},
});
Bár ez működik, van néhány probléma vele:
meglehetősen bőbeszédű
nem támogatja a widget gyökérelemének futásidejű cseréjét, mivel a kötés csak akkor történik meg, amikor a
start()fut (a widget inicializálása során)megköveteli a
this-kötési problémák kezelését
A widgetek ezért biztosítanak egy rövidítést a DOM események kötésére a events segítségével:
local.MyWidget = instance.Widget.extend({
events: {
"click .my_button": "button_clicked",
},
button_clicked: function() {
..
}
});
events egy objektum (leképezés), amely egy eseményt a meghívandó függvényhez vagy metódushoz rendel, amikor az esemény bekövetkezik:
a kulcs egy esemény neve, amelyet esetleg egy CSS szelekcióval finomítanak, ebben az esetben csak akkor fut le a függvény vagy metódus, ha az esemény egy kiválasztott al-elemen történik:
clickkezeli az összes kattintást a widgeten belül, de aclick .my_buttoncsak azokat a kattintásokat kezeli, amelyek amy_buttonosztályú elemekben történnekaz érték az a művelet, amelyet az esemény bekövetkezésekor kell végrehajtani
Ez lehet egy függvény:
events: { 'click': function (e) { /* code here */ } }
vagy az objektum egy metódusának neve (lásd a fenti példát).
Mindkét esetben a
thisa widget példányára utal, és a kezelő egyetlen paramétert kap, az esemény jQuery esemény objektum-ját.
Widget Események és Tulajdonságok¶
Események¶
A widgetek egy eseményrendszert biztosítanak (különállóan a fent leírt DOM/jQuery eseményrendszertől): egy widget képes eseményeket kiváltani önmagán, és más widgetek (vagy önmaga) képesek ezekhez az eseményekhez kötődni és figyelni azokat:
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>");
},
});
Ez a widget homlokzatként működik, átalakítva a felhasználói bemenetet (DOM eseményeken keresztül) egy dokumentálható belső eseménnyé, amelyhez a szülő widgetek kötődhetnek.
trigger() az esemény nevét veszi első (kötelező) argumentumként, bármely további argumentumot eseményadatként kezel és közvetlenül továbbítja a hallgatóknak.
Ezután beállíthatunk egy szülő eseményt, amely példányosítja az általános widgetünket, és figyeli a user_chose eseményt a on() használatával:
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() egy függvényt köt össze, amelyet akkor hívnak meg, amikor az event_name által azonosított esemény bekövetkezik. A func argumentum a meghívandó függvény, és az object az az objektum, amelyhez a függvény kapcsolódik, ha az egy metódus. A kötött függvényt a trigger() további argumentumaival hívják meg, ha vannak ilyenek. Példa:
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"
}
Megjegyzés
Események kiváltása más widgeteken általában rossz ötlet. A fő kivétel ez alól az odoo.web.bus, amely kifejezetten azért létezik, hogy közvetítse azokat az eseményeket, amelyek bármely widget számára érdekesek lehetnek az Odoo webalkalmazásban.
Tulajdonságok¶
A tulajdonságok nagyon hasonlóak a normál objektum attribútumokhoz abban, hogy lehetővé teszik az adatok tárolását egy widget példányon, azonban van egy további funkciójuk, hogy eseményeket váltanak ki, amikor beállítják őket:
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()beállítja egy tulajdonság értékét, és kiváltja achange:propname(ahol propname az első paraméterként aset()-nek átadott tulajdonság neve) és achangeeseménytget()lekéri egy tulajdonság értékét.
Gyakorlat¶
Exercise
Widget tulajdonságok és események
Create a widget ColorInputWidget that will display 3 <input
type="text">. Each of these <input> is dedicated to type a
hexadecimal number from 00 to FF. When any of these <input> is
modified by the user the widget must query the content of the three
<input>, concatenate their values to have a complete CSS color code
(ie: #00FF00) and put the result in a property named color. Please
note the jQuery change() event that you can bind on any HTML
<input> element and the val() method that can query the current
value of that <input> could be useful to you for this exercise.
Ezután módosítsa a HomePage widgetet, hogy példányosítsa a ColorInputWidget-et és megjelenítse azt. A HomePage widgetnek egy üres téglalapot is meg kell jelenítenie. Ennek a téglalapnak mindig, bármely pillanatban, ugyanazt a háttérszínt kell mutatnia, mint a ColorInputWidget példány color tulajdonságában lévő szín.
Használja a QWeb-et az összes HTML generálásához.
Meglévő widgetek és osztályok módosítása¶
Az Odoo web keretrendszer osztályrendszere lehetővé teszi a meglévő osztályok közvetlen módosítását az include() metódus használatával:
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"
Ez a rendszer hasonló az öröklési mechanizmushoz, kivéve, hogy a célosztályt helyben módosítja, ahelyett, hogy új osztályt hozna létre.
Ebben az esetben a this._super() a lecserélt/újradefiniált metódus eredeti megvalósítását hívja meg. Ha az osztálynak már voltak alosztályai, akkor az összes this._super() hívás az alosztályokban az új megvalósításokat fogja hívni, amelyeket az include() hívásban definiáltak. Ez akkor is működni fog, ha az osztály (vagy bármelyik alosztályának) néhány példánya már létrejött az include() hívása előtt.
Fordítások¶
A szöveg fordításának folyamata a Python és JavaScript kódban nagyon hasonló. Észrevehetted ezeket a sorokat a petstore.js fájl elején:
var _t = instance.web._t,
_lt = instance.web._lt;
Ezek a sorok egyszerűen a fordítási függvények importálására szolgálnak az aktuális JavaScript modulban. Így használják őket:
this.$el.text(_t("Hello user!"));
Az Odoo-ban a fordítási fájlok automatikusan generálódnak a forráskód átvizsgálásával. Minden kódrészlet, amely egy bizonyos függvényt hív, észlelésre kerül, és tartalmuk hozzáadódik egy fordítási fájlhoz, amelyet aztán a fordítóknak küldenek. Pythonban a függvény _(). JavaScriptben a függvény _t() (és szintén _lt()).
A _t() visszaadja a megadott szöveghez definiált fordítást. Ha nincs fordítás definiálva az adott szöveghez, az eredeti szöveget adja vissza változatlanul.
Megjegyzés
A felhasználó által megadott értékek beillesztéséhez a fordítható szövegekbe ajánlott az _.str.sprintf használata névvel ellátott argumentumokkal a fordítás után:
this.$el.text(_.str.sprintf(
_t("Hello, %(user)s!"), {
user: "Ed"
}));
Ez a fordítható szövegeket olvashatóbbá teszi a fordítók számára, és nagyobb rugalmasságot biztosít számukra a paraméterek átrendezésére vagy figyelmen kívül hagyására.
_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.
Arra használják, hogy a fordítható kifejezéseket meghatározzák, mielőtt a fordítási rendszer inicializálásra kerülne, például osztályattribútumok esetén (mivel a modulok betöltődnek, mielőtt a felhasználó nyelve beállításra kerülne és a fordítások letöltésre kerülnének).
Kommunikáció az Odoo szerverrel¶
Modellek elérése¶
A legtöbb Odoo-val kapcsolatos művelet magában foglalja az üzleti vonatkozásokat megvalósító modellekkel való kommunikációt, ezek a modellek pedig (esetlegesen) kapcsolatba lépnek valamilyen tárolómotorral (általában PostgreSQL).
Bár a jQuery biztosít egy $.ajax függvényt a hálózati interakciókhoz, az Odoo-val való kommunikációhoz további metaadatokra van szükség, amelyek minden hívás előtti beállítása bőbeszédű és hibára hajlamos lenne. Ennek eredményeként az Odoo web magasabb szintű kommunikációs primitíveket biztosít.
Ennek bemutatására a petstore.py fájl már tartalmaz egy kis modellt egy mintamódszerrel:
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),
Ez egy modellt deklarál két mezővel, és egy my_method() módszerrel, amely egy literális szótárt ad vissza.
Itt van egy minta widget, amely meghívja a my_method()-t és megjeleníti az eredményt:
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
});
},
});
Az Odoo modellek hívására használt osztály a odoo.Model(). Az Odoo modell nevével van példányosítva első paraméterként (itt oepetstore.message_of_the_day).
call() használható bármely (nyilvános) Odoo modell módszerének hívására. Az alábbi pozicionális argumentumokat veszi fel:
nameA meghívandó metódus neve, itt
my_methodargsegy pozicionális argumentumok tömb, amelyet a metódusnak kell átadni. Mivel a példában nincs átadandó pozicionális argumentum, az
argsparaméter nincs megadva.Itt egy másik példa pozicionális argumentumokkal:
@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
kwargsegy kulcsszó argumentumok leképezés, amelyet át kell adni. A példa egyetlen névvel ellátott argumentumot ad meg,
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() egy halasztott értéket ad vissza, amelyet a modell metódusának visszaadott értéke old meg első argumentumként.
CompoundContext¶
Az előző rész egy context argumentumot használt, amely nem volt megmagyarázva a metódushívásban:
model.call("my_method", {context: new instance.web.CompoundContext()})
The context is like a „magic” argument that the web client will always give to the server when calling a method. The context is a dictionary containing multiple keys. One of the most important key is the language of the user, used by the server to translate all the messages of the application. Another one is the time zone of the user, used to compute correctly dates and times if Odoo is used by people in different countries.
Az argument minden metódusban szükséges, különben rossz dolgok történhetnek (például az alkalmazás nem lesz helyesen lefordítva). Ezért, amikor egy modell metódusát hívja meg, mindig meg kell adnia ezt az argumentumot. A megoldás erre az, hogy használja a odoo.web.CompoundContext()-et.
CompoundContext() egy osztály, amelyet a felhasználó kontextusának (nyelv, időzóna stb.) szerverre történő továbbítására használnak, valamint új kulcsok hozzáadására a kontextushoz (egyes modellek metódusai önkényes kulcsokat használnak, amelyeket a kontextushoz adtak). Létrehozása során bármennyi szótárt vagy más CompoundContext() példányt adhatunk meg a konstruktorának. Ezeket a kontextusokat egyesíti, mielőtt a szerverre küldené őket.
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}
Látható, hogy az context argumentumban lévő szótár tartalmaz néhány kulcsot, amelyek az aktuális felhasználó Odoo-ban lévő konfigurációjához kapcsolódnak, valamint a new_key kulcsot, amelyet a CompoundContext() példányosításakor adtak hozzá.
Lekérdezések¶
Bár a call() elegendő bármilyen interakcióhoz az Odoo modellekkel, az Odoo Web biztosít egy segédeszközt a modellek egyszerűbb és világosabb lekérdezéséhez (rekordok lekérése különböző feltételek alapján): query(), amely egy rövidítés a gyakori search() és read() kombinációhoz. Világosabb szintaxist biztosít a modellek kereséséhez és olvasásához:
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
});
szemben:
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()opcionálisan egy mezőlistát vesz fel paraméterként (ha nem adunk meg mezőt, a modell összes mezője lekérdezésre kerül). Egyodoo.web.Query()-t ad vissza, amely tovább testreszabható, mielőtt végrehajtanánk.Query()a felépülő lekérdezést képviseli. Ez változatlan, a lekérdezés testreszabására szolgáló metódusok valójában egy módosított másolatot adnak vissza, így lehetséges az eredeti és az új verzió párhuzamos használata. LásdQuery()a testreszabási lehetőségeiért.
Amikor a lekérdezés a kívánt módon van beállítva, egyszerűen hívja meg a all()-t a végrehajtásához, és egy halasztott eredményt ad vissza. Az eredmény ugyanaz, mint a read()-é, egy szótárakból álló tömb, ahol minden szótár egy kért rekord, minden kért mező pedig egy szótárkulcs.
Gyakorlatok¶
Exercise
A nap üzenete
Hozzon létre egy MessageOfTheDay widgetet, amely az oepetstore.message_of_the_day modell utolsó rekordját jeleníti meg. A widgetnek azonnal le kell kérnie a rekordját, amint megjelenik.
Jelenítse meg a widgetet a Pet Store kezdőlapján.
Exercise
Kisállat játékok listája
Hozzon létre egy PetToysList widgetet, amely 5 játékot jelenít meg (a nevükkel és a képeikkel).
A kisállat játékok nem egy új modellben vannak tárolva, hanem a product.product-ben egy speciális kategória Pet Toys használatával. A generált játékokat megtekintheti és újakat adhat hozzá a következő útvonalon: . Valószínűleg meg kell vizsgálnia a product.product-t, hogy létrehozza a megfelelő domaint, amely csak a kisállat játékokat választja ki.
In Odoo, images are generally stored in regular fields encoded as
base64, HTML supports displaying images straight from base64 with
<img src="data:mime_type;base64,base64_image_data"/>
A PetToysList widgetet a kezdőlapon kell megjeleníteni a MessageOfTheDay widget jobb oldalán. Ehhez némi elrendezést kell készítenie CSS-sel.
Meglévő webkomponensek¶
Az Akciókezelő¶
Az Odoo-ban sok művelet egy akció-ból indul: menüpont megnyitása (egy nézethez), jelentés nyomtatása, …
Az akciók olyan adategységek, amelyek leírják, hogyan kell a kliensnek reagálnia egy tartalom aktiválására. Az akciók tárolhatók (és egy modellen keresztül olvashatók), vagy generálhatók menet közben (helyileg a kliens által javascript kóddal, vagy távolról egy modell metódusa által).
Az Odoo Web-ben az a komponens, amely felelős ezeknek az akcióknak a kezeléséért és reagálásáért, az Action Manager.
Az Action Manager használata¶
Az action manager explicit módon meghívható JavaScript kódból egy szótár létrehozásával, amely leírja a megfelelő típusú akciót, és az action manager példányának meghívásával.
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: {},
});
},
});
A leggyakoribb akció type az ir.actions.act_window, amely nézeteket biztosít egy modellhez (különböző módokon jeleníti meg a modellt), leggyakoribb attribútumai:
res_modelA modell, amelyet a nézetekben meg kell jeleníteni
res_id(opcionális)Űrlap nézetek esetén egy előre kiválasztott rekord a
res_model-benviewsFelsorolja az akción keresztül elérhető nézeteket. Egy
[view_id, view_type]lista, ahol aview_idlehet a megfelelő típusú nézet adatbázis-azonosítója, vagyfalse, hogy alapértelmezés szerint a megadott típushoz tartozó nézetet használja. A nézet típusok nem szerepelhetnek többször. Az akció alapértelmezés szerint a lista első nézetét nyitja meg.targetEither
current(the default) which replaces the „content” section of the web client by the action, ornewto open the action in a dialog box.contextTovábbi kontextusadatok az akción belüli használatra.
Exercise
Ugrás a termékre
Módosítsa a PetToysList komponenst úgy, hogy egy játékra kattintva a kezdőlapot a játék űrlap nézetével helyettesítse.
Ügyfél műveletek¶
Ebben az útmutatóban egy egyszerű HomePage widgetet használtunk, amelyet a webes kliens automatikusan elindít, amikor a megfelelő menüpontot választjuk. De honnan tudta az Odoo web, hogy el kell indítani ezt a widgetet? Mert a widget ügyfél műveletként van regisztrálva.
Az ügyfél művelet (ahogy a neve is sugallja) egy olyan művelettípus, amely szinte teljes egészében az ügyfélben van meghatározva, javascriptben az Odoo web számára. A szerver egyszerűen küld egy művelet címkét (egy tetszőleges nevet), és opcionálisan hozzáad néhány paramétert, de ezen túl mindent az egyedi ügyfélkód kezel.
A widgetünk így van regisztrálva az ügyfél művelet kezelőjeként:
instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');
A instance.web.client_actions egy Registry(), amelyben a műveletkezelő megkeresi az ügyfél művelet kezelőit, amikor végre kell hajtania egyet. A add() első paramétere az ügyfél művelet neve (címkéje), a második paraméter pedig az útvonal a widgethez az Odoo web kliens gyökerétől.
Amikor egy ügyfél műveletet végre kell hajtani, a műveletkezelő megkeresi a címkéjét a regisztrációban, végigjárja a megadott útvonalat, és megjeleníti a végén található widgetet.
Megjegyzés
egy ügyfél művelet kezelője lehet egy szokásos függvény is, ebben az esetben meghívásra kerül, és az eredménye (ha van) a következő végrehajtandó műveletként lesz értelmezve.
A szerver oldalon egyszerűen definiáltunk egy ir.actions.client műveletet:
<record id="action_home_page" model="ir.actions.client">
<field name="tag">petstore.homepage</field>
</record>
és egy menü, amely megnyitja a műveletet:
<menuitem id="home_page_petstore_menu" parent="petstore_menu"
name="Home Page" action="action_home_page"/>
A nézetek architektúrája¶
Az Odoo web hasznosságának (és összetettségének) nagy része a nézetekben rejlik. Minden nézettípus egy módja annak, hogy egy modellt megjelenítsünk a kliensben.
A Nézetkezelő¶
Amikor egy ActionManager példány ir.actions.act_window típusú akciót kap, a nézetek szinkronizálását és kezelését egy nézetkezelőre bízza, amely az eredeti akció követelményeitől függően egy vagy több nézetet állít be:
A Nézetek¶
A legtöbb Odoo nézet a odoo.web.View() alosztályán keresztül van megvalósítva, amely egy kis általános alapstruktúrát biztosít az események kezeléséhez és a modell információk megjelenítéséhez.
A keresési nézetet az Odoo fő keretrendszere nézettípusként kezeli, de a web kliens külön kezeli (mivel ez egy állandóbb elem, és más nézetekkel is képes interakcióba lépni, amit a normál nézetek nem tesznek).
Egy nézet felelős a saját leíró XML-jének betöltéséért (a fields_view_get használatával) és bármely más szükséges adatforrásért. E célból a nézetek egy opcionális nézetazonosítóval vannak ellátva, amely a view_id attribútumként van beállítva.
A nézetek egy DataSet() példánnyal is rendelkeznek, amely a legtöbb szükséges modellinformációt tartalmazza (a modell nevét és esetleg különböző rekordazonosítókat).
A nézetek keresési lekérdezéseket is kezelhetnek a do_search() felülírásával, és szükség szerint frissíthetik a DataSet()-jüket.
A űrlap nézet mezői¶
Gyakori igény a webes űrlap nézet kiterjesztése, hogy új módokat adjunk a mezők megjelenítésére.
Minden beépített mezőnek van egy alapértelmezett megjelenítési megvalósítása, egy új űrlap widget szükséges lehet egy új mezőtípus (pl. egy GIS mező) helyes kezeléséhez, vagy új megjelenítések és interakciós módok biztosításához a meglévő mezőtípusokkal (pl. Char mezők validálása, amelyeknek e-mail címeket kell tartalmazniuk, és e-mail linkekként való megjelenítésük).
Ahhoz, hogy kifejezetten meghatározzuk, melyik űrlap widgetet kell használni egy mező megjelenítéséhez, egyszerűen használjuk a widget attribútumot a nézet XML leírásában:
<field name="contact_mail" widget="email"/>
Megjegyzés
the same widget is used in both „view” (read-only) and „edit” modes of a form view, it’s not possible to use a widget in one and an other widget in the other
és egy adott mező (név) nem használható többször ugyanabban az űrlapban
egy widget figyelmen kívül hagyhatja az űrlap nézet aktuális módját, és ugyanaz maradhat mind a nézet, mind a szerkesztési módban
A mezőket az űrlap nézet példányosítja, miután elolvasta az XML leírását és megalkotta a leírást reprezentáló megfelelő HTML-t. Ezt követően az űrlap nézet néhány metódus segítségével kommunikál a mező objektumokkal. Ezeket a metódusokat a FieldInterface interfész határozza meg. Szinte minden mező örökli az AbstractField absztrakt osztályt. Ez az osztály meghatároz néhány alapértelmezett mechanizmust, amelyeket a legtöbb mezőnek implementálnia kell.
Íme néhány a mező osztály felelősségei közül:
A mező osztálynak meg kell jelenítenie és lehetővé kell tennie a felhasználó számára a mező értékének szerkesztését.
Helyesen kell megvalósítania az Odoo összes mezőjében elérhető 3 mezőattribútumot. Az
AbstractFieldosztály már megvalósít egy algoritmust, amely dinamikusan kiszámítja ezeknek az attribútumoknak az értékét (bármikor változhatnak, mert értékük más mezők értékétől függően változik). Értékeiket a Widget Tulajdonságok tárolják (a widget tulajdonságokat korábban ebben az útmutatóban magyaráztuk el). Minden mezőosztály felelőssége, hogy ellenőrizze ezeket a widget tulajdonságokat, és dinamikusan alkalmazkodjon azok értékétől függően. Itt található ezeknek az attribútumoknak a leírása:required: A mezőnek értékkel kell rendelkeznie a mentés előtt. Ha arequiredtrueés a mezőnek nincs értéke, a mezőis_valid()metódusánakfalse-t kell visszaadnia.invisible: Ha eztrue, a mezőnek láthatatlannak kell lennie. AzAbstractFieldosztály már rendelkezik ennek a viselkedésnek egy alapvető megvalósításával, amely a legtöbb mezőhöz illeszkedik.readonly: Hatrue, a mező nem szerkeszthető a felhasználó által. Az Odoo legtöbb mezője teljesen eltérően viselkedik areadonlyértékétől függően. Például aFieldCharegy HTML<input>-ot jelenít meg, amikor szerkeszthető, és egyszerűen megjeleníti a szöveget, amikor csak olvasható. Ez azt is jelenti, hogy sokkal több kódot kellene megvalósítania, ha csak egy viselkedést kellene megvalósítania, de ez szükséges a jó felhasználói élmény biztosításához.
Fields have two methods,
set_value()andget_value(), which are called by the form view to give it the value to display and get back the new value entered by the user. These methods must be able to handle the value as given by the Odoo server when aread()is performed on a model and give back a valid value for awrite(). Remember that the JavaScript/Python data types used to represent the values given byread()and given towrite()is not necessarily the same in Odoo. As example, when you read a many2one, it is always a tuple whose first value is the id of the pointed record and the second one is the name get (ie:(15, "Agrolait")). But when you write a many2one it must be a single integer, not a tuple anymore.AbstractFieldhas a default implementation of these methods that works well for simple data type and set a widget property namedvalue.
Kérjük, vegye figyelembe, hogy a mezők megvalósításának jobb megértése érdekében erősen ajánlott közvetlenül megnézni a FieldInterface interfész és az AbstractField osztály definícióját az Odoo web kliens kódjában.
Új Mezőtípus Létrehozása¶
Ebben a részben elmagyarázzuk, hogyan lehet új mezőtípust létrehozni. Az itt bemutatott példa a FieldChar osztály újraimplementálása lesz, és fokozatosan elmagyarázzuk minden részét.
Egyszerű Csak Olvasható Mező¶
Itt van egy első megvalósítás, amely csak szöveget fog megjeleníteni. A felhasználó nem tudja módosítani a mező tartalmát.
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');
In this example, we declare a class named FieldChar2 inheriting from
AbstractField. We also register this class in the registry
instance.web.form.widgets under the key char2. That will allow us to
use this new field in any form view by specifying widget="char2" in the
<field/> tag in the XML declaration of the view.
Ebben a példában egyetlen metódust definiálunk: render_value(). Csak annyit tesz, hogy megjeleníti a widget tulajdonság value-t. Ezek két eszköz, amelyet az AbstractField osztály definiál. Ahogy korábban elmagyaráztuk, az űrlap nézet hívja a mező set_value() metódusát, hogy beállítsa a megjelenítendő értéket. Ennek a metódusnak már van egy alapértelmezett megvalósítása az AbstractField-ben, amely egyszerűen beállítja a widget tulajdonság value-t. Az AbstractField figyeli a change:value eseményt is saját magán, és hívja a render_value()-t, amikor ez bekövetkezik. Tehát a render_value() egy kényelmi metódus, amelyet a gyermekosztályokban kell megvalósítani, hogy minden alkalommal végrehajtson valamilyen műveletet, amikor a mező értéke megváltozik.
Az init() metódusban meghatározzuk a mező alapértelmezett értékét is, ha a form nézet nem ad meg ilyet (itt feltételezzük, hogy egy char mező alapértelmezett értéke üres string kell legyen).
Olvasható-Írható Mező¶
Az olvasható mezők, amelyek csak tartalmat jelenítenek meg és nem engedik a felhasználónak a módosítást, hasznosak lehetnek, de az Odoo-ban a legtöbb mező szerkeszthető is. Ez bonyolultabbá teszi a mező osztályokat, főként azért, mert a mezőknek kezelniük kell mind a szerkeszthető, mind a nem szerkeszthető módot, ezek a módok gyakran teljesen különbözőek (tervezési és használhatósági célból), és a mezőknek bármikor képesnek kell lenniük váltani a módok között.
Annak érdekében, hogy tudjuk, melyik módban kell lennie az aktuális mezőnek, az AbstractField osztály beállít egy effective_readonly nevű widget tulajdonságot. A mezőnek figyelnie kell a widget tulajdonság változásait, és ennek megfelelően kell megjelenítenie a helyes módot. Példa:
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>
A start() metódusban (amelyet közvetlenül azután hívunk meg, hogy egy widgetet hozzáadtunk a DOM-hoz), az change:effective_readonly eseményre kötünk. Ez lehetővé teszi számunkra, hogy újra megjelenítsük a mezőt minden alkalommal, amikor a widget tulajdonság effective_readonly változik. Ez az eseménykezelő meghívja a display_field()-et, amelyet közvetlenül a start()-ban is hívunk. Ez a display_field() kifejezetten ehhez a mezőhöz lett létrehozva, nem egy metódus, amely az AbstractField-ben vagy bármely más osztályban van definiálva. Ezt a metódust használhatjuk a mező tartalmának megjelenítésére az aktuális mód függvényében.
Innentől kezdve ennek a mezőnek a koncepciója tipikus, kivéve, hogy sok ellenőrzés van annak érdekében, hogy megtudjuk az effective_readonly tulajdonság állapotát:
In the QWeb template used to display the content of the widget, it displays an
<input type="text" />if we are in read-write mode and nothing in particular in read-only mode.In the
display_field()method, we have to bind on thechangeevent of the<input type="text" />to know when the user has changed the value. When it happens, we call theinternal_set_value()method with the new value of the field. This is a convenience method provided by theAbstractFieldclass. That method will set a new value in thevalueproperty but will not trigger a call torender_value()(which is not necessary since the<input type="text" />already contains the correct value).A
render_value()-ban teljesen más kódot használunk a mező értékének megjelenítésére attól függően, hogy olvasható vagy olvasható-írható módban vagyunk.
Exercise
Szín Mező Létrehozása
Create a FieldColor class. The value of this field should be a string
containing a color code like those used in CSS (example: #FF0000 for
red). In read-only mode, this color field should display a little block
whose color corresponds to the value of the field. In read-write mode, you
should display an <input type="color" />. That type of <input />
is an HTML5 component that doesn’t work in all browsers but works well in
Google Chrome. So it’s OK to use as an exercise.
Ezt a widgetet használhatja a message_of_the_day modell form nézetében a color nevű mezőjéhez. Bónuszként megváltoztathatja a korábbi részben létrehozott MessageOfTheDay widgetet, hogy a napi üzenetet a color mezőben megadott háttérszínnel jelenítse meg.
A Form Nézet Egyedi Widgetjei¶
Űrlapmezők egyetlen mező szerkesztésére szolgálnak, és szorosan kapcsolódnak egy mezőhöz. Mivel ez korlátozó lehet, lehetőség van űrlap widgetek létrehozására is, amelyek nem annyira korlátozottak, és kevésbé kötődnek egy adott életciklushoz.
Egyedi űrlap widgetek hozzáadhatók egy űrlap nézethez a widget címke segítségével:
<widget type="xxx" />
Ez a típusú widget egyszerűen az űrlap nézet által jön létre az XML definíció alapján az HTML létrehozása során. Közös tulajdonságokkal rendelkeznek a mezőkkel (mint például az effective_readonly tulajdonság), de nem rendelődnek hozzá egy pontos mezőhöz. Így nincsenek olyan metódusaik, mint a get_value() és set_value(). Az FormWidget absztrakt osztályból kell származniuk.
Az űrlap widgetek képesek interakcióba lépni az űrlap mezőkkel azok változásainak figyelésével, valamint értékeik lekérésével vagy módosításával. Az űrlap mezőkhöz az field_manager attribútumon keresztül férhetnek hozzá:
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 általában maga a FormView(), de az onnan használt funkciókat korlátozni kell a FieldManagerMixin() által meghatározottakra, amelyek közül a leghasznosabbak:
get_field_value(field_name)()amely visszaadja egy mező értékét.set_values(values)()több mező értékét állítja be, egy{field_name: value_to_set}leképezést vesz felEgy
field_changed:field_nameesemény váltódik ki bármikor, amikor afield_namenevű mező értéke megváltozik
Exercise
Koordináták megjelenítése a Google Térképen
Adj hozzá két mezőt a product.product-hoz, amelyek egy szélességi és egy hosszúsági fokot tárolnak, majd hozz létre egy új űrlap widgetet, amely megjeleníti egy termék eredetének szélességi és hosszúsági fokát egy térképen
A térkép megjelenítéséhez használja a Google Map beágyazását:
<iframe width="400" height="300" src="https://maps.google.com/?ie=UTF8&ll=XXX,YYY&output=embed">
</iframe>
ahol az XXX helyére a szélességi fokot, az YYY helyére pedig a hosszúsági fokot kell beírni.
Két pozíció mezőt és egy térkép widgetet jelenítsen meg a termék űrlap nézetének új jegyzetfüzet oldalán.
Exercise
Az aktuális koordináta lekérése
Adjon hozzá egy gombot, amely visszaállítja a termék koordinátáit a felhasználó helyére; ezeket a koordinátákat a javascript geolocation API segítségével szerezheti meg.
Most szeretnénk megjeleníteni egy további gombot, amely automatikusan beállítja a koordinátákat az aktuális felhasználó helyére.
A felhasználó koordinátáinak megszerzéséhez egyszerű mód a geolocation JavaScript API használata. Lásd az online dokumentációt, hogy megtudja, hogyan kell használni.
Kérjük, vegye figyelembe, hogy a felhasználó nem kattinthat erre a gombra, amikor az űrlap nézet csak olvasható módban van. Tehát ennek az egyedi widgetnek helyesen kell kezelnie az effective_readonly tulajdonságot, akárcsak bármely mező. Egyik módja ennek az lenne, ha a gomb eltűnne, amikor az effective_readonly igaz.
- 1
mint külön fogalom az példányoktól. Sok nyelvben az osztályok teljes értékű objektumok és maguk is példányok (metaclassoké), de továbbra is két meglehetősen különálló hierarchia marad az osztályok és példányok között
- 2
valamint a böngészők közötti különbségek áthidalása, bár ez az idők során kevésbé vált szükségessé