本章我們將介紹一些關於odoo web服務方面的基礎知識。進階的內容,將在第十四章介紹。
odoo中的web請求是由python的werkzeug庫驅動的。odoo為了操作方便,對werkzeug進行了封裝。
本章,將包含如下內容:
- 配置url路徑
- 為url配置訪問控制
- 處理請求內容
- 繼承url的處理函數
- 提供靜態資源
配置url路徑
准備
我們想所有的用戶都可以獲取所有的圖書列表。此外,我們希望通過JSON請求向程序提供相同的信息。
步驟
- 添加controller/main.py如下:
from odoo import http
from odoo.http import request
class Main(http.Controller): @http.route('/my_library/books', type='http',auth='none')
def books(self):
books = request.env['library.book'].sudo().search([])
html_result = '<html><body><ul>'
for book in books:
html_result += "<li> %s </li>" % book.name
html_result += '</ul></body></html>'
return html_result
- 添加一個函數以JSON格式提供相同的信息,示例如下:
@http.route('/my_library/books/json', type='json', auth='none')
def books_json(self):
records = request.env['library.book'].sudo().search([])
return records.read(['name'])
- 添加controllers/init.py
from . import main
- 引入controller
from . import controllers
重啟odoo服務后,我們可通過/my_library/books url訪問頁面,將展示圖書列表。為了測試JSON-RPC部分,可通過如下:
curl -i -X POST -H "Content-Type: application/json" -d "{}" localhost:8069/my_library/books/json
如果報錯(404),可能是因為我們有多個數據庫。odoo並不知道需要使用哪個數據庫。
可通過--db-filter='^yourdatabasename$'啟動odoo實例。
原理
兩個核心的要點,controller,衍生自odoo.http.Controller;方法,衍生自odoo.http.route。controller將注冊到odoo的路由系統中,與模型的注冊方式類似。
通常,由你的附加組件處理的路徑應該以你的附加組件的名稱開始,以避免名稱沖突。當然,如果您擴展了該附加組件的一些功能,您將使用該附加組件的名稱。
odoo.http.route
route裝飾器將標識該方法是允許被web訪問的,第一個參數是訪問的地址。除了字符串形式,第一個參數也可以的字符串的列表,這可以使用同一個函數處理多個url的請求。
type參數默認是http,這將決定請求的支持的模式。嚴格來說,JSON也是HTTP,將type='json'將會讓工作變的容易很多,因為odoo將會幫我們解決類型轉化的問題。
auth變量將在隨后的章節中介紹。
返回值
請求的返回值是由type參數決定的。對於type='http'的函數,通常返回HTML,所以第一個函數返回了HTML的字符串。另一種方式是通過request.make_response(),我們可以自定義返回header和body。因此,為了指明頁面最后一次更新的時間,我們可以將books()中的最后一行更改為以下代碼:
return request.make_response(
html_result, headers=[
('Last-modified', email.utils.formatdate((fields.Datetime.from_string(request.env['library.book'].sudo().search([], order='write_date desc', limit=1).write_date) -datetime.datetime(1970, 1, 1)).total_seconds(),usegmt=True)),
])
這段代碼發送一個last -modified頭和我們生成的HTML,告訴瀏覽器列表最后一次修改的時間。我們可以從庫的write_date字段中提取這一信息。本模型。
為了讓上面的代碼片段工作,你必須在文件的頂部添加一些導入,如下所示:
import email
import datetime
from odoo import fields
您還可以手動創建werkzeug的響應對象並返回該對象,但是這樣做效率比較低。
重要信息
為了演示的目的,手動生成HTML很好,但是在生產代碼中絕不應該這樣做,應該始終使用模板request.render()。
這將為你提供免費的本地化服務,並通過將業務邏輯與表示層分離使你的代碼變得更好。此外,模板還提供了在輸出HTML之前轉義數據的函數。前面的代碼容易受到跨站點腳本攻擊(例如,如果用戶設法將腳本標記插入到圖書名中)。
對於JSON請求,只需返回您想要移交給客戶端的數據結構;Odoo負責序列化。要實現這一點,您應該限制自己使用JSON可序列化的數據類型,這通常意味着字典、列表、字符串、浮點數和整數。
odoo.http.request
請求對象是一個靜態對象,引用當前處理的請求,其中包含采取操作所需的一切。這里最重要的方面是request.env,它包含一個與self.env相同的環境對象。這個環境綁定到當前用戶,而在前面的示例中並沒有,因為我們使用了auth='none'。缺少用戶也是為什么我們必須在示例代碼中sudo()所有對模型方法的調用。
如果你習慣web開發,您將會面對會話處理。通過request.session,它是OpenERPSession對象(werkzeug的session的簡單封裝),和request.session.sid訪問session的ID。要存儲會話值,只需處理請求。會話作為字典,示例如下:
request.session['hello'] = 'world'
request.session.get('hello')
小貼士
注意,在會話中存儲數據與使用全局變量沒有什么不同。只有在必要的時候才使用它。這通常是多請求操作的情況,例如website_sale模塊中的簽出。
更多
route裝飾器有額外的一些參數用於定制化請求行為。默認,所有的http請求都是可以的。methods參數,用於指定接收的請求類型,一般是['GET']或['POST']。
通過設置cors參數為"*",以允許跨域請求(處於安全考慮,瀏覽器將屏蔽跨域的AJAX以及其他的一些網絡請求)。如果cors未設置,Access-Control-Allow-Origin頭部將不會設置,瀏覽器將默認不允許跨域請求。在我們的例子中,我們將在/my_module/books/json設置cors參數,以允許所有的請求都可以訪問該URL。
默認,odoo通過在請求中攜帶token保護請求免收跨站點偽造請求。如果想關掉該功能,可將csrf設置為False。
參考
關於HTTP路由的相關內容參考如下
- 如果你在一個odoo實例中管理多個數據庫,不同的數據庫可能有不同的域。因此,你可以使用--db-filter,或者dbfilter_from_header模塊,可幫助我們基於域過濾數據庫。
- 如何通過templates實現模塊化,可通過 在后續章節中了解。
為url配置訪問控制
本節,我們將探索三種權限驗證。
准備
我們將利用第四章中的library.book模型。
步驟
定義controllers/main.py
- 添加顯示所有圖書的路由
@http.route('/my_library/all-books', type='http', auth='none')
def all_books(self):
books = request.env['library.book'].sudo().search([])
html_result = '<html><body><ul>'
for book in books:
html_result += "<li> %s </li>" % book.name
html_result += '</ul></body></html>'
return html_result
- 添加路由顯示所有的圖書以及被當前用戶修改的圖書。
@http.route('/my_library/all-books/mark-mine', type='http', auth='public')
def all_books_mark_mine(self):
books = request.env['library.book'].sudo().search([])
html_result = '<html><body><ul>'
for book in books:
if request.env.user.partner_id.id in book.author_ ids.ids:
html_result += "<li> <b>%s</b> </li>" % book.name
else:
html_result += "<li> %s </li>" % book.name
html_result += '</ul></body></html>'
return html_result
- 添加路由顯示當前用戶的圖書
@http.route('/my_library/all-books/mine', type='http', auth='user')
def all_books_mine(self):
books = request.env['library.book'].search([ ('author_ids', 'in', request.env.user.partner_id.ids), ])
html_result = '<html><body><ul>'
for book in books:
html_result += "<li> %s </li>" % book.name
html_result += '</ul></body></html>'
return html_result
"/my_libaray/all-books"和"/my_library/all-books/mark-mine"路由對於未授權用戶而言是一樣的,而登錄用戶在后面的路徑上看到他們的書以粗體顯示。"/my_library/all-books/mine"對於未授權用戶是不可見的。如果您試圖在沒有經過身份驗證的情況下訪問它,您將被重定向到登錄頁面。
原理
不同權限驗證方法之間的不同可在request.env.user中的上下文體現出來。
對於auth='none',用戶記錄總是空的,即便授權用戶訪問該URL。如果您希望提供不依賴於用戶的內容,或者希望在服務器范圍的模塊中提供與數據庫無關的功能,可以使用此選項。
對於auth='public',對於未授權用戶,用戶記錄將被設置為base.public_user的XML ID,授權用戶將設置為自己的ID。如果你計划為授權用戶及未授權用戶提供服務,只是授權用戶將獲得額外的內容時,可以使用此選項。
對於auth='user',可確保僅授權用戶可訪問。request.env.user將指向系統中的用戶。
更多
身份驗證方法定義在ir.http模型中。無論你在auth中傳遞何值,odoo將ir.http模型中查找_auth_method_
例如,我們提供了一個名為base_group_user的身份驗證方法,該方法將僅運行登錄賬戶屬於base.group_user權限組的時候才對URL可見。
from odoo import exceptions, http, models
from odoo.http import request
class IrHttp(models.Model):
_inherit = 'ir.http'
def _auth_method_base_group_user(self):
self._auth_method_user()
if not request.env.user.has_group('base.group_user'):
raise exceptions.AccessDenied()
現在,我們可以在裝飾器中使用auth='base_group_user'以確保只有base.group_user的用戶可以訪問該URL。With a little trickery, you can extend this to auth='groups(xmlid1,...)'; its implementation is left as an exercise to the reader but is included in the GitHub repository example code at Chapter13/ r2_paths_auth/my_library/models/sample_auth_http.py.
使用路由中傳遞的參數
本節將探討幾種針對用戶輸入進行響應的方式。
步驟
首先,我們添加一個接收圖書ID並展示相應圖書詳細內容的路由。然后,我們將參數寫入路由中。
- 添加接收圖書ID的路由
@http.route('/my_library/book_details', type='http', auth='none')
def book_details(self, book_id):
record = request.env['library.book'].sudo().browse(int(book_id))
return u'<html><body><h1>%s</h1>Authors: %s' % (record.name, u', '.join(record.author_ids.mapped('name')) or 'none',)
- 添加接收圖書ID作為URL一部分的路由
@http.route("/my_library/book_details/<model('library. book'):book>",type='http', auth='none')
def book_details_in_path(self, book):
return self.book_details(book.id)
如果我們瀏覽/my_library/book_details?book_id=1,你可以看到id=1的圖書的詳細頁面。如果id不存在,將會報錯。
第二個路由是允許我們通過/my_library/book_details/1展示id=1的詳細頁面。
原理
默認,odoo是混用GET和POST模式的。所以,我們定義了一個路由,其中包含Book_id的參數,那么該路由是支持GET(變量位於url中)或者POST(通過form提交)的。如果我們未傳遞該變量,且未設置該變量的默認值,那么將會報錯。
第二個例子中,我們利用在werkzeug環境下,大部分路由都是虛擬的的現實情況。因此,我們可以方便在路由中包含變量。
更多
在路徑中定義參數是由werkzeug提供的一種稱為轉換器的功能。模型轉換器是由Odoo添加的,Odoo還定義了轉換器模型,這些模型接受以逗號分隔的id列表,並將包含這些id的記錄集傳遞給處理程序。
轉換器的美妙之處在於運行時將參數強制轉換為期望的類型,而您可以自己使用普通的關鍵字參數。它們是作為字符串傳遞的,您必須自己處理必要的類型轉換,如第一個示例所示。
內置的werkzeug轉換器包括int、float和string,但也包括更復雜的,如path、any和uuid。你可以在https:// werkzeug.palletsprojects.com/en/1.0.x/上查找它們的語義。
參考
如果您想了解更多關於HTTP路由的信息,請參考以下幾點:
- Odoo的自定義轉換器定義在基本模塊的ir_http.py中,並在ir.http的_get_converters類方法中注冊。作為練習,您可以創建自己的轉換器,它允許您訪問/my_library/ book_details/Odoo+cookbook頁面來接收該書的詳細信息(如果您之前已將其添加到庫中)。
- 如果你想了解更多關於路徑上的表單提交,請參考CMS網站開發第14章的“從用戶獲取輸入”。
繼承url的處理函數
當你安裝website模塊時,/website/info路徑會顯示你的Odoo實例的一些信息。在這個配方中,我們將重寫它,以更改該信息頁面的布局,並更改顯示的內容。
准備
安裝website模塊
步驟
我們必須調整現有的模板並重寫現有的處理程序。我們可以這樣做:
- 重寫qweb模板, views/templates.xml
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="show_website_info" inherit_id="website.show_website_info">
<xpath expr="//dl[@t-foreach='apps']" position="replace">
<table class="table">
<tr t-foreach="apps" t-as="app">
<th>
<a t-att-href="app.website">
<t t-esc="app.name" />
</a>
</th>
<td>
<t t-esc="app.summary" />
</td>
</tr>
</table>
</xpath>
</template>
</odoo>
- 重寫路由, controllers/main.py
from odoo import http
from odoo.addons.website.controllers.main import Website
class WebsiteInfo(Website):
@http.route()
def website_info(self):
result = super(WebsiteInfo, self).website_info()
result.qcontext['apps'] = result.qcontext['apps'].filtered(lambda x: x.name != 'website')
return result
現在,訪問信息頁面,我們僅能看到已安裝的應用列表。
原理
步驟1,我們重寫了QWeb模板,為了明確目標模板,需查閱源碼。通常如下代碼標識我們需要改寫的模板名稱:
return request.render('template.name', values)
在本案例中,處理程序使用的是名為website_info的模板,但它又被名為website.show_website_info的模板繼承並改寫,因此需要改寫這個模板。我們替換了展示已安裝應用的列表。對於QWeb的繼承原理,可查閱第十五章,客戶端開發。
為了重寫處理程序,我們必須找到名為odoo.addons.website.controllers.main.website的處理程序。通過引入該類並繼承實現改寫。現在,通過改寫我們將返回給內容進行了調整。注意,為簡潔起見,此處重寫的處理程序返回的是一個響應對象,而不是前面的方法所返回的HTML字符串。這個對象包含了對要使用的模板的引用以及模板可以訪問的值,但是它只在請求的最后才被求值。
通常,有三種方法可以更改現有的處理程序:
- 如果它使用QWeb模板,最簡單的更改方法是重寫模板。對於布局更改和小的邏輯更改,這是正確的選擇。
- QWeb模板獲得傳遞的上下文,該上下文作為qcontext成員在響應中可用。這通常是一個可以根據需要添加或刪除值的字典。在前面的例子中,我們只過濾了網站上的應用程序列表。
- 如果處理程序接收到參數,您也可以對這些參數進行預處理,以使覆蓋的處理程序按照您想要的方式運行。
更多
如前一節所述,控制器繼承與模型繼承的工作原理略有不同;實際上,您需要一個基類的引用,並在其上使用Python繼承。
不要忘記用@http.route裝飾新處理程序;Odoo使用它作為標記,將其方法暴露給網絡層。如果忽略了裝飾器,實際上會使處理程序的路徑不可訪問。
@http.route裝飾器本身的行為類似於字段聲明:你沒有設置的每個值都將從你要重寫的函數的裝飾器派生,所以我們不必重復我們不想更改的值。
從你覆蓋的函數接收到一個響應對象后,你可以做更多的事情,而不僅僅是改變QWeb上下文:
- 您可以通過操作response.headers來添加或刪除HTTP標頭。
- 如果您想呈現一個完全不同的模板,您可以覆蓋response.template。
- 要首先檢測響應是否基於QWeb,請查詢
response.is_qweb。 - 通過調用response.render()可以獲得生成的HTML代碼。
提供靜態資源
Web頁面包含幾種類型的靜態資源,比如圖像、視頻、CSS等等。在本教程中,我們將了解如何為模塊管理這樣的靜態資源。
准備
對於這個配方,我們將在頁面上顯示一個圖像。所以,准備一張圖片。另外,從之前的章節中獲取my_library模塊。
步驟
按照以下步驟在頁面上顯示圖像:
- 將圖像添加到/my_library/static/src/img目錄。
- 在controller中定義新的路由。在代碼中,將圖像URL替換為你的圖像URL:
@http.route('/demo_page', type='http', auth='none')
def books(self):
image_url = '/my_library/static/src/image/odoo.png'
html_result = """<html>
<body>
<img src="%s"/>
</body>
</html>""" % image_url
return html_result
重新啟動服務器並更新模塊以應用更改。現在訪問/demo_page查看頁面上的圖像。
原理
位於/static文件夾下的所有文件都被認為是靜態資源,可以公開訪問。在我們的例子中,我們將圖像放在/static/src/img目錄中。你可以把靜態資源放在靜態目錄下的任何位置,但是根據文件類型有一個推薦的目錄結構:
- /static/src/img是用於存放圖像的目錄。
- /static/src/css是css文件的目錄。
- /static/src/scss是存放scss文件的目錄。
- /static/src/fonts是用於字體文件的目錄。
- /static/src/js是JavaScript文件的目錄。
- /static/src/xml是客戶端QWeb模板的xml文件目錄。
- /static/lib是用於存放外部庫文件的目錄。
在我們的示例中,我們在頁面上顯示了一個圖像。您還可以訪問該映像
直接從/my_library/static/src/image/ odoo.png。
在本節中,我們在頁面上顯示了一個靜態資源(圖像),並看到了不同靜態資源的推薦目錄。還有更簡單的方法來表示頁面內容和靜態資源,我們將在下一章中看到。