PY.JS
不知道你是否知道,在早些版本的odoo,在树形列表视图中,有一种可以根据值来改变字体颜色的方法。
<tree string="Languages" colors="gray:not active">
...
</tree>
这种方式可以让我根据字段的值来定义想要显示的字体颜色,一旦满足条件后,整条记录的字体都会变成定义的颜色。但是从9.0版本起,不知道官方出于什么考虑,这个方法被弃用了,转而推出了decoration-*系列属性,然后如果我们想要再使用这种简单的特性就只有从第三方模块那里实现了。
PY.JS 是什么
不知道你是否思考过,上面所提到的属性,是怎样实现的?要回答这个问题,就需要本章的主角登场了:PY.JS。PY.JS不是一个独立的框架,它只是odoo用来使用Python的方式来处理前端Python式逻辑表达式的工具。
例如,我们有一个decorator系列的属性:
<tree decoration-muted="scrapped == True" string="Stock Moves">
PY.JS的工作就是负责解析scrapped == True的表达式,找到相关的字段,并进行表达式的布尔判断,然后将解析后的结果交给具体的业务逻辑代码进行处理,从而实现页面元素的样式的变化。
PY.JS的功能
从PY.JS的说明文档可知,当前模块并不完善,并没有完全实现Python相关的所有特性。仅支持下面的内置类型或方法:
- type: 仅支持创建类型,不支持获取类型属性
- None
- True
- False
- NotImplemented: 运算符没有实现或不受支持时返回(Rich Comparisions)。
- issubclass
- object
- bool: 不能从int转换,因为int没有实现
- float
- str
- tuple: 不支持构造和强制转换,仅支持字面
- list: 目前只是tuple的别称
- dict: 仅支持getter、setter和len
数据模型协议
受支持的协议列表:
- 富比较协议 : 支持大多数的协议,但是_hash_返回的是一个javascript的string
- 布尔转换: _nonzero_
- 自定义属性访问: getter ,setter 但是_delatrr_不支持
- 描述符协议: 不支持_delete_
- 可调用对象
- 抽象基类集合
- 数学类型仿真: abs,divmod和pow尚未实现
API 列表
py.eval
py.eval(expr[,context])
解析expr表达式,并使用传入的上下文对象context,其内部实际调用了tokenize、parse和evaluate方法。
py.tokenize
py.tokenize(expr)
接收一个表达式参数
py.parse
py.parse(tokens)
接收一个由py.tokenize()方法返回的对象,返回一个抽象的语法树
py.evaluate
py.evaluate(ast[, context])
接收两个参数,ast是py.parse方法的返回值,context是Python表达式的上下文变量
PY.JS在搜索视图中的应用
不知道有没有同学想过,我们在搜索视图中使用当前用户uid来进行过滤的时候,为什么uid就可以正常使用,而user就不能使用?如果你不清楚上面问题的答案,那么现在我们就来对这种机制一探究竟。
首先,我们从odoo的提示中可以知道,搜索视图支持的变量只有uid,而其他的变量并不支持。经过一番努力,我们找到了搜索视图(13.0)中对于上下文的赋值代码:
getQuery: function () {
var userContext = session.user_context;
var context = _.extend(
pyUtils.eval('contexts', this._getQueryContext(), userContext),
this._getTimeRangeMenuData(true)
);
var domain = Domain.prototype.stringToArray(this._getDomain(), userContext);
// this must be done because pyUtils.eval does not know that it needs to evaluate domains within contexts
if (context.timeRangeMenuData) {
if (typeof context.timeRangeMenuData.timeRange === 'string') {
context.timeRangeMenuData.timeRange = pyUtils.eval('domain', context.timeRangeMenuData.timeRange);
}
if (typeof context.timeRangeMenuData.comparisonTimeRange === 'string') {
context.timeRangeMenuData.comparisonTimeRange = pyUtils.eval('domain', context.timeRangeMenuData.comparisonTimeRange);
}
}
var action_context = this.actionContext;
var results = pyUtils.eval_domains_and_contexts({
domains: [this.actionDomain].concat([domain] || []),
contexts: [action_context].concat(context || []),
eval_context: session.user_context,
});
if (results.error) {
throw new Error(_.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s",
JSON.stringify(results.error)));
}
var groupBys = this._getGroupBy();
var groupBy = groupBys.length ?
groupBys :
(this.actionContext.group_by || []);
groupBy = (typeof groupBy === 'string') ? [groupBy] : groupBy;
context = _.omit(results.context, 'time_ranges');
return {
context: context,
domain: results.domain,
groupBy: this.searchMenuTypes.includes('groupBy') ? groupBy : [],
orderedBy: this._getOrderedBy(),
};
},
由代码中可以看出来,odoo在把domain表达式交给py.js进行解析的时候,同时传入了上下文对象context。而context的来源有两处:session中的user_context和、_getQueryContext()方法。_getQueryContext方法的作用,我们按下不表,只看一下user_context的来源:
def session_info(self):
user = request.env.user
version_info = odoo.service.common.exp_version()
user_context = request.session.get_context() if request.session.uid else {}
max_file_upload_size = int(self.env['ir.config_parameter'].sudo().get_param(
'web.max_file_upload_size',
default=128 * 1024 * 1024, # 128MiB
))
session_info = {
"uid": request.session.uid,
"is_system": user._is_system() if request.session.uid else False,
"is_admin": user._is_admin() if request.session.uid else False,
"user_context": request.session.get_context() if request.session.uid else {},
"db": request.session.db,
"server_version": version_info.get('server_version'),
"server_version_info": version_info.get('server_version_info'),
"name": user.name,
"username": user.login,
"partner_display_name": user.partner_id.display_name,
"company_id": user.company_id.id if request.session.uid else None, # YTI TODO: Remove this from the user context
"partner_id": user.partner_id.id if request.session.uid and user.partner_id else None,
"web.base.url": self.env['ir.config_parameter'].sudo().get_param('web.base.url', default=''),
"max_file_upload_size": max_file_upload_size,
}
if self.env.user.has_group('base.group_user'):
# the following is only useful in the context of a webclient bootstrapping
# but is still included in some other calls (e.g. '/web/session/authenticate')
# to avoid access errors and unnecessary information, it is only included for users
# with access to the backend ('internal'-type users)
mods = module_boot()
qweb_checksum = HomeStaticTemplateHelpers.get_qweb_templates_checksum(addons=mods, debug=request.session.debug)
lang = user_context.get("lang")
translation_hash = request.env['ir.translation'].get_web_translations_hash(mods, lang)
menu_json_utf8 = json.dumps(request.env['ir.ui.menu'].load_menus(request.session.debug), default=ustr, sort_keys=True).encode()
cache_hashes = {
"load_menus": hashlib.sha1(menu_json_utf8).hexdigest(),
"qweb": qweb_checksum,
"translations": translation_hash,
}
session_info.update({
# current_company should be default_company
"user_companies": {'current_company': (user.company_id.id, user.company_id.name), 'allowed_companies': [(comp.id, comp.name) for comp in user.company_ids]},
"currencies": self.get_currencies(),
"show_effect": True,
"display_switch_company_menu": user.has_group('base.group_multi_company') and len(user.company_ids) > 1,
"cache_hashes": cache_hashes,
})
return session_info
我们从session的session_info的数据构造过程中得知, user_context是由get_context方法返回的。
def get_context(self):
"""
Re-initializes the current user's session context (based on his
preferences) by calling res.users.get_context() with the old context.
:returns: the new context
"""
assert self.uid, "The user needs to be logged-in to initialize his context"
self.context = dict(request.env['res.users'].context_get() or {})
self.context['uid'] = self.uid
self._fix_lang(self.context)
return self.context
而get_context方法的值根据res.users对象的context_get方法返回,具体的这里就不再展开了。同时需要注意的是,我们前面提到的uid也是在get_context方法中进行了赋值。也因此,这对于我们进行拓展提供了有效的思路。
我们在实际使用过程中,通常也会有客户提出想要根据当前公司进行条件过滤,那么,读到这里的聪明的你一定知道怎么做了吧。不知道也没有关系,笔者将此功能集成到了欧姆基础模块中,有需要的小朋友可以到商城联系笔者购买哦。