Merged Latest.
bzr revid: tta@openerp.com-20121206111229-q2fn8zfp71ntclxv
This commit is contained in:
commit
3464960518
|
@ -42,6 +42,7 @@ This module provides the core of the OpenERP Web Client.
|
||||||
"static/lib/py.js/lib/py.js",
|
"static/lib/py.js/lib/py.js",
|
||||||
"static/src/js/boot.js",
|
"static/src/js/boot.js",
|
||||||
"static/src/js/testing.js",
|
"static/src/js/testing.js",
|
||||||
|
"static/src/js/pyeval.js",
|
||||||
"static/src/js/corelib.js",
|
"static/src/js/corelib.js",
|
||||||
"static/src/js/coresetup.js",
|
"static/src/js/coresetup.js",
|
||||||
"static/src/js/dates.js",
|
"static/src/js/dates.js",
|
||||||
|
|
|
@ -28,9 +28,9 @@ except ImportError:
|
||||||
xlwt = None
|
xlwt = None
|
||||||
|
|
||||||
import openerp
|
import openerp
|
||||||
|
from openerp.tools.translate import _
|
||||||
|
|
||||||
from .. import http
|
from .. import http
|
||||||
from .. import nonliterals
|
|
||||||
openerpweb = http
|
openerpweb = http
|
||||||
|
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
|
@ -361,40 +361,15 @@ def set_cookie_and_redirect(req, redirect_url):
|
||||||
redirect.set_cookie('instance0|session_id', cookie_val)
|
redirect.set_cookie('instance0|session_id', cookie_val)
|
||||||
return redirect
|
return redirect
|
||||||
|
|
||||||
def eval_context_and_domain(session, context, domain=None):
|
|
||||||
e_context = session.eval_context(context)
|
|
||||||
# should we give the evaluated context as an evaluation context to the domain?
|
|
||||||
e_domain = session.eval_domain(domain or [])
|
|
||||||
|
|
||||||
return e_context, e_domain
|
|
||||||
|
|
||||||
def load_actions_from_ir_values(req, key, key2, models, meta):
|
def load_actions_from_ir_values(req, key, key2, models, meta):
|
||||||
context = req.session.eval_context(req.context)
|
|
||||||
Values = req.session.model('ir.values')
|
Values = req.session.model('ir.values')
|
||||||
actions = Values.get(key, key2, models, meta, context)
|
actions = Values.get(key, key2, models, meta, req.context)
|
||||||
|
|
||||||
return [(id, name, clean_action(req, action, context))
|
return [(id, name, clean_action(req, action))
|
||||||
for id, name, action in actions]
|
for id, name, action in actions]
|
||||||
|
|
||||||
def clean_action(req, action, context, do_not_eval=False):
|
def clean_action(req, action):
|
||||||
action.setdefault('flags', {})
|
action.setdefault('flags', {})
|
||||||
|
|
||||||
context = context or {}
|
|
||||||
eval_ctx = req.session.evaluation_context(context)
|
|
||||||
|
|
||||||
if not do_not_eval:
|
|
||||||
# values come from the server, we can just eval them
|
|
||||||
if action.get('context') and isinstance(action.get('context'), basestring):
|
|
||||||
action['context'] = eval( action['context'], eval_ctx ) or {}
|
|
||||||
|
|
||||||
if action.get('domain') and isinstance(action.get('domain'), basestring):
|
|
||||||
action['domain'] = eval( action['domain'], eval_ctx ) or []
|
|
||||||
else:
|
|
||||||
if 'context' in action:
|
|
||||||
action['context'] = parse_context(action['context'], req.session)
|
|
||||||
if 'domain' in action:
|
|
||||||
action['domain'] = parse_domain(action['domain'], req.session)
|
|
||||||
|
|
||||||
action_type = action.setdefault('type', 'ir.actions.act_window_close')
|
action_type = action.setdefault('type', 'ir.actions.act_window_close')
|
||||||
if action_type == 'ir.actions.act_window':
|
if action_type == 'ir.actions.act_window':
|
||||||
return fix_view_modes(action)
|
return fix_view_modes(action)
|
||||||
|
@ -473,39 +448,6 @@ def fix_view_modes(action):
|
||||||
|
|
||||||
return action
|
return action
|
||||||
|
|
||||||
def parse_domain(domain, session):
|
|
||||||
""" Parses an arbitrary string containing a domain, transforms it
|
|
||||||
to either a literal domain or a :class:`nonliterals.Domain`
|
|
||||||
|
|
||||||
:param domain: the domain to parse, if the domain is not a string it
|
|
||||||
is assumed to be a literal domain and is returned as-is
|
|
||||||
:param session: Current OpenERP session
|
|
||||||
:type session: openerpweb.OpenERPSession
|
|
||||||
"""
|
|
||||||
if not isinstance(domain, basestring):
|
|
||||||
return domain
|
|
||||||
try:
|
|
||||||
return ast.literal_eval(domain)
|
|
||||||
except ValueError:
|
|
||||||
# not a literal
|
|
||||||
return nonliterals.Domain(session, domain)
|
|
||||||
|
|
||||||
def parse_context(context, session):
|
|
||||||
""" Parses an arbitrary string containing a context, transforms it
|
|
||||||
to either a literal context or a :class:`nonliterals.Context`
|
|
||||||
|
|
||||||
:param context: the context to parse, if the context is not a string it
|
|
||||||
is assumed to be a literal domain and is returned as-is
|
|
||||||
:param session: Current OpenERP session
|
|
||||||
:type session: openerpweb.OpenERPSession
|
|
||||||
"""
|
|
||||||
if not isinstance(context, basestring):
|
|
||||||
return context
|
|
||||||
try:
|
|
||||||
return ast.literal_eval(context)
|
|
||||||
except ValueError:
|
|
||||||
return nonliterals.Context(session, context)
|
|
||||||
|
|
||||||
def _local_web_translations(trans_file):
|
def _local_web_translations(trans_file):
|
||||||
messages = []
|
messages = []
|
||||||
try:
|
try:
|
||||||
|
@ -821,7 +763,7 @@ class Database(openerpweb.Controller):
|
||||||
except xmlrpclib.Fault, e:
|
except xmlrpclib.Fault, e:
|
||||||
if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
|
if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
|
||||||
return {'error': e.faultCode, 'title': 'Drop Database'}
|
return {'error': e.faultCode, 'title': 'Drop Database'}
|
||||||
return {'error': 'Could not drop database !', 'title': 'Drop Database'}
|
return {'error': _('Could not drop database !'), 'title': _('Drop Database')}
|
||||||
|
|
||||||
@openerpweb.httprequest
|
@openerpweb.httprequest
|
||||||
def backup(self, req, backup_db, backup_pwd, token):
|
def backup(self, req, backup_db, backup_pwd, token):
|
||||||
|
@ -839,7 +781,7 @@ class Database(openerpweb.Controller):
|
||||||
{'fileToken': int(token)}
|
{'fileToken': int(token)}
|
||||||
)
|
)
|
||||||
except xmlrpclib.Fault, e:
|
except xmlrpclib.Fault, e:
|
||||||
return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
|
return simplejson.dumps([[],[{'error': e.faultCode, 'title': _('Backup Database')}]])
|
||||||
|
|
||||||
@openerpweb.httprequest
|
@openerpweb.httprequest
|
||||||
def restore(self, req, db_file, restore_pwd, new_db):
|
def restore(self, req, db_file, restore_pwd, new_db):
|
||||||
|
@ -860,8 +802,8 @@ class Database(openerpweb.Controller):
|
||||||
return req.session.proxy("db").change_admin_password(old_password, new_password)
|
return req.session.proxy("db").change_admin_password(old_password, new_password)
|
||||||
except xmlrpclib.Fault, e:
|
except xmlrpclib.Fault, e:
|
||||||
if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
|
if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
|
||||||
return {'error': e.faultCode, 'title': 'Change Password'}
|
return {'error': e.faultCode, 'title': _('Change Password')}
|
||||||
return {'error': 'Error, password not changed !', 'title': 'Change Password'}
|
return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
|
||||||
|
|
||||||
class Session(openerpweb.Controller):
|
class Session(openerpweb.Controller):
|
||||||
_cp_path = "/web/session"
|
_cp_path = "/web/session"
|
||||||
|
@ -897,87 +839,34 @@ class Session(openerpweb.Controller):
|
||||||
old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
|
old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
|
||||||
dict(map(operator.itemgetter('name', 'value'), fields)))
|
dict(map(operator.itemgetter('name', 'value'), fields)))
|
||||||
if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
|
if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
|
||||||
return {'error':'You cannot leave any password empty.','title': 'Change Password'}
|
return {'error':_('You cannot leave any password empty.'),'title': _('Change Password')}
|
||||||
if new_password != confirm_password:
|
if new_password != confirm_password:
|
||||||
return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
|
return {'error': _('The new password and its confirmation must be identical.'),'title': _('Change Password')}
|
||||||
try:
|
try:
|
||||||
if req.session.model('res.users').change_password(
|
if req.session.model('res.users').change_password(
|
||||||
old_password, new_password):
|
old_password, new_password):
|
||||||
return {'new_password':new_password}
|
return {'new_password':new_password}
|
||||||
except Exception:
|
except Exception:
|
||||||
return {'error': 'The old password you provided is incorrect, your password was not changed.', 'title': 'Change Password'}
|
return {'error': _('The old password you provided is incorrect, your password was not changed.'), 'title': _('Change Password')}
|
||||||
return {'error': 'Error, password not changed !', 'title': 'Change Password'}
|
return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
@openerpweb.jsonrequest
|
||||||
def sc_list(self, req):
|
def sc_list(self, req):
|
||||||
return req.session.model('ir.ui.view_sc').get_sc(
|
return req.session.model('ir.ui.view_sc').get_sc(
|
||||||
req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
|
req.session._uid, "ir.ui.menu", req.context)
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
@openerpweb.jsonrequest
|
||||||
def get_lang_list(self, req):
|
def get_lang_list(self, req):
|
||||||
try:
|
try:
|
||||||
return req.session.proxy("db").list_lang() or []
|
return req.session.proxy("db").list_lang() or []
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
return {"error": e, "title": "Languages"}
|
return {"error": e, "title": _("Languages")}
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
@openerpweb.jsonrequest
|
||||||
def modules(self, req):
|
def modules(self, req):
|
||||||
# return all installed modules. Web client is smart enough to not load a module twice
|
# return all installed modules. Web client is smart enough to not load a module twice
|
||||||
return module_installed(req)
|
return module_installed(req)
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
|
||||||
def eval_domain_and_context(self, req, contexts, domains,
|
|
||||||
group_by_seq=None):
|
|
||||||
""" Evaluates sequences of domains and contexts, composing them into
|
|
||||||
a single context, domain or group_by sequence.
|
|
||||||
|
|
||||||
:param list contexts: list of contexts to merge together. Contexts are
|
|
||||||
evaluated in sequence, all previous contexts
|
|
||||||
are part of their own evaluation context
|
|
||||||
(starting at the session context).
|
|
||||||
:param list domains: list of domains to merge together. Domains are
|
|
||||||
evaluated in sequence and appended to one another
|
|
||||||
(implicit AND), their evaluation domain is the
|
|
||||||
result of merging all contexts.
|
|
||||||
:param list group_by_seq: list of domains (which may be in a different
|
|
||||||
order than the ``contexts`` parameter),
|
|
||||||
evaluated in sequence, their ``'group_by'``
|
|
||||||
key is extracted if they have one.
|
|
||||||
:returns:
|
|
||||||
a 3-dict of:
|
|
||||||
|
|
||||||
context (``dict``)
|
|
||||||
the global context created by merging all of
|
|
||||||
``contexts``
|
|
||||||
|
|
||||||
domain (``list``)
|
|
||||||
the concatenation of all domains
|
|
||||||
|
|
||||||
group_by (``list``)
|
|
||||||
a list of fields to group by, potentially empty (in which case
|
|
||||||
no group by should be performed)
|
|
||||||
"""
|
|
||||||
context, domain = eval_context_and_domain(req.session,
|
|
||||||
nonliterals.CompoundContext(*(contexts or [])),
|
|
||||||
nonliterals.CompoundDomain(*(domains or [])))
|
|
||||||
|
|
||||||
group_by_sequence = []
|
|
||||||
for candidate in (group_by_seq or []):
|
|
||||||
ctx = req.session.eval_context(candidate, context)
|
|
||||||
group_by = ctx.get('group_by')
|
|
||||||
if not group_by:
|
|
||||||
continue
|
|
||||||
elif isinstance(group_by, basestring):
|
|
||||||
group_by_sequence.append(group_by)
|
|
||||||
else:
|
|
||||||
group_by_sequence.extend(group_by)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'context': context,
|
|
||||||
'domain': domain,
|
|
||||||
'group_by': group_by_sequence
|
|
||||||
}
|
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
@openerpweb.jsonrequest
|
||||||
def save_session_action(self, req, the_action):
|
def save_session_action(self, req, the_action):
|
||||||
"""
|
"""
|
||||||
|
@ -1047,18 +936,19 @@ class Menu(openerpweb.Controller):
|
||||||
:rtype: list(int)
|
:rtype: list(int)
|
||||||
"""
|
"""
|
||||||
s = req.session
|
s = req.session
|
||||||
context = s.eval_context(req.context)
|
|
||||||
Menus = s.model('ir.ui.menu')
|
Menus = s.model('ir.ui.menu')
|
||||||
# If a menu action is defined use its domain to get the root menu items
|
# If a menu action is defined use its domain to get the root menu items
|
||||||
user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
|
user_menu_id = s.model('res.users').read([s._uid], ['menu_id'],
|
||||||
|
req.context)[0]['menu_id']
|
||||||
|
|
||||||
menu_domain = [('parent_id', '=', False)]
|
menu_domain = [('parent_id', '=', False)]
|
||||||
if user_menu_id:
|
if user_menu_id:
|
||||||
domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
|
domain_string = s.model('ir.actions.act_window').read(
|
||||||
|
[user_menu_id[0]], ['domain'],req.context)[0]['domain']
|
||||||
if domain_string:
|
if domain_string:
|
||||||
menu_domain = ast.literal_eval(domain_string)
|
menu_domain = ast.literal_eval(domain_string)
|
||||||
|
|
||||||
return Menus.search(menu_domain, 0, False, False, context)
|
return Menus.search(menu_domain, 0, False, False, req.context)
|
||||||
|
|
||||||
def do_load(self, req):
|
def do_load(self, req):
|
||||||
""" Loads all menu items (all applications and their sub-menus).
|
""" Loads all menu items (all applications and their sub-menus).
|
||||||
|
@ -1068,23 +958,30 @@ class Menu(openerpweb.Controller):
|
||||||
:return: the menu root
|
:return: the menu root
|
||||||
:rtype: dict('children': menu_nodes)
|
:rtype: dict('children': menu_nodes)
|
||||||
"""
|
"""
|
||||||
context = req.session.eval_context(req.context)
|
|
||||||
Menus = req.session.model('ir.ui.menu')
|
Menus = req.session.model('ir.ui.menu')
|
||||||
|
|
||||||
menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
|
fields = ['name', 'sequence', 'parent_id', 'action',
|
||||||
menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
|
'needaction_enabled', 'needaction_counter']
|
||||||
|
menu_roots = Menus.read(self.do_get_user_roots(req), fields, req.context)
|
||||||
|
menu_root = {
|
||||||
|
'id': False,
|
||||||
|
'name': 'root',
|
||||||
|
'parent_id': [-1, ''],
|
||||||
|
'children': menu_roots
|
||||||
|
}
|
||||||
|
|
||||||
# menus are loaded fully unlike a regular tree view, cause there are a
|
# menus are loaded fully unlike a regular tree view, cause there are a
|
||||||
# limited number of items (752 when all 6.1 addons are installed)
|
# limited number of items (752 when all 6.1 addons are installed)
|
||||||
menu_ids = Menus.search([], 0, False, False, context)
|
menu_ids = Menus.search([], 0, False, False, req.context)
|
||||||
menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
|
menu_items = Menus.read(menu_ids, fields, req.context)
|
||||||
# adds roots at the end of the sequence, so that they will overwrite
|
# adds roots at the end of the sequence, so that they will overwrite
|
||||||
# equivalent menu items from full menu read when put into id:item
|
# equivalent menu items from full menu read when put into id:item
|
||||||
# mapping, resulting in children being correctly set on the roots.
|
# mapping, resulting in children being correctly set on the roots.
|
||||||
menu_items.extend(menu_roots)
|
menu_items.extend(menu_roots)
|
||||||
|
|
||||||
# make a tree using parent_id
|
# make a tree using parent_id
|
||||||
menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
|
menu_items_map = dict(
|
||||||
|
(menu_item["id"], menu_item) for menu_item in menu_items)
|
||||||
for menu_item in menu_items:
|
for menu_item in menu_items:
|
||||||
if menu_item['parent_id']:
|
if menu_item['parent_id']:
|
||||||
parent = menu_item['parent_id'][0]
|
parent = menu_item['parent_id'][0]
|
||||||
|
@ -1134,12 +1031,10 @@ class DataSet(openerpweb.Controller):
|
||||||
"""
|
"""
|
||||||
Model = req.session.model(model)
|
Model = req.session.model(model)
|
||||||
|
|
||||||
context, domain = eval_context_and_domain(
|
ids = Model.search(domain, offset or 0, limit or False, sort or False,
|
||||||
req.session, req.context, domain)
|
req.context)
|
||||||
|
|
||||||
ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
|
|
||||||
if limit and len(ids) == limit:
|
if limit and len(ids) == limit:
|
||||||
length = Model.search_count(domain, context)
|
length = Model.search_count(domain, req.context)
|
||||||
else:
|
else:
|
||||||
length = len(ids) + (offset or 0)
|
length = len(ids) + (offset or 0)
|
||||||
if fields and fields == ['id']:
|
if fields and fields == ['id']:
|
||||||
|
@ -1149,7 +1044,7 @@ class DataSet(openerpweb.Controller):
|
||||||
'records': [{'id': id} for id in ids]
|
'records': [{'id': id} for id in ids]
|
||||||
}
|
}
|
||||||
|
|
||||||
records = Model.read(ids, fields or False, context)
|
records = Model.read(ids, fields or False, req.context)
|
||||||
records.sort(key=lambda obj: ids.index(obj['id']))
|
records.sort(key=lambda obj: ids.index(obj['id']))
|
||||||
return {
|
return {
|
||||||
'length': length,
|
'length': length,
|
||||||
|
@ -1160,37 +1055,15 @@ class DataSet(openerpweb.Controller):
|
||||||
def load(self, req, model, id, fields):
|
def load(self, req, model, id, fields):
|
||||||
m = req.session.model(model)
|
m = req.session.model(model)
|
||||||
value = {}
|
value = {}
|
||||||
r = m.read([id], False, req.session.eval_context(req.context))
|
r = m.read([id], False, req.context)
|
||||||
if r:
|
if r:
|
||||||
value = r[0]
|
value = r[0]
|
||||||
return {'value': value}
|
return {'value': value}
|
||||||
|
|
||||||
def call_common(self, req, model, method, args, domain_id=None, context_id=None):
|
def call_common(self, req, model, method, args, domain_id=None, context_id=None):
|
||||||
has_domain = domain_id is not None and domain_id < len(args)
|
|
||||||
has_context = context_id is not None and context_id < len(args)
|
|
||||||
|
|
||||||
domain = args[domain_id] if has_domain else []
|
|
||||||
context = args[context_id] if has_context else {}
|
|
||||||
c, d = eval_context_and_domain(req.session, context, domain)
|
|
||||||
if has_domain:
|
|
||||||
args[domain_id] = d
|
|
||||||
if has_context:
|
|
||||||
args[context_id] = c
|
|
||||||
|
|
||||||
return self._call_kw(req, model, method, args, {})
|
return self._call_kw(req, model, method, args, {})
|
||||||
|
|
||||||
def _call_kw(self, req, model, method, args, kwargs):
|
|
||||||
for i in xrange(len(args)):
|
|
||||||
if isinstance(args[i], nonliterals.BaseContext):
|
|
||||||
args[i] = req.session.eval_context(args[i])
|
|
||||||
elif isinstance(args[i], nonliterals.BaseDomain):
|
|
||||||
args[i] = req.session.eval_domain(args[i])
|
|
||||||
for k in kwargs.keys():
|
|
||||||
if isinstance(kwargs[k], nonliterals.BaseContext):
|
|
||||||
kwargs[k] = req.session.eval_context(kwargs[k])
|
|
||||||
elif isinstance(kwargs[k], nonliterals.BaseDomain):
|
|
||||||
kwargs[k] = req.session.eval_domain(kwargs[k])
|
|
||||||
|
|
||||||
|
def _call_kw(self, req, model, method, args, kwargs):
|
||||||
# Temporary implements future display_name special field for model#read()
|
# Temporary implements future display_name special field for model#read()
|
||||||
if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
|
if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
|
||||||
if 'display_name' in args[1]:
|
if 'display_name' in args[1]:
|
||||||
|
@ -1203,39 +1076,9 @@ class DataSet(openerpweb.Controller):
|
||||||
|
|
||||||
return getattr(req.session.model(model), method)(*args, **kwargs)
|
return getattr(req.session.model(model), method)(*args, **kwargs)
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
|
||||||
def onchange(self, req, model, method, args, context_id=None):
|
|
||||||
""" Support method for handling onchange calls: behaves much like call
|
|
||||||
with the following differences:
|
|
||||||
|
|
||||||
* Does not take a domain_id
|
|
||||||
* Is aware of the return value's structure, and will parse the domains
|
|
||||||
if needed in order to return either parsed literal domains (in JSON)
|
|
||||||
or non-literal domain instances, allowing those domains to be used
|
|
||||||
from JS
|
|
||||||
|
|
||||||
:param req:
|
|
||||||
:type req: web.common.http.JsonRequest
|
|
||||||
:param str model: object type on which to call the method
|
|
||||||
:param str method: name of the onchange handler method
|
|
||||||
:param list args: arguments to call the onchange handler with
|
|
||||||
:param int context_id: index of the context object in the list of
|
|
||||||
arguments
|
|
||||||
:return: result of the onchange call with all domains parsed
|
|
||||||
"""
|
|
||||||
result = self.call_common(req, model, method, args, context_id=context_id)
|
|
||||||
if not result or 'domain' not in result:
|
|
||||||
return result
|
|
||||||
|
|
||||||
result['domain'] = dict(
|
|
||||||
(k, parse_domain(v, req.session))
|
|
||||||
for k, v in result['domain'].iteritems())
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
@openerpweb.jsonrequest
|
||||||
def call(self, req, model, method, args, domain_id=None, context_id=None):
|
def call(self, req, model, method, args, domain_id=None, context_id=None):
|
||||||
return self.call_common(req, model, method, args, domain_id, context_id)
|
return self._call_kw(req, model, method, args, {})
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
@openerpweb.jsonrequest
|
||||||
def call_kw(self, req, model, method, args, kwargs):
|
def call_kw(self, req, model, method, args, kwargs):
|
||||||
|
@ -1243,10 +1086,9 @@ class DataSet(openerpweb.Controller):
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
@openerpweb.jsonrequest
|
||||||
def call_button(self, req, model, method, args, domain_id=None, context_id=None):
|
def call_button(self, req, model, method, args, domain_id=None, context_id=None):
|
||||||
context = req.session.eval_context(req.context)
|
action = self._call_kw(req, model, method, args, {})
|
||||||
action = self.call_common(req, model, method, args, domain_id, context_id)
|
|
||||||
if isinstance(action, dict) and action.get('type') != '':
|
if isinstance(action, dict) and action.get('type') != '':
|
||||||
return clean_action(req, action, context)
|
return clean_action(req, action)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
@openerpweb.jsonrequest
|
||||||
|
@ -1282,12 +1124,9 @@ class View(openerpweb.Controller):
|
||||||
def fields_view_get(self, req, model, view_id, view_type,
|
def fields_view_get(self, req, model, view_id, view_type,
|
||||||
transform=True, toolbar=False, submenu=False):
|
transform=True, toolbar=False, submenu=False):
|
||||||
Model = req.session.model(model)
|
Model = req.session.model(model)
|
||||||
context = req.session.eval_context(req.context)
|
fvg = Model.fields_view_get(view_id, view_type, req.context, toolbar, submenu)
|
||||||
fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
|
|
||||||
# todo fme?: check that we should pass the evaluated context here
|
# todo fme?: check that we should pass the evaluated context here
|
||||||
self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
|
self.process_view(req.session, fvg, req.context, transform, (view_type == 'kanban'))
|
||||||
if toolbar and transform:
|
|
||||||
self.process_toolbar(req, fvg['toolbar'])
|
|
||||||
return fvg
|
return fvg
|
||||||
|
|
||||||
def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
|
def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
|
||||||
|
@ -1304,12 +1143,8 @@ class View(openerpweb.Controller):
|
||||||
arch = fvg['arch']
|
arch = fvg['arch']
|
||||||
fvg['arch_string'] = arch
|
fvg['arch_string'] = arch
|
||||||
|
|
||||||
if transform:
|
fvg['arch'] = xml2json_from_elementtree(
|
||||||
evaluation_context = session.evaluation_context(context or {})
|
ElementTree.fromstring(arch), preserve_whitespaces)
|
||||||
xml = self.transform_view(arch, session, evaluation_context)
|
|
||||||
else:
|
|
||||||
xml = ElementTree.fromstring(arch)
|
|
||||||
fvg['arch'] = xml2json_from_elementtree(xml, preserve_whitespaces)
|
|
||||||
|
|
||||||
if 'id' in fvg['fields']:
|
if 'id' in fvg['fields']:
|
||||||
# Special case for id's
|
# Special case for id's
|
||||||
|
@ -1318,29 +1153,8 @@ class View(openerpweb.Controller):
|
||||||
id_field['type'] = 'id'
|
id_field['type'] = 'id'
|
||||||
|
|
||||||
for field in fvg['fields'].itervalues():
|
for field in fvg['fields'].itervalues():
|
||||||
if field.get('views'):
|
for view in field.get("views", {}).itervalues():
|
||||||
for view in field["views"].itervalues():
|
self.process_view(session, view, None, transform)
|
||||||
self.process_view(session, view, None, transform)
|
|
||||||
if field.get('domain'):
|
|
||||||
field["domain"] = parse_domain(field["domain"], session)
|
|
||||||
if field.get('context'):
|
|
||||||
field["context"] = parse_context(field["context"], session)
|
|
||||||
|
|
||||||
def process_toolbar(self, req, toolbar):
|
|
||||||
"""
|
|
||||||
The toolbar is a mapping of section_key: [action_descriptor]
|
|
||||||
|
|
||||||
We need to clean all those actions in order to ensure correct
|
|
||||||
round-tripping
|
|
||||||
"""
|
|
||||||
for actions in toolbar.itervalues():
|
|
||||||
for action in actions:
|
|
||||||
if 'context' in action:
|
|
||||||
action['context'] = parse_context(
|
|
||||||
action['context'], req.session)
|
|
||||||
if 'domain' in action:
|
|
||||||
action['domain'] = parse_domain(
|
|
||||||
action['domain'], req.session)
|
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
@openerpweb.jsonrequest
|
||||||
def add_custom(self, req, view_id, arch):
|
def add_custom(self, req, view_id, arch):
|
||||||
|
@ -1349,57 +1163,22 @@ class View(openerpweb.Controller):
|
||||||
'user_id': req.session._uid,
|
'user_id': req.session._uid,
|
||||||
'ref_id': view_id,
|
'ref_id': view_id,
|
||||||
'arch': arch
|
'arch': arch
|
||||||
}, req.session.eval_context(req.context))
|
}, req.context)
|
||||||
return {'result': True}
|
return {'result': True}
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
@openerpweb.jsonrequest
|
||||||
def undo_custom(self, req, view_id, reset=False):
|
def undo_custom(self, req, view_id, reset=False):
|
||||||
CustomView = req.session.model('ir.ui.view.custom')
|
CustomView = req.session.model('ir.ui.view.custom')
|
||||||
context = req.session.eval_context(req.context)
|
|
||||||
vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
|
vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
|
||||||
0, False, False, context)
|
0, False, False, req.context)
|
||||||
if vcustom:
|
if vcustom:
|
||||||
if reset:
|
if reset:
|
||||||
CustomView.unlink(vcustom, context)
|
CustomView.unlink(vcustom, req.context)
|
||||||
else:
|
else:
|
||||||
CustomView.unlink([vcustom[0]], context)
|
CustomView.unlink([vcustom[0]], req.context)
|
||||||
return {'result': True}
|
return {'result': True}
|
||||||
return {'result': False}
|
return {'result': False}
|
||||||
|
|
||||||
def transform_view(self, view_string, session, context=None):
|
|
||||||
# transform nodes on the fly via iterparse, instead of
|
|
||||||
# doing it statically on the parsing result
|
|
||||||
parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
|
|
||||||
root = None
|
|
||||||
for event, elem in parser:
|
|
||||||
if event == "start":
|
|
||||||
if root is None:
|
|
||||||
root = elem
|
|
||||||
self.parse_domains_and_contexts(elem, session)
|
|
||||||
return root
|
|
||||||
|
|
||||||
def parse_domains_and_contexts(self, elem, session):
|
|
||||||
""" Converts domains and contexts from the view into Python objects,
|
|
||||||
either literals if they can be parsed by literal_eval or a special
|
|
||||||
placeholder object if the domain or context refers to free variables.
|
|
||||||
|
|
||||||
:param elem: the current node being parsed
|
|
||||||
:type param: xml.etree.ElementTree.Element
|
|
||||||
:param session: OpenERP session object, used to store and retrieve
|
|
||||||
non-literal objects
|
|
||||||
:type session: openerpweb.openerpweb.OpenERPSession
|
|
||||||
"""
|
|
||||||
for el in ['domain', 'filter_domain']:
|
|
||||||
domain = elem.get(el, '').strip()
|
|
||||||
if domain:
|
|
||||||
elem.set(el, parse_domain(domain, session))
|
|
||||||
elem.set(el + '_string', domain)
|
|
||||||
for el in ['context', 'default_get']:
|
|
||||||
context_string = elem.get(el, '').strip()
|
|
||||||
if context_string:
|
|
||||||
elem.set(el, parse_context(context_string, session))
|
|
||||||
elem.set(el + '_string', context_string)
|
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
@openerpweb.jsonrequest
|
||||||
def load(self, req, model, view_id, view_type, toolbar=False):
|
def load(self, req, model, view_id, view_type, toolbar=False):
|
||||||
return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
|
return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
|
||||||
|
@ -1413,50 +1192,6 @@ class TreeView(View):
|
||||||
req,'action', 'tree_but_open',[(model, id)],
|
req,'action', 'tree_but_open',[(model, id)],
|
||||||
False)
|
False)
|
||||||
|
|
||||||
class SearchView(View):
|
|
||||||
_cp_path = "/web/searchview"
|
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
|
||||||
def load(self, req, model, view_id):
|
|
||||||
fields_view = self.fields_view_get(req, model, view_id, 'search')
|
|
||||||
return {'fields_view': fields_view}
|
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
|
||||||
def fields_get(self, req, model):
|
|
||||||
Model = req.session.model(model)
|
|
||||||
fields = Model.fields_get(False, req.session.eval_context(req.context))
|
|
||||||
for field in fields.values():
|
|
||||||
# shouldn't convert the views too?
|
|
||||||
if field.get('domain'):
|
|
||||||
field["domain"] = parse_domain(field["domain"], req.session)
|
|
||||||
if field.get('context'):
|
|
||||||
field["context"] = parse_context(field["context"], req.session)
|
|
||||||
return {'fields': fields}
|
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
|
||||||
def get_filters(self, req, model):
|
|
||||||
logger = logging.getLogger(__name__ + '.SearchView.get_filters')
|
|
||||||
Model = req.session.model("ir.filters")
|
|
||||||
filters = Model.get_filters(model)
|
|
||||||
for filter in filters:
|
|
||||||
try:
|
|
||||||
parsed_context = parse_context(filter["context"], req.session)
|
|
||||||
filter["context"] = (parsed_context
|
|
||||||
if not isinstance(parsed_context, nonliterals.BaseContext)
|
|
||||||
else req.session.eval_context(parsed_context))
|
|
||||||
|
|
||||||
parsed_domain = parse_domain(filter["domain"], req.session)
|
|
||||||
filter["domain"] = (parsed_domain
|
|
||||||
if not isinstance(parsed_domain, nonliterals.BaseDomain)
|
|
||||||
else req.session.eval_domain(parsed_domain))
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to parse custom filter %s in %s",
|
|
||||||
filter['name'], model)
|
|
||||||
filter['disabled'] = True
|
|
||||||
del filter['context']
|
|
||||||
del filter['domain']
|
|
||||||
return filters
|
|
||||||
|
|
||||||
class Binary(openerpweb.Controller):
|
class Binary(openerpweb.Controller):
|
||||||
_cp_path = "/web/binary"
|
_cp_path = "/web/binary"
|
||||||
|
|
||||||
|
@ -1464,7 +1199,6 @@ class Binary(openerpweb.Controller):
|
||||||
def image(self, req, model, id, field, **kw):
|
def image(self, req, model, id, field, **kw):
|
||||||
last_update = '__last_update'
|
last_update = '__last_update'
|
||||||
Model = req.session.model(model)
|
Model = req.session.model(model)
|
||||||
context = req.session.eval_context(req.context)
|
|
||||||
headers = [('Content-Type', 'image/png')]
|
headers = [('Content-Type', 'image/png')]
|
||||||
etag = req.httprequest.headers.get('If-None-Match')
|
etag = req.httprequest.headers.get('If-None-Match')
|
||||||
hashed_session = hashlib.md5(req.session_id).hexdigest()
|
hashed_session = hashlib.md5(req.session_id).hexdigest()
|
||||||
|
@ -1475,22 +1209,22 @@ class Binary(openerpweb.Controller):
|
||||||
if not id and hashed_session == etag:
|
if not id and hashed_session == etag:
|
||||||
return werkzeug.wrappers.Response(status=304)
|
return werkzeug.wrappers.Response(status=304)
|
||||||
else:
|
else:
|
||||||
date = Model.read([id], [last_update], context)[0].get(last_update)
|
date = Model.read([id], [last_update], req.context)[0].get(last_update)
|
||||||
if hashlib.md5(date).hexdigest() == etag:
|
if hashlib.md5(date).hexdigest() == etag:
|
||||||
return werkzeug.wrappers.Response(status=304)
|
return werkzeug.wrappers.Response(status=304)
|
||||||
|
|
||||||
retag = hashed_session
|
retag = hashed_session
|
||||||
try:
|
try:
|
||||||
if not id:
|
if not id:
|
||||||
res = Model.default_get([field], context).get(field)
|
res = Model.default_get([field], req.context).get(field)
|
||||||
image_base64 = res
|
image_base64 = res
|
||||||
else:
|
else:
|
||||||
res = Model.read([id], [last_update, field], context)[0]
|
res = Model.read([id], [last_update, field], req.context)[0]
|
||||||
retag = hashlib.md5(res.get(last_update)).hexdigest()
|
retag = hashlib.md5(res.get(last_update)).hexdigest()
|
||||||
image_base64 = res.get(field)
|
image_base64 = res.get(field)
|
||||||
|
|
||||||
if kw.get('resize'):
|
if kw.get('resize'):
|
||||||
resize = kw.get('resize').split(',');
|
resize = kw.get('resize').split(',')
|
||||||
if len(resize) == 2 and int(resize[0]) and int(resize[1]):
|
if len(resize) == 2 and int(resize[0]) and int(resize[1]):
|
||||||
width = int(resize[0])
|
width = int(resize[0])
|
||||||
height = int(resize[1])
|
height = int(resize[1])
|
||||||
|
@ -1532,14 +1266,13 @@ class Binary(openerpweb.Controller):
|
||||||
:returns: :class:`werkzeug.wrappers.Response`
|
:returns: :class:`werkzeug.wrappers.Response`
|
||||||
"""
|
"""
|
||||||
Model = req.session.model(model)
|
Model = req.session.model(model)
|
||||||
context = req.session.eval_context(req.context)
|
|
||||||
fields = [field]
|
fields = [field]
|
||||||
if filename_field:
|
if filename_field:
|
||||||
fields.append(filename_field)
|
fields.append(filename_field)
|
||||||
if id:
|
if id:
|
||||||
res = Model.read([int(id)], fields, context)[0]
|
res = Model.read([int(id)], fields, req.context)[0]
|
||||||
else:
|
else:
|
||||||
res = Model.default_get(fields, context)
|
res = Model.default_get(fields, req.context)
|
||||||
filecontent = base64.b64decode(res.get(field, ''))
|
filecontent = base64.b64decode(res.get(field, ''))
|
||||||
if not filecontent:
|
if not filecontent:
|
||||||
return req.not_found()
|
return req.not_found()
|
||||||
|
@ -1558,9 +1291,8 @@ class Binary(openerpweb.Controller):
|
||||||
field = jdata['field']
|
field = jdata['field']
|
||||||
id = jdata.get('id', None)
|
id = jdata.get('id', None)
|
||||||
filename_field = jdata.get('filename_field', None)
|
filename_field = jdata.get('filename_field', None)
|
||||||
context = jdata.get('context', dict())
|
context = jdata.get('context', {})
|
||||||
|
|
||||||
context = req.session.eval_context(context)
|
|
||||||
Model = req.session.model(model)
|
Model = req.session.model(model)
|
||||||
fields = [field]
|
fields = [field]
|
||||||
if filename_field:
|
if filename_field:
|
||||||
|
@ -1571,7 +1303,7 @@ class Binary(openerpweb.Controller):
|
||||||
res = Model.default_get(fields, context)
|
res = Model.default_get(fields, context)
|
||||||
filecontent = base64.b64decode(res.get(field, ''))
|
filecontent = base64.b64decode(res.get(field, ''))
|
||||||
if not filecontent:
|
if not filecontent:
|
||||||
raise ValueError("No content found for field '%s' on '%s:%s'" %
|
raise ValueError(_("No content found for field '%s' on '%s:%s'") %
|
||||||
(field, model, id))
|
(field, model, id))
|
||||||
else:
|
else:
|
||||||
filename = '%s_%s' % (model.replace('.', '_'), id)
|
filename = '%s_%s' % (model.replace('.', '_'), id)
|
||||||
|
@ -1599,7 +1331,6 @@ class Binary(openerpweb.Controller):
|
||||||
|
|
||||||
@openerpweb.httprequest
|
@openerpweb.httprequest
|
||||||
def upload_attachment(self, req, callback, model, id, ufile):
|
def upload_attachment(self, req, callback, model, id, ufile):
|
||||||
context = req.session.eval_context(req.context)
|
|
||||||
Model = req.session.model('ir.attachment')
|
Model = req.session.model('ir.attachment')
|
||||||
try:
|
try:
|
||||||
out = """<script language="javascript" type="text/javascript">
|
out = """<script language="javascript" type="text/javascript">
|
||||||
|
@ -1612,7 +1343,7 @@ class Binary(openerpweb.Controller):
|
||||||
'datas_fname': ufile.filename,
|
'datas_fname': ufile.filename,
|
||||||
'res_model': model,
|
'res_model': model,
|
||||||
'res_id': int(id)
|
'res_id': int(id)
|
||||||
}, context)
|
}, req.context)
|
||||||
args = {
|
args = {
|
||||||
'filename': ufile.filename,
|
'filename': ufile.filename,
|
||||||
'id': attachment_id
|
'id': attachment_id
|
||||||
|
@ -1625,12 +1356,9 @@ class Action(openerpweb.Controller):
|
||||||
_cp_path = "/web/action"
|
_cp_path = "/web/action"
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
@openerpweb.jsonrequest
|
||||||
def load(self, req, action_id, do_not_eval=False, eval_context=None):
|
def load(self, req, action_id, do_not_eval=False):
|
||||||
Actions = req.session.model('ir.actions.actions')
|
Actions = req.session.model('ir.actions.actions')
|
||||||
value = False
|
value = False
|
||||||
context = req.session.eval_context(req.context)
|
|
||||||
eval_context = req.session.eval_context(nonliterals.CompoundContext(context, eval_context or {}))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
action_id = int(action_id)
|
action_id = int(action_id)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -1641,25 +1369,24 @@ class Action(openerpweb.Controller):
|
||||||
except Exception:
|
except Exception:
|
||||||
action_id = 0 # force failed read
|
action_id = 0 # force failed read
|
||||||
|
|
||||||
base_action = Actions.read([action_id], ['type'], context)
|
base_action = Actions.read([action_id], ['type'], req.context)
|
||||||
if base_action:
|
if base_action:
|
||||||
ctx = {}
|
ctx = {}
|
||||||
action_type = base_action[0]['type']
|
action_type = base_action[0]['type']
|
||||||
if action_type == 'ir.actions.report.xml':
|
if action_type == 'ir.actions.report.xml':
|
||||||
ctx.update({'bin_size': True})
|
ctx.update({'bin_size': True})
|
||||||
ctx.update(context)
|
ctx.update(req.context)
|
||||||
action = req.session.model(action_type).read([action_id], False, ctx)
|
action = req.session.model(action_type).read([action_id], False, ctx)
|
||||||
if action:
|
if action:
|
||||||
value = clean_action(req, action[0], eval_context, do_not_eval)
|
value = clean_action(req, action[0])
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
@openerpweb.jsonrequest
|
||||||
def run(self, req, action_id):
|
def run(self, req, action_id):
|
||||||
context = req.session.eval_context(req.context)
|
|
||||||
return_action = req.session.model('ir.actions.server').run(
|
return_action = req.session.model('ir.actions.server').run(
|
||||||
[action_id], req.session.eval_context(req.context))
|
[action_id], req.context)
|
||||||
if return_action:
|
if return_action:
|
||||||
return clean_action(req, return_action, context)
|
return clean_action(req, return_action)
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -1682,7 +1409,7 @@ class Export(View):
|
||||||
|
|
||||||
def fields_get(self, req, model):
|
def fields_get(self, req, model):
|
||||||
Model = req.session.model(model)
|
Model = req.session.model(model)
|
||||||
fields = Model.fields_get(False, req.session.eval_context(req.context))
|
fields = Model.fields_get(False, req.context)
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
@openerpweb.jsonrequest
|
||||||
|
@ -1834,12 +1561,11 @@ class Export(View):
|
||||||
'import_compat')(
|
'import_compat')(
|
||||||
simplejson.loads(data))
|
simplejson.loads(data))
|
||||||
|
|
||||||
context = req.session.eval_context(req.context)
|
|
||||||
Model = req.session.model(model)
|
Model = req.session.model(model)
|
||||||
ids = ids or Model.search(domain, 0, False, False, context)
|
ids = ids or Model.search(domain, 0, False, False, req.context)
|
||||||
|
|
||||||
field_names = map(operator.itemgetter('name'), fields)
|
field_names = map(operator.itemgetter('name'), fields)
|
||||||
import_data = Model.export_data(ids, field_names, context).get('datas',[])
|
import_data = Model.export_data(ids, field_names, req.context).get('datas',[])
|
||||||
|
|
||||||
if import_compat:
|
if import_compat:
|
||||||
columns_headers = field_names
|
columns_headers = field_names
|
||||||
|
@ -1944,9 +1670,8 @@ class Reports(View):
|
||||||
action = simplejson.loads(action)
|
action = simplejson.loads(action)
|
||||||
|
|
||||||
report_srv = req.session.proxy("report")
|
report_srv = req.session.proxy("report")
|
||||||
context = req.session.eval_context(
|
context = dict(req.context)
|
||||||
nonliterals.CompoundContext(
|
context.update(action["context"])
|
||||||
req.context or {}, action[ "context"]))
|
|
||||||
|
|
||||||
report_data = {}
|
report_data = {}
|
||||||
report_ids = context["active_ids"]
|
report_ids = context["active_ids"]
|
||||||
|
|
|
@ -11,7 +11,7 @@ from mako.template import Template
|
||||||
from openerp.modules import module
|
from openerp.modules import module
|
||||||
|
|
||||||
from .main import module_topological_sort
|
from .main import module_topological_sort
|
||||||
from ..http import Controller, httprequest
|
from .. import http
|
||||||
|
|
||||||
NOMODULE_TEMPLATE = Template(u"""<!DOCTYPE html>
|
NOMODULE_TEMPLATE = Template(u"""<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
@ -82,10 +82,10 @@ TESTING = Template(u"""<!DOCTYPE html>
|
||||||
</html>
|
</html>
|
||||||
""")
|
""")
|
||||||
|
|
||||||
class TestRunnerController(Controller):
|
class TestRunnerController(http.Controller):
|
||||||
_cp_path = '/web/tests'
|
_cp_path = '/web/tests'
|
||||||
|
|
||||||
@httprequest
|
@http.httprequest
|
||||||
def index(self, req, mod=None, **kwargs):
|
def index(self, req, mod=None, **kwargs):
|
||||||
ms = module.get_modules()
|
ms = module.get_modules()
|
||||||
manifests = dict(
|
manifests = dict(
|
||||||
|
|
|
@ -32,11 +32,15 @@ information:
|
||||||
|
|
||||||
The major difference is in the lifecycle of these:
|
The major difference is in the lifecycle of these:
|
||||||
|
|
||||||
* if the client action maps to a function, the function will simply be
|
* if the client action maps to a function, the function will be called
|
||||||
called when executing the action. The function can have no further
|
when executing the action. The function can have no further
|
||||||
interaction with the Web Client itself, although it can return an
|
interaction with the Web Client itself, although it can return an
|
||||||
action which will be executed after it.
|
action which will be executed after it.
|
||||||
|
|
||||||
|
The function takes 2 parameters: the ActionManager calling it and
|
||||||
|
the descriptor for the current action (the ``ir.actions.client``
|
||||||
|
dictionary).
|
||||||
|
|
||||||
* if, on the other hand, the client action maps to a
|
* if, on the other hand, the client action maps to a
|
||||||
:js:class:`~openerp.web.Widget`, that
|
:js:class:`~openerp.web.Widget`, that
|
||||||
:js:class:`~openerp.web.Widget` will be instantiated and added to
|
:js:class:`~openerp.web.Widget` will be instantiated and added to
|
||||||
|
@ -51,7 +55,7 @@ object::
|
||||||
// Registers the object 'openerp.web_dashboard.Widget' to the client
|
// Registers the object 'openerp.web_dashboard.Widget' to the client
|
||||||
// action tag 'board.home.widgets'
|
// action tag 'board.home.widgets'
|
||||||
instance.web.client_actions.add(
|
instance.web.client_actions.add(
|
||||||
'board.home.widgets', 'openerp.web_dashboard.Widget');
|
'board.home.widgets', 'instance.web_dashboard.Widget');
|
||||||
instance.web_dashboard.Widget = instance.web.Widget.extend({
|
instance.web_dashboard.Widget = instance.web.Widget.extend({
|
||||||
template: 'HomeWidget'
|
template: 'HomeWidget'
|
||||||
});
|
});
|
||||||
|
@ -60,15 +64,15 @@ At this point, the generic :js:class:`~openerp.web.Widget` lifecycle
|
||||||
takes over, the template is rendered, inserted in the client DOM,
|
takes over, the template is rendered, inserted in the client DOM,
|
||||||
bound on the object's ``$el`` property and the object is started.
|
bound on the object's ``$el`` property and the object is started.
|
||||||
|
|
||||||
If the client action takes parameters, these parameters are passed in as a
|
The second parameter to the constructor is the descriptor for the
|
||||||
second positional parameter to the constructor::
|
action itself, which contains any parameter provided::
|
||||||
|
|
||||||
init: function (parent, params) {
|
init: function (parent, action) {
|
||||||
// execute the Widget's init
|
// execute the Widget's init
|
||||||
this._super(parent);
|
this._super(parent);
|
||||||
// board.home.widgets only takes a single param, the identifier of the
|
// board.home.widgets only takes a single param, the identifier of the
|
||||||
// res.widget object it should display. Store it for later
|
// res.widget object it should display. Store it for later
|
||||||
this.widget_id = params.widget_id;
|
this.widget_id = action.params.widget_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
More complex initialization (DOM manipulations, RPC requests, ...)
|
More complex initialization (DOM manipulations, RPC requests, ...)
|
||||||
|
@ -82,9 +86,6 @@ method.
|
||||||
code it should return a ``$.Deferred`` so callers know when it's
|
code it should return a ``$.Deferred`` so callers know when it's
|
||||||
ready for interaction.
|
ready for interaction.
|
||||||
|
|
||||||
Although generally speaking client actions are not really
|
|
||||||
interacted with.
|
|
||||||
|
|
||||||
.. code-block:: javascript
|
.. code-block:: javascript
|
||||||
|
|
||||||
start: function () {
|
start: function () {
|
||||||
|
@ -93,7 +94,7 @@ method.
|
||||||
// Simply read the res.widget object this action should display
|
// Simply read the res.widget object this action should display
|
||||||
new instance.web.Model('res.widget').call(
|
new instance.web.Model('res.widget').call(
|
||||||
'read', [[this.widget_id], ['title']])
|
'read', [[this.widget_id], ['title']])
|
||||||
.then(this.proxy('on_widget_loaded'));
|
.then(this.proxy('on_widget_loaded'));
|
||||||
}
|
}
|
||||||
|
|
||||||
The client action can then behave exactly as it wishes to within its
|
The client action can then behave exactly as it wishes to within its
|
||||||
|
|
|
@ -11,14 +11,14 @@ Contents:
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
presentation
|
module
|
||||||
async
|
|
||||||
|
|
||||||
testing
|
testing
|
||||||
|
|
||||||
widget
|
widget
|
||||||
qweb
|
|
||||||
|
async
|
||||||
rpc
|
rpc
|
||||||
|
qweb
|
||||||
client_action
|
client_action
|
||||||
form_view
|
form_view
|
||||||
search_view
|
search_view
|
||||||
|
|
|
@ -257,16 +257,17 @@ method you want to call) and a mapping of attributes to values (applied
|
||||||
as keyword arguments on the Python method [#]_). This function fetches
|
as keyword arguments on the Python method [#]_). This function fetches
|
||||||
the return value of the Python methods, converted to JSON.
|
the return value of the Python methods, converted to JSON.
|
||||||
|
|
||||||
For instance, to call the ``eval_domain_and_context`` of the
|
For instance, to call the ``resequence`` of the
|
||||||
:class:`~web.controllers.main.Session` controller:
|
:class:`~web.controllers.main.DataSet` controller:
|
||||||
|
|
||||||
.. code-block:: javascript
|
.. code-block:: javascript
|
||||||
|
|
||||||
openerp.connection.rpc('/web/session/eval_domain_and_context', {
|
openerp.connection.rpc('/web/dataset/resequence', {
|
||||||
domains: ds,
|
model: some_model,
|
||||||
contexts: cs
|
ids: array_of_ids,
|
||||||
|
offset: 42
|
||||||
}).then(function (result) {
|
}).then(function (result) {
|
||||||
// handle result
|
// resequenced on server
|
||||||
});
|
});
|
||||||
|
|
||||||
.. [#] with a small twist: SQLAlchemy's ``orm.query.Query.group_by``
|
.. [#] with a small twist: SQLAlchemy's ``orm.query.Query.group_by``
|
||||||
|
|
|
@ -17,7 +17,6 @@ import tempfile
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import urllib
|
|
||||||
import urlparse
|
import urlparse
|
||||||
import uuid
|
import uuid
|
||||||
import xmlrpclib
|
import xmlrpclib
|
||||||
|
@ -33,7 +32,6 @@ import werkzeug.wsgi
|
||||||
|
|
||||||
import openerp
|
import openerp
|
||||||
|
|
||||||
import nonliterals
|
|
||||||
import session
|
import session
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
@ -95,12 +93,12 @@ class WebRequest(object):
|
||||||
if not self.session:
|
if not self.session:
|
||||||
self.session = session.OpenERPSession()
|
self.session = session.OpenERPSession()
|
||||||
self.httpsession[self.session_id] = self.session
|
self.httpsession[self.session_id] = self.session
|
||||||
self.context = self.params.pop('context', None)
|
self.context = self.params.pop('context', {})
|
||||||
self.debug = self.params.pop('debug', False) is not False
|
self.debug = self.params.pop('debug', False) is not False
|
||||||
# Determine self.lang
|
# Determine self.lang
|
||||||
lang = self.params.get('lang', None)
|
lang = self.params.get('lang', None)
|
||||||
if lang is None:
|
if lang is None:
|
||||||
lang = self.session.eval_context(self.context).get('lang')
|
lang = self.context.get('lang')
|
||||||
if lang is None:
|
if lang is None:
|
||||||
lang = self.httprequest.cookies.get('lang')
|
lang = self.httprequest.cookies.get('lang')
|
||||||
if lang is None:
|
if lang is None:
|
||||||
|
@ -112,6 +110,11 @@ class WebRequest(object):
|
||||||
# we use _ as seprator where RFC2616 uses '-'
|
# we use _ as seprator where RFC2616 uses '-'
|
||||||
self.lang = lang.replace('-', '_')
|
self.lang = lang.replace('-', '_')
|
||||||
|
|
||||||
|
def reject_nonliteral(dct):
|
||||||
|
if '__ref' in dct:
|
||||||
|
raise ValueError(
|
||||||
|
"Non literal contexts can not be sent to the server anymore (%r)" % (dct,))
|
||||||
|
return dct
|
||||||
|
|
||||||
class JsonRequest(WebRequest):
|
class JsonRequest(WebRequest):
|
||||||
""" JSON-RPC2 over HTTP.
|
""" JSON-RPC2 over HTTP.
|
||||||
|
@ -182,9 +185,9 @@ class JsonRequest(WebRequest):
|
||||||
try:
|
try:
|
||||||
# Read POST content or POST Form Data named "request"
|
# Read POST content or POST Form Data named "request"
|
||||||
if requestf:
|
if requestf:
|
||||||
self.jsonrequest = simplejson.load(requestf, object_hook=nonliterals.non_literal_decoder)
|
self.jsonrequest = simplejson.load(requestf, object_hook=reject_nonliteral)
|
||||||
else:
|
else:
|
||||||
self.jsonrequest = simplejson.loads(request, object_hook=nonliterals.non_literal_decoder)
|
self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral)
|
||||||
self.init(self.jsonrequest.get("params", {}))
|
self.init(self.jsonrequest.get("params", {}))
|
||||||
if _logger.isEnabledFor(logging.DEBUG):
|
if _logger.isEnabledFor(logging.DEBUG):
|
||||||
_logger.debug("--> %s.%s\n%s", method.im_class.__name__, method.__name__, pprint.pformat(self.jsonrequest))
|
_logger.debug("--> %s.%s\n%s", method.im_class.__name__, method.__name__, pprint.pformat(self.jsonrequest))
|
||||||
|
@ -233,10 +236,10 @@ class JsonRequest(WebRequest):
|
||||||
# We need then to manage http sessions manually.
|
# We need then to manage http sessions manually.
|
||||||
response['httpsessionid'] = self.httpsession.sid
|
response['httpsessionid'] = self.httpsession.sid
|
||||||
mime = 'application/javascript'
|
mime = 'application/javascript'
|
||||||
body = "%s(%s);" % (jsonp, simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder),)
|
body = "%s(%s);" % (jsonp, simplejson.dumps(response),)
|
||||||
else:
|
else:
|
||||||
mime = 'application/json'
|
mime = 'application/json'
|
||||||
body = simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder)
|
body = simplejson.dumps(response)
|
||||||
|
|
||||||
r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))])
|
r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))])
|
||||||
return r
|
return r
|
||||||
|
@ -402,8 +405,10 @@ def session_context(request, session_store, session_lock, sid):
|
||||||
for k, v in request.session.iteritems():
|
for k, v in request.session.iteritems():
|
||||||
stored = in_store.get(k)
|
stored = in_store.get(k)
|
||||||
if stored and isinstance(v, session.OpenERPSession):
|
if stored and isinstance(v, session.OpenERPSession):
|
||||||
v.contexts_store.update(stored.contexts_store)
|
if hasattr(v, 'contexts_store'):
|
||||||
v.domains_store.update(stored.domains_store)
|
del v.contexts_store
|
||||||
|
if hasattr(v, 'domains_store'):
|
||||||
|
del v.domains_store
|
||||||
if not hasattr(v, 'jsonp_requests'):
|
if not hasattr(v, 'jsonp_requests'):
|
||||||
v.jsonp_requests = {}
|
v.jsonp_requests = {}
|
||||||
v.jsonp_requests.update(getattr(
|
v.jsonp_requests.update(getattr(
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,267 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
""" Manages the storage and lifecycle of non-literal domains and contexts
|
|
||||||
(and potentially other structures) which have to be evaluated with client data,
|
|
||||||
but still need to be safely round-tripped to and from the browser (and thus
|
|
||||||
can't be sent there themselves).
|
|
||||||
"""
|
|
||||||
import binascii
|
|
||||||
import hashlib
|
|
||||||
import simplejson.encoder
|
|
||||||
|
|
||||||
__all__ = ['Domain', 'Context', 'NonLiteralEncoder', 'non_literal_decoder', 'CompoundDomain', 'CompoundContext']
|
|
||||||
|
|
||||||
#: 48 bits should be sufficient to have almost no chance of collision
|
|
||||||
#: with a million hashes, according to hg@67081329d49a
|
|
||||||
SHORT_HASH_BYTES_SIZE = 6
|
|
||||||
|
|
||||||
class NonLiteralEncoder(simplejson.encoder.JSONEncoder):
|
|
||||||
def default(self, object):
|
|
||||||
if not isinstance(object, (BaseDomain, BaseContext)):
|
|
||||||
return super(NonLiteralEncoder, self).default(object)
|
|
||||||
if isinstance(object, Domain):
|
|
||||||
return {
|
|
||||||
'__ref': 'domain',
|
|
||||||
'__id': object.key,
|
|
||||||
'__debug': object.get_domain_string()
|
|
||||||
}
|
|
||||||
elif isinstance(object, Context):
|
|
||||||
return {
|
|
||||||
'__ref': 'context',
|
|
||||||
'__id': object.key,
|
|
||||||
'__debug': object.get_context_string()
|
|
||||||
}
|
|
||||||
elif isinstance(object, CompoundDomain):
|
|
||||||
return {
|
|
||||||
'__ref': 'compound_domain',
|
|
||||||
'__domains': object.domains,
|
|
||||||
'__eval_context': object.get_eval_context()
|
|
||||||
}
|
|
||||||
elif isinstance(object, CompoundContext):
|
|
||||||
return {
|
|
||||||
'__ref': 'compound_context',
|
|
||||||
'__contexts': object.contexts,
|
|
||||||
'__eval_context': object.get_eval_context()
|
|
||||||
}
|
|
||||||
raise TypeError('Could not encode unknown non-literal %s' % object)
|
|
||||||
|
|
||||||
_ALLOWED_KEYS = frozenset(['__ref', "__id", '__domains', '__debug',
|
|
||||||
'__contexts', '__eval_context'])
|
|
||||||
|
|
||||||
def non_literal_decoder(dct):
|
|
||||||
""" Decodes JSON dicts into :class:`Domain` and :class:`Context` based on
|
|
||||||
magic attribute tags.
|
|
||||||
"""
|
|
||||||
if '__ref' in dct:
|
|
||||||
for x in dct:
|
|
||||||
if x not in _ALLOWED_KEYS:
|
|
||||||
raise ValueError("'%s' key not allowed in non literal domain/context" % x)
|
|
||||||
if dct['__ref'] == 'domain':
|
|
||||||
return Domain(None, key=dct['__id'])
|
|
||||||
elif dct['__ref'] == 'context':
|
|
||||||
return Context(None, key=dct['__id'])
|
|
||||||
elif dct["__ref"] == "compound_domain":
|
|
||||||
cdomain = CompoundDomain()
|
|
||||||
for el in dct["__domains"]:
|
|
||||||
cdomain.domains.append(el)
|
|
||||||
cdomain.set_eval_context(dct.get("__eval_context"))
|
|
||||||
return cdomain
|
|
||||||
elif dct["__ref"] == "compound_context":
|
|
||||||
ccontext = CompoundContext()
|
|
||||||
for el in dct["__contexts"]:
|
|
||||||
ccontext.contexts.append(el)
|
|
||||||
ccontext.set_eval_context(dct.get("__eval_context"))
|
|
||||||
return ccontext
|
|
||||||
return dct
|
|
||||||
|
|
||||||
# TODO: use abstract base classes if 2.6+?
|
|
||||||
class BaseDomain(object):
|
|
||||||
def evaluate(self, context=None):
|
|
||||||
raise NotImplementedError('Non literals must implement evaluate()')
|
|
||||||
|
|
||||||
class BaseContext(object):
|
|
||||||
def evaluate(self, context=None):
|
|
||||||
raise NotImplementedError('Non literals must implement evaluate()')
|
|
||||||
|
|
||||||
class Domain(BaseDomain):
|
|
||||||
def __init__(self, session, domain_string=None, key=None):
|
|
||||||
""" Uses session information to store the domain string and map it to a
|
|
||||||
domain key, which can be safely round-tripped to the client.
|
|
||||||
|
|
||||||
If initialized with a domain string, will generate a key for that
|
|
||||||
string and store the domain string out of the way. When initialized
|
|
||||||
with a key, considers this key is a reference to an existing domain
|
|
||||||
string.
|
|
||||||
|
|
||||||
:param session: the OpenERP Session to use when evaluating the domain
|
|
||||||
:type session: web.common.session.OpenERPSession
|
|
||||||
:param str domain_string: a non-literal domain in string form
|
|
||||||
:param str key: key used to retrieve the domain string
|
|
||||||
"""
|
|
||||||
if domain_string and key:
|
|
||||||
raise ValueError("A nonliteral domain can not take both a key "
|
|
||||||
"and a domain string")
|
|
||||||
|
|
||||||
self.session = session
|
|
||||||
if domain_string:
|
|
||||||
self.key = binascii.hexlify(
|
|
||||||
hashlib.sha256(domain_string).digest()[:SHORT_HASH_BYTES_SIZE])
|
|
||||||
self.session.domains_store[self.key] = domain_string
|
|
||||||
elif key:
|
|
||||||
self.key = key
|
|
||||||
|
|
||||||
def get_domain_string(self):
|
|
||||||
""" Retrieves the domain string linked to this non-literal domain in
|
|
||||||
the provided session.
|
|
||||||
"""
|
|
||||||
return self.session.domains_store[self.key]
|
|
||||||
|
|
||||||
def evaluate(self, context=None):
|
|
||||||
""" Forces the evaluation of the linked domain, using the provided
|
|
||||||
context (as well as the session's base context), and returns the
|
|
||||||
evaluated result.
|
|
||||||
"""
|
|
||||||
ctx = self.session.evaluation_context(context)
|
|
||||||
|
|
||||||
try:
|
|
||||||
return eval(self.get_domain_string(), SuperDict(ctx))
|
|
||||||
except NameError as e:
|
|
||||||
raise ValueError('Error during evaluation of this domain: "%s", message: "%s"' % (self.get_domain_string(), e.message))
|
|
||||||
|
|
||||||
class Context(BaseContext):
|
|
||||||
def __init__(self, session, context_string=None, key=None):
|
|
||||||
""" Uses session information to store the context string and map it to
|
|
||||||
a key (stored in a secret location under a secret mountain), which can
|
|
||||||
be safely round-tripped to the client.
|
|
||||||
|
|
||||||
If initialized with a context string, will generate a key for that
|
|
||||||
string and store the context string out of the way. When initialized
|
|
||||||
with a key, considers this key is a reference to an existing context
|
|
||||||
string.
|
|
||||||
|
|
||||||
:param session: the OpenERP Session to use when evaluating the context
|
|
||||||
:type session: web.common.session.OpenERPSession
|
|
||||||
:param str context_string: a non-literal context in string form
|
|
||||||
:param str key: key used to retrieve the context string
|
|
||||||
"""
|
|
||||||
if context_string and key:
|
|
||||||
raise ValueError("A nonliteral domain can not take both a key "
|
|
||||||
"and a domain string")
|
|
||||||
|
|
||||||
self.session = session
|
|
||||||
|
|
||||||
if context_string:
|
|
||||||
self.key = binascii.hexlify(
|
|
||||||
hashlib.sha256(context_string).digest()[:SHORT_HASH_BYTES_SIZE])
|
|
||||||
self.session.contexts_store[self.key] = context_string
|
|
||||||
elif key:
|
|
||||||
self.key = key
|
|
||||||
|
|
||||||
def get_context_string(self):
|
|
||||||
""" Retrieves the context string linked to this non-literal context in
|
|
||||||
the provided session.
|
|
||||||
"""
|
|
||||||
return self.session.contexts_store[self.key]
|
|
||||||
|
|
||||||
def evaluate(self, context=None):
|
|
||||||
""" Forces the evaluation of the linked context, using the provided
|
|
||||||
context (as well as the session's base context), and returns the
|
|
||||||
evaluated result.
|
|
||||||
"""
|
|
||||||
ctx = self.session.evaluation_context(context)
|
|
||||||
|
|
||||||
try:
|
|
||||||
return eval(self.get_context_string(), SuperDict(ctx))
|
|
||||||
except NameError as e:
|
|
||||||
raise ValueError('Error during evaluation of this context: "%s", message: "%s"' % (self.get_context_string(), e.message))
|
|
||||||
|
|
||||||
class SuperDict(dict):
|
|
||||||
def __getattr__(self, name):
|
|
||||||
try:
|
|
||||||
return self[name]
|
|
||||||
except KeyError:
|
|
||||||
raise AttributeError(name)
|
|
||||||
def __getitem__(self, key):
|
|
||||||
tmp = super(SuperDict, self).__getitem__(key)
|
|
||||||
if isinstance(tmp, dict):
|
|
||||||
return SuperDict(tmp)
|
|
||||||
return tmp
|
|
||||||
|
|
||||||
class CompoundDomain(BaseDomain):
|
|
||||||
def __init__(self, *domains):
|
|
||||||
self.domains = []
|
|
||||||
self.session = None
|
|
||||||
self.eval_context = None
|
|
||||||
for domain in domains:
|
|
||||||
self.add(domain)
|
|
||||||
|
|
||||||
def evaluate(self, context=None):
|
|
||||||
ctx = dict(context or {})
|
|
||||||
eval_context = self.get_eval_context()
|
|
||||||
if eval_context:
|
|
||||||
eval_context = self.session.eval_context(eval_context)
|
|
||||||
ctx.update(eval_context)
|
|
||||||
final_domain = []
|
|
||||||
for domain in self.domains:
|
|
||||||
if not isinstance(domain, (list, BaseDomain)):
|
|
||||||
raise TypeError(
|
|
||||||
"Domain %r is not a list or a nonliteral Domain" % domain)
|
|
||||||
|
|
||||||
if isinstance(domain, list):
|
|
||||||
final_domain.extend(domain)
|
|
||||||
continue
|
|
||||||
|
|
||||||
domain.session = self.session
|
|
||||||
final_domain.extend(domain.evaluate(ctx))
|
|
||||||
return final_domain
|
|
||||||
|
|
||||||
def add(self, domain):
|
|
||||||
self.domains.append(domain)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def set_eval_context(self, eval_context):
|
|
||||||
self.eval_context = eval_context
|
|
||||||
return self
|
|
||||||
|
|
||||||
def get_eval_context(self):
|
|
||||||
return self.eval_context
|
|
||||||
|
|
||||||
class CompoundContext(BaseContext):
|
|
||||||
def __init__(self, *contexts):
|
|
||||||
self.contexts = []
|
|
||||||
self.eval_context = None
|
|
||||||
self.session = None
|
|
||||||
for context in contexts:
|
|
||||||
self.add(context)
|
|
||||||
|
|
||||||
def evaluate(self, context=None):
|
|
||||||
ctx = dict(context or {})
|
|
||||||
eval_context = self.get_eval_context()
|
|
||||||
if eval_context:
|
|
||||||
eval_context = self.session.eval_context(eval_context)
|
|
||||||
ctx.update(eval_context)
|
|
||||||
final_context = {}
|
|
||||||
for context_to_eval in self.contexts:
|
|
||||||
if not isinstance(context_to_eval, (dict, BaseContext)):
|
|
||||||
raise TypeError(
|
|
||||||
"Context %r is not a dict or a nonliteral Context" % context_to_eval)
|
|
||||||
|
|
||||||
if isinstance(context_to_eval, dict):
|
|
||||||
final_context.update(context_to_eval)
|
|
||||||
continue
|
|
||||||
|
|
||||||
ctx.update(final_context)
|
|
||||||
|
|
||||||
context_to_eval.session = self.session
|
|
||||||
final_context.update(context_to_eval.evaluate(ctx))
|
|
||||||
return final_context
|
|
||||||
|
|
||||||
def add(self, context):
|
|
||||||
self.contexts.append(context)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def set_eval_context(self, eval_context):
|
|
||||||
self.eval_context = eval_context
|
|
||||||
return self
|
|
||||||
|
|
||||||
def get_eval_context(self):
|
|
||||||
return self.eval_context
|
|
|
@ -10,8 +10,6 @@ import xmlrpclib
|
||||||
|
|
||||||
import openerp
|
import openerp
|
||||||
|
|
||||||
import nonliterals
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
|
@ -81,8 +79,6 @@ class OpenERPSession(object):
|
||||||
self._password = False
|
self._password = False
|
||||||
self._suicide = False
|
self._suicide = False
|
||||||
self.context = {}
|
self.context = {}
|
||||||
self.contexts_store = {}
|
|
||||||
self.domains_store = {}
|
|
||||||
self.jsonp_requests = {} # FIXME use a LRU
|
self.jsonp_requests = {} # FIXME use a LRU
|
||||||
|
|
||||||
def send(self, service_name, method, *args):
|
def send(self, service_name, method, *args):
|
||||||
|
@ -192,79 +188,4 @@ class OpenERPSession(object):
|
||||||
|
|
||||||
context['lang'] = lang
|
context['lang'] = lang
|
||||||
|
|
||||||
@property
|
|
||||||
def base_eval_context(self):
|
|
||||||
""" Default evaluation context for the session.
|
|
||||||
|
|
||||||
Used to evaluate contexts and domains.
|
|
||||||
"""
|
|
||||||
base = dict(
|
|
||||||
uid=self._uid,
|
|
||||||
current_date=datetime.date.today().strftime('%Y-%m-%d'),
|
|
||||||
time=time,
|
|
||||||
datetime=datetime,
|
|
||||||
relativedelta=dateutil.relativedelta.relativedelta
|
|
||||||
)
|
|
||||||
base.update(self.context)
|
|
||||||
return base
|
|
||||||
|
|
||||||
def evaluation_context(self, context=None):
|
|
||||||
""" Returns the session's evaluation context, augmented with the
|
|
||||||
provided context if any.
|
|
||||||
|
|
||||||
:param dict context: to add merge in the session's base eval context
|
|
||||||
:returns: the augmented context
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
d = dict(self.base_eval_context)
|
|
||||||
if context:
|
|
||||||
d.update(context)
|
|
||||||
d['context'] = d
|
|
||||||
return d
|
|
||||||
|
|
||||||
def eval_context(self, context_to_eval, context=None):
|
|
||||||
""" Evaluates the provided context_to_eval in the context (haha) of
|
|
||||||
the context. Also merges the evaluated context with the session's context.
|
|
||||||
|
|
||||||
:param context_to_eval: a context to evaluate. Must be a dict or a
|
|
||||||
non-literal context. If it's a dict, will be
|
|
||||||
returned as-is
|
|
||||||
:type context_to_eval: openerpweb.nonliterals.Context
|
|
||||||
:returns: the evaluated context
|
|
||||||
:rtype: dict
|
|
||||||
|
|
||||||
:raises: ``TypeError`` if ``context_to_eval`` is neither a dict nor
|
|
||||||
a Context
|
|
||||||
"""
|
|
||||||
ctx = dict(
|
|
||||||
self.base_eval_context,
|
|
||||||
**(context or {}))
|
|
||||||
|
|
||||||
# adding the context of the session to send to the openerp server
|
|
||||||
ccontext = nonliterals.CompoundContext(self.context, context_to_eval or {})
|
|
||||||
ccontext.session = self
|
|
||||||
return ccontext.evaluate(ctx)
|
|
||||||
|
|
||||||
def eval_domain(self, domain, context=None):
|
|
||||||
""" Evaluates the provided domain using the provided context
|
|
||||||
(merged with the session's evaluation context)
|
|
||||||
|
|
||||||
:param domain: an OpenERP domain as a list or as a
|
|
||||||
:class:`openerpweb.nonliterals.Domain` instance
|
|
||||||
|
|
||||||
In the second case, it will be evaluated and returned.
|
|
||||||
:type domain: openerpweb.nonliterals.Domain
|
|
||||||
:param dict context: the context to use in the evaluation, if any.
|
|
||||||
:returns: the evaluated domain
|
|
||||||
:rtype: list
|
|
||||||
|
|
||||||
:raises: ``TypeError`` if ``domain`` is neither a list nor a Domain
|
|
||||||
"""
|
|
||||||
if isinstance(domain, list):
|
|
||||||
return domain
|
|
||||||
|
|
||||||
cdomain = nonliterals.CompoundDomain(domain)
|
|
||||||
cdomain.session = self
|
|
||||||
return cdomain.evaluate(context or {})
|
|
||||||
|
|
||||||
# vim:et:ts=4:sw=4:
|
# vim:et:ts=4:sw=4:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
repo: 076b192d0d8ab2b92d1dbcfa3da055382f30eaea
|
repo: 076b192d0d8ab2b92d1dbcfa3da055382f30eaea
|
||||||
node: 1758bfec1ec1dcff95dcc4c72269cc0e3d000afd
|
node: 142c22b230636674a0cee6bc29e6975f0f1600a5
|
||||||
branch: default
|
branch: default
|
||||||
latesttag: 0.5
|
latesttag: 0.7
|
||||||
latesttagdistance: 11
|
latesttagdistance: 9
|
||||||
|
|
|
@ -1,14 +1,7 @@
|
||||||
What
|
What
|
||||||
====
|
====
|
||||||
|
|
||||||
``py.js`` is a parser and evaluator of Python expressions, written in
|
|
||||||
pure javascript.
|
|
||||||
|
|
||||||
``py.js`` is not intended to implement a full Python interpreter
|
|
||||||
(although it could be used for such an effort later on), its
|
|
||||||
specification document is the `Python 2.7 Expressions spec
|
|
||||||
<http://docs.python.org/reference/expressions.html>`_ (along with the
|
|
||||||
lexical analysis part).
|
|
||||||
|
|
||||||
Syntax
|
Syntax
|
||||||
------
|
------
|
||||||
|
@ -60,7 +53,7 @@ Builtins
|
||||||
Same as tuple (``list`` is currently an alias for ``tuple``)
|
Same as tuple (``list`` is currently an alias for ``tuple``)
|
||||||
|
|
||||||
``dict``
|
``dict``
|
||||||
Implements just about nothing
|
Implements trivial getting and setting, nothing beyond that.
|
||||||
|
|
||||||
Note that most methods are probably missing from all of these.
|
Note that most methods are probably missing from all of these.
|
||||||
|
|
||||||
|
@ -69,17 +62,17 @@ Data model protocols
|
||||||
|
|
||||||
``py.js`` currently implements the following protocols (or
|
``py.js`` currently implements the following protocols (or
|
||||||
sub-protocols) of the `Python 2.7 data model
|
sub-protocols) of the `Python 2.7 data model
|
||||||
<http://docs.python.org/reference/datamodel.html>`_:
|
<>`_:
|
||||||
|
|
||||||
Rich comparisons
|
Rich comparisons
|
||||||
Roughly complete implementation but for two limits: ``__eq__`` and
|
Pretty much complete (including operator fallbacks), although the
|
||||||
``__ne__`` can't return ``NotImplemented`` (well they can but it's
|
behavior is currently undefined if an operation does not return
|
||||||
not going to work right), and the behavior is undefined if a
|
either a ``py.bool`` or ``NotImplemented``.
|
||||||
rich-comparison operation does not return a ``py.bool``.
|
|
||||||
|
|
||||||
Also, a ``NotImplemented`` result does not try the reverse
|
``__hash__`` is supported (and used), but it should return **a
|
||||||
operation, not sure if it's supposed to. It directly falls back to
|
javascript string**. ``py.js``'s dict build on javascript objects,
|
||||||
comparing type names.
|
reimplementing numeral hashing is worthless complexity at this
|
||||||
|
point.
|
||||||
|
|
||||||
Boolean conversion
|
Boolean conversion
|
||||||
Implementing ``__nonzero__`` should work.
|
Implementing ``__nonzero__`` should work.
|
||||||
|
@ -93,6 +86,12 @@ Descriptor protocol
|
||||||
As with attributes, ``__delete__`` is not implemented.
|
As with attributes, ``__delete__`` is not implemented.
|
||||||
|
|
||||||
Callable objects
|
Callable objects
|
||||||
|
Work, although the handling of arguments isn't exactly nailed
|
||||||
|
down. For now, callables get two (javascript) arguments ``args``
|
||||||
|
and ``kwargs``, holding (respectively) positional and keyword
|
||||||
|
arguments.
|
||||||
|
|
||||||
|
Conflicts are *not* handled at this point.
|
||||||
|
|
||||||
Collections Abstract Base Classes
|
Collections Abstract Base Classes
|
||||||
Container is the only implemented ABC protocol (ABCs themselves
|
Container is the only implemented ABC protocol (ABCs themselves
|
||||||
|
@ -119,8 +118,8 @@ implementation:
|
||||||
``py.js`` types.
|
``py.js`` types.
|
||||||
|
|
||||||
When accessing instance methods, ``py.js`` automatically wraps
|
When accessing instance methods, ``py.js`` automatically wraps
|
||||||
these in a variant of ``py.def`` automatically, to behave as
|
these in a variant of ``py.def``, to behave as Python's (bound)
|
||||||
Python's (bound) methods.
|
methods.
|
||||||
|
|
||||||
Why
|
Why
|
||||||
===
|
===
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
# Makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line.
|
||||||
|
SPHINXOPTS =
|
||||||
|
SPHINXBUILD = sphinx-build
|
||||||
|
PAPER =
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
# Internal variables.
|
||||||
|
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||||
|
PAPEROPT_letter = -D latex_paper_size=letter
|
||||||
|
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
|
# the i18n builder cannot share the environment and doctrees with the others
|
||||||
|
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
|
|
||||||
|
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Please use \`make <target>' where <target> is one of"
|
||||||
|
@echo " html to make standalone HTML files"
|
||||||
|
@echo " dirhtml to make HTML files named index.html in directories"
|
||||||
|
@echo " singlehtml to make a single large HTML file"
|
||||||
|
@echo " pickle to make pickle files"
|
||||||
|
@echo " json to make JSON files"
|
||||||
|
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||||
|
@echo " qthelp to make HTML files and a qthelp project"
|
||||||
|
@echo " devhelp to make HTML files and a Devhelp project"
|
||||||
|
@echo " epub to make an epub"
|
||||||
|
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||||
|
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||||
|
@echo " text to make text files"
|
||||||
|
@echo " man to make manual pages"
|
||||||
|
@echo " texinfo to make Texinfo files"
|
||||||
|
@echo " info to make Texinfo files and run them through makeinfo"
|
||||||
|
@echo " gettext to make PO message catalogs"
|
||||||
|
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||||
|
@echo " linkcheck to check all external links for integrity"
|
||||||
|
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
-rm -rf $(BUILDDIR)/*
|
||||||
|
|
||||||
|
html:
|
||||||
|
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||||
|
|
||||||
|
dirhtml:
|
||||||
|
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||||
|
|
||||||
|
singlehtml:
|
||||||
|
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||||
|
|
||||||
|
pickle:
|
||||||
|
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the pickle files."
|
||||||
|
|
||||||
|
json:
|
||||||
|
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the JSON files."
|
||||||
|
|
||||||
|
htmlhelp:
|
||||||
|
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||||
|
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||||
|
|
||||||
|
qthelp:
|
||||||
|
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||||
|
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||||
|
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyjs.qhcp"
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyjs.qhc"
|
||||||
|
|
||||||
|
devhelp:
|
||||||
|
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished."
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# mkdir -p $$HOME/.local/share/devhelp/pyjs"
|
||||||
|
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyjs"
|
||||||
|
@echo "# devhelp"
|
||||||
|
|
||||||
|
epub:
|
||||||
|
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||||
|
|
||||||
|
latex:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||||
|
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||||
|
"(use \`make latexpdf' here to do that automatically)."
|
||||||
|
|
||||||
|
latexpdf:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo "Running LaTeX files through pdflatex..."
|
||||||
|
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||||
|
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||||
|
|
||||||
|
text:
|
||||||
|
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||||
|
|
||||||
|
man:
|
||||||
|
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||||
|
|
||||||
|
texinfo:
|
||||||
|
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||||
|
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||||
|
"(use \`make info' here to do that automatically)."
|
||||||
|
|
||||||
|
info:
|
||||||
|
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
|
@echo "Running Texinfo files through makeinfo..."
|
||||||
|
make -C $(BUILDDIR)/texinfo info
|
||||||
|
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||||
|
|
||||||
|
gettext:
|
||||||
|
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||||
|
|
||||||
|
changes:
|
||||||
|
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||||
|
@echo
|
||||||
|
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||||
|
|
||||||
|
linkcheck:
|
||||||
|
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||||
|
@echo
|
||||||
|
@echo "Link check complete; look for any errors in the above output " \
|
||||||
|
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||||
|
|
||||||
|
doctest:
|
||||||
|
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||||
|
@echo "Testing of doctests in the sources finished, look at the " \
|
||||||
|
"results in $(BUILDDIR)/doctest/output.txt."
|
|
@ -0,0 +1,55 @@
|
||||||
|
.. default-domain: python
|
||||||
|
|
||||||
|
.. _builtins:
|
||||||
|
|
||||||
|
Supported Python builtins
|
||||||
|
=========================
|
||||||
|
|
||||||
|
.. function:: py.type(object)
|
||||||
|
|
||||||
|
Gets the class of a provided object, if possible.
|
||||||
|
|
||||||
|
.. note:: currently doesn't work correctly when called on a class
|
||||||
|
object, will return the class itself (also, classes
|
||||||
|
don't currently have a type).
|
||||||
|
|
||||||
|
.. js:function:: py.type(name, bases, dict)
|
||||||
|
|
||||||
|
Not exactly a builtin as this form is solely javascript-level
|
||||||
|
(currently). Used to create new ``py.js`` types. See :doc:`types`
|
||||||
|
for its usage.
|
||||||
|
|
||||||
|
.. data:: py.None
|
||||||
|
|
||||||
|
.. data:: py.True
|
||||||
|
|
||||||
|
.. data:: py.False
|
||||||
|
|
||||||
|
.. data:: py.NotImplemented
|
||||||
|
|
||||||
|
.. class:: py.object
|
||||||
|
|
||||||
|
Base class for all types, even implicitly (if no bases are
|
||||||
|
provided to :js:func:`py.type`)
|
||||||
|
|
||||||
|
.. class:: py.bool([object])
|
||||||
|
|
||||||
|
.. class:: py.float([object])
|
||||||
|
|
||||||
|
.. class:: py.str([object])
|
||||||
|
|
||||||
|
.. class:: py.unicode([object])
|
||||||
|
|
||||||
|
.. class:: py.tuple()
|
||||||
|
|
||||||
|
.. class:: py.list()
|
||||||
|
|
||||||
|
.. class:: py.dict()
|
||||||
|
|
||||||
|
.. function:: py.len(object)
|
||||||
|
|
||||||
|
.. function:: py.isinstance(object, type)
|
||||||
|
|
||||||
|
.. function:: py.issubclass(type, other_type)
|
||||||
|
|
||||||
|
.. class:: py.classmethod
|
|
@ -0,0 +1,247 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# py.js documentation build configuration file, created by
|
||||||
|
# sphinx-quickstart on Sun Sep 9 19:36:23 2012.
|
||||||
|
#
|
||||||
|
# This file is execfile()d with the current directory set to its containing dir.
|
||||||
|
#
|
||||||
|
# Note that not all possible configuration values are present in this
|
||||||
|
# autogenerated file.
|
||||||
|
#
|
||||||
|
# All configuration values have a default; values that are commented out
|
||||||
|
# serve to show the default.
|
||||||
|
|
||||||
|
import sys, os
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
#sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
|
||||||
|
# -- General configuration -----------------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
#needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||||
|
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||||
|
extensions = ['sphinx.ext.todo']
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
# The suffix of source filenames.
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The encoding of source files.
|
||||||
|
#source_encoding = 'utf-8-sig'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = u'py.js'
|
||||||
|
copyright = u'2012, Xavier Morel'
|
||||||
|
|
||||||
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
# |version| and |release|, also used in various other places throughout the
|
||||||
|
# built documents.
|
||||||
|
#
|
||||||
|
# The short X.Y version.
|
||||||
|
version = '0.6'
|
||||||
|
# The full version, including alpha/beta/rc tags.
|
||||||
|
release = '0.6'
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#language = None
|
||||||
|
|
||||||
|
# There are two options for replacing |today|: either, you set today to some
|
||||||
|
# non-false value, then it is used:
|
||||||
|
#today = ''
|
||||||
|
# Else, today_fmt is used as the format for a strftime call.
|
||||||
|
#today_fmt = '%B %d, %Y'
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
exclude_patterns = ['_build']
|
||||||
|
|
||||||
|
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||||
|
#default_role = None
|
||||||
|
|
||||||
|
# Default sphinx domain
|
||||||
|
default_domain = 'js'
|
||||||
|
|
||||||
|
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||||
|
#add_function_parentheses = True
|
||||||
|
|
||||||
|
# If true, the current module name will be prepended to all description
|
||||||
|
# unit titles (such as .. function::).
|
||||||
|
#add_module_names = True
|
||||||
|
|
||||||
|
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||||
|
# output. They are ignored by default.
|
||||||
|
#show_authors = False
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = 'sphinx'
|
||||||
|
# default code-block highlighting
|
||||||
|
highlight_language = 'javascript'
|
||||||
|
|
||||||
|
# A list of ignored prefixes for module index sorting.
|
||||||
|
#modindex_common_prefix = []
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output ---------------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
html_theme = 'default'
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
|
# further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
#html_theme_options = {}
|
||||||
|
|
||||||
|
# Add any paths that contain custom themes here, relative to this directory.
|
||||||
|
#html_theme_path = []
|
||||||
|
|
||||||
|
# The name for this set of Sphinx documents. If None, it defaults to
|
||||||
|
# "<project> v<release> documentation".
|
||||||
|
#html_title = None
|
||||||
|
|
||||||
|
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||||
|
#html_short_title = None
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top
|
||||||
|
# of the sidebar.
|
||||||
|
#html_logo = None
|
||||||
|
|
||||||
|
# The name of an image file (within the static path) to use as favicon of the
|
||||||
|
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||||
|
# pixels large.
|
||||||
|
#html_favicon = None
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ['_static']
|
||||||
|
|
||||||
|
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||||
|
# using the given strftime format.
|
||||||
|
#html_last_updated_fmt = '%b %d, %Y'
|
||||||
|
|
||||||
|
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||||
|
# typographically correct entities.
|
||||||
|
#html_use_smartypants = True
|
||||||
|
|
||||||
|
# Custom sidebar templates, maps document names to template names.
|
||||||
|
#html_sidebars = {}
|
||||||
|
|
||||||
|
# Additional templates that should be rendered to pages, maps page names to
|
||||||
|
# template names.
|
||||||
|
#html_additional_pages = {}
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#html_domain_indices = True
|
||||||
|
|
||||||
|
# If false, no index is generated.
|
||||||
|
#html_use_index = True
|
||||||
|
|
||||||
|
# If true, the index is split into individual pages for each letter.
|
||||||
|
#html_split_index = False
|
||||||
|
|
||||||
|
# If true, links to the reST sources are added to the pages.
|
||||||
|
#html_show_sourcelink = True
|
||||||
|
|
||||||
|
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||||
|
#html_show_sphinx = True
|
||||||
|
|
||||||
|
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||||
|
#html_show_copyright = True
|
||||||
|
|
||||||
|
# If true, an OpenSearch description file will be output, and all pages will
|
||||||
|
# contain a <link> tag referring to it. The value of this option must be the
|
||||||
|
# base URL from which the finished HTML is served.
|
||||||
|
#html_use_opensearch = ''
|
||||||
|
|
||||||
|
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||||
|
#html_file_suffix = None
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'pyjsdoc'
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for LaTeX output --------------------------------------------------
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
|
#'papersize': 'letterpaper',
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#'pointsize': '10pt',
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#'preamble': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||||
|
latex_documents = [
|
||||||
|
('index', 'pyjs.tex', u'py.js Documentation',
|
||||||
|
u'Xavier Morel', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top of
|
||||||
|
# the title page.
|
||||||
|
#latex_logo = None
|
||||||
|
|
||||||
|
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||||
|
# not chapters.
|
||||||
|
#latex_use_parts = False
|
||||||
|
|
||||||
|
# If true, show page references after internal links.
|
||||||
|
#latex_show_pagerefs = False
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
#latex_show_urls = False
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
#latex_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#latex_domain_indices = True
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for manual page output --------------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [
|
||||||
|
('index', 'pyjs', u'py.js Documentation',
|
||||||
|
[u'Xavier Morel'], 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
#man_show_urls = False
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Texinfo output ------------------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree into Texinfo files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# dir menu entry, description, category)
|
||||||
|
texinfo_documents = [
|
||||||
|
('index', 'pyjs', u'py.js Documentation',
|
||||||
|
u'Xavier Morel', 'pyjs', 'One line description of project.',
|
||||||
|
'Miscellaneous'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
#texinfo_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#texinfo_domain_indices = True
|
||||||
|
|
||||||
|
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||||
|
#texinfo_show_urls = 'footnote'
|
|
@ -0,0 +1,64 @@
|
||||||
|
Differences with Python
|
||||||
|
=======================
|
||||||
|
|
||||||
|
* ``py.js`` completely ignores old-style classes as well as their
|
||||||
|
lookup details. All ``py.js`` types should be considered matching
|
||||||
|
the behavior of new-style classes
|
||||||
|
|
||||||
|
* New types can only have a single base. This is due to ``py.js``
|
||||||
|
implementing its types on top of Javascript's, and javascript being
|
||||||
|
a single-inheritance language.
|
||||||
|
|
||||||
|
This may change if ``py.js`` ever reimplements its object model from
|
||||||
|
scratch.
|
||||||
|
|
||||||
|
* Piggybacking on javascript's object model also means metaclasses are
|
||||||
|
not available (:js:func:`py.type` is a function)
|
||||||
|
|
||||||
|
* A python-level function (created through :js:class:`py.PY_def`) set
|
||||||
|
on a new type will not become a method, it'll remain a function.
|
||||||
|
|
||||||
|
* :js:func:`py.PY_parseArgs` supports keyword-only arguments (though
|
||||||
|
it's a Python 3 feature)
|
||||||
|
|
||||||
|
* Because the underlying type is a javascript ``String``, there
|
||||||
|
currently is no difference between :js:class:`py.str` and
|
||||||
|
:js:class:`py.unicode`. As a result, there also is no difference
|
||||||
|
between :js:func:`__str__` and :js:func:`__unicode__`.
|
||||||
|
|
||||||
|
Unsupported features
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
These are Python features which are not supported at all in ``py.js``,
|
||||||
|
usually because they don't make sense or there is no way to support them
|
||||||
|
|
||||||
|
* The ``__delattr__``, ``__delete__`` and ``__delitem__``: as
|
||||||
|
``py.js`` only handles expressions and these are accessed via the
|
||||||
|
``del`` statement, there would be no way to call them.
|
||||||
|
|
||||||
|
* ``__del__`` the lack of cross-platform GC hook means there is no way
|
||||||
|
to know when an object is deallocated.
|
||||||
|
|
||||||
|
* ``__slots__`` are not handled
|
||||||
|
|
||||||
|
* Dedicated (and deprecated) slicing special methods are unsupported
|
||||||
|
|
||||||
|
Missing features
|
||||||
|
----------------
|
||||||
|
|
||||||
|
These are Python features which are missing because they haven't been
|
||||||
|
implemented yet:
|
||||||
|
|
||||||
|
* Class-binding of descriptors doesn't currently work.
|
||||||
|
|
||||||
|
* Instance and subclass checks can't be customized
|
||||||
|
|
||||||
|
* "poor" comparison methods (``__cmp__`` and ``__rcmp__``) are not
|
||||||
|
supported and won't be falled-back to.
|
||||||
|
|
||||||
|
* ``__coerce__`` is currently supported
|
||||||
|
|
||||||
|
* Context managers are not currently supported
|
||||||
|
|
||||||
|
* Unbound methods are not supported, instance methods can only be
|
||||||
|
accessed from instances.
|
|
@ -0,0 +1,161 @@
|
||||||
|
.. py.js documentation master file, created by
|
||||||
|
sphinx-quickstart on Sun Sep 9 19:36:23 2012.
|
||||||
|
You can adapt this file completely to your liking, but it should at least
|
||||||
|
contain the root `toctree` directive.
|
||||||
|
|
||||||
|
py.js, a Python expressions parser and evaluator
|
||||||
|
================================================
|
||||||
|
|
||||||
|
``py.js`` is a parser and evaluator of Python expressions, written in
|
||||||
|
pure javascript.
|
||||||
|
|
||||||
|
``py.js`` is not intended to implement a full Python interpreter, its
|
||||||
|
specification document is the `Python 2.7 Expressions spec
|
||||||
|
<http://docs.python.org/reference/expressions.html>`_ (along with the
|
||||||
|
lexical analysis part) as well as the Python builtins.
|
||||||
|
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
builtins
|
||||||
|
types
|
||||||
|
utility
|
||||||
|
differences
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
To evaluate a Python expression, simply call
|
||||||
|
:func:`py.eval`. :func:`py.eval` takes a mandatory Python expression
|
||||||
|
parameter, as a string, and an optional evaluation context (namespace
|
||||||
|
for the expression's free variables), and returns a javascript value::
|
||||||
|
|
||||||
|
> py.eval("t in ('a', 'b', 'c') and foo", {t: 'c', foo: true});
|
||||||
|
true
|
||||||
|
|
||||||
|
If the expression needs to be repeatedly evaluated, or the result of
|
||||||
|
the expression is needed in its "python" form without being converted
|
||||||
|
back to javascript, you can use the underlying triplet of functions
|
||||||
|
:func:`py.tokenize`, :func:`py.parse` and :func:`py.evaluate`
|
||||||
|
directly.
|
||||||
|
|
||||||
|
API
|
||||||
|
---
|
||||||
|
|
||||||
|
Core functions
|
||||||
|
++++++++++++++
|
||||||
|
|
||||||
|
.. function:: py.eval(expr[, context])
|
||||||
|
|
||||||
|
"Do everything" function, to use for one-shot evaluation of Python
|
||||||
|
expressions. Chains tokenizing, parsing and evaluating the
|
||||||
|
expression then :ref:`converts the result back to javascript
|
||||||
|
<convert-js>`
|
||||||
|
|
||||||
|
:param expr: Python expression to evaluate
|
||||||
|
:type expr: String
|
||||||
|
:param context: evaluation context for the expression's free
|
||||||
|
variables
|
||||||
|
:type context: Object
|
||||||
|
:returns: the expression's result, converted back to javascript
|
||||||
|
|
||||||
|
.. function:: py.tokenize(expr)
|
||||||
|
|
||||||
|
Expression tokenizer
|
||||||
|
|
||||||
|
:param expr: Python expression to tokenize
|
||||||
|
:type expr: String
|
||||||
|
:returns: token stream
|
||||||
|
|
||||||
|
.. function:: py.parse(tokens)
|
||||||
|
|
||||||
|
Parses a token stream and returns the corresponding parse tree.
|
||||||
|
|
||||||
|
The parse tree is stateless and can be memoized and reused for
|
||||||
|
frequently evaluated expressions.
|
||||||
|
|
||||||
|
:param tokens: token stream from :func:`py.tokenize`
|
||||||
|
:returns: parse tree
|
||||||
|
|
||||||
|
.. function:: py.evaluate(tree[, context])
|
||||||
|
|
||||||
|
Evaluates the expression represented by the provided parse tree,
|
||||||
|
using the provided context for the exprssion's free variables.
|
||||||
|
|
||||||
|
:param tree: parse tree returned by :func:`py.parse`
|
||||||
|
:param context: evaluation context
|
||||||
|
:returns: the "python object" resulting from the expression's
|
||||||
|
evaluation
|
||||||
|
:rtype: :class:`py.object`
|
||||||
|
|
||||||
|
.. _convert-py:
|
||||||
|
|
||||||
|
Conversions from Javascript to Python
|
||||||
|
+++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
``py.js`` will automatically attempt to convert non-:class:`py.object`
|
||||||
|
values into their ``py.js`` equivalent in the following situations:
|
||||||
|
|
||||||
|
* Values passed through the context of :func:`py.eval` or
|
||||||
|
:func:`py.evaluate`
|
||||||
|
|
||||||
|
* Attributes accessed directly on objects
|
||||||
|
|
||||||
|
* Values of mappings passed to :class:`py.dict`
|
||||||
|
|
||||||
|
Notably, ``py.js`` will *not* attempt an automatic conversion of
|
||||||
|
values returned by functions or methods, these must be
|
||||||
|
:class:`py.object` instances.
|
||||||
|
|
||||||
|
The automatic conversions performed by ``py.js`` are the following:
|
||||||
|
|
||||||
|
* ``null`` is converted to :data:`py.None`
|
||||||
|
|
||||||
|
* ``true`` is converted to :data:`py.True`
|
||||||
|
|
||||||
|
* ``false`` is converted to :data:`py.False`
|
||||||
|
|
||||||
|
* numbers are converted to :class:`py.float`
|
||||||
|
|
||||||
|
* strings are converted to :class:`py.str`
|
||||||
|
|
||||||
|
* functions are wrapped into :class:`py.PY_dev`
|
||||||
|
|
||||||
|
* ``Array`` instances are converted to :class:`py.list`
|
||||||
|
|
||||||
|
The rest generates an error, except for ``undefined`` which
|
||||||
|
specifically generates a ``NameError``.
|
||||||
|
|
||||||
|
.. _convert-js:
|
||||||
|
|
||||||
|
Conversions from Python to Javascript
|
||||||
|
+++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
py.js types (extensions of :js:class:`py.object`) can be converted
|
||||||
|
back to javascript by calling their :js:func:`py.object.toJSON`
|
||||||
|
method.
|
||||||
|
|
||||||
|
The default implementation raises an error, as arbitrary objects can
|
||||||
|
not be converted back to javascript.
|
||||||
|
|
||||||
|
Most built-in objects provide a :js:func:`py.object.toJSON`
|
||||||
|
implementation out of the box.
|
||||||
|
|
||||||
|
Javascript-level exceptions
|
||||||
|
+++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
Javascript allows throwing arbitrary things, but runtimes don't seem
|
||||||
|
to provide any useful information (when they ever do) if what is
|
||||||
|
thrown isn't a direct instance of ``Error``. As a result, while
|
||||||
|
``py.js`` tries to match the exception-throwing semantics of Python it
|
||||||
|
only ever throws bare ``Error`` at the javascript-level. Instead, it
|
||||||
|
prefixes the error message with the name of the Python expression, a
|
||||||
|
colon, a space, and the actual message.
|
||||||
|
|
||||||
|
For instance, where Python would throw ``KeyError("'foo'")`` when
|
||||||
|
accessing an invalid key on a ``dict``, ``py.js`` will throw
|
||||||
|
``Error("KeyError: 'foo'")``.
|
||||||
|
|
||||||
|
.. _Python Data Model: http://docs.python.org/reference/datamodel.html
|
||||||
|
|
|
@ -0,0 +1,190 @@
|
||||||
|
@ECHO OFF
|
||||||
|
|
||||||
|
REM Command file for Sphinx documentation
|
||||||
|
|
||||||
|
if "%SPHINXBUILD%" == "" (
|
||||||
|
set SPHINXBUILD=sphinx-build
|
||||||
|
)
|
||||||
|
set BUILDDIR=_build
|
||||||
|
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
||||||
|
set I18NSPHINXOPTS=%SPHINXOPTS% .
|
||||||
|
if NOT "%PAPER%" == "" (
|
||||||
|
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||||
|
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "" goto help
|
||||||
|
|
||||||
|
if "%1" == "help" (
|
||||||
|
:help
|
||||||
|
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||||
|
echo. html to make standalone HTML files
|
||||||
|
echo. dirhtml to make HTML files named index.html in directories
|
||||||
|
echo. singlehtml to make a single large HTML file
|
||||||
|
echo. pickle to make pickle files
|
||||||
|
echo. json to make JSON files
|
||||||
|
echo. htmlhelp to make HTML files and a HTML help project
|
||||||
|
echo. qthelp to make HTML files and a qthelp project
|
||||||
|
echo. devhelp to make HTML files and a Devhelp project
|
||||||
|
echo. epub to make an epub
|
||||||
|
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||||
|
echo. text to make text files
|
||||||
|
echo. man to make manual pages
|
||||||
|
echo. texinfo to make Texinfo files
|
||||||
|
echo. gettext to make PO message catalogs
|
||||||
|
echo. changes to make an overview over all changed/added/deprecated items
|
||||||
|
echo. linkcheck to check all external links for integrity
|
||||||
|
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "clean" (
|
||||||
|
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||||
|
del /q /s %BUILDDIR%\*
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "html" (
|
||||||
|
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "dirhtml" (
|
||||||
|
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "singlehtml" (
|
||||||
|
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "pickle" (
|
||||||
|
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can process the pickle files.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "json" (
|
||||||
|
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can process the JSON files.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "htmlhelp" (
|
||||||
|
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||||
|
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "qthelp" (
|
||||||
|
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||||
|
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||||
|
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pyjs.qhcp
|
||||||
|
echo.To view the help file:
|
||||||
|
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pyjs.ghc
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "devhelp" (
|
||||||
|
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "epub" (
|
||||||
|
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "latex" (
|
||||||
|
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "text" (
|
||||||
|
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "man" (
|
||||||
|
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "texinfo" (
|
||||||
|
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "gettext" (
|
||||||
|
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "changes" (
|
||||||
|
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.The overview file is in %BUILDDIR%/changes.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "linkcheck" (
|
||||||
|
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Link check complete; look for any errors in the above output ^
|
||||||
|
or in %BUILDDIR%/linkcheck/output.txt.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "doctest" (
|
||||||
|
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Testing of doctests in the sources finished, look at the ^
|
||||||
|
results in %BUILDDIR%/doctest/output.txt.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
:end
|
|
@ -0,0 +1,248 @@
|
||||||
|
Implementing a custom type
|
||||||
|
==========================
|
||||||
|
|
||||||
|
To implement a custom python-level type, one can use the
|
||||||
|
:func:`py.type` builtin. At the JS-level, it is a function with the
|
||||||
|
same signature as the :py:class:`type` builtin [#bases]_. It returns a
|
||||||
|
child type of its one base (or :py:class:`py.object` if no base is
|
||||||
|
provided).
|
||||||
|
|
||||||
|
The ``dict`` parameter to :func:`py.type` can contain any
|
||||||
|
attribute, javascript-level or python-level: the default
|
||||||
|
``__getattribute__`` implementation will ensure they are converted to
|
||||||
|
Python-level attributes if needed. Most methods are also wrapped and
|
||||||
|
converted to :ref:`types-methods-python`, although there are a number
|
||||||
|
of special cases:
|
||||||
|
|
||||||
|
* Most "magic methods" of the data model ("dunder" methods) remain
|
||||||
|
javascript-level. See :ref:`the listing of magic methods and their
|
||||||
|
signatures <types-methods-dunder>`. As a result, they do not respect
|
||||||
|
the :ref:`types-methods-python-call`
|
||||||
|
|
||||||
|
* The ``toJSON`` and ``fromJSON`` methods are special-cased to remain
|
||||||
|
javascript-level and don't follow the
|
||||||
|
:ref:`types-methods-python-call`
|
||||||
|
|
||||||
|
* Functions which have been wrapped explicitly (via
|
||||||
|
:class:`py.PY_def`, :py:class:`py.classmethod` or
|
||||||
|
:py:class:`py.staticmethod`) are associated to the class
|
||||||
|
untouched. But due to their wrapper, they will use the
|
||||||
|
:ref:`types-methods-python-call` anyway
|
||||||
|
|
||||||
|
.. _types-methods-python:
|
||||||
|
|
||||||
|
Python-level callable
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Wrapped javascript function *or* the :func:`__call__` method itself
|
||||||
|
follow the :ref:`types-methods-python-call`. As a result, they can't
|
||||||
|
(easily) be called directly from javascript code. Because
|
||||||
|
:func:`__new__` and :func:`__init__` follow from :func:`__call__`,
|
||||||
|
they also follow the :ref:`types-methods-python-call`.
|
||||||
|
|
||||||
|
:func:`py.PY_call` should be used when interacting with them from
|
||||||
|
javascript is necessary.
|
||||||
|
|
||||||
|
Because ``__call__`` follows the :ref:`types-methods-python-call`,
|
||||||
|
instantiating a ``py.js`` type from javascript requires using
|
||||||
|
:func:`py.PY_call`.
|
||||||
|
|
||||||
|
.. _types-methods-python-call:
|
||||||
|
|
||||||
|
Python calling conventions
|
||||||
|
++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
The python-level arguments should be considered completely opaque,
|
||||||
|
they should be interacted with through :func:`py.PY_parseArgs` (to
|
||||||
|
extract python-level arguments to javascript implementation code) and
|
||||||
|
:func:`py.PY_call` (to call :ref:`types-methods-python` from
|
||||||
|
javascript code).
|
||||||
|
|
||||||
|
A callable following the :ref:`types-methods-python-call` *must*
|
||||||
|
return a ``py.js`` object, an error will be generated when failing to
|
||||||
|
do so.
|
||||||
|
|
||||||
|
.. todo:: arguments forwarding when e.g. overriding methods?
|
||||||
|
|
||||||
|
.. _types-methods-dunder:
|
||||||
|
|
||||||
|
Magic methods
|
||||||
|
-------------
|
||||||
|
|
||||||
|
``py.js`` doesn't support calling magic ("dunder") methods of the
|
||||||
|
datamodel from Python code, and these methods remain javascript-level
|
||||||
|
(they don't follow the :ref:`types-methods-python-call`).
|
||||||
|
|
||||||
|
Here is a list of the understood datamodel methods, refer to `the
|
||||||
|
relevant Python documentation
|
||||||
|
<http://docs.python.org/reference/datamodel.html?highlight=data%20model#special-method-names>`_
|
||||||
|
for their roles.
|
||||||
|
|
||||||
|
Basic customization
|
||||||
|
+++++++++++++++++++
|
||||||
|
|
||||||
|
.. function:: __hash__()
|
||||||
|
|
||||||
|
:returns: String
|
||||||
|
|
||||||
|
.. function:: __eq__(other)
|
||||||
|
|
||||||
|
The default implementation tests for identity
|
||||||
|
|
||||||
|
:param other: :py:class:`py.object` to compare this object with
|
||||||
|
:returns: :py:class:`py.bool`
|
||||||
|
|
||||||
|
.. function:: __ne__(other)
|
||||||
|
|
||||||
|
The default implementation calls :func:`__eq__` and reverses
|
||||||
|
its result.
|
||||||
|
|
||||||
|
:param other: :py:class:`py.object` to compare this object with
|
||||||
|
:returns: :py:class:`py.bool`
|
||||||
|
|
||||||
|
.. function:: __lt__(other)
|
||||||
|
|
||||||
|
The default implementation simply returns
|
||||||
|
:data:`py.NotImplemented`.
|
||||||
|
|
||||||
|
:param other: :py:class:`py.object` to compare this object with
|
||||||
|
:returns: :py:class:`py.bool`
|
||||||
|
|
||||||
|
|
||||||
|
.. function:: __le__(other)
|
||||||
|
|
||||||
|
The default implementation simply returns
|
||||||
|
:data:`py.NotImplemented`.
|
||||||
|
|
||||||
|
:param other: :py:class:`py.object` to compare this object with
|
||||||
|
:returns: :py:class:`py.bool`
|
||||||
|
|
||||||
|
|
||||||
|
.. function:: __ge__(other)
|
||||||
|
|
||||||
|
The default implementation simply returns
|
||||||
|
:data:`py.NotImplemented`.
|
||||||
|
|
||||||
|
:param other: :py:class:`py.object` to compare this object with
|
||||||
|
:returns: :py:class:`py.bool`
|
||||||
|
|
||||||
|
|
||||||
|
.. function:: __gt__(other)
|
||||||
|
|
||||||
|
The default implementation simply returns
|
||||||
|
:data:`py.NotImplemented`.
|
||||||
|
|
||||||
|
:param other: :py:class:`py.object` to compare this object with
|
||||||
|
:returns: :py:class:`py.bool`
|
||||||
|
|
||||||
|
.. function:: __str__()
|
||||||
|
|
||||||
|
Simply calls :func:`__unicode__`. This method should not be
|
||||||
|
overridden, :func:`__unicode__` should be overridden instead.
|
||||||
|
|
||||||
|
:returns: :py:class:`py.str`
|
||||||
|
|
||||||
|
.. function:: __unicode__()
|
||||||
|
|
||||||
|
:returns: :py:class:`py.unicode`
|
||||||
|
|
||||||
|
.. function:: __nonzero__()
|
||||||
|
|
||||||
|
The default implementation always returns :data:`py.True`
|
||||||
|
|
||||||
|
:returns: :py:class:`py.bool`
|
||||||
|
|
||||||
|
Customizing attribute access
|
||||||
|
++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
.. function:: __getattribute__(name)
|
||||||
|
|
||||||
|
:param String name: name of the attribute, as a javascript string
|
||||||
|
:returns: :py:class:`py.object`
|
||||||
|
|
||||||
|
.. function:: __getattr__(name)
|
||||||
|
|
||||||
|
:param String name: name of the attribute, as a javascript string
|
||||||
|
:returns: :py:class:`py.object`
|
||||||
|
|
||||||
|
.. function:: __setattr__(name, value)
|
||||||
|
|
||||||
|
:param String name: name of the attribute, as a javascript string
|
||||||
|
:param value: :py:class:`py.object`
|
||||||
|
|
||||||
|
Implementing descriptors
|
||||||
|
++++++++++++++++++++++++
|
||||||
|
|
||||||
|
.. function:: __get__(instance)
|
||||||
|
|
||||||
|
.. note:: readable descriptors don't currently handle "owner
|
||||||
|
classes"
|
||||||
|
|
||||||
|
:param instance: :py:class:`py.object`
|
||||||
|
:returns: :py:class:`py.object`
|
||||||
|
|
||||||
|
.. function:: __set__(instance, value)
|
||||||
|
|
||||||
|
:param instance: :py:class:`py.object`
|
||||||
|
:param value: :py:class:`py.object`
|
||||||
|
|
||||||
|
Emulating Numeric Types
|
||||||
|
+++++++++++++++++++++++
|
||||||
|
|
||||||
|
* Non-in-place binary numeric methods (e.g. ``__add__``, ``__mul__``,
|
||||||
|
...) should all be supported including reversed calls (in case the
|
||||||
|
primary call is not available or returns
|
||||||
|
:py:data:`py.NotImplemented`). They take a single
|
||||||
|
:py:class:`py.object` parameter and return a single
|
||||||
|
:py:class:`py.object` parameter.
|
||||||
|
|
||||||
|
* Unary operator numeric methods are all supported:
|
||||||
|
|
||||||
|
.. function:: __pos__()
|
||||||
|
|
||||||
|
:returns: :py:class:`py.object`
|
||||||
|
|
||||||
|
.. function:: __neg__()
|
||||||
|
|
||||||
|
:returns: :py:class:`py.object`
|
||||||
|
|
||||||
|
.. function:: __invert__()
|
||||||
|
|
||||||
|
:returns: :py:class:`py.object`
|
||||||
|
|
||||||
|
* For non-operator numeric methods, support is contingent on the
|
||||||
|
corresponding :ref:`builtins <builtins>` being implemented
|
||||||
|
|
||||||
|
Emulating container types
|
||||||
|
+++++++++++++++++++++++++
|
||||||
|
|
||||||
|
.. function:: __len__()
|
||||||
|
|
||||||
|
:returns: :py:class:`py.int`
|
||||||
|
|
||||||
|
.. function:: __getitem__(name)
|
||||||
|
|
||||||
|
:param name: :py:class:`py.object`
|
||||||
|
:returns: :py:class:`py.object`
|
||||||
|
|
||||||
|
.. function:: __setitem__(name, value)
|
||||||
|
|
||||||
|
:param name: :py:class:`py.object`
|
||||||
|
:param value: :py:class:`py.object`
|
||||||
|
|
||||||
|
.. function:: __iter__()
|
||||||
|
|
||||||
|
:returns: :py:class:`py.object`
|
||||||
|
|
||||||
|
.. function:: __reversed__()
|
||||||
|
|
||||||
|
:returns: :py:class:`py.object`
|
||||||
|
|
||||||
|
.. function:: __contains__(other)
|
||||||
|
|
||||||
|
:param other: :py:class:`py.object`
|
||||||
|
:returns: :py:class:`py.bool`
|
||||||
|
|
||||||
|
.. [#bases] with the limitation that, because :ref:`py.js builds its
|
||||||
|
object model on top of javascript's
|
||||||
|
<details-object-model>`, only one base is allowed.
|
|
@ -0,0 +1,248 @@
|
||||||
|
Utility functions for interacting with ``py.js`` objects
|
||||||
|
========================================================
|
||||||
|
|
||||||
|
Essentially the ``py.js`` version of the Python C API, these functions
|
||||||
|
are used to implement new ``py.js`` types or to interact with existing
|
||||||
|
ones.
|
||||||
|
|
||||||
|
They are prefixed with ``PY_``.
|
||||||
|
|
||||||
|
.. function:: py.PY_parseArgs(arguments, format)
|
||||||
|
|
||||||
|
Arguments parser converting from the :ref:`user-defined calling
|
||||||
|
conventions <types-methods-python-call>` to a JS object mapping
|
||||||
|
argument names to values. It serves the same role as
|
||||||
|
`PyArg_ParseTupleAndKeywords`_.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
var args = py.PY_parseArgs(
|
||||||
|
arguments, ['foo', 'bar', ['baz', 3], ['qux', "foo"]]);
|
||||||
|
|
||||||
|
roughly corresponds to the argument spec:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def func(foo, bar, baz=3, qux="foo"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
.. note:: a significant difference is that "default values" will
|
||||||
|
be re-evaluated at each call, since they are within the
|
||||||
|
function.
|
||||||
|
|
||||||
|
:param arguments: array-like objects holding the args and kwargs
|
||||||
|
passed to the callable, generally the
|
||||||
|
``arguments`` of the caller.
|
||||||
|
|
||||||
|
:param format: mapping declaration to the actual arguments of the
|
||||||
|
function. A javascript array composed of five
|
||||||
|
possible types of elements:
|
||||||
|
|
||||||
|
* The literal string ``'*'`` marks all following
|
||||||
|
parameters as keyword-only, regardless of them
|
||||||
|
having a default value or not [#kwonly]_. Can
|
||||||
|
only be present once in the parameters list.
|
||||||
|
|
||||||
|
* A string prefixed by ``*``, marks the positional
|
||||||
|
variadic parameter for the function: gathers all
|
||||||
|
provided positional arguments left and makes all
|
||||||
|
following parameters keyword-only
|
||||||
|
[#star-args]_. ``*args`` is incompatible with
|
||||||
|
``*``.
|
||||||
|
|
||||||
|
* A string prefixed with ``**``, marks the
|
||||||
|
positional keyword variadic parameter for the
|
||||||
|
function: gathers all provided keyword arguments
|
||||||
|
left and closes the argslist. If present, this
|
||||||
|
must be the last parameter of the format list.
|
||||||
|
|
||||||
|
* A string defines a required parameter, accessible
|
||||||
|
positionally or through keyword
|
||||||
|
|
||||||
|
* A pair of ``[String, py.object]`` defines an
|
||||||
|
optional parameter and its default value.
|
||||||
|
|
||||||
|
For simplicity, when not using optional parameters
|
||||||
|
it is possible to use a simple string as the format
|
||||||
|
(using space-separated elements). The string will
|
||||||
|
be split on whitespace and processed as a normal
|
||||||
|
format array.
|
||||||
|
|
||||||
|
:returns: a javascript object mapping argument names to values
|
||||||
|
|
||||||
|
:raises: ``TypeError`` if the provided arguments don't match the
|
||||||
|
format
|
||||||
|
|
||||||
|
.. class:: py.PY_def(fn)
|
||||||
|
|
||||||
|
Type wrapping javascript functions into py.js callables. The
|
||||||
|
wrapped function follows :ref:`the py.js calling conventions
|
||||||
|
<types-methods-python-call>`
|
||||||
|
|
||||||
|
:param Function fn: the javascript function to wrap
|
||||||
|
:returns: a callable py.js object
|
||||||
|
|
||||||
|
Object Protocol
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. function:: py.PY_hasAttr(o, attr_name)
|
||||||
|
|
||||||
|
Returns ``true`` if ``o`` has the attribute ``attr_name``,
|
||||||
|
otherwise returns ``false``. Equivalent to Python's ``hasattr(o,
|
||||||
|
attr_name)``
|
||||||
|
|
||||||
|
:param o: A :class:`py.object`
|
||||||
|
:param attr_name: a javascript ``String``
|
||||||
|
:rtype: ``Boolean``
|
||||||
|
|
||||||
|
.. function:: py.PY_getAttr(o, attr_name)
|
||||||
|
|
||||||
|
Retrieve an attribute ``attr_name`` from the object ``o``. Returns
|
||||||
|
the attribute value on success, raises ``AttributeError`` on
|
||||||
|
failure. Equivalent to the python expression ``o.attr_name``.
|
||||||
|
|
||||||
|
:param o: A :class:`py.object`
|
||||||
|
:param attr_name: a javascript ``String``
|
||||||
|
:returns: A :class:`py.object`
|
||||||
|
:raises: ``AttributeError``
|
||||||
|
|
||||||
|
.. function:: py.PY_str(o)
|
||||||
|
|
||||||
|
Computes a string representation of ``o``, returns the string
|
||||||
|
representation. Equivalent to ``str(o)``
|
||||||
|
|
||||||
|
:param o: A :class:`py.object`
|
||||||
|
:returns: :class:`py.str`
|
||||||
|
|
||||||
|
.. function:: py.PY_isInstance(inst, cls)
|
||||||
|
|
||||||
|
Returns ``true`` if ``inst`` is an instance of ``cls``, ``false``
|
||||||
|
otherwise.
|
||||||
|
|
||||||
|
.. function:: py.PY_isSubclass(derived, cls)
|
||||||
|
|
||||||
|
Returns ``true`` if ``derived`` is ``cls`` or a subclass thereof.
|
||||||
|
|
||||||
|
.. function:: py.PY_call(callable[, args][, kwargs])
|
||||||
|
|
||||||
|
Call an arbitrary python-level callable from javascript.
|
||||||
|
|
||||||
|
:param callable: A ``py.js`` callable object (broadly speaking,
|
||||||
|
either a class or an object with a ``__call__``
|
||||||
|
method)
|
||||||
|
|
||||||
|
:param args: javascript Array of :class:`py.object`, used as
|
||||||
|
positional arguments to ``callable``
|
||||||
|
|
||||||
|
:param kwargs: javascript Object mapping names to
|
||||||
|
:class:`py.object`, used as named arguments to
|
||||||
|
``callable``
|
||||||
|
|
||||||
|
:returns: nothing or :class:`py.object`
|
||||||
|
|
||||||
|
.. function:: py.PY_isTrue(o)
|
||||||
|
|
||||||
|
Returns ``true`` if the object is considered truthy, ``false``
|
||||||
|
otherwise. Equivalent to ``bool(o)``.
|
||||||
|
|
||||||
|
:param o: A :class:`py.object`
|
||||||
|
:rtype: Boolean
|
||||||
|
|
||||||
|
.. function:: py.PY_not(o)
|
||||||
|
|
||||||
|
Inverse of :func:`py.PY_isTrue`.
|
||||||
|
|
||||||
|
.. function:: py.PY_size(o)
|
||||||
|
|
||||||
|
If ``o`` is a sequence or mapping, returns its length. Otherwise,
|
||||||
|
raises ``TypeError``.
|
||||||
|
|
||||||
|
:param o: A :class:`py.object`
|
||||||
|
:returns: ``Number``
|
||||||
|
:raises: ``TypeError`` if the object doesn't have a length
|
||||||
|
|
||||||
|
.. function:: py.PY_getItem(o, key)
|
||||||
|
|
||||||
|
Returns the element of ``o`` corresponding to the object
|
||||||
|
``key``. This is equivalent to ``o[key]``.
|
||||||
|
|
||||||
|
:param o: :class:`py.object`
|
||||||
|
:param key: :class:`py.object`
|
||||||
|
:returns: :class:`py.object`
|
||||||
|
:raises: ``TypeError`` if ``o`` does not support the operation, if
|
||||||
|
``key`` or the return value is not a :class:`py.object`
|
||||||
|
|
||||||
|
.. function:: py.PY_setItem(o, key, v)
|
||||||
|
|
||||||
|
Maps the object ``key`` to the value ``v`` in ``o``. Equivalent to
|
||||||
|
``o[key] = v``.
|
||||||
|
|
||||||
|
:param o: :class:`py.object`
|
||||||
|
:param key: :class:`py.object`
|
||||||
|
:param v: :class:`py.object`
|
||||||
|
:raises: ``TypeError`` if ``o`` does not support the operation, or
|
||||||
|
if ``key`` or ``v`` are not :class:`py.object`
|
||||||
|
|
||||||
|
Number Protocol
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. function:: py.PY_add(o1, o2)
|
||||||
|
|
||||||
|
Returns the result of adding ``o1`` and ``o2``, equivalent to
|
||||||
|
``o1 + o2``.
|
||||||
|
|
||||||
|
:param o1: :class:`py.object`
|
||||||
|
:param o2: :class:`py.object`
|
||||||
|
:returns: :class:`py.object`
|
||||||
|
|
||||||
|
.. function:: py.PY_subtract(o1, o2)
|
||||||
|
|
||||||
|
Returns the result of subtracting ``o2`` from ``o1``, equivalent
|
||||||
|
to ``o1 - o2``.
|
||||||
|
|
||||||
|
:param o1: :class:`py.object`
|
||||||
|
:param o2: :class:`py.object`
|
||||||
|
:returns: :class:`py.object`
|
||||||
|
|
||||||
|
.. function:: py.PY_multiply(o1, o2)
|
||||||
|
|
||||||
|
Returns the result of multiplying ``o1`` by ``o2``, equivalent to
|
||||||
|
``o1 * o2``.
|
||||||
|
|
||||||
|
:param o1: :class:`py.object`
|
||||||
|
:param o2: :class:`py.object`
|
||||||
|
:returns: :class:`py.object`
|
||||||
|
|
||||||
|
.. function:: py.PY_divide(o1, o2)
|
||||||
|
|
||||||
|
Returns the result of dividing ``o1`` by ``o2``, equivalent to
|
||||||
|
``o1 / o2``.
|
||||||
|
|
||||||
|
:param o1: :class:`py.object`
|
||||||
|
:param o2: :class:`py.object`
|
||||||
|
:returns: :class:`py.object`
|
||||||
|
|
||||||
|
.. function:: py.PY_negative(o)
|
||||||
|
|
||||||
|
Returns the negation of ``o``, equivalent to ``-o``.
|
||||||
|
|
||||||
|
:param o: :class:`py.object`
|
||||||
|
:returns: :class:`py.object`
|
||||||
|
|
||||||
|
.. function:: py.PY_positive(o)
|
||||||
|
|
||||||
|
Returns the "positive" of ``o``, equivalent to ``+o``.
|
||||||
|
|
||||||
|
:param o: :class:`py.object`
|
||||||
|
:returns: :class:`py.object`
|
||||||
|
|
||||||
|
.. [#kwonly] Python 2, which py.js currently implements, does not
|
||||||
|
support Python-level keyword-only parameters (it can be
|
||||||
|
done through the C-API), but it seemed neat and easy
|
||||||
|
enough so there.
|
||||||
|
|
||||||
|
.. [#star-args] due to this and contrary to Python 2, py.js allows
|
||||||
|
arguments other than ``**kwargs`` to follow ``*args``.
|
||||||
|
|
||||||
|
.. _PyArg_ParseTupleAndKeywords:
|
||||||
|
http://docs.python.org/c-api/arg.html#PyArg_ParseTupleAndKeywords
|
File diff suppressed because it is too large
Load Diff
|
@ -1,132 +0,0 @@
|
||||||
var py = require('../lib/py.js'),
|
|
||||||
expect = require('expect.js');
|
|
||||||
|
|
||||||
expect.Assertion.prototype.tokens = function (n) {
|
|
||||||
var length = this.obj.length;
|
|
||||||
this.assert(length === n + 1,
|
|
||||||
'expected ' + this.obj + ' to have ' + n + ' tokens',
|
|
||||||
'expected ' + this.obj + ' to not have ' + n + ' tokens');
|
|
||||||
this.assert(this.obj[length-1].id === '(end)',
|
|
||||||
'expected ' + this.obj + ' to have and end token',
|
|
||||||
'expected ' + this.obj + ' to not have an end token');
|
|
||||||
};
|
|
||||||
|
|
||||||
expect.Assertion.prototype.named = function (value) {
|
|
||||||
this.assert(this.obj.id === '(name)',
|
|
||||||
'expected ' + this.obj + ' to be a name token',
|
|
||||||
'expected ' + this.obj + ' not to be a name token');
|
|
||||||
this.assert(this.obj.value === value,
|
|
||||||
'expected ' + this.obj + ' to have tokenized ' + value,
|
|
||||||
'expected ' + this.obj + ' not to have tokenized ' + value);
|
|
||||||
};
|
|
||||||
expect.Assertion.prototype.constant = function (value) {
|
|
||||||
this.assert(this.obj.id === '(constant)',
|
|
||||||
'expected ' + this.obj + ' to be a constant token',
|
|
||||||
'expected ' + this.obj + ' not to be a constant token');
|
|
||||||
this.assert(this.obj.value === value,
|
|
||||||
'expected ' + this.obj + ' to have tokenized ' + value,
|
|
||||||
'expected ' + this.obj + ' not to have tokenized ' + value);
|
|
||||||
};
|
|
||||||
expect.Assertion.prototype.number = function (value) {
|
|
||||||
this.assert(this.obj.id === '(number)',
|
|
||||||
'expected ' + this.obj + ' to be a number token',
|
|
||||||
'expected ' + this.obj + ' not to be a number token');
|
|
||||||
this.assert(this.obj.value === value,
|
|
||||||
'expected ' + this.obj + ' to have tokenized ' + value,
|
|
||||||
'expected ' + this.obj + ' not to have tokenized ' + value);
|
|
||||||
};
|
|
||||||
expect.Assertion.prototype.string = function (value) {
|
|
||||||
this.assert(this.obj.id === '(string)',
|
|
||||||
'expected ' + this.obj + ' to be a string token',
|
|
||||||
'expected ' + this.obj + ' not to be a string token');
|
|
||||||
this.assert(this.obj.value === value,
|
|
||||||
'expected ' + this.obj + ' to have tokenized ' + value,
|
|
||||||
'expected ' + this.obj + ' not to have tokenized ' + value);
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Tokenizer', function () {
|
|
||||||
describe('simple literals', function () {
|
|
||||||
it('tokenizes numbers', function () {
|
|
||||||
var toks = py.tokenize('1');
|
|
||||||
expect(toks).to.have.tokens(1);
|
|
||||||
expect(toks[0]).to.be.number(1);
|
|
||||||
|
|
||||||
var toks = py.tokenize('-1');
|
|
||||||
expect(toks).to.have.tokens(2);
|
|
||||||
expect(toks[0].id).to.be('-');
|
|
||||||
expect(toks[1]).to.be.number(1);
|
|
||||||
|
|
||||||
var toks = py.tokenize('1.2');
|
|
||||||
expect(toks).to.have.tokens(1);
|
|
||||||
expect(toks[0]).to.be.number(1.2);
|
|
||||||
|
|
||||||
var toks = py.tokenize('.42');
|
|
||||||
expect(toks).to.have.tokens(1);
|
|
||||||
expect(toks[0]).to.be.number(0.42);
|
|
||||||
});
|
|
||||||
it('tokenizes strings', function () {
|
|
||||||
var toks = py.tokenize('"foo"');
|
|
||||||
expect(toks).to.have.tokens(1);
|
|
||||||
expect(toks[0]).to.be.string('foo');
|
|
||||||
|
|
||||||
var toks = py.tokenize("'foo'");
|
|
||||||
expect(toks).to.have.tokens(1);
|
|
||||||
expect(toks[0]).to.be.string('foo');
|
|
||||||
});
|
|
||||||
it('tokenizes bare names', function () {
|
|
||||||
var toks = py.tokenize('foo');
|
|
||||||
expect(toks).to.have.tokens(1);
|
|
||||||
expect(toks[0].id).to.be('(name)');
|
|
||||||
expect(toks[0].value).to.be('foo');
|
|
||||||
});
|
|
||||||
it('tokenizes constants', function () {
|
|
||||||
var toks = py.tokenize('None');
|
|
||||||
expect(toks).to.have.tokens(1);
|
|
||||||
expect(toks[0]).to.be.constant('None');
|
|
||||||
|
|
||||||
var toks = py.tokenize('True');
|
|
||||||
expect(toks).to.have.tokens(1);
|
|
||||||
expect(toks[0]).to.be.constant('True');
|
|
||||||
|
|
||||||
var toks = py.tokenize('False');
|
|
||||||
expect(toks).to.have.tokens(1);
|
|
||||||
expect(toks[0]).to.be.constant('False');
|
|
||||||
});
|
|
||||||
it('does not fuck up on trailing spaces', function () {
|
|
||||||
var toks = py.tokenize('None ');
|
|
||||||
expect(toks).to.have.tokens(1);
|
|
||||||
expect(toks[0]).to.be.constant('None');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('collections', function () {
|
|
||||||
it('tokenizes opening and closing symbols', function () {
|
|
||||||
var toks = py.tokenize('()');
|
|
||||||
expect(toks).to.have.tokens(2);
|
|
||||||
expect(toks[0].id).to.be('(');
|
|
||||||
expect(toks[1].id).to.be(')');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('functions', function () {
|
|
||||||
it('tokenizes kwargs', function () {
|
|
||||||
var toks = py.tokenize('foo(bar=3, qux=4)');
|
|
||||||
expect(toks).to.have.tokens(10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Parser', function () {
|
|
||||||
describe('functions', function () {
|
|
||||||
var ast = py.parse(py.tokenize('foo(bar=3, qux=4)'));
|
|
||||||
expect(ast.id).to.be('(');
|
|
||||||
expect(ast.first).to.be.named('foo');
|
|
||||||
|
|
||||||
args = ast.second;
|
|
||||||
expect(args[0].id).to.be('=');
|
|
||||||
expect(args[0].first).to.be.named('bar');
|
|
||||||
expect(args[0].second).to.be.number(3);
|
|
||||||
|
|
||||||
expect(args[1].id).to.be('=');
|
|
||||||
expect(args[1].first).to.be.named('qux');
|
|
||||||
expect(args[1].second).to.be.number(4);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,388 +0,0 @@
|
||||||
var py = require('../lib/py.js'),
|
|
||||||
expect = require('expect.js');
|
|
||||||
|
|
||||||
var ev = function (str, context) {
|
|
||||||
return py.evaluate(py.parse(py.tokenize(str)), context);
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Literals', function () {
|
|
||||||
describe('Number', function () {
|
|
||||||
it('should have the right type', function () {
|
|
||||||
expect(ev('1')).to.be.a(py.float);
|
|
||||||
});
|
|
||||||
it('should yield the corresponding JS value', function () {
|
|
||||||
expect(py.eval('1')).to.be(1);
|
|
||||||
expect(py.eval('42')).to.be(42);
|
|
||||||
expect(py.eval('9999')).to.be(9999);
|
|
||||||
});
|
|
||||||
it('should correctly handle negative literals', function () {
|
|
||||||
expect(py.eval('-1')).to.be(-1);
|
|
||||||
expect(py.eval('-42')).to.be(-42);
|
|
||||||
expect(py.eval('-9999')).to.be(-9999);
|
|
||||||
});
|
|
||||||
it('should correctly handle float literals', function () {
|
|
||||||
expect(py.eval('.42')).to.be(0.42);
|
|
||||||
expect(py.eval('1.2')).to.be(1.2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('Booleans', function () {
|
|
||||||
it('should have the right type', function () {
|
|
||||||
expect(ev('False')).to.be.a(py.bool);
|
|
||||||
expect(ev('True')).to.be.a(py.bool);
|
|
||||||
});
|
|
||||||
it('should yield the corresponding JS value', function () {
|
|
||||||
expect(py.eval('False')).to.be(false);
|
|
||||||
expect(py.eval('True')).to.be(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('None', function () {
|
|
||||||
it('should have the right type', function () {
|
|
||||||
expect(ev('None')).to.be.a(py.object)
|
|
||||||
});
|
|
||||||
it('should yield a JS null', function () {
|
|
||||||
expect(py.eval('None')).to.be(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('String', function () {
|
|
||||||
it('should have the right type', function () {
|
|
||||||
expect(ev('"foo"')).to.be.a(py.str);
|
|
||||||
expect(ev("'foo'")).to.be.a(py.str);
|
|
||||||
});
|
|
||||||
it('should yield the corresponding JS string', function () {
|
|
||||||
expect(py.eval('"somestring"')).to.be('somestring');
|
|
||||||
expect(py.eval("'somestring'")).to.be('somestring');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('Tuple', function () {
|
|
||||||
it('shoud have the right type', function () {
|
|
||||||
expect(ev('()')).to.be.a(py.tuple);
|
|
||||||
});
|
|
||||||
it('should map to a JS array', function () {
|
|
||||||
expect(py.eval('()')).to.eql([]);
|
|
||||||
expect(py.eval('(1, 2, 3)')).to.eql([1, 2, 3]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('List', function () {
|
|
||||||
it('shoud have the right type', function () {
|
|
||||||
expect(ev('[]')).to.be.a(py.list);
|
|
||||||
});
|
|
||||||
it('should map to a JS array', function () {
|
|
||||||
expect(py.eval('[]')).to.eql([]);
|
|
||||||
expect(py.eval('[1, 2, 3]')).to.eql([1, 2, 3]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('Dict', function () {
|
|
||||||
it('shoud have the right type', function () {
|
|
||||||
expect(ev('{}')).to.be.a(py.dict);
|
|
||||||
});
|
|
||||||
it('should map to a JS object', function () {
|
|
||||||
expect(py.eval("{}")).to.eql({});
|
|
||||||
expect(py.eval("{'foo': 1, 'bar': 2}"))
|
|
||||||
.to.eql({foo: 1, bar: 2});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('Free variables', function () {
|
|
||||||
it('should return its identity', function () {
|
|
||||||
expect(py.eval('foo', {foo: 1})).to.be(1);
|
|
||||||
expect(py.eval('foo', {foo: true})).to.be(true);
|
|
||||||
expect(py.eval('foo', {foo: false})).to.be(false);
|
|
||||||
expect(py.eval('foo', {foo: null})).to.be(null);
|
|
||||||
expect(py.eval('foo', {foo: 'bar'})).to.be('bar');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('Comparisons', function () {
|
|
||||||
describe('equality', function () {
|
|
||||||
it('should work with literals', function () {
|
|
||||||
expect(py.eval('1 == 1')).to.be(true);
|
|
||||||
expect(py.eval('"foo" == "foo"')).to.be(true);
|
|
||||||
expect(py.eval('"foo" == "bar"')).to.be(false);
|
|
||||||
});
|
|
||||||
it('should work with free variables', function () {
|
|
||||||
expect(py.eval('1 == a', {a: 1})).to.be(true);
|
|
||||||
expect(py.eval('foo == "bar"', {foo: 'bar'})).to.be(true);
|
|
||||||
expect(py.eval('foo == "bar"', {foo: 'qux'})).to.be(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('inequality', function () {
|
|
||||||
it('should work with literals', function () {
|
|
||||||
expect(py.eval('1 != 2')).to.be(true);
|
|
||||||
expect(py.eval('"foo" != "foo"')).to.be(false);
|
|
||||||
expect(py.eval('"foo" != "bar"')).to.be(true);
|
|
||||||
});
|
|
||||||
it('should work with free variables', function () {
|
|
||||||
expect(py.eval('1 != a', {a: 42})).to.be(true);
|
|
||||||
expect(py.eval('foo != "bar"', {foo: 'bar'})).to.be(false);
|
|
||||||
expect(py.eval('foo != "bar"', {foo: 'qux'})).to.be(true);
|
|
||||||
expect(py.eval('foo != bar', {foo: 'qux', bar: 'quux'}))
|
|
||||||
.to.be(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('rich comparisons', function () {
|
|
||||||
it('should work with numbers', function () {
|
|
||||||
expect(py.eval('3 < 5')).to.be(true);
|
|
||||||
expect(py.eval('5 >= 3')).to.be(true);
|
|
||||||
expect(py.eval('3 >= 3')).to.be(true);
|
|
||||||
expect(py.eval('3 > 5')).to.be(false);
|
|
||||||
});
|
|
||||||
it('should support comparison chains', function () {
|
|
||||||
expect(py.eval('1 < 3 < 5')).to.be(true);
|
|
||||||
expect(py.eval('5 > 3 > 1')).to.be(true);
|
|
||||||
expect(py.eval('1 < 3 > 2 == 2 > -2')).to.be(true);
|
|
||||||
});
|
|
||||||
it('should compare strings', function () {
|
|
||||||
expect(py.eval('date >= current',
|
|
||||||
{date: '2010-06-08', current: '2010-06-05'}))
|
|
||||||
.to.be(true);
|
|
||||||
expect(py.eval('state == "cancel"', {state: 'cancel'}))
|
|
||||||
.to.be(true);
|
|
||||||
expect(py.eval('state == "cancel"', {state: 'open'}))
|
|
||||||
.to.be(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('missing eq/neq', function () {
|
|
||||||
it('should fall back on identity', function () {
|
|
||||||
var typ = new py.type(function MyType() {});
|
|
||||||
expect(py.eval('MyType() == MyType()', {MyType: typ})).to.be(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('un-comparable types', function () {
|
|
||||||
it('should default to type-name ordering', function () {
|
|
||||||
var t1 = new py.type(function Type1() {});
|
|
||||||
var t2 = new py.type(function Type2() {});
|
|
||||||
expect(py.eval('T1() < T2()', {T1: t1, T2: t2})).to.be(true);
|
|
||||||
expect(py.eval('T1() > T2()', {T1: t1, T2: t2})).to.be(false);
|
|
||||||
});
|
|
||||||
it('should handle native stuff', function () {
|
|
||||||
expect(py.eval('None < 42')).to.be(true);
|
|
||||||
expect(py.eval('42 > None')).to.be(true);
|
|
||||||
expect(py.eval('None > 42')).to.be(false);
|
|
||||||
|
|
||||||
expect(py.eval('None < False')).to.be(true);
|
|
||||||
expect(py.eval('None < True')).to.be(true);
|
|
||||||
expect(py.eval('False > None')).to.be(true);
|
|
||||||
expect(py.eval('True > None')).to.be(true);
|
|
||||||
expect(py.eval('None > False')).to.be(false);
|
|
||||||
expect(py.eval('None > True')).to.be(false);
|
|
||||||
|
|
||||||
expect(py.eval('False < ""')).to.be(true);
|
|
||||||
expect(py.eval('"" > False')).to.be(true);
|
|
||||||
expect(py.eval('False > ""')).to.be(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('Boolean operators', function () {
|
|
||||||
it('should work', function () {
|
|
||||||
expect(py.eval("foo == 'foo' or foo == 'bar'",
|
|
||||||
{foo: 'bar'}))
|
|
||||||
.to.be(true);;
|
|
||||||
expect(py.eval("foo == 'foo' and bar == 'bar'",
|
|
||||||
{foo: 'foo', bar: 'bar'}))
|
|
||||||
.to.be(true);;
|
|
||||||
});
|
|
||||||
it('should be lazy', function () {
|
|
||||||
// second clause should nameerror if evaluated
|
|
||||||
expect(py.eval("foo == 'foo' or bar == 'bar'",
|
|
||||||
{foo: 'foo'}))
|
|
||||||
.to.be(true);;
|
|
||||||
expect(py.eval("foo == 'foo' and bar == 'bar'",
|
|
||||||
{foo: 'bar'}))
|
|
||||||
.to.be(false);;
|
|
||||||
});
|
|
||||||
it('should return the actual object', function () {
|
|
||||||
expect(py.eval('"foo" or "bar"')).to.be('foo');
|
|
||||||
expect(py.eval('None or "bar"')).to.be('bar');
|
|
||||||
expect(py.eval('False or None')).to.be(null);
|
|
||||||
expect(py.eval('0 or 1')).to.be(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('Containment', function () {
|
|
||||||
describe('in sequences', function () {
|
|
||||||
it('should match collection items', function () {
|
|
||||||
expect(py.eval("'bar' in ('foo', 'bar')"))
|
|
||||||
.to.be(true);
|
|
||||||
expect(py.eval('1 in (1, 2, 3, 4)'))
|
|
||||||
.to.be(true);;
|
|
||||||
expect(py.eval('1 in (2, 3, 4)'))
|
|
||||||
.to.be(false);;
|
|
||||||
expect(py.eval('"url" in ("url",)'))
|
|
||||||
.to.be(true);
|
|
||||||
expect(py.eval('"foo" in ["foo", "bar"]'))
|
|
||||||
.to.be(true);
|
|
||||||
});
|
|
||||||
it('should not be recursive', function () {
|
|
||||||
expect(py.eval('"ur" in ("url",)'))
|
|
||||||
.to.be(false);;
|
|
||||||
});
|
|
||||||
it('should be negatable', function () {
|
|
||||||
expect(py.eval('1 not in (2, 3, 4)')).to.be(true);
|
|
||||||
expect(py.eval('"ur" not in ("url",)')).to.be(true);
|
|
||||||
expect(py.eval('-2 not in (1, 2, 3)')).to.be(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('in dict', function () {
|
|
||||||
// TODO
|
|
||||||
});
|
|
||||||
describe('in strings', function () {
|
|
||||||
it('should match the whole string', function () {
|
|
||||||
expect(py.eval('"view" in "view"')).to.be(true);
|
|
||||||
expect(py.eval('"bob" in "view"')).to.be(false);
|
|
||||||
});
|
|
||||||
it('should match substrings', function () {
|
|
||||||
expect(py.eval('"ur" in "url"')).to.be(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('Conversions', function () {
|
|
||||||
describe('to bool', function () {
|
|
||||||
describe('strings', function () {
|
|
||||||
it('should be true if non-empty', function () {
|
|
||||||
expect(py.eval('bool(date_deadline)',
|
|
||||||
{date_deadline: '2008'}))
|
|
||||||
.to.be(true);
|
|
||||||
});
|
|
||||||
it('should be false if empty', function () {
|
|
||||||
expect(py.eval('bool(s)', {s: ''})) .to.be(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('Attribute access', function () {
|
|
||||||
it("should return the attribute's value", function () {
|
|
||||||
var o = new py.object();
|
|
||||||
o.bar = py.True;
|
|
||||||
expect(py.eval('foo.bar', {foo: o})).to.be(true);
|
|
||||||
o.bar = py.False;
|
|
||||||
expect(py.eval('foo.bar', {foo: o})).to.be(false);
|
|
||||||
});
|
|
||||||
it("should work with functions", function () {
|
|
||||||
var o = new py.object();
|
|
||||||
o.bar = new py.def(function () {
|
|
||||||
return new py.str("ok");
|
|
||||||
});
|
|
||||||
expect(py.eval('foo.bar()', {foo: o})).to.be('ok');
|
|
||||||
});
|
|
||||||
it('should not convert function attributes into methods', function () {
|
|
||||||
var o = new py.object();
|
|
||||||
o.bar = new py.type(function bar() {});
|
|
||||||
o.bar.__getattribute__ = function () {
|
|
||||||
return o.bar.baz;
|
|
||||||
}
|
|
||||||
o.bar.baz = py.True;
|
|
||||||
expect(py.eval('foo.bar.baz', {foo: o})).to.be(true);
|
|
||||||
});
|
|
||||||
it('should work on instance attributes', function () {
|
|
||||||
var typ = py.type(function MyType() {
|
|
||||||
this.attr = new py.float(3);
|
|
||||||
}, py.object, {});
|
|
||||||
expect(py.eval('MyType().attr', {MyType: typ})).to.be(3);
|
|
||||||
});
|
|
||||||
it('should work on class attributes', function () {
|
|
||||||
var typ = py.type(function MyType() {}, py.object, {
|
|
||||||
attr: new py.float(3)
|
|
||||||
});
|
|
||||||
expect(py.eval('MyType().attr', {MyType: typ})).to.be(3);
|
|
||||||
});
|
|
||||||
it('should work with methods', function () {
|
|
||||||
var typ = py.type(function MyType() {
|
|
||||||
this.attr = new py.float(3);
|
|
||||||
}, py.object, {
|
|
||||||
some_method: function () { return new py.str('ok'); },
|
|
||||||
get_attr: function () { return this.attr; }
|
|
||||||
});
|
|
||||||
expect(py.eval('MyType().some_method()', {MyType: typ})).to.be('ok');
|
|
||||||
expect(py.eval('MyType().get_attr()', {MyType: typ})).to.be(3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('Callables', function () {
|
|
||||||
it('should wrap JS functions', function () {
|
|
||||||
expect(py.eval('foo()', {foo: function foo() { return new py.float(3); }}))
|
|
||||||
.to.be(3);
|
|
||||||
});
|
|
||||||
it('should work on custom types', function () {
|
|
||||||
var typ = py.type(function MyType() {}, py.object, {
|
|
||||||
toJSON: function () { return true; }
|
|
||||||
});
|
|
||||||
expect(py.eval('MyType()', {MyType: typ})).to.be(true);
|
|
||||||
});
|
|
||||||
it('should accept kwargs', function () {
|
|
||||||
expect(py.eval('foo(ok=True)', {
|
|
||||||
foo: function foo() { return py.True; }
|
|
||||||
})).to.be(true);
|
|
||||||
});
|
|
||||||
it('should be able to get its kwargs', function () {
|
|
||||||
expect(py.eval('foo(ok=True)', {
|
|
||||||
foo: function foo(args, kwargs) { return kwargs.ok; }
|
|
||||||
})).to.be(true);
|
|
||||||
});
|
|
||||||
it('should be able to have both args and kwargs', function () {
|
|
||||||
expect(py.eval('foo(1, 2, 3, ok=True, nok=False)', {
|
|
||||||
foo: function (args, kwargs) {
|
|
||||||
expect(args).to.have.length(3);
|
|
||||||
expect(args[0].toJSON()).to.be(1);
|
|
||||||
expect(kwargs).to.only.have.keys('ok', 'nok')
|
|
||||||
expect(kwargs.nok.toJSON()).to.be(false);
|
|
||||||
return kwargs.ok;
|
|
||||||
}
|
|
||||||
})).to.be(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('issubclass', function () {
|
|
||||||
it('should say a type is its own subclass', function () {
|
|
||||||
expect(py.issubclass.__call__([py.dict, py.dict]).toJSON())
|
|
||||||
.to.be(true);
|
|
||||||
expect(py.eval('issubclass(dict, dict)'))
|
|
||||||
.to.be(true);
|
|
||||||
});
|
|
||||||
it('should work with subtypes', function () {
|
|
||||||
expect(py.issubclass.__call__([py.bool, py.object]).toJSON())
|
|
||||||
.to.be(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('builtins', function () {
|
|
||||||
it('should aways be available', function () {
|
|
||||||
expect(py.eval('bool("foo")')).to.be(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('numerical protocols', function () {
|
|
||||||
describe('True numbers (float)', function () {
|
|
||||||
describe('Basic arithmetic', function () {
|
|
||||||
it('can be added', function () {
|
|
||||||
expect(py.eval('1 + 1')).to.be(2);
|
|
||||||
expect(py.eval('1.5 + 2')).to.be(3.5);
|
|
||||||
expect(py.eval('1 + -1')).to.be(0);
|
|
||||||
});
|
|
||||||
it('can be subtracted', function () {
|
|
||||||
expect(py.eval('1 - 1')).to.be(0);
|
|
||||||
expect(py.eval('1.5 - 2')).to.be(-0.5);
|
|
||||||
expect(py.eval('2 - 1.5')).to.be(0.5);
|
|
||||||
});
|
|
||||||
it('can be multiplied', function () {
|
|
||||||
expect(py.eval('1 * 3')).to.be(3);
|
|
||||||
expect(py.eval('0 * 5')).to.be(0);
|
|
||||||
expect(py.eval('42 * -2')).to.be(-84);
|
|
||||||
});
|
|
||||||
it('can be divided', function () {
|
|
||||||
expect(py.eval('1 / 2')).to.be(0.5);
|
|
||||||
expect(py.eval('2 / 1')).to.be(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('Strings', function () {
|
|
||||||
describe('Basic arithmetics operators', function () {
|
|
||||||
it('can be added (concatenation)', function () {
|
|
||||||
expect(py.eval('"foo" + "bar"')).to.be('foobar');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('Type converter', function () {
|
|
||||||
it('should convert bare objects to objects', function () {
|
|
||||||
expect(py.eval('foo.bar', {foo: {bar: 3}})).to.be(3);
|
|
||||||
});
|
|
||||||
it('should convert arrays to lists', function () {
|
|
||||||
expect(py.eval('foo[3]', {foo: [9, 8, 7, 6, 5]}))
|
|
||||||
.to.be(6);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -652,7 +652,7 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.openerp .oe_dropdown_toggle {
|
.openerp .oe_dropdown_toggle {
|
||||||
color: #4C4C4C;
|
color: #4c4c4c;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
.openerp .oe_dropdown_hover:hover .oe_dropdown_menu, .openerp .oe_dropdown_menu.oe_opened {
|
.openerp .oe_dropdown_hover:hover .oe_dropdown_menu, .openerp .oe_dropdown_menu.oe_opened {
|
||||||
|
@ -795,6 +795,20 @@
|
||||||
.openerp .oe_notification {
|
.openerp .oe_notification {
|
||||||
z-index: 1050;
|
z-index: 1050;
|
||||||
}
|
}
|
||||||
|
.openerp .oe_webclient_timezone_notification a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.openerp .oe_webclient_timezone_notification p {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
.openerp .oe_webclient_timezone_notification dt {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.openerp .oe_timezone_systray span {
|
||||||
|
margin-top: 1px;
|
||||||
|
background-color: #f6cf3b;
|
||||||
|
}
|
||||||
.openerp .oe_login {
|
.openerp .oe_login {
|
||||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAKUlEQVQIHWO8e/fufwYsgAUkJigoiCIF5DMyoYggcUiXgNnBiGQKmAkARpcEQeriln4AAAAASUVORK5CYII=);
|
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAKUlEQVQIHWO8e/fufwYsgAUkJigoiCIF5DMyoYggcUiXgNnBiGQKmAkARpcEQeriln4AAAAASUVORK5CYII=);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -2249,6 +2263,7 @@
|
||||||
.openerp .oe_form .oe_form_field_url button img {
|
.openerp .oe_form .oe_form_field_url button img {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
.openerp .oe_form .oe_form_field_monetary,
|
||||||
.openerp .oe_form .oe_form_field_date,
|
.openerp .oe_form .oe_form_field_date,
|
||||||
.openerp .oe_form .oe_form_field_datetime {
|
.openerp .oe_form .oe_form_field_datetime {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -2348,7 +2363,7 @@
|
||||||
.openerp .oe_form .oe_form_field_image .oe_form_field_image_controls {
|
.openerp .oe_form .oe_form_field_image .oe_form_field_image_controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1px;
|
top: 1px;
|
||||||
padding: 4px;
|
padding: 4px 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: none;
|
display: none;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -2376,6 +2391,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
left: 2px;
|
left: 2px;
|
||||||
top: 7px;
|
top: 7px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.openerp .oe_fileupload .oe_add button {
|
.openerp .oe_fileupload .oe_add button {
|
||||||
display: inline;
|
display: inline;
|
||||||
|
@ -2402,10 +2418,11 @@
|
||||||
}
|
}
|
||||||
.openerp .oe_fileupload .oe_add input.oe_form_binary_file {
|
.openerp .oe_fileupload .oe_add input.oe_form_binary_file {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: -5px;
|
margin-left: -85px;
|
||||||
height: 28px;
|
height: 22px;
|
||||||
width: 52px;
|
width: 152px;
|
||||||
margin-top: -26px;
|
margin-top: -24px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.openerp .oe_fileupload .oe_add .oe_attach_label {
|
.openerp .oe_fileupload .oe_add .oe_attach_label {
|
||||||
color: #7c7bad;
|
color: #7c7bad;
|
||||||
|
|
|
@ -671,9 +671,21 @@ $sheet-padding: 16px
|
||||||
border-bottom-right-radius: 8px
|
border-bottom-right-radius: 8px
|
||||||
border-bottom-left-radius: 8px
|
border-bottom-left-radius: 8px
|
||||||
// }}}
|
// }}}
|
||||||
// Notification {{{
|
// Notifications {{{
|
||||||
.oe_notification
|
.oe_notification
|
||||||
z-index: 1050
|
z-index: 1050
|
||||||
|
.oe_webclient_timezone_notification
|
||||||
|
a
|
||||||
|
color: white
|
||||||
|
text-decoration: underline
|
||||||
|
p
|
||||||
|
margin-top: 1em
|
||||||
|
dt
|
||||||
|
font-weight: bold
|
||||||
|
.oe_timezone_systray
|
||||||
|
span
|
||||||
|
margin-top: 1px
|
||||||
|
background-color: #f6cf3b
|
||||||
// }}}
|
// }}}
|
||||||
// Login {{{
|
// Login {{{
|
||||||
.oe_login
|
.oe_login
|
||||||
|
@ -1788,6 +1800,7 @@ $sheet-padding: 16px
|
||||||
border-left: 8px solid #eee
|
border-left: 8px solid #eee
|
||||||
.oe_form_field_url button img
|
.oe_form_field_url button img
|
||||||
vertical-align: top
|
vertical-align: top
|
||||||
|
.oe_form_field_monetary,
|
||||||
.oe_form_field_date,
|
.oe_form_field_date,
|
||||||
.oe_form_field_datetime
|
.oe_form_field_datetime
|
||||||
white-space: nowrap
|
white-space: nowrap
|
||||||
|
@ -1880,7 +1893,7 @@ $sheet-padding: 16px
|
||||||
.oe_form_field_image_controls
|
.oe_form_field_image_controls
|
||||||
position: absolute
|
position: absolute
|
||||||
top: 1px
|
top: 1px
|
||||||
padding: 4px
|
padding: 4px 0
|
||||||
width: 100%
|
width: 100%
|
||||||
display: none
|
display: none
|
||||||
text-align: center
|
text-align: center
|
||||||
|
@ -1900,6 +1913,7 @@ $sheet-padding: 16px
|
||||||
width: 100%
|
width: 100%
|
||||||
left: +2px
|
left: +2px
|
||||||
top: +7px
|
top: +7px
|
||||||
|
overflow: hidden
|
||||||
button
|
button
|
||||||
display: inline
|
display: inline
|
||||||
height: 24px
|
height: 24px
|
||||||
|
@ -1922,10 +1936,11 @@ $sheet-padding: 16px
|
||||||
left: -9px
|
left: -9px
|
||||||
input.oe_form_binary_file
|
input.oe_form_binary_file
|
||||||
display: inline-block
|
display: inline-block
|
||||||
margin-left: -5px
|
margin-left: -85px
|
||||||
height: 28px
|
height: 22px
|
||||||
width: 52px
|
width: 152px
|
||||||
margin-top: -26px
|
margin-top: -24px
|
||||||
|
cursor: pointer
|
||||||
.oe_attach_label
|
.oe_attach_label
|
||||||
color: #7C7BAD
|
color: #7C7BAD
|
||||||
margin-left: -3px
|
margin-left: -3px
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
* @param {Array|String} modules list of modules to initialize
|
* @param {Array|String} modules list of modules to initialize
|
||||||
*/
|
*/
|
||||||
init: function(modules) {
|
init: function(modules) {
|
||||||
if (modules === "fuck your shit, don't load anything you cunt") {
|
if (modules === null) {
|
||||||
modules = [];
|
modules = [];
|
||||||
} else {
|
} else {
|
||||||
modules = _.union(['web'], modules || []);
|
modules = _.union(['web'], modules || []);
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
* OpenERP Web web module split
|
* OpenERP Web web module split
|
||||||
*---------------------------------------------------------*/
|
*---------------------------------------------------------*/
|
||||||
openerp.web = function(session) {
|
openerp.web = function(session) {
|
||||||
var files = ["corelib","coresetup","dates","formats","chrome","data","views","search","list","form","list_editable","web_mobile","view_tree","data_export","data_import"];
|
var files = ["pyeval", "corelib","coresetup","dates","formats","chrome","data","views","search","list","form","list_editable","web_mobile","view_tree","data_export","data_import"];
|
||||||
for(var i=0; i<files.length; i++) {
|
for(var i=0; i<files.length; i++) {
|
||||||
if(openerp.web[files[i]]) {
|
if(openerp.web[files[i]]) {
|
||||||
openerp.web[files[i]](session);
|
openerp.web[files[i]](session);
|
||||||
|
|
|
@ -24,7 +24,7 @@ instance.web.Notification = instance.web.Widget.extend({
|
||||||
if (sticky) {
|
if (sticky) {
|
||||||
opts.expires = false;
|
opts.expires = false;
|
||||||
}
|
}
|
||||||
this.$el.notify('create', {
|
return this.$el.notify('create', {
|
||||||
title: title,
|
title: title,
|
||||||
text: text
|
text: text
|
||||||
}, opts);
|
}, opts);
|
||||||
|
@ -35,7 +35,7 @@ instance.web.Notification = instance.web.Widget.extend({
|
||||||
if (sticky) {
|
if (sticky) {
|
||||||
opts.expires = false;
|
opts.expires = false;
|
||||||
}
|
}
|
||||||
this.$el.notify('create', 'oe_notification_alert', {
|
return this.$el.notify('create', 'oe_notification_alert', {
|
||||||
title: title,
|
title: title,
|
||||||
text: text
|
text: text
|
||||||
}, opts);
|
}, opts);
|
||||||
|
@ -289,7 +289,7 @@ instance.web.CrashManager = instance.web.Class.extend({
|
||||||
});
|
});
|
||||||
|
|
||||||
instance.web.Loading = instance.web.Widget.extend({
|
instance.web.Loading = instance.web.Widget.extend({
|
||||||
template: 'Loading',
|
template: _t("Loading"),
|
||||||
init: function(parent) {
|
init: function(parent) {
|
||||||
this._super(parent);
|
this._super(parent);
|
||||||
this.count = 0;
|
this.count = 0;
|
||||||
|
@ -390,11 +390,11 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
|
||||||
self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
|
self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
|
||||||
self.$el.find("form[name=change_pwd_form]").validate({
|
self.$el.find("form[name=change_pwd_form]").validate({
|
||||||
messages: {
|
messages: {
|
||||||
old_pwd: "Please enter your previous password",
|
old_pwd: _t("Please enter your previous password"),
|
||||||
new_pwd: "Please enter your new password",
|
new_pwd: _t("Please enter your new password"),
|
||||||
confirm_pwd: {
|
confirm_pwd: {
|
||||||
required: "Please confirm your new password",
|
required: _t("Please confirm your new password"),
|
||||||
equalTo: "The confirmation does not match the password"
|
equalTo: _t("The confirmation does not match the password")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
submitHandler: self.do_change_password
|
submitHandler: self.do_change_password
|
||||||
|
@ -478,7 +478,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
|
||||||
self.display_error(result);
|
self.display_error(result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.do_notify("Duplicating database", "The database has been duplicated.");
|
self.do_notify(_t("Duplicating database"), _t("The database has been duplicated."));
|
||||||
self.start();
|
self.start();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -488,7 +488,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
|
||||||
fields = $form.serializeArray(),
|
fields = $form.serializeArray(),
|
||||||
$db_list = $form.find('[name=drop_db]'),
|
$db_list = $form.find('[name=drop_db]'),
|
||||||
db = $db_list.val();
|
db = $db_list.val();
|
||||||
if (!db || !confirm("Do you really want to delete the database: " + db + " ?")) {
|
if (!db || !confirm(_.str.sprintf(_t("Do you really want to delete the database: %s ?"), db))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
|
self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
|
||||||
|
@ -496,7 +496,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
|
||||||
self.display_error(result);
|
self.display_error(result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.do_notify("Dropping database", "The database '" + db + "' has been dropped");
|
self.do_notify(_t("Dropping database"), _.str.sprintf(_t("The database %s has been dropped"), db));
|
||||||
self.start();
|
self.start();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -511,7 +511,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
|
||||||
error: function(error){
|
error: function(error){
|
||||||
if(error){
|
if(error){
|
||||||
self.display_error({
|
self.display_error({
|
||||||
title: 'Backup Database',
|
title: _t("Backup Database"),
|
||||||
error: 'AccessDenied'
|
error: 'AccessDenied'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -534,13 +534,13 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
|
||||||
|
|
||||||
if (body.indexOf('403 Forbidden') !== -1) {
|
if (body.indexOf('403 Forbidden') !== -1) {
|
||||||
self.display_error({
|
self.display_error({
|
||||||
title: 'Access Denied',
|
title: _t("Access Denied"),
|
||||||
error: 'Incorrect super-administrator password'
|
error: _t("Incorrect super-administrator password")
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
self.display_error({
|
self.display_error({
|
||||||
title: 'Restore Database',
|
title: _t("Restore Database"),
|
||||||
error: 'Could not restore the database'
|
error: _t("Could not restore the database")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -560,7 +560,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.unblockUI();
|
self.unblockUI();
|
||||||
self.do_notify("Changed Password", "Password has been changed successfully");
|
self.do_notify(_t("Changed Password"), _t("Password has been changed successfully"));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
do_exit: function () {
|
do_exit: function () {
|
||||||
|
@ -642,7 +642,7 @@ instance.web.Login = instance.web.Widget.extend({
|
||||||
}
|
}
|
||||||
var db = this.$("form [name=db]").val();
|
var db = this.$("form [name=db]").val();
|
||||||
if (!db) {
|
if (!db) {
|
||||||
this.do_warn("Login", "No database selected !");
|
this.do_warn(_t("Login"), _t("No database selected !"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var login = this.$("form input[name=login]").val();
|
var login = this.$("form input[name=login]").val();
|
||||||
|
@ -678,7 +678,7 @@ instance.web.Login = instance.web.Widget.extend({
|
||||||
self.trigger('login_successful');
|
self.trigger('login_successful');
|
||||||
}, function () {
|
}, function () {
|
||||||
self.$(".oe_login_pane").fadeIn("fast", function() {
|
self.$(".oe_login_pane").fadeIn("fast", function() {
|
||||||
self.show_error("Invalid username or password");
|
self.show_error(_t("Invalid username or password"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -765,7 +765,7 @@ instance.web.ChangePassword = instance.web.Widget.extend({
|
||||||
template: "ChangePassword",
|
template: "ChangePassword",
|
||||||
start: function() {
|
start: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.getParent().dialog_title = "Change Password";
|
this.getParent().dialog_title = _t("Change Password");
|
||||||
var $button = self.$el.find('.oe_form_button');
|
var $button = self.$el.find('.oe_form_button');
|
||||||
$button.appendTo(this.getParent().$buttons);
|
$button.appendTo(this.getParent().$buttons);
|
||||||
$button.eq(2).click(function(){
|
$button.eq(2).click(function(){
|
||||||
|
@ -822,7 +822,7 @@ instance.web.Menu = instance.web.Widget.extend({
|
||||||
this.renderElement();
|
this.renderElement();
|
||||||
this.limit_entries();
|
this.limit_entries();
|
||||||
// Hide toplevel item if there is only one
|
// Hide toplevel item if there is only one
|
||||||
var $toplevel = this.$("li")
|
var $toplevel = this.$("li");
|
||||||
if($toplevel.length == 1) {
|
if($toplevel.length == 1) {
|
||||||
$toplevel.hide();
|
$toplevel.hide();
|
||||||
}
|
}
|
||||||
|
@ -1106,7 +1106,6 @@ instance.web.WebClient = instance.web.Client.extend({
|
||||||
start: function() {
|
start: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
return $.when(this._super()).then(function() {
|
return $.when(this._super()).then(function() {
|
||||||
self.$(".oe_logo").attr("href", $.param.fragment("" + window.location, "", 2).slice(0, -1));
|
|
||||||
if (jQuery.param !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
|
if (jQuery.param !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
|
||||||
$("body").addClass("kitten-mode-activated");
|
$("body").addClass("kitten-mode-activated");
|
||||||
if ($.blockUI) {
|
if ($.blockUI) {
|
||||||
|
@ -1163,6 +1162,36 @@ instance.web.WebClient = instance.web.Client.extend({
|
||||||
self.user_menu.do_update();
|
self.user_menu.do_update();
|
||||||
self.bind_hashchange();
|
self.bind_hashchange();
|
||||||
self.set_title();
|
self.set_title();
|
||||||
|
self.check_timezone();
|
||||||
|
},
|
||||||
|
check_timezone: function() {
|
||||||
|
var self = this;
|
||||||
|
return new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']]).then(function(result) {
|
||||||
|
var user_offset = result[0]['tz_offset'];
|
||||||
|
var offset = -(new Date().getTimezoneOffset());
|
||||||
|
// _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
|
||||||
|
var browser_offset = (offset < 0) ? "-" : "+";
|
||||||
|
browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
|
||||||
|
browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
|
||||||
|
if (browser_offset !== user_offset) {
|
||||||
|
var $icon = $(QWeb.render('WebClient.timezone_systray'));
|
||||||
|
$icon.on('click', function() {
|
||||||
|
var notification = self.do_warn(_t("Timezone mismatch"), QWeb.render('WebClient.timezone_notification', {
|
||||||
|
user_timezone: instance.session.user_context.tz || 'UTC',
|
||||||
|
user_offset: user_offset,
|
||||||
|
browser_offset: browser_offset,
|
||||||
|
}), true);
|
||||||
|
notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
|
||||||
|
notification.close();
|
||||||
|
}).find('a').on('click', function() {
|
||||||
|
notification.close();
|
||||||
|
self.user_menu.on_menu_settings();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$icon.appendTo(self.$('.oe_systray'));
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
destroy_content: function() {
|
destroy_content: function() {
|
||||||
_.each(_.clone(this.getChildren()), function(el) {
|
_.each(_.clone(this.getChildren()), function(el) {
|
||||||
|
@ -1179,11 +1208,11 @@ instance.web.WebClient = instance.web.Client.extend({
|
||||||
},
|
},
|
||||||
do_notify: function() {
|
do_notify: function() {
|
||||||
var n = this.notification;
|
var n = this.notification;
|
||||||
n.notify.apply(n, arguments);
|
return n.notify.apply(n, arguments);
|
||||||
},
|
},
|
||||||
do_warn: function() {
|
do_warn: function() {
|
||||||
var n = this.notification;
|
var n = this.notification;
|
||||||
n.warn.apply(n, arguments);
|
return n.warn.apply(n, arguments);
|
||||||
},
|
},
|
||||||
on_logout: function() {
|
on_logout: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
@ -1240,11 +1269,12 @@ instance.web.WebClient = instance.web.Client.extend({
|
||||||
var self = this;
|
var self = this;
|
||||||
return this.rpc("/web/action/load", { action_id: options.action_id })
|
return this.rpc("/web/action/load", { action_id: options.action_id })
|
||||||
.then(function (result) {
|
.then(function (result) {
|
||||||
var action = result;
|
|
||||||
if (options.needaction) {
|
if (options.needaction) {
|
||||||
action.context.search_default_message_unread = true;
|
result.context = new instance.web.CompoundContext(
|
||||||
|
result.context,
|
||||||
|
{search_default_message_unread: true});
|
||||||
}
|
}
|
||||||
return $.when(self.action_manager.do_action(action, {
|
return $.when(self.action_manager.do_action(result, {
|
||||||
clear_breadcrumbs: true,
|
clear_breadcrumbs: true,
|
||||||
action_menu_id: self.menu.current_menu,
|
action_menu_id: self.menu.current_menu,
|
||||||
})).fail(function() {
|
})).fail(function() {
|
||||||
|
|
|
@ -948,296 +948,6 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, {
|
||||||
this.server = this.origin; // keep chs happy
|
this.server = this.origin; // keep chs happy
|
||||||
this.rpc_function = (this.origin == window_origin) ? this.rpc_json : this.rpc_jsonp;
|
this.rpc_function = (this.origin == window_origin) ? this.rpc_json : this.rpc_jsonp;
|
||||||
},
|
},
|
||||||
test_eval_get_context: function () {
|
|
||||||
var asJS = function (arg) {
|
|
||||||
if (arg instanceof py.object) {
|
|
||||||
return arg.toJSON();
|
|
||||||
}
|
|
||||||
return arg;
|
|
||||||
};
|
|
||||||
|
|
||||||
var datetime = new py.object();
|
|
||||||
datetime.datetime = new py.type(function datetime() {
|
|
||||||
throw new Error('datetime.datetime not implemented');
|
|
||||||
});
|
|
||||||
var date = datetime.date = new py.type(function date(y, m, d) {
|
|
||||||
if (y instanceof Array) {
|
|
||||||
d = y[2];
|
|
||||||
m = y[1];
|
|
||||||
y = y[0];
|
|
||||||
}
|
|
||||||
this.year = asJS(y);
|
|
||||||
this.month = asJS(m);
|
|
||||||
this.day = asJS(d);
|
|
||||||
}, py.object, {
|
|
||||||
strftime: function (args) {
|
|
||||||
var f = asJS(args[0]), self = this;
|
|
||||||
return new py.str(f.replace(/%([A-Za-z])/g, function (m, c) {
|
|
||||||
switch (c) {
|
|
||||||
case 'Y': return self.year;
|
|
||||||
case 'm': return _.str.sprintf('%02d', self.month);
|
|
||||||
case 'd': return _.str.sprintf('%02d', self.day);
|
|
||||||
}
|
|
||||||
throw new Error('ValueError: No known conversion for ' + m);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
date.__getattribute__ = function (name) {
|
|
||||||
if (name === 'today') {
|
|
||||||
return date.today;
|
|
||||||
}
|
|
||||||
throw new Error("AttributeError: object 'date' has no attribute '" + name +"'");
|
|
||||||
};
|
|
||||||
date.today = new py.def(function () {
|
|
||||||
var d = new Date();
|
|
||||||
return new date(d.getFullYear(), d.getMonth() + 1, d.getDate());
|
|
||||||
});
|
|
||||||
datetime.time = new py.type(function time() {
|
|
||||||
throw new Error('datetime.time not implemented');
|
|
||||||
});
|
|
||||||
|
|
||||||
var time = new py.object();
|
|
||||||
time.strftime = new py.def(function (args) {
|
|
||||||
return date.today.__call__().strftime(args);
|
|
||||||
});
|
|
||||||
|
|
||||||
var relativedelta = new py.type(function relativedelta(args, kwargs) {
|
|
||||||
if (!_.isEmpty(args)) {
|
|
||||||
throw new Error('Extraction of relative deltas from existing datetimes not supported');
|
|
||||||
}
|
|
||||||
this.ops = kwargs;
|
|
||||||
}, py.object, {
|
|
||||||
__add__: function (other) {
|
|
||||||
if (!(other instanceof datetime.date)) {
|
|
||||||
return py.NotImplemented;
|
|
||||||
}
|
|
||||||
// TODO: test this whole mess
|
|
||||||
var year = asJS(this.ops.year) || asJS(other.year);
|
|
||||||
if (asJS(this.ops.years)) {
|
|
||||||
year += asJS(this.ops.years);
|
|
||||||
}
|
|
||||||
|
|
||||||
var month = asJS(this.ops.month) || asJS(other.month);
|
|
||||||
if (asJS(this.ops.months)) {
|
|
||||||
month += asJS(this.ops.months);
|
|
||||||
// FIXME: no divmod in JS?
|
|
||||||
while (month < 1) {
|
|
||||||
year -= 1;
|
|
||||||
month += 12;
|
|
||||||
}
|
|
||||||
while (month > 12) {
|
|
||||||
year += 1;
|
|
||||||
month -= 12;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastMonthDay = new Date(year, month, 0).getDate();
|
|
||||||
var day = asJS(this.ops.day) || asJS(other.day);
|
|
||||||
if (day > lastMonthDay) { day = lastMonthDay; }
|
|
||||||
var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0);
|
|
||||||
if (days_offset) {
|
|
||||||
day = new Date(year, month-1, day + days_offset).getDate();
|
|
||||||
}
|
|
||||||
// TODO: leapdays?
|
|
||||||
// TODO: hours, minutes, seconds? Not used in XML domains
|
|
||||||
// TODO: weekday?
|
|
||||||
return new datetime.date(year, month, day);
|
|
||||||
},
|
|
||||||
__radd__: function (other) {
|
|
||||||
return this.__add__(other);
|
|
||||||
},
|
|
||||||
|
|
||||||
__sub__: function (other) {
|
|
||||||
if (!(other instanceof datetime.date)) {
|
|
||||||
return py.NotImplemented;
|
|
||||||
}
|
|
||||||
// TODO: test this whole mess
|
|
||||||
var year = asJS(this.ops.year) || asJS(other.year);
|
|
||||||
if (asJS(this.ops.years)) {
|
|
||||||
year -= asJS(this.ops.years);
|
|
||||||
}
|
|
||||||
|
|
||||||
var month = asJS(this.ops.month) || asJS(other.month);
|
|
||||||
if (asJS(this.ops.months)) {
|
|
||||||
month -= asJS(this.ops.months);
|
|
||||||
// FIXME: no divmod in JS?
|
|
||||||
while (month < 1) {
|
|
||||||
year -= 1;
|
|
||||||
month += 12;
|
|
||||||
}
|
|
||||||
while (month > 12) {
|
|
||||||
year += 1;
|
|
||||||
month -= 12;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastMonthDay = new Date(year, month, 0).getDate();
|
|
||||||
var day = asJS(this.ops.day) || asJS(other.day);
|
|
||||||
if (day > lastMonthDay) { day = lastMonthDay; }
|
|
||||||
var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0);
|
|
||||||
if (days_offset) {
|
|
||||||
day = new Date(year, month-1, day - days_offset).getDate();
|
|
||||||
}
|
|
||||||
// TODO: leapdays?
|
|
||||||
// TODO: hours, minutes, seconds? Not used in XML domains
|
|
||||||
// TODO: weekday?
|
|
||||||
return new datetime.date(year, month, day);
|
|
||||||
},
|
|
||||||
__rsub__: function (other) {
|
|
||||||
return this.__sub__(other);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
uid: new py.float(this.uid),
|
|
||||||
datetime: datetime,
|
|
||||||
time: time,
|
|
||||||
relativedelta: relativedelta,
|
|
||||||
current_date: date.today.__call__().strftime(['%Y-%m-%d'])
|
|
||||||
};
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* FIXME: Huge testing hack, especially the evaluation context, rewrite + test for real before switching
|
|
||||||
*/
|
|
||||||
test_eval: function (source, expected) {
|
|
||||||
var match_template = '<ul>' +
|
|
||||||
'<li>Source: %(source)s</li>' +
|
|
||||||
'<li>Local: %(local)s</li>' +
|
|
||||||
'<li>Remote: %(remote)s</li>' +
|
|
||||||
'</ul>',
|
|
||||||
fail_template = '<ul>' +
|
|
||||||
'<li>Error: %(error)s</li>' +
|
|
||||||
'<li>Source: %(source)s</li>' +
|
|
||||||
'</ul>';
|
|
||||||
try {
|
|
||||||
// see Session.eval_context in Python
|
|
||||||
var ctx = this.test_eval_contexts(
|
|
||||||
([this.context] || []).concat(source.contexts));
|
|
||||||
if (!_.isEqual(ctx, expected.context)) {
|
|
||||||
instance.webclient.notification.warn('Context mismatch, report to xmo',
|
|
||||||
_.str.sprintf(match_template, {
|
|
||||||
source: JSON.stringify(source.contexts),
|
|
||||||
local: JSON.stringify(ctx),
|
|
||||||
remote: JSON.stringify(expected.context)
|
|
||||||
}), true);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
instance.webclient.notification.warn('Context fail, report to xmo',
|
|
||||||
_.str.sprintf(fail_template, {
|
|
||||||
error: e.message,
|
|
||||||
source: JSON.stringify(source.contexts)
|
|
||||||
}), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
var dom = this.test_eval_domains(source.domains, this.test_eval_get_context());
|
|
||||||
if (!_.isEqual(dom, expected.domain)) {
|
|
||||||
instance.webclient.notification.warn('Domains mismatch, report to xmo',
|
|
||||||
_.str.sprintf(match_template, {
|
|
||||||
source: JSON.stringify(source.domains),
|
|
||||||
local: JSON.stringify(dom),
|
|
||||||
remote: JSON.stringify(expected.domain)
|
|
||||||
}), true);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
instance.webclient.notification.warn('Domain fail, report to xmo',
|
|
||||||
_.str.sprintf(fail_template, {
|
|
||||||
error: e.message,
|
|
||||||
source: JSON.stringify(source.domains)
|
|
||||||
}), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
var groups = this.test_eval_groupby(source.group_by_seq);
|
|
||||||
if (!_.isEqual(groups, expected.group_by)) {
|
|
||||||
instance.webclient.notification.warn('GroupBy mismatch, report to xmo',
|
|
||||||
_.str.sprintf(match_template, {
|
|
||||||
source: JSON.stringify(source.group_by_seq),
|
|
||||||
local: JSON.stringify(groups),
|
|
||||||
remote: JSON.stringify(expected.group_by)
|
|
||||||
}), true);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
instance.webclient.notification.warn('GroupBy fail, report to xmo',
|
|
||||||
_.str.sprintf(fail_template, {
|
|
||||||
error: e.message,
|
|
||||||
source: JSON.stringify(source.group_by_seq)
|
|
||||||
}), true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
test_eval_contexts: function (contexts, evaluation_context) {
|
|
||||||
evaluation_context = evaluation_context || {};
|
|
||||||
var self = this;
|
|
||||||
return _(contexts).reduce(function (result_context, ctx) {
|
|
||||||
// __eval_context evaluations can lead to some of `contexts`'s
|
|
||||||
// values being null, skip them as well as empty contexts
|
|
||||||
if (_.isEmpty(ctx)) { return result_context; }
|
|
||||||
var evaluated = ctx;
|
|
||||||
switch(ctx.__ref) {
|
|
||||||
case 'context':
|
|
||||||
evaluated = py.eval(ctx.__debug, evaluation_context);
|
|
||||||
break;
|
|
||||||
case 'compound_context':
|
|
||||||
var eval_context = self.test_eval_contexts([ctx.__eval_context]);
|
|
||||||
evaluated = self.test_eval_contexts(
|
|
||||||
ctx.__contexts, _.extend({}, evaluation_context, eval_context));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// add newly evaluated context to evaluation context for following
|
|
||||||
// siblings
|
|
||||||
_.extend(evaluation_context, evaluated);
|
|
||||||
return _.extend(result_context, evaluated);
|
|
||||||
}, _.extend({}, this.user_context));
|
|
||||||
},
|
|
||||||
test_eval_domains: function (domains, evaluation_context) {
|
|
||||||
var result_domain = [], self = this;
|
|
||||||
_(domains).each(function (dom) {
|
|
||||||
switch(dom.__ref) {
|
|
||||||
case 'domain':
|
|
||||||
result_domain.push.apply(
|
|
||||||
result_domain, py.eval(dom.__debug, evaluation_context));
|
|
||||||
break;
|
|
||||||
case 'compound_domain':
|
|
||||||
var eval_context = self.test_eval_contexts([dom.__eval_context]);
|
|
||||||
result_domain.push.apply(
|
|
||||||
result_domain, self.test_eval_domains(
|
|
||||||
dom.__domains, _.extend(
|
|
||||||
{}, evaluation_context, eval_context)));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
result_domain.push.apply(
|
|
||||||
result_domain, dom);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result_domain;
|
|
||||||
},
|
|
||||||
test_eval_groupby: function (contexts) {
|
|
||||||
var result_group = [], self = this;
|
|
||||||
_(contexts).each(function (ctx) {
|
|
||||||
var group;
|
|
||||||
switch(ctx.__ref) {
|
|
||||||
case 'context':
|
|
||||||
group = py.eval(ctx.__debug).group_by;
|
|
||||||
break;
|
|
||||||
case 'compound_context':
|
|
||||||
group = self.test_eval_contexts(
|
|
||||||
ctx.__contexts, ctx.__eval_context).group_by;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
group = ctx.group_by
|
|
||||||
}
|
|
||||||
if (!group) { return; }
|
|
||||||
if (typeof group === 'string') {
|
|
||||||
result_group.push(group);
|
|
||||||
} else if (group instanceof Array) {
|
|
||||||
result_group.push.apply(result_group, group);
|
|
||||||
} else {
|
|
||||||
throw new Error('Got invalid groupby {{'
|
|
||||||
+ JSON.stringify(group) + '}}');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result_group;
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
* Executes an RPC call, registering the provided callbacks.
|
* Executes an RPC call, registering the provided callbacks.
|
||||||
*
|
*
|
||||||
|
@ -1259,6 +969,9 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, {
|
||||||
if (_.isString(url)) {
|
if (_.isString(url)) {
|
||||||
url = { url: url };
|
url = { url: url };
|
||||||
}
|
}
|
||||||
|
_.defaults(params, {
|
||||||
|
context: this.user_context || {}
|
||||||
|
});
|
||||||
// Construct a JSON-RPC2 request, method is currently unused
|
// Construct a JSON-RPC2 request, method is currently unused
|
||||||
if (this.debug)
|
if (this.debug)
|
||||||
params.debug = 1;
|
params.debug = 1;
|
||||||
|
@ -1271,22 +984,19 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, {
|
||||||
var deferred = $.Deferred();
|
var deferred = $.Deferred();
|
||||||
if (! options.shadow)
|
if (! options.shadow)
|
||||||
this.trigger('request', url, payload);
|
this.trigger('request', url, payload);
|
||||||
var request = this.rpc_function(url, payload).done(
|
|
||||||
|
this.rpc_function(url, payload).then(
|
||||||
function (response, textStatus, jqXHR) {
|
function (response, textStatus, jqXHR) {
|
||||||
if (! options.shadow)
|
if (! options.shadow)
|
||||||
self.trigger('response', response);
|
self.trigger('response', response);
|
||||||
if (!response.error) {
|
if (!response.error) {
|
||||||
if (url.url === '/web/session/eval_domain_and_context') {
|
|
||||||
self.test_eval(params, response.result);
|
|
||||||
}
|
|
||||||
deferred.resolve(response["result"], textStatus, jqXHR);
|
deferred.resolve(response["result"], textStatus, jqXHR);
|
||||||
} else if (response.error.data.type === "session_invalid") {
|
} else if (response.error.data.type === "session_invalid") {
|
||||||
self.uid = false;
|
self.uid = false;
|
||||||
} else {
|
} else {
|
||||||
deferred.reject(response.error, $.Event());
|
deferred.reject(response.error, $.Event());
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
).fail(
|
|
||||||
function(jqXHR, textStatus, errorThrown) {
|
function(jqXHR, textStatus, errorThrown) {
|
||||||
if (! options.shadow)
|
if (! options.shadow)
|
||||||
self.trigger('response_failed', jqXHR);
|
self.trigger('response_failed', jqXHR);
|
||||||
|
|
|
@ -622,7 +622,7 @@ var messages_by_seconds = function() {
|
||||||
[120, _t("Don't leave yet,<br />it's still loading...")],
|
[120, _t("Don't leave yet,<br />it's still loading...")],
|
||||||
[300, _t("You may not believe it,<br />but the application is actually loading...")],
|
[300, _t("You may not believe it,<br />but the application is actually loading...")],
|
||||||
[420, _t("Take a minute to get a coffee,<br />because it's loading...")],
|
[420, _t("Take a minute to get a coffee,<br />because it's loading...")],
|
||||||
[3600, _t("Maybe you should consider reloading the application by pressing F5...")],
|
[3600, _t("Maybe you should consider reloading the application by pressing F5...")]
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -61,8 +61,10 @@ instance.web.Query = instance.web.Class.extend({
|
||||||
return instance.session.rpc('/web/dataset/search_read', {
|
return instance.session.rpc('/web/dataset/search_read', {
|
||||||
model: this._model.name,
|
model: this._model.name,
|
||||||
fields: this._fields || false,
|
fields: this._fields || false,
|
||||||
domain: this._model.domain(this._filter),
|
domain: instance.web.pyeval.eval('domains',
|
||||||
context: this._model.context(this._context),
|
[this._model.domain(this._filter)]),
|
||||||
|
context: instance.web.pyeval.eval('contexts',
|
||||||
|
[this._model.context(this._context)]),
|
||||||
offset: this._offset,
|
offset: this._offset,
|
||||||
limit: this._limit,
|
limit: this._limit,
|
||||||
sort: instance.web.serialize_sort(this._order_by)
|
sort: instance.web.serialize_sort(this._order_by)
|
||||||
|
@ -121,9 +123,8 @@ instance.web.Query = instance.web.Class.extend({
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
// FIXME: when pyeval is merged
|
var ctx = instance.web.pyeval.eval(
|
||||||
var ctx = instance.session.test_eval_contexts(
|
'context', this._model.context(this._context));
|
||||||
[this._model.context(this._context)]);
|
|
||||||
return this._model.call('read_group', {
|
return this._model.call('read_group', {
|
||||||
groupby: grouping,
|
groupby: grouping,
|
||||||
fields: _.uniq(grouping.concat(this._fields || [])),
|
fields: _.uniq(grouping.concat(this._fields || [])),
|
||||||
|
@ -289,6 +290,7 @@ instance.web.Model = instance.web.Class.extend({
|
||||||
kwargs = args;
|
kwargs = args;
|
||||||
args = [];
|
args = [];
|
||||||
}
|
}
|
||||||
|
instance.web.pyeval.ensure_evaluated(args, kwargs);
|
||||||
var debug = instance.session.debug ? '/'+this.name+':'+method : '';
|
var debug = instance.session.debug ? '/'+this.name+':'+method : '';
|
||||||
return instance.session.rpc('/web/dataset/call_kw' + debug, {
|
return instance.session.rpc('/web/dataset/call_kw' + debug, {
|
||||||
model: this.name,
|
model: this.name,
|
||||||
|
@ -301,7 +303,7 @@ instance.web.Model = instance.web.Class.extend({
|
||||||
* Fetches a Query instance bound to this model, for searching
|
* Fetches a Query instance bound to this model, for searching
|
||||||
*
|
*
|
||||||
* @param {Array<String>} [fields] fields to ultimately fetch during the search
|
* @param {Array<String>} [fields] fields to ultimately fetch during the search
|
||||||
* @returns {openerp.web.Query}
|
* @returns {instance.web.Query}
|
||||||
*/
|
*/
|
||||||
query: function (fields) {
|
query: function (fields) {
|
||||||
return new instance.web.Query(this, fields);
|
return new instance.web.Query(this, fields);
|
||||||
|
@ -349,9 +351,11 @@ instance.web.Model = instance.web.Class.extend({
|
||||||
* FIXME: remove when evaluator integrated
|
* FIXME: remove when evaluator integrated
|
||||||
*/
|
*/
|
||||||
call_button: function (method, args) {
|
call_button: function (method, args) {
|
||||||
|
instance.web.pyeval.ensure_evaluated(args, {});
|
||||||
return instance.session.rpc('/web/dataset/call_button', {
|
return instance.session.rpc('/web/dataset/call_button', {
|
||||||
model: this.name,
|
model: this.name,
|
||||||
method: method,
|
method: method,
|
||||||
|
// Should not be necessary anymore. Integrate remote in this?
|
||||||
domain_id: null,
|
domain_id: null,
|
||||||
context_id: args.length - 1,
|
context_id: args.length - 1,
|
||||||
args: args || []
|
args: args || []
|
||||||
|
@ -606,7 +610,8 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin,
|
||||||
return instance.session.rpc('/web/dataset/resequence', {
|
return instance.session.rpc('/web/dataset/resequence', {
|
||||||
model: this.model,
|
model: this.model,
|
||||||
ids: ids,
|
ids: ids,
|
||||||
context: this.get_context(options.context),
|
context: instance.web.pyeval.eval(
|
||||||
|
'context', this.get_context(options.context)),
|
||||||
}).then(function (results) {
|
}).then(function (results) {
|
||||||
return results;
|
return results;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
|
||||||
openerp.web.dates = function(instance) {
|
openerp.web.dates = function(instance) {
|
||||||
|
var _t = instance.web._t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a string to a Date javascript object using OpenERP's
|
* Converts a string to a Date javascript object using OpenERP's
|
||||||
|
@ -18,11 +19,11 @@ instance.web.str_to_datetime = function(str) {
|
||||||
var regex = /^(\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)(?:\.\d+)?$/;
|
var regex = /^(\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)(?:\.\d+)?$/;
|
||||||
var res = regex.exec(str);
|
var res = regex.exec(str);
|
||||||
if ( !res ) {
|
if ( !res ) {
|
||||||
throw new Error("'" + str + "' is not a valid datetime");
|
throw new Error(_.str.sprintf(_t("'%s' is not a valid datetime"), str));
|
||||||
}
|
}
|
||||||
var obj = Date.parseExact(res[1] + " UTC", 'yyyy-MM-dd HH:mm:ss zzz');
|
var obj = Date.parseExact(res[1] + " UTC", 'yyyy-MM-dd HH:mm:ss zzz');
|
||||||
if (! obj) {
|
if (! obj) {
|
||||||
throw new Error("'" + str + "' is not a valid datetime");
|
throw new Error(_.str.sprintf(_t("'%s' is not a valid datetime"), str));
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
@ -45,11 +46,11 @@ instance.web.str_to_date = function(str) {
|
||||||
var regex = /^\d\d\d\d-\d\d-\d\d$/;
|
var regex = /^\d\d\d\d-\d\d-\d\d$/;
|
||||||
var res = regex.exec(str);
|
var res = regex.exec(str);
|
||||||
if ( !res ) {
|
if ( !res ) {
|
||||||
throw new Error("'" + str + "' is not a valid date");
|
throw new Error(_.str.sprintf(_t("'%s' is not a valid date"), str));
|
||||||
}
|
}
|
||||||
var obj = Date.parseExact(str, 'yyyy-MM-dd');
|
var obj = Date.parseExact(str, 'yyyy-MM-dd');
|
||||||
if (! obj) {
|
if (! obj) {
|
||||||
throw new Error("'" + str + "' is not a valid date");
|
throw new Error(_.str.sprintf(_t("'%s' is not a valid date"), str));
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
@ -72,11 +73,11 @@ instance.web.str_to_time = function(str) {
|
||||||
var regex = /^(\d\d:\d\d:\d\d)(?:\.\d+)?$/;
|
var regex = /^(\d\d:\d\d:\d\d)(?:\.\d+)?$/;
|
||||||
var res = regex.exec(str);
|
var res = regex.exec(str);
|
||||||
if ( !res ) {
|
if ( !res ) {
|
||||||
throw new Error("'" + str + "' is not a valid time");
|
throw new Error(_.str.sprintf(_t("'%s' is not a valid time"), str));
|
||||||
}
|
}
|
||||||
var obj = Date.parseExact("1970-01-01 " + res[1], 'yyyy-MM-dd HH:mm:ss');
|
var obj = Date.parseExact("1970-01-01 " + res[1], 'yyyy-MM-dd HH:mm:ss');
|
||||||
if (! obj) {
|
if (! obj) {
|
||||||
throw new Error("'" + str + "' is not a valid time");
|
throw new Error(_.str.sprintf(_t("'%s' is not a valid time"), str));
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
|
@ -224,7 +224,7 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
|
||||||
} while(tmp !== value);
|
} while(tmp !== value);
|
||||||
tmp = Number(value);
|
tmp = Number(value);
|
||||||
if (isNaN(tmp))
|
if (isNaN(tmp))
|
||||||
throw new Error(value + " is not a correct integer");
|
throw new Error(_.str.sprintf(_t("'%s' is not a correct integer"), value));
|
||||||
return tmp;
|
return tmp;
|
||||||
case 'float':
|
case 'float':
|
||||||
var tmp = Number(value);
|
var tmp = Number(value);
|
||||||
|
@ -239,7 +239,7 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
|
||||||
var reformatted_value = tmp.replace(instance.web._t.database.parameters.decimal_point, ".");
|
var reformatted_value = tmp.replace(instance.web._t.database.parameters.decimal_point, ".");
|
||||||
var parsed = Number(reformatted_value);
|
var parsed = Number(reformatted_value);
|
||||||
if (isNaN(parsed))
|
if (isNaN(parsed))
|
||||||
throw new Error(value + " is not a correct float");
|
throw new Error(_.str.sprintf(_t("'%s' is not a correct float"), value));
|
||||||
return parsed;
|
return parsed;
|
||||||
case 'float_time':
|
case 'float_time':
|
||||||
var factor = 1;
|
var factor = 1;
|
||||||
|
@ -263,7 +263,7 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
|
||||||
datetime = Date.parse(value);
|
datetime = Date.parse(value);
|
||||||
if (datetime !== null)
|
if (datetime !== null)
|
||||||
return instance.web.datetime_to_str(datetime);
|
return instance.web.datetime_to_str(datetime);
|
||||||
throw new Error(value + " is not a valid datetime");
|
throw new Error(_.str.sprintf(_t("'%s' is not a correct datetime"), value));
|
||||||
case 'date':
|
case 'date':
|
||||||
var date = Date.parseExact(value, date_pattern);
|
var date = Date.parseExact(value, date_pattern);
|
||||||
if (date !== null)
|
if (date !== null)
|
||||||
|
@ -271,7 +271,7 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
|
||||||
date = Date.parse(value);
|
date = Date.parse(value);
|
||||||
if (date !== null)
|
if (date !== null)
|
||||||
return instance.web.date_to_str(date);
|
return instance.web.date_to_str(date);
|
||||||
throw new Error(value + " is not a valid date");
|
throw new Error(_.str.sprintf(_t("'%s' is not a correct date"), value));
|
||||||
case 'time':
|
case 'time':
|
||||||
var time = Date.parseExact(value, time_pattern);
|
var time = Date.parseExact(value, time_pattern);
|
||||||
if (time !== null)
|
if (time !== null)
|
||||||
|
@ -279,7 +279,7 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
|
||||||
time = Date.parse(value);
|
time = Date.parse(value);
|
||||||
if (time !== null)
|
if (time !== null)
|
||||||
return instance.web.time_to_str(time);
|
return instance.web.time_to_str(time);
|
||||||
throw new Error(value + " is not a valid time");
|
throw new Error(_.str.sprintf(_t("'%s' is not a correct time"), value));
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
@ -294,7 +294,7 @@ instance.web.auto_str_to_date = function(value, type) {
|
||||||
try {
|
try {
|
||||||
return instance.web.str_to_time(value);
|
return instance.web.str_to_time(value);
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
throw new Error("'" + value + "' is not a valid date, datetime nor time");
|
throw new Error(_.str.sprintf(_t("'%s' is not a correct date, datetime nor time"), value));
|
||||||
};
|
};
|
||||||
|
|
||||||
instance.web.auto_date_to_str = function(value, type) {
|
instance.web.auto_date_to_str = function(value, type) {
|
||||||
|
@ -306,7 +306,7 @@ instance.web.auto_date_to_str = function(value, type) {
|
||||||
case 'time':
|
case 'time':
|
||||||
return instance.web.time_to_str(value);
|
return instance.web.time_to_str(value);
|
||||||
default:
|
default:
|
||||||
throw new Error(type + " is not convertible to date, datetime nor time");
|
throw new Error(_.str.sprintf(_t("'%s' is not convertible to date, datetime nor time"), type));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,776 @@
|
||||||
|
/*
|
||||||
|
* py.js helpers and setup
|
||||||
|
*/
|
||||||
|
openerp.web.pyeval = function (instance) {
|
||||||
|
instance.web.pyeval = {};
|
||||||
|
|
||||||
|
var obj = function () {};
|
||||||
|
obj.prototype = py.object;
|
||||||
|
var asJS = function (arg) {
|
||||||
|
if (arg instanceof obj) {
|
||||||
|
return arg.toJSON();
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
};
|
||||||
|
|
||||||
|
var datetime = py.PY_call(py.object);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* computes (Math.floor(a/b), a%b and passes that to the callback.
|
||||||
|
*
|
||||||
|
* returns the callback's result
|
||||||
|
*/
|
||||||
|
var divmod = function (a, b, fn) {
|
||||||
|
var mod = a%b;
|
||||||
|
// in python, sign(a % b) === sign(b). Not in JS. If wrong side, add a
|
||||||
|
// round of b
|
||||||
|
if (mod > 0 && b < 0 || mod < 0 && b > 0) {
|
||||||
|
mod += b;
|
||||||
|
}
|
||||||
|
return fn(Math.floor(a/b), mod);
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Passes the fractional and integer parts of x to the callback, returns
|
||||||
|
* the callback's result
|
||||||
|
*/
|
||||||
|
var modf = function (x, fn) {
|
||||||
|
var mod = x%1;
|
||||||
|
if (mod < 0) {
|
||||||
|
mod += 1;
|
||||||
|
}
|
||||||
|
return fn(mod, Math.floor(x));
|
||||||
|
};
|
||||||
|
var zero = py.float.fromJSON(0);
|
||||||
|
|
||||||
|
// Port from pypy/lib_pypy/datetime.py
|
||||||
|
var DAYS_IN_MONTH = [null, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||||
|
var DAYS_BEFORE_MONTH = [null];
|
||||||
|
var dbm = 0;
|
||||||
|
for (var i=1; i<DAYS_IN_MONTH.length; ++i) {
|
||||||
|
DAYS_BEFORE_MONTH.push(dbm);
|
||||||
|
dbm += DAYS_IN_MONTH[i];
|
||||||
|
}
|
||||||
|
var is_leap = function (year) {
|
||||||
|
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
||||||
|
};
|
||||||
|
var days_before_year = function (year) {
|
||||||
|
var y = year - 1;
|
||||||
|
return y*365 + Math.floor(y/4) - Math.floor(y/100) + Math.floor(y/400);
|
||||||
|
};
|
||||||
|
var days_in_month = function (year, month) {
|
||||||
|
if (month === 2 && is_leap(year)) {
|
||||||
|
return 29;
|
||||||
|
}
|
||||||
|
return DAYS_IN_MONTH[month];
|
||||||
|
};
|
||||||
|
var days_before_month = function (year, month) {
|
||||||
|
var post_leap_feb = month > 2 && is_leap(year);
|
||||||
|
return DAYS_BEFORE_MONTH[month]
|
||||||
|
+ (post_leap_feb ? 1 : 0);
|
||||||
|
};
|
||||||
|
var ymd2ord = function (year, month, day) {
|
||||||
|
var dim = days_in_month(year, month);
|
||||||
|
if (!(1 <= day && day <= dim)) {
|
||||||
|
throw new Error("ValueError: day must be in 1.." + dim);
|
||||||
|
}
|
||||||
|
return days_before_year(year)
|
||||||
|
+ days_before_month(year, month)
|
||||||
|
+ day;
|
||||||
|
};
|
||||||
|
var DI400Y = days_before_year(401);
|
||||||
|
var DI100Y = days_before_year(101);
|
||||||
|
var DI4Y = days_before_year(5);
|
||||||
|
var assert = function (bool) {
|
||||||
|
if (!bool) {
|
||||||
|
throw new Error("AssertionError");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var ord2ymd = function (n) {
|
||||||
|
--n;
|
||||||
|
var n400, n100, n4, n1, n0;
|
||||||
|
divmod(n, DI400Y, function (_n400, n) {
|
||||||
|
n400 = _n400;
|
||||||
|
divmod(n, DI100Y, function (_n100, n) {
|
||||||
|
n100 = _n100;
|
||||||
|
divmod(n, DI4Y, function (_n4, n) {
|
||||||
|
n4 = _n4;
|
||||||
|
divmod(n, 365, function (_n1, n) {
|
||||||
|
n1 = _n1;
|
||||||
|
n0 = n;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
n = n0;
|
||||||
|
var year = n400 * 400 + 1 + n100 * 100 + n4 * 4 + n1;
|
||||||
|
if (n1 == 4 || n100 == 100) {
|
||||||
|
assert(n0 === 0);
|
||||||
|
return {
|
||||||
|
year: year - 1,
|
||||||
|
month: 12,
|
||||||
|
day: 31
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var leapyear = n1 === 3 && (n4 !== 24 || n100 == 3);
|
||||||
|
assert(leapyear == is_leap(year));
|
||||||
|
var month = (n + 50) >> 5;
|
||||||
|
var preceding = DAYS_BEFORE_MONTH[month] + ((month > 2 && leapyear) ? 1 : 0);
|
||||||
|
if (preceding > n) {
|
||||||
|
--month;
|
||||||
|
preceding -= DAYS_IN_MONTH[month] + ((month === 2 && leapyear) ? 1 : 0);
|
||||||
|
}
|
||||||
|
n -= preceding;
|
||||||
|
return {
|
||||||
|
year: year,
|
||||||
|
month: month,
|
||||||
|
day: n+1
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the stuff passed in into a valid date, applying overflows as needed
|
||||||
|
*/
|
||||||
|
var tmxxx = function (year, month, day, hour, minute, second, microsecond) {
|
||||||
|
hour = hour || 0; minute = minute || 0; second = second || 0;
|
||||||
|
microsecond = microsecond || 0;
|
||||||
|
|
||||||
|
if (microsecond < 0 || microsecond > 999999) {
|
||||||
|
divmod(microsecond, 1000000, function (carry, ms) {
|
||||||
|
microsecond = ms;
|
||||||
|
second += carry
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (second < 0 || second > 59) {
|
||||||
|
divmod(second, 60, function (carry, s) {
|
||||||
|
second = s;
|
||||||
|
minute += carry;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (minute < 0 || minute > 59) {
|
||||||
|
divmod(minute, 60, function (carry, m) {
|
||||||
|
minute = m;
|
||||||
|
hour += carry;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (hour < 0 || hour > 23) {
|
||||||
|
divmod(hour, 24, function (carry, h) {
|
||||||
|
hour = h;
|
||||||
|
day += carry;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// That was easy. Now it gets muddy: the proper range for day
|
||||||
|
// can't be determined without knowing the correct month and year,
|
||||||
|
// but if day is, e.g., plus or minus a million, the current month
|
||||||
|
// and year values make no sense (and may also be out of bounds
|
||||||
|
// themselves).
|
||||||
|
// Saying 12 months == 1 year should be non-controversial.
|
||||||
|
if (month < 1 || month > 12) {
|
||||||
|
divmod(month-1, 12, function (carry, m) {
|
||||||
|
month = m + 1;
|
||||||
|
year += carry;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Now only day can be out of bounds (year may also be out of bounds
|
||||||
|
// for a datetime object, but we don't care about that here).
|
||||||
|
// If day is out of bounds, what to do is arguable, but at least the
|
||||||
|
// method here is principled and explainable.
|
||||||
|
var dim = days_in_month(year, month);
|
||||||
|
if (day < 1 || day > dim) {
|
||||||
|
// Move day-1 days from the first of the month. First try to
|
||||||
|
// get off cheap if we're only one day out of range (adjustments
|
||||||
|
// for timezone alone can't be worse than that).
|
||||||
|
if (day === 0) {
|
||||||
|
--month;
|
||||||
|
if (month > 0) {
|
||||||
|
day = days_in_month(year, month);
|
||||||
|
} else {
|
||||||
|
--year; month=12; day=31;
|
||||||
|
}
|
||||||
|
} else if (day == dim + 1) {
|
||||||
|
++month;
|
||||||
|
day = 1;
|
||||||
|
if (month > 12) {
|
||||||
|
month = 1;
|
||||||
|
++year;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var r = ord2ymd(ymd2ord(year, month, 1) + (day - 1));
|
||||||
|
year = r.year;
|
||||||
|
month = r.month;
|
||||||
|
day = r.day;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
year: year,
|
||||||
|
month: month,
|
||||||
|
day: day,
|
||||||
|
hour: hour,
|
||||||
|
minute: minute,
|
||||||
|
second: second,
|
||||||
|
microsecond: microsecond
|
||||||
|
};
|
||||||
|
};
|
||||||
|
datetime.timedelta = py.type('timedelta', null, {
|
||||||
|
__init__: function () {
|
||||||
|
var args = py.PY_parseArgs(arguments, [
|
||||||
|
['days', zero], ['seconds', zero], ['microseconds', zero],
|
||||||
|
['milliseconds', zero], ['minutes', zero], ['hours', zero],
|
||||||
|
['weeks', zero]
|
||||||
|
]);
|
||||||
|
|
||||||
|
var d = 0, s = 0, m = 0;
|
||||||
|
var days = args.days.toJSON() + args.weeks.toJSON() * 7;
|
||||||
|
var seconds = args.seconds.toJSON()
|
||||||
|
+ args.minutes.toJSON() * 60
|
||||||
|
+ args.hours.toJSON() * 3600;
|
||||||
|
var microseconds = args.microseconds.toJSON()
|
||||||
|
+ args.milliseconds.toJSON() * 1000;
|
||||||
|
|
||||||
|
// Get rid of all fractions, and normalize s and us.
|
||||||
|
// Take a deep breath <wink>.
|
||||||
|
var daysecondsfrac = modf(days, function (dayfrac, days) {
|
||||||
|
d = days;
|
||||||
|
if (dayfrac) {
|
||||||
|
return modf(dayfrac * 24 * 3600, function (dsf, dsw) {
|
||||||
|
s = dsw;
|
||||||
|
return dsf;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
var secondsfrac = modf(seconds, function (sf, s) {
|
||||||
|
seconds = s;
|
||||||
|
return sf + daysecondsfrac;
|
||||||
|
});
|
||||||
|
divmod(seconds, 24*3600, function (days, seconds) {
|
||||||
|
d += days;
|
||||||
|
s += seconds
|
||||||
|
});
|
||||||
|
// seconds isn't referenced again before redefinition
|
||||||
|
|
||||||
|
microseconds += secondsfrac * 1e6;
|
||||||
|
divmod(microseconds, 1000000, function (seconds, microseconds) {
|
||||||
|
divmod(seconds, 24*3600, function (days, seconds) {
|
||||||
|
d += days;
|
||||||
|
s += seconds;
|
||||||
|
m += Math.round(microseconds);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Carrying still possible here?
|
||||||
|
|
||||||
|
this.days = d;
|
||||||
|
this.seconds = s;
|
||||||
|
this.microseconds = m;
|
||||||
|
},
|
||||||
|
__str__: function () {
|
||||||
|
var hh, mm, ss;
|
||||||
|
divmod(this.seconds, 60, function (m, s) {
|
||||||
|
divmod(m, 60, function (h, m) {
|
||||||
|
hh = h;
|
||||||
|
mm = m;
|
||||||
|
ss = s;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var s = _.str.sprintf("%d:%02d:%02d", hh, mm, ss);
|
||||||
|
if (this.days) {
|
||||||
|
s = _.str.sprintf("%d day%s, %s",
|
||||||
|
this.days,
|
||||||
|
(this.days != 1 && this.days != -1) ? 's' : '',
|
||||||
|
s);
|
||||||
|
}
|
||||||
|
if (this.microseconds) {
|
||||||
|
s = _.str.sprintf("%s.%06d", s, this.microseconds);
|
||||||
|
}
|
||||||
|
return py.str.fromJSON(s);
|
||||||
|
},
|
||||||
|
__eq__: function (other) {
|
||||||
|
if (!py.PY_isInstance(other, datetime.timedelta)) {
|
||||||
|
return py.False;
|
||||||
|
}
|
||||||
|
return (this.days === other.days
|
||||||
|
&& this.seconds === other.seconds
|
||||||
|
&& this.microseconds === other.microseconds)
|
||||||
|
? py.True : py.False;
|
||||||
|
},
|
||||||
|
__add__: function (other) {
|
||||||
|
if (!py.PY_isInstance(other, datetime.timedelta)) {
|
||||||
|
return py.NotImplemented;
|
||||||
|
}
|
||||||
|
return py.PY_call(datetime.timedelta, [
|
||||||
|
py.float.fromJSON(this.days + other.days),
|
||||||
|
py.float.fromJSON(this.seconds + other.seconds),
|
||||||
|
py.float.fromJSON(this.microseconds + other.microseconds)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
__radd__: function (other) { return this.__add__(other); },
|
||||||
|
__sub__: function (other) {
|
||||||
|
if (!py.PY_isInstance(other, datetime.timedelta)) {
|
||||||
|
return py.NotImplemented;
|
||||||
|
}
|
||||||
|
return py.PY_call(datetime.timedelta, [
|
||||||
|
py.float.fromJSON(this.days - other.days),
|
||||||
|
py.float.fromJSON(this.seconds - other.seconds),
|
||||||
|
py.float.fromJSON(this.microseconds - other.microseconds)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
__rsub__: function (other) {
|
||||||
|
if (!py.PY_isInstance(other, datetime.timedelta)) {
|
||||||
|
return py.NotImplemented;
|
||||||
|
}
|
||||||
|
return this.__neg__().__add__(other);
|
||||||
|
},
|
||||||
|
__neg__: function () {
|
||||||
|
return py.PY_call(datetime.timedelta, [
|
||||||
|
py.float.fromJSON(-this.days),
|
||||||
|
py.float.fromJSON(-this.seconds),
|
||||||
|
py.float.fromJSON(-this.microseconds)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
__pos__: function () { return this; },
|
||||||
|
__mul__: function (other) {
|
||||||
|
if (!py.PY_isInstance(other, py.float)) {
|
||||||
|
return py.NotImplemented;
|
||||||
|
}
|
||||||
|
var n = other.toJSON();
|
||||||
|
return py.PY_call(datetime.timedelta, [
|
||||||
|
py.float.fromJSON(this.days * n),
|
||||||
|
py.float.fromJSON(this.seconds * n),
|
||||||
|
py.float.fromJSON(this.microseconds * n)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
__rmul__: function (other) { return this.__mul__(other); },
|
||||||
|
__div__: function (other) {
|
||||||
|
if (!py.PY_isInstance(other, py.float)) {
|
||||||
|
return py.NotImplemented;
|
||||||
|
}
|
||||||
|
var usec = ((this.days * 24 * 3600) + this.seconds) * 1000000
|
||||||
|
+ this.microseconds;
|
||||||
|
return py.PY_call(
|
||||||
|
datetime.timedelta, [
|
||||||
|
zero, zero, py.float.fromJSON(usec / other.toJSON())]);
|
||||||
|
},
|
||||||
|
__floordiv__: function (other) { return this.__div__(other); },
|
||||||
|
total_seconds: function () {
|
||||||
|
return py.float.fromJSON(
|
||||||
|
this.days * 86400
|
||||||
|
+ this.seconds
|
||||||
|
+ this.microseconds / 1000000)
|
||||||
|
},
|
||||||
|
__nonzero__: function () {
|
||||||
|
return (!!this.days || !!this.seconds || !!this.microseconds)
|
||||||
|
? py.True
|
||||||
|
: py.False;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
datetime.datetime = py.type('datetime', null, {
|
||||||
|
__init__: function () {
|
||||||
|
var zero = py.float.fromJSON(0);
|
||||||
|
var args = py.PY_parseArgs(arguments, [
|
||||||
|
'year', 'month', 'day',
|
||||||
|
['hour', zero], ['minute', zero], ['second', zero],
|
||||||
|
['microsecond', zero], ['tzinfo', py.None]
|
||||||
|
]);
|
||||||
|
for(var key in args) {
|
||||||
|
if (!args.hasOwnProperty(key)) { continue; }
|
||||||
|
this[key] = asJS(args[key]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
strftime: function () {
|
||||||
|
var self = this;
|
||||||
|
var args = py.PY_parseArgs(arguments, 'format');
|
||||||
|
return py.str.fromJSON(args.format.toJSON()
|
||||||
|
.replace(/%([A-Za-z])/g, function (m, c) {
|
||||||
|
switch (c) {
|
||||||
|
case 'Y': return self.year;
|
||||||
|
case 'm': return _.str.sprintf('%02d', self.month);
|
||||||
|
case 'd': return _.str.sprintf('%02d', self.day);
|
||||||
|
case 'H': return _.str.sprintf('%02d', self.hour);
|
||||||
|
case 'M': return _.str.sprintf('%02d', self.minute);
|
||||||
|
case 'S': return _.str.sprintf('%02d', self.second);
|
||||||
|
}
|
||||||
|
throw new Error('ValueError: No known conversion for ' + m);
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
now: py.classmethod.fromJSON(function () {
|
||||||
|
var d = new Date();
|
||||||
|
return py.PY_call(datetime.datetime,
|
||||||
|
[d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(),
|
||||||
|
d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(),
|
||||||
|
d.getUTCMilliseconds() * 1000]);
|
||||||
|
}),
|
||||||
|
combine: py.classmethod.fromJSON(function () {
|
||||||
|
var args = py.PY_parseArgs(arguments, 'date time');
|
||||||
|
return py.PY_call(datetime.datetime, [
|
||||||
|
py.PY_getAttr(args.date, 'year'),
|
||||||
|
py.PY_getAttr(args.date, 'month'),
|
||||||
|
py.PY_getAttr(args.date, 'day'),
|
||||||
|
py.PY_getAttr(args.time, 'hour'),
|
||||||
|
py.PY_getAttr(args.time, 'minute'),
|
||||||
|
py.PY_getAttr(args.time, 'second')
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
datetime.date = py.type('date', null, {
|
||||||
|
__init__: function () {
|
||||||
|
var args = py.PY_parseArgs(arguments, 'year month day');
|
||||||
|
this.year = asJS(args.year);
|
||||||
|
this.month = asJS(args.month);
|
||||||
|
this.day = asJS(args.day);
|
||||||
|
},
|
||||||
|
strftime: function () {
|
||||||
|
var self = this;
|
||||||
|
var args = py.PY_parseArgs(arguments, 'format');
|
||||||
|
return py.str.fromJSON(args.format.toJSON()
|
||||||
|
.replace(/%([A-Za-z])/g, function (m, c) {
|
||||||
|
switch (c) {
|
||||||
|
case 'Y': return self.year;
|
||||||
|
case 'm': return _.str.sprintf('%02d', self.month);
|
||||||
|
case 'd': return _.str.sprintf('%02d', self.day);
|
||||||
|
}
|
||||||
|
throw new Error('ValueError: No known conversion for ' + m);
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
__eq__: function (other) {
|
||||||
|
return (this.year === other.year
|
||||||
|
&& this.month === other.month
|
||||||
|
&& this.day === other.day)
|
||||||
|
? py.True : py.False;
|
||||||
|
},
|
||||||
|
__add__: function (other) {
|
||||||
|
if (!py.PY_isInstance(other, datetime.timedelta)) {
|
||||||
|
return py.NotImplemented;
|
||||||
|
}
|
||||||
|
var s = tmxxx(this.year, this.month, this.day + other.days);
|
||||||
|
return datetime.date.fromJSON(s.year, s.month, s.day);
|
||||||
|
},
|
||||||
|
__radd__: function (other) { return this.__add__(other); },
|
||||||
|
__sub__: function (other) {
|
||||||
|
if (py.PY_isInstance(other, datetime.timedelta)) {
|
||||||
|
return this.__add__(other.__neg__());
|
||||||
|
}
|
||||||
|
if (py.PY_isInstance(other, datetime.date)) {
|
||||||
|
// FIXME: getattr and sub API methods
|
||||||
|
return py.PY_call(datetime.timedelta, [
|
||||||
|
py.PY_subtract(
|
||||||
|
py.PY_call(py.PY_getAttr(this, 'toordinal')),
|
||||||
|
py.PY_call(py.PY_getAttr(other, 'toordinal')))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return py.NotImplemented;
|
||||||
|
},
|
||||||
|
toordinal: function () {
|
||||||
|
return py.float.fromJSON(ymd2ord(this.year, this.month, this.day));
|
||||||
|
},
|
||||||
|
fromJSON: function (year, month, day) {
|
||||||
|
return py.PY_call(datetime.date, [year, month, day])
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
Returns the current local date, which means the date on the client (which can be different
|
||||||
|
compared to the date of the server).
|
||||||
|
|
||||||
|
@return {datetime.date}
|
||||||
|
*/
|
||||||
|
var context_today = function() {
|
||||||
|
var d = new Date();
|
||||||
|
return py.PY_call(
|
||||||
|
datetime.date, [d.getFullYear(), d.getMonth() + 1, d.getDate()]);
|
||||||
|
};
|
||||||
|
datetime.time = py.type('time', null, {
|
||||||
|
__init__: function () {
|
||||||
|
var zero = py.float.fromJSON(0);
|
||||||
|
var args = py.PY_parseArgs(arguments, [
|
||||||
|
['hour', zero], ['minute', zero], ['second', zero], ['microsecond', zero],
|
||||||
|
['tzinfo', py.None]
|
||||||
|
]);
|
||||||
|
|
||||||
|
for(var k in args) {
|
||||||
|
if (!args.hasOwnProperty(k)) { continue; }
|
||||||
|
this[k] = asJS(args[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var time = py.PY_call(py.object);
|
||||||
|
time.strftime = py.PY_def.fromJSON(function () {
|
||||||
|
var args = py.PY_parseArgs(arguments, 'format');
|
||||||
|
var dt_class = py.PY_getAttr(datetime, 'datetime');
|
||||||
|
var d = py.PY_call(py.PY_getAttr(dt_class, 'now'));
|
||||||
|
return py.PY_call(py.PY_getAttr(d, 'strftime'), [args.format]);
|
||||||
|
});
|
||||||
|
|
||||||
|
var relativedelta = py.type('relativedelta', null, {
|
||||||
|
__init__: function () {
|
||||||
|
this.ops = py.PY_parseArgs(arguments,
|
||||||
|
'* year month day hour minute second microsecond '
|
||||||
|
+ 'years months weeks days hours minutes secondes microseconds '
|
||||||
|
+ 'weekday leakdays yearday nlyearday');
|
||||||
|
},
|
||||||
|
__add__: function (other) {
|
||||||
|
if (!py.PY_isInstance(other, datetime.date)) {
|
||||||
|
return py.NotImplemented;
|
||||||
|
}
|
||||||
|
// TODO: test this whole mess
|
||||||
|
var year = asJS(this.ops.year) || asJS(other.year);
|
||||||
|
if (asJS(this.ops.years)) {
|
||||||
|
year += asJS(this.ops.years);
|
||||||
|
}
|
||||||
|
|
||||||
|
var month = asJS(this.ops.month) || asJS(other.month);
|
||||||
|
if (asJS(this.ops.months)) {
|
||||||
|
month += asJS(this.ops.months);
|
||||||
|
// FIXME: no divmod in JS?
|
||||||
|
while (month < 1) {
|
||||||
|
year -= 1;
|
||||||
|
month += 12;
|
||||||
|
}
|
||||||
|
while (month > 12) {
|
||||||
|
year += 1;
|
||||||
|
month -= 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastMonthDay = new Date(year, month, 0).getDate();
|
||||||
|
var day = asJS(this.ops.day) || asJS(other.day);
|
||||||
|
if (day > lastMonthDay) { day = lastMonthDay; }
|
||||||
|
var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0);
|
||||||
|
if (days_offset) {
|
||||||
|
day = new Date(year, month-1, day + days_offset).getDate();
|
||||||
|
}
|
||||||
|
// TODO: leapdays?
|
||||||
|
// TODO: hours, minutes, seconds? Not used in XML domains
|
||||||
|
// TODO: weekday?
|
||||||
|
// FIXME: use date.replace
|
||||||
|
return py.PY_call(datetime.date, [
|
||||||
|
py.float.fromJSON(year),
|
||||||
|
py.float.fromJSON(month),
|
||||||
|
py.float.fromJSON(day)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
__radd__: function (other) {
|
||||||
|
return this.__add__(other);
|
||||||
|
},
|
||||||
|
|
||||||
|
__sub__: function (other) {
|
||||||
|
if (!py.PY_isInstance(other, datetime.date)) {
|
||||||
|
return py.NotImplemented;
|
||||||
|
}
|
||||||
|
// TODO: test this whole mess
|
||||||
|
var year = asJS(this.ops.year) || asJS(other.year);
|
||||||
|
if (asJS(this.ops.years)) {
|
||||||
|
year -= asJS(this.ops.years);
|
||||||
|
}
|
||||||
|
|
||||||
|
var month = asJS(this.ops.month) || asJS(other.month);
|
||||||
|
if (asJS(this.ops.months)) {
|
||||||
|
month -= asJS(this.ops.months);
|
||||||
|
// FIXME: no divmod in JS?
|
||||||
|
while (month < 1) {
|
||||||
|
year -= 1;
|
||||||
|
month += 12;
|
||||||
|
}
|
||||||
|
while (month > 12) {
|
||||||
|
year += 1;
|
||||||
|
month -= 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastMonthDay = new Date(year, month, 0).getDate();
|
||||||
|
var day = asJS(this.ops.day) || asJS(other.day);
|
||||||
|
if (day > lastMonthDay) { day = lastMonthDay; }
|
||||||
|
var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0);
|
||||||
|
if (days_offset) {
|
||||||
|
day = new Date(year, month-1, day - days_offset).getDate();
|
||||||
|
}
|
||||||
|
// TODO: leapdays?
|
||||||
|
// TODO: hours, minutes, seconds? Not used in XML domains
|
||||||
|
// TODO: weekday?
|
||||||
|
return py.PY_call(datetime.date, [
|
||||||
|
py.float.fromJSON(year),
|
||||||
|
py.float.fromJSON(month),
|
||||||
|
py.float.fromJSON(day)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
__rsub__: function (other) {
|
||||||
|
return this.__sub__(other);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var eval_contexts = function (contexts, evaluation_context) {
|
||||||
|
evaluation_context = evaluation_context || {};
|
||||||
|
return _(contexts).reduce(function (result_context, ctx) {
|
||||||
|
// __eval_context evaluations can lead to some of `contexts`'s
|
||||||
|
// values being null, skip them as well as empty contexts
|
||||||
|
if (_.isEmpty(ctx)) { return result_context; }
|
||||||
|
if (_.isString(ctx)) {
|
||||||
|
// wrap raw strings in context
|
||||||
|
ctx = { __ref: 'context', __debug: ctx };
|
||||||
|
}
|
||||||
|
var evaluated = ctx;
|
||||||
|
switch(ctx.__ref) {
|
||||||
|
case 'context':
|
||||||
|
evaluation_context.context = py.dict.fromJSON(evaluation_context);
|
||||||
|
evaluated = py.eval(ctx.__debug, evaluation_context);
|
||||||
|
break;
|
||||||
|
case 'compound_context':
|
||||||
|
var eval_context = eval_contexts([ctx.__eval_context]);
|
||||||
|
evaluated = eval_contexts(
|
||||||
|
ctx.__contexts, _.extend({}, evaluation_context, eval_context));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// add newly evaluated context to evaluation context for following
|
||||||
|
// siblings
|
||||||
|
_.extend(evaluation_context, evaluated);
|
||||||
|
return _.extend(result_context, evaluated);
|
||||||
|
}, _.extend({}, instance.session.user_context));
|
||||||
|
};
|
||||||
|
var eval_domains = function (domains, evaluation_context) {
|
||||||
|
var result_domain = [];
|
||||||
|
_(domains).each(function (domain) {
|
||||||
|
if (_.isString(domain)) {
|
||||||
|
// wrap raw strings in domain
|
||||||
|
domain = { __ref: 'domain', __debug: domain };
|
||||||
|
}
|
||||||
|
switch(domain.__ref) {
|
||||||
|
case 'domain':
|
||||||
|
evaluation_context.context = py.dict.fromJSON(evaluation_context);
|
||||||
|
result_domain.push.apply(
|
||||||
|
result_domain, py.eval(domain.__debug, evaluation_context));
|
||||||
|
break;
|
||||||
|
case 'compound_domain':
|
||||||
|
var eval_context = eval_contexts([domain.__eval_context]);
|
||||||
|
result_domain.push.apply(
|
||||||
|
result_domain, eval_domains(
|
||||||
|
domain.__domains, _.extend(
|
||||||
|
{}, evaluation_context, eval_context)));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result_domain.push.apply(result_domain, domain);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result_domain;
|
||||||
|
};
|
||||||
|
var eval_groupbys = function (contexts, evaluation_context) {
|
||||||
|
var result_group = [];
|
||||||
|
_(contexts).each(function (ctx) {
|
||||||
|
if (_.isString(ctx)) {
|
||||||
|
// wrap raw strings in context
|
||||||
|
ctx = { __ref: 'context', __debug: ctx };
|
||||||
|
}
|
||||||
|
var group;
|
||||||
|
var evaluated = ctx;
|
||||||
|
switch(ctx.__ref) {
|
||||||
|
case 'context':
|
||||||
|
evaluation_context.context = py.dict.fromJSON(evaluation_context);
|
||||||
|
evaluated = py.eval(ctx.__debug, evaluation_context);
|
||||||
|
break;
|
||||||
|
case 'compound_context':
|
||||||
|
var eval_context = eval_contexts([ctx.__eval_context]);
|
||||||
|
evaluated = eval_contexts(
|
||||||
|
ctx.__contexts, _.extend({}, evaluation_context, eval_context));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
group = evaluated.group_by;
|
||||||
|
if (!group) { return; }
|
||||||
|
if (typeof group === 'string') {
|
||||||
|
result_group.push(group);
|
||||||
|
} else if (group instanceof Array) {
|
||||||
|
result_group.push.apply(result_group, group);
|
||||||
|
} else {
|
||||||
|
throw new Error('Got invalid groupby {{'
|
||||||
|
+ JSON.stringify(group) + '}}');
|
||||||
|
}
|
||||||
|
_.extend(evaluation_context, evaluated);
|
||||||
|
});
|
||||||
|
return result_group;
|
||||||
|
};
|
||||||
|
|
||||||
|
instance.web.pyeval.context = function () {
|
||||||
|
return {
|
||||||
|
uid: py.float.fromJSON(instance.session.uid),
|
||||||
|
datetime: datetime,
|
||||||
|
context_today: context_today,
|
||||||
|
time: time,
|
||||||
|
relativedelta: relativedelta,
|
||||||
|
current_date: py.PY_call(
|
||||||
|
time.strftime, [py.str.fromJSON('%Y-%m-%d')]),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} type "domains", "contexts" or "groupbys"
|
||||||
|
* @param {Array} object domains or contexts to evaluate
|
||||||
|
* @param {Object} [context] evaluation context
|
||||||
|
*/
|
||||||
|
instance.web.pyeval.eval = function (type, object, context) {
|
||||||
|
context = _.extend(instance.web.pyeval.context(), context || {});
|
||||||
|
context['context'] = py.dict.fromJSON(context);
|
||||||
|
|
||||||
|
//noinspection FallthroughInSwitchStatementJS
|
||||||
|
switch(type) {
|
||||||
|
case 'context': object = [object];
|
||||||
|
case 'contexts': return eval_contexts(object, context);
|
||||||
|
case 'domain': object = [object];
|
||||||
|
case 'domains': return eval_domains(object, context);
|
||||||
|
case 'groupbys': return eval_groupbys(object, context);
|
||||||
|
}
|
||||||
|
throw new Error("Unknow evaluation type " + type)
|
||||||
|
};
|
||||||
|
|
||||||
|
var eval_arg = function (arg) {
|
||||||
|
if (typeof arg !== 'object' || !arg.__ref) { return arg; }
|
||||||
|
switch(arg.__ref) {
|
||||||
|
case 'domain': case 'compound_domain':
|
||||||
|
return instance.web.pyeval.eval('domains', [arg]);
|
||||||
|
case 'context': case 'compound_context':
|
||||||
|
return instance.web.pyeval.eval('contexts', [arg]);
|
||||||
|
default:
|
||||||
|
throw new Error(instance.web._t("Unknown nonliteral type " + arg.__ref));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* If args or kwargs are unevaluated contexts or domains (compound or not),
|
||||||
|
* evaluated them in-place.
|
||||||
|
*
|
||||||
|
* Potentially mutates both parameters.
|
||||||
|
*
|
||||||
|
* @param args
|
||||||
|
* @param kwargs
|
||||||
|
*/
|
||||||
|
instance.web.pyeval.ensure_evaluated = function (args, kwargs) {
|
||||||
|
for (var i=0; i<args.length; ++i) {
|
||||||
|
args[i] = eval_arg(args[i]);
|
||||||
|
}
|
||||||
|
for (var k in kwargs) {
|
||||||
|
if (!kwargs.hasOwnProperty(k)) { continue; }
|
||||||
|
kwargs[k] = eval_arg(kwargs[k]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
instance.web.pyeval.eval_domains_and_contexts = function (source) {
|
||||||
|
return new $.Deferred(function (d) {setTimeout(function () {
|
||||||
|
try {
|
||||||
|
var contexts = ([instance.session.user_context] || []).concat(source.contexts);
|
||||||
|
// see Session.eval_context in Python
|
||||||
|
d.resolve({
|
||||||
|
context: instance.web.pyeval.eval('contexts', contexts),
|
||||||
|
domain: instance.web.pyeval.eval('domains', source.domains),
|
||||||
|
group_by: instance.web.pyeval.eval('groupbys', source.group_by_seq || [])
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
d.resolve({ error: {
|
||||||
|
code: 400,
|
||||||
|
message: instance.web._t("Evaluation Error"),
|
||||||
|
data: {
|
||||||
|
type: 'local_exception',
|
||||||
|
debug: _.str.sprintf(
|
||||||
|
instance.web._t("Local evaluation failure\n%s\n\n%s"),
|
||||||
|
e.message, JSON.stringify(source))
|
||||||
|
}
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
}, 0); });
|
||||||
|
}
|
||||||
|
};
|
|
@ -341,17 +341,19 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
|
||||||
if (this.headless) {
|
if (this.headless) {
|
||||||
this.ready.resolve();
|
this.ready.resolve();
|
||||||
} else {
|
} else {
|
||||||
var load_view = this.rpc("/web/searchview/load", {
|
var load_view = this.rpc("/web/view/load", {
|
||||||
model: this.model,
|
model: this.model,
|
||||||
view_id: this.view_id,
|
view_id: this.view_id,
|
||||||
context: this.dataset.get_context() });
|
view_type: 'search',
|
||||||
|
context: instance.web.pyeval.eval(
|
||||||
|
'context', this.dataset.get_context())
|
||||||
|
});
|
||||||
|
|
||||||
$.when(load_view)
|
$.when(load_view).then(function (r) {
|
||||||
.then(function(r) {
|
return self.search_view_loaded(r)
|
||||||
self.search_view_loaded(r)
|
}).fail(function () {
|
||||||
}, function () {
|
self.ready.reject.apply(null, arguments);
|
||||||
self.ready.reject.apply(null, arguments);
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
instance.web.bus.on('click', this, function(ev) {
|
instance.web.bus.on('click', this, function(ev) {
|
||||||
|
@ -615,16 +617,16 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
|
||||||
|
|
||||||
search_view_loaded: function(data) {
|
search_view_loaded: function(data) {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.fields_view = data.fields_view;
|
this.fields_view = data;
|
||||||
if (data.fields_view.type !== 'search' ||
|
if (data.type !== 'search' ||
|
||||||
data.fields_view.arch.tag !== 'search') {
|
data.arch.tag !== 'search') {
|
||||||
throw new Error(_.str.sprintf(
|
throw new Error(_.str.sprintf(
|
||||||
"Got non-search view after asking for a search view: type %s, arch root %s",
|
"Got non-search view after asking for a search view: type %s, arch root %s",
|
||||||
data.fields_view.type, data.fields_view.arch.tag));
|
data.type, data.arch.tag));
|
||||||
}
|
}
|
||||||
this.make_widgets(
|
this.make_widgets(
|
||||||
data.fields_view['arch'].children,
|
data['arch'].children,
|
||||||
data.fields_view.fields);
|
data.fields);
|
||||||
|
|
||||||
this.add_common_inputs();
|
this.add_common_inputs();
|
||||||
|
|
||||||
|
@ -632,6 +634,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
|
||||||
var drawer_started = $.when.apply(
|
var drawer_started = $.when.apply(
|
||||||
null, _(this.select_for_drawer()).invoke(
|
null, _(this.select_for_drawer()).invoke(
|
||||||
'appendTo', this.$('.oe_searchview_drawer')));
|
'appendTo', this.$('.oe_searchview_drawer')));
|
||||||
|
|
||||||
|
|
||||||
// load defaults
|
// load defaults
|
||||||
var defaults_fetched = $.when.apply(null, _(this.inputs).invoke(
|
var defaults_fetched = $.when.apply(null, _(this.inputs).invoke(
|
||||||
|
@ -1006,6 +1009,7 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
|
||||||
get_context: function (facet) {
|
get_context: function (facet) {
|
||||||
var contexts = facet.values.chain()
|
var contexts = facet.values.chain()
|
||||||
.map(function (f) { return f.get('value').attrs.context; })
|
.map(function (f) { return f.get('value').attrs.context; })
|
||||||
|
.without('{}')
|
||||||
.reject(_.isEmpty)
|
.reject(_.isEmpty)
|
||||||
.value();
|
.value();
|
||||||
|
|
||||||
|
@ -1024,6 +1028,7 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
|
||||||
get_groupby: function (facet) {
|
get_groupby: function (facet) {
|
||||||
return facet.values.chain()
|
return facet.values.chain()
|
||||||
.map(function (f) { return f.get('value').attrs.context; })
|
.map(function (f) { return f.get('value').attrs.context; })
|
||||||
|
.without('{}')
|
||||||
.reject(_.isEmpty)
|
.reject(_.isEmpty)
|
||||||
.value();
|
.value();
|
||||||
},
|
},
|
||||||
|
@ -1036,6 +1041,7 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
|
||||||
get_domain: function (facet) {
|
get_domain: function (facet) {
|
||||||
var domains = facet.values.chain()
|
var domains = facet.values.chain()
|
||||||
.map(function (f) { return f.get('value').attrs.domain; })
|
.map(function (f) { return f.get('value').attrs.domain; })
|
||||||
|
.without('[]')
|
||||||
.reject(_.isEmpty)
|
.reject(_.isEmpty)
|
||||||
.value();
|
.value();
|
||||||
|
|
||||||
|
@ -1506,16 +1512,10 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
|
||||||
this.$el.on('click', 'h4', function () {
|
this.$el.on('click', 'h4', function () {
|
||||||
self.$el.toggleClass('oe_opened');
|
self.$el.toggleClass('oe_opened');
|
||||||
});
|
});
|
||||||
// FIXME: local eval of domain and context to get rid of special endpoint
|
return this.model.call('get_filters', [this.view.model])
|
||||||
return this.rpc('/web/searchview/get_filters', {
|
|
||||||
model: this.view.model
|
|
||||||
})
|
|
||||||
.then(this.proxy('set_filters'))
|
.then(this.proxy('set_filters'))
|
||||||
.then(function () {
|
.done(function () { self.is_ready.resolve(); })
|
||||||
self.is_ready.resolve(null);
|
.fail(function () { self.is_ready.reject.apply(self.is_ready, arguments); });
|
||||||
}, function () {
|
|
||||||
self.is_ready.reject();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Special implementation delaying defaults until CustomFilters is loaded
|
* Special implementation delaying defaults until CustomFilters is loaded
|
||||||
|
@ -1614,7 +1614,7 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
|
||||||
var set_as_default = this.$('#oe_searchview_custom_default').prop('checked');
|
var set_as_default = this.$('#oe_searchview_custom_default').prop('checked');
|
||||||
|
|
||||||
var search = this.view.build_search_data();
|
var search = this.view.build_search_data();
|
||||||
this.rpc('/web/session/eval_domain_and_context', {
|
instance.web.pyeval.eval_domains_and_contexts({
|
||||||
domains: search.domains,
|
domains: search.domains,
|
||||||
contexts: search.contexts,
|
contexts: search.contexts,
|
||||||
group_by_seq: search.groupbys || []
|
group_by_seq: search.groupbys || []
|
||||||
|
@ -1708,10 +1708,10 @@ instance.web.search.Advanced = instance.web.search.Input.extend({
|
||||||
});
|
});
|
||||||
return $.when(
|
return $.when(
|
||||||
this._super(),
|
this._super(),
|
||||||
this.rpc("/web/searchview/fields_get", {model: this.view.model}).done(function(data) {
|
new instance.web.Model(this.view.model).call('fields_get').done(function(data) {
|
||||||
self.fields = _.extend({
|
self.fields = _.extend({
|
||||||
id: { string: 'ID', type: 'id' }
|
id: { string: 'ID', type: 'id' }
|
||||||
}, data.fields);
|
}, data);
|
||||||
})).done(function () {
|
})).done(function () {
|
||||||
self.append_proposition();
|
self.append_proposition();
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
openerp.testing = {};
|
openerp.testing = {};
|
||||||
(function (testing) {
|
(function (testing) {
|
||||||
var dependencies = {
|
var dependencies = {
|
||||||
corelib: [],
|
pyeval: [],
|
||||||
|
corelib: ['pyeval'],
|
||||||
coresetup: ['corelib'],
|
coresetup: ['corelib'],
|
||||||
data: ['corelib', 'coresetup'],
|
data: ['corelib', 'coresetup'],
|
||||||
dates: [],
|
dates: [],
|
||||||
|
@ -262,7 +263,7 @@ openerp.testing = {};
|
||||||
++di;
|
++di;
|
||||||
}
|
}
|
||||||
|
|
||||||
instance = openerp.init("fuck your shit, don't load anything you cunt");
|
instance = openerp.init(null);
|
||||||
_(d).chain()
|
_(d).chain()
|
||||||
.reverse()
|
.reverse()
|
||||||
.uniq()
|
.uniq()
|
||||||
|
@ -270,6 +271,9 @@ openerp.testing = {};
|
||||||
openerp.web[module](instance);
|
openerp.web[module](instance);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (instance.session) {
|
||||||
|
instance.session.uid = 42;
|
||||||
|
}
|
||||||
if (_.isNumber(opts.asserts)) {
|
if (_.isNumber(opts.asserts)) {
|
||||||
expect(opts.asserts);
|
expect(opts.asserts);
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,7 +148,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
||||||
load_form: function(data) {
|
load_form: function(data) {
|
||||||
var self = this;
|
var self = this;
|
||||||
if (!data) {
|
if (!data) {
|
||||||
throw new Error("No data provided.");
|
throw new Error(_t("No data provided."));
|
||||||
}
|
}
|
||||||
if (this.arch) {
|
if (this.arch) {
|
||||||
throw "Form view does not support multiple calls to load_form";
|
throw "Form view does not support multiple calls to load_form";
|
||||||
|
@ -316,12 +316,12 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
||||||
var self = this, set_values = [];
|
var self = this, set_values = [];
|
||||||
if (!record) {
|
if (!record) {
|
||||||
this.set({ 'title' : undefined });
|
this.set({ 'title' : undefined });
|
||||||
this.do_warn("Form", "The record could not be found in the database.", true);
|
this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
|
||||||
return $.Deferred().reject();
|
return $.Deferred().reject();
|
||||||
}
|
}
|
||||||
this.datarecord = record;
|
this.datarecord = record;
|
||||||
this._actualize_mode();
|
this._actualize_mode();
|
||||||
this.set({ 'title' : record.id ? record.display_name : "New" });
|
this.set({ 'title' : record.id ? record.display_name : _t("New") });
|
||||||
|
|
||||||
_(this.fields).each(function (field, f) {
|
_(this.fields).each(function (field, f) {
|
||||||
field._dirty_flag = false;
|
field._dirty_flag = false;
|
||||||
|
@ -431,31 +431,31 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
||||||
var onchange = _.str.trim(on_change);
|
var onchange = _.str.trim(on_change);
|
||||||
var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
|
var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
|
||||||
if (!call) {
|
if (!call) {
|
||||||
throw new Error("Wrong on change format: " + onchange);
|
throw new Error(_.str.sprintf( _t("Wrong on change format: %s"), onchange ));
|
||||||
}
|
}
|
||||||
|
|
||||||
var method = call[1];
|
var method = call[1];
|
||||||
if (!_.str.trim(call[2])) {
|
if (!_.str.trim(call[2])) {
|
||||||
return {method: method, args: [], context_index: null}
|
return {method: method, args: []}
|
||||||
}
|
}
|
||||||
|
|
||||||
var argument_replacement = {
|
var argument_replacement = {
|
||||||
'False': function () {return false;},
|
'False': function () {return false;},
|
||||||
'True': function () {return true;},
|
'True': function () {return true;},
|
||||||
'None': function () {return null;},
|
'None': function () {return null;},
|
||||||
'context': function (i) {
|
'context': function () {
|
||||||
context_index = i;
|
return new instance.web.CompoundContext(
|
||||||
var ctx = new instance.web.CompoundContext(self.dataset.get_context(), widget.build_context() ? widget.build_context() : {});
|
self.dataset.get_context(),
|
||||||
return ctx;
|
widget.build_context() ? widget.build_context() : {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var parent_fields = null, context_index = null;
|
var parent_fields = null;
|
||||||
var args = _.map(call[2].split(','), function (a, i) {
|
var args = _.map(call[2].split(','), function (a, i) {
|
||||||
var field = _.str.trim(a);
|
var field = _.str.trim(a);
|
||||||
|
|
||||||
// literal constant or context
|
// literal constant or context
|
||||||
if (field in argument_replacement) {
|
if (field in argument_replacement) {
|
||||||
return argument_replacement[field](i);
|
return argument_replacement[field]();
|
||||||
}
|
}
|
||||||
// literal number
|
// literal number
|
||||||
if (/^-?\d+(\.\d+)?$/.test(field)) {
|
if (/^-?\d+(\.\d+)?$/.test(field)) {
|
||||||
|
@ -490,8 +490,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
||||||
|
|
||||||
return {
|
return {
|
||||||
method: method,
|
method: method,
|
||||||
args: args,
|
args: args
|
||||||
context_index: context_index
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
do_onchange: function(widget, processed) {
|
do_onchange: function(widget, processed) {
|
||||||
|
@ -510,18 +509,15 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
||||||
var on_change = widget.node.attrs.on_change;
|
var on_change = widget.node.attrs.on_change;
|
||||||
if (on_change) {
|
if (on_change) {
|
||||||
var change_spec = self.parse_on_change(on_change, widget);
|
var change_spec = self.parse_on_change(on_change, widget);
|
||||||
def = self.rpc('/web/dataset/onchange', {
|
var id = [self.datarecord.id == null ? [] : [self.datarecord.id]];
|
||||||
model: self.dataset.model,
|
def = new instance.web.Model(self.dataset.model).call(
|
||||||
method: change_spec.method,
|
change_spec.method, id.concat(change_spec.args));
|
||||||
args: [(self.datarecord.id == null ? [] : [self.datarecord.id])].concat(change_spec.args),
|
|
||||||
context_id: change_spec.context_index == undefined ? null : change_spec.context_index + 1
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
def = $.when({});
|
def = $.when({});
|
||||||
}
|
}
|
||||||
return def.then(function(response) {
|
return def.then(function(response) {
|
||||||
if (widget.field['change_default']) {
|
if (widget.field['change_default']) {
|
||||||
var fieldname = widget.name
|
var fieldname = widget.name;
|
||||||
var value_;
|
var value_;
|
||||||
if (response.value && (fieldname in response.value)) {
|
if (response.value && (fieldname in response.value)) {
|
||||||
// Use value from onchange if onchange executed
|
// Use value from onchange if onchange executed
|
||||||
|
@ -533,11 +529,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
||||||
var condition = fieldname + '=' + value_;
|
var condition = fieldname + '=' + value_;
|
||||||
|
|
||||||
if (value_) {
|
if (value_) {
|
||||||
return self.rpc('/web/dataset/call', {
|
return new instance.web.Model('ir.values').call(
|
||||||
model: 'ir.values',
|
'get_defaults', [self.model, condition]
|
||||||
method: 'get_defaults',
|
).then(function (results) {
|
||||||
args: [self.model, condition]
|
|
||||||
}).then(function (results) {
|
|
||||||
if (!results.length) {
|
if (!results.length) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
@ -872,7 +866,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
||||||
}).value();
|
}).value();
|
||||||
warnings.unshift('<ul>');
|
warnings.unshift('<ul>');
|
||||||
warnings.push('</ul>');
|
warnings.push('</ul>');
|
||||||
this.do_warn("The following fields are invalid :", warnings.join(''));
|
this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Reload the form after saving
|
* Reload the form after saving
|
||||||
|
@ -1033,7 +1027,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
||||||
|| field.get("readonly")
|
|| field.get("readonly")
|
||||||
|| field.field.type === 'one2many'
|
|| field.field.type === 'one2many'
|
||||||
|| field.field.type === 'many2many'
|
|| field.field.type === 'many2many'
|
||||||
|| field.field.type === 'binary') {
|
|| field.field.type === 'binary'
|
||||||
|
|| field.password) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1240,11 +1235,11 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt
|
||||||
_.each(this.fields_to_init, function($elem) {
|
_.each(this.fields_to_init, function($elem) {
|
||||||
var name = $elem.attr("name");
|
var name = $elem.attr("name");
|
||||||
if (!self.fvg.fields[name]) {
|
if (!self.fvg.fields[name]) {
|
||||||
throw new Error("Field '" + name + "' specified in view could not be found.");
|
throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
|
||||||
}
|
}
|
||||||
var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
|
var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
|
||||||
if (!obj) {
|
if (!obj) {
|
||||||
throw new Error("Widget type '"+ $elem.attr('widget') + "' is not implemented");
|
throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
|
||||||
}
|
}
|
||||||
var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
|
var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
|
||||||
var $label = self.labels[$elem.attr("name")];
|
var $label = self.labels[$elem.attr("name")];
|
||||||
|
@ -1681,11 +1676,11 @@ instance.web.form.compute_domain = function(expr, fields) {
|
||||||
switch (op.toLowerCase()) {
|
switch (op.toLowerCase()) {
|
||||||
case '=':
|
case '=':
|
||||||
case '==':
|
case '==':
|
||||||
stack.push(field_value == val);
|
stack.push(_.isEqual(field_value, val));
|
||||||
break;
|
break;
|
||||||
case '!=':
|
case '!=':
|
||||||
case '<>':
|
case '<>':
|
||||||
stack.push(field_value != val);
|
stack.push(!_.isEqual(field_value, val));
|
||||||
break;
|
break;
|
||||||
case '<':
|
case '<':
|
||||||
stack.push(field_value < val);
|
stack.push(field_value < val);
|
||||||
|
@ -2318,7 +2313,7 @@ instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
|
||||||
},
|
},
|
||||||
on_button_clicked: function() {
|
on_button_clicked: function() {
|
||||||
if (!this.get('value') || !this.is_syntax_valid()) {
|
if (!this.get('value') || !this.is_syntax_valid()) {
|
||||||
this.do_warn("E-mail error", "Can't send email to invalid e-mail address");
|
this.do_warn(_t("E-mail error"), _t("Can't send email to invalid e-mail address"));
|
||||||
} else {
|
} else {
|
||||||
location.href = 'mailto:' + this.get('value');
|
location.href = 'mailto:' + this.get('value');
|
||||||
}
|
}
|
||||||
|
@ -2347,7 +2342,7 @@ instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
|
||||||
},
|
},
|
||||||
on_button_clicked: function() {
|
on_button_clicked: function() {
|
||||||
if (!this.get('value')) {
|
if (!this.get('value')) {
|
||||||
this.do_warn("Resource error", "This resource is empty");
|
this.do_warn(_t("Resource error"), _t("This resource is empty"));
|
||||||
} else {
|
} else {
|
||||||
var url = $.trim(this.get('value'));
|
var url = $.trim(this.get('value'));
|
||||||
if(/^www\./i.test(url))
|
if(/^www\./i.test(url))
|
||||||
|
@ -3384,7 +3379,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
|
||||||
var views = [];
|
var views = [];
|
||||||
_.each(modes, function(mode) {
|
_.each(modes, function(mode) {
|
||||||
if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
|
if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
|
||||||
throw new Error(_.str.sprintf("View type '%s' is not supported in One2Many.", mode));
|
throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
|
||||||
}
|
}
|
||||||
var view = {
|
var view = {
|
||||||
view_id: false,
|
view_id: false,
|
||||||
|
@ -4074,13 +4069,12 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
|
||||||
If you see this options, do not use it, it's basically a dirty hack to make one
|
If you see this options, do not use it, it's basically a dirty hack to make one
|
||||||
precise o2m to behave the way we want.
|
precise o2m to behave the way we want.
|
||||||
*/
|
*/
|
||||||
instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({
|
instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
|
||||||
multi_selection: false,
|
multi_selection: false,
|
||||||
disable_utility_classes: true,
|
disable_utility_classes: true,
|
||||||
init: function(field_manager, node) {
|
init: function(field_manager, node) {
|
||||||
this._super(field_manager, node);
|
this._super(field_manager, node);
|
||||||
this.is_loaded = $.Deferred();
|
this.is_loaded = $.Deferred();
|
||||||
this.initial_is_loaded = this.is_loaded;
|
|
||||||
this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
|
this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
|
||||||
this.dataset.m2m = this;
|
this.dataset.m2m = this;
|
||||||
var self = this;
|
var self = this;
|
||||||
|
@ -4088,24 +4082,44 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({
|
||||||
self.dataset_changed();
|
self.dataset_changed();
|
||||||
});
|
});
|
||||||
this.set_value([]);
|
this.set_value([]);
|
||||||
|
this.list_dm = new instance.web.DropMisordered();
|
||||||
|
this.render_value_dm = new instance.web.DropMisordered();
|
||||||
},
|
},
|
||||||
start: function() {
|
initialize_content: function() {
|
||||||
this.$el.addClass('oe_form_field oe_form_field_many2many');
|
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.load_view();
|
this.$el.addClass('oe_form_field oe_form_field_many2many');
|
||||||
this.is_loaded.done(function() {
|
|
||||||
self.on("change:effective_readonly", self, function() {
|
this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
|
||||||
self.is_loaded = self.is_loaded.then(function() {
|
'addable': this.get("effective_readonly") ? null : _t("Add"),
|
||||||
self.list_view.destroy();
|
'deletable': this.get("effective_readonly") ? false : true,
|
||||||
return $.when(self.load_view()).done(function() {
|
'selectable': this.multi_selection,
|
||||||
self.render_value();
|
'sortable': false,
|
||||||
});
|
'reorderable': false,
|
||||||
});
|
'import_enabled': false,
|
||||||
});
|
});
|
||||||
|
var embedded = (this.field.views || {}).tree;
|
||||||
|
if (embedded) {
|
||||||
|
this.list_view.set_embedded_view(embedded);
|
||||||
|
}
|
||||||
|
this.list_view.m2m_field = this;
|
||||||
|
var loaded = $.Deferred();
|
||||||
|
this.list_view.on("list_view_loaded", this, function() {
|
||||||
|
loaded.resolve();
|
||||||
});
|
});
|
||||||
this._super.apply(this, arguments);
|
this.list_view.appendTo(this.$el);
|
||||||
|
|
||||||
|
var old_def = self.is_loaded;
|
||||||
|
self.is_loaded = $.Deferred().done(function() {
|
||||||
|
old_def.resolve();
|
||||||
|
});
|
||||||
|
this.list_dm.add(loaded).then(function() {
|
||||||
|
self.is_loaded.resolve();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroy_content: function() {
|
||||||
|
this.list_view.destroy();
|
||||||
|
this.list_view = undefined;
|
||||||
},
|
},
|
||||||
set_value: function(value_) {
|
set_value: function(value_) {
|
||||||
value_ = value_ || [];
|
value_ = value_ || [];
|
||||||
|
@ -4120,35 +4134,10 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({
|
||||||
is_false: function () {
|
is_false: function () {
|
||||||
return _(this.get("value")).isEmpty();
|
return _(this.get("value")).isEmpty();
|
||||||
},
|
},
|
||||||
load_view: function() {
|
|
||||||
var self = this;
|
|
||||||
this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
|
|
||||||
'addable': self.get("effective_readonly") ? null : _t("Add"),
|
|
||||||
'deletable': self.get("effective_readonly") ? false : true,
|
|
||||||
'selectable': self.multi_selection,
|
|
||||||
'sortable': false,
|
|
||||||
'reorderable': false,
|
|
||||||
'import_enabled': false,
|
|
||||||
});
|
|
||||||
var embedded = (this.field.views || {}).tree;
|
|
||||||
if (embedded) {
|
|
||||||
this.list_view.set_embedded_view(embedded);
|
|
||||||
}
|
|
||||||
this.list_view.m2m_field = this;
|
|
||||||
var loaded = $.Deferred();
|
|
||||||
this.list_view.on("list_view_loaded", self, function() {
|
|
||||||
self.initial_is_loaded.resolve();
|
|
||||||
loaded.resolve();
|
|
||||||
});
|
|
||||||
$.async_when().done(function () {
|
|
||||||
self.list_view.appendTo(self.$el);
|
|
||||||
});
|
|
||||||
return loaded;
|
|
||||||
},
|
|
||||||
render_value: function() {
|
render_value: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.dataset.set_ids(this.get("value"));
|
this.dataset.set_ids(this.get("value"));
|
||||||
this.is_loaded = this.is_loaded.then(function() {
|
this.render_value_dm.add(this.is_loaded).then(function() {
|
||||||
return self.list_view.reload_content();
|
return self.list_view.reload_content();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -4590,7 +4579,7 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend
|
||||||
var self = this;
|
var self = this;
|
||||||
this.init_dataset();
|
this.init_dataset();
|
||||||
if (this.options.initial_view == "search") {
|
if (this.options.initial_view == "search") {
|
||||||
self.rpc('/web/session/eval_domain_and_context', {
|
instance.web.pyeval.eval_domains_and_contexts({
|
||||||
domains: [],
|
domains: [],
|
||||||
contexts: [this.context]
|
contexts: [this.context]
|
||||||
}).done(function (results) {
|
}).done(function (results) {
|
||||||
|
@ -4663,7 +4652,7 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend
|
||||||
},
|
},
|
||||||
do_search: function(domains, contexts, groupbys) {
|
do_search: function(domains, contexts, groupbys) {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.rpc('/web/session/eval_domain_and_context', {
|
instance.web.pyeval.eval_domains_and_contexts({
|
||||||
domains: domains || [],
|
domains: domains || [],
|
||||||
contexts: contexts || [],
|
contexts: contexts || [],
|
||||||
group_by_seq: groupbys || []
|
group_by_seq: groupbys || []
|
||||||
|
@ -4839,7 +4828,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.
|
||||||
},
|
},
|
||||||
on_file_uploaded: function(size, name, content_type, file_base64) {
|
on_file_uploaded: function(size, name, content_type, file_base64) {
|
||||||
if (size === false) {
|
if (size === false) {
|
||||||
this.do_warn("File Upload", "There was a problem while uploading your file");
|
this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
|
||||||
// TODO: use openerp web crashmanager
|
// TODO: use openerp web crashmanager
|
||||||
console.warn("Error while uploading file : ", name);
|
console.warn("Error while uploading file : ", name);
|
||||||
} else {
|
} else {
|
||||||
|
@ -5008,7 +4997,7 @@ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractFie
|
||||||
this.field_manager = field_manager;
|
this.field_manager = field_manager;
|
||||||
this.node = node;
|
this.node = node;
|
||||||
if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
|
if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
|
||||||
throw "The type of the field '"+this.field.string+"' must be a many2many field with a relation to 'ir.attachment' model.";
|
throw _.str.sprintf(_t("The type of the field '%s' must be a many2many field with a relation to 'ir.attachment' model."), this.field.string);
|
||||||
}
|
}
|
||||||
this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
|
this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
|
||||||
this.fileupload_id = _.uniqueId('oe_fileupload_temp');
|
this.fileupload_id = _.uniqueId('oe_fileupload_temp');
|
||||||
|
@ -5271,6 +5260,7 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
|
||||||
|
|
||||||
instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
|
instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
|
||||||
template: "FieldMonetary",
|
template: "FieldMonetary",
|
||||||
|
widget_class: 'oe_form_field_float oe_form_field_monetary',
|
||||||
init: function() {
|
init: function() {
|
||||||
this._super.apply(this, arguments);
|
this._super.apply(this, arguments);
|
||||||
this.set({"currency": false});
|
this.set({"currency": false});
|
||||||
|
|
|
@ -389,7 +389,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
|
||||||
if (range_stop > total) {
|
if (range_stop > total) {
|
||||||
range_stop = total;
|
range_stop = total;
|
||||||
}
|
}
|
||||||
spager = _.str.sprintf('%d-%d of %d', range_start, range_stop, total);
|
spager = _.str.sprintf(_t("%d-%d of %d"), range_start, range_stop, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$pager.find('.oe_list_pager_state').text(spager);
|
this.$pager.find('.oe_list_pager_state').text(spager);
|
||||||
|
@ -600,6 +600,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
|
||||||
|
|
||||||
this.dataset.index = _(this.dataset.ids).indexOf(ids[0]);
|
this.dataset.index = _(this.dataset.ids).indexOf(ids[0]);
|
||||||
if (this.sidebar) {
|
if (this.sidebar) {
|
||||||
|
this.options.$sidebar.show();
|
||||||
this.sidebar.$el.show();
|
this.sidebar.$el.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -887,8 +888,8 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
|
||||||
var $row;
|
var $row;
|
||||||
if (attribute === 'id') {
|
if (attribute === 'id') {
|
||||||
if (old_value) {
|
if (old_value) {
|
||||||
throw new Error("Setting 'id' attribute on existing record "
|
throw new Error(_.str.sprintf( _t("Setting 'id' attribute on existing record %s"),
|
||||||
+ JSON.stringify(record.attributes));
|
JSON.stringify(record.attributes) ));
|
||||||
}
|
}
|
||||||
if (!_.contains(self.dataset.ids, value)) {
|
if (!_.contains(self.dataset.ids, value)) {
|
||||||
// add record to dataset if not already in (added by
|
// add record to dataset if not already in (added by
|
||||||
|
@ -922,6 +923,18 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
|
||||||
}, this);
|
}, this);
|
||||||
|
|
||||||
this.$current = $('<tbody>')
|
this.$current = $('<tbody>')
|
||||||
|
.delegate('input[readonly=readonly]', 'click', function (e) {
|
||||||
|
/*
|
||||||
|
Against all logic and sense, as of right now @readonly
|
||||||
|
apparently does nothing on checkbox and radio inputs, so
|
||||||
|
the trick of using @readonly to have, well, readonly
|
||||||
|
checkboxes (which still let clicks go through) does not
|
||||||
|
work out of the box. We *still* need to preventDefault()
|
||||||
|
on the event, otherwise the checkbox's state *will* toggle
|
||||||
|
on click
|
||||||
|
*/
|
||||||
|
e.preventDefault();
|
||||||
|
})
|
||||||
.delegate('th.oe_list_record_selector', 'click', function (e) {
|
.delegate('th.oe_list_record_selector', 'click', function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
var selection = self.get_selection();
|
var selection = self.get_selection();
|
||||||
|
@ -956,7 +969,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
|
||||||
if (row_id) {
|
if (row_id) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!self.dataset.select_id(row_id)) {
|
if (!self.dataset.select_id(row_id)) {
|
||||||
throw new Error("Could not find id in dataset");
|
throw new Error(_t("Could not find id in dataset"));
|
||||||
}
|
}
|
||||||
self.row_clicked(e);
|
self.row_clicked(e);
|
||||||
}
|
}
|
||||||
|
@ -1016,7 +1029,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
|
||||||
var command = value[0];
|
var command = value[0];
|
||||||
// 1. an array of m2m commands (usually (6, false, ids))
|
// 1. an array of m2m commands (usually (6, false, ids))
|
||||||
if (command[0] !== 6) {
|
if (command[0] !== 6) {
|
||||||
throw new Error(_t("Unknown m2m command ") + command[0]);
|
throw new Error(_.str.sprintf( _t("Unknown m2m command %s"), command[0]));
|
||||||
}
|
}
|
||||||
ids = command[2];
|
ids = command[2];
|
||||||
} else {
|
} else {
|
||||||
|
@ -1324,7 +1337,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
|
||||||
}
|
}
|
||||||
// group_label is html-clean (through format or explicit
|
// group_label is html-clean (through format or explicit
|
||||||
// escaping if format failed), can inject straight into HTML
|
// escaping if format failed), can inject straight into HTML
|
||||||
$group_column.html(_.str.sprintf("%s (%d)",
|
$group_column.html(_.str.sprintf(_t("%s (%d)"),
|
||||||
group_label, group.length));
|
group_label, group.length));
|
||||||
|
|
||||||
if (group.length && group.openable) {
|
if (group.length && group.openable) {
|
||||||
|
@ -1743,7 +1756,7 @@ var Record = instance.web.Class.extend(/** @lends Record# */{
|
||||||
} else if (val instanceof Array) {
|
} else if (val instanceof Array) {
|
||||||
output[k] = val[0];
|
output[k] = val[0];
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Can't convert value " + val + " to context");
|
throw new Error(_.str.sprintf(_t("Can't convert value %s to context"), val));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
* @namespace
|
* @namespace
|
||||||
*/
|
*/
|
||||||
openerp.web.list_editable = function (instance) {
|
openerp.web.list_editable = function (instance) {
|
||||||
|
var _t = instance.web._t;
|
||||||
|
|
||||||
// editability status of list rows
|
// editability status of list rows
|
||||||
instance.web.ListView.prototype.defaults.editable = null;
|
instance.web.ListView.prototype.defaults.editable = null;
|
||||||
|
|
||||||
|
@ -383,7 +385,8 @@ openerp.web.list_editable = function (instance) {
|
||||||
version: '7.0'
|
version: '7.0'
|
||||||
});
|
});
|
||||||
_(view.arch.children).chain()
|
_(view.arch.children).chain()
|
||||||
.zip(this.columns)
|
.zip(_(this.columns).filter(function (c) {
|
||||||
|
return !(c instanceof instance.web.list.MetaColumn);}))
|
||||||
.each(function (ar) {
|
.each(function (ar) {
|
||||||
var widget = ar[0], column = ar[1];
|
var widget = ar[0], column = ar[1];
|
||||||
var modifiers = _.extend({}, column.modifiers);
|
var modifiers = _.extend({}, column.modifiers);
|
||||||
|
@ -775,7 +778,7 @@ openerp.web.list_editable = function (instance) {
|
||||||
cancel: function (force) {
|
cancel: function (force) {
|
||||||
if (!(force || this.form.can_be_discarded())) {
|
if (!(force || this.form.can_be_discarded())) {
|
||||||
return $.Deferred().reject({
|
return $.Deferred().reject({
|
||||||
message: "The form's data can not be discarded"}).promise();
|
message: _t("The form's data can not be discarded")}).promise();
|
||||||
}
|
}
|
||||||
var record = this.record;
|
var record = this.record;
|
||||||
this.record = null;
|
this.record = null;
|
||||||
|
|
|
@ -44,7 +44,8 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie
|
||||||
view_id: this.view_id,
|
view_id: this.view_id,
|
||||||
view_type: "tree",
|
view_type: "tree",
|
||||||
toolbar: this.view_manager ? !!this.view_manager.sidebar : false,
|
toolbar: this.view_manager ? !!this.view_manager.sidebar : false,
|
||||||
context: this.dataset.get_context()
|
context: instance.web.pyeval.eval(
|
||||||
|
'context', this.dataset.get_context())
|
||||||
}).done(this.on_loaded);
|
}).done(this.on_loaded);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
@ -227,8 +228,9 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie
|
||||||
return this.rpc('/web/treeview/action', {
|
return this.rpc('/web/treeview/action', {
|
||||||
id: id,
|
id: id,
|
||||||
model: this.dataset.model,
|
model: this.dataset.model,
|
||||||
context: new instance.web.CompoundContext(
|
context: instance.web.pyeval.eval(
|
||||||
this.dataset.get_context(), local_context)
|
'context', new instance.web.CompoundContext(
|
||||||
|
this.dataset.get_context(), local_context))
|
||||||
}).then(function (actions) {
|
}).then(function (actions) {
|
||||||
if (!actions.length) { return; }
|
if (!actions.length) { return; }
|
||||||
var action = actions[0][2];
|
var action = actions[0][2];
|
||||||
|
@ -236,7 +238,7 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie
|
||||||
if (action.context) {
|
if (action.context) {
|
||||||
c.add(action.context);
|
c.add(action.context);
|
||||||
}
|
}
|
||||||
return self.rpc('/web/session/eval_domain_and_context', {
|
return instance.web.pyeval.eval_domains_and_contexts({
|
||||||
contexts: [c], domains: []
|
contexts: [c], domains: []
|
||||||
}).then(function (res) {
|
}).then(function (res) {
|
||||||
action.context = res.context;
|
action.context = res.context;
|
||||||
|
|
|
@ -256,7 +256,9 @@ instance.web.ActionManager = instance.web.Widget.extend({
|
||||||
on_close: function() {},
|
on_close: function() {},
|
||||||
action_menu_id: null,
|
action_menu_id: null,
|
||||||
});
|
});
|
||||||
if (_.isString(action) && instance.web.client_actions.contains(action)) {
|
if (action === false) {
|
||||||
|
action = { type: 'ir.actions.act_window_close' };
|
||||||
|
} else if (_.isString(action) && instance.web.client_actions.contains(action)) {
|
||||||
var action_client = { type: "ir.actions.client", tag: action, params: {} };
|
var action_client = { type: "ir.actions.client", tag: action, params: {} };
|
||||||
return this.do_action(action_client, options);
|
return this.do_action(action_client, options);
|
||||||
} else if (_.isNumber(action) || _.isString(action)) {
|
} else if (_.isNumber(action) || _.isString(action)) {
|
||||||
|
@ -265,6 +267,17 @@ instance.web.ActionManager = instance.web.Widget.extend({
|
||||||
return self.do_action(result, options);
|
return self.do_action(result, options);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure context & domain are evaluated and can be manipulated/used
|
||||||
|
if (action.context) {
|
||||||
|
action.context = instance.web.pyeval.eval(
|
||||||
|
'context', action.context);
|
||||||
|
}
|
||||||
|
if (action.domain) {
|
||||||
|
action.domain = instance.web.pyeval.eval(
|
||||||
|
'domain', action.domain, action.context || {});
|
||||||
|
}
|
||||||
|
|
||||||
if (!action.type) {
|
if (!action.type) {
|
||||||
console.error("No type for action", action);
|
console.error("No type for action", action);
|
||||||
return $.Deferred().reject();
|
return $.Deferred().reject();
|
||||||
|
@ -382,6 +395,7 @@ instance.web.ActionManager = instance.web.Widget.extend({
|
||||||
options.on_close();
|
options.on_close();
|
||||||
}
|
}
|
||||||
this.dialog_stop();
|
this.dialog_stop();
|
||||||
|
return $.when();
|
||||||
},
|
},
|
||||||
ir_actions_server: function (action, options) {
|
ir_actions_server: function (action, options) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
@ -395,29 +409,36 @@ instance.web.ActionManager = instance.web.Widget.extend({
|
||||||
ir_actions_report_xml: function(action, options) {
|
ir_actions_report_xml: function(action, options) {
|
||||||
var self = this;
|
var self = this;
|
||||||
instance.web.blockUI();
|
instance.web.blockUI();
|
||||||
self.rpc("/web/session/eval_domain_and_context", {
|
return instance.web.pyeval.eval_domains_and_contexts({
|
||||||
contexts: [action.context],
|
contexts: [action.context],
|
||||||
domains: []
|
domains: []
|
||||||
}).done(function(res) {
|
}).then(function(res) {
|
||||||
action = _.clone(action);
|
action = _.clone(action);
|
||||||
action.context = res.context;
|
action.context = res.context;
|
||||||
var c = instance.webclient.crashmanager;
|
var c = instance.webclient.crashmanager;
|
||||||
self.session.get_file({
|
return $.Deferred(function (d) {
|
||||||
url: '/web/report',
|
self.session.get_file({
|
||||||
data: {action: JSON.stringify(action)},
|
url: '/web/report',
|
||||||
complete: instance.web.unblockUI,
|
data: {action: JSON.stringify(action)},
|
||||||
success: function(){
|
complete: instance.web.unblockUI,
|
||||||
if (!self.dialog) {
|
success: function(){
|
||||||
options.on_close();
|
if (!self.dialog) {
|
||||||
|
options.on_close();
|
||||||
|
}
|
||||||
|
self.dialog_stop();
|
||||||
|
d.resolve();
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
c.rpc_error.apply(c, arguments);
|
||||||
|
d.reject();
|
||||||
}
|
}
|
||||||
self.dialog_stop();
|
})
|
||||||
},
|
});
|
||||||
error: c.rpc_error.bind(c)
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
ir_actions_act_url: function (action) {
|
ir_actions_act_url: function (action) {
|
||||||
window.open(action.url, action.target === 'self' ? '_self' : '_blank');
|
window.open(action.url, action.target === 'self' ? '_self' : '_blank');
|
||||||
|
return $.when();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -666,7 +687,7 @@ instance.web.ViewManager = instance.web.Widget.extend({
|
||||||
var self = this,
|
var self = this,
|
||||||
controller = this.views[this.active_view].controller,
|
controller = this.views[this.active_view].controller,
|
||||||
action_context = this.action.context || {};
|
action_context = this.action.context || {};
|
||||||
this.rpc('/web/session/eval_domain_and_context', {
|
instance.web.pyeval.eval_domains_and_contexts({
|
||||||
domains: [this.action.domain || []].concat(domains || []),
|
domains: [this.action.domain || []].concat(domains || []),
|
||||||
contexts: [action_context].concat(contexts || []),
|
contexts: [action_context].concat(contexts || []),
|
||||||
group_by_seq: groupbys || []
|
group_by_seq: groupbys || []
|
||||||
|
@ -737,12 +758,6 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({
|
||||||
dataset.index = 0;
|
dataset.index = 0;
|
||||||
}
|
}
|
||||||
this.dataset = dataset;
|
this.dataset = dataset;
|
||||||
|
|
||||||
// setup storage for session-wise menu hiding
|
|
||||||
if (this.session.hidden_menutips) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.session.hidden_menutips = {};
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Initializes the ViewManagerAction: sets up the searchview (if the
|
* Initializes the ViewManagerAction: sets up the searchview (if the
|
||||||
|
@ -786,7 +801,7 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({
|
||||||
break;
|
break;
|
||||||
case 'tests':
|
case 'tests':
|
||||||
this.do_action({
|
this.do_action({
|
||||||
name: "JS Tests",
|
name: _t("JS Tests"),
|
||||||
target: 'new',
|
target: 'new',
|
||||||
type : 'ir.actions.act_url',
|
type : 'ir.actions.act_url',
|
||||||
url: '/web/tests?mod=*'
|
url: '/web/tests?mod=*'
|
||||||
|
@ -814,7 +829,7 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({
|
||||||
break;
|
break;
|
||||||
case 'translate':
|
case 'translate':
|
||||||
this.do_action({
|
this.do_action({
|
||||||
name: "Technical Translation",
|
name: _t("Technical Translation"),
|
||||||
res_model : 'ir.translation',
|
res_model : 'ir.translation',
|
||||||
domain : [['type', '!=', 'object'], '|', ['name', '=', this.dataset.model], ['name', 'ilike', this.dataset.model + ',']],
|
domain : [['type', '!=', 'object'], '|', ['name', '=', this.dataset.model], ['name', 'ilike', this.dataset.model + ',']],
|
||||||
views: [[false, 'list'], [false, 'form']],
|
views: [[false, 'list'], [false, 'form']],
|
||||||
|
@ -880,7 +895,11 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({
|
||||||
nested: true,
|
nested: true,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.session.get_file({ url: '/web/report', data: {action: JSON.stringify(action)}, complete: instance.web.unblockUI });
|
this.session.get_file({
|
||||||
|
url: '/web/report',
|
||||||
|
data: {action: JSON.stringify(action)},
|
||||||
|
complete: instance.web.unblockUI
|
||||||
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -1069,14 +1088,17 @@ instance.web.Sidebar = instance.web.Widget.extend({
|
||||||
active_id: ids[0],
|
active_id: ids[0],
|
||||||
active_ids: ids,
|
active_ids: ids,
|
||||||
active_model: self.getParent().dataset.model
|
active_model: self.getParent().dataset.model
|
||||||
};
|
};
|
||||||
|
var c = instance.web.pyeval.eval('context',
|
||||||
|
new instance.web.CompoundContext(
|
||||||
|
sidebar_eval_context, active_ids_context));
|
||||||
self.rpc("/web/action/load", {
|
self.rpc("/web/action/load", {
|
||||||
action_id: item.action.id,
|
action_id: item.action.id,
|
||||||
context: active_ids_context,
|
context: c
|
||||||
eval_context: new instance.web.CompoundContext(sidebar_eval_context, active_ids_context),
|
|
||||||
}).done(function(result) {
|
}).done(function(result) {
|
||||||
console.log(result.context);
|
result.context = new instance.web.CompoundContext(
|
||||||
result.context = new instance.web.CompoundContext(result.context || {}, active_ids_context);
|
result.context || {}, active_ids_context)
|
||||||
|
.set_eval_context(c);
|
||||||
result.flags = result.flags || {};
|
result.flags = result.flags || {};
|
||||||
result.flags.new_window = true;
|
result.flags.new_window = true;
|
||||||
self.do_action(result, {
|
self.do_action(result, {
|
||||||
|
@ -1137,7 +1159,6 @@ instance.web.Sidebar = instance.web.Widget.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
on_attachment_delete: function(e) {
|
on_attachment_delete: function(e) {
|
||||||
var self = this;
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
var self = this;
|
var self = this;
|
||||||
|
@ -1182,7 +1203,8 @@ instance.web.View = instance.web.Widget.extend({
|
||||||
"view_id": this.view_id,
|
"view_id": this.view_id,
|
||||||
"view_type": this.view_type,
|
"view_type": this.view_type,
|
||||||
toolbar: !!this.options.$sidebar,
|
toolbar: !!this.options.$sidebar,
|
||||||
context: this.dataset.get_context(context)
|
context: instance.web.pyeval.eval(
|
||||||
|
'context', this.dataset.get_context(context))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return view_loaded.then(function(r) {
|
return view_loaded.then(function(r) {
|
||||||
|
@ -1226,8 +1248,7 @@ instance.web.View = instance.web.Widget.extend({
|
||||||
};
|
};
|
||||||
var context = new instance.web.CompoundContext(dataset.get_context(), action_data.context || {});
|
var context = new instance.web.CompoundContext(dataset.get_context(), action_data.context || {});
|
||||||
|
|
||||||
var handler = function (r) {
|
var handler = function (action) {
|
||||||
var action = r;
|
|
||||||
if (action && action.constructor == Object) {
|
if (action && action.constructor == Object) {
|
||||||
var ncontext = new instance.web.CompoundContext(context);
|
var ncontext = new instance.web.CompoundContext(context);
|
||||||
if (record_id) {
|
if (record_id) {
|
||||||
|
@ -1238,18 +1259,10 @@ instance.web.View = instance.web.Widget.extend({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ncontext.add(action.context || {});
|
ncontext.add(action.context || {});
|
||||||
return self.rpc('/web/session/eval_domain_and_context', {
|
action.context = ncontext;
|
||||||
contexts: [ncontext],
|
return self.do_action(action, {
|
||||||
domains: []
|
on_close: result_handler,
|
||||||
}).then(function (results) {
|
});
|
||||||
action.context = results.context;
|
|
||||||
/* niv: previously we were overriding once more with action_data.context,
|
|
||||||
* I assumed this was not a correct behavior and removed it
|
|
||||||
*/
|
|
||||||
return self.do_action(action, {
|
|
||||||
on_close: result_handler,
|
|
||||||
});
|
|
||||||
}, null);
|
|
||||||
} else {
|
} else {
|
||||||
self.do_action({"type":"ir.actions.act_window_close"});
|
self.do_action({"type":"ir.actions.act_window_close"});
|
||||||
return result_handler();
|
return result_handler();
|
||||||
|
@ -1271,11 +1284,15 @@ instance.web.View = instance.web.Widget.extend({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
args.push(context);
|
args.push(context);
|
||||||
return dataset.call_button(action_data.name, args).done(handler);
|
return dataset.call_button(action_data.name, args).then(handler);
|
||||||
} else if (action_data.type=="action") {
|
} else if (action_data.type=="action") {
|
||||||
return this.rpc('/web/action/load', { action_id: action_data.name, context: context, do_not_eval: true}).done(handler);
|
return this.rpc('/web/action/load', {
|
||||||
|
action_id: action_data.name,
|
||||||
|
context: instance.web.pyeval.eval('context', context),
|
||||||
|
do_not_eval: true
|
||||||
|
}).then(handler);
|
||||||
} else {
|
} else {
|
||||||
return dataset.exec_workflow(record_id, action_data.name).done(handler);
|
return dataset.exec_workflow(record_id, action_data.name).then(handler);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
@ -1395,7 +1412,7 @@ instance.web.json_node_to_xml = function(node, human_readable, indent) {
|
||||||
return sindent + node;
|
return sindent + node;
|
||||||
} else if (typeof(node.tag) !== 'string' || !node.children instanceof Array || !node.attrs instanceof Object) {
|
} else if (typeof(node.tag) !== 'string' || !node.children instanceof Array || !node.attrs instanceof Object) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
_.str.sprintf("Node [%s] is not a JSONified XML node",
|
_.str.sprintf(_t("Node [%s] is not a JSONified XML node"),
|
||||||
JSON.stringify(node)));
|
JSON.stringify(node)));
|
||||||
}
|
}
|
||||||
for (var attr in node.attrs) {
|
for (var attr in node.attrs) {
|
||||||
|
@ -1429,7 +1446,7 @@ instance.web.xml_to_str = function(node) {
|
||||||
} else if (window.ActiveXObject) {
|
} else if (window.ActiveXObject) {
|
||||||
return node.xml;
|
return node.xml;
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Could not serialize XML");
|
throw new Error(_t("Could not serialize XML"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
instance.web.str_to_xml = function(s) {
|
instance.web.str_to_xml = function(s) {
|
||||||
|
@ -1437,7 +1454,7 @@ instance.web.str_to_xml = function(s) {
|
||||||
var dp = new DOMParser();
|
var dp = new DOMParser();
|
||||||
var r = dp.parseFromString(s, "text/xml");
|
var r = dp.parseFromString(s, "text/xml");
|
||||||
if (r.body && r.body.firstChild && r.body.firstChild.nodeName == 'parsererror') {
|
if (r.body && r.body.firstChild && r.body.firstChild.nodeName == 'parsererror') {
|
||||||
throw new Error("Could not parse string to xml");
|
throw new Error(_t("Could not parse string to xml"));
|
||||||
}
|
}
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
@ -1445,7 +1462,7 @@ instance.web.str_to_xml = function(s) {
|
||||||
try {
|
try {
|
||||||
xDoc = new ActiveXObject("MSXML2.DOMDocument");
|
xDoc = new ActiveXObject("MSXML2.DOMDocument");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error("Could not find a DOM Parser: " + e.message);
|
throw new Error(_.str.sprintf( _t("Could not find a DOM Parser: %s"), e.message));
|
||||||
}
|
}
|
||||||
xDoc.async = false;
|
xDoc.async = false;
|
||||||
xDoc.preserveWhiteSpace = true;
|
xDoc.preserveWhiteSpace = true;
|
||||||
|
|
|
@ -436,7 +436,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="oe_leftbar" valign="top">
|
<td class="oe_leftbar" valign="top">
|
||||||
<a class="oe_logo" href="#"><img t-att-src='_s + "/web/static/src/img/logo.png"'/></a>
|
<a class="oe_logo" t-attf-href="/?ts=#{Date.now()}"><img t-att-src='_s + "/web/static/src/img/logo.png"'/></a>
|
||||||
<div class="oe_secondary_menus_container"/>
|
<div class="oe_secondary_menus_container"/>
|
||||||
<div class="oe_footer">
|
<div class="oe_footer">
|
||||||
Powered by <a href="http://www.openerp.com" target="_blank"><span>OpenERP</span></a>
|
Powered by <a href="http://www.openerp.com" target="_blank"><span>OpenERP</span></a>
|
||||||
|
@ -448,6 +448,25 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
|
<t t-name="WebClient.timezone_notification">
|
||||||
|
<div class="oe_webclient_timezone_notification">
|
||||||
|
<p>Your user's preference timezone does not match your browser timezone:</p>
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<dt>User's timezone</dt>
|
||||||
|
<dd><t t-esc="user_timezone"/> (<t t-esc="user_offset"/>)</dd>
|
||||||
|
<dt>Browser's timezone</dt>
|
||||||
|
<dd><t t-esc="browser_offset"/></dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<p><a href="#">Click here to change your user's timezone.</a></p>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-name="WebClient.timezone_systray">
|
||||||
|
<div class="oe_topbar_item oe_timezone_systray" title="Timezone mismatch">
|
||||||
|
<span class="ui-icon ui-state-error ui-icon-alert"/>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
<t t-name="EmbedClient">
|
<t t-name="EmbedClient">
|
||||||
<div class="openerp">
|
<div class="openerp">
|
||||||
|
@ -915,11 +934,11 @@
|
||||||
</li>
|
</li>
|
||||||
<li t-if="widget.node.attrs.context" data-item="context">
|
<li t-if="widget.node.attrs.context" data-item="context">
|
||||||
<span class="oe_tooltip_technical_title">Context:</span>
|
<span class="oe_tooltip_technical_title">Context:</span>
|
||||||
<t t-esc="widget.node.attrs.context_string"/>
|
<t t-esc="widget.node.attrs.context"/>
|
||||||
</li>
|
</li>
|
||||||
<li t-if="widget.node.attrs.domain" data-item="domain">
|
<li t-if="widget.node.attrs.domain" data-item="domain">
|
||||||
<span class="oe_tooltip_technical_title">Domain:</span>
|
<span class="oe_tooltip_technical_title">Domain:</span>
|
||||||
<t t-esc="widget.node.attrs.domain_string"/>
|
<t t-esc="widget.node.attrs.domain"/>
|
||||||
</li>
|
</li>
|
||||||
<li t-if="widget.node.attrs.modifiers and widget.node.attrs.modifiers != '{}'" data-item="modifiers">
|
<li t-if="widget.node.attrs.modifiers and widget.node.attrs.modifiers != '{}'" data-item="modifiers">
|
||||||
<span class="oe_tooltip_technical_title">Modifiers:</span>
|
<span class="oe_tooltip_technical_title">Modifiers:</span>
|
||||||
|
|
|
@ -1,11 +1,573 @@
|
||||||
|
openerp.testing.section('eval.types', {
|
||||||
|
dependencies: ['web.coresetup'],
|
||||||
|
setup: function (instance) {
|
||||||
|
instance.session.uid = 42;
|
||||||
|
}
|
||||||
|
}, function (test) {
|
||||||
|
test('strftime', function (instance) {
|
||||||
|
var d = new Date();
|
||||||
|
var context = instance.web.pyeval.context();
|
||||||
|
strictEqual(
|
||||||
|
py.eval("time.strftime('%Y')", context),
|
||||||
|
String(d.getFullYear()));
|
||||||
|
strictEqual(
|
||||||
|
py.eval("time.strftime('%Y')+'-01-30'", context),
|
||||||
|
String(d.getFullYear()) + '-01-30');
|
||||||
|
strictEqual(
|
||||||
|
py.eval("time.strftime('%Y-%m-%d %H:%M:%S')", context),
|
||||||
|
_.str.sprintf('%04d-%02d-%02d %02d:%02d:%02d',
|
||||||
|
d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(),
|
||||||
|
d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds()));
|
||||||
|
});
|
||||||
|
test('context_today', function (instance) {
|
||||||
|
var d = new Date();
|
||||||
|
var context = instance.web.pyeval.context();
|
||||||
|
strictEqual(
|
||||||
|
py.eval("context_today().strftime('%Y-%m-%d')", context),
|
||||||
|
String(_.str.sprintf('%04d-%02d-%02d', d.getFullYear(), d.getMonth() + 1, d.getDate())));
|
||||||
|
});
|
||||||
|
// Port from pypy/lib_pypy/test_datetime.py
|
||||||
|
var makeEq = function (instance, c2) {
|
||||||
|
var ctx = instance.web.pyeval.context();
|
||||||
|
var c = _.extend({ td: ctx.datetime.timedelta }, c2 || {});
|
||||||
|
return function (a, b, message) {
|
||||||
|
ok(py.eval(a + ' == ' + b, c), message);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
test('timedelta.test_constructor', function (instance) {
|
||||||
|
var eq = makeEq(instance);
|
||||||
|
|
||||||
|
// keyword args to constructor
|
||||||
|
eq('td()', 'td(weeks=0, days=0, hours=0, minutes=0, seconds=0, ' +
|
||||||
|
'milliseconds=0, microseconds=0)');
|
||||||
|
eq('td(1)', 'td(days=1)');
|
||||||
|
eq('td(0, 1)', 'td(seconds=1)');
|
||||||
|
eq('td(0, 0, 1)', 'td(microseconds=1)');
|
||||||
|
eq('td(weeks=1)', 'td(days=7)');
|
||||||
|
eq('td(days=1)', 'td(hours=24)');
|
||||||
|
eq('td(hours=1)', 'td(minutes=60)');
|
||||||
|
eq('td(minutes=1)', 'td(seconds=60)');
|
||||||
|
eq('td(seconds=1)', 'td(milliseconds=1000)');
|
||||||
|
eq('td(milliseconds=1)', 'td(microseconds=1000)');
|
||||||
|
|
||||||
|
// Check float args to constructor
|
||||||
|
eq('td(weeks=1.0/7)', 'td(days=1)');
|
||||||
|
eq('td(days=1.0/24)', 'td(hours=1)');
|
||||||
|
eq('td(hours=1.0/60)', 'td(minutes=1)');
|
||||||
|
eq('td(minutes=1.0/60)', 'td(seconds=1)');
|
||||||
|
eq('td(seconds=0.001)', 'td(milliseconds=1)');
|
||||||
|
eq('td(milliseconds=0.001)', 'td(microseconds=1)');
|
||||||
|
});
|
||||||
|
test('timedelta.test_computations', function (instance) {
|
||||||
|
var c = instance.web.pyeval.context();
|
||||||
|
var zero = py.float.fromJSON(0);
|
||||||
|
var eq = makeEq(instance, {
|
||||||
|
// one week
|
||||||
|
a: py.PY_call(c.datetime.timedelta, [
|
||||||
|
py.float.fromJSON(7)]),
|
||||||
|
// one minute
|
||||||
|
b: py.PY_call(c.datetime.timedelta, [
|
||||||
|
zero, py.float.fromJSON(60)]),
|
||||||
|
// one millisecond
|
||||||
|
c: py.PY_call(c.datetime.timedelta, [
|
||||||
|
zero, zero, py.float.fromJSON(1000)]),
|
||||||
|
});
|
||||||
|
|
||||||
|
eq('a+b+c', 'td(7, 60, 1000)');
|
||||||
|
eq('a-b', 'td(6, 24*3600 - 60)');
|
||||||
|
eq('-a', 'td(-7)');
|
||||||
|
eq('+a', 'td(7)');
|
||||||
|
eq('-b', 'td(-1, 24*3600 - 60)');
|
||||||
|
eq('-c', 'td(-1, 24*3600 - 1, 999000)');
|
||||||
|
// eq('abs(a)', 'a');
|
||||||
|
// eq('abs(-a)', 'a');
|
||||||
|
eq('td(6, 24*3600)', 'a');
|
||||||
|
eq('td(0, 0, 60*1000000)', 'b');
|
||||||
|
eq('a*10', 'td(70)');
|
||||||
|
eq('a*10', '10*a');
|
||||||
|
// eq('a*10L', '10*a');
|
||||||
|
eq('b*10', 'td(0, 600)');
|
||||||
|
eq('10*b', 'td(0, 600)');
|
||||||
|
// eq('b*10L', 'td(0, 600)');
|
||||||
|
eq('c*10', 'td(0, 0, 10000)');
|
||||||
|
eq('10*c', 'td(0, 0, 10000)');
|
||||||
|
// eq('c*10L', 'td(0, 0, 10000)');
|
||||||
|
eq('a*-1', '-a');
|
||||||
|
eq('b*-2', '-b-b');
|
||||||
|
eq('c*-2', '-c+-c');
|
||||||
|
eq('b*(60*24)', '(b*60)*24');
|
||||||
|
eq('b*(60*24)', '(60*b)*24');
|
||||||
|
eq('c*1000', 'td(0, 1)');
|
||||||
|
eq('1000*c', 'td(0, 1)');
|
||||||
|
eq('a//7', 'td(1)');
|
||||||
|
eq('b//10', 'td(0, 6)');
|
||||||
|
eq('c//1000', 'td(0, 0, 1)');
|
||||||
|
eq('a//10', 'td(0, 7*24*360)');
|
||||||
|
eq('a//3600000', 'td(0, 0, 7*24*1000)');
|
||||||
|
|
||||||
|
// Issue #11576
|
||||||
|
eq('td(999999999, 86399, 999999) - td(999999999, 86399, 999998)', 'td(0, 0, 1)');
|
||||||
|
eq('td(999999999, 1, 1) - td(999999999, 1, 0)',
|
||||||
|
'td(0, 0, 1)')
|
||||||
|
});
|
||||||
|
test('timedelta.test_basic_attributes', function (instance) {
|
||||||
|
var ctx = instance.web.pyeval.context();
|
||||||
|
strictEqual(py.eval('datetime.timedelta(1, 7, 31).days', ctx), 1);
|
||||||
|
strictEqual(py.eval('datetime.timedelta(1, 7, 31).seconds', ctx), 7);
|
||||||
|
strictEqual(py.eval('datetime.timedelta(1, 7, 31).microseconds', ctx), 31);
|
||||||
|
});
|
||||||
|
test('timedelta.test_total_seconds', function (instance) {
|
||||||
|
var c = { timedelta: instance.web.pyeval.context().datetime.timedelta };
|
||||||
|
strictEqual(py.eval('timedelta(365).total_seconds()', c), 31536000);
|
||||||
|
strictEqual(
|
||||||
|
py.eval('timedelta(seconds=123456.789012).total_seconds()', c),
|
||||||
|
123456.789012);
|
||||||
|
strictEqual(
|
||||||
|
py.eval('timedelta(seconds=-123456.789012).total_seconds()', c),
|
||||||
|
-123456.789012);
|
||||||
|
strictEqual(
|
||||||
|
py.eval('timedelta(seconds=0.123456).total_seconds()', c), 0.123456);
|
||||||
|
strictEqual(py.eval('timedelta().total_seconds()', c), 0);
|
||||||
|
strictEqual(
|
||||||
|
py.eval('timedelta(seconds=1000000).total_seconds()', c), 1e6);
|
||||||
|
});
|
||||||
|
test('timedelta.test_str', function (instance) {
|
||||||
|
var c = { td: instance.web.pyeval.context().datetime.timedelta };
|
||||||
|
|
||||||
|
strictEqual(py.eval('str(td(1))', c), "1 day, 0:00:00");
|
||||||
|
strictEqual(py.eval('str(td(-1))', c), "-1 day, 0:00:00");
|
||||||
|
strictEqual(py.eval('str(td(2))', c), "2 days, 0:00:00");
|
||||||
|
strictEqual(py.eval('str(td(-2))', c), "-2 days, 0:00:00");
|
||||||
|
|
||||||
|
strictEqual(py.eval('str(td(hours=12, minutes=58, seconds=59))', c),
|
||||||
|
"12:58:59");
|
||||||
|
strictEqual(py.eval('str(td(hours=2, minutes=3, seconds=4))', c),
|
||||||
|
"2:03:04");
|
||||||
|
strictEqual(
|
||||||
|
py.eval('str(td(weeks=-30, hours=23, minutes=12, seconds=34))', c),
|
||||||
|
"-210 days, 23:12:34");
|
||||||
|
|
||||||
|
strictEqual(py.eval('str(td(milliseconds=1))', c), "0:00:00.001000");
|
||||||
|
strictEqual(py.eval('str(td(microseconds=3))', c), "0:00:00.000003");
|
||||||
|
|
||||||
|
strictEqual(
|
||||||
|
py.eval('str(td(days=999999999, hours=23, minutes=59, seconds=59, microseconds=999999))', c),
|
||||||
|
"999999999 days, 23:59:59.999999");
|
||||||
|
});
|
||||||
|
test('timedelta.test_massive_normalization', function (instance) {
|
||||||
|
var td = py.PY_call(
|
||||||
|
instance.web.pyeval.context().datetime.timedelta,
|
||||||
|
{microseconds: py.float.fromJSON(-1)});
|
||||||
|
strictEqual(td.days, -1);
|
||||||
|
strictEqual(td.seconds, 24 * 3600 - 1);
|
||||||
|
strictEqual(td.microseconds, 999999);
|
||||||
|
});
|
||||||
|
test('timedelta.test_bool', function (instance) {
|
||||||
|
var c = { td: instance.web.pyeval.context().datetime.timedelta };
|
||||||
|
ok(py.eval('bool(td(1))', c));
|
||||||
|
ok(py.eval('bool(td(0, 1))', c));
|
||||||
|
ok(py.eval('bool(td(0, 0, 1))', c));
|
||||||
|
ok(py.eval('bool(td(microseconds=1))', c));
|
||||||
|
ok(py.eval('bool(not td(0))', c));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('date.test_computations', function (instance) {
|
||||||
|
var d = instance.web.pyeval.context().datetime;
|
||||||
|
|
||||||
|
var a = d.date.fromJSON(2002, 1, 31);
|
||||||
|
var b = d.date.fromJSON(1956, 1, 31);
|
||||||
|
strictEqual(
|
||||||
|
py.eval('(a - b).days', {a: a, b: b}),
|
||||||
|
46 * 365 + 12);
|
||||||
|
strictEqual(py.eval('(a - b).seconds', {a: a, b: b}), 0);
|
||||||
|
strictEqual(py.eval('(a - b).microseconds', {a: a, b: b}), 0);
|
||||||
|
|
||||||
|
var day = py.PY_call(d.timedelta, [py.float.fromJSON(1)]);
|
||||||
|
var week = py.PY_call(d.timedelta, [py.float.fromJSON(7)]);
|
||||||
|
a = d.date.fromJSON(2002, 3, 2);
|
||||||
|
var ctx = {
|
||||||
|
a: a,
|
||||||
|
day: day,
|
||||||
|
week: week,
|
||||||
|
date: d.date
|
||||||
|
};
|
||||||
|
ok(py.eval('a + day == date(2002, 3, 3)', ctx));
|
||||||
|
ok(py.eval('day + a == date(2002, 3, 3)', ctx)); // 5
|
||||||
|
ok(py.eval('a - day == date(2002, 3, 1)', ctx));
|
||||||
|
ok(py.eval('-day + a == date(2002, 3, 1)', ctx));
|
||||||
|
ok(py.eval('a + week == date(2002, 3, 9)', ctx));
|
||||||
|
ok(py.eval('a - week == date(2002, 2, 23)', ctx));
|
||||||
|
ok(py.eval('a + 52*week == date(2003, 3, 1)', ctx)); // 10
|
||||||
|
ok(py.eval('a - 52*week == date(2001, 3, 3)', ctx));
|
||||||
|
ok(py.eval('(a + week) - a == week', ctx));
|
||||||
|
ok(py.eval('(a + day) - a == day', ctx));
|
||||||
|
ok(py.eval('(a - week) - a == -week', ctx));
|
||||||
|
ok(py.eval('(a - day) - a == -day', ctx)); // 15
|
||||||
|
ok(py.eval('a - (a + week) == -week', ctx));
|
||||||
|
ok(py.eval('a - (a + day) == -day', ctx));
|
||||||
|
ok(py.eval('a - (a - week) == week', ctx));
|
||||||
|
ok(py.eval('a - (a - day) == day', ctx));
|
||||||
|
|
||||||
|
raises(function () {
|
||||||
|
py.eval('a + 1', ctx);
|
||||||
|
}, /^Error: TypeError:/); // 20
|
||||||
|
raises(function () {
|
||||||
|
py.eval('a - 1', ctx);
|
||||||
|
}, /^Error: TypeError:/);
|
||||||
|
raises(function () {
|
||||||
|
py.eval('1 + a', ctx);
|
||||||
|
}, /^Error: TypeError:/);
|
||||||
|
raises(function () {
|
||||||
|
py.eval('1 - a', ctx);
|
||||||
|
}, /^Error: TypeError:/);
|
||||||
|
|
||||||
|
// delta - date is senseless.
|
||||||
|
raises(function () {
|
||||||
|
py.eval('day - a', ctx);
|
||||||
|
}, /^Error: TypeError:/);
|
||||||
|
// mixing date and (delta or date) via * or // is senseless
|
||||||
|
raises(function () {
|
||||||
|
py.eval('day * a', ctx);
|
||||||
|
}, /^Error: TypeError:/); // 25
|
||||||
|
raises(function () {
|
||||||
|
py.eval('a * day', ctx);
|
||||||
|
}, /^Error: TypeError:/);
|
||||||
|
raises(function () {
|
||||||
|
py.eval('day // a', ctx);
|
||||||
|
}, /^Error: TypeError:/);
|
||||||
|
raises(function () {
|
||||||
|
py.eval('a // day', ctx);
|
||||||
|
}, /^Error: TypeError:/);
|
||||||
|
raises(function () {
|
||||||
|
py.eval('a * a', ctx);
|
||||||
|
}, /^Error: TypeError:/);
|
||||||
|
raises(function () {
|
||||||
|
py.eval('a // a', ctx);
|
||||||
|
}, /^Error: TypeError:/); // 30
|
||||||
|
// date + date is senseless
|
||||||
|
raises(function () {
|
||||||
|
py.eval('a + a', ctx);
|
||||||
|
}, /^Error: TypeError:/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
openerp.testing.section('eval.edc', {
|
||||||
|
dependencies: ['web.data'],
|
||||||
|
rpc: 'mock',
|
||||||
|
setup: function (instance, $fix, mock) {
|
||||||
|
var user = { login: 'admin', id: 1, lang: 'en_US', tz: false };
|
||||||
|
instance.edc = function (domains, contexts) {
|
||||||
|
return instance.web.pyeval.eval_domains_and_contexts({
|
||||||
|
contexts: contexts || [],
|
||||||
|
domains: domains || []
|
||||||
|
});
|
||||||
|
};
|
||||||
|
mock('res.lang:load_lang', function () { return true; });
|
||||||
|
mock('res.users:write', function (args) {
|
||||||
|
_.extend(user, args[1]);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
mock('/web/session/get_session_info', function () {
|
||||||
|
return {
|
||||||
|
session_id: 'foobar',
|
||||||
|
db: '3',
|
||||||
|
login: user.login,
|
||||||
|
uid: user.id,
|
||||||
|
context: {
|
||||||
|
uid: user.id,
|
||||||
|
lang: user.lang,
|
||||||
|
tz: user.tz
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return instance.session.session_reload();
|
||||||
|
}
|
||||||
|
}, function (test) {
|
||||||
|
test('empty, basic', {asserts: 3}, function (instance) {
|
||||||
|
return instance.edc().then(function (result) {
|
||||||
|
// default values for new db
|
||||||
|
deepEqual(result.context, {
|
||||||
|
lang: 'en_US',
|
||||||
|
tz: false,
|
||||||
|
uid: 1
|
||||||
|
});
|
||||||
|
deepEqual(result.domain, []);
|
||||||
|
deepEqual(result.group_by, []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('empty, context altered', {
|
||||||
|
asserts: 3,
|
||||||
|
setup: function (instance) {
|
||||||
|
var lang = new instance.web.Model('res.lang');
|
||||||
|
var users = new instance.web.Model('res.users');
|
||||||
|
return lang.call('load_lang', ['ru_RU']).then(function () {
|
||||||
|
return users.call('write', [instance.session.uid, {
|
||||||
|
lang: 'ru_RU',
|
||||||
|
tz: 'America/Santarem'
|
||||||
|
}]);
|
||||||
|
}).then(instance.session.session_reload.bind(instance.session));
|
||||||
|
}
|
||||||
|
}, function (instance) {
|
||||||
|
return instance.edc().then(function (result) {
|
||||||
|
// default values for new db
|
||||||
|
deepEqual(result.context, {
|
||||||
|
lang: 'ru_RU',
|
||||||
|
tz: 'America/Santarem',
|
||||||
|
uid: 1
|
||||||
|
});
|
||||||
|
deepEqual(result.domain, []);
|
||||||
|
deepEqual(result.group_by, []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('context_merge_00', {asserts: 1}, function (instance) {
|
||||||
|
var ctx = [
|
||||||
|
{
|
||||||
|
"__contexts": [
|
||||||
|
{ "lang": "en_US", "tz": false, "uid": 1 },
|
||||||
|
{
|
||||||
|
"active_id": 8,
|
||||||
|
"active_ids": [ 8 ],
|
||||||
|
"active_model": "sale.order",
|
||||||
|
"bin_raw": true,
|
||||||
|
"default_composition_mode": "comment",
|
||||||
|
"default_model": "sale.order",
|
||||||
|
"default_res_id": 8,
|
||||||
|
"default_template_id": 18,
|
||||||
|
"default_use_template": true,
|
||||||
|
"edi_web_url_view": "faaaake",
|
||||||
|
"lang": "en_US",
|
||||||
|
"mark_so_as_sent": null,
|
||||||
|
"show_address": null,
|
||||||
|
"tz": false,
|
||||||
|
"uid": null
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
"__eval_context": null,
|
||||||
|
"__ref": "compound_context"
|
||||||
|
},
|
||||||
|
{ "active_id": 9, "active_ids": [ 9 ], "active_model": "mail.compose.message" }
|
||||||
|
];
|
||||||
|
return instance.edc([], ctx).then(function (result) {
|
||||||
|
deepEqual(result.context, {
|
||||||
|
active_id: 9,
|
||||||
|
active_ids: [9],
|
||||||
|
active_model: 'mail.compose.message',
|
||||||
|
bin_raw: true,
|
||||||
|
default_composition_mode: 'comment',
|
||||||
|
default_model: 'sale.order',
|
||||||
|
default_res_id: 8,
|
||||||
|
default_template_id: 18,
|
||||||
|
default_use_template: true,
|
||||||
|
edi_web_url_view: "faaaake",
|
||||||
|
lang: 'en_US',
|
||||||
|
mark_so_as_sent: null,
|
||||||
|
show_address: null,
|
||||||
|
tz: false,
|
||||||
|
uid: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('context_merge_01', {asserts: 1}, function (instance) {
|
||||||
|
var ctx = [{
|
||||||
|
"__contexts": [
|
||||||
|
{
|
||||||
|
"lang": "en_US",
|
||||||
|
"tz": false,
|
||||||
|
"uid": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default_attachment_ids": [],
|
||||||
|
"default_body": "",
|
||||||
|
"default_content_subtype": "html",
|
||||||
|
"default_model": "res.users",
|
||||||
|
"default_parent_id": false,
|
||||||
|
"default_res_id": 1
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
"__eval_context": null,
|
||||||
|
"__ref": "compound_context"
|
||||||
|
}];
|
||||||
|
return instance.edc([], ctx).then(function (result) {
|
||||||
|
deepEqual(result.context, {
|
||||||
|
"default_attachment_ids": [],
|
||||||
|
"default_body": "",
|
||||||
|
"default_content_subtype": "html",
|
||||||
|
"default_model": "res.users",
|
||||||
|
"default_parent_id": false,
|
||||||
|
"default_res_id": 1,
|
||||||
|
"lang": "en_US",
|
||||||
|
"tz": false,
|
||||||
|
"uid": 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
openerp.testing.section('eval.edc.nonliterals', {
|
||||||
|
dependencies: ['web.data'],
|
||||||
|
setup: function (instance) {
|
||||||
|
instance.session.user_context = {
|
||||||
|
lang: 'en_US',
|
||||||
|
tz: false,
|
||||||
|
uid: 1
|
||||||
|
};
|
||||||
|
_.extend(instance, {
|
||||||
|
edc: function (domains, contexts) {
|
||||||
|
return instance.web.pyeval.eval_domains_and_contexts({
|
||||||
|
contexts: contexts || [],
|
||||||
|
domains: domains || []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, function (test) {
|
||||||
|
test('domain with time', {asserts: 1}, function (instance) {
|
||||||
|
return instance.edc([
|
||||||
|
[['type', '=', 'contract']],
|
||||||
|
{ "__domains": [["|"], [["state", "in", ["open", "draft"]]], [["state", "=", "pending"]]],
|
||||||
|
"__eval_context": null,
|
||||||
|
"__ref": "compound_domain"
|
||||||
|
},
|
||||||
|
"['|', '&', ('date', '!=', False), ('date', '<=', time.strftime('%Y-%m-%d')), ('is_overdue_quantity', '=', True)]",
|
||||||
|
[['user_id', '=', 1]]
|
||||||
|
]).then(function (result) {
|
||||||
|
var d = new Date();
|
||||||
|
var today = _.str.sprintf("%04d-%02d-%02d",
|
||||||
|
d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
|
||||||
|
deepEqual(result.domain, [
|
||||||
|
["type", "=", "contract"],
|
||||||
|
"|", ["state", "in", ["open", "draft"]],
|
||||||
|
["state", "=", "pending"],
|
||||||
|
"|",
|
||||||
|
"&", ["date", "!=", false],
|
||||||
|
["date", "<=", today],
|
||||||
|
["is_overdue_quantity", "=", true],
|
||||||
|
["user_id", "=", 1]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('conditional context', {asserts: 2}, function (instance) {
|
||||||
|
var d = {
|
||||||
|
__ref: 'domain',
|
||||||
|
__debug: "[('company_id', '=', context.get('company_id',False))]"
|
||||||
|
};
|
||||||
|
var e1 = instance.edc([d]).then(function (result) {
|
||||||
|
deepEqual(result.domain, [
|
||||||
|
['company_id', '=', false]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
var cd = new instance.web.CompoundDomain(d);
|
||||||
|
cd.set_eval_context({company_id: 42});
|
||||||
|
var e2 = instance.edc([cd]).then(function (result) {
|
||||||
|
deepEqual(result.domain, [
|
||||||
|
['company_id', '=', 42]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $.when(e1, e2);
|
||||||
|
});
|
||||||
|
test('substitution in context', {asserts: 1}, function (instance) {
|
||||||
|
var c = "{'default_opportunity_id': active_id, 'default_duration': 1.0, 'lng': lang}";
|
||||||
|
var cc = new instance.web.CompoundContext(c);
|
||||||
|
cc.set_eval_context({active_id: 42});
|
||||||
|
return instance.edc([], [cc]).then(function (result) {
|
||||||
|
deepEqual(result.context, {
|
||||||
|
lang: "en_US",
|
||||||
|
tz: false,
|
||||||
|
uid: 1,
|
||||||
|
default_opportunity_id: 42,
|
||||||
|
default_duration: 1.0,
|
||||||
|
lng: "en_US"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('date', {asserts: 1}, function (instance) {
|
||||||
|
var d = "[('state','!=','cancel'),('opening_date','>',context_today().strftime('%Y-%m-%d'))]";
|
||||||
|
return instance.edc([d]).then(function (result) {
|
||||||
|
var d = new Date();
|
||||||
|
var today = _.str.sprintf("%04d-%02d-%02d",
|
||||||
|
d.getFullYear(), d.getMonth() + 1, d.getDate());
|
||||||
|
deepEqual(result.domain, [
|
||||||
|
['state', '!=', 'cancel'],
|
||||||
|
['opening_date', '>', today]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('delta', {asserts: 1}, function (instance) {
|
||||||
|
var d = "[('type','=','in'),('day','<=', time.strftime('%Y-%m-%d')),('day','>',(context_today()-datetime.timedelta(days=15)).strftime('%Y-%m-%d'))]";
|
||||||
|
return instance.edc([d]).then(function (result) {
|
||||||
|
var d = new Date();
|
||||||
|
var today = _.str.sprintf("%04d-%02d-%02d",
|
||||||
|
d.getFullYear(), d.getMonth() + 1, d.getDate());
|
||||||
|
d.setDate(d.getDate() - 15);
|
||||||
|
var ago_15_d = _.str.sprintf("%04d-%02d-%02d",
|
||||||
|
d.getFullYear(), d.getMonth() + 1, d.getDate());
|
||||||
|
deepEqual(result.domain, [
|
||||||
|
['type', '=', 'in'],
|
||||||
|
['day', '<=', today],
|
||||||
|
['day', '>', ago_15_d]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('horror from the deep', {asserts: 1}, function (instance) {
|
||||||
|
var cs = [
|
||||||
|
{"__ref": "compound_context",
|
||||||
|
"__contexts": [
|
||||||
|
{"__ref": "context", "__debug": "{'k': 'foo,' + str(context.get('test_key', False))}"},
|
||||||
|
{"__ref": "compound_context",
|
||||||
|
"__contexts": [
|
||||||
|
{"lang": "en_US", "tz": false, "uid": 1},
|
||||||
|
{"lang": "en_US", "tz": false, "uid": 1,
|
||||||
|
"active_model": "sale.order", "default_type": "out",
|
||||||
|
"show_address": 1, "contact_display": "partner_address",
|
||||||
|
"active_ids": [9], "active_id": 9},
|
||||||
|
{}
|
||||||
|
], "__eval_context": null },
|
||||||
|
{"active_id": 8, "active_ids": [8],
|
||||||
|
"active_model": "stock.picking.out"},
|
||||||
|
{"__ref": "context", "__debug": "{'default_ref': 'stock.picking.out,'+str(context.get('active_id', False))}", "__id": "54d6ad1d6c45"}
|
||||||
|
], "__eval_context": null}
|
||||||
|
];
|
||||||
|
return instance.edc([], cs).then(function (result) {
|
||||||
|
deepEqual(result.context, {
|
||||||
|
k: 'foo,False',
|
||||||
|
lang: 'en_US',
|
||||||
|
tz: false,
|
||||||
|
uid: 1,
|
||||||
|
active_model: 'stock.picking.out',
|
||||||
|
active_id: 8,
|
||||||
|
active_ids: [8],
|
||||||
|
default_type: 'out',
|
||||||
|
show_address: 1,
|
||||||
|
contact_display: 'partner_address',
|
||||||
|
default_ref: 'stock.picking.out,8'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
openerp.testing.section('eval.contexts', {
|
openerp.testing.section('eval.contexts', {
|
||||||
dependencies: ['web.coresetup']
|
dependencies: ['web.coresetup']
|
||||||
}, function (test) {
|
}, function (test) {
|
||||||
|
test('context_recursive', function (instance) {
|
||||||
|
var context_to_eval = [{
|
||||||
|
__ref: 'context',
|
||||||
|
__debug: '{"foo": context.get("bar", "qux")}'
|
||||||
|
}];
|
||||||
|
deepEqual(
|
||||||
|
instance.web.pyeval.eval('contexts', context_to_eval, {bar: "ok"}),
|
||||||
|
{foo: 'ok'});
|
||||||
|
deepEqual(
|
||||||
|
instance.web.pyeval.eval('contexts', context_to_eval, {bar: false}),
|
||||||
|
{foo: false});
|
||||||
|
deepEqual(
|
||||||
|
instance.web.pyeval.eval('contexts', context_to_eval),
|
||||||
|
{foo: 'qux'});
|
||||||
|
});
|
||||||
test('context_sequences', function (instance) {
|
test('context_sequences', function (instance) {
|
||||||
// Context n should have base evaluation context + all of contexts
|
// Context n should have base evaluation context + all of contexts
|
||||||
// 0..n-1 in its own evaluation context
|
// 0..n-1 in its own evaluation context
|
||||||
var active_id = 4;
|
var active_id = 4;
|
||||||
var result = instance.session.test_eval_contexts([
|
var result = instance.web.pyeval.eval('contexts', [
|
||||||
{
|
{
|
||||||
"__contexts": [
|
"__contexts": [
|
||||||
{
|
{
|
||||||
|
@ -49,7 +611,7 @@ openerp.testing.section('eval.contexts', {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('non-literal_eval_contexts', function (instance) {
|
test('non-literal_eval_contexts', function (instance) {
|
||||||
var result = instance.session.test_eval_contexts([{
|
var result = instance.web.pyeval.eval('contexts', [{
|
||||||
"__ref": "compound_context",
|
"__ref": "compound_context",
|
||||||
"__contexts": [
|
"__contexts": [
|
||||||
{"__ref": "context", "__debug": "{'type':parent.type}",
|
{"__ref": "context", "__debug": "{'type':parent.type}",
|
||||||
|
@ -127,17 +689,101 @@ openerp.testing.section('eval.contexts', {
|
||||||
deepEqual(result, {type: 'out_invoice'});
|
deepEqual(result, {type: 'out_invoice'});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
openerp.testing.section('eval.contexts', {
|
openerp.testing.section('eval.domains', {
|
||||||
dependencies: ['web.coresetup', 'web.dates']
|
dependencies: ['web.coresetup', 'web.dates']
|
||||||
}, function (test) {
|
}, function (test) {
|
||||||
test('current_date', function (instance) {
|
test('current_date', function (instance) {
|
||||||
var current_date = instance.web.date_to_str(new Date());
|
var current_date = instance.web.date_to_str(new Date());
|
||||||
var result = instance.session.test_eval_domains(
|
var result = instance.web.pyeval.eval('domains',
|
||||||
[[],{"__ref":"domain","__debug":"[('name','>=',current_date),('name','<=',current_date)]","__id":"5dedcfc96648"}],
|
[[],{"__ref":"domain","__debug":"[('name','>=',current_date),('name','<=',current_date)]","__id":"5dedcfc96648"}],
|
||||||
instance.session.test_eval_get_context());
|
instance.web.pyeval.context());
|
||||||
deepEqual(result, [
|
deepEqual(result, [
|
||||||
['name', '>=', current_date],
|
['name', '>=', current_date],
|
||||||
['name', '<=', current_date]
|
['name', '<=', current_date]
|
||||||
]);
|
]);
|
||||||
})
|
});
|
||||||
|
test('context_freevar', function (instance) {
|
||||||
|
var domains_to_eval = [{
|
||||||
|
__ref: 'domain',
|
||||||
|
__debug: '[("foo", "=", context.get("bar", "qux"))]'
|
||||||
|
}, [['bar', '>=', 42]]];
|
||||||
|
deepEqual(
|
||||||
|
instance.web.pyeval.eval('domains', domains_to_eval, {bar: "ok"}),
|
||||||
|
[['foo', '=', 'ok'], ['bar', '>=', 42]]);
|
||||||
|
deepEqual(
|
||||||
|
instance.web.pyeval.eval('domains', domains_to_eval, {bar: false}),
|
||||||
|
[['foo', '=', false], ['bar', '>=', 42]]);
|
||||||
|
deepEqual(
|
||||||
|
instance.web.pyeval.eval('domains', domains_to_eval),
|
||||||
|
[['foo', '=', 'qux'], ['bar', '>=', 42]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
openerp.testing.section('eval.groupbys', {
|
||||||
|
dependencies: ['web.coresetup']
|
||||||
|
}, function (test) {
|
||||||
|
test('groupbys_00', function (instance) {
|
||||||
|
var result = instance.web.pyeval.eval('groupbys', [
|
||||||
|
{group_by: 'foo'},
|
||||||
|
{group_by: ['bar', 'qux']},
|
||||||
|
{group_by: null},
|
||||||
|
{group_by: 'grault'}
|
||||||
|
]);
|
||||||
|
deepEqual(result, ['foo', 'bar', 'qux', 'grault']);
|
||||||
|
});
|
||||||
|
test('groupbys_01', function (instance) {
|
||||||
|
var result = instance.web.pyeval.eval('groupbys', [
|
||||||
|
{group_by: 'foo'},
|
||||||
|
{ __ref: 'context', __debug: '{"group_by": "bar"}' },
|
||||||
|
{group_by: 'grault'}
|
||||||
|
]);
|
||||||
|
deepEqual(result, ['foo', 'bar', 'grault']);
|
||||||
|
});
|
||||||
|
test('groupbys_02', function (instance) {
|
||||||
|
var result = instance.web.pyeval.eval('groupbys', [
|
||||||
|
{group_by: 'foo'},
|
||||||
|
{
|
||||||
|
__ref: 'compound_context',
|
||||||
|
__contexts: [ {group_by: 'bar'} ],
|
||||||
|
__eval_context: null
|
||||||
|
},
|
||||||
|
{group_by: 'grault'}
|
||||||
|
]);
|
||||||
|
deepEqual(result, ['foo', 'bar', 'grault']);
|
||||||
|
});
|
||||||
|
test('groupbys_03', function (instance) {
|
||||||
|
var result = instance.web.pyeval.eval('groupbys', [
|
||||||
|
{group_by: 'foo'},
|
||||||
|
{
|
||||||
|
__ref: 'compound_context',
|
||||||
|
__contexts: [
|
||||||
|
{ __ref: 'context', __debug: '{"group_by": value}' }
|
||||||
|
],
|
||||||
|
__eval_context: { value: 'bar' }
|
||||||
|
},
|
||||||
|
{group_by: 'grault'}
|
||||||
|
]);
|
||||||
|
deepEqual(result, ['foo', 'bar', 'grault']);
|
||||||
|
});
|
||||||
|
test('groupbys_04', function (instance) {
|
||||||
|
var result = instance.web.pyeval.eval('groupbys', [
|
||||||
|
{group_by: 'foo'},
|
||||||
|
{
|
||||||
|
__ref: 'compound_context',
|
||||||
|
__contexts: [
|
||||||
|
{ __ref: 'context', __debug: '{"group_by": value}' }
|
||||||
|
],
|
||||||
|
__eval_context: { value: 'bar' }
|
||||||
|
},
|
||||||
|
{group_by: 'grault'}
|
||||||
|
], { value: 'bar' });
|
||||||
|
deepEqual(result, ['foo', 'bar', 'grault']);
|
||||||
|
});
|
||||||
|
test('groupbys_05', function (instance) {
|
||||||
|
var result = instance.web.pyeval.eval('groupbys', [
|
||||||
|
{group_by: 'foo'},
|
||||||
|
{ __ref: 'context', __debug: '{"group_by": value}' },
|
||||||
|
{group_by: 'grault'}
|
||||||
|
], { value: 'bar' });
|
||||||
|
deepEqual(result, ['foo', 'bar', 'grault']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -152,9 +152,9 @@ var makeSearchView = function (instance, dummy_widget_attributes, defaults) {
|
||||||
instance.dummy = {};
|
instance.dummy = {};
|
||||||
instance.dummy.DummyWidget = instance.web.search.Field.extend(
|
instance.dummy.DummyWidget = instance.web.search.Field.extend(
|
||||||
dummy_widget_attributes || {});
|
dummy_widget_attributes || {});
|
||||||
if (!('/web/searchview/load' in instance.session.responses)) {
|
if (!('/web/view/load' in instance.session.responses)) {
|
||||||
instance.session.responses['/web/searchview/load'] = function () {
|
instance.session.responses['/web/view/load'] = function () {
|
||||||
return {fields_view: {
|
return {
|
||||||
type: 'search',
|
type: 'search',
|
||||||
fields: {
|
fields: {
|
||||||
dummy: {type: 'char', string: "Dummy"}
|
dummy: {type: 'char', string: "Dummy"}
|
||||||
|
@ -171,16 +171,16 @@ var makeSearchView = function (instance, dummy_widget_attributes, defaults) {
|
||||||
children: []
|
children: []
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
instance.session.responses['/web/searchview/get_filters'] = function () {
|
instance.session.responses['ir.filters:get_filters'] = function () {
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
instance.session.responses['/web/searchview/fields_get'] = function () {
|
instance.session.responses['dummy.model:fields_get'] = function () {
|
||||||
return {fields: {
|
return {
|
||||||
dummy: {type: 'char', string: 'Dummy'}
|
dummy: {type: 'char', string: 'Dummy'}
|
||||||
}};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
var dataset = {model: 'dummy.model', get_context: function () { return {}; }};
|
var dataset = {model: 'dummy.model', get_context: function () { return {}; }};
|
||||||
|
@ -845,6 +845,42 @@ openerp.testing.section('search-serialization', {
|
||||||
ok(!context.get_eval_context(), "context should have no evaluation context");
|
ok(!context.get_eval_context(), "context should have no evaluation context");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
test('Empty filter domains', {asserts: 4}, function (instance) {
|
||||||
|
var view = {inputs: [], query: {on: function () {}}};
|
||||||
|
var filter_a = new instance.web.search.Filter(
|
||||||
|
{attrs: {name: 'a', context: '{}', domain: '[]'}}, view);
|
||||||
|
var filter_b = new instance.web.search.Filter(
|
||||||
|
{attrs: {name: 'b', context: '{}', domain: '[]'}}, view);
|
||||||
|
var filter_c = new instance.web.search.Filter(
|
||||||
|
{attrs: {name: 'c', context: '{b: 42}', domain: '[["a", "=", 3]]'}}, view);
|
||||||
|
var group = new instance.web.search.FilterGroup(
|
||||||
|
[filter_a, filter_b, filter_c], view);
|
||||||
|
var t1 = group.facet_for_defaults({a: true, c: true})
|
||||||
|
.done(function (facet) {
|
||||||
|
var model = facet;
|
||||||
|
if (!(model instanceof instance.web.search.Facet)) {
|
||||||
|
model = new instance.web.search.Facet(facet);
|
||||||
|
}
|
||||||
|
|
||||||
|
var domain = group.get_domain(model);
|
||||||
|
deepEqual(domain, '[["a", "=", 3]]', "domain should ignore empties");
|
||||||
|
var context = group.get_context(model);
|
||||||
|
deepEqual(context, '{b: 42}', "context should ignore empties");
|
||||||
|
});
|
||||||
|
var t2 = group.facet_for_defaults({a: true, b: true})
|
||||||
|
.done(function (facet) {
|
||||||
|
var model = facet;
|
||||||
|
if (!(model instanceof instance.web.search.Facet)) {
|
||||||
|
model = new instance.web.search.Facet(facet);
|
||||||
|
}
|
||||||
|
|
||||||
|
var domain = group.get_domain(model);
|
||||||
|
equal(domain, null, "domain should ignore empties");
|
||||||
|
var context = group.get_context(model);
|
||||||
|
equal(context, null, "context should ignore empties");
|
||||||
|
});
|
||||||
|
return $.when(t1, t2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
openerp.testing.section('removal', {
|
openerp.testing.section('removal', {
|
||||||
dependencies: ['web.search'],
|
dependencies: ['web.search'],
|
||||||
|
@ -890,9 +926,9 @@ openerp.testing.section('filters', {
|
||||||
rpc: 'mock',
|
rpc: 'mock',
|
||||||
templates: true,
|
templates: true,
|
||||||
setup: function (instance, $s, mock) {
|
setup: function (instance, $s, mock) {
|
||||||
mock('/web/searchview/load', function () {
|
mock('/web/view/load', function () {
|
||||||
// view with a single group of filters
|
// view with a single group of filters
|
||||||
return {fields_view: {
|
return {
|
||||||
type: 'search',
|
type: 'search',
|
||||||
fields: {},
|
fields: {},
|
||||||
arch: {
|
arch: {
|
||||||
|
@ -915,7 +951,7 @@ openerp.testing.section('filters', {
|
||||||
children: []
|
children: []
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, function (test) {
|
}, function (test) {
|
||||||
|
@ -992,7 +1028,7 @@ openerp.testing.section('saved_filters', {
|
||||||
}, function (test) {
|
}, function (test) {
|
||||||
test('checkboxing', {asserts: 6}, function (instance, $fix, mock) {
|
test('checkboxing', {asserts: 6}, function (instance, $fix, mock) {
|
||||||
var view = makeSearchView(instance);
|
var view = makeSearchView(instance);
|
||||||
mock('/web/searchview/get_filters', function () {
|
mock('ir.filters:get_filters', function () {
|
||||||
return [{ name: "filter name", user_id: 42 }];
|
return [{ name: "filter name", user_id: 42 }];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1015,7 +1051,7 @@ openerp.testing.section('saved_filters', {
|
||||||
});
|
});
|
||||||
test('removal', {asserts: 1}, function (instance, $fix, mock) {
|
test('removal', {asserts: 1}, function (instance, $fix, mock) {
|
||||||
var view = makeSearchView(instance);
|
var view = makeSearchView(instance);
|
||||||
mock('/web/searchview/get_filters', function () {
|
mock('ir.filters:get_filters', function () {
|
||||||
return [{ name: "filter name", user_id: 42 }];
|
return [{ name: "filter name", user_id: 42 }];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from . import test_dataset, test_menu, test_serving_base, test_view, test_js
|
from . import test_dataset, test_menu, test_serving_base, test_js
|
||||||
|
|
||||||
fast_suite = []
|
fast_suite = []
|
||||||
checks = [
|
checks = [
|
||||||
test_dataset,
|
test_dataset,
|
||||||
test_menu,
|
test_menu,
|
||||||
test_serving_base,
|
test_serving_base,
|
||||||
test_view,
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -18,14 +18,14 @@ class TestDataSetController(unittest2.TestCase):
|
||||||
self.dataset.do_search_read(self.request, 'fake.model'),
|
self.dataset.do_search_read(self.request, 'fake.model'),
|
||||||
{'records': [], 'length': 0})
|
{'records': [], 'length': 0})
|
||||||
self.read.assert_called_once_with(
|
self.read.assert_called_once_with(
|
||||||
[], False, self.request.session.eval_context())
|
[], False, self.request.context)
|
||||||
|
|
||||||
def test_regular_find(self):
|
def test_regular_find(self):
|
||||||
self.search.return_value = [1, 2, 3]
|
self.search.return_value = [1, 2, 3]
|
||||||
|
|
||||||
self.dataset.do_search_read(self.request, 'fake.model')
|
self.dataset.do_search_read(self.request, 'fake.model')
|
||||||
self.read.assert_called_once_with(
|
self.read.assert_called_once_with(
|
||||||
[1, 2, 3], False,self.request.session.eval_context())
|
[1, 2, 3], False,self.request.context)
|
||||||
|
|
||||||
def test_ids_shortcut(self):
|
def test_ids_shortcut(self):
|
||||||
self.search.return_value = [1, 2, 3]
|
self.search.return_value = [1, 2, 3]
|
||||||
|
|
|
@ -4,7 +4,6 @@ import mock
|
||||||
import unittest2
|
import unittest2
|
||||||
|
|
||||||
from ..controllers import main
|
from ..controllers import main
|
||||||
from ..session import OpenERPSession
|
|
||||||
|
|
||||||
class Placeholder(object):
|
class Placeholder(object):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
@ -40,11 +39,11 @@ class LoadTest(unittest2.TestCase):
|
||||||
root = self.menu.do_load(self.request)
|
root = self.menu.do_load(self.request)
|
||||||
|
|
||||||
self.MockMenus.search.assert_called_with(
|
self.MockMenus.search.assert_called_with(
|
||||||
[], 0, False, False, self.request.session.eval_context())
|
[], 0, False, False, self.request.context)
|
||||||
self.MockMenus.read.assert_called_with(
|
self.MockMenus.read.assert_called_with(
|
||||||
[], ['name', 'sequence', 'parent_id',
|
[], ['name', 'sequence', 'parent_id',
|
||||||
'action', 'needaction_enabled', 'needaction_counter'],
|
'action', 'needaction_enabled', 'needaction_counter'],
|
||||||
self.request.session.eval_context())
|
self.request.context)
|
||||||
|
|
||||||
self.assertListEqual(
|
self.assertListEqual(
|
||||||
root['children'],
|
root['children'],
|
||||||
|
@ -63,7 +62,7 @@ class LoadTest(unittest2.TestCase):
|
||||||
self.MockMenus.read.assert_called_with(
|
self.MockMenus.read.assert_called_with(
|
||||||
[1, 2, 3], ['name', 'sequence', 'parent_id',
|
[1, 2, 3], ['name', 'sequence', 'parent_id',
|
||||||
'action', 'needaction_enabled', 'needaction_counter'],
|
'action', 'needaction_enabled', 'needaction_counter'],
|
||||||
self.request.session.eval_context())
|
self.request.context)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
root['children'],
|
root['children'],
|
||||||
|
|
|
@ -1,128 +0,0 @@
|
||||||
import copy
|
|
||||||
import xml.etree.ElementTree
|
|
||||||
import mock
|
|
||||||
|
|
||||||
import unittest2
|
|
||||||
import simplejson
|
|
||||||
|
|
||||||
import openerp.addons.web.controllers.main
|
|
||||||
from .. import nonliterals, session as s
|
|
||||||
|
|
||||||
def field_attrs(fields_view_get, fieldname):
|
|
||||||
(field,) = filter(lambda f: f['attrs'].get('name') == fieldname,
|
|
||||||
fields_view_get['arch']['children'])
|
|
||||||
return field['attrs']
|
|
||||||
|
|
||||||
#noinspection PyCompatibility
|
|
||||||
class DomainsAndContextsTest(unittest2.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.view = openerp.addons.web.controllers.main.View()
|
|
||||||
|
|
||||||
def test_convert_literal_domain(self):
|
|
||||||
e = xml.etree.ElementTree.Element(
|
|
||||||
'field', domain=" [('somefield', '=', 3)] ")
|
|
||||||
self.view.parse_domains_and_contexts(e, None)
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
e.get('domain'),
|
|
||||||
[('somefield', '=', 3)])
|
|
||||||
|
|
||||||
def test_convert_complex_domain(self):
|
|
||||||
e = xml.etree.ElementTree.Element(
|
|
||||||
'field',
|
|
||||||
domain="[('account_id.type','in',['receivable','payable']),"
|
|
||||||
"('reconcile_id','=',False),"
|
|
||||||
"('reconcile_partial_id','=',False),"
|
|
||||||
"('state', '=', 'valid')]"
|
|
||||||
)
|
|
||||||
self.view.parse_domains_and_contexts(e, None)
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
e.get('domain'),
|
|
||||||
[('account_id.type', 'in', ['receivable', 'payable']),
|
|
||||||
('reconcile_id', '=', False),
|
|
||||||
('reconcile_partial_id', '=', False),
|
|
||||||
('state', '=', 'valid')]
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_retrieve_nonliteral_domain(self):
|
|
||||||
session = mock.Mock(spec=s.OpenERPSession)
|
|
||||||
session.domains_store = {}
|
|
||||||
domain_string = ("[('month','=',(datetime.date.today() - "
|
|
||||||
"datetime.timedelta(365/12)).strftime('%%m'))]")
|
|
||||||
e = xml.etree.ElementTree.Element(
|
|
||||||
'field', domain=domain_string)
|
|
||||||
|
|
||||||
self.view.parse_domains_and_contexts(e, session)
|
|
||||||
|
|
||||||
self.assertIsInstance(e.get('domain'), nonliterals.Domain)
|
|
||||||
self.assertEqual(
|
|
||||||
nonliterals.Domain(
|
|
||||||
session, key=e.get('domain').key).get_domain_string(),
|
|
||||||
domain_string)
|
|
||||||
|
|
||||||
def test_convert_literal_context(self):
|
|
||||||
e = xml.etree.ElementTree.Element(
|
|
||||||
'field', context=" {'some_prop': 3} ")
|
|
||||||
self.view.parse_domains_and_contexts(e, None)
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
e.get('context'),
|
|
||||||
{'some_prop': 3})
|
|
||||||
|
|
||||||
def test_convert_complex_context(self):
|
|
||||||
e = xml.etree.ElementTree.Element(
|
|
||||||
'field',
|
|
||||||
context="{'account_id.type': ['receivable','payable'],"
|
|
||||||
"'reconcile_id': False,"
|
|
||||||
"'reconcile_partial_id': False,"
|
|
||||||
"'state': 'valid'}"
|
|
||||||
)
|
|
||||||
self.view.parse_domains_and_contexts(e, None)
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
e.get('context'),
|
|
||||||
{'account_id.type': ['receivable', 'payable'],
|
|
||||||
'reconcile_id': False,
|
|
||||||
'reconcile_partial_id': False,
|
|
||||||
'state': 'valid'}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_retrieve_nonliteral_context(self):
|
|
||||||
session = mock.Mock(spec=s.OpenERPSession)
|
|
||||||
session.contexts_store = {}
|
|
||||||
context_string = ("{'month': (datetime.date.today() - "
|
|
||||||
"datetime.timedelta(365/12)).strftime('%%m')}")
|
|
||||||
e = xml.etree.ElementTree.Element(
|
|
||||||
'field', context=context_string)
|
|
||||||
|
|
||||||
self.view.parse_domains_and_contexts(e, session)
|
|
||||||
|
|
||||||
self.assertIsInstance(e.get('context'), nonliterals.Context)
|
|
||||||
self.assertEqual(
|
|
||||||
nonliterals.Context(
|
|
||||||
session, key=e.get('context').key).get_context_string(),
|
|
||||||
context_string)
|
|
||||||
|
|
||||||
class AttrsNormalizationTest(unittest2.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.view = openerp.addons.web.controllers.main.View()
|
|
||||||
|
|
||||||
def test_identity(self):
|
|
||||||
web_view = """
|
|
||||||
<form string="Title">
|
|
||||||
<group>
|
|
||||||
<field name="some_field"/>
|
|
||||||
<field name="some_other_field"/>
|
|
||||||
</group>
|
|
||||||
<field name="stuff"/>
|
|
||||||
</form>
|
|
||||||
"""
|
|
||||||
|
|
||||||
pristine = xml.etree.ElementTree.fromstring(web_view)
|
|
||||||
transformed = self.view.transform_view(web_view, None)
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
xml.etree.ElementTree.tostring(transformed),
|
|
||||||
xml.etree.ElementTree.tostring(pristine)
|
|
||||||
)
|
|
|
@ -7,15 +7,15 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: openerp-web\n"
|
"Project-Id-Version: openerp-web\n"
|
||||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"POT-Creation-Date: 2012-11-24 01:23+0000\n"
|
"POT-Creation-Date: 2012-11-30 18:13+0000\n"
|
||||||
"PO-Revision-Date: 2012-11-24 06:30+0000\n"
|
"PO-Revision-Date: 2012-11-24 06:30+0000\n"
|
||||||
"Last-Translator: kifcaliph <Unknown>\n"
|
"Last-Translator: kifcaliph <Unknown>\n"
|
||||||
"Language-Team: Arabic <ar@li.org>\n"
|
"Language-Team: Arabic <ar@li.org>\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"X-Launchpad-Export-Date: 2012-11-25 06:41+0000\n"
|
"X-Launchpad-Export-Date: 2012-12-01 05:21+0000\n"
|
||||||
"X-Generator: Launchpad (build 16293)\n"
|
"X-Generator: Launchpad (build 16319)\n"
|
||||||
|
|
||||||
#. module: web_calendar
|
#. module: web_calendar
|
||||||
#. openerp-web
|
#. openerp-web
|
||||||
|
@ -38,6 +38,13 @@ msgstr "التفاصيل"
|
||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr "حفظ"
|
msgstr "حفظ"
|
||||||
|
|
||||||
|
#. module: web_calendar
|
||||||
|
#. openerp-web
|
||||||
|
#: code:addons/web_calendar/static/src/js/calendar.js:99
|
||||||
|
#, python-format
|
||||||
|
msgid "Calendar view has a 'date_delay' type != float"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. module: web_calendar
|
#. module: web_calendar
|
||||||
#. openerp-web
|
#. openerp-web
|
||||||
#: code:addons/web_calendar/static/src/js/calendar.js:147
|
#: code:addons/web_calendar/static/src/js/calendar.js:147
|
||||||
|
@ -202,5 +209,12 @@ msgstr "إلغاء"
|
||||||
msgid "Calendar"
|
msgid "Calendar"
|
||||||
msgstr "التقويم"
|
msgstr "التقويم"
|
||||||
|
|
||||||
|
#. module: web_calendar
|
||||||
|
#. openerp-web
|
||||||
|
#: code:addons/web_calendar/static/src/js/calendar.js:91
|
||||||
|
#, python-format
|
||||||
|
msgid "Calendar view has not defined 'date_start' attribute."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#~ msgid "Navigator"
|
#~ msgid "Navigator"
|
||||||
#~ msgstr "المتصفح"
|
#~ msgstr "المتصفح"
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue