Tùy chỉnh máy khách web¶
Nguy hiểm
Hướng dẫn này đã lỗi thời.
Hướng dẫn này nói về cách tạo mô-đun cho ứng dụng khách web của SoOn.
Để tạo trang web bằng SoOn, hãy xem Xây dựng một trang web; để bổ sung khả năng kinh doanh hoặc mở rộng hệ thống kinh doanh hiện có của SoOn, hãy xem Xây dựng một mô-đun.
Cảnh báo
Hướng dẫn này giả định kiến thức về:
Khái niệm cơ bản về Javascript và các phương pháp hay
It also requires an installed Odoo, and Git.
Một mô-đun đơn giản¶
Hãy bắt đầu với một mô-đun SoOn đơn giản chứa cấu hình thành phần web cơ bản và cho phép chúng tôi kiểm tra khung web.
Mô-đun ví dụ có sẵn trực tuyến và có thể được tải xuống bằng lệnh sau:
$ git clone http://github.com/odoo/petstore
Điều này sẽ tạo một thư mục petstore
bất cứ nơi nào bạn thực hiện lệnh. Sau đó, bạn cần thêm thư mục đó vào addons path
của SoOn, tạo cơ sở dữ liệu mới và cài đặt mô-đun oepetstore
.
Nếu duyệt thư mục petstore
, bạn sẽ thấy nội dung sau:
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
Mô-đun này đã chứa nhiều tùy chỉnh máy chủ khác nhau. Chúng ta sẽ quay lại vấn đề này sau, bây giờ hãy tập trung vào nội dung liên quan đến web, trong thư mục static
.
Các tệp được sử dụng ở phía "web" của mô-đun SoOn phải được đặt trong thư mục static
để chúng có sẵn cho trình duyệt web, các tệp bên ngoài thư mục đó không thể được trình duyệt tìm nạp. Các thư mục con src/css
, src/js
và src/xml
là thông thường và không thực sự cần thiết.
oepetstore/static/css/petstore.css
Hiện đang trống, sẽ giữ CSS cho nội dung cửa hàng thú cưng
oepetstore/static/xml/petstore.xml
Hầu hết trống, sẽ chứa các mẫu Mẫu QWeb
oepetstore/static/js/petstore.js
Phần quan trọng nhất (và thú vị) chứa logic của ứng dụng (hoặc ít nhất là phía trình duyệt web của nó) dưới dạng javascript. Hiện tại nó sẽ trông giống như:
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'); }
Chỉ in một thông báo nhỏ trong bảng điều khiển của trình duyệt.
Các tập tin trong thư mục static
cần được xác định trong mô-đun để chúng có thể được tải chính xác. Mọi thứ trong src/xml
được định nghĩa trong __manifest__.py
trong khi nội dung của src/css
và src/js
được định nghĩa trong petstore.xml
, hoặc một tập tin tương tự.
Cảnh báo
Tất cả các tệp JavaScript được nối và minified để cải thiện thời gian tải ứng dụng.
Một trong những nhược điểm là việc gỡ lỗi trở nên khó khăn hơn khi các tệp riêng lẻ biến mất và mã trở nên khó đọc hơn đáng kể. Có thể tắt quy trình này bằng cách bật "chế độ nhà phát triển": đăng nhập vào phiên bản SoOn của bạn (người dùng admin mật khẩu admin theo mặc định) mở menu người dùng (ở góc trên bên phải của màn hình SoOn) và chọn Giới thiệu về SoOn rồi Kích hoạt chế độ nhà phát triển:


Điều này sẽ tải lại ứng dụng khách web đã tắt tính năng tối ưu hóa, giúp việc phát triển và gỡ lỗi trở nên thoải mái hơn đáng kể.
Mô-đun JavaScript của SoOn¶
Javascript không có mô-đun tích hợp. Kết quả là các biến được xác định trong các tệp khác nhau đều được trộn lẫn với nhau và có thể xung đột. Điều này đã làm phát sinh nhiều mẫu mô-đun khác nhau được sử dụng để xây dựng các không gian tên rõ ràng và hạn chế rủi ro xung đột đặt tên.
Khung công tác SoOn sử dụng một mẫu như vậy để xác định các mô-đun trong các tiện ích bổ sung của web, nhằm vừa mã vùng không gian tên vừa sắp xếp thứ tự tải chính xác.
oepetstore/static/js/petstore.js
chứa một khai báo mô-đun:
odoo.oepetstore = function(instance, local) {
local.xxx = ...;
}
Trong web SoOn, các mô-đun được khai báo là các hàm được đặt trên biến odoo
toàn cục. Tên của hàm phải giống với tên của addon (trong trường hợp này là oepetstore
) để framework có thể tìm thấy nó và tự động khởi tạo nó.
Khi máy khách web tải mô-đun của bạn, nó sẽ gọi hàm gốc và cung cấp hai tham số:
tham số đầu tiên là phiên bản hiện tại của máy khách web SoOn, nó cho phép truy cập vào các khả năng khác nhau được xác định bởi SoOn (bản dịch, dịch vụ mạng) cũng như các đối tượng được xác định bởi lõi hoặc bởi các mô-đun khác.
tham số thứ hai là không gian tên cục bộ của riêng bạn được máy khách web tự động tạo. Các đối tượng và biến có thể truy cập được từ bên ngoài mô-đun của bạn (vì ứng dụng web SoOn cần gọi chúng hoặc vì những người khác có thể muốn tùy chỉnh chúng) nên được đặt bên trong không gian tên đó.
Các lớp học¶
Giống như các mô-đun và trái ngược với hầu hết các ngôn ngữ hướng đối tượng, javascript không được xây dựng trong classes1 mặc dù nó cung cấp các cơ chế gần tương đương (nếu ở cấp độ thấp hơn và dài dòng hơn).
Để đơn giản và thân thiện với nhà phát triển, web SoOn cung cấp một hệ thống lớp dựa trên Kế thừa JavaScript đơn giản của John Resig.
Các lớp mới được xác định bằng cách gọi phương thức extend()
của odoo.web.Class()
:
var MyClass = instance.web.Class.extend({
say_hello: function() {
console.log("hello");
},
});
Phương thức extend()
lấy một từ điển mô tả nội dung của lớp mới (các phương thức và thuộc tính tĩnh). Trong trường hợp này, nó sẽ chỉ có phương thức say_hello
không có tham số.
Các lớp được khởi tạo bằng toán tử new
:
var my_object = new MyClass();
my_object.say_hello();
// print "hello" in the console
Và các thuộc tính của instance có thể được truy cập thông qua 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
Các lớp có thể cung cấp một trình khởi tạo để thực hiện thiết lập ban đầu của cá thể, bằng cách định nghĩa một phương thức init()
. Trình khởi tạo nhận các tham số được truyền khi sử dụng toán tử 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
Cũng có thể tạo các lớp con từ các lớp hiện có (được xác định bởi người dùng) bằng cách gọi extend()
trên lớp cha, như được thực hiện với lớp con :class:`~odoo.web.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
Khi ghi đè một phương thức bằng tính kế thừa, bạn có thể sử dụng this._super()
để gọi phương thức gốc:
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
Cảnh báo
_super
không phải là một phương thức tiêu chuẩn, nó được đặt nhanh chóng sang phương thức tiếp theo trong chuỗi kế thừa hiện tại, nếu có. Nó chỉ được xác định trong phần đồng bộ của lệnh gọi phương thức, để sử dụng trong các trình xử lý không đồng bộ (sau khi gọi mạng hoặc trong lệnh gọi lại setTimeout
), tham chiếu đến giá trị của nó phải được giữ lại, không được truy cập thông qua `` cái này``:
// 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);
}
Thông tin cơ bản về Widget¶
Máy khách web SoOn gói jQuery để thao tác DOM dễ dàng. Nó hữu ích và cung cấp API tốt hơn W3C DOM2 tiêu chuẩn, nhưng không đủ để cấu trúc các ứng dụng phức tạp dẫn đến khó bảo trì.
Giống như bộ công cụ giao diện người dùng máy tính để bàn hướng đối tượng (ví dụ: Qt, Cocoa hoặc GTK), SoOn Web tạo ra các thành phần cụ thể chịu trách nhiệm cho các phần của trang. Trong web SoOn, cơ sở cho các thành phần như vậy là lớp Widget()
, một thành phần chuyên xử lý một phần trang và hiển thị thông tin cho người dùng.
Tiện ích đầu tiên của bạn¶
Mô-đun trình diễn ban đầu đã cung cấp một tiện ích cơ bản
local.HomePage = instance.Widget.extend({
start: function() {
console.log("pet store home page loaded");
},
});
Nó mở rộng Widget()
và ghi đè phương thức tiêu chuẩn start()
, phương thức này — giống như MyClass
trước đây — hiện tại không có tác dụng mấy.
Dòng này ở cuối tập tin:
instance.web.client_actions.add(
'petstore.homepage', 'instance.oepetstore.HomePage');
đăng ký tiện ích cơ bản của chúng tôi dưới dạng hành động của khách hàng. Hành động của khách hàng sẽ được giải thích sau, hiện tại đây chỉ là thứ cho phép tiện ích của chúng tôi được gọi và hiển thị khi chúng tôi chọn menu
.Cảnh báo
vì tiện ích sẽ được gọi từ bên ngoài mô-đun của chúng tôi nên máy khách web cần có tên "đủ điều kiện" chứ không phải phiên bản cục bộ.
Hiển thị nội dung¶
Widget có một số phương pháp và tính năng, nhưng những điều cơ bản rất đơn giản:
thiết lập một tiện ích
định dạng dữ liệu của widget
hiển thị tiện ích
Tiện ích HomePage
đã có phương thức start()
. Phương thức đó là một phần của vòng đời tiện ích thông thường và được gọi tự động sau khi tiện ích được chèn vào trang. Chúng ta có thể sử dụng nó để hiển thị một số nội dung.
Tất cả các tiện ích đều có $el
đại diện cho phần trang mà chúng phụ trách (dưới dạng đối tượng jQuery). Nội dung widget nên được chèn vào đó. Theo mặc định, $el
là một phần tử <div>
trống.
Phần tử <div>
thường không hiển thị với người dùng nếu nó không có nội dung (hoặc không có kiểu cụ thể nào cho kích thước của nó), đó là lý do tại sao không có gì được hiển thị trên trang khi HomePage
được khởi chạy.
Hãy thêm một số nội dung vào phần tử gốc của tiện ích bằng cách sử dụng jQuery:
local.HomePage = instance.Widget.extend({
start: function() {
this.$el.append("<div>Hello dear Odoo user!</div>");
},
});
Thông báo đó bây giờ sẽ xuất hiện khi bạn mở
Ghi chú
để làm mới mã javascript được tải trong SoOn Web, bạn sẽ cần tải lại trang. Không cần phải khởi động lại máy chủ SoOn.
Tiện ích HomePage
được SoOn Web sử dụng và quản lý tự động. Để tìm hiểu cách sử dụng một tiện ích "từ đầu", hãy tạo một tiện ích mới
local.GreetingsWidget = instance.Widget.extend({
start: function() {
this.$el.append("<div>We are so happy to see you again in this menu!</div>");
},
});
Bây giờ chúng ta có thể thêm GreetingsWidget
vào HomePage
bằng cách sử dụng phương thức appendTo()
của GreetingsWidget
:
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
trước tiên thêm nội dung của chính nó vào thư mục gốc DOMHomePage
sau đó khởi tạoGreetingsWidget
Cuối cùng, nó báo cho
GreetingsWidget
nơi cần chèn chính nó, ủy thác một phần$el
của nó choGreetingsWidget
.
Khi phương thức appendTo()
được gọi, nó sẽ yêu cầu tiện ích chèn chính nó vào vị trí đã chỉ định và hiển thị nội dung của nó. Phương thức start()
sẽ được gọi trong cuộc gọi tới appendTo()
.
Để xem điều gì xảy ra dưới giao diện hiển thị, chúng ta sẽ sử dụng DOM Explorer của trình duyệt. Nhưng trước tiên, hãy thay đổi các widget của chúng ta một chút để chúng ta có thể dễ dàng tìm thấy vị trí của chúng hơn, bằng cách thêm một lớp vào các phần tử gốc
:
local.HomePage = instance.Widget.extend({
className: 'oe_petstore_homepage',
...
});
local.GreetingsWidget = instance.Widget.extend({
className: 'oe_petstore_greetings',
...
});
Nếu bạn có thể tìm thấy phần có liên quan của DOM (nhấp chuột phải vào văn bản rồi Inspect Element), nó sẽ trông như thế này:
<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>
Điều này hiển thị rõ ràng hai phần tử <div>
được tạo tự động bởi Widget()
, bởi vì chúng tôi đã thêm một số lớp trên chúng.
Chúng ta cũng có thể thấy hai div chứa tin nhắn mà chúng ta đã tự thêm vào
Cuối cùng, hãy lưu ý phần tử <div class="oe_petstore_greetings">
đại diện cho phiên bản GreetingsWidget
là bên trong <div class="oe_petstore_homepage">
đại diện cho ``HomePage` ` chẳng hạn, vì chúng tôi đã thêm
Widget Cha mẹ và Trẻ em¶
Trong phần trước, chúng ta đã khởi tạo một widget bằng cú pháp sau:
new local.GreetingsWidget(this);
Đối số đầu tiên là this
, trong trường hợp đó là một phiên bản HomePage
. Điều này cho tiện ích đang được tạo biết tiện ích nào khác là mục mẹ của nó.
Như chúng ta đã thấy, các widget thường được chèn vào DOM bởi một widget khác và bên trong phần tử gốc của widget khác đó. Điều này có nghĩa là hầu hết các tiện ích đều là "một phần" của một tiện ích khác và tồn tại thay mặt cho tiện ích đó. Chúng tôi gọi vùng chứa là cha và tiện ích được chứa là con.
Vì nhiều lý do kỹ thuật và khái niệm, một widget cần phải biết ai là cha mẹ và ai là con của nó.
getParent()
có thể được sử dụng để lấy cha mẹ của một widget
local.GreetingsWidget = instance.Widget.extend({ start: function() { console.log(this.getParent().$el ); // will print "div.oe_petstore_homepage" in the console }, });
getChildren()
có thể được sử dụng để có được danh sách con của nó
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 }, });
Khi ghi đè phương thức init()
của một widget, điều quan trọng nhất là chuyển cấp độ gốc cho lệnh gọi this._super()
, nếu không thì mối quan hệ sẽ không được thiết lập lên chính xác:
local.GreetingsWidget = instance.Widget.extend({
init: function(parent, name) {
this._super(parent);
this.name = name;
},
});
Cuối cùng, nếu một tiện ích không có tiện ích gốc (ví dụ: vì đó là tiện ích gốc của ứng dụng), null
có thể được cung cấp làm tiện ích gốc:
new local.GreetingsWidget(null);
Phá hủy Widget¶
Nếu bạn có thể hiển thị nội dung cho người dùng của mình thì bạn cũng có thể xóa nội dung đó. Việc này được thực hiện thông qua phương thức destroy()
:
greeting.destroy();
Khi một widget bị hủy, trước tiên nó sẽ gọi destroy()
trên tất cả các widget con của nó. Sau đó nó tự xóa khỏi DOM. Nếu bạn đã thiết lập các cấu trúc cố định trong init()
hoặc start()
mà phải được dọn sạch một cách rõ ràng (vì trình thu gom rác sẽ không xử lý chúng), bạn có thể ghi đè destroy()
.
Nguy hiểm
khi ghi đè destroy()
, _super()
phải luôn được gọi nếu không thì tiện ích con và các tiện ích con của nó không được dọn sạch chính xác, có thể gây rò rỉ bộ nhớ và "sự kiện ảo", ngay cả khi không có lỗi được hiển thị
Công cụ mẫu QWeb¶
Trong phần trước, chúng tôi đã thêm nội dung vào các tiện ích của mình bằng cách thao tác trực tiếp (và thêm vào) DOM của chúng
this.$el.append("<div>Hello dear Odoo user!</div>");
Điều này cho phép tạo và hiển thị bất kỳ loại nội dung nào, nhưng sẽ khó sử dụng khi tạo số lượng DOM đáng kể (nhiều trùng lặp, vấn đề trích dẫn, ...)
Giống như nhiều môi trường khác, giải pháp của SoOn là sử dụng công cụ tạo mẫu. Công cụ tạo mẫu của SoOn được gọi là Mẫu QWeb.
QWeb là một ngôn ngữ tạo khuôn mẫu dựa trên XML, tương tự như Genshi, `Thymeleaf <http://en.wikipedia.org/wiki/Thymeleaf >`_ hoặc Facelets. Nó có các đặc điểm sau:
Nó được triển khai hoàn toàn bằng JavaScript và được hiển thị trong trình duyệt
Mỗi tệp mẫu (tệp XML) chứa nhiều mẫu
Nó được hỗ trợ đặc biệt trong
Widget()
của SoOn Web, mặc dù nó có thể được sử dụng bên ngoài ứng dụng khách web của SoOn (và có thể sử dụngWidget()
mà không cần dựa vào QWeb)
Ghi chú
Lý do đằng sau việc sử dụng QWeb thay vì các công cụ tạo mẫu javascript hiện có là khả năng mở rộng của các mẫu có sẵn (của bên thứ ba), giống như SoOn views.
Hầu hết các công cụ tạo mẫu javascript đều dựa trên văn bản, điều này ngăn cản khả năng mở rộng cấu trúc dễ dàng trong đó công cụ tạo khuôn mẫu dựa trên XML có thể được thay đổi một cách tổng quát bằng cách sử dụng ví dụ: XPath hoặc CSS và DSL thay đổi cây (hoặc thậm chí chỉ XSLT). Tính linh hoạt và khả năng mở rộng này là đặc điểm cốt lõi của SoOn và việc mất đi tính năng này được coi là không thể chấp nhận được.
Sử dụng QWeb¶
Trước tiên, hãy xác định một mẫu QWeb đơn giản trong tệp oepetstore/static/src/xml/petstore.xml
gần như trống rỗng:
<?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>
Bây giờ chúng ta có thể sử dụng mẫu này bên trong tiện ích HomePage
. Sử dụng biến trình tải QWeb
được xác định ở đầu trang, chúng ta có thể gọi tới mẫu được xác định trong tệp XML:
local.HomePage = instance.Widget.extend({
start: function() {
this.$el.append(QWeb.render("HomePageTemplate"));
},
});
QWeb.render()
tìm kiếm mẫu đã chỉ định, hiển thị nó thành một chuỗi và trả về kết quả.
Tuy nhiên, vì Widget()
có tích hợp đặc biệt cho QWeb nên mẫu có thể được đặt trực tiếp trên tiện ích thông qua thuộc tính template
của nó:
local.HomePage = instance.Widget.extend({
template: "HomePageTemplate",
start: function() {
...
},
});
Mặc dù kết quả trông giống nhau nhưng có hai điểm khác biệt giữa các cách sử dụng này:
với phiên bản thứ hai, mẫu được hiển thị ngay trước khi
start()
được gọitrong phiên bản đầu tiên, nội dung của mẫu được thêm vào phần tử gốc của tiện ích, trong khi ở phiên bản thứ hai, phần tử gốc của mẫu được đặt trực tiếp làm phần tử gốc của tiện ích. Đó là lý do tại sao widget phụ "chào mừng" cũng có nền màu đỏ
Cảnh báo
các mẫu phải có một phần tử gốc không phải t
, đặc biệt nếu chúng được đặt làm template
của tiện ích. Nếu có nhiều "phần tử gốc", kết quả sẽ không được xác định (thường chỉ phần tử gốc đầu tiên sẽ được sử dụng và các phần tử gốc khác sẽ bị bỏ qua)
Bối cảnh QWeb¶
Các mẫu QWeb có thể được cung cấp dữ liệu và có thể chứa logic hiển thị cơ bản.
Đối với các lệnh gọi rõ ràng tới QWeb.render()
, dữ liệu mẫu được truyền dưới dạng tham số thứ hai:
QWeb.render("HomePageTemplate", {name: "Klaus"});
với mẫu được sửa đổi thành:
<t t-name="HomePageTemplate">
<div>Hello <t t-esc="name"/></div>
</t>
sẽ cho kết quả:
<div>Hello Klaus</div>
Khi sử dụng tích hợp của Widget()
, không thể cung cấp dữ liệu bổ sung cho mẫu. Mẫu sẽ được cung cấp một biến ngữ cảnh widget
duy nhất, tham chiếu đến tiện ích được hiển thị ngay trước khi start()
được gọi (trạng thái của tiện ích về cơ bản sẽ được thiết lập bởi :func:` ~odoo.Widget.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() {
},
});
Kết quả:
<div>Hello Mordecai</div>
Tuyên bố mẫu¶
Chúng ta đã thấy cách kết xuất các mẫu QWeb, bây giờ chúng ta hãy xem cú pháp của các mẫu đó.
Mẫu QWeb bao gồm XML thông thường được trộn lẫn với các chỉ thị của QWeb. Lệnh QWeb được khai báo bằng các thuộc tính XML bắt đầu bằng t-
.
Lệnh cơ bản nhất là t-name
, dùng để khai báo các mẫu mới trong tệp mẫu:
<templates>
<t t-name="HomePageTemplate">
<div>This is some simple HTML</div>
</t>
</templates>
t-name
lấy tên của mẫu đang được xác định và khai báo rằng nó có thể được gọi bằng cách sử dụng QWeb.render()
. Nó chỉ có thể được sử dụng ở cấp cao nhất của tệp mẫu.
Trốn thoát¶
Lệnh t-esc
có thể được sử dụng để xuất văn bản:
<div>Hello <t t-esc="name"/></div>
Nó lấy một biểu thức Javascript để đánh giá, kết quả của biểu thức sau đó được thoát HTML và chèn vào tài liệu. Vì đây là một biểu thức nên có thể chỉ cung cấp tên biến như trên hoặc một biểu thức phức tạp hơn như phép tính:
<div><t t-esc="3+5"/></div>
hoặc gọi phương thức:
<div><t t-esc="name.toUpperCase()"/></div>
Xuất HTML¶
Để đưa HTML vào trang đang được hiển thị, hãy sử dụng t-raw
. Giống như t-esc
, nó lấy một biểu thức Javascript tùy ý làm tham số, nhưng nó không thực hiện bước thoát HTML.
<div><t t-raw="name.link(user_account)"/></div>
Nguy hiểm
t-raw
không được được sử dụng trên bất kỳ dữ liệu nào có thể chứa nội dung do người dùng cung cấp không thoát vì điều này dẫn đến lỗ hổng `` cross-site scripting`_
Câu điều kiện¶
QWeb có thể có các khối điều kiện sử dụng t-if
. Lệnh này lấy một biểu thức tùy ý, nếu biểu thức sai (false
, null
, 0
hoặc một chuỗi trống) thì toàn bộ khối sẽ bị chặn, nếu không thì nó sẽ được hiển thị.
<div>
<t t-if="true == true">
true is true
</t>
<t t-if="true == false">
true is not true
</t>
</div>
Ghi chú
QWeb không có cấu trúc "else", hãy sử dụng t-if
thứ hai với điều kiện ban đầu được đảo ngược. Bạn có thể muốn lưu trữ điều kiện trong một biến cục bộ nếu đó là một biểu thức phức tạp hoặc đắt tiền.
Lặp lại¶
Để lặp lại một danh sách, hãy sử dụng t-foreach
và t-as
. t-foreach
lấy một biểu thức trả về một danh sách để lặp lại trên t-as
lấy một tên biến để liên kết với từng mục trong quá trình lặp.
<div>
<t t-foreach="names" t-as="name">
<div>
Hello <t t-esc="name"/>
</div>
</t>
</div>
Ghi chú
t-foreach
cũng có thể được sử dụng với số và đối tượng (từ điển)
Xác định thuộc tính¶
QWeb cung cấp hai chỉ thị liên quan để xác định các thuộc tính được tính toán: t-att-name
và t-attf-name
. Trong cả hai trường hợp, name là tên của thuộc tính cần tạo (ví dụ: t-att-id
xác định thuộc tính id
sau khi hiển thị).
t-att-
nhận một biểu thức javascript có kết quả được đặt làm giá trị của thuộc tính, sẽ hữu ích nhất nếu tất cả giá trị của thuộc tính được tính toán:
<div>
Input your name:
<input type="text" t-att-value="defaultName"/>
</div>
t-attf-
nhận một chuỗi định dạng. Chuỗi định dạng là văn bản bằng chữ có các khối nội suy bên trong, khối nội suy là một biểu thức javascript giữa {{
và }}
, sẽ được thay thế bằng kết quả của biểu thức. Nó hữu ích nhất cho các thuộc tính được hiểu một phần theo nghĩa đen và được tính toán một phần, chẳng hạn như một lớp:
<div t-attf-class="container {{ left ? 'text-left' : '' }} {{ extra_class }}">
insert content here
</div>
Gọi các mẫu khác¶
Các mẫu có thể được chia thành các mẫu phụ (để đơn giản, dễ bảo trì, có thể sử dụng lại hoặc để tránh lồng đánh dấu quá mức).
Việc này được thực hiện bằng cách sử dụng lệnh t-call
, lệnh này lấy tên của mẫu để hiển thị:
<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>
hiển thị mẫu A
sẽ dẫn đến:
<div class="i-am-a">
<div class="i-am-b"/>
</div>
Các mẫu phụ kế thừa bối cảnh hiển thị của người gọi chúng.
Để tìm hiểu thêm về QWeb¶
Để tham khảo QWeb, hãy xem Mẫu QWeb.
Bài tập¶
Exercise
Cách sử dụng QWeb trong Widget
Tạo một tiện ích có hàm tạo có hai tham số ngoài parent
: product_names
và color
.
product_names
phải là một chuỗi các chuỗi, mỗi chuỗi là tên của một sản phẩmcolor
là một chuỗi chứa một màu ở định dạng màu CSS (ví dụ:#000000
cho màu đen).
Tiện ích sẽ hiển thị các tên sản phẩm nhất định lần lượt, mỗi tên trong một hộp riêng biệt có màu nền với giá trị color
và đường viền. Bạn nên sử dụng QWeb để hiển thị HTML. Mọi CSS cần thiết đều phải có trong oepetstore/static/src/css/petstore.css
.
Sử dụng tiện ích trong Trang chủ
với nửa tá sản phẩm.
Người trợ giúp tiện ích¶
Bộ chọn jQuery của Widget
¶
Việc chọn các phần tử DOM trong một widget có thể được thực hiện bằng cách gọi phương thức find()
trên DOM root của widget:
this.$el.find("input.my_input")...
Nhưng vì đây là một hoạt động phổ biến, Widget()
cung cấp một lối tắt tương đương thông qua phương thức $()
local.MyWidget = instance.Widget.extend({
start: function() {
this.$("input.my_input")...
},
});
Cảnh báo
Hàm jQuery toàn cục $()
nên không bao giờ được sử dụng trừ khi nó thực sự cần thiết: lựa chọn trên thư mục gốc của một widget được đặt trong phạm vi của widget và cục bộ với nó, nhưng các lựa chọn với $()
là toàn cục với trang/ứng dụng và có thể khớp với các phần của tiện ích và chế độ xem khác, dẫn đến các tác dụng phụ kỳ lạ hoặc nguy hiểm. Vì một tiện ích thường chỉ hoạt động trên phần DOM mà nó sở hữu nên không có lý do gì để lựa chọn toàn cục.
Liên kết sự kiện DOM dễ dàng hơn¶
Trước đây chúng tôi đã ràng buộc các sự kiện DOM bằng cách sử dụng các trình xử lý sự kiện jQuery thông thường (ví dụ: .click()
hoặc .change()
) trên các phần tử widget:
local.MyWidget = instance.Widget.extend({
start: function() {
var self = this;
this.$(".my_button").click(function() {
self.button_clicked();
});
},
button_clicked: function() {
..
},
});
Trong khi điều này hoạt động, nó có một số vấn đề:
nó khá dài dòng
nó không hỗ trợ thay thế phần tử gốc của widget khi chạy vì liên kết chỉ được thực hiện khi
start()
được chạy (trong quá trình khởi tạo widget)nó đòi hỏi phải giải quyết các vấn đề ràng buộc về
điều này
Do đó, các widget cung cấp một lối tắt để liên kết sự kiện DOM thông qua events
:
local.MyWidget = instance.Widget.extend({
events: {
"click .my_button": "button_clicked",
},
button_clicked: function() {
..
}
});
events
là một đối tượng (ánh xạ) của một sự kiện tới hàm hoặc phương thức để gọi khi sự kiện được kích hoạt:
khóa là tên sự kiện, có thể được tinh chỉnh bằng bộ chọn CSS trong trường hợp này chỉ khi sự kiện xảy ra trên phần tử phụ được chọn thì hàm hoặc phương thức sẽ chạy:
click
sẽ xử lý tất cả các nhấp chuột trong tiện ích, nhưng `` click .my_button`` sẽ chỉ xử lý các nhấp chuột trong các phần tử mang lớpmy_button
giá trị là hành động thực hiện khi sự kiện được kích hoạt
Nó có thể là một chức năng:
events: { 'click': function (e) { /* code here */ } }
hoặc tên của một phương thức trên đối tượng (xem ví dụ ở trên).
Trong cả hai trường hợp,
this
là phiên bản widget và trình xử lý được cung cấp một tham số duy nhất, đối tượng sự kiện jQuery cho sự kiện.
Sự kiện và thuộc tính của widget¶
Sự kiện¶
Các tiện ích cung cấp một hệ thống sự kiện (tách biệt với hệ thống sự kiện DOM/jQuery được mô tả ở trên): một tiện ích có thể tự kích hoạt các sự kiện và các tiện ích khác (hoặc chính nó) có thể tự liên kết và lắng nghe các sự kiện này:
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>");
},
});
Tiện ích này hoạt động như một mặt tiền, chuyển đổi đầu vào của người dùng (thông qua các sự kiện DOM) thành một sự kiện nội bộ có thể ghi lại mà các tiện ích gốc có thể tự liên kết.
trigger()
lấy tên của sự kiện để kích hoạt làm đối số đầu tiên (bắt buộc), mọi đối số tiếp theo sẽ được coi là dữ liệu sự kiện và được truyền trực tiếp đến người nghe.
Sau đó, chúng ta có thể thiết lập một sự kiện gốc khởi tạo tiện ích chung của mình và lắng nghe sự kiện user_chose
bằng cách sử dụng 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()
liên kết một hàm để được gọi khi sự kiện được xác định bởi event_name
diễn ra. Đối số func
là hàm cần gọi và object
là đối tượng mà hàm đó có liên quan nếu nó là một phương thức. Hàm liên kết sẽ được gọi với các đối số bổ sung là trigger()
nếu có. Ví dụ:
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"
}
Ghi chú
Việc kích hoạt các sự kiện trên một tiện ích khác thường là một ý tưởng tồi. Ngoại lệ chính cho quy tắc đó là odoo.web.bus
tồn tại đặc biệt để phát sóng các sự kiện trong đó bất kỳ tiện ích nào cũng có thể được quan tâm trong toàn bộ ứng dụng web SoOn.
Của cải¶
Các thuộc tính rất giống với các thuộc tính đối tượng thông thường ở chỗ chúng cho phép lưu trữ dữ liệu trên một phiên bản widget, tuy nhiên chúng có tính năng bổ sung là kích hoạt các sự kiện khi được đặt:
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()
đặt giá trị của một thuộc tính và kích hoạtchange:propname
(trong đó propname là tên thuộc tính được truyền làm tham số đầu tiên choset()
) vàchange
get()
truy xuất giá trị của một thuộc tính.
Bài tập¶
Exercise
Thuộc tính và sự kiện của widget
Tạo một widget ColorInputWidget
sẽ hiển thị 3 <input type="text">
. Mỗi <input>
này được dành riêng để nhập số thập lục phân từ 00 đến FF. Khi bất kỳ <input>
nào được người dùng sửa đổi, tiện ích phải truy vấn nội dung của ba <input>
, nối các giá trị của chúng để có mã màu CSS hoàn chỉnh (ví dụ: #00FF00 ``) và đặt kết quả vào thuộc tính có tên ``color
. Vui lòng lưu ý sự kiện change()
của jQuery mà bạn có thể liên kết trên bất kỳ phần tử <input>
HTML nào và phương thức val()
có thể truy vấn giá trị hiện tại của ``<input> đó `` có thể hữu ích cho bạn trong bài tập này.
Sau đó, sửa đổi tiện ích HomePage
để khởi tạo ColorInputWidget
và hiển thị nó. Tiện ích HomePage
cũng sẽ hiển thị một hình chữ nhật trống. Hình chữ nhật đó bất kỳ lúc nào cũng phải có cùng màu nền với màu trong thuộc tính color
của phiên bản ColorInputWidget
.
Sử dụng QWeb để tạo tất cả HTML.
Sửa đổi các vật dụng và lớp hiện có¶
Hệ thống lớp của khung web SoOn cho phép sửa đổi trực tiếp các lớp hiện có bằng cách sử dụng phương thức include()
:
var TestClass = instance.web.Class.extend({
testMethod: function() {
return "hello";
},
});
TestClass.include({
testMethod: function() {
return this._super() + " world";
},
});
console.log(new TestClass().testMethod());
// will print "hello world"
Hệ thống này tương tự như cơ chế kế thừa, ngoại trừ việc nó sẽ thay đổi lớp mục tiêu tại chỗ thay vì tạo một lớp mới.
Trong trường hợp đó, this._super()
sẽ gọi triển khai ban đầu của một phương thức đang được thay thế/xác định lại. Nếu lớp đã có các lớp con, tất cả lệnh gọi tới this._super()
trong các lớp con sẽ gọi các triển khai mới được xác định trong lệnh gọi tới include()
. Điều này cũng sẽ hoạt động nếu một số phiên bản của lớp (hoặc của bất kỳ lớp con nào của nó) được tạo trước lệnh gọi tới include()
.
Bản dịch¶
Quá trình dịch văn bản bằng mã Python và JavaScript rất giống nhau. Bạn có thể nhận thấy những dòng này ở đầu tệp petstore.js
:
var _t = instance.web._t,
_lt = instance.web._lt;
Những dòng này chỉ được sử dụng để nhập các hàm dịch trong mô-đun JavaScript hiện tại. Chúng được sử dụng như vậy:
this.$el.text(_t("Hello user!"));
Trong SoOn, các tệp dịch được tạo tự động bằng cách quét mã nguồn. Tất cả đoạn mã gọi một chức năng nhất định đều được phát hiện và nội dung của chúng được thêm vào tệp dịch, sau đó sẽ được gửi cho người dịch. Trong Python, hàm là _()
. Trong JavaScript, hàm này là _t()
(và cả _lt()
).
_t()
sẽ trả về bản dịch được xác định cho văn bản được cung cấp. Nếu không có bản dịch nào được xác định cho văn bản đó, nó sẽ trả về nguyên văn bản gốc.
Ghi chú
Để đưa các giá trị do người dùng cung cấp vào các chuỗi có thể dịch được, bạn nên sử dụng _.str.sprintf với các đối số được đặt tên sau dịch:
this.$el.text(_.str.sprintf(
_t("Hello, %(user)s!"), {
user: "Ed"
}));
Điều này làm cho các chuỗi có thể dịch dễ đọc hơn đối với người dịch và giúp họ linh hoạt hơn trong việc sắp xếp lại hoặc bỏ qua các tham số.
_lt()
("lazy dịch") cũng tương tự nhưng phức tạp hơn một chút: thay vì dịch tham số của nó ngay lập tức, nó trả về một đối tượng mà khi chuyển đổi thành chuỗi sẽ thực hiện dịch.
Nó được sử dụng để xác định các thuật ngữ có thể dịch được trước khi hệ thống dịch được khởi tạo, chẳng hạn như đối với các thuộc tính lớp (vì các mô-đun được tải trước khi ngôn ngữ của người dùng được định cấu hình và các bản dịch được tải xuống).
Giao tiếp với máy chủ SoOn¶
Liên hệ với người mẫu¶
Hầu hết các hoạt động với SoOn đều liên quan đến việc giao tiếp với mô hình thực hiện mối quan tâm kinh doanh, sau đó các mô hình này sẽ (có khả năng) tương tác với một số công cụ lưu trữ (thường là PostgreSQL).
Mặc dù jQuery cung cấp hàm $.ajax cho các tương tác mạng, nhưng việc giao tiếp với SoOn yêu cầu siêu dữ liệu bổ sung mà việc thiết lập trước mỗi lệnh gọi sẽ dài dòng và dễ xảy ra lỗi. Kết quả là, web SoOn cung cấp các nguyên tắc giao tiếp cấp cao hơn.
Để chứng minh điều này, tệp petstore.py
đã chứa một mô hình nhỏ với một phương thức mẫu:
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),
Hàm này khai báo một mô hình có hai trường và một phương thức my_method()
trả về một từ điển theo nghĩa đen.
Đây là một tiện ích mẫu gọi my_method()
và hiển thị kết quả:
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
});
},
});
Lớp được sử dụng để gọi các mô hình SoOn là odoo.Model()
. Nó được khởi tạo với tên của mô hình SoOn làm tham số đầu tiên (oepetstore.message_of_the_day
ở đây).
call()
có thể được sử dụng để gọi bất kỳ phương thức (công khai) nào của mô hình SoOn. Nó có các đối số vị trí sau:
tên
Tên của phương thức cần gọi,
my_method
ở đâyargs
một mảng đối số vị trí để cung cấp cho phương thức. Vì ví dụ không có đối số vị trí để cung cấp nên tham số
args
không được cung cấp.Đây là một ví dụ khác với các đối số vị trí:
@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
ánh xạ của đối số từ khóa để chuyển. Ví dụ này cung cấp một đối số được đặt tên duy nhất
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()
trả về một giá trị hoãn lại được giải quyết với giá trị được phương thức của mô hình trả về làm đối số đầu tiên.
Bối cảnh phức hợp¶
Phần trước sử dụng đối số context
chưa được giải thích trong lệnh gọi phương thức:
model.call("my_method", {context: new instance.web.CompoundContext()})
Bối cảnh giống như một đối số "ma thuật" mà máy khách web sẽ luôn đưa ra cho máy chủ khi gọi một phương thức. Ngữ cảnh là một từ điển chứa nhiều khóa. Một trong những chìa khóa quan trọng nhất là ngôn ngữ của người dùng, được máy chủ sử dụng để dịch tất cả các thông báo của ứng dụng. Một cái khác là múi giờ của người dùng, được sử dụng để tính toán ngày và giờ chính xác nếu người dùng ở các quốc gia khác nhau sử dụng SoOn.
đối số
là cần thiết trong tất cả các phương thức, nếu không những điều tồi tệ có thể xảy ra (chẳng hạn như ứng dụng không được dịch chính xác). Đó là lý do tại sao khi gọi một phương thức của mô hình, bạn phải luôn cung cấp đối số đó. Giải pháp để đạt được điều đó là sử dụng odoo.web.CompoundContext()
.
CompoundContext()
là một lớp được sử dụng để truyền ngữ cảnh của người dùng (với ngôn ngữ, múi giờ, v.v.) đến máy chủ cũng như thêm các khóa mới vào ngữ cảnh (một số mô hình sử dụng phương thức này các khóa tùy ý được thêm vào ngữ cảnh). Nó được tạo bằng cách cung cấp cho hàm tạo của nó bất kỳ số lượng từ điển hoặc các phiên bản CompoundContext()
khác. Nó sẽ hợp nhất tất cả các bối cảnh đó trước khi gửi chúng đến máy chủ.
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}
Bạn có thể thấy từ điển trong đối số context
chứa một số khóa liên quan đến cấu hình của người dùng hiện tại trong SoOn cộng với khóa new_key
đã được thêm khi khởi tạo Hợp chấtContext()
.
Truy vấn¶
Trong khi call()
là đủ cho mọi tương tác với các mô hình SoOn, SoOn Web cung cấp một trình trợ giúp để truy vấn các mô hình đơn giản và rõ ràng hơn (tìm nạp bản ghi dựa trên các điều kiện khác nhau): query()
hoạt động như một lối tắt cho sự kết hợp chung của search()
và :read()
. Nó cung cấp một cú pháp rõ ràng hơn để tìm kiếm và đọc các mô hình:
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
});
đấu với:
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()
lấy danh sách các trường tùy chọn làm tham số (nếu không có trường nào được cung cấp, tất cả các trường của mô hình sẽ được tìm nạp). Nó trả về mộtodoo.web.Query()
có thể được tùy chỉnh thêm trước khi được thực thiQuery()
đại diện cho truy vấn đang được xây dựng. Không thể thay đổi, các phương pháp tùy chỉnh truy vấn thực sự trả về một bản sao đã sửa đổi, do đó có thể sử dụng song song phiên bản gốc và phiên bản mới. XemQuery()
để biết các tùy chọn tùy chỉnh của nó.
Khi truy vấn được thiết lập như mong muốn, chỉ cần gọi all()
để thực thi nó và trả về kết quả bị trì hoãn. Kết quả giống như read()
's, một mảng từ điển trong đó mỗi từ điển là một bản ghi được yêu cầu, với mỗi trường được yêu cầu là một khóa từ điển.
Bài tập¶
Exercise
Tin nhắn của ngày
Tạo một widget MessageOfTheDay
hiển thị bản ghi cuối cùng của mô hình oepetstore.message_of_the_day
. Tiện ích sẽ tìm nạp bản ghi của nó ngay khi nó được hiển thị.
Hiển thị widget trên trang chủ Pet Store.
Exercise
Danh sách đồ chơi thú cưng
Tạo một widget PetToysList
hiển thị 5 món đồ chơi (sử dụng tên và hình ảnh của chúng).
Đồ chơi thú cưng không được lưu trữ trong mẫu mới, thay vào đó chúng được lưu trữ trong product.product
bằng cách sử dụng danh mục đặc biệt Đồ chơi thú cưng. Bạn có thể xem các đồ chơi được tạo trước và thêm đồ chơi mới bằng cách truy cập . Có thể bạn sẽ cần khám phá product.product
để tạo miền phù hợp để chỉ chọn đồ chơi cho thú cưng.
Trong SoOn, hình ảnh thường được lưu trữ trong các trường thông thường được mã hóa dưới dạng base64, HTML hỗ trợ hiển thị hình ảnh trực tiếp từ base64 với <img src="data:mime_type;base64,base64_image_data"/>
Tiện ích PetToysList
sẽ được hiển thị trên trang chủ ở bên phải tiện ích MessageOfTheDay
. Bạn sẽ cần tạo một số bố cục bằng CSS để đạt được điều này.
Các thành phần web hiện có¶
Trình quản lý hành động¶
Trong SoOn, nhiều thao tác bắt đầu từ action: mở một mục menu (để xem), in báo cáo, ...
Hành động là những phần dữ liệu mô tả cách khách hàng sẽ phản ứng với việc kích hoạt một phần nội dung. Các hành động có thể được lưu trữ (và đọc qua mô hình) hoặc chúng có thể được tạo nhanh chóng (cục bộ cho máy khách bằng mã javascript hoặc từ xa bằng phương thức của mô hình).
Trong Web SoOn, thành phần chịu trách nhiệm xử lý và phản hồi những hành động này là Trình quản lý hành động.
Sử dụng Trình quản lý hành động¶
Trình quản lý hành động có thể được gọi một cách rõ ràng từ mã javascript bằng cách tạo một từ điển mô tả an action đúng loại và gọi một phiên bản trình quản lý hành động với nó.
do_action()
là lối tắt của Widget()
tra cứu trình quản lý hành động "hiện tại" và thực hiện hành động:
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: {},
});
},
});
Hành động phổ biến nhất type
là ir.actions.act_window
cung cấp chế độ xem cho một mô hình (hiển thị mô hình theo nhiều cách khác nhau), các thuộc tính phổ biến nhất của nó là:
res_model
Mô hình hiển thị trong dạng xem
res_id
(tùy chọn)Đối với chế độ xem biểu mẫu, một bản ghi được chọn trước trong
res_model
lượt xem
Liệt kê các chế độ xem có sẵn thông qua hành động. Danh sách
[view_id, view_type]
,view_id
có thể là mã định danh cơ sở dữ liệu của chế độ xem thuộc loại phù hợp hoặcfalse
để sử dụng chế độ xem theo mặc định cho loại đã chỉ định. Các loại chế độ xem không thể hiện diện nhiều lần. Hành động này sẽ mở chế độ xem đầu tiên của danh sách theo mặc định.mục tiêu
Hoặc
current
(mặc định) thay thế phần "nội dung" của máy khách web bằng hành động hoặcmới
để mở hành động trong hộp thoại.- `` bối cảnh``
Dữ liệu ngữ cảnh bổ sung để sử dụng trong hành động.
Exercise
Chuyển đến sản phẩm
Sửa đổi thành phần PetToysList
để việc nhấp vào đồ chơi sẽ thay thế trang chủ bằng chế độ xem biểu mẫu của đồ chơi.
Hành động của khách hàng¶
Trong suốt hướng dẫn này, chúng tôi đã sử dụng một tiện ích Trang chủ
đơn giản mà ứng dụng khách web tự động khởi động khi chúng tôi chọn mục menu bên phải. Nhưng làm thế nào trang web SoOn biết cách khởi động tiện ích này? Vì tiện ích này được đăng ký dưới dạng hành động của khách hàng.
Hành động của khách hàng (như tên gọi của nó ngụ ý) là một loại hành động được xác định gần như hoàn toàn trong khách hàng, trong javascript dành cho web SoOn. Máy chủ chỉ cần gửi một thẻ hành động (một tên tùy ý) và tùy ý thêm một vài tham số, nhưng ngoài ra mọi thứ đều được xử lý bởi mã máy khách tùy chỉnh.
Tiện ích của chúng tôi được đăng ký làm trình xử lý cho hành động của khách hàng thông qua điều này
instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');
instance.web.client_actions
là một Registry()
trong đó trình quản lý hành động tra cứu các trình xử lý hành động của máy khách khi nó cần thực thi một trình xử lý đó. Tham số đầu tiên của add()
là tên (thẻ) của hành động máy khách và tham số thứ hai là đường dẫn đến tiện ích từ thư mục gốc của máy khách web SoOn.
Khi một hành động của khách hàng phải được thực thi, trình quản lý hành động sẽ tra cứu thẻ của nó trong sổ đăng ký, đi theo đường dẫn đã chỉ định và hiển thị tiện ích mà nó tìm thấy ở cuối.
Ghi chú
trình xử lý hành động của máy khách cũng có thể là một hàm thông thường, trong trường hợp đó nó sẽ được gọi và kết quả của nó (nếu có) sẽ được hiểu là hành động tiếp theo cần thực thi.
Về phía máy chủ, chúng tôi chỉ định nghĩa một hành động ir.actions.client
:
<record id="action_home_page" model="ir.actions.client">
<field name="tag">petstore.homepage</field>
</record>
và một menu mở hành động:
<menuitem id="home_page_petstore_menu" parent="petstore_menu"
name="Home Page" action="action_home_page"/>
Kiến trúc của các khung nhìn¶
Phần lớn tính hữu ích (và độ phức tạp) của web SoOn nằm ở chế độ xem. Mỗi loại chế độ xem là một cách hiển thị mô hình trong máy khách.
Trình quản lý chế độ xem¶
Khi một phiên bản ActionManager
nhận được một hành động thuộc loại ir.actions.act_window
, nó ủy quyền việc đồng bộ hóa và xử lý các chế độ xem cho một trình quản lý chế độ xem, sau đó sẽ thiết lập một hoặc nhiều chế độ xem tùy theo theo yêu cầu của hành động ban đầu:

Quan điểm¶
Hầu hết Chế độ xem SoOn được triển khai thông qua một lớp con của odoo.web.View()
cung cấp một chút cấu trúc cơ bản chung để xử lý các sự kiện và hiển thị thông tin mô hình.
Chế độ xem tìm kiếm được coi là một loại chế độ xem theo khung SoOn chính, nhưng được xử lý riêng biệt bởi máy khách web (vì đây là một chế độ cố định lâu dài hơn và có thể tương tác với các chế độ xem khác, điều mà các chế độ xem thông thường không thực hiện được).
Một khung nhìn chịu trách nhiệm tải XML mô tả của riêng nó (sử dụng fields_view_get
) và bất kỳ nguồn dữ liệu nào khác mà nó cần. Với mục đích đó, các chế độ xem được cung cấp một mã định danh chế độ xem tùy chọn được đặt làm thuộc tính view_id
.
Các khung nhìn cũng được cung cấp một phiên bản DataSet()
chứa hầu hết thông tin mô hình cần thiết (tên mô hình và có thể cả các id bản ghi khác nhau).
Chế độ xem cũng có thể muốn xử lý các truy vấn tìm kiếm bằng cách ghi đè do_search()
và cập nhật DataSet()
của chúng nếu cần.
Các trường xem biểu mẫu¶
Nhu cầu chung là mở rộng chế độ xem biểu mẫu web để thêm các cách hiển thị trường mới.
Tất cả các trường tích hợp đều có triển khai hiển thị mặc định, tiện ích biểu mẫu mới có thể cần thiết để tương tác chính xác với loại trường mới (ví dụ: trường GIS) hoặc để cung cấp cách trình bày và cách mới để tương tác với các loại trường hiện có (ví dụ: xác thực các trường Char
phải chứa địa chỉ email và hiển thị chúng dưới dạng liên kết email).
Để chỉ định rõ ràng tiện ích biểu mẫu nào sẽ được sử dụng để hiển thị một trường, chỉ cần sử dụng thuộc tính widget
trong mô tả XML của dạng xem:
<field name="contact_mail" widget="email"/>
Ghi chú
cùng một tiện ích được sử dụng ở cả chế độ "xem" (chỉ đọc) và "chỉnh sửa" của chế độ xem biểu mẫu, không thể sử dụng tiện ích này ở chế độ này và tiện ích khác ở chế độ xem biểu mẫu khác
và một trường (tên) nhất định không thể được sử dụng nhiều lần trong cùng một biểu mẫu
một tiện ích có thể bỏ qua chế độ hiện tại của chế độ xem biểu mẫu và giữ nguyên ở cả chế độ xem và chỉnh sửa
Các trường được chế độ xem biểu mẫu khởi tạo sau khi nó đọc mô tả XML và xây dựng HTML tương ứng thể hiện mô tả đó. Sau đó, chế độ xem biểu mẫu sẽ giao tiếp với các đối tượng trường bằng một số phương thức. Các phương thức này được xác định bởi giao diện FieldInterface
. Hầu hết tất cả các trường đều kế thừa lớp trừu tượng AbstractField
. Lớp đó định nghĩa một số cơ chế mặc định cần được hầu hết các trường triển khai.
Dưới đây là một số trách nhiệm của một lớp trường:
Lớp trường phải hiển thị và cho phép người dùng chỉnh sửa giá trị của trường.
Nó phải triển khai chính xác 3 thuộc tính trường có sẵn trong tất cả các trường của SoOn. Lớp
AbstractField
đã triển khai một thuật toán tính toán động giá trị của các thuộc tính này (chúng có thể thay đổi bất kỳ lúc nào vì giá trị của chúng thay đổi theo giá trị của các trường khác). Giá trị của chúng được lưu trữ trong Thuộc tính tiện ích (thuộc tính tiện ích đã được giải thích trước đó trong hướng dẫn này). Mỗi lớp trường có trách nhiệm kiểm tra các thuộc tính tiện ích này và điều chỉnh linh hoạt tùy thuộc vào giá trị của chúng. Dưới đây là mô tả về từng thuộc tính này:bắt buộc
: Trường phải có giá trị trước khi lưu. Nếurequired
làtrue
và trường không có giá trị, phương thứcis_valid()
của trường phải trả vềfalse
.vô hình
: Khi điều này làtrue
, trường phải ở chế độ ẩn. LớpAbstractField
đã triển khai cơ bản hành vi này phù hợp với hầu hết các trường.readonly
: Khitrue
, người dùng không thể chỉnh sửa trường này. Hầu hết các trường trong SoOn có hành vi hoàn toàn khác tùy thuộc vào giá trị củachỉ đọc
. Ví dụ:FieldChar
hiển thị HTML<input>
khi nó có thể chỉnh sửa được và chỉ hiển thị văn bản khi nó ở chế độ chỉ đọc. Điều này cũng có nghĩa là nó có nhiều mã hơn nên chỉ cần thực hiện một hành vi nhưng điều này là cần thiết để đảm bảo trải nghiệm người dùng tốt.
Các trường có hai phương thức,
set_value()
vàget_value()
, được gọi bởi chế độ xem biểu mẫu để cung cấp giá trị hiển thị và lấy lại giá trị mới do người dùng nhập. Các phương thức này phải có khả năng xử lý giá trị do máy chủ SoOn đưa ra khiread()
được thực hiện trên một mô hình và trả về giá trị hợp lệ chowrite()
. Hãy nhớ rằng các kiểu dữ liệu JavaScript/Python được sử dụng để biểu thị các giá trị được cung cấp bởiread()
và được cung cấp chowrite()
không nhất thiết phải giống nhau trong SoOn. Ví dụ, khi bạn đọc many2one, nó luôn là một bộ có giá trị đầu tiên là id của bản ghi nhọn và giá trị thứ hai là tên get (ví dụ:(15, "Agrolait")
). Nhưng khi bạn viết many2one thì nó phải là một số nguyên duy nhất, không còn là một bộ nữa.AbstractField
có cách triển khai mặc định các phương thức này hoạt động tốt với kiểu dữ liệu đơn giản và đặt thuộc tính widget có tênvalue
.
Xin lưu ý rằng, để hiểu rõ hơn về cách triển khai các trường, bạn nên xem định nghĩa của giao diện FieldInterface
và lớp AbstractField
trực tiếp trong mã của ứng dụng khách web SoOn.
Tạo một loại trường mới¶
Trong phần này chúng tôi sẽ giải thích cách tạo một loại trường mới. Ví dụ ở đây sẽ là triển khai lại lớp FieldChar
và giải thích dần dần từng phần.
Trường chỉ đọc đơn giản¶
Đây là cách triển khai đầu tiên sẽ chỉ hiển thị văn bản. Người dùng sẽ không thể sửa đổi nội dung của trường.
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');
Trong ví dụ này, chúng ta khai báo một lớp có tên FieldChar2
kế thừa từ AbstractField
. Chúng tôi cũng đăng ký lớp này trong sổ đăng ký instance.web.form.widgets
dưới khóa char2
. Điều đó sẽ cho phép chúng ta sử dụng trường mới này trong bất kỳ chế độ xem biểu mẫu nào bằng cách chỉ định widget="char2"
trong thẻ <field/>
trong khai báo XML của chế độ xem.
Trong ví dụ này, chúng ta định nghĩa một phương thức duy nhất: render_value()
. Tất cả những gì nó làm là hiển thị thuộc tính widget value
. Đó là hai công cụ được định nghĩa bởi lớp AbstractField
. Như đã giải thích trước đó, dạng xem biểu mẫu sẽ gọi phương thức set_value()
của trường để đặt giá trị hiển thị. Phương thức này đã được triển khai mặc định trong AbstractField
, phương thức này chỉ đơn giản đặt thuộc tính widget value
. AbstractField
cũng tự mình theo dõi sự kiện change:value
và gọi render_value()
khi nó xảy ra. Vì vậy, render_value()
là một phương thức tiện lợi được triển khai trong các lớp con để thực hiện một số thao tác mỗi khi giá trị của trường thay đổi.
Trong phương thức init()
, chúng tôi cũng xác định giá trị mặc định của trường nếu không có giá trị nào được chỉ định bởi chế độ xem biểu mẫu (ở đây chúng tôi giả sử giá trị mặc định của trường char
phải là một chuỗi trống).
Trường đọc-ghi¶
Các trường chỉ đọc, chỉ hiển thị nội dung và không cho phép người dùng sửa đổi nội dung đó, có thể hữu ích nhưng hầu hết các trường trong SoOn cũng cho phép chỉnh sửa. Điều này làm cho các lớp trường trở nên phức tạp hơn, chủ yếu là do các trường phải xử lý cả chế độ có thể chỉnh sửa và không thể chỉnh sửa, các chế độ đó thường hoàn toàn khác nhau (vì mục đích thiết kế và khả năng sử dụng) và các trường phải có khả năng chuyển đổi giữa các chế độ bất kỳ lúc nào.
Để biết trường hiện tại nên ở chế độ nào, lớp AbstractField
đặt một thuộc tính widget có tên là effect_readonly
. Trường sẽ theo dõi các thay đổi trong thuộc tính tiện ích đó và hiển thị chế độ chính xác tương ứng. Ví dụ:
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>
Trong phương thức start()
(được gọi ngay sau khi một widget được thêm vào DOM), chúng ta liên kết với sự kiện change:effect_readonly
. Điều đó cho phép chúng tôi hiển thị lại trường mỗi khi thuộc tính widget effect_readonly
thay đổi. Trình xử lý sự kiện này sẽ gọi display_field()
, cũng được gọi trực tiếp trong start()
. display_field()
này được tạo riêng cho trường này, nó không phải là một phương thức được định nghĩa trong AbstractField
hoặc bất kỳ lớp nào khác. Chúng ta có thể sử dụng phương pháp này để hiển thị nội dung của trường tùy thuộc vào chế độ hiện tại.
Từ bây giờ quan niệm về trường này là điển hình, ngoại trừ có rất nhiều xác minh để biết trạng thái của thuộc tính effect_readonly
:
Trong mẫu QWeb được sử dụng để hiển thị nội dung của tiện ích, nó sẽ hiển thị
<input type="text" />
nếu chúng ta đang ở chế độ đọc-ghi và không có gì đặc biệt ở chế độ chỉ đọc.Trong phương thức
display_field()
, chúng ta phải liên kết với sự kiệnchange
của<input type="text" />
để biết khi nào người dùng thay đổi giá trị. Khi điều đó xảy ra, chúng ta gọi phương thứcinternal_set_value()
với giá trị mới của trường. Đây là một phương thức tiện lợi được cung cấp bởi lớpAbstractField
. Phương thức đó sẽ đặt một giá trị mới trong thuộc tínhvalue
nhưng sẽ không kích hoạt lệnh gọi đếnrender_value()
(điều này không cần thiết vì<input type="text" />
đã có chứa giá trị đúng).Trong
render_value()
, chúng tôi sử dụng một mã hoàn toàn khác để hiển thị giá trị của trường tùy thuộc vào việc chúng tôi đang ở chế độ chỉ đọc hay ở chế độ đọc-ghi.
Exercise
Tạo trường màu
Tạo một lớp FieldColor
. Giá trị của trường này phải là một chuỗi chứa mã màu giống như mã màu được sử dụng trong CSS (ví dụ: #FF0000
cho màu đỏ). Ở chế độ chỉ đọc, trường màu này sẽ hiển thị một khối nhỏ có màu tương ứng với giá trị của trường. Ở chế độ đọc-ghi, bạn sẽ hiển thị <input type="color" />
. Loại <input />
đó là một thành phần HTML5 không hoạt động trong tất cả các trình duyệt nhưng hoạt động tốt trong Google Chrome. Vì vậy, nó có thể được sử dụng như một bài tập.
Bạn có thể sử dụng tiện ích đó trong chế độ xem biểu mẫu của mô hình message_of_the_day
cho trường của nó có tên color
. Như một phần thưởng, bạn có thể thay đổi tiện ích MessageOfTheDay
được tạo trong phần trước của hướng dẫn này để hiển thị thông báo trong ngày với màu nền được chỉ định trong trường color
.
Các tiện ích tùy chỉnh của chế độ xem biểu mẫu¶
Các trường biểu mẫu được sử dụng để chỉnh sửa một trường duy nhất và về bản chất được liên kết với một trường. Bởi vì điều này có thể bị hạn chế nên cũng có thể tạo tiện ích biểu mẫu không bị hạn chế quá mức và có ít ràng buộc hơn với một vòng đời cụ thể.
Các tiện ích biểu mẫu tùy chỉnh có thể được thêm vào chế độ xem biểu mẫu thông qua thẻ widget
:
<widget type="xxx" />
Loại tiện ích này sẽ được tạo đơn giản bằng chế độ xem biểu mẫu trong quá trình tạo HTML theo định nghĩa XML. Chúng có các thuộc tính chung với các trường (như thuộc tính effect_readonly
) nhưng chúng không được chỉ định một trường chính xác. Và vì vậy họ không có các phương thức như get_value()
và set_value()
. Chúng phải kế thừa từ lớp trừu tượng FormWidget
.
Các tiện ích biểu mẫu có thể tương tác với các trường biểu mẫu bằng cách lắng nghe các thay đổi của chúng và tìm nạp hoặc thay đổi giá trị của chúng. Họ có thể truy cập các trường biểu mẫu thông qua thuộc tính field_manager
:
local.WidgetMultiplication = instance.web.form.FormWidget.extend({
start: function() {
this._super();
this.field_manager.on("field_changed:integer_a", this, this.display_result);
this.field_manager.on("field_changed:integer_b", this, this.display_result);
this.display_result();
},
display_result: function() {
var result = this.field_manager.get_field_value("integer_a") *
this.field_manager.get_field_value("integer_b");
this.$el.text("a*b = " + result);
}
});
instance.web.form.custom_widgets.add('multiplication', 'instance.oepetstore.WidgetMultiplication');
FormWidget
nói chung là chính FormView()
, nhưng các tính năng được sử dụng từ nó phải được giới hạn ở những tính năng được xác định bởi FieldManagerMixin()
, hữu ích nhất là:
get_field_value(field_name)()
trả về giá trị của một trường.set_values(values)()
đặt nhiều giá trị trường, lấy ánh xạ{field_name: value_to_set}
Một sự kiện
field_changed:field_name
được kích hoạt bất cứ khi nào giá trị của trường có tênfield_name
bị thay đổi
Exercise
Hiển thị tọa độ trên Google Map
Thêm hai trường vào product.product
lưu trữ vĩ độ và kinh độ, sau đó tạo tiện ích biểu mẫu mới để hiển thị vĩ độ và kinh độ của nguồn gốc sản phẩm trên bản đồ
Để hiển thị bản đồ, hãy sử dụng tính năng nhúng của Google Map:
<iframe width="400" height="300" src="https://maps.google.com/?ie=UTF8&ll=XXX,YYY&output=embed">
</iframe>
trong đó XXX
phải được thay thế bằng vĩ độ và YYY
bằng kinh độ.
Hiển thị hai trường vị trí và tiện ích bản đồ bằng cách sử dụng chúng trong trang sổ ghi chép mới của chế độ xem biểu mẫu của sản phẩm.
Exercise
Lấy tọa độ hiện tại
Thêm nút đặt lại tọa độ của sản phẩm vào vị trí của người dùng, bạn có thể lấy các tọa độ này bằng cách sử dụng API định vị địa lý javascript.
Bây giờ chúng tôi muốn hiển thị thêm một nút để tự động đặt tọa độ cho vị trí của người dùng hiện tại.
Để có được tọa độ của người dùng, một cách dễ dàng là sử dụng API JavaScript định vị địa lý. Xem tài liệu trực tuyến để biết cách sử dụng nó.
Cũng xin lưu ý rằng người dùng sẽ không thể nhấp vào nút đó khi chế độ xem biểu mẫu ở chế độ chỉ đọc. Vì vậy, tiện ích tùy chỉnh này phải xử lý chính xác thuộc tính effect_readonly
giống như bất kỳ trường nào. Một cách để làm điều này là làm cho nút biến mất khi `` hiệu quả_chỉ đọc`` là đúng.
- 1
như một khái niệm riêng biệt với các trường hợp. Trong nhiều ngôn ngữ, các lớp là các đối tượng chính thức và bản thân chúng là thể hiện (của siêu dữ liệu) nhưng vẫn có hai hệ thống phân cấp khá riêng biệt giữa các lớp và thể hiện
- 2
cũng như ghi lại những khác biệt giữa các trình duyệt, mặc dù điều này đã trở nên ít cần thiết hơn theo thời gian