[MERGE] trunk

bzr revid: fme@openerp.com-20121212133800-k67gy1gctt5dxzwk
This commit is contained in:
Fabien Meghazi
2012-12-12 14:38:00 +01:00
421 changed files with 165773 additions and 81912 deletions
+13 -18
View File
@@ -1,19 +1,14 @@
.*.swp
.bzrignore
.idea
.project
.pydevproject
.ropeproject
.settings
.DS_Store
openerp/addons/*
openerp/filestore*
.Python
*.pyc
*.pyo
bin/*
.*
*.egg-info
*.orig
*.vim
build/
include/
lib/
share/
doc/_build/*
RE:^bin/
RE:^dist/
RE:^include/
RE:^share/
RE:^man/
RE:^lib/
RE:^addons/\w+/doc/_build/
+1
View File
@@ -1,4 +1,5 @@
import http
import controllers
import cli
wsgi_postload = http.wsgi_postload
+19 -1
View File
@@ -18,7 +18,7 @@ This module provides the core of the OpenERP Web Client.
"static/lib/datejs/parser.js",
"static/lib/datejs/sugarpak.js",
"static/lib/datejs/extras.js",
"static/lib/jquery/jquery-1.8.2.js",
"static/lib/jquery/jquery-1.8.3.js",
"static/lib/jquery.MD5/jquery.md5.js",
"static/lib/jquery.form/jquery.form.js",
"static/lib/jquery.validate/jquery.validate.js",
@@ -41,6 +41,8 @@ This module provides the core of the OpenERP Web Client.
"static/lib/cleditor/jquery.cleditor.js",
"static/lib/py.js/lib/py.js",
"static/src/js/boot.js",
"static/src/js/testing.js",
"static/src/js/pyeval.js",
"static/src/js/corelib.js",
"static/src/js/coresetup.js",
"static/src/js/dates.js",
@@ -68,5 +70,21 @@ This module provides the core of the OpenERP Web Client.
'qweb' : [
"static/src/xml/*.xml",
],
'test': [
"static/test/testing.js",
"static/test/class.js",
"static/test/registry.js",
"static/test/form.js",
"static/test/list-utils.js",
"static/test/formats.js",
"static/test/rpc.js",
"static/test/evals.js",
"static/test/search.js",
"static/test/Widget.js",
"static/test/list.js",
"static/test/list-editable.js",
"static/test/mutex.js"
],
'bootstrap': True,
'twitter': False,
}
+1
View File
@@ -0,0 +1 @@
import test_js
+35
View File
@@ -0,0 +1,35 @@
import logging
import optparse
import sys
import unittest2
import openerp
import openerp.addons.web.tests
_logger = logging.getLogger(__name__)
class TestJs(openerp.cli.Command):
def run(self, args):
self.parser = parser = optparse.OptionParser()
parser.add_option("-d", "--database", dest="db_name", default=False, help="specify the database name")
parser.add_option("--xmlrpc-port", dest="xmlrpc_port", default=8069, help="specify the TCP port for the XML-RPC protocol", type="int")
# proably need to add both --superadmin-password and --database-admin-password
self.parser.parse_args(args)
# test ony uses db_name xmlrpc_port admin_passwd, so use the server one for the actual parsing
config = openerp.tools.config
config.parse_config(args)
# needed until runbot is fixed
config['db_password'] = config['admin_passwd']
# run js tests
openerp.netsvc.init_alternative_logger()
suite = unittest2.TestSuite()
suite.addTests(unittest2.TestLoader().loadTestsFromModule(openerp.addons.web.tests.test_js))
r = unittest2.TextTestRunner(verbosity=2).run(suite)
if r.errors or r.failures:
sys.exit(1)
# vim:et:ts=4:sw=4:
+1
View File
@@ -1 +1,2 @@
from . import main
from . import testing
+139 -428
View File
@@ -28,9 +28,9 @@ except ImportError:
xlwt = None
import openerp
from openerp.tools.translate import _
from .. import http
from .. import nonliterals
openerpweb = http
#----------------------------------------------------------
@@ -82,7 +82,6 @@ def rjsmin(script):
return result
def db_list(req):
dbs = []
proxy = req.session.proxy("db")
dbs = proxy.list()
h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
@@ -92,7 +91,7 @@ def db_list(req):
return dbs
def db_monodb(req):
# if only one db is listed returns it else return False
# if only one db exists, return it else return False
try:
dbs = db_list(req)
if len(dbs) == 1:
@@ -189,14 +188,14 @@ def module_installed_bypass_session(dbname):
sorted_modules = module_topological_sort(modules)
return sorted_modules
def module_boot(req):
def module_boot(req, db=None):
server_wide_modules = openerp.conf.server_wide_modules or ['web']
serverside = []
dbside = []
for i in server_wide_modules:
if i in openerpweb.addons_manifest:
serverside.append(i)
monodb = db_monodb(req)
monodb = db or db_monodb(req)
if monodb:
dbside = module_installed_bypass_session(monodb)
dbside = [i for i in dbside if i not in serverside]
@@ -258,18 +257,24 @@ def concat_files(file_list, reader=None, intersperse=""):
files_concat = intersperse.join(files_content)
return files_concat, checksum.hexdigest()
concat_js_cache = {}
def concat_js(file_list):
content, checksum = concat_files(file_list, intersperse=';')
content = rjsmin(content)
return content, checksum
if checksum in concat_js_cache:
content = concat_js_cache[checksum]
else:
content = rjsmin(content)
concat_js_cache[checksum] = content
return content, checksum
def fs2web(path):
"""convert FS path into web path"""
return '/'.join(path.split(os.path.sep))
def manifest_glob(req, addons, key):
def manifest_glob(req, extension, addons=None, db=None):
if addons is None:
addons = module_boot(req)
addons = module_boot(req, db=db)
else:
addons = addons.split(',')
r = []
@@ -279,19 +284,21 @@ def manifest_glob(req, addons, key):
continue
# ensure does not ends with /
addons_path = os.path.join(manifest['addons_path'], '')[:-1]
globlist = manifest.get(key, [])
globlist = manifest.get(extension, [])
for pattern in globlist:
for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
r.append((path, fs2web(path[len(addons_path):])))
return r
def manifest_list(req, mods, extension):
def manifest_list(req, extension, mods=None, db=None):
if not req.debug:
path = '/web/webclient/' + extension
if mods is not None:
path += '?mods=' + mods
elif db:
path += '?db=' + db
return [path]
files = manifest_glob(req, mods, extension)
files = manifest_glob(req, extension, addons=mods, db=db)
i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
if i_am_diabetic:
@@ -338,7 +345,13 @@ def make_conditional(req, response, last_modified=None, etag=None):
return response.make_conditional(req.httprequest)
def login_and_redirect(req, db, login, key, redirect_url='/'):
req.session.authenticate(db, login, key, {})
wsgienv = req.httprequest.environ
env = dict(
base_location=req.httprequest.url_root.rstrip('/'),
HTTP_HOST=wsgienv['HTTP_HOST'],
REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
)
req.session.authenticate(db, login, key, env)
return set_cookie_and_redirect(req, redirect_url)
def set_cookie_and_redirect(req, redirect_url):
@@ -348,40 +361,15 @@ def set_cookie_and_redirect(req, redirect_url):
redirect.set_cookie('instance0|session_id', cookie_val)
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):
context = req.session.eval_context(req.context)
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))
for id, name, action in actions]
def clean_action(req, action, do_not_eval=False):
def clean_action(req, action):
action.setdefault('flags', {})
context = req.session.eval_context(req.context)
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')
if action_type == 'ir.actions.act_window':
return fix_view_modes(action)
@@ -460,39 +448,6 @@ def fix_view_modes(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):
messages = []
try:
@@ -568,21 +523,7 @@ html_template = """<!DOCTYPE html>
<body>
<!--[if lte IE 8]>
<script src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
<script>
var test = function() {
CFInstall.check({
mode: "overlay"
});
};
if (window.localStorage && false) {
if (! localStorage.getItem("hasShownGFramePopup")) {
test();
localStorage.setItem("hasShownGFramePopup", true);
}
} else {
test();
}
</script>
<script>CFInstall.check({mode: "overlay"});</script>
<![endif]-->
</body>
</html>
@@ -592,14 +533,14 @@ class Home(openerpweb.Controller):
_cp_path = '/'
@openerpweb.httprequest
def index(self, req, s_action=None, **kw):
js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, None, 'js'))
css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, None, 'css'))
def index(self, req, s_action=None, db=None, **kw):
js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, 'js', db=db))
css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, 'css', db=db))
r = html_template % {
'js': js,
'css': css,
'modules': simplejson.dumps(module_boot(req)),
'modules': simplejson.dumps(module_boot(req, db=db)),
'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
}
return r
@@ -613,19 +554,19 @@ class WebClient(openerpweb.Controller):
@openerpweb.jsonrequest
def csslist(self, req, mods=None):
return manifest_list(req, mods, 'css')
return manifest_list(req, 'css', mods=mods)
@openerpweb.jsonrequest
def jslist(self, req, mods=None):
return manifest_list(req, mods, 'js')
return manifest_list(req, 'js', mods=mods)
@openerpweb.jsonrequest
def qweblist(self, req, mods=None):
return manifest_list(req, mods, 'qweb')
return manifest_list(req, 'qweb', mods=mods)
@openerpweb.httprequest
def css(self, req, mods=None):
files = list(manifest_glob(req, mods, 'css'))
def css(self, req, mods=None, db=None):
files = list(manifest_glob(req, 'css', addons=mods, db=db))
last_modified = get_last_modified(f[0] for f in files)
if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
return werkzeug.wrappers.Response(status=304)
@@ -663,8 +604,8 @@ class WebClient(openerpweb.Controller):
last_modified, checksum)
@openerpweb.httprequest
def js(self, req, mods=None):
files = [f[0] for f in manifest_glob(req, mods, 'js')]
def js(self, req, mods=None, db=None):
files = [f[0] for f in manifest_glob(req, 'js', addons=mods, db=db)]
last_modified = get_last_modified(files)
if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
return werkzeug.wrappers.Response(status=304)
@@ -676,8 +617,8 @@ class WebClient(openerpweb.Controller):
last_modified, checksum)
@openerpweb.httprequest
def qweb(self, req, mods=None):
files = [f[0] for f in manifest_glob(req, mods, 'qweb')]
def qweb(self, req, mods=None, db=None):
files = [f[0] for f in manifest_glob(req, 'qweb', addons=mods, db=db)]
last_modified = get_last_modified(files)
if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
return werkzeug.wrappers.Response(status=304)
@@ -786,6 +727,17 @@ class Database(openerpweb.Controller):
params['db_original_name'],
params['db_name'])
@openerpweb.jsonrequest
def duplicate(self, req, fields):
params = dict(map(operator.itemgetter('name', 'value'), fields))
duplicate_attrs = (
params['super_admin_pwd'],
params['db_original_name'],
params['db_name'],
)
return req.session.proxy("db").duplicate_database(*duplicate_attrs)
@openerpweb.jsonrequest
def drop(self, req, fields):
password, db = operator.itemgetter(
@@ -797,7 +749,7 @@ class Database(openerpweb.Controller):
except xmlrpclib.Fault, e:
if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
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
def backup(self, req, backup_db, backup_pwd, token):
@@ -815,7 +767,7 @@ class Database(openerpweb.Controller):
{'fileToken': int(token)}
)
except xmlrpclib.Fault, e:
return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
return simplejson.dumps([[],[{'error': e.faultCode, 'title': _('Backup Database')}]])
@openerpweb.httprequest
def restore(self, req, db_file, restore_pwd, new_db):
@@ -836,8 +788,8 @@ class Database(openerpweb.Controller):
return req.session.proxy("db").change_admin_password(old_password, new_password)
except xmlrpclib.Fault, e:
if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
return {'error': e.faultCode, 'title': 'Change Password'}
return {'error': 'Error, password not changed !', 'title': 'Change Password'}
return {'error': e.faultCode, 'title': _('Change Password')}
return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
class Session(openerpweb.Controller):
_cp_path = "/web/session"
@@ -873,90 +825,34 @@ class Session(openerpweb.Controller):
old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
dict(map(operator.itemgetter('name', 'value'), fields)))
if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
return {'error':'All passwords have to be filled.','title': 'Change Password'}
return {'error':_('You cannot leave any password empty.'),'title': _('Change 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:
if req.session.model('res.users').change_password(
old_password, new_password):
return {'new_password':new_password}
except Exception:
return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
return {'error': 'Error, password 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')}
@openerpweb.jsonrequest
def sc_list(self, req):
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
def get_lang_list(self, req):
try:
return {
'lang_list': (req.session.proxy("db").list_lang() or []),
'error': ""
}
return req.session.proxy("db").list_lang() or []
except Exception, e:
return {"error": e, "title": "Languages"}
return {"error": e, "title": _("Languages")}
@openerpweb.jsonrequest
def modules(self, req):
# return all installed modules. Web client is smart enough to not load a module twice
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
def save_session_action(self, req, the_action):
"""
@@ -1009,15 +905,8 @@ class Session(openerpweb.Controller):
class Menu(openerpweb.Controller):
_cp_path = "/web/menu"
@openerpweb.jsonrequest
def load(self, req):
return {'data': self.do_load(req)}
@openerpweb.jsonrequest
def get_user_roots(self, req):
return self.do_get_user_roots(req)
def do_get_user_roots(self, req):
""" Return all root menu ids visible for the session user.
:param req: A request object, with an OpenERP session attribute
@@ -1026,20 +915,22 @@ class Menu(openerpweb.Controller):
:rtype: list(int)
"""
s = req.session
context = s.eval_context(req.context)
Menus = s.model('ir.ui.menu')
# 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)]
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:
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):
@openerpweb.jsonrequest
def load(self, req):
""" Loads all menu items (all applications and their sub-menus).
:param req: A request object, with an OpenERP session attribute
@@ -1047,23 +938,30 @@ class Menu(openerpweb.Controller):
:return: the menu root
:rtype: dict('children': menu_nodes)
"""
context = req.session.eval_context(req.context)
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)
menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
fields = ['name', 'sequence', 'parent_id', 'action',
'needaction_enabled']
menu_roots = Menus.read(self.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
# limited number of items (752 when all 6.1 addons are installed)
menu_ids = Menus.search([], 0, False, False, context)
menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
menu_ids = Menus.search([], 0, False, False, req.context)
menu_items = Menus.read(menu_ids, fields, req.context)
# 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
# mapping, resulting in children being correctly set on the roots.
menu_items.extend(menu_roots)
# 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:
if menu_item['parent_id']:
parent = menu_item['parent_id'][0]
@@ -1080,8 +978,24 @@ class Menu(openerpweb.Controller):
return menu_root
@openerpweb.jsonrequest
def load_needaction(self, req, menu_ids=False):
""" Loads needaction counters for all or some specific menu ids.
:return: needaction data
:rtype: dict(menu_id: {'needaction_enabled': boolean, 'needaction_counter': int})
"""
Menus = req.session.model('ir.ui.menu')
if menu_ids == False:
menu_ids = Menus.search([('needaction_enabled', '=', True)], context=req.context)
menu_needaction_data = Menus.get_needaction_data(menu_ids, req.context)
return menu_needaction_data
@openerpweb.jsonrequest
def action(self, req, menu_id):
# still used by web_shortcut
actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
[('ir.ui.menu', menu_id)], False)
return {"action": actions}
@@ -1113,12 +1027,10 @@ class DataSet(openerpweb.Controller):
"""
Model = req.session.model(model)
context, domain = eval_context_and_domain(
req.session, req.context, domain)
ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
ids = Model.search(domain, offset or 0, limit or False, sort or False,
req.context)
if limit and len(ids) == limit:
length = Model.search_count(domain, context)
length = Model.search_count(domain, req.context)
else:
length = len(ids) + (offset or 0)
if fields and fields == ['id']:
@@ -1128,7 +1040,7 @@ class DataSet(openerpweb.Controller):
'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']))
return {
'length': length,
@@ -1139,37 +1051,15 @@ class DataSet(openerpweb.Controller):
def load(self, req, model, id, fields):
m = req.session.model(model)
value = {}
r = m.read([id], False, req.session.eval_context(req.context))
r = m.read([id], False, req.context)
if r:
value = r[0]
return {'value': value}
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, {})
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()
if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
if 'display_name' in args[1]:
@@ -1182,39 +1072,9 @@ class DataSet(openerpweb.Controller):
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
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
def call_kw(self, req, model, method, args, kwargs):
@@ -1222,7 +1082,7 @@ class DataSet(openerpweb.Controller):
@openerpweb.jsonrequest
def call_button(self, req, model, method, args, domain_id=None, context_id=None):
action = self.call_common(req, model, method, args, domain_id, context_id)
action = self._call_kw(req, model, method, args, {})
if isinstance(action, dict) and action.get('type') != '':
return clean_action(req, action)
return False
@@ -1257,69 +1117,6 @@ class DataSet(openerpweb.Controller):
class View(openerpweb.Controller):
_cp_path = "/web/view"
def fields_view_get(self, req, model, view_id, view_type,
transform=True, toolbar=False, submenu=False):
Model = req.session.model(model)
context = req.session.eval_context(req.context)
fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
# todo fme?: check that we should pass the evaluated context here
self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
if toolbar and transform:
self.process_toolbar(req, fvg['toolbar'])
return fvg
def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
# depending on how it feels, xmlrpclib.ServerProxy can translate
# XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
# enjoy unicode strings which can not be trivially converted to
# strings, and it blows up during parsing.
# So ensure we fix this retardation by converting view xml back to
# bit strings.
if isinstance(fvg['arch'], unicode):
arch = fvg['arch'].encode('utf-8')
else:
arch = fvg['arch']
fvg['arch_string'] = arch
if transform:
evaluation_context = session.evaluation_context(context or {})
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']:
# Special case for id's
id_field = fvg['fields']['id']
id_field['original_type'] = id_field['type']
id_field['type'] = 'id'
for field in fvg['fields'].itervalues():
if field.get('views'):
for view in field["views"].itervalues():
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
def add_custom(self, req, view_id, arch):
CustomView = req.session.model('ir.ui.view.custom')
@@ -1327,61 +1124,22 @@ class View(openerpweb.Controller):
'user_id': req.session._uid,
'ref_id': view_id,
'arch': arch
}, req.session.eval_context(req.context))
}, req.context)
return {'result': True}
@openerpweb.jsonrequest
def undo_custom(self, req, view_id, reset=False):
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)],
0, False, False, context)
0, False, False, req.context)
if vcustom:
if reset:
CustomView.unlink(vcustom, context)
CustomView.unlink(vcustom, req.context)
else:
CustomView.unlink([vcustom[0]], context)
CustomView.unlink([vcustom[0]], req.context)
return {'result': True}
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
def load(self, req, model, view_id, view_type, toolbar=False):
return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
class TreeView(View):
_cp_path = "/web/treeview"
@@ -1391,50 +1149,6 @@ class TreeView(View):
req,'action', 'tree_but_open',[(model, id)],
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):
_cp_path = "/web/binary"
@@ -1442,7 +1156,6 @@ class Binary(openerpweb.Controller):
def image(self, req, model, id, field, **kw):
last_update = '__last_update'
Model = req.session.model(model)
context = req.session.eval_context(req.context)
headers = [('Content-Type', 'image/png')]
etag = req.httprequest.headers.get('If-None-Match')
hashed_session = hashlib.md5(req.session_id).hexdigest()
@@ -1453,22 +1166,22 @@ class Binary(openerpweb.Controller):
if not id and hashed_session == etag:
return werkzeug.wrappers.Response(status=304)
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:
return werkzeug.wrappers.Response(status=304)
retag = hashed_session
try:
if not id:
res = Model.default_get([field], context).get(field)
res = Model.default_get([field], req.context).get(field)
image_base64 = res
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()
image_base64 = res.get(field)
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]):
width = int(resize[0])
height = int(resize[1])
@@ -1510,14 +1223,13 @@ class Binary(openerpweb.Controller):
:returns: :class:`werkzeug.wrappers.Response`
"""
Model = req.session.model(model)
context = req.session.eval_context(req.context)
fields = [field]
if filename_field:
fields.append(filename_field)
if id:
res = Model.read([int(id)], fields, context)[0]
res = Model.read([int(id)], fields, req.context)[0]
else:
res = Model.default_get(fields, context)
res = Model.default_get(fields, req.context)
filecontent = base64.b64decode(res.get(field, ''))
if not filecontent:
return req.not_found()
@@ -1534,22 +1246,24 @@ class Binary(openerpweb.Controller):
jdata = simplejson.loads(data)
model = jdata['model']
field = jdata['field']
data = jdata['data']
id = jdata.get('id', 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)
fields = [field]
if filename_field:
fields.append(filename_field)
if id:
if data:
res = { field: data }
elif id:
res = Model.read([int(id)], fields, context)[0]
else:
res = Model.default_get(fields, context)
filecontent = base64.b64decode(res.get(field, ''))
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))
else:
filename = '%s_%s' % (model.replace('.', '_'), id)
@@ -1577,7 +1291,6 @@ class Binary(openerpweb.Controller):
@openerpweb.httprequest
def upload_attachment(self, req, callback, model, id, ufile):
context = req.session.eval_context(req.context)
Model = req.session.model('ir.attachment')
try:
out = """<script language="javascript" type="text/javascript">
@@ -1590,13 +1303,13 @@ class Binary(openerpweb.Controller):
'datas_fname': ufile.filename,
'res_model': model,
'res_id': int(id)
}, context)
}, req.context)
args = {
'filename': ufile.filename,
'id': attachment_id
}
except Exception, e:
args = { 'error': e.message }
except Exception,e:
args = {'error':e.faultCode }
return out % (simplejson.dumps(callback), simplejson.dumps(args))
class Action(openerpweb.Controller):
@@ -1606,8 +1319,6 @@ class Action(openerpweb.Controller):
def load(self, req, action_id, do_not_eval=False):
Actions = req.session.model('ir.actions.actions')
value = False
context = req.session.eval_context(req.context)
try:
action_id = int(action_id)
except ValueError:
@@ -1618,22 +1329,22 @@ class Action(openerpweb.Controller):
except Exception:
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:
ctx = {}
action_type = base_action[0]['type']
if action_type == 'ir.actions.report.xml':
ctx.update({'bin_size': True})
ctx.update(context)
ctx.update(req.context)
action = req.session.model(action_type).read([action_id], False, ctx)
if action:
value = clean_action(req, action[0], do_not_eval)
value = clean_action(req, action[0])
return value
@openerpweb.jsonrequest
def run(self, req, action_id):
return_action = req.session.model('ir.actions.server').run(
[action_id], req.session.eval_context(req.context))
[action_id], req.context)
if return_action:
return clean_action(req, return_action)
else:
@@ -1658,7 +1369,7 @@ class Export(View):
def fields_get(self, req, 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
@openerpweb.jsonrequest
@@ -1728,7 +1439,9 @@ class Export(View):
def fields_info(self, req, model, export_fields):
info = {}
fields = self.fields_get(req, model)
if ".id" in export_fields:
fields['.id'] = fields.pop('id', {'string': 'ID'})
# To make fields retrieval more efficient, fetch all sub-fields of a
# given field at the same time. Because the order in the export list is
# arbitrary, this requires ordering all sub-fields of a given field
@@ -1808,12 +1521,11 @@ class Export(View):
'import_compat')(
simplejson.loads(data))
context = req.session.eval_context(req.context)
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)
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:
columns_headers = field_names
@@ -1918,9 +1630,8 @@ class Reports(View):
action = simplejson.loads(action)
report_srv = req.session.proxy("report")
context = req.session.eval_context(
nonliterals.CompoundContext(
req.context or {}, action[ "context"]))
context = dict(req.context)
context.update(action["context"])
report_data = {}
report_ids = context["active_ids"]
+162
View File
@@ -0,0 +1,162 @@
# coding=utf-8
# -*- encoding: utf-8 -*-
import glob
import itertools
import json
import operator
import os
from mako.template import Template
from openerp.modules import module
from .main import module_topological_sort
from .. import http
NOMODULE_TEMPLATE = Template(u"""<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>OpenERP Testing</title>
</head>
<body>
<form action="/web/tests" method="GET">
<button name="mod" value="*">Run all tests</button>
<ul>
% for name, module in modules:
<li>${name} <button name="mod" value="${module}">
Run Tests</button></li>
% endfor
</ul>
</form>
</body>
</html>
""")
NOTFOUND = Template(u"""
<p>Unable to find the module [${module}], please check that the module
name is correct and the module is on OpenERP's path.</p>
<a href="/web/tests">&lt;&lt; Back to tests</a>
""")
TESTING = Template(u"""<!DOCTYPE html>
<html style="height: 100%">
<%def name="to_path(module, p)">/${module}/${p}</%def>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>OpenERP Web Tests</title>
<link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
<link rel="stylesheet" href="/web/static/lib/qunit/qunit.css">
<script src="/web/static/lib/qunit/qunit.js"></script>
<script type="text/javascript">
var oe_db_info = ${db_info};
// List of modules, each module is preceded by its dependencies
var oe_all_dependencies = ${dependencies};
QUnit.config.testTimeout = 5 * 60 * 1000;
</script>
</head>
<body id="oe" class="openerp">
<div id="qunit"></div>
<div id="qunit-fixture"></div>
</body>
% for module, jss, tests, templates in files:
% for js in jss:
<script src="${to_path(module, js)}"></script>
% endfor
% if tests or templates:
<script>
openerp.testing.current_module = "${module}";
% for template in templates:
openerp.testing.add_template("${to_path(module, template)}");
% endfor
</script>
% endif
% if tests:
% for test in tests:
<script type="text/javascript" src="${to_path(module, test)}"></script>
% endfor
% endif
% endfor
</html>
""")
class TestRunnerController(http.Controller):
_cp_path = '/web/tests'
@http.httprequest
def index(self, req, mod=None, **kwargs):
ms = module.get_modules()
manifests = dict(
(name, desc)
for name, desc in zip(ms, map(self.load_manifest, ms))
if desc # remove not-actually-openerp-modules
)
if not mod:
return NOMODULE_TEMPLATE.render(modules=(
(manifest['name'], name)
for name, manifest in manifests.iteritems()
if any(testfile.endswith('.js')
for testfile in manifest['test'])
))
sorted_mods = module_topological_sort(dict(
(name, manifest.get('depends', []))
for name, manifest in manifests.iteritems()
))
# to_load and to_test should be zippable lists of the same length.
# A falsy value in to_test indicate nothing to test at that index (just
# load the corresponding part of to_load)
to_test = sorted_mods
if mod != '*':
if mod not in manifests:
return req.not_found(NOTFOUND.render(module=mod))
idx = sorted_mods.index(mod)
to_test = [None] * len(sorted_mods)
to_test[idx] = mod
tests_candicates = [
filter(lambda path: path.endswith('.js'),
manifests[mod]['test'] if mod else [])
for mod in to_test]
# remove trailing test-less modules
tests = reversed(list(
itertools.dropwhile(
operator.not_,
reversed(tests_candicates))))
files = [
(mod, manifests[mod]['js'], tests, manifests[mod]['qweb'])
for mod, tests in itertools.izip(sorted_mods, tests)
]
# if all three db_info parameters are present, send them to the page
db_info = dict((k, v) for k, v in kwargs.iteritems()
if k in ['source', 'supadmin', 'password'])
if len(db_info) != 3:
db_info = None
return TESTING.render(files=files, dependencies=json.dumps(
[name for name in sorted_mods
if module.get_module_resource(name, 'static')
if manifests[name]['js']]), db_info=json.dumps(db_info))
def load_manifest(self, name):
manifest = module.load_information_from_description_file(name)
if manifest:
path = module.get_module_path(name)
manifest['js'] = list(
self.expand_patterns(path, manifest.get('js', [])))
manifest['test'] = list(
self.expand_patterns(path, manifest.get('test', [])))
manifest['qweb'] = list(
self.expand_patterns(path, manifest.get('qweb', [])))
return manifest
def expand_patterns(self, root, patterns):
for pattern in patterns:
normalized_pattern = os.path.normpath(os.path.join(root, pattern))
for path in glob.glob(normalized_pattern):
# replace OS path separators (from join & normpath) by URI ones
yield path[len(root):].replace(os.path.sep, '/')
+97
View File
@@ -240,6 +240,103 @@ for instance (to take advantage of :js:func:`when` 's special
treatment of single-value promises).
jQuery.Deferred API
~~~~~~~~~~~~~~~~~~~
.. js:function:: when(deferreds…)
:param deferreds: deferred objects to multiplex
:returns: a multiplexed deferred
:rtype: :js:class:`Deferred`
.. js:class:: Deferred
.. js:function:: Deferred.then(doneCallback[, failCallback])
Attaches new callbacks to the resolution or rejection of the
deferred object. Callbacks are executed in the order they are
attached to the deferred.
To provide only a failure callback, pass ``null`` as the
``doneCallback``, to provide only a success callback the
second argument can just be ignored (and not passed at all).
Returns a new deferred which resolves to the result of the
corresponding callback, if a callback returns a deferred
itself that new deferred will be used as the resolution of the
chain.
:param doneCallback: function called when the deferred is resolved
:type doneCallback: Function
:param failCallback: function called when the deferred is rejected
:type failCallback: Function
:returns: the deferred object on which it was called
:rtype: :js:class:`Deferred`
.. js:function:: Deferred.done(doneCallback)
Attaches a new success callback to the deferred, shortcut for
``deferred.then(doneCallback)``.
This is a jQuery extension to `CommonJS Promises/A`_ providing
little value over calling :js:func:`~Deferred.then` directly,
it should be avoided.
:param doneCallback: function called when the deferred is resolved
:type doneCallback: Function
:returns: the deferred object on which it was called
:rtype: :js:class:`Deferred`
.. js:function:: Deferred.fail(failCallback)
Attaches a new failure callback to the deferred, shortcut for
``deferred.then(null, failCallback)``.
A second jQuery extension to `Promises/A <CommonJS
Promises/A>`_. Although it provides more value than
:js:func:`~Deferred.done`, it still is not much and should be
avoided as well.
:param failCallback: function called when the deferred is rejected
:type failCallback: Function
:returns: the deferred object on which it was called
:rtype: :js:class:`Deferred`
.. js:function:: Deferred.promise()
Returns a read-only view of the deferred object, with all
mutators (resolve and reject) methods removed.
.. js:function:: Deferred.resolve(value…)
Called to resolve a deferred, any value provided will be
passed onto the success handlers of the deferred object.
Resolving a deferred which has already been resolved or
rejected has no effect.
.. js:function:: Deferred.reject(value…)
Called to reject (fail) a deferred, any value provided will be
passed onto the failure handler of the deferred object.
Rejecting a deferred which has already been resolved or
rejected has no effect.
.. [#] or simply calling :js:class:`Deferred` as a function, the
result is the same
.. [#] or not-promises, the `CommonJS Promises/B`_ role of
:js:func:`when` is to be able to treat values and promises
uniformly: :js:func:`when` will pass promises through directly,
but non-promise values and objects will be transformed into a
resolved promise (resolving themselves with the value itself).
jQuery's :js:func:`when` keeps this behavior making deferreds
easy to build from "static" values, or allowing defensive code
where expected promises are wrapped in :js:func:`when` just in
case.
.. _promises: http://en.wikipedia.org/wiki/Promise_(programming)
.. _jQuery's deferred: http://api.jquery.com/category/deferred-object/
.. _CommonJS Promises/A: http://wiki.commonjs.org/wiki/Promises/A
+12 -11
View File
@@ -32,11 +32,15 @@ information:
The major difference is in the lifecycle of these:
* if the client action maps to a function, the function will simply be
called when executing the action. The function can have no further
* if the client action maps to a function, the function will be called
when executing the action. The function can have no further
interaction with the Web Client itself, although it can return an
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
:js:class:`~openerp.web.Widget`, that
: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
// action tag 'board.home.widgets'
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({
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,
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
second positional parameter to the constructor::
The second parameter to the constructor is the descriptor for the
action itself, which contains any parameter provided::
init: function (parent, params) {
init: function (parent, action) {
// execute the Widget's init
this._super(parent);
// board.home.widgets only takes a single param, the identifier of the
// 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, ...)
@@ -82,9 +86,6 @@ method.
code it should return a ``$.Deferred`` so callers know when it's
ready for interaction.
Although generally speaking client actions are not really
interacted with.
.. code-block:: javascript
start: function () {
@@ -93,7 +94,7 @@ method.
// Simply read the res.widget object this action should display
new instance.web.Model('res.widget').call(
'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
+12 -11
View File
@@ -16,10 +16,9 @@ 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('.'))
sys.path.append(os.path.abspath('_themes'))
sys.path.append(os.path.abspath('..'))
sys.path.append(os.path.abspath('../openerp'))
sys.path.insert(0, os.path.abspath('../addons'))
sys.path.insert(0, os.path.abspath('..'))
# -- General configuration -----------------------------------------------------
@@ -43,7 +42,7 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
project = u'OpenERP Server Developers Documentation'
project = u'OpenERP Web Developers Documentation'
copyright = u'2012, OpenERP s.a.'
# The version info for the project you're documenting, acts as replacement for
@@ -53,7 +52,7 @@ copyright = u'2012, OpenERP s.a.'
# The short X.Y version.
version = '7.0'
# The full version, including alpha/beta/rc tags.
release = '7.0b'
release = '7.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -171,7 +170,7 @@ html_sidebars = {
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'openerp-server-doc'
htmlhelp_basename = 'openerp-web-doc'
# -- Options for LaTeX output --------------------------------------------------
@@ -190,7 +189,7 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'openerp-server-doc.tex', u'OpenERP Server Developers Documentation',
('index', 'openerp-web-doc.tex', u'OpenERP Web Developers Documentation',
u'OpenERP s.a.', 'manual'),
]
@@ -220,7 +219,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'openerp-server-doc', u'OpenERP Server Developers Documentation',
('index', 'openerp-web-doc', u'OpenERP Web Developers Documentation',
[u'OpenERP s.a.'], 1)
]
@@ -234,8 +233,8 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'OpenERPServerDocumentation', u'OpenERP Server Developers Documentation',
u'OpenERP s.a.', 'OpenERPServerDocumentation', 'Developers documentation for the openobject-server project.',
('index', 'OpenERPWebDocumentation', u'OpenERP Web Developers Documentation',
u'OpenERP s.a.', 'OpenERPWebDocumentation', 'Developers documentation for the openerp-web project.',
'Miscellaneous'),
]
@@ -248,10 +247,12 @@ texinfo_documents = [
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
todo_include_todos = True
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
'python': ('http://docs.python.org/', None),
'openerpweb': ('http://doc.openerp.com/trunk/developers/web', None),
'openerpserver': ('http://doc.openerp.com/trunk/developers/server', None),
'openerpdev': ('http://doc.openerp.com/trunk/developers', None),
'openerpcommand': ('http://doc.openerp.com/trunk/developers/command', None),
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+8 -6
View File
@@ -11,20 +11,22 @@ Contents:
.. toctree::
:maxdepth: 1
presentation
async
module
widget
qweb
async
rpc
qweb
client_action
form_view
testing
search_view
list_view
form_view
changelog-7.0
Indices and tables
==================
+281
View File
@@ -0,0 +1,281 @@
Building an OpenERP Web module
==============================
There is no significant distinction between an OpenERP Web module and
an OpenERP module, the web part is mostly additional data and code
inside a regular OpenERP module. This allows providing more seamless
features by integrating your module deeper into the web client.
A Basic Module
--------------
A very basic OpenERP module structure will be our starting point:
.. code-block:: text
web_example
├── __init__.py
└── __openerp__.py
.. literalinclude:: module/__openerp__.py
:language: python
This is a sufficient minimal declaration of a valid OpenERP module.
Web Declaration
---------------
There is no such thing as a "web module" declaration. An OpenERP
module is automatically recognized as "web-enabled" if it contains a
``static`` directory at its root, so:
.. code-block:: text
web_example
├── __init__.py
├── __openerp__.py
└── static
is the extent of it. You should also change the dependency to list
``web``:
.. literalinclude:: module/__openerp__.py.1.diff
:language: diff
.. note::
This does not matter in normal operation so you may not realize
it's wrong (the web module does the loading of everything else, so
it can only be loaded), but when e.g. testing the loading process
is slightly different than normal, and incorrect dependency may
lead to broken code.
This makes the "web" discovery system consider the module as having a
"web part", and check if it has web controllers to mount or javascript
files to load. The content of the ``static/`` folder is also
automatically made available to web browser at the URL
``$module-name/static/$file-path``. This is sufficient to provide
pictures (of cats, usually) through your module. However there are
still a few more steps to running javascript code.
Getting Things Done
-------------------
The first one is to add javascript code. It's customary to put it in
``static/src/js``, to have room for e.g. other file types, or
third-party libraries.
.. literalinclude:: module/static/src/js/first_module.js
:language: javascript
The client won't load any file unless specified, thus the new file
should be listed in the module's manifest file, under a new key ``js``
(a list of file names, or glob patterns):
.. literalinclude:: module/__openerp__.py.2.diff
:language: diff
At this point, if the module is installed and the client reloaded the
message should appear in your browser's development console.
.. note::
Because the manifest file has been edited, you will have to
restart the OpenERP server itself for it to be taken in account.
You may also want to open your browser's console *before*
reloading, depending on the browser messages printed while the
console is closed may not work or may not appear after opening it.
.. note::
If the message does not appear, try cleaning your browser's caches
and ensure the file is correctly loaded from the server logs or
the "resources" tab of your browser's developers tools.
At this point the code runs, but it runs only once when the module is
initialized, and it can't get access to the various APIs of the web
client (such as making RPC requests to the server). This is done by
providing a `javascript module`_:
.. literalinclude:: module/static/src/js/first_module.js.1.diff
:language: diff
If you reload the client, you'll see a message in the console exactly
as previously. The differences, though invisible at this point, are:
* All javascript files specified in the manifest (only this one so
far) have been fully loaded
* An instance of the web client and a namespace inside that instance
(with the same name as the module) have been created and are
available for use
The latter point is what the ``instance`` parameter to the function
provides: an instance of the OpenERP Web client, with the contents of
all the new module's dependencies loaded in and initialized. These are
the entry points to the web client's APIs.
To demonstrate, let's build a simple :doc:`client action
<client_action>`: a stopwatch
First, the action declaration:
.. literalinclude:: module/__openerp__.py.3.diff
:language: diff
.. literalinclude:: module/web_example.xml
:language: xml
then set up the :doc:`client action hook <client_action>` to register
a function (for now):
.. literalinclude:: module/static/src/js/first_module.js.2.diff
:language: diff
Updating the module (in order to load the XML description) and
re-starting the server should display a new menu *Example Client
Action* at the top-level. Opening said menu will make the message
appear, as usual, in the browser's console.
Paint it black
--------------
The next step is to take control of the page itself, rather than just
print little messages in the console. This we can do by replacing our
client action function by a :doc:`widget`. Our widget will simply use
its :js:func:`~openerp.web.Widget.start` to add some content to its
DOM:
.. literalinclude:: module/static/src/js/first_module.js.3.diff
:language: diff
after reloading the client (to update the javascript file), instead of
printing to the console the menu item clears the whole screen and
displays the specified message in the page.
Since we've added a class on the widget's :ref:`DOM root
<widget-dom_root>` we can now see how to add a stylesheet to a module:
first create the stylesheet file:
.. literalinclude:: module/static/src/css/web_example.css
:language: css
then add a reference to the stylesheet in the module's manifest (which
will require restarting the OpenERP Server to see the changes, as
usual):
.. literalinclude:: module/__openerp__.py.4.diff
:language: diff
the text displayed by the menu item should now be huge, and
white-on-black (instead of small and black-on-white). From there on,
the world's your canvas.
.. note::
Prefixing CSS rules with both ``.openerp`` (to ensure the rule
will apply only within the confines of the OpenERP Web client) and
a class at the root of your own hierarchy of widgets is strongly
recommended to avoid "leaking" styles in case the code is running
embedded in an other web page, and does not have the whole screen
to itself.
So far we haven't built much (any, really) DOM content. It could all
be done in :js:func:`~openerp.web.Widget.start` but that gets unwieldy
and hard to maintain fast. It is also very difficult to extend by
third parties (trying to add or change things in your widgets) unless
broken up into multiple methods which each perform a little bit of the
rendering.
The first way to handle this method is to delegate the content to
plenty of sub-widgets, which can be individually overridden. An other
method [#DOM-building]_ is to use `a template
<http://en.wikipedia.org/wiki/Web_template>`_ to render a widget's
DOM.
OpenERP Web's template language is :doc:`qweb`. Although any
templating engine can be used (e.g. `mustache
<http://mustache.github.com/>`_ or `_.template
<http://underscorejs.org/#template>`_) QWeb has important features
which other template engines may not provide, and has special
integration to OpenERP Web widgets.
Adding a template file is similar to adding a style sheet:
.. literalinclude:: module/static/src/xml/web_example.xml
:language: xml
.. literalinclude:: module/__openerp__.py.5.diff
:language: diff
The template can then easily be hooked in the widget:
.. literalinclude:: module/static/src/js/first_module.js.4.diff
:language: diff
And finally the CSS can be altered to style the new (and more complex)
template-generated DOM, rather than the code-generated one:
.. literalinclude:: module/static/src/css/web_example.css.1.diff
:language: diff
.. note::
The last section of the CSS change is an example of "state
classes": a CSS class (or set of classes) on the root of the
widget, which is toggled when the state of the widget changes and
can perform drastic alterations in rendering (usually
showing/hiding various elements).
This pattern is both fairly simple (to read and understand) and
efficient (because most of the hard work is pushed to the
browser's CSS engine, which is usually highly optimized, and done
in a single repaint after toggling the class).
The last step (until the next one) is to add some behavior and make
our stopwatch watch. First hook some events on the buttons to toggle
the widget's state:
.. literalinclude:: module/static/src/js/first_module.js.5.diff
:language: diff
This demonstrates the use of the "events hash" and event delegation to
declaratively handle events on the widget's DOM. And already changes
the button displayed in the UI. Then comes some actual logic:
.. literalinclude:: module/static/src/js/first_module.js.6.diff
:language: diff
* An initializer (the ``init`` method) is introduced to set-up a few
internal variables: ``_start`` will hold the start of the timer (as
a javascript Date object), and ``_watch`` will hold a ticker to
update the interface regularly and display the "current time".
* ``update_counter`` is in charge of taking the time difference
between "now" and ``_start``, formatting as ``HH:MM:SS`` and
displaying the result on screen.
* ``watch_start`` is augmented to initialize ``_start`` with its value
and set-up the update of the counter display every 33ms.
* ``watch_stop`` disables the updater, does a final update of the
counter display and resets everything.
* Finally, because javascript Interval and Timeout objects execute
"outside" the widget, they will keep going even after the widget has
been destroyed (especially an issue with intervals as they repeat
indefinitely). So ``_watch`` *must* be cleared when the widget is
destroyed (then the ``_super`` must be called as well in order to
perform the "normal" widget cleanup).
Starting and stopping the watch now works, and correctly tracks time
since having started the watch, neatly formatted.
.. [#DOM-building] they are not alternative solutions: they work very
well together. Templates are used to build "just
DOM", sub-widgets are used to build DOM subsections
*and* delegate part of the behavior (e.g. events
handling).
.. _javascript module:
http://addyosmani.com/resources/essentialjsdesignpatterns/book/#modulepatternjavascript
View File
+7
View File
@@ -0,0 +1,7 @@
# __openerp__.py
{
'name': "Web Example",
'description': "Basic example of a (future) web module",
'category': 'Hidden',
'depends': ['base'],
}
@@ -0,0 +1,11 @@
--- web_example/__openerp__.py
+++ web_example/__openerp__.py
@@ -1,7 +1,7 @@
# __openerp__.py
{
'name': "Web Example",
'description': "Basic example of a (future) web module",
'category': 'Hidden',
- 'depends': ['base'],
+ 'depends': ['web'],
}
@@ -0,0 +1,11 @@
--- web_example/__openerp__.py
+++ web_example/__openerp__.py
@@ -1,7 +1,8 @@
# __openerp__.py
{
'name': "Web Example",
'description': "Basic example of a (future) web module",
'category': 'Hidden',
'depends': ['web'],
+ 'js': ['static/src/js/first_module.js'],
}
@@ -0,0 +1,12 @@
--- web_example/__openerp__.py
+++ web_example/__openerp__.py
@@ -1,8 +1,9 @@
# __openerp__.py
{
'name': "Web Example",
'description': "Basic example of a (future) web module",
'category': 'Hidden',
'depends': ['web'],
+ 'data': ['web_example.xml'],
'js': ['static/src/js/first_module.js'],
}
@@ -0,0 +1,13 @@
--- web_example/__openerp__.py
+++ web_example/__openerp__.py
@@ -1,9 +1,10 @@
# __openerp__.py
{
'name': "Web Example",
'description': "Basic example of a (future) web module",
'category': 'Hidden',
'depends': ['web'],
'data': ['web_example.xml'],
'js': ['static/src/js/first_module.js'],
+ 'css': ['static/src/css/web_example.css'],
}
@@ -0,0 +1,14 @@
--- web_example/__openerp__.py
+++ web_example/__openerp__.py
@@ -1,10 +1,11 @@
# __openerp__.py
{
'name': "Web Example",
'description': "Basic example of a (future) web module",
'category': 'Hidden',
'depends': ['web'],
'data': ['web_example.xml'],
'js': ['static/src/js/first_module.js'],
'css': ['static/src/css/web_example.css'],
+ 'qweb': ['static/src/xml/web_example.xml'],
}
@@ -0,0 +1,6 @@
.openerp .oe_web_example {
color: white;
background-color: black;
height: 100%;
font-size: 400%;
}
@@ -0,0 +1,17 @@
--- web_example/static/src/css/web_example.css
+++ web_example/static/src/css/web_example.css
@@ -1,6 +1,13 @@
.openerp .oe_web_example {
color: white;
background-color: black;
height: 100%;
- font-size: 400%;
}
+.openerp .oe_web_example h4 {
+ margin: 0;
+ font-size: 200%;
+}
+.openerp .oe_web_example.oe_web_example_started .oe_web_example_start button,
+.openerp .oe_web_example.oe_web_example_stopped .oe_web_example_stop button {
+ display: none
+}
@@ -0,0 +1,2 @@
// static/src/js/first_module.js
console.log("Debug statement: file loaded");
@@ -0,0 +1,8 @@
--- web_example/static/src/js/first_module.js
+++ web_example/static/src/js/first_module.js
@@ -1,2 +1,4 @@
// static/src/js/first_module.js
-console.log("Debug statement: file loaded");
+openerp.web_example = function (instance) {
+ console.log("Module loaded");
+};
@@ -0,0 +1,11 @@
--- web_example/static/src/js/first_module.js
+++ web_example/static/src/js/first_module.js
@@ -1,4 +1,7 @@
// static/src/js/first_module.js
openerp.web_example = function (instance) {
- console.log("Module loaded");
+ instance.web.client_actions.add('example.action', 'instance.web_example.action');
+ instance.web_example.action = function (parent, action) {
+ console.log("Executed the action", action);
+ };
};
@@ -0,0 +1,18 @@
--- web_example/static/src/js/first_module.js
+++ web_example/static/src/js/first_module.js
@@ -1,7 +1,11 @@
// static/src/js/first_module.js
openerp.web_example = function (instance) {
- instance.web.client_actions.add('example.action', 'instance.web_example.action');
- instance.web_example.action = function (parent, action) {
- console.log("Executed the action", action);
- };
+ instance.web.client_actions.add('example.action', 'instance.web_example.Action');
+ instance.web_example.Action = instance.web.Widget.extend({
+ className: 'oe_web_example',
+ start: function () {
+ this.$el.text("Hello, world!");
+ return this._super();
+ }
+ });
};
@@ -0,0 +1,15 @@
--- web_example/static/src/js/first_module.js
+++ web_example/static/src/js/first_module.js
@@ -1,11 +1,7 @@
// static/src/js/first_module.js
openerp.web_example = function (instance) {
instance.web.client_actions.add('example.action', 'instance.web_example.Action');
instance.web_example.Action = instance.web.Widget.extend({
+ template: 'web_example.action'
- className: 'oe_web_example',
- start: function () {
- this.$el.text("Hello, world!");
- return this._super();
- }
});
};
@@ -0,0 +1,23 @@
--- web_example/static/src/js/first_module.js
+++ web_example/static/src/js/first_module.js
@@ -1,7 +1,19 @@
// static/src/js/first_module.js
openerp.web_example = function (instance) {
instance.web.client_actions.add('example.action', 'instance.web_example.Action');
instance.web_example.Action = instance.web.Widget.extend({
- template: 'web_example.action'
+ template: 'web_example.action',
+ events: {
+ 'click .oe_web_example_start button': 'watch_start',
+ 'click .oe_web_example_stop button': 'watch_stop'
+ },
+ watch_start: function () {
+ this.$el.addClass('oe_web_example_started')
+ .removeClass('oe_web_example_stopped');
+ },
+ watch_stop: function () {
+ this.$el.removeClass('oe_web_example_started')
+ .addClass('oe_web_example_stopped');
+ },
});
};
@@ -0,0 +1,55 @@
--- web_example/static/src/js/first_module.js
+++ web_example/static/src/js/first_module.js
@@ -1,19 +1,52 @@
// static/src/js/first_module.js
openerp.web_example = function (instance) {
instance.web.client_actions.add('example.action', 'instance.web_example.Action');
instance.web_example.Action = instance.web.Widget.extend({
template: 'web_example.action',
events: {
'click .oe_web_example_start button': 'watch_start',
'click .oe_web_example_stop button': 'watch_stop'
},
+ init: function () {
+ this._super.apply(this, arguments);
+ this._start = null;
+ this._watch = null;
+ },
+ update_counter: function () {
+ var h, m, s;
+ // Subtracting javascript dates returns the difference in milliseconds
+ var diff = new Date() - this._start;
+ s = diff / 1000;
+ m = Math.floor(s / 60);
+ s -= 60*m;
+ h = Math.floor(m / 60);
+ m -= 60*h;
+ this.$('.oe_web_example_timer').text(
+ _.str.sprintf("%02d:%02d:%02d", h, m, s));
+ },
watch_start: function () {
this.$el.addClass('oe_web_example_started')
.removeClass('oe_web_example_stopped');
+ this._start = new Date();
+ // Update the UI to the current time
+ this.update_counter();
+ // Update the counter at 30 FPS (33ms/frame)
+ this._watch = setInterval(
+ this.proxy('update_counter'),
+ 33);
},
watch_stop: function () {
+ clearInterval(this._watch);
+ this.update_counter();
+ this._start = this._watch = null;
this.$el.removeClass('oe_web_example_started')
.addClass('oe_web_example_stopped');
},
+ destroy: function () {
+ if (this._watch) {
+ clearInterval(this._watch);
+ }
+ this._super();
+ }
});
};
@@ -0,0 +1,11 @@
<templates>
<div t-name="web_example.action" class="oe_web_example oe_web_example_stopped">
<h4 class="oe_web_example_timer">00:00:00</h4>
<p class="oe_web_example_start">
<button type="button">Start</button>
</p>
<p class="oe_web_example_stop">
<button type="button">Stop</button>
</p>
</div>
</templates>
+11
View File
@@ -0,0 +1,11 @@
<!-- web_example/web_example.xml -->
<openerp>
<data>
<record model="ir.actions.client" id="action_client_example">
<field name="name">Example Client Action</field>
<field name="tag">example.action</field>
</record>
<menuitem action="action_client_example"
id="menu_client_example"/>
</data>
</openerp>
+1 -1
View File
@@ -35,7 +35,7 @@ More recommendations related to the specific case of the OpenERP Web Client:
* Use QWeb templates for html rendering.
* All css classes should have the prefix *oe-* .
* All css classes should have the prefix *oe_* .
* Functions that call rpc() should return a deferred, even if it calls it indirectly. So a function that calls a function that calls a function that calls rpc() should return a deferred too.
+527 -60
View File
@@ -1,79 +1,546 @@
QWeb
====
QWeb is the template engine used by the OpenERP Web Client. It is a home made engine create by OpenERP developers. There are a few things to note about it:
QWeb is the template engine used by the OpenERP Web Client. It is an
XML-based templating language, similar to `Genshi
<http://en.wikipedia.org/wiki/Genshi_(templating_language)>`_,
`Thymeleaf <http://en.wikipedia.org/wiki/Thymeleaf>`_ or `Facelets
<http://en.wikipedia.org/wiki/Facelets>`_ with a few peculiarities:
* Template are rendered in javascript on the client-side, the server does nothing.
* It is an xml template engine, like Facelets_ for example. The source file must be a valid xml.
* Templates are not interpreted. There are compiled to javascript. This makes them a lot faster to render, but sometimes harder to debug.
* Most of the time it is used through the Widget class, but you can also use it directly using *openerp.web.qweb.render()* .
* It's implemented fully in javascript and rendered in the browser.
* Each template file (XML files) contains multiple templates, where
template engine usually have a 1:1 mapping between template files
and templates.
* It has special support in OpenERP Web's
:class:`~instance.web.Widget`, though it can be used outside of
OpenERP Web (and it's possible to use :class:`~instance.web.Widget`
without relying on the QWeb integration).
.. _Facelets: http://en.wikipedia.org/wiki/Facelets
The rationale behind using QWeb instead of a more popular template syntax is
that its extension mechanism is very similar to the openerp view inheritance
mechanism. Like openerp views a QWeb template is an xml tree and therefore
xpath or dom manipulations are easy to performs on it.
Here is a typical QWeb file:
Here's an example demonstrating most of the basic QWeb features:
::
.. code-block:: xml
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="Template1">
<div>...</div>
</t>
<t t-name="Template2">
<div>...</div>
</t>
<div t-name="example_template" t-attf-class="base #{cls}">
<h4 t-if="title"><t t-esc="title"/></h4>
<ul>
<li t-foreach="items" t-as="item" t-att-class="item_parity">
<t t-call="example_template.sub">
<t t-set="arg" t-value="item_value"/>
</t>
</li>
</ul>
</div>
<t t-name="example_template.sub">
<t t-esc="arg.name"/>
<dl>
<t t-foreach="arg.tags" t-as="tag" t-if="tag_index lt 5">
<dt><t t-esc="tag"/></dt>
<dd><t t-esc="tag_value"/></dd>
</t>
</dl>
</t>
</templates>
A QWeb file contains multiple templates, they are simply identified by a name.
rendered with the following context:
Here is a sample QWeb template:
.. code-block:: json
::
{
"class1": "foo",
"title": "Random Title",
"items": [
{ "name": "foo", "tags": {"bar": "baz", "qux": "quux"} },
{ "name": "Lorem", "tags": {
"ipsum": "dolor",
"sit": "amet",
"consectetur": "adipiscing",
"elit": "Sed",
"hendrerit": "ullamcorper",
"ante": "id",
"vestibulum": "Lorem",
"ipsum": "dolor",
"sit": "amet"
}
}
]
}
<t t-name="UserPage">
<div>
<p>Name: <t t-esc="widget.user_name"/></p>
<p>Password: <input type="text" t-att-value="widget.password"/></p>
<p t-if="widget.is_admin">This user is an Administrator</p>
<t t-foreach="widget.roles" t-as="role">
<p>User has role: <t t-esc="role"/></p>
</t>
</div>
</t>
will yield this section of HTML document (reformated for readability):
.. code-block:: html
*widget* is a variable given to the template engine by Widget sub-classes when they decide to render their associated template, it is simply *this*. Here is the corresponding Widget sub-class:
::
UserPageWidget = openerp.base.Widget.extend({
template: "UserPage",
init: function(parent) {
this._super(parent);
this.user_name = "Xavier";
this.password = "lilo";
this.is_admin = true;
this.roles = ["Web Developer", "IE Hater", "Steve Jobs Worshiper"];
},
});
It could output something like this:
::
<div>
<p>Name: Xavier</p>
<p>Password: <input type="text" value="lilo"/></p>
<p>This user is an Administrator</p
<p>User has role: Web Developer</p>
<p>User has role: IE Hater</p>
<p>User has role: Steve Jobs Worshiper</p>
<div class="base foo">
<h4>Random Title</h4>
<ul>
<li class="even">
foo
<dl>
<dt>bar</dt>
<dd>baz</dd>
<dt>qux</dt>
<dd>quux</dd>
</dl>
</li>
<li class="odd">
Lorem
<dl>
<dt>ipsum</dt>
<dd>dolor</dd>
<dt>sit</dt>
<dd>amet</dd>
<dt>consectetur</dt>
<dd>adipiscing</dd>
<dt>elit</dt>
<dd>Sed</dd>
<dt>hendrerit</dt>
<dd>ullamcorper</dd>
</dl>
</li>
</ul>
</div>
A QWeb template should always contain one unique root element to be used effectively with the Widget class, here it is a *<div>*. QWeb only react to *<t>* elements or attributes prefixed by *t-*. The *<t>* is simply a null element, it is only used when you need to use a *t-* attribute without outputting an html element at the same time. Here are the effects of the most common QWeb attributes:
API
---
* *t-esc* outputs the result of the evaluation of the given javascript expression
* *t-att-ATTR* sets the value of the *ATTR* attribute to the result of the evaluation of the given javascript expression
* *t-if* outputs the element and its content only if the given javascript expression returns true
* *t-foreach* outputs as many times as contained in the list returned by the given javascript expression. For each iteration, a variable with the name defined by *t-as* contains the current element in the list.
While QWeb implements a number of attributes and methods for
customization and configuration, only two things are really important
to the user:
.. js:class:: QWeb2.Engine
The QWeb "renderer", handles most of QWeb's logic (loading,
parsing, compiling and rendering templates).
OpenERP Web instantiates one for the user, and sets it to
``instance.web.qweb``. It also loads all the template files of the
various modules into that QWeb instance.
A :js:class:`QWeb2.Engine` also serves as a "template namespace".
.. js:function:: QWeb2.Engine.render(template[, context])
Renders a previously loaded template to a String, using
``context`` (if provided) to find the variables accessed
during template rendering (e.g. strings to display).
:param String template: the name of the template to render
:param Object context: the basic namespace to use for template
rendering
:returns: String
The engine exposes an other method which may be useful in some
cases (e.g. if you need a separate template namespace with, in
OpenERP Web, Kanban views get their own :js:class:`QWeb2.Engine`
instance so their templates don't collide with more general
"module" templates):
.. js:function:: QWeb2.Engine.add_template(templates)
Loads a template file (a collection of templates) in the QWeb
instance. The templates can be specified as:
An XML string
QWeb will attempt to parse it to an XML document then load
it.
A URL
QWeb will attempt to download the URL content, then load
the resulting XML string.
A ``Document`` or ``Node``
QWeb will traverse the first level of the document (the
child nodes of the provided root) and load any named
template or template override.
:type templates: String | Document | Node
A :js:class:`QWeb2.Engine` also exposes various attributes for
behavior customization:
.. js:attribute:: QWeb2.Engine.prefix
Prefix used to recognize :ref:`directives <qweb-directives>`
during parsing. A string. By default, ``t``.
.. js:attribute:: QWeb2.Engine.debug
Boolean flag putting the engine in "debug mode". Normally,
QWeb intercepts any error raised during template execution. In
debug mode, it leaves all exceptions go through without
intercepting them.
.. js:attribute:: QWeb2.Engine.jQuery
The jQuery instance used during :ref:`template inheritance
<qweb-directives-inheritance>` processing. Defaults to
``window.jQuery``.
.. js:attribute:: QWeb2.Engine.preprocess_node
A ``Function``. If present, called before compiling each DOM
node to template code. In OpenERP Web, this is used to
automatically translate text content and some attributes in
templates. Defaults to ``null``.
.. _qweb-directives:
Directives
----------
A basic QWeb template is nothing more than an XHTML document (as it
must be valid XML), which will be output as-is. But the rendering can
be customized with bits of logic called "directives". Directives are
attributes elements prefixed by :js:attr:`~QWeb2.Engine.prefix` (this
document will use the default prefix ``t``, as does OpenERP Web).
A directive will usually control or alter the output of the element it
is set on. If no suitable element is available, the prefix itself can
be used as a "no-operation" element solely for supporting directives
(or internal content, which will be rendered). This means:
.. code-block:: xml
<t>Something something</t>
will simply output the string "Something something" (the element
itself will be skipped and "unwrapped"):
.. code-block:: javascript
var e = new QWeb2.Engine();
e.add_template('<templates>\
<t t-name="test1"><t>Test 1</t></t>\
<t t-name="test2"><span>Test 2</span></t>\
</templates>');
e.render('test1'); // Test 1
e.render('test2'); // <span>Test 2</span>
.. note::
The conventions used in directive descriptions are the following:
* directives are described as compound functions, potentially with
optional sections. Each section of the function name is an
attribute of the element bearing the directive.
* a special parameter is ``BODY``, which does not have a name and
designates the content of the element.
* special parameter types (aside from ``BODY`` which remains
untyped) are ``Name``, which designates a valid javascript
variable name, ``Expression`` which designates a valid
javascript expression, and ``Format`` which designates a
Ruby-style format string (a literal string with
``#{Expression}`` inclusions executed and replaced by their
result)
.. note::
``Expression`` actually supports a few extensions on the
javascript syntax: because some syntactic elements of javascript
are not compatible with XML and must be escaped, text
substitutions are performed from forms which don't need to be
escaped. Thus the following "keyword operators" are available in
an ``Expression``: ``and`` (maps to ``&&``), ``or`` (maps to
``||``), ``gt`` (maps to ``>``), ``gte`` (maps to ``>=``), ``lt``
(maps to ``<``) and ``lte`` (maps to ``<=``).
.. _qweb-directives-templates:
Defining Templates
++++++++++++++++++
.. _qweb-directive-name:
.. function:: t-name=name
:param String name: an arbitrary javascript string. Each template
name is unique in a given
:js:class:`QWeb2.Engine` instance, defining a
new template with an existing name will
overwrite the previous one without warning.
When multiple templates are related, it is
customary to use dotted names as a kind of
"namespace" e.g. ``foo`` and ``foo.bar`` which
will be used either by ``foo`` or by a
sub-widget of the widget used by ``foo``.
Templates can only be defined as the children of the document
root. The document root's name is irrelevant (it's not checked)
but is usually ``<templates>`` for simplicity.
.. code-block:: xml
<templates>
<t t-name="template1">
<!-- template code -->
</t>
</templates>
:ref:`t-name <qweb-directive-name>` can be used on an element with
an output as well:
.. code-block:: xml
<templates>
<div t-name="template2">
<!-- template code -->
</div>
</templates>
which ensures the template has a single root (if a template has
multiple roots and is then passed directly to jQuery, odd things
occur).
.. _qweb-directives-output:
Output
++++++
.. _qweb-directive-esc:
.. function:: t-esc=content
:param Expression content:
Evaluates, html-escapes and outputs ``content``.
.. _qweb-directive-escf:
.. function:: t-escf=content
:param Format content:
Similar to :ref:`t-esc <qweb-directive-esc>` but evaluates a
``Format`` instead of just an expression.
.. _qweb-directive-raw:
.. function:: t-raw=content
:param Expression content:
Similar to :ref:`t-esc <qweb-directive-esc>` but does *not*
html-escape the result of evaluating ``content``. Should only ever
be used for known-secure content, or will be an XSS attack vector.
.. _qweb-directive-rawf:
.. function:: t-rawf=content
:param Format content:
``Format``-based version of :ref:`t-raw <qweb-directive-raw>`.
.. _qweb-directive-att:
.. function:: t-att=map
:param Expression map:
Evaluates ``map`` expecting an ``Object`` result, sets each
key:value pair as an attribute (and its value) on the holder
element:
.. code-block:: xml
<span t-att="{foo: 3, bar: 42}"/>
will yield
.. code-block:: html
<span foo="3" bar="42"/>
.. function:: t-att-ATTNAME=value
:param Name ATTNAME:
:param Expression value:
Evaluates ``value`` and sets it on the attribute ``ATTNAME`` on
the holder element.
If ``value``'s result is ``undefined``, suppresses the creation of
the attribute.
.. _qweb-directive-attf:
.. function:: t-attf-ATTNAME=value
:param Name ATTNAME:
:param Format value:
Similar to :ref:`t-att-* <qweb-directive-att>` but the value of
the attribute is specified via a ``Format`` instead of an
expression. Useful for specifying e.g. classes mixing literal
classes and computed ones.
.. _qweb-directives-flow:
Flow Control
++++++++++++
.. _qweb-directive-set:
.. function:: t-set=name (t-value=value | BODY)
:param Name name:
:param Expression value:
:param BODY:
Creates a new binding in the template context. If ``value`` is
specified, evaluates it and sets it to the specified
``name``. Otherwise, processes ``BODY`` and uses that instead.
.. _qweb-directive-if:
.. function:: t-if=condition
:param Expression condition:
Evaluates ``condition``, suppresses the output of the holder
element and its content of the result is falsy.
.. _qweb-directive-foreach:
.. function:: t-foreach=iterable [t-as=name]
:param Expression iterable:
:param Name name:
Evaluates ``iterable``, iterates on it and evaluates the holder
element and its body once per iteration round.
If ``name`` is not specified, computes a ``name`` based on
``iterable`` (by replacing non-``Name`` characters by ``_``).
If ``iterable`` yields a ``Number``, treats it as a range from 0
to that number (excluded).
While iterating, :ref:`t-foreach <qweb-directive-foreach>` adds a
number of variables in the context:
``#{name}``
If iterating on an array (or a range), the current value in
the iteration. If iterating on an *object*, the current key.
``#{name}_all``
The collection being iterated (the array generated for a
``Number``)
``#{name}_value``
The current iteration value (current item for an array, value
for the current item for an object)
``#{name}_index``
The 0-based index of the current iteration round.
``#{name}_first``
Whether the current iteration round is the first one.
``#{name}_parity``
``"odd"`` if the current iteration round is odd, ``"even"``
otherwise. ``0`` is considered even.
.. _qweb-directive-call:
.. function:: t-call=template [BODY]
:param String template:
:param BODY:
Calls the specified ``template`` and returns its result. If
``BODY`` is specified, it is evaluated *before* calling
``template`` and can be used to specify e.g. parameters. This
usage is similar to `call-template with with-param in XSLT
<http://zvon.org/xxl/XSLTreference/OutputOverview/xslt_with-param_frame.html>`_.
.. _qweb-directives-inheritance:
Template Inheritance and Extension
++++++++++++++++++++++++++++++++++
.. _qweb-directive-extend:
.. function:: t-extend=template BODY
:param String template: name of the template to extend
Works similarly to OpenERP models: if used on its own, will alter
the specified template in-place; if used in conjunction with
:ref:`t-name <qweb-directive-name>` will create a new template
using the old one as a base.
``BODY`` should be a sequence of :ref:`t-jquery
<qweb-directive-jquery>` alteration directives.
.. note::
The inheritance in the second form is *static*: the parent
template is copied and transformed when :ref:`t-extend
<qweb-directive-extend>` is called. If it is altered later (by
a :ref:`t-extend <qweb-directive-extend>` without a
:ref:`t-name <qweb-directive-name>`), these changes will *not*
appear in the "child" templates.
.. _qweb-directive-jquery:
.. function:: t-jquery=selector [t-operation=operation] BODY
:param String selector: a CSS selector into the parent template
:param operation: one of ``append``, ``prepend``, ``before``,
``after``, ``inner`` or ``replace``.
:param BODY: ``operation`` argument, or alterations to perform
* If ``operation`` is specified, applies the selector to the
parent template to find a *context node*, then applies
``operation`` (as a jQuery operation) to the *context node*,
passing ``BODY`` as parameter.
.. note::
``replace`` maps to jQuery's `replaceWith(newContent)
<http://api.jquery.com/replaceWith/>`_, ``inner`` maps to
`html(htmlString) <http://api.jquery.com/html/>`_.
* If ``operation`` is not provided, ``BODY`` is evaluated as
javascript code, with the *context node* as ``this``.
.. warning::
While this second form is much more powerful than the first,
it is also much harder to read and maintain and should be
avoided. It is usually possible to either avoid it or
replace it with a sequence of ``t-jquery:t-operation:``.
Escape Hatches / debugging
++++++++++++++++++++++++++
.. _qweb-directive-log:
.. function:: t-log=expression
:param Expression expression:
Evaluates the provided expression (in the current template
context) and logs its result via ``console.log``.
.. _qweb-directive-debug:
.. function:: t-debug
Injects a debugger breakpoint (via the ``debugger;`` statement) in
the compiled template output.
.. _qweb-directive-js:
.. function:: t-js=context BODY
:param Name context:
:param BODY: javascript code
Injects the provided ``BODY`` javascript code into the compiled
template, passing it the current template context using the name
specified by ``context``.
+7 -6
View File
@@ -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
the return value of the Python methods, converted to JSON.
For instance, to call the ``eval_domain_and_context`` of the
:class:`~web.controllers.main.Session` controller:
For instance, to call the ``resequence`` of the
:class:`~web.controllers.main.DataSet` controller:
.. code-block:: javascript
openerp.connection.rpc('/web/session/eval_domain_and_context', {
domains: ds,
contexts: cs
openerp.connection.rpc('/web/dataset/resequence', {
model: some_model,
ids: array_of_ids,
offset: 42
}).then(function (result) {
// handle result
// resequenced on server
});
.. [#] with a small twist: SQLAlchemy's ``orm.query.Query.group_by``
+25
View File
@@ -0,0 +1,25 @@
test_empty_find (openerp.addons.web.tests.test_dataset.TestDataSetController) ... ok
test_ids_shortcut (openerp.addons.web.tests.test_dataset.TestDataSetController) ... ok
test_regular_find (openerp.addons.web.tests.test_dataset.TestDataSetController) ... ok
web.testing.stack: direct, value, success ... ok
web.testing.stack: direct, deferred, success ... ok
web.testing.stack: direct, value, error ... ok
web.testing.stack: direct, deferred, failure ... ok
web.testing.stack: successful setup ... ok
web.testing.stack: successful teardown ... ok
web.testing.stack: successful setup and teardown ... ok
[snip ~150 lines]
test_convert_complex_context (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
test_convert_complex_domain (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
test_convert_literal_context (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
test_convert_literal_domain (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
test_retrieve_nonliteral_context (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
test_retrieve_nonliteral_domain (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok
----------------------------------------------------------------------
Ran 181 tests in 15.706s
OK
+693
View File
@@ -0,0 +1,693 @@
.. highlight:: javascript
Testing in OpenERP Web
======================
Javascript Unit Testing
-----------------------
OpenERP Web 7.0 includes means to unit-test both the core code of
OpenERP Web and your own javascript modules. On the javascript side,
unit-testing is based on QUnit_ with a number of helpers and
extensions for better integration with OpenERP.
To see what the runner looks like, find (or start) an OpenERP server
with the web client enabled, and navigate to ``/web/tests`` e.g. `on
OpenERP's CI <http://trunk.runbot.openerp.com/web/tests>`_. This will
show the runner selector, which lists all modules with javascript unit
tests, and allows starting any of them (or all javascript tests in all
modules at once).
.. image:: ./images/runner.png
:align: center
Clicking any runner button will launch the corresponding tests in the
bundled QUnit_ runner:
.. image:: ./images/tests.png
:align: center
Writing a test case
-------------------
The first step is to list the test file(s). This is done through the
``test`` key of the openerp manifest, by adding javascript files to it
(next to the usual YAML files, if any):
.. code-block:: python
{
'name': "Demonstration of web/javascript tests",
'category': 'Hidden',
'depends': ['web'],
'test': ['static/test/demo.js'],
}
and to create the corresponding test file(s)
.. note::
Test files which do not exist will be ignored, if all test files
of a module are ignored (can not be found), the test runner will
consider that the module has no javascript tests.
After that, refreshing the runner selector will display the new module
and allow running all of its (0 so far) tests:
.. image:: ./images/runner2.png
:align: center
The next step is to create a test case::
openerp.testing.section('basic section', function (test) {
test('my first test', function () {
ok(false, "this test has run");
});
});
All testing helpers and structures live in the ``openerp.testing``
module. OpenERP tests live in a :js:func:`~openerp.testing.section`,
which is itself part of a module. The first argument to a section is
the name of the section, the second one is the section body.
:js:func:`test <openerp.testing.case>`, provided by the
:js:func:`~openerp.testing.section` to the callback, is used to
register a given test case which will be run whenever the test runner
actually does its job. OpenERP Web test case use standard `QUnit
assertions`_ within them.
Launching the test runner at this point will run the test and display
the corresponding assertion message, with red colors indicating the
test failed:
.. image:: ./images/tests2.png
:align: center
Fixing the test (by replacing ``false`` to ``true`` in the assertion)
will make it pass:
.. image:: ./images/tests3.png
:align: center
Assertions
----------
As noted above, OpenERP Web's tests use `qunit assertions`_. They are
available globally (so they can just be called without references to
anything). The following list is available:
.. js:function:: ok(state[, message])
checks that ``state`` is truthy (in the javascript sense)
.. js:function:: strictEqual(actual, expected[, message])
checks that the actual (produced by a method being tested) and
expected values are identical (roughly equivalent to ``ok(actual
=== expected, message)``)
.. js:function:: notStrictEqual(actual, expected[, message])
checks that the actual and expected values are *not* identical
(roughly equivalent to ``ok(actual !== expected, message)``)
.. js:function:: deepEqual(actual, expected[, message])
deep comparison between actual and expected: recurse into
containers (objects and arrays) to ensure that they have the same
keys/number of elements, and the values match.
.. js:function:: notDeepEqual(actual, expected[, message])
inverse operation to :js:func:`deepEqual`
.. js:function:: throws(block[, expected][, message])
checks that, when called, the ``block`` throws an
error. Optionally validates that error against ``expected``.
:param Function block:
:param expected: if a regexp, checks that the thrown error's
message matches the regular expression. If an
error type, checks that the thrown error is of
that type.
:type expected: Error | RegExp
.. js:function:: equal(actual, expected[, message])
checks that ``actual`` and ``expected`` are loosely equal, using
the ``==`` operator and its coercion rules.
.. js:function:: notEqual(actual, expected[, message])
inverse operation to :js:func:`equal`
Getting an OpenERP instance
---------------------------
The OpenERP instance is the base through which most OpenERP Web
modules behaviors (functions, objects, …) are accessed. As a result,
the test framework automatically builds one, and loads the module
being tested and all of its dependencies inside it. This new instance
is provided as the first positional parameter to your test
cases. Let's observe by adding javascript code (not test code) to the
test module:
.. code-block:: python
{
'name': "Demonstration of web/javascript tests",
'category': 'Hidden',
'depends': ['web'],
'js': ['static/src/js/demo.js'],
'test': ['static/test/demo.js'],
}
::
// src/js/demo.js
openerp.web_tests_demo = function (instance) {
instance.web_tests_demo = {
value_true: true,
SomeType: instance.web.Class.extend({
init: function (value) {
this.value = value;
}
})
};
};
and then adding a new test case, which simply checks that the
``instance`` contains all the expected stuff we created in the
module::
// test/demo.js
test('module content', function (instance) {
ok(instance.web_tests_demo.value_true, "should have a true value");
var type_instance = new instance.web_tests_demo.SomeType(42);
strictEqual(type_instance.value, 42, "should have provided value");
});
DOM Scratchpad
--------------
As in the wider client, arbitrarily accessing document content is
strongly discouraged during tests. But DOM access is still needed to
e.g. fully initialize :js:class:`widgets <~openerp.web.Widget>` before
testing them.
Thus, a test case gets a DOM scratchpad as its second positional
parameter, in a jQuery instance. That scratchpad is fully cleaned up
before each test, and as long as it doesn't do anything outside the
scratchpad your code can do whatever it wants::
// test/demo.js
test('DOM content', function (instance, $scratchpad) {
$scratchpad.html('<div><span class="foo bar">ok</span></div>');
ok($scratchpad.find('span').hasClass('foo'),
"should have provided class");
});
test('clean scratchpad', function (instance, $scratchpad) {
ok(!$scratchpad.children().length, "should have no content");
ok(!$scratchpad.text(), "should have no text");
});
.. note::
The top-level element of the scratchpad is not cleaned up, test
cases can add text or DOM children but shoud not alter
``$scratchpad`` itself.
Loading templates
-----------------
To avoid the corresponding processing costs, by default templates are
not loaded into QWeb. If you need to render e.g. widgets making use of
QWeb templates, you can request their loading through the
:js:attr:`~TestOptions.templates` option to the :js:func:`test case
function <openerp.testing.case>`.
This will automatically load all relevant templates in the instance's
qweb before running the test case:
.. code-block:: python
{
'name': "Demonstration of web/javascript tests",
'category': 'Hidden',
'depends': ['web'],
'js': ['static/src/js/demo.js'],
'test': ['static/test/demo.js'],
'qweb': ['static/src/xml/demo.xml'],
}
.. code-block:: xml
<!-- src/xml/demo.xml -->
<templates id="template" xml:space="preserve">
<t t-name="DemoTemplate">
<t t-foreach="5" t-as="value">
<p><t t-esc="value"/></p>
</t>
</t>
</templates>
::
// test/demo.js
test('templates', {templates: true}, function (instance) {
var s = instance.web.qweb.render('DemoTemplate');
var texts = $(s).find('p').map(function () {
return $(this).text();
}).get();
deepEqual(texts, ['0', '1', '2', '3', '4']);
});
Asynchronous cases
------------------
The test case examples so far are all synchronous, they execute from
the first to the last line and once the last line has executed the
test is done. But the web client is full of :doc:`asynchronous code
</async>`, and thus test cases need to be async-aware.
This is done by returning a :js:class:`deferred <Deferred>` from the
case callback::
// test/demo.js
test('asynchronous', {
asserts: 1
}, function () {
var d = $.Deferred();
setTimeout(function () {
ok(true);
d.resolve();
}, 100);
return d;
});
This example also uses the :js:class:`options parameter <TestOptions>`
to specify the number of assertions the case should expect, if less or
more assertions are specified the case will count as failed.
Asynchronous test cases *must* specify the number of assertions they
will run. This allows more easily catching situations where e.g. the
test architecture was not warned about asynchronous operations.
.. note::
Asynchronous test cases also have a 2 seconds timeout: if the test
does not finish within 2 seconds, it will be considered
failed. This pretty much always means the test will not
resolve. This timeout *only* applies to the test itself, not to
the setup and teardown processes.
.. note::
If the returned deferred is rejected, the test will be failed
unless :js:attr:`~TestOptions.fail_on_rejection` is set to
``false``.
RPC
---
An important subset of asynchronous test cases is test cases which
need to perform (and chain, to an extent) RPC calls.
.. note::
Because they are a subset of asynchronous cases, RPC cases must
also provide a valid :js:attr:`assertions count
<TestOptions.asserts>`.
By default, test cases will fail when trying to perform an RPC
call. The ability to perform RPC calls must be explicitly requested by
a test case (or its containing test suite) through
:js:attr:`~TestOptions.rpc`, and can be one of two modes: ``mock`` or
``rpc``.
Mock RPC
++++++++
The preferred (and fastest from a setup and execution time point of
view) way to do RPC during tests is to mock the RPC calls: while
setting up the test case, provide what the RPC responses "should" be,
and only test the code between the "user" (the test itself) and the
RPC call, before the call is effectively done.
To do this, set the :js:attr:`rpc option <TestOptions.rpc>` to
``mock``. This will add a third parameter to the test case callback:
.. js:function:: mock(rpc_spec, handler)
Can be used in two different ways depending on the shape of the
first parameter:
* If it matches the pattern ``model:method`` (if it contains a
colon, essentially) the call will set up the mocking of an RPC
call straight to the OpenERP server (through XMLRPC) as
performed via e.g. :js:func:`openerp.web.Model.call`.
In that case, ``handler`` should be a function taking two
arguments ``args`` and ``kwargs``, matching the corresponding
arguments on the server side and should simply return the value
as if it were returned by the Python XMLRPC handler::
test('XML-RPC', {rpc: 'mock', asserts: 3}, function (instance, $s, mock) {
// set up mocking
mock('people.famous:name_search', function (args, kwargs) {
strictEqual(kwargs.name, 'bob');
return [
[1, "Microsoft Bob"],
[2, "Bob the Builder"],
[3, "Silent Bob"]
];
});
// actual test code
return new instance.web.Model('people.famous')
.call('name_search', {name: 'bob'}).then(function (result) {
strictEqual(result.length, 3, "shoud return 3 people");
strictEqual(result[0][1], "Microsoft Bob",
"the most famous bob should be Microsoft Bob");
});
});
* Otherwise, if it matches an absolute path (e.g. ``/a/b/c``) it
will mock a JSON-RPC call to a web client controller, such as
``/web/webclient/translations``. In that case, the handler takes
a single ``params`` argument holding all of the parameters
provided over JSON-RPC.
As previously, the handler should simply return the result value
as if returned by the original JSON-RPC handler::
test('JSON-RPC', {rpc: 'mock', asserts: 3, templates: true}, function (instance, $s, mock) {
var fetched_dbs = false, fetched_langs = false;
mock('/web/database/get_list', function () {
fetched_dbs = true;
return ['foo', 'bar', 'baz'];
});
mock('/web/session/get_lang_list', function () {
fetched_langs = true;
return [['vo_IS', 'Hopelandic / Vonlenska']];
});
// widget needs that or it blows up
instance.webclient = {toggle_bars: openerp.testing.noop};
var dbm = new instance.web.DatabaseManager({});
return dbm.appendTo($s).then(function () {
ok(fetched_dbs, "should have fetched databases");
ok(fetched_langs, "should have fetched languages");
deepEqual(dbm.db_list, ['foo', 'bar', 'baz']);
});
});
.. note::
Mock handlers can contain assertions, these assertions should be
part of the assertions count (and if multiple calls are made to a
handler containing assertions, it multiplies the effective number
of assertions).
.. _testing-rpc-rpc:
Actual RPC
++++++++++
A more realistic (but significantly slower and more expensive) way to
perform RPC calls is to perform actual calls to an actually running
OpenERP server. To do this, set the :js:attr:`rpc option
<~TestOptions.rpc>` to ``rpc``, it will not provide any new parameter
but will enable actual RPC, and the automatic creation and destruction
of databases (from a specified source) around tests.
First, create a basic model we can test stuff with:
.. code-block:: javascript
from openerp.osv import orm, fields
class TestObject(orm.Model):
_name = 'web_tests_demo.model'
_columns = {
'name': fields.char("Name", required=True),
'thing': fields.char("Thing"),
'other': fields.char("Other", required=True)
}
_defaults = {
'other': "bob"
}
then the actual test::
test('actual RPC', {rpc: 'rpc', asserts: 4}, function (instance) {
var Model = new instance.web.Model('web_tests_demo.model');
return Model.call('create', [{name: "Bob"}])
.then(function (id) {
return Model.call('read', [[id]]);
}).then(function (records) {
strictEqual(records.length, 1);
var record = records[0];
strictEqual(record.name, "Bob");
strictEqual(record.thing, false);
// default value
strictEqual(record.other, 'bob');
});
});
This test looks like a "mock" RPC test but for the lack of mock
response (and the different ``rpc`` type), however it has further
ranging consequences in that it will copy an existing database to a
new one, run the test in full on that temporary database and destroy
the database, to simulate an isolated and transactional context and
avoid affecting other tests. One of the consequences is that it takes
a *long* time to run (5~10s, most of that time being spent waiting for
a database duplication).
Furthermore, as the test needs to clone a database, it also has to ask
which database to clone, the database/super-admin password and the
password of the ``admin`` user (in order to authenticate as said
user). As a result, the first time the test runner encounters an
``rpc: "rpc"`` test configuration it will produce the following
prompt:
.. image:: ./images/db-query.png
:align: center
and stop the testing process until the necessary information has been
provided.
The prompt will only appear once per test run, all tests will use the
same "source" database.
.. note::
The handling of that information is currently rather brittle and
unchecked, incorrect values will likely crash the runner.
.. note::
The runner does not currently store this information (for any
longer than a test run that is), the prompt will have to be filled
every time.
Testing API
-----------
.. js:function:: openerp.testing.section(name[, options], body)
A test section, serves as shared namespace for related tests (for
constants or values to only set up once). The ``body`` function
should contain the tests themselves.
Note that the order in which tests are run is essentially
undefined, do *not* rely on it.
:param String name:
:param TestOptions options:
:param body:
:type body: Function<:js:func:`~openerp.testing.case`, void>
.. js:function:: openerp.testing.case(name[, options], callback)
Registers a test case callback in the test runner, the callback
will only be run once the runner is started (or maybe not at all,
if the test is filtered out).
:param String name:
:param TestOptions options:
:param callback:
:type callback: Function<instance, $, Function<String, Function, void>>
.. js:class:: TestOptions
the various options which can be passed to
:js:func:`~openerp.testing.section` or
:js:func:`~openerp.testing.case`. Except for
:js:attr:`~TestOptions.setup` and
:js:attr:`~TestOptions.teardown`, an option on
:js:func:`~openerp.testing.case` will overwrite the corresponding
option on :js:func:`~openerp.testing.section` so
e.g. :js:attr:`~TestOptions.rpc` can be set for a
:js:func:`~openerp.testing.section` and then differently set for
some :js:func:`~openerp.testing.case` of that
:js:func:`~openerp.testing.section`
.. js:attribute:: TestOptions.asserts
An integer, the number of assertions which should run during a
normal execution of the test. Mandatory for asynchronous tests.
.. js:attribute:: TestOptions.setup
Test case setup, run right before each test case. A section's
:js:func:`~TestOptions.setup` is run before the case's own, if
both are specified.
.. js:attribute:: TestOptions.teardown
Test case teardown, a case's :js:func:`~TestOptions.teardown`
is run before the corresponding section if both are present.
.. js:attribute:: TestOptions.fail_on_rejection
If the test is asynchronous and its resulting promise is
rejected, fail the test. Defaults to ``true``, set to
``false`` to not fail the test in case of rejection::
// test/demo.js
test('unfail rejection', {
asserts: 1,
fail_on_rejection: false
}, function () {
var d = $.Deferred();
setTimeout(function () {
ok(true);
d.reject();
}, 100);
return d;
});
.. js:attribute:: TestOptions.rpc
RPC method to use during tests, one of ``"mock"`` or
``"rpc"``. Any other value will disable RPC for the test (if
they were enabled by the suite for instance).
.. js:attribute:: TestOptions.templates
Whether the current module (and its dependencies)'s templates
should be loaded into QWeb before starting the test. A
boolean, ``false`` by default.
The test runner can also use two global configuration values set
directly on the ``window`` object:
* ``oe_all_dependencies`` is an ``Array`` of all modules with a web
component, ordered by dependency (for a module ``A`` with
dependencies ``A'``, any module of ``A'`` must come before ``A`` in
the array)
* ``oe_db_info`` is an object with 3 keys ``source``, ``supadmin`` and
``password``. It is used to pre-configure :ref:`actual RPC
<testing-rpc-rpc>` tests, to avoid a prompt being displayed
(especially for headless situations).
Running through Python
----------------------
The web client includes the means to run these tests on the
command-line (or in a CI system), but while actually running it is
pretty simple the setup of the pre-requisite parts has some
complexities.
1. Install unittest2_ and QUnitSuite_ in your Python environment. Both
can trivially be installed via `pip <http://pip-installer.org>`_ or
`easy_install
<http://packages.python.org/distribute/easy_install.html>`_.
The former is the unit-testing framework used by OpenERP, the
latter is an adapter module to run qunit_ test suites and convert
their result into something unittest2_ can understand and report.
2. Install PhantomJS_. It is a headless
browser which allows automating running and testing web
pages. QUnitSuite_ uses it to actually run the qunit_ test suite.
The PhantomJS_ website provides pre-built binaries for some
platforms, and your OS's package management probably provides it as
well.
If you're building PhantomJS_ from source, I recommend preparing
for some knitting time as it's not exactly fast (it needs to
compile both `Qt <http://qt-project.org/>`_ and `Webkit
<http://www.webkit.org/>`_, both being pretty big projects).
.. note::
Because PhantomJS_ is webkit-based, it will not be able to test
if Firefox, Opera or Internet Explorer can correctly run the
test suite (and it is only an approximation for Safari and
Chrome). It is therefore recommended to *also* run the test
suites in actual browsers once in a while.
.. note::
The version of PhantomJS_ this was build through is 1.7,
previous versions *should* work but are not actually supported
(and tend to just segfault when something goes wrong in
PhantomJS_ itself so they're a pain to debug).
3. Set up :ref:`OpenERP Command <openerpcommand:openerp-command>`,
which will be used to actually run the tests: running the qunit_
test suite requires a running server, so at this point OpenERP
Server isn't able to do it on its own during the building/testing
process.
4. Install a new database with all relevant modules (all modules with
a web component at least), then restart the server
.. note::
For some tests, a source database needs to be duplicated. This
operation requires that there be no connection to the database
being duplicated, but OpenERP doesn't currently break
existing/outstanding connections, so restarting the server is
the simplest way to ensure everything is in the right state.
5. Launch ``oe run-tests -d $DATABASE -mweb`` with the correct
addons-path specified (and replacing ``$DATABASE`` by the source
database you created above)
.. note::
If you leave out ``-mweb``, the runner will attempt to run all
the tests in all the modules, which may or may not work.
If everything went correctly, you should now see a list of tests with
(hopefully) ``ok`` next to their names, closing with a report of the
number of tests run and the time it took:
.. literalinclude:: test-report.txt
:language: text
Congratulation, you have just performed a successful "offline" run of
the OpenERP Web test suite.
.. note::
Note that this runs all the Python tests for the ``web`` module,
but all the web tests for all of OpenERP. This can be surprising.
.. _qunit: http://qunitjs.com/
.. _qunit assertions: http://api.qunitjs.com/category/assert/
.. _unittest2: http://pypi.python.org/pypi/unittest2
.. _QUnitSuite: http://pypi.python.org/pypi/QUnitSuite/
.. _PhantomJS: http://phantomjs.org/
+2
View File
@@ -33,6 +33,8 @@ view. It provides a number of services to handle a section of a page:
* Backbone-compatible shortcuts
.. _widget-dom_root:
DOM Root
--------
+28 -19
View File
@@ -17,7 +17,6 @@ import tempfile
import threading
import time
import traceback
import urllib
import urlparse
import uuid
import xmlrpclib
@@ -33,7 +32,6 @@ import werkzeug.wsgi
import openerp
import nonliterals
import session
_logger = logging.getLogger(__name__)
@@ -95,12 +93,12 @@ class WebRequest(object):
if not self.session:
self.session = session.OpenERPSession()
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
# Determine self.lang
lang = self.params.get('lang', None)
if lang is None:
lang = self.session.eval_context(self.context).get('lang')
lang = self.context.get('lang')
if lang is None:
lang = self.httprequest.cookies.get('lang')
if lang is None:
@@ -112,6 +110,11 @@ class WebRequest(object):
# we use _ as seprator where RFC2616 uses '-'
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):
""" JSON-RPC2 over HTTP.
@@ -182,9 +185,9 @@ class JsonRequest(WebRequest):
try:
# Read POST content or POST Form Data named "request"
if requestf:
self.jsonrequest = simplejson.load(requestf, object_hook=nonliterals.non_literal_decoder)
self.jsonrequest = simplejson.load(requestf, object_hook=reject_nonliteral)
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", {}))
if _logger.isEnabledFor(logging.DEBUG):
_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.
response['httpsessionid'] = self.httpsession.sid
mime = 'application/javascript'
body = "%s(%s);" % (jsonp, simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder),)
body = "%s(%s);" % (jsonp, simplejson.dumps(response),)
else:
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))])
return r
@@ -402,8 +405,10 @@ def session_context(request, session_store, session_lock, sid):
for k, v in request.session.iteritems():
stored = in_store.get(k)
if stored and isinstance(v, session.OpenERPSession):
v.contexts_store.update(stored.contexts_store)
v.domains_store.update(stored.domains_store)
if hasattr(v, 'contexts_store'):
del v.contexts_store
if hasattr(v, 'domains_store'):
del v.domains_store
if not hasattr(v, 'jsonp_requests'):
v.jsonp_requests = {}
v.jsonp_requests.update(getattr(
@@ -458,6 +463,16 @@ class DisableCacheMiddleware(object):
start_response(status, new_headers)
return self.app(environ, start_wrapped)
def session_path():
try:
username = getpass.getuser()
except Exception:
username = "unknown"
path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
if not os.path.exists(path):
os.mkdir(path, 0700)
return path
class Root(object):
"""Root WSGI application for the OpenERP Web Client.
"""
@@ -469,13 +484,7 @@ class Root(object):
self.dispatch = DisableCacheMiddleware(app)
# Setup http sessions
try:
username = getpass.getuser()
except Exception:
username = "unknown"
path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
if not os.path.exists(path):
os.mkdir(path, 0700)
path = session_path()
self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path)
self.session_lock = threading.Lock()
_logger.debug('HTTP sessions stored in: %s', path)
@@ -528,7 +537,7 @@ class Root(object):
"""
statics = {}
for addons_path in openerp.modules.module.ad_paths:
for module in os.listdir(addons_path):
for module in sorted(os.listdir(addons_path)):
if module not in addons_module:
manifest_path = os.path.join(addons_path, module, '__openerp__.py')
path_static = os.path.join(addons_path, module, 'static')
@@ -561,7 +570,7 @@ class Root(object):
:rtype: ``Controller | None``
"""
if l:
ps = '/' + '/'.join(l)
ps = '/' + '/'.join(filter(None, l))
method_name = 'index'
while ps:
c = controllers_path.get(ps)
+2513 -1385
View File
File diff suppressed because it is too large Load Diff
+2520 -1392
View File
File diff suppressed because it is too large Load Diff
+2521 -1401
View File
File diff suppressed because it is too large Load Diff
+2389 -1347
View File
File diff suppressed because it is too large Load Diff
+2327 -1338
View File
File diff suppressed because it is too large Load Diff
+2510 -1390
View File
File diff suppressed because it is too large Load Diff
+2490 -1396
View File
File diff suppressed because it is too large Load Diff
+2523 -1391
View File
File diff suppressed because it is too large Load Diff
+2512 -1399
View File
File diff suppressed because it is too large Load Diff
+2530 -1402
View File
File diff suppressed because it is too large Load Diff
+2562 -1422
View File
File diff suppressed because it is too large Load Diff
+2426 -1369
View File
File diff suppressed because it is too large Load Diff
+2520 -1400
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2541 -1413
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2343 -1344
View File
File diff suppressed because it is too large Load Diff
+2340 -1339
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2500 -1380
View File
File diff suppressed because it is too large Load Diff
+2574 -1421
View File
File diff suppressed because it is too large Load Diff
+2327 -1338
View File
File diff suppressed because it is too large Load Diff
+2521 -1401
View File
File diff suppressed because it is too large Load Diff
+2401 -1372
View File
File diff suppressed because it is too large Load Diff
+2398 -1358
View File
File diff suppressed because it is too large Load Diff
+2549 -1421
View File
File diff suppressed because it is too large Load Diff
+2466 -1371
View File
File diff suppressed because it is too large Load Diff
+2340 -1339
View File
File diff suppressed because it is too large Load Diff
+2538 -1400
View File
File diff suppressed because it is too large Load Diff
+2522 -1394
View File
File diff suppressed because it is too large Load Diff
+2514 -1394
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2429 -1369
View File
File diff suppressed because it is too large Load Diff
+2468 -1375
View File
File diff suppressed because it is too large Load Diff
+2421 -1287
View File
File diff suppressed because it is too large Load Diff
+2440 -1370
View File
File diff suppressed because it is too large Load Diff
+2546 -1395
View File
File diff suppressed because it is too large Load Diff
+2488 -1397
View File
File diff suppressed because it is too large Load Diff
+2464 -1348
View File
File diff suppressed because it is too large Load Diff
+2524 -1389
View File
File diff suppressed because it is too large Load Diff
+2540 -1397
View File
File diff suppressed because it is too large Load Diff
+2521 -1393
View File
File diff suppressed because it is too large Load Diff
+2541 -1396
View File
File diff suppressed because it is too large Load Diff
+2327 -1338
View File
File diff suppressed because it is too large Load Diff
+2490 -1387
View File
File diff suppressed because it is too large Load Diff
+2327 -1338
View File
File diff suppressed because it is too large Load Diff
+2518 -1404
View File
File diff suppressed because it is too large Load Diff
+2515 -1387
View File
File diff suppressed because it is too large Load Diff
+2549 -1419
View File
File diff suppressed because it is too large Load Diff
+2327 -1338
View File
File diff suppressed because it is too large Load Diff
+2354 -1367
View File
File diff suppressed because it is too large Load Diff
+2525 -1393
View File
File diff suppressed because it is too large Load Diff
+2367 -1358
View File
File diff suppressed because it is too large Load Diff
-267
View File
@@ -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
-79
View File
@@ -10,8 +10,6 @@ import xmlrpclib
import openerp
import nonliterals
_logger = logging.getLogger(__name__)
#----------------------------------------------------------
@@ -81,8 +79,6 @@ class OpenERPSession(object):
self._password = False
self._suicide = False
self.context = {}
self.contexts_store = {}
self.domains_store = {}
self.jsonp_requests = {} # FIXME use a LRU
def send(self, service_name, method, *args):
@@ -192,79 +188,4 @@ class OpenERPSession(object):
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:
@@ -8952,7 +8952,8 @@ $.extend( $.ui.dialog.overlay, {
$( document ).bind( $.ui.dialog.overlay.events, function( event ) {
// stop events if the z-index of the target is < the z-index of the overlay
// we cannot return true when we don't want to cancel the event (#3523)
if ( $( event.target ).zIndex() < $.ui.dialog.overlay.maxZ ) {
if ($(event.target).zIndex() < $.ui.dialog.overlay.maxZ &&
$(event.target).closest('.ui-dialog').zIndex() < $.ui.dialog.overlay.maxZ) {
return false;
}
});

Some files were not shown because too many files have changed in this diff Show More