Table of Contentshtml
命令入口node
服務器python
啓動serverweb
thread 模式sql
prefork 模式shell
gevent模式數據庫
wsgi 應用json
響應 客戶端請求api
xmlrpc服務器
odoo-bin 腳本
支持的子命令
help
deploy
scaffold
server
shell
start
子命令註冊
command 基類, 往 commands 註冊 支持的子命令, 等定義 接口 run()…. 每一個子命令必須實現它
運行 子命令 server ,
檢查運行用戶、pg用戶、讀取配置、顯示主要配置
加載 全局 addons… load_server_wide_modules() #L864 service\server.py
此時 initialize_sys_path() , 並 import odoo模塊, 名稱空間爲 'odoo.addons.'+module_name
# 默認全局加載的模塊爲 web
根據 配置 運行 具體模式server 的 run(preload, stop) 方法 ; 支持如下集中模式
運行 server 時, 根據配置 肯定server ,而後將 wsgi應用 傳遞給 它, 也就是將 self.app 指定爲 wsgi 應用 odoo.service.wsgi_server.application
, 以便在啓動http server後,以此wsgi 應用來響應請求
對於 Thread 模式, 經過 run 入口 調用 http_spawn() 以線程模式啓動 wsgi服務器
以 werkzeug.serving.ThreadedWSGIServer 模式 啓動 wsgi application
對於 Prefork 模式, 經過 run 入口 調用 worker_spawn() 依次調用對應的 worker,
http worker --> WorkerHTTP
cron worker --> WorkerCron
long_polling 進程
在 worker_spawn 初始化 對應的 worker 後 調用 run() 啓動它
對於 http worker ,,, 則 調用WorkerHTTP,self.workers_http), 啓動 BaseWSGIServerNoBind(self.multi.app)
使用 werkzeug 啓動 wsgi 服務器werkzeug.serving.BaseWSGIServer.__init__(self,"127.0.0.1",0,app)
對於 gevent 模式, 以 gevent.wsgi 方式啓動 服務器
說明: gevent模式專用於 longpooling
封裝了2個 wsgi 處理器
當對 http server發起請求時, http server將請求轉發給 wsgi應用
當wsgi 接收到 請求時, 嘗試調用全部支持的 wsgi 處理器[handler]對請求進行處理,
調用的前後順序爲
wsgi_xmlrpc(environ,start_response)
odoo.http.root(environ,start_response)
若是handler處理不了,則返回 "No handler found." 錯誤
調用 xmlrpc 處理器 #L102 wsgi_xmlrpc wsgi_server.py
驗證xmlrpc 調用請求,取出 xmlrpc 調用的服務,方法,以及參數, 傳遞給 odoo.http.dispatch_rpc(service,method,params),返回 最終 dispath() 方法
各個服務將調用相應的模塊的dispatch() 方法來 分發 遠程調用
每一個服務都實現了 dispatch 方法
odoo web 將 http請求分爲2種
它們都繼承webreqeust
http.root # odoo/http.py#L1294
http Root() 初始初始化,並 將請求經過 dispatch() 進行分發
先判斷是否 第一次加載 addons, 如是, 則 加載 模塊 ,並使用 靜態文件 處理器的 dispatch 方法 分發請求
若是不是 第一次加載 addons, 則調用 root 的 dispatch() 方法 分發請求
加載 模塊,目的是 加載全部包含了 靜態文件 和 controller的 odoo addons
對於靜態文件的addons, 則將 使用 disablecachemiddleware 對 wsgi 應用 進行 處理
對於其餘的,則使用 root 的 dispatch 方法, 根據 請求的不一樣情形 進行分發
可能的情形
同時,在作實際的dispatch() 以前,先對 web request 進行識別, 判斷究竟是 jsonrequest 仍是 httpreqeust
根據請求數據 判別 request 類型, 而後用 對應的 request 方式進行數據處理
ir_http dispatch 經過 _find_handler() 調用 ir.http 類方法 routing_map()獲取 路由表 # L227 ir_http.py
根據已經 安裝的 模塊, 經由 http.routing_map() 獲得 route map.
例如,
根據 route_map 選擇 對應的 endpoint 處理 web 請求
調用 web 請求的 dispatch() 方法 對請求進行處理, 而相應的 request 最終會 調用
父類 Request 方法 _call_function() 調用 endpoint 處理 request…
若是是 http 類型, 調用 HttpRequest.dispatch() 處理
使用 對應的 endpoint 處理 reqeust.. 並返回結果
若是是 http 類型, 調用 JsonRequest.dispatch() 處理
對應的endpoint 處理 請求, 返回結果 經 _json_response 處理爲 jsonrpc 返回數據規範
在加載odoo addons的時候,若是是controller 會往 controllers_per_module{} 註冊 控制器類, 註冊內容是
{ 模塊:[ (模塊名.類名, 類)] }
例如
生成路由表時, 從 註冊表讀出已安裝的模塊, 而後從上面數據讀出控制器類,並讀出 方法的 routing 屬性
routing屬性,是在往 控制器方法修飾 route 時, 注入進去的
registry , 每一個數據庫 一個 註冊表, 在 每一個 odoo實例 的 registry 對象 的 registries 屬性記錄 所有的 註冊表
主要 方法
load() |
加載 模型, 構建 model class |
setup_models() |
設置 base , 設置 字段, 設置 計算字段 |
init_models() |
初始模型,調用 model 的 auto_init() 和 init( ) 操做數據庫, 創建 數據庫表 , 增長字段 字段 , 增長 約束 /// 在此 實現 MPTT 【 預排序遍歷樹 】 // 提示, 能夠在model 定製 init() 改變 數據庫初始化邏輯 |
wsgi 應用 Dispatch 請求時, dispatch 邏輯裏,在檢查註冊表時, 先嚐試 獲取 註冊表, 而後檢查 "信號" # odoo/odoo/http.py:1445
註冊表獲取 # odoo/odoo/__init__.py:76
根據db 建立 註冊表, 加載 模塊 # odoo/odoo/modules/registry.py:61
加載 模塊 # odoo/odoo/modules/registry.py:85
load_information_from_description_file()
在 安裝/升級 模塊時, 執行 migrations
Migrations 定義:
This class manage the migration of modules
Migrations files must be python files containing a `migrate(cr, installed_version)`
function. Theses files must respect a directory tree structure: A 'migrations' folder
which containt a folder by version. Version can be 'module' version or 'server.module'
version (in this case, the files will only be processed by this version of the server).
Python file names must start by `pre` or `post` and will be executed, respectively,
before and after the module initialisation. `end` scripts are run after all modules have
been updated.
Example:
<moduledir>
`-- migrations === 目錄名必須
|-- 1.0 === 版本號, odoo服務版本號,或者模塊版本號
| |-- pre-update_table_x.py === 升級前執行腳本
| |-- pre-update_table_y.py
| |-- post-create_plop_records.py === 升級後執行腳本
| |-- end-cleanup.py === 最終執行腳本
| `-- README.txt # not processed
|-- 9.0.1.1 # processed only on a 9.0 server
| |-- pre-delete_table_z.py
| `-- post-clean-data.py
`-- foo.py # not processed
當 遷移腳本的 版本 處於 已安裝的版本, 和當前版本直接時, 才 會執行
if parsed_installed_version < parse_version(convert_version(version)) <= current_version:
代碼 odoo/odoo/modules/loading.py
背景知識點: python 環境 sys.modules
load_openerp_module 處理 odoo 名稱 空間
odoo.addons.[addons_name].models.[model_name]
別名
openerp.addons. *
# 注意
此外,在引入 odoo模塊的時候,經過 MetaModel 將 addons 登記 module_to_models, 以便 註冊表 在 load 模型時, 構建 model.
在 模塊加載 邏輯的 第二步, 更新 數據庫表 ir_module_module 往裏面 登記 須要 加載的模塊
往註冊表 加載 模塊時, 調用 model 的 build_model()方法 創建 模型 # L233 load() registry.py
def load(self, cr, module):
""" Load a given module in the registry, and return the names of the
modified models.
At the Python level, the modules are already loaded, but not yet on a
per-registry level. This method populates a registry with the given
modules, i.e. it instanciates all the classes of a the given module
and registers them in the registry.
"""
from .. import models
lazy_property.reset_all(self)
# Instantiate registered classes (via the MetaModel automatic discovery
# or via explicit constructor call), and add them to the pool.
model_names = []
for cls in models.MetaModel.module_to_models.get(module.name, []):
# models register themselves in self.models
model = cls._build_model(self, cr)
model_names.append(model._name)
return self.descendants(model_names, '_inherit', '_inherits')
根據 addons depends 以及 _inherit 來決定 model 繼承
例如 , 經過 type(env['res.users']).mro() 查看 繼承 順序, 調用 supper() 時, 調用父級的前後順序
env['res.users'] 爲 空記錄集
type(env['res.users']) 獲得 記錄集對應的模型, 類型 [ class ]
例如, res.users 模型
在 模塊 加載時, 經過 _load_data() 調用 tools.convert_file() 將 data file 導入到 db
tools.convert_file(cr, module_name, filename, idref, mode, noupdate, kind, report)
convert_file()
def convert_file(cr, module, filename, idref, mode='update', noupdate=False, kind=None, report=None, pathname=None):
if pathname is None:
pathname = os.path.join(module, filename)
ext = os.path.splitext(filename)[1].lower()
with file_open(pathname, 'rb') as fp:
if ext == '.csv':
convert_csv_import(cr, module, pathname, fp.read(), idref, mode, noupdate)
elif ext == '.sql':
convert_sql_import(cr, fp)
elif ext == '.yml':
convert_yaml_import(cr, module, fp, kind, idref, mode, noupdate, report)
elif ext == '.xml':
convert_xml_import(cr, module, fp, idref, mode, noupdate, report)
elif ext == '.js':
pass # .js files are valid but ignored here.
else:
raise ValueError("Can't load unknown file type %s.", filename)
實例化一個 model..
records=object.__new__(cls)
而後 給它的 屬性 _ids 賦值
這樣, 就能夠 經過 __getitem__() 獲取 字段數據, __setitem__() 設置 字段數據
記錄集 操做
Recordsets are immutable, but sets of the same model can be combined using various set operations, returning new recordsets. Set operations do not preserve order.
_read_from_database()
調用 cursor 執行 數據庫 讀取, 同時 更新 record cache.
# store result in cache
for vals in result:
record = self.browse(vals.pop('id'), self._prefetch)
record._cache.update(record._convert_to_cache(vals, validate=False))
read()
create()
write()
unlink()
模型的 _constraints 屬性和 _sql_constraints 屬性
其中 _constraints 已廢棄,改用 @api. Constraints()
(name, sql_definition, message)
模型初始化 db 時,往 db 創建約束, _add_sql_constraints () #L2175 model.py
經過 _validate_fields() 驗證 #L933 model.py /// create() 和 write() 時調用。
經過返回 true, 不然返回異常
detailed,,,
_constraints
list of (constraint_function, message, fields) defining Python constraints. The fields list is indicative
Deprecated since version 8.0: use constrains()
_sql_constraints
list of (name, sql_definition, message) triples defining SQL constraints to execute when generating the backing table
模型 defaults 屬性
經過上下文默認值,用戶默認值, 模型默認值[ 字段默認值,父級默認值] 進行維護
上下文默認值
default_ 開頭, 加上 字段
用戶默認值
self.env['ir.default'].get_model_defaults(self._name)
模型默認值
字段默認值
field.default
父級字段默認值
if field and field.inherited:
field = field.related_field
parent_fields[field.model_name].append(field.name)
具體 邏輯
default_get() #L974 model.py
實踐用法:
改寫 default_get() 改變默認值
Web client 經過 rpc 調用 load_views 獲得視圖定義
@api.model
def load_views(self, views, options=None):
""" Returns the fields_views of given views, along with the fields of
the current model, and optionally its filters for the given action.
:param views: list of [view_id, view_type]
:param options['toolbar']: True to include contextual actions when loading fields_views
:param options['load_filters']: True to return the model's filters
:param options['action_id']: id of the action to get the filters
:return: dictionary with fields_views, fields and optionally filters
"""
options = options or {}
result = {}
toolbar = options.get('toolbar')
result['fields_views'] = {
v_type: self.fields_view_get(v_id, v_type if v_type != 'list' else 'tree',
toolbar=toolbar if v_type != 'search' else False)
for [v_id, v_type] in views
}
result['fields'] = self.fields_get()
if options.get('load_filters'):
result['filters'] = self.env['ir.filters'].get_filters(self._name, options.get('action_id'))
return result
底層 2個 方法:
fields_view_get 獲取視圖定義
fields_get 獲取字段定義
ACL 記錄在 ir.model.access
Record rule 記錄在 ir.rule
對於 admin ,,, user_id =1 旁路
if self._uid == 1:
# User root have all accesses
return True
對於 admin ,,, user_id =1 旁路
if self._uid == SUPERUSER_ID:
return
在 增create() 刪 unlink() 改 write() 查 search() 時, 調用 check_access_rights() 以及 check_access_rule() 檢查是否有權限
調用 _apply_group() 將 無權限訪問的 node 去除
對於 field 設置了 權限的, 將 字段設置爲 readonly
wsgi 應用
導出 xmlrpc 和 http 服務
WSGI application 被 server 使用
http.py dispatch_rpc()
根據不一樣的名稱空間,轉發給對應的 handler
http.py Root.dispatch()
# 經過 Root 類 的 __call__() 調用 dispatch()