From e46502580a5c9e19c31e15dd308e99307bbb68ed Mon Sep 17 00:00:00 2001 From: "Bharat Devnani (OpenERP)" Date: Wed, 25 Jul 2012 16:43:27 +0530 Subject: [PATCH 001/125] [IMP] changed the condition in def create_move of account_asset/account_asset.py bzr revid: bde@tinyerp.com-20120725111327-rd1se9de0yoj8wgg --- addons/account_asset/account_asset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/account_asset/account_asset.py b/addons/account_asset/account_asset.py index 8cd73c60aa7..fe0ee8b3922 100644 --- a/addons/account_asset/account_asset.py +++ b/addons/account_asset/account_asset.py @@ -366,8 +366,6 @@ class account_asset_depreciation_line(osv.osv): currency_obj = self.pool.get('res.currency') created_move_ids = [] for line in self.browse(cr, uid, ids, context=context): - if currency_obj.is_zero(cr, uid, line.asset_id.currency_id, line.remaining_value): - can_close = True depreciation_date = line.asset_id.prorata and line.asset_id.purchase_date or time.strftime('%Y-%m-%d') period_ids = period_obj.find(cr, uid, depreciation_date, context=context) company_currency = line.asset_id.company_id.currency_id.id @@ -419,6 +417,8 @@ class account_asset_depreciation_line(osv.osv): }) self.write(cr, uid, line.id, {'move_id': move_id}, context=context) created_move_ids.append(move_id) + if currency_obj.is_zero(cr, uid, line.asset_id.currency_id, line.asset_id.value_residual): + can_close = True if can_close: asset_obj.write(cr, uid, [line.asset_id.id], {'state': 'close'}, context=context) return created_move_ids From 1cf9b2a60da502de27aa67e8318f41ad6692e5a9 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 24 Oct 2012 13:01:46 +0200 Subject: [PATCH 002/125] [REM] test_support controller bzr revid: xmo@openerp.com-20121024110146-bzuf19sfptew0dn0 --- addons/web/test_support/__init__.py | 37 ----------------------- addons/web/test_support/controllers.py | 42 -------------------------- 2 files changed, 79 deletions(-) delete mode 100644 addons/web/test_support/__init__.py delete mode 100644 addons/web/test_support/controllers.py diff --git a/addons/web/test_support/__init__.py b/addons/web/test_support/__init__.py deleted file mode 100644 index 59f6cc67ca2..00000000000 --- a/addons/web/test_support/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -import xmlrpclib -from ..common.openerplib.main import Connector - -execute_map = {} - -class TestConnector(Connector): - def db_list_lang(self): - return [('en_US', u'Test Language')] - - def common_authenticate(self, db, login, password, environment): - return 87539319 - - def common_login(self, db, login, password): - return self.common_authenticate(db, login, password, {}) - - def object_execute_kw(self, db, uid, password, model, method, args, kwargs): - if model in execute_map and hasattr(execute_map[model], method): - return getattr(execute_map[model], method)(*args, **kwargs) - - raise xmlrpclib.Fault({ - 'model': model, - 'method': method, - 'args': args, - 'kwargs': kwargs - }, '') - - def send(self, service_name, method, *args): - method_name = '%s_%s' % (service_name, method) - if hasattr(self, method_name): - return getattr(self, method_name)(*args) - - raise xmlrpclib.Fault({ - 'service': service_name, - 'method': method, - 'args': args - }, '') diff --git a/addons/web/test_support/controllers.py b/addons/web/test_support/controllers.py deleted file mode 100644 index f1013fbaaf8..00000000000 --- a/addons/web/test_support/controllers.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- - -from ..common import http, nonliterals -from ..controllers.main import Session - -UID = 87539319 -DB = 'test_db' -LOGIN = 'test_login' -PASSWORD = 'test_password' -CONTEXT = {'lang': 'en_US', 'tz': 'UTC', 'uid': UID} - -def bind(session): - session.bind(DB, UID, LOGIN, PASSWORD) - session.context = CONTEXT - session.build_connection().set_login_info(DB, LOGIN, PASSWORD, UID) - -class TestController(http.Controller): - _cp_path = '/tests' - - @http.jsonrequest - def add_nonliterals(self, req, domains, contexts): - return { - 'domains': [nonliterals.Domain(req.session, domain) - for domain in domains], - 'contexts': [nonliterals.Context(req.session, context) - for context in contexts] - } - -class TestSession(Session): - _cp_path = '/web/session' - - def session_info(self, req): - if not req.session._uid: - bind(req.session) - - return { - "session_id": req.session_id, - "uid": req.session._uid, - "context": CONTEXT, - "db": req.session._db, - "login": req.session._login, - } From 99a2dd3938c7d7d4b7e9c724ec8acbb5eeedf35f Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 25 Oct 2012 17:47:45 +0200 Subject: [PATCH 003/125] [ADD] first 90% of non-sucky testing API, last 90% left also all existing tests shoud now be completely broken and need to be fixed bzr revid: xmo@openerp.com-20121025154745-rw2gktfd6tp68k2m --- .bzrignore | 31 +- addons/web/__openerp__.py | 13 + addons/web/controllers/__init__.py | 1 + addons/web/controllers/main.py | 8 +- addons/web/controllers/testing.py | 155 ++++++ addons/web/doc/images/runner.png | Bin 0 -> 3963 bytes addons/web/doc/images/runner2.png | Bin 0 -> 6807 bytes addons/web/doc/images/tests.png | Bin 0 -> 65388 bytes addons/web/doc/images/tests2.png | Bin 0 -> 37769 bytes addons/web/doc/images/tests3.png | Bin 0 -> 37827 bytes addons/web/doc/index.rst | 2 + addons/web/doc/testing.rst | 479 ++++++++++++++++++ addons/web/http.py | 2 +- addons/web/static/src/js/chrome.js | 4 +- addons/web/static/src/js/testing.js | 179 +++++++ addons/web/static/src/js/views.js | 4 +- addons/web/static/test/test.html | 62 --- addons/web/static/test/testing.js | 97 ---- .../web_graph/static/lib/flotr2/lib/bean.js | 4 +- addons/web_tests_demo/__init__.py | 0 addons/web_tests_demo/__openerp__.py | 8 + addons/web_tests_demo/static/src/js/demo.js | 11 + addons/web_tests_demo/static/src/xml/demo.xml | 7 + addons/web_tests_demo/static/test/demo.js | 87 ++++ 24 files changed, 963 insertions(+), 191 deletions(-) create mode 100644 addons/web/controllers/testing.py create mode 100644 addons/web/doc/images/runner.png create mode 100644 addons/web/doc/images/runner2.png create mode 100644 addons/web/doc/images/tests.png create mode 100644 addons/web/doc/images/tests2.png create mode 100644 addons/web/doc/images/tests3.png create mode 100644 addons/web/doc/testing.rst create mode 100644 addons/web/static/src/js/testing.js delete mode 100644 addons/web/static/test/test.html delete mode 100644 addons/web/static/test/testing.js create mode 100644 addons/web_tests_demo/__init__.py create mode 100644 addons/web_tests_demo/__openerp__.py create mode 100644 addons/web_tests_demo/static/src/js/demo.js create mode 100644 addons/web_tests_demo/static/src/xml/demo.xml create mode 100644 addons/web_tests_demo/static/test/demo.js diff --git a/.bzrignore b/.bzrignore index 8531ad42bbb..1e0564801ab 100644 --- a/.bzrignore +++ b/.bzrignore @@ -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/ diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py index fcfdca60a2a..7bdfa8b55dd 100644 --- a/addons/web/__openerp__.py +++ b/addons/web/__openerp__.py @@ -40,6 +40,7 @@ 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/corelib.js", "static/src/js/coresetup.js", "static/src/js/dates.js", @@ -67,4 +68,16 @@ This module provides the core of the OpenERP Web Client. 'qweb' : [ "static/src/xml/*.xml", ], + 'test': [ + "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-editable.js" + ], } diff --git a/addons/web/controllers/__init__.py b/addons/web/controllers/__init__.py index 12a7e529b67..74c27518ece 100644 --- a/addons/web/controllers/__init__.py +++ b/addons/web/controllers/__init__.py @@ -1 +1,2 @@ from . import main +from . import testing diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 72ffd9567f6..114a896e77a 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -800,8 +800,7 @@ class Database(openerpweb.Controller): @openerpweb.jsonrequest def get_list(self, req): - dbs = db_list(req) - return {"db_list": dbs} + return db_list(req) @openerpweb.jsonrequest def create(self, req, fields): @@ -922,10 +921,7 @@ class Session(openerpweb.Controller): @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"} diff --git a/addons/web/controllers/testing.py b/addons/web/controllers/testing.py new file mode 100644 index 00000000000..0cfc633a492 --- /dev/null +++ b/addons/web/controllers/testing.py @@ -0,0 +1,155 @@ +# 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 ..http import Controller, httprequest + +NOMODULE_TEMPLATE = Template(u""" + + + + + OpenERP Testing + + +
+ +
    + % for name, module in modules: +
  • ${name}
  • + % endfor +
+
+ + +""") +NOTFOUND = Template(u""" +

Unable to find the module [${module}], please check that the module + name is correct and the module is on OpenERP's path.

+<< Back to tests +""") +TESTING = Template(u""" + +<%def name="to_path(module, p)">/${module}/${p} + + + + OpenERP Web Tests + + + + + + + + +
+
+ +% for module, jss, tests, templates in files: + % for js in jss: + + % endfor + % if tests or templates: + + % endif + % if tests: + % for test in tests: + + % endfor + % endif +% endfor + +""") + +class TestRunnerController(Controller): + _cp_path = '/web/tests' + + @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) + ] + + 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']])) + + 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, '/') diff --git a/addons/web/doc/images/runner.png b/addons/web/doc/images/runner.png new file mode 100644 index 0000000000000000000000000000000000000000..bd48e9d29221767295afe05d2722ff332c93ab00 GIT binary patch literal 3963 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 C-5}fm literal 0 HcmV?d00001 diff --git a/addons/web/doc/images/tests.png b/addons/web/doc/images/tests.png new file mode 100644 index 0000000000000000000000000000000000000000..84083d9e5ed7583da33261259fb1f4e84d46a650 GIT binary patch literal 65388 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%0DZOjvNOog+{rl3Xfb#C5*7QgCjgOz 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 Wnj<@$ntjj@VI)Lkzf}n7`~4p&g3;9g literal 0 HcmV?d00001 diff --git a/addons/web/doc/images/tests2.png b/addons/web/doc/images/tests2.png new file mode 100644 index 0000000000000000000000000000000000000000..59835b257b5e18f236303a34c66ede42f0bbc29e GIT binary patch literal 37769 zcmb?>WpG?gujZJUnK5Q&hM1X|nVC6eh?$w$F|%WgV;FPH%*@QN&ij6QcWZxa)vdZy zH8biy>Xy_ksdb)nA{FH&5MXg(0RRAkl%%LK004^oN#8<4e*TU~6R`mRm?%~vB8pNX zBE*W$4(3+2W&nU@XtO4iqUw0+Id29`FVk+ei+Ip(b_)KN1NcN?5SSp)AW~AP$RKcG zB@|@Ectas2RUruQgyINdQYoX_o{#R;PGvb0Z^R zXl+1{2x&h(LR0%00e*Wu8Xzb@G#`|Fji2mS)7ZFNUr#bok)wg8zVvI$UG2+T>(~41 z*h_E#OL%>_Mu-Jdv_s$GDk~iyDgYdIaMFad-+xDgsT3*P0D16xPyGea>ka4qBOIC4 z$FYfB1(=CHz){rW!O;K!JzbZ}yF7!{BJyqM*HozIQ}`<(;oxzu|GDF+D#8N_5+dST zOkTg{;*Q|r)tdqb|GUx5+S&ur<7DgW7cRYzh&tkjwvRp6*GW`-^t%fa;?1wOi1qbB zIvpDqCulVeFs;#!N{~Fn=-_={@EyTa9`nvO*f}plnw2hD9oi7%ZewF!?PMw0=onTv z78nzC;O+}sxmuMX%8I#Ke>6p(emrr$28>+Xy+Bp#yf-tD3~mJ+`)J%~0p&gb&*Tdn48k{w2Onxswq7t?AtX%zYgW zC&E(fmN=JKpR5!m4+T&18#ysf0-q#IF{(0@RhX;jGjV@HBII7U%Tp|+A=d0IKnER8ji+}FmUfvFKovL^mz zEP`Np39NjZsn$PlhYN??w@C1i{Shd?sEpy65z_%<%(5K&sHHeAL^q(U-*+L^ zf8fr;pTx%eE;adH7qjTIh;oYW4kyv;v}Nk_%q5`<%>beCt!l_^N8AyvO^-&K2870p zhLwh~lB-g;(z+6{(x{TVQjZUZF+`(ZX9w&O`1sWqt(!(C9%PqDyi8t9Z;F<2( z=63Q>1LqJMk!g|TH%l^;E3+&sDGmfq40bTK3yu!91dc~KS9()=XNEj0J97+^9ZMs# z4T}$}1q&x@kNLXMmiZqO7*p;c=Ww$`i~*Vv>pt9m?~LX=w~UxvxzO<>`lRa6GkV@2 zz2vpTH*6n_7vvX6D9UgU32X_JA+T68d2xBU3BZK!g#E-92G}&7G(io41^ya^Rrpoy zRpHgpRln6LzEi$1J|#X3zDZAauTW1rPZiHBFUqG&UujO&L*YL-<8_vrf zjC@o@)JwEBDkiErYASL|%5)M>2}h*}tzk(j{c>T;uj%AJNPJb>lpj-|#YLsa6?YSd zU6ew~JInJdBM-Yj z`%_C)xF`oJ`%GU?3s28YUzi@6Mw=;{@|bo5qei?(Qir<}$$&?rUx3ZPY64{5O~1|S zKqs%LSCS|AC;cY_*eKX7SUFgksMjd_sFElX>09Z2=_P3-q(UTXB3YhAFGn3MVQ%Gi zv8Sr25b1pBu93pZ_{vu6=Hnl@wV6M)LzkO)T0645+n<{tH4t_YVsTbE zMe#DRb6N3Ox0$ZlDp|GJ)!F2k&RIz^H8aq&e3>xV5jfAdLbyzQ4_^x+`$5A+5I2~? z%*D+Q#wo@_$KoeKk1M2vR9)=>epH{N$HeKxeR=Hcw#;Y+L|Ok#(wxM! zYyLTgX0jNvGtuQ!so*4pquHaaBNH4N9J4aTvR4IuUV4v?!(07Qjr|?EqaQ&(@|JR! z;wFc)sref7c9L%BG)Nw8;cIuCiKS14h9a!a|KSwCJN7U1Gw5z&+N zej7x0aOqiUFRK4GaBVrXs5iJeK|M>{SH7Q~DalIfWQft7?_52 zqfM=;r5IWfRg+oUQcc)d=vq<})=W{guVE*7cB?j8<>waV=5}{_*NzYoRvuO&UN(5J z;9A?vF2$zC#>!!#2enXC6VS2cZqhVRT=GylJzlB<+A8g!VsFv%wc4q=%H#E1ww1f7 z%aYUn;!N?V%}?Xn-i}hiLJCVn!=AI7x7z1J|FtXT#pof;$JfW)N6%-2zb@dia4>H_n~voGwOZz0Jh=7ImTJ)5B{;ZHnwWK)X`Dh z#NTGp+E|BogZ9#RnKoP-%b(O0`L_A?#`NI$(4!1Ou8S-n0PwAX__hlGKulm=%?4bL z_CUbA0jF#WW+I9tzo3&@BfcT4d>N92s32U7OXKjs@>oZ*?!ddx4XrUA*ms?Eo$zc15}f`gG(@v<7q@LJrScc~3i0 z;ay<)u;nE7sAe_Sm1n8RRQ=wL2@HQ^j1-mvIVDutV%c%ARIaIurIOM=wh`Cx7RN;c;4()x2%*@Q=WUa95uta zZSr7VAiC=xNuRrSlzHaezbh*!IBW&S3*`8xzBdOR1alH$@H^9*Rv7Xgc^@C$g0x$zCz?Ka60jx z{c&e@@$&v|@V+2oNm3F|TTbW-IS5M|Y0^uth_Gn`z4Y5Mk+zv6hLj#ELukE7^3PAE zT~OC$&5G3WamxrxNSkI$hm*>al4B3tXgmfSy9`5FU^o{NT!vJ}s1}tbjV4e_ zW)XU^Vd>8z*h?*r`@G*wSh z(j@^g{uD-pv=--UHE#*=r;4O9FxK(L-A1j3he_Wtuk;mHjtgDa^@JPTr&X-p&rA?{TkX)8l(!Q+T<$Z^5sf@kCLCI4IF!w5;^Yt4qy#NEmwWb z$ECC8wI>~%k2;FXo$-Gg9htDsOLC=t@+!1w|~rxs}d&+F6bbr!~-+KQ^i4cUbY`{6gs^~9C#pH&|& zwv!(YA2MI_P}1VnsqiZa2x63ae;BPeMyiH+y1&vC#Ayn_=Fb;x6)cgN9AZ-!-a_Hr zV%uearYqFx8g>oNPYMjw_lr%^kGrIECNqYAWcJo=b}%)5ruJIRmPfZpW}v||AIo!t zzjFo@?p_`Z9W4qHEG=H8X9Yz?N+qWio2CBz$ejEv%xv?*%q$jLK8q&XDQAZ*zEz=v zv;&V{`MYCVM$l5MSDIL1NyA>)oZE>I9!Sl8xI4amqgo>oUK+7mh;*SJT z2&F{1#A~!|TvSYIoc?#KD1>OVsH_;qU=7%vtAvnj(SCB%>0q>_OKlWMEtxavQ9i4z ziL`h2o%4$5nbeHzjJyVWi##rq#Qt>Fq!mw&=WKUEx%J;q9j}LQJP^A<72gp;oy498 zZU@Vk%Q-v-De|9cWvnH&Cp1tS!#-dt}bZJ^pw!uA#Ltt`x1DToM;Dw)S`#nmWFq zJeV9%vMMMb5P1r4D!-3-sQT&NR{#E8P_cUC&tntcO9kW_}msim9_HmHA z)oO?iC=c`MII{uRlD->)AaMD^gBZ$#_C6B}W$%JRQ}BIj2!BTrByJl7fR0^ife6Nf zRx(Yx0zfvgheMZ4dB=#-h{=CLHj0`Iqe*{*Ezn_tdyUE$mn23c`HFIY+={#tVIZkr zjKTu16>%!;A7+xkK?AN9{Qm&InfIv{z@QWv!JuLQRJnO=4k&k2A0i3TGL{1l4JZ%LI7G$-kL=no zLGfPAcmrtEOw4nPicp0PO(zE!S}A^NGbFH=jWi3CSTcm_qw3RK z$Xp3qGJ8yOb`zPKvs>1!_^k&#M2u|=tDLu70Kq)_C~ID`Y6B0LgdqJ-mKQ&X2Qsh; zR0at9c%SkiA4ZuAVFS6Idq8z>fnizvT;0r85K3HO?4aX7chn@eeX3;Mt-zDF^`KN7 z{P;}qSpDfp_Ecl0*X|7wW0S!c{Wa~M_TPS9J-($4JslOLm&d^8a`7RGoD*ew-PS|x zE4xEPS;~s6y>87~`MejQ=l;|oqTB}fl};+p0Uqra@$b;|^#!eR;I3fWc{ZMUF zb4+NJwN52uFaCBj6A~yNO`jhogwV(}%4YZb@P!iRT<- zT;N*{asS2%4nY!ECxIXS^d|C~CK0D123{PK1S^_SJt1`-!)h`Yr0%J);Qg{ zVBLsbXWmF%&0KvTr(^ok%u=t?DU)easMF|^I%zd%S4wq=?AM^=76l}`Dq0s_d^yjQZ$s-hd+;Nj>T!iYej12HE6Xo)?GNF z^CezHyGOd|IX&E@-(BCnAL4Dc;>P2P60IUv;&Gtm;sc4;(a|u5us)6-D@0?8&vsj8 z+mdi`(pA7Rtd@AVhgbVtG7gXo^3gk~>1gk%Q`(x8=G79E%)COY%%6_eo_`GX%O&pv z@!U8~?D5u(Y;;aMr?qDMuDd;ImGWi*J0gdFu9I37^58wX+OAk$n|~kv@_f?y3G=#} z^N`D`N^~rUJ0V9OLU-;fttIZyUElB%w}s=U{oC6+QvG<4UER*lm)o>gec;Bs1PF}~ zq^%G%6~NE)0xDQl5mdVmS~Jv-5fSGT&Vf{gLJ+Qkh76(k1?L3$C+snToQ!>oz#z~! z@>ij%5=y!EEL|qp`q|SV3OvCetP98u1?&E z`jy6+#=!ztb$iWO0c)j2si3K)Ih*yiRfj*7cg(hPYU(9fmA1c^m>sJ7*@$OJggaBpE!=jM z<}N~6zZq_>_nXA+<@tJPbDv%YdGFqiQq~qI4bB&eqR`foQQDe*rnt~UInnsSQ7A`Z@G#GfD*0~4tLEHN=LpR=hskFu!ve};dy z_(?5YT^)HC89h8a7(7@R9Goo}nYp>S8JSoZSy<>lIp|%y>|KpK>Fr&}{+;B1&m(H) zV&ZJ&=xXI)PyBaYBVz|QSAJ5`zk>ej^B+C!9REv@y~}^B`m~SH)5wvLnSqJ%zh?d% z$oFX@kBGCGk*kBVs)K{AfUt>!vpuo9nX}7ZQLGH?e2o7u%YS6@G5%HL|5EH|LxWl z|L;?Omhm{>8{&}Ee{4CLbFeor8>mqJD}$4}8kTt9_Yvpf)n~7^`ujhEgEnC3?Tu_- z&GMiL|4JuDUYs%pT=hk<_kNppPAWBF-oAXq%+EKH{pKd{e%*YXGMFNie0FZEjD*Udg=!p7Vkq-xL}Al+*6Xz?h)zM@*^F z+Ti8#Z`0irEjmv(_9J>zhwnsmG3(9`F%ZNF~q-lK`>kkztPP0 z>Q{Bcx!B|Yhw^F?5I*~oHr*Tl}$16`+`CBRZhu=WhUIgbIC(YRBX z#G3*R1+gWCyyg?$;M>*Zv)0}$!O|x@ZNTsC9@HOxCXF}6n!)kzpMP(oG_+q16A3$D zu%}S|qDJoU)l(NVL0Gtk68>0tBkrO8Wm(NJh-i{uFhx`Z@uH_|Vn9gd(?e?nIpqKe z9M{77I17JXHQ0w*>`mV0gHIV!%xjGPM9OxUPzBEvCX5DS-yC2mP56W&IooGX)R2r@ zyNSI#9iE_-&nX^k$~Q{`2Ug*y1Dn)~;i67|e=FdRyanW?cXz&e!X#m7ng5=csp<+o znyu#TO8S+sBRmOIX@8hHq4Q`$b$VywRwFyy22&zP>eB~o%|&umIW6{xu)yHAG+OUU zNdG|RO3vf)Rtqj{(lpF}r5?roWX7v_W`EHv&;ER?8-@O0oB37;OQ5ua-yoQ5vQx6)F^D9Bm$5wZ>h z*9q8~N$n$vUvvH5N1MPEaAdNcfQESefVj0Z#6FyOBb%Txtr2$JBa5~Q>7M6I1dH>jlwSZwDn#EDzv+3AzCr( zK@ipOjz}Q6i`2kUe8F|snN-%14A{El@gJ5kkCgVn(wnyLD9k zRHW*r92tS#J&6tXEz!`Yg2LQUEZD_#s{lCQv}THXZ3o3a-kFj`{7uqGGLaLohqKYx zh9T3qq||z!a;0X*9AhF0p!`iOl#K8H)sazY<*j?uD&?`BvcVrh7Hzgg{9bRI(yB$eV z1_%~*kh8-PdzXrW7*YX^s_#P8SAGS7#Bd}ZO$zqbajmez%iH;PIY~JPmI|__^hp(r zZzozc5BNUx8Dkz;28v=bq<@tSw&KIS+s2CqW%J6y{@9;n7)FH#4>}n_2tX3mydb}E z_MnFfs0)~?;nwCjXCm^yRM@;nzFB|;2oAn_HK6lhTT_pXZaaEoeo=!Ik~bZJUjJAr zr?^JddK^p+i|wf-&oepFLQg#baN8w@HB9lufD%XiB*K#j>VXwGGizXtbtbJ{>|{Te zYjuia=?W&8)8pE_pb@b@VW{54W^mf3xgTaUsIc41!O5mckj70(Hua(ANufge1{+IQ zX?Vs=ZK)_m!Ss87P*J(4SS<@KW2m~@;}0glll8vDV!SUnHc@@))q5v%U4j1tzJCm? zyRN6A^#+E*6NF^zu@e5!wF7d2`Op;`o`+!<>*v27LB{C|LCW`A zBa?j&vtc|GEa50zUWL7;h;60C1=K*LDQ{taQ2>)`Ypdt7mwwB05Z-isSY+%~SbN>i zc2Lw<70`UDUH@&tU6rjEhyBVa7k3&_ZA@rCj_>y^k?jq&)JCt!cXtkd6#jG!#@JpW zjIQ%j2sqnGR8L6A`29VoKx?QkDuMT?M&?EO|#j?=Tud>y0A74_st+%U?ec+b4hINhie@+z>laiuYLV1LEKH9t(;l zTr8^goF?K;3;h7r++I|*!sm0#Uu!@`)G*-;-Z}BATj22jE?eSy&+q9W-36~Kf$gKY zn7y)oL-jHIK*54&{JdY4C4#I^Fjgr>>p?x!z@pTPcjoYc)EjdBF5UQ)tEyx@V&qo` z=YFt=Ff=1)2nN1g=-Dg_Q|DLTEK%eRU(5-sclkgyqK_o^gbzc*kSKYuXSDl%YX;A| zLWj=en|8UpMb}pVo(2Oc-?P|x>_h3>xA#jClR85x!4bNXO_0skN#CsXV5sS!_^Pw) zFVM$8+KYSJ0^2#?n{8TOYq!HGR(t51Txo!qN)iwTHm)GX46lSHTw&wAJwtRd-t(Lw zAH*za9HE2}SfO_`U3VLh!2VrZA7QH3fC4SO>3Rp69C2GQ18mH-LOvCrVRH^2ziaT zyEHQLl;Obod?ggcDbP0`*TMmr%>m*fH=y=UwAmblK_Ro>?T2cm;HDAoG0rKP00P=! z^N3fLUQ5Q_#w|9@Q|)+wy-+G7<^27f#5eQkR&ZizlLu92d0LORje61EX5hKP1nmn| zPE$0i58v1N4kh2e5K6-RVkM7at3xn_=V8MQp2^f0KCx8CaTeq$Y2t&n)xBlLJJY+o z7%m19oaxmU2IXM}82b?Rp>X5uoS;+ZeIly|*oSvHeG1C+V9JFul(^a4uOP&@r?7o=Qki)}J2q7xU z^#Ngto3^G?e22oZ72UQZNnDdR1x989}Au;u=N}Ht&_J$V9xZhkh z;G3ybWmQ*NUi+KAuJC4+Si!9A(fKbX#5Dj5#r^mn#oTvOo7_D?i_G6suCWXp?!zQM za}n(KDxp8GN}zr@VcVT(wz^%Dp%UnD!(2LYUpqOCA_)TdetA>=5l6(1TC};_2Ufo{ zczFwP(R{eW!v*e@D3OaxSwLLRoOPJW6AQsmR+>fJ59Org>>M}=e$zT4K@Q!~Y1_L_IFtN+>DnSa)=nu%g z*xJ4Y@oP{g2vm-_6^h$8BsQO10haak3JIGZPzD58usQ-u>!+H9uhzM7fVY$xy7xY% z%_~;&IHU2r@e8N6pLnBLwj#3_r$)Wo_=_PX^ z(3!Y1RE>sB$;+vZt%BFhPUw07-$UcsW<(-w7NP2{pXQDVGNH^`)e;i-XNQqm7cDDJ zEVZ@qnQDO1F!@faVRUm6L+cG@yVej}turrmQ6ni$T&+S11QJ|o1S*Jf>U*|FF}s}F zOS12HkNiCd=RbD?TH2$b_K+h?tUTg008d1n!P(QlAn>pco+FR1$F=jR!N=2>j*H8V zXuiApsYDHTq;`s_a2O2q&;Bm=FmI?=0A5U9VOZXKbB)_x2T1lXwUMeUTP}$bd2j7U zunD^au$wO|SC}R-3_ZI z?{A0&(ym@U2m!_n0z|zFzs|*$eQ<;^m3a&?e|(6iLxqg`K8W2&%=jigGxrG}ONsEx z&|;-cUaKG!UyPTGWIwbe6u8g8XK;ncGjEBuD7hR_oPQgo8Jp2>UcOhG?!ncL-PeO8 zW}#ObX*{OmJ)2d!UzcS7d2A}u$>wZg%1Z|MdOC zAVo;Sr^BVSTCs4K^fv}-996)P@%6_m;g5FPXi6F341D5Y2r?hst=7VB{sc)#G3CjQ ztzU))Cp{FbzU=$S>Fa`oJZzK9drAZwYbCv@mCOEC6bwx5NQ(#3fDJ_hcG zJzaM8wCt)#+yg7r@ZPSr-K5lL=;twp*Td_S((t*go7(MHI@so^0hc`b3gf1-(#Vzh zv3yK77qXQdW1-V5x1K22gi$>bJALh^axxj`o19F5-FM*Y#&+7%&JiE%C<%)|uH?;6 zR9CT|UNhG1KJPvL#V52LsL`DLgfG1O6MOM`nvnr3_vE6Q6KJ}-E0aiI1@s!){o+15 zKk1YA`(1LK$TS~njfQjHYl85tT|BQc;_#-M!*0hKEeG9tXO1~(bfp5+@CCjW6i_ld z1Himq2;QhtQ@b*-jx79_Qfinc&v!}}jNvr?+AVeg$4+KT^uAUucQl1<*hWJh1@xAmPPUJP(82G${Wmp%b!9AbyQ)Bkf-uN1dUk z%1knp`vyfso(SpcdWu{sswEF_UcBWTRT1zn=#O*W2pE5~!}bObKbm^4C)yH9!Y7;B z8ZBqLwXpt~&f&Wp!Mz?S(JBs*h4Q{XnxDF|k!I1l-u}5i7t2%0KCvO8r>h+aGNniJ z%bC1sS6Qv!tz9>k4VUW~D%C@uv)E>wwysu}gHaJz z`b^>VoeGVc#lfwl9hTpnV?f%5v2N5Ix1H&$Tb|}Ne<12j>x}9!z9r?!>nH1To}o#X zHQBq$G{8UdT;KmNY8YoM)ZeWlVlB}q8RdsYybe2#s`@AOEyRwQvSehhZLaLxZ%AX| z{rXxcDHBD$ssb~sqNm??6s;mpcmr>?9%ON}t28>Ov47d;MNA&0bS47x@E}^&F1k-* zaWwvv8PGSQL%o-k=apQ2Fo1-z23<3VshkPjnbNepzkru^dk<@-`k?uXKjn}O16_|{ zN3=A_z-1t($k zozXu44)LQM^8W~K^MBAEt1Gp63*N_fOz;0lARtlykKndw=>H!j`(5xrPhI_9=ZN$l zuxZmHNHg$X*lCl6^xOXgMgNX_;4q#-tD*VRojx=)l*@Z4MV5k$g(b1xWC#@-d(l(a zBkYd%8k@(KDwl)$*7WQ(y96`*FAI^=;Q5iE> z9*)LicY*lbd$^54C+iRT#a?(`T7%#R`0+RS-$QX`yWbMPtMY3+;RG>%Ge4&;+DuKS zc1#oartJ;s-MQoG{HlHt+YB=taZ4_X0Qw#e}#d1$TOU(E09yR1g>7eW==&G z4?onKLBSvANzV|`EF_;AO7OscFhUdKrfDcbE)G07=cuc7rQa<9=iE7N-_f*)3hq;s0)FIS`a*^ok`IMUbn=DP0Z-yuh7$t3xqQKk0n zIBY_?CM9)rJ68GV`*tR**BMLA(tOyAZ~Z-eA*8L@;30DpQe27OFGWIcYp6)Notd>V z;qzGS3bu{f*4l_FD%~p-h-Uch?nsqG?CpR%FAiE8HoFzX&2_7zTP44t#BrbTr3#Sd z8?8XC02x~5CiU?7VfSA4742Ab@U(IBqlp=g2KuvQ_8JMiHg(;pt%XaIz)c=Um1-f& zX6;#Pc8VmJF8w%J3K5DV5MnUdZ$Q&uhD?TRxJp%XpA4N{)v1gri$|+?-z#X}a}|)D zZ-x&+EGKGq-Y^uLQF>4O#fw2Ju>m8_J?2{@N7L-*hOci9Q;d~Q#EKeqv9=?) z$=bxa!b7t*jC+5>pIdKGjT&jenBP9O7YF2phplsHWF9$8$ZX^5a0-l4G){s(}OegS+(dWRh6u5S9UFm?FNmkzLU(u|ctUG`% z_C~LrtS-bJkIJN^hQ<6Slj~%K!-C_%VZj<>-vi4s=dwOn{4z-1din;rca?YnS?#(C5`(r)DY)=rUf_b5Hh5L2RHOPMH!>7xOs;`;mxd|}!PqZDYOnQg?T{G`0s&#|NZn(RF(Qv_ zJD`vv%upWDCR^vu=UHtjTYp*_vQy?7&(PP??34gZ{kei!3J3nvCl3{C3gZcSjN4zMZql1T!-!4v?R0Gt& z=}{7(&wp! zP4aaMv4c*UAq@?caIoC!=(lN_QF-Hs-+lK1{_(ECRvTSQ+s+PFY}s^Ld_I#eAF)T# zPJ_H_eoi+T za(k1}{V^JQ7*3I^t~wZxkQy+d%jL=VL?YVnk}dex+S#sMg$*|1BUk4YdEJqRMFG#A z_r)@&N85d4kMq~Oa={;vuJfgl_tx;b%d&zoT+DD(D-6;FsSI}NI4uax@s)4;4R&t} z6&t4g4H&xE_cTVTjV?nh-qD$nxb$js)A`j_;LYjp3CXb z;vIkBX6sfdJn^nGw{tr_Cnsk(f!t?U>g%@{frfcsdM8Lw^34~s&NRNCnOSD;?qON` zT+r>3i+64i4X=z6S}&xF4-Cw5`j z#d^-h$e$n8$`8E+8_>KN5NsQ>HycKiTak3YVlx2kUP9O_0+X3KvMQ^d!~x*AtBvi* z5#XD=UtN;5ZH%L5-^obWRM6{N7}Mui%icsRl`{aB_Z?6?`5|EE_8Bc^|Ab56XE_p* zmWCx=g4(Pdla+;MZy)oMjv}d|poke8{@#M-Zd!JrMPz3AGzIj~hPI--vgt`)#hp;K z;Lv7rJFO*>y9oE8bJV%jaiY=h<`w;0O)`%qJgIA1c|MvMFQH+QKP2Pi<2OJGDA%}6 z?YIz9Hh(?d#W|~2(T6O2a~hZ1A$uyFcV2~w&z8->i;oKaNwMH|8(nN2_PUl9!O`RO z4+&dSu5Jg4iCq3ROr6#fZ=%;10+uQ$wsW;C?uH#HXDu28Zs`|>^Vva(31>P=B4u1o z1X#q*N&f*&amaa)fc*h2)&z?&lZgc+uzd z2;}1a-+e~l3ju}aMtwhgY)wsRZM^9ryk@Zr(4%xtnL+ipWPY`8w;XP|*lNc#vh?)J*l(%YP#djIg(|$CH9#!rachE_Dxy#mT6?T38(YHGqridg_v*PV zlZ@FdCYkjYyo{36eroN8`}Y{eYopiUK=~VY^>cT<4|vKU_F0Ry6awG6CwjhLUyFa! z81MytOU_tLb4?Fm^zM1e^+n4ZDZXc;?e>o=2NAKPzOOS-=f`U3F~47M0O{rkHqt5c zl6vo075iLbGFy71mB&O0#!{qDY&01*bmrOay{)qPW0|aN6nz;>{$S(fw4Lztjf{Wa z1(;;1F=*(2(Z0qQy0fYh`7S_-yc)9VYGkLSg)=ud_xSwGF(#Y2y}vK6riP(Wqie+= zbhODV|3lukN{Emp>_{Y*B|r<+1|-6)H@cqnzyElSk@*yX!%pLia``|}kGgvP_PG*z%orV#SvlHx9*|4IQY!|y0 z^(mWQR_KD`#=u1KstYJ)cW1~qPz=iv)$%OQw(0EonaG&E)&A%$3(ubg0wp%Bp+8kT z=GFBdoS=riUNAB3@kV|K=QxJ&bheL86_@bM2og@%+i4cG#uYWJ_xZ)0mL9H%#l(SC z3hmL=u2w&a35075Lgy|SbuYpOEJ}a; zC~2R&6DgaydOoS0DN`U)R#8!+!lx5Fxx?r8PUiQyXR?{6OCCu@oZ%jA%52c)&s~2n z&pM|*uB5J-E4;iMk}xyCk~=pxuU5L*kET}Ci-m*$nU71!MA8SUt$Qvf6jXGJ5}ZGJ z`>gOK?j8D+@o>}1puzLd53gBymc{*bW-FThf(iGwnidYripm-7Ondg;7ou=c@sO-^q~?H z*KYcGa9|;cTisodam>530xlP~PjZZ^!!8W*0BjX>A z3Pz=MV+LZftx*0 z^%Qjf+_CqXi2_Leo0PV1^v(Q3>&Jf|L-_wA4FBhaNruC*dE zTe;Y0_W$bf>;6I`#-^{d=JE$-qaA4{)B*tW`AqaXcJu;v^CmRz~oOubP~VL{10~kTcxOQ6VLG zp67Jq*FP;pv3Pvil>AB~cKo+q&p68|Pk3PG7Lvf^cRlMiD^Q9nZhhb1A=l|#sA+k< zpr)8-iR@xpVNDgx#ME2!@C0_!af? zHzDi{wiNj6%8UDXplB)L_wcQy08XwwJ(+xe131FZ%@|3F zr1?C|Utd0+-+sRbz5Vf8175UF{M7OzGN!Zt@#N5Sdly+hb13}4x~~i2#Nysz`{fTg zou0$O1D?{={tj6?S(6iUvr9PkG>3!vc1`7A%Utr*{p+=7l$;l@M+Aw}9TGlc_?9p; zUy^SKwS;v7wd(y1Qp7$|WM3QD=73+80&|Znr2Vx`OBSnvwU7ldA4NJBx94vkBR(w= z$rb2lF(}Z@nI)thAgSqA0(>ljcwj=2Sqku)@yM(II1z`Yr$!kK#-;(U^jrJr z^Ia1^vgPk(<@CdY#SUhVvmquK5|1wd?$_6pFT)J163K2nZ0dQmlX+h~N}!`-EyN%0 zt-^U7eFHcSDrH4SK-f+GG=r_{ayA-oW}O+|GwLtChZBF{G)-Z7ut?#ADCId4sLet8yu9NGGWO8JbKUA zyQj!3%!Ij`*73wk@c4iz`fg(BFZx6+-;_-)Xf8=&3~xh)J6|rXBjgx#wczS4L4@76 z9=yOpb#F-OrZMbq7R0jW1xyYN(SjWE@kI(`O3Olu1W(M%63hzIIM~6|_H?B(DoBT( zE%YD=5~iukcNEi9aJ%5EboueF^5*CKeRa%5M+3~Q1j5~qW_ZQO!QrFh)AjO$at@9G z(2u~O+##~rt9?q#}%u3Y^`QN5&~si}ckP7XrM z46D4hZI!r6p+|myvrqCl7k+w_JnQ%BbS_DzbQZ^UTO0@155ZuwAAR3|9dXv>FWxk8 z^7R%QQuh|wMjC?uRdIVz+1H15BTg`55y{=YbT%b>WrE?kg6 zkU)SWxCbY=yYm9UHMj(KcZXoX-QC?AcWu0J_lCx`aUI^TzB@BDQ}^Dg`|H%{-F?pK zz4l(ubDq8SS|YMCiYgU|pCw}3^Hlq%&)qJH1(Uniz6Rf}Ib?6z=n=*fFk85w=0s7N zd00Pd6r;&d2~!U0uawhyw;R*|g8q`YQF(mW^KN|kMjGdE5A%rP(Q$K$uric7yp$ms z^4ZuHd2JOiD3Jf|b}5@Fr6<)qPhij)AL1{Z?CrJOFt?S~;#wom<2}jmu9Nb0&VTRt zz|bytHGff2&L6wEzE1S=sC5IZI}S8jf&Q>a4Wu2{o*1&osFUpOZw>MCR@?Oh(J2bL zbwrZX33i@~qP^deefXW{vd&9f$ot@gC&ZZ62A9U|@E#kJ?*}j;QBw6*Bo~m0Cr@Vf z1^uH;RL`qulKoygD1mhDMU?Tn6`Ry26)s5gF|xyD7<~)FH`)jf2oyuUNnJK|baE;W zSWxL*O7|$Ur|J;`(0T(Ts&2G2kh2IP_x!v}Bh-6*{>XuS=}0~l-(X&yn0tXL!DzQJ z?#uAxJ!5RqtkA8|H{s~_Y7OtF)ksoMolFU&9rn9(T~lJQtc|DDSNIW-7qSMI!ISx` z-`;!a?H@Nx6EhyaBN!JOzHMTBR7dz+@bnv@*(OHTrJCDTpe9k_bvl$EdP!=D;$*MV z3MR%zVTJM!()sI63vy;fm+fHAw+>c6bYYS6$-tQ&j*xd;%5bhna6Ic^$hr6;NGcEt zFLu$$G4P#`JgpSTu zwvi$Gf$N`n#~oG1kMJ0)RfuD@F~a)40yk!J?9sWH=ju;OI{6!G{vL_9rM0QfIA3Yp)Omb7X}C)+^F% zpIm~K9Z}+JZ@z84DZtC{dcXF%b+n6KQj<<-<}J76GheU zcx4Q&2Mh~zT*@UI-$W-ot9qRw73?#JowKFR=Dd6$@n9CPbZt;d_`^#hdc_s$0{}m= zZLrqo(X8~b&FMLd5v?R2c{;Uo;<4xr@1EQ3?IcWP9U0oO3U-IW0KA2sQ1!~<)zBVe zaxB0Z3^!a7W4LB#1XdA+-byqMLlfPw)Be4L7UHaqEs=8r<{cu)u=Kb$so&0b$I8|Z z4{F|xAyqf@x7m-NSe|GU`Ft=AxwUolmsG|9)82)u;%&xg{&)^^$Vy;suZ?=oG2k2NnwN#Uen_pzkaOc$l>{ur80iS2dFi? zw6uJ&Kq#sp6A&W7ced6QxXEHqFhc}1>Z6_0ygKWll#^`a64Rux144LonZXM~^C@6@z@={ae11>=?cvRV^CKEv{zy!2V|4 zD!3*UE~28ym#erzw_FT1Nr(@-R8n+p%~U1|0#emp)S{_U_lr(<%q>>195}Qz%m>q%d|@L~-X-ofj~>^; zYg60SJ~uzmKt$oN!!MLsB)25w`siEY=CJ{7GVl{8cync)9#mRmNAr@?uPg$=>M~tem_J)|#2kJJ3=8108-72RmwamUY-b~F_}evACg=i-$#zd=9p8d z5$yblT#J?nd&p6Xp<76k`V98?=k^gpf0EEdJ$cY?9j@=^bwxqt5R4=GV-)UihE1i* zmGQ-vJOreE4;#+us{U z;~jW^k#rdZv)@F&pZ4-A^_q8Wj(Elu&Hi!Cqka;r&U;?ungdIgIOh*Laf$=4Odw=U zDiV?;2J``>^glX56*Ics_FvIA$i3!a(K_6@-dY9n^XRDxh!dv$WvhD%bW0z!x#mdy zM}j2Xy>>T}8|&1fGZxkhNBl=hfzk${&%UsK>qVO~6Z5xrN!=Xu)ysCSq9KMps>gOJ z2-~O!W6=A4p{~5eIkzQsw5^f*4+w(VjnRxw3(STlO>+kyd`3uLZ%0gz#>&Sx=Tx+9 zc+8Ep!y3)d>U~!~_^!1@n7uV*oqD7cq6|r2m!PvYnqk7UyFv09CJQ7>m=D{E6YbP| z6^8bBh2^{3Lmb)_zB{6yhv20bWzco&d>p<&vT%;(<>F}@8$=~aYgP{bd}D&OtDeGT zxjgDw&hHd^j<#FtvHEJdhs#@7uxq?$)_O;>IpuR?h^AjIqOBWHuVz$@UBSz$^LsQ$ zukbh`W5R`&{9v4Q=*0*g$PR;_Ec?J#fJ3rXW)LQS3I-cw=Zs<%Eb-99q$+N5mZ;I-a~VdA zdU3Qp>~5rBFl`L+Oy^eq+N^}VVBad~j`rl1S!>W%v$W5}1M=1b_OP^O2 z3zLq91UleYGK6;Xm|_%F$sbbsR%4=xFg&h$Kka(o)4+)qD8UDlH)zSljTmQkS^Ipi z3GX0!VqUONbVtm+wu7@>fJ=|AiNUHn(I8I2A18yyslM;x$VNRn7+z|PQNcn`M7#h3 zfo!yG@V`p z*1P4A9Q%NIO~i$3qhxFJMP0owQmFGEQ0V5?{un|}R97<%^TK@d885T5wL$}Ej~&|j zM0@MdCOy;mwj}Zot$)3@8p#hhOy_PNZU;RW{M(Z;NC1N7yK=W^ANNKekTrSN6#}@} z8tUnyBFMu3WDMyyz`fyJ>o+zQ%tW5l&U}}N@55OvrLoa1sgQ_3ru@zB z$HVx6*e?$8VUjFX+=iS5o5@leL(=X;P9^c~E*bU4I4`$5xIFeJxnUfeNp_4{VGF98 z3|4^#BonR|me|u_Jz|IsGaCdQU zE`(Od_dd1%ww@=iE-p9SDgh1t zQ#o_QtRvT|`_ys0vVZLhRd7bTHN80PXQBIZrth^}J)I8a8>1@tsv^Pru%d+ekoEA& zMofe2U)L2~UjNP5_DYSR-vVW_N%`%k=a7`l%bCqiDpWNyiZE$`waWEd>)Yrpi@BA( zivAGOp4zS@I=$0J@t@poX|k0rcmJGi2g+8HF~}ZO0)i<41YCbKPA#&14!^g$?8%;N zM)8H-kh5q+d1*)zw=@a>LFI9ph#$+5Hd-4TOh4MVoA$nVKV0mq4i+y}3ucai~c}lykqe6Ye3^M*QAX2x)8lQ)#lPv{9%Gy)&=t&z`LOYVd9OYBzBO zUJuitvCxTCnV%U@ER4rn!`lnoKD)z-RNgX%5a*MT-d5=!rLGE)jPni1X^H;6N=5yP z)x3iBb>jPi&q*%H*@zQ)(TIqqE7~z*wm1Y5qTW79!|!9=R3o}VeX%!I7L)-xIeGNu zL-)=>p~A40b^#}66$rL*+=M3Ez3)p9hxdGrlvs&M9a;tR`Aflh5|tMHDjIs47%qgX zZ{a4^Vd&1mLTb&7l*`~Y;rKd*L$qD>k_4d{Mm3hkHzebNlgPp@vim5IUnHTTy)W_4 zC@yDuAfnH*X7S|4Bvo35v!m8Jt^yA}tb~9gSD1*NYL3993TSdaj{_niK}zlWEKKIV zLr%TySO$Y+v?pX%|N2l@NwOW7p)9w$;gBkat~brUGMG~q=KE1euE|3SdzAs+-?nol zBc-Rh_C~e=`9lvMStO&a?J&JS?YbxLLS;6w&#o;mLTQS9*OuB?Djnl^>`I-sG-}YY~}x`+@E*+gRNb+w3M8-{G6^^XTov$*ub?u^uYi-6**@#Jk_)vgoU_ z8I2P-J3s($mx<%8uS1$j=$<+|oi&^3&66%qS|8iHF}oLvu8}yj=?p6;l05I~pUFjX zM4DB@oCpQ?ev|4v$fNh;|C*+AR%;ZWJzlF?1#A3ZoFeV0z5j%5N%;l?TTqDgq4|~+ z^|Ye8`cHaC!7X}0`aWV3j&zT);p{DcVIyWIW@#!+qz-LD7st^tzM9M9br^Y}NNl|S zZ=q|?u^{q4?Np(L%;!J6B%6y!axbnaDfLz=)Z0ARE9E^Tf!v*Fp7L2K^3;hYuX6+Y ztUS6##wnX-4yk* ze`0s2T*U-+m8_Uo0Qd93p7h$hT0m7hPFJ3dM~fGxN&+lWU!X!sjOj}x-|1cd&HP;A z@BubOV`xDrbXu2DqF6C6QhIA!+c0hAN9=e1DfccfVPs+eWh?*7744a}&)@X5g5gBk z4nsb0aV>xTc5aNV$%`%{Bg9<(10PCttiHvi0Xg?da-$^O6M>aCEPoF$@OR^%?@5{8 zv9?Zub7#|0H@_he{Q&bR#psblP)MZox7otI(gY!&2({Tk?}*Qh8JRH)XJ%R{y)=DZ z_Kwr^$n#3hXvuIl)>`^~p$*wR^#=utPnTCt;I!jcNk&2)Ab*t^9p((2C1!TlHOX2` zSN{Y$?))N~rN*~h1L%U&ToU$Hv1-CEH3bC{N+LRwKe^Xe|Y*FfD8k zfgabq_gFC}Gi8 zP1us#^PKqx%db~q;L0E2hB`s&8z8e%HG{6U>!tPmh2Jnwd?e(UO^7sKx$}I;^q3w%yrm zR4O18wf%}=XIynn4!d(TPdZ;MT-6?!Thf44ewTtAVMc#LF%WUC?rHP`^i0n`p%IDW z|AhJj+RshZONiYEC{yq1t$1_^fWMhw7)oM(kueZx^V(n~s1yb0X}bq4NeT`qY6r_s zhcI~G!u znH!zsiV4V%88@*pV2^RTBexy?=~|o!{%R#kWg> zf`2eUCt9L^(?AB+1dNl84-X4!SJAt34qR^Q|3@r9%|Pth&@-koM`)9YAuI01U+G+{ zMfTztjnj7?Pzvg$Z82@S^kc3aO)ERk5;8=zLYlv|`xcvzMb#>vj$BEj? zoPXX3M#0Z8Uo;E}EnmeOIAlU{{8~J+AEkeq|JfcrY&)jF>zgGr)3FC|N(i^bJ4k%L z90r5<>!}Agq3T(q^urnw3!jb42wrh_YX*Ln35lvgV{!a!XcNnu|Mg0|f34zLKz#$X zyQKwHs_@y_d@X-+ijpvkKX2mnY23gG;SI00}kUShmUx|Ih6+srK(ojM*z=4V(LXwF;D=?)ud0 zh{i3vzAQJi@01!*P4)ih$tO16LG>HvLPA+4=!?QpO$cIHLsVLJ7nJGr!R@(k+gaM~ zJ_;J?6rHJ2TP`1G^FIbDYSX%Rs%nXd4W3tt$oHv*0! z@WA0r!{ejaI@a0C+wGZvVELo`^P3f9oFPG$H9!aH69vUCtOzqM{13JAO0E;%#^eR9 zs>jBPs_rK3^+pEW_Qn+5lSMTz?e>+3w#Eg~PC#`y(jR|+{4inv*w~<;wGn}Yk+sS3 z^O!UpNV=@B?DdN`zt%{mli)to!%uKO6ZDV?%JjG>>e01BZhp^phl0=+82Z}pet2{X z;^S4~d1w;Sb2}C3>TG~V|1Fgf{K@{B^Qub!qyR6!15tEuwR?R~n@GOrXKy|k1izPU zY2w`X1z8)L>Bi4xYGZIgKBwEp9FCk;%1IO3UFUM6`tH(0=k6(SO4rc23it_={f}IT^K|#8CD{=|vTPBHJRoD_lZ;Wm*aZSNQ{rhB7LWdey zq~H{s6%xmzTXnLw{7EZ#w6jJw|D}!8MvxNkwfh+T1hQaL#qNq8?Q8NyxZvQhf^XQ5 z2=#>827Z$Pe~c@0w@S?BH8pj7*MxDqUNyHS3@HD=XNr+UPI7-1*r7^bjuS8$SWpuc z)x9)ibx%!ah5Cucv!?s>(c3>}x>*!{vg++ppGk6El1)&J>616*;>JXm>6;AT`p2QU z#H5ovR>r!VbYIJD;Q`AbrZX?m?~g|He$}#DnHV>OMBH!xgm1bzo`|qW6o{~-zmR*v zlyi;_>)BKDY(OV8-nTrptlvuzgUVmsAG@_q#laE0a_e>@==zb3_n8+So$l71*XpMt zRzJUcTn@H)uHag7M@kgj_zeyv{+BSuZJts?N*t+{q>34NWY!$_gW=(xYcli73}n3^4U z`VS?(xPs#$Z3jLH9nsB_eDg*0dZbq^PLzb$&L2wL+5ISa^2~-KX(#yoo%225HP|XWCC{# z`7Q;EE8!`OasH?mbssTw$n!1Kj|mr;Y#nq>J7YM8D}7Vu2D#BXzR+up&|70ZA=ILx zU39S`N=jm6=LvqRN13KBVR3(6zj!%xx?crvLh^U11ijY%>Y2VMP+oK}mogA-pyH#J znWTVS+Htwtn4+I?@;C||>zclXc)HJKM=qZBtuKZdN|PDru>=A)-)(}+g+u!4%D?x> zLKRzoJ(KI`wJv`mH=)D3iXcHE=E877q;}CpxCPBqnE3wV&6}T-0A)@dIll2T`ts7o z_7K}r#NwVXi}s(Hn=;z!4OaxK4Y7jh-#-{7S*Y1m{gBMmQM3xu5-D>o{9ai5q(H}x zQ~Ed9<^2+)@$(1Ys@zr0>{qD+bJp5mW&Yqru~_I(i$pzx?rEZGM>G>#@sKWm^wXf zp(Xq7TlncF>O#&3A|dNxrvo2!1Wr>Sy9T4Q?^Eoe%r-=xcsrlYcCVFBy6)Gx@wSlN z&Zzj|(mL7(9`n`-|KJhGFQWT(b8sp8+;p4km9R;tU^by+Sm>^(+Y()?Gj0GLr*3o# ze*^N3o`Oj$0CZeb&QZ~g6q9sm7O8R{9eU66lw#~x)0;1vkbbg~C<+KlA+mrXYyd=*a zwb4rZNs0o(BxWW3PX)B(9TzUkh}T|p%`xHU%Qp_%Kd%(A!jAR zvN=x)vx(T6V7*(Aa$>WV9beHowKA<^aV-W4An=x-GG{h@kJ$fltMFykeER6+m`;nx zZaJaW&Fiwv#~grMJg)P(q5eeQ$@cLfQz602aPf?kT zn;o~+_kDCct|7RxH-V3iMG6l4_o4X;QouwOk{Te9gCCCHnUhZ9k;#5>v&R@%0>fg- zOGjI@0M7@*-21^Tzv0NEf(h@bOy%A?&>vwiQ}*3sy;6NCg`2o?! zh466j!hPa#TK0Lw><3!GZPH#e7F;C@?f-QTNma8$-=b^NDxksFBsV{8P@6NJ48+JT$yEABDITw>e8fi5WDR zm;b$0?@*22x#^+1n8FpR&j7jK%np7^9_=eo66vx)o%n; z`0XqqB}+_}7%op=zOe4{$gCvOp zB00ooBF8ENW4*to*DPrvTMr-WCa0CaW+eZdJKo1|*x0w<J~BY3q#a_!H)KpyONgipb2lKd%es&rIv~?L#3- zjpV4p;O;L$f&}xC9fBGWix^8O`7zISS-$kAJ;CPAEo=Kwk`8e8+oQMOo1kP!i^Ofw zYz#!DU^*9%N5t#bo7)-Op_TLF-x_S)W@BO$B2?>Fa0NRiqom!$MUF5vK}lS<*jOH3 z>)*#`qRd)c*5v)T7Mix{>90AfVRp`H^+RtVO+D({5{nUPLIH67`wrVG^L6ePNj+I1 zpziajmqS$g>)uZ(fxYR--sPdYgE)xmVKH_xMl65hH~x9J^8^}Vq`sWQv@(?n0v9#c zPTi$%j3T+6&!G=PquV#If^SkrI;&R?-bOZ{eblf{QX1WyMZWaKxVC9?cG4WHf-dBh zeP~of1F8Lo8*$qwe1`12uD=+=fZ~A*4RhF=!e6j<{gF*sy>CsUap0N6zD1_-kZf1q zM+j+1h86QZ<86#=^b_*&J!|x?Pob=P=eV@|_5xIyTUVu@Nq;%)2OEwN#G4)etswR~ zl4q@LK-$cOqubj=a_K7WJx4y*!RKugLUpEBr^qoO0RNc|Yu9NsAS%v#$g(>C_TjB$ z{*ju(Dya_4NzplFWc=(gy#EWBY|;2?vcf$XN4|H_0%TC>TUq$o8A+tyRqJF0@VQ^D z#=l%b6|}txIF<|oIt73opB=uP5(=cF*pTb!R4c$ zVj_Ce?<7D~Y{hU6R1)FtoWAPcl~?i(B_8Rs_Z*7$Zt_DM`P|81QHXvK&3C zY=966`!y_pST#NNV8s`MZ&2xJP38VGVo;O&)t$&G`Nm_~$>V;$b;5GXx}{0t+rDt)wKiOpo%I}t z3U$@p)8Ms=MNsUZyJj7_>S2MVVDQC7SSaPoh@wukiHNV!%R<-E-=qzLj3o8UmP+vx`Vfgjb}6DuJhpm6nKCZRdhzIi=LgUV4ZmEH}t(yTdZqj zw!{5o{!Su(KV{JVsOg~2dE}_+xxws{{41!Oe&}tfvZOgMMgFMO_m7X{FsjSVDWTvg z6(waD6lLb;d<+Ah=H}$nsr>j4Y(||#`6E16Gs&xmLw+t$&Kxw&;Vnz+6_iI)ha>v; z(9$zTn8x#}p^|>GzMh^s;4y@_R!cM4$mDX(7?NHb?}G!f^~~v?Y{*_kPqZq{!9jCj z`QlZ@Mw`)Dl7VAs)6#dl4lEWcpwRf&?|}(LUvPL&ZNPK(QE|Dm=nrZ; zAE}6>*e?OMw@ciKjj}yH_MG(qji?tl+6STL{6Re=(&CCk0oXhaCB1Z_d$FR!-&{p9 z^)y+1HPC3Ksbj!FiQD22oh|C$M(Rs*qs5*g(|?p|-V4QyXQ~X7*oD>9aksCy=PLkT zxEKrOkP;_MCiQ8TotNpX;`Rzk3NvI1`Z7E3%wHmr2fKyVDsyUMyHm2ddZEuMyXG@b#yG+bV@rFh25r#d zgVD4&^tL*s=j_xN6AVo`&Dbz`@DN{XoFnYon12`#v;RtbVh-M4P3J!PFy(0DLCTe^ zr9{S{tu%lmqipZW4mBp){p@_<9E!+n|CA0-F~@abp77@DG%+LErNfD$Ajt;GoQvH* z*||hOgo)VSznD-kLDbbCOZG_4(ibGrYD&bt4Cq}jCHC$d>>f`Xn1J_Z^oChqp6@e~ zL|s!m*#)_1Ii3*lukT#$cLHLWQb5S<7iD7ov5AN0{4l?NYa(<^?H5Mb{>(T&=l__j zt7!{4o`2y^(fSi|0S@2%DTsrxKR7?%cOH%3@|4ivE+nlBjGV8^Xf6)-~k+`)i(V5Wt z;mP1^Icpev9A5i;`q9G*N6D6D=9ex}*9HOhYX_%*yIB+G+1kuNBc1fUtrYTYwCUNJ zBcYWk&#`P)!C2QX*wcx_ zUJGUhjqJtI0nxEg>l!{SRI;+oK@2>yIm~@zis5NP97GfmMSp?+Zkh`Q)-;hnyg5s# z548C{imFF)%QbEYOdFXTh2uY+9U+}^Dtlpy{zYGo?5g4MD8ypjhqgZdb0I1(Me0`K zu{3WisRY=u;-n6q%2d&qk7y@KVe_7~aAT1FPU8!;a?uL; zZ~;5A)R3k~mq`3{N#2fR-Q z_k_q%xZzpz{MA?rIx7qXH6=8vQ5;v2IQZj&97AN29&?Q3SbF|kF33?Dk^n~-Q%lb< zCe7L(&}lIga^I=<&3R;;sFPYgihU z*yg94OypVs?nHY`r8S*V17=4HjMyCFo!$`|(f;e`hvbx9=x+G6uudo7w*9-Abx_~3 zkZ>ot7H>l2=Eq824h z*s!P5XI2Wy?d%v5)4|Oj=02J;2>D)qjM->|@sB%JxV{Ph)W&|me`4L_puW9i;|Pt@ zx}|;BxeS(n=R|!_0MJ#K>TLNKkyw3*A)tm7$>)gN@P&;p2KId zxt*>?fVurZeL$6(S8i^|L{e1mrw!k2F17PYz*xv`VO>i%oe%CA5u}kLe6g-I`chej zOGb%BCH+h_g;`X6uKR_&n8Qr*T*Af zmL!${MR8hR^7hGoSe6gHUb?*UK&E8-fkl9zukJF={pw&e5j818)I$KBseB|WhH{g} z^n@#h5>E(0}G^ z^5mZ1H<>6 z0S=fUC3Y|SqkjN{PS5mXE2PykaXGlslM-m_T!3p)*+KUn>6Y;PC zKfQorDw_ck+v{Iz-h+uj6x7yJ?)hbbjJ!Q|wGZ0g997P}aduC1sXN z4Zk+1<72O_+txp%%a!Pl<#|VHB1(#Yx=%+Jxm9YfP7Tdda12^R;f=1D(wcu@V~TF9Oypu0eA1 zfzyk1H%imiLStTnh$rtKu3|49CsV5|j0}v=uJ)oN3AlUmEvYVJNTR_O$GW}WhAkg1 zuii@rp1{>j^iOn1x#!s{m?fRul}v}f{m=(3XL^3iy724hL^4C8^&LemJ^5orJX*Jx z_q95%**7l*E~dE^`@y=20h279aUC%qYhLH{vpiot?zur>LJ$A4JzXTyfzS8YAuF>( z1w>k1on7N^P>_k!PtWpr>T-mKjhQJXKH38pt>O#hkcLbN8U``k`ys3c%+R8WD_?Pe z8m?RW9)yQrwp<0A-2ZT-bHBRTfpfaQC6#rCoJ0R1OCQwR-$GlcS98Ij_V@0+EcQXn zuc{-Mfjsh|!{D~muZ=HOJUK8nMs_|ftpk~IYD{wa$8P1$>z_Nw&a~1+?VK=#);B`l zt_*k1DzF`-ia*$^)HQovGjh{0WZ6|^4Hn~w%`%79xVZ>Ak?G7K@U7fGG*Lx+!T@yI z^Zzno-%CJm`f@f+t(JOB(tw>oMw; zS&!p=)fZRTVqEJvO<~#zCsf(3*UJJC+5kXdU%7CtZ6Lji+RtOMQfoRe2UK_8j---Y zbaWq$)P}Y8oCoT8+O^zIN&{B0qitSA{?o~+7MlUM_}8`kApGkz%2`;f{+8dBaCy!4 zPiaTk)ChS0joZvV_;eBXuG9&)av_~<|e zKcR7fb_@@Gim2+Nql=r3+Kc_h!(P0`Mu+`ms%l-OzUFZH7Ej*$*RlDf#K8dX`Gn@C zRICQk&8u8R9o`&6VH+aTvC9Jte%1d8Q=yJ{YV9`kx6^OVgsNb!SRJ>RdBKg@>3*F5 zI{FwAtiR_h@%(&w=MCpZv4X zsFlWYB_2AcB-Z5Bo>JEJ3`IUDvpKmJt`R<&^?(_Vf4*DXG>xd!+Pu-<6+pe_j&{Wu z^u-V%QrJIb!53#YQr9Ev1#V9ASxNLw+~PVk9*fAC{W7>D{m+6{Cgi(|ILqCt<-^_> zA|6b9Lj1$f3Rz^pxqf6}oEEN4=WpVeWDfCOp_a^K6UsD$xhZeCB%CR{z)e*Pd5Q?N zwoWbH@^^3Uig@6>d(7|uK{t}sxcHyfwRa|9OB0)f?+wgOPrm=469gQT4by94W;uFZ z`*$b`KE6*tvi~3a=3BGP%wKQ|7-L~WFUspAzyB}U!grdegJUd=Io&CbrDbbywL>=z z&ytx@j}vyuzQw=X0Q|nf<~kD+umLyEe>$==odJn2Uxa;mUR9yK7@+A7k*TDRynlX2 zFL3dKyF~+{Tkr{-{L~<>7LOS9Mzmg&qGcrS8}q;2N%Cv<^{a&ZHsOIKWN)Dd=k&yf zU&+91Vh2#G1T*Ci9E_WNb=hagl+(6hc=CEKKrXL}AhEH_AA+B}rQ)=<=tHBCz<|zT zz%W_<#GhV7Nmft%BE zGnXY^NP)K?wZz($G;3awZ)N5!v2U{*jI)-o$y+tKbvY5Q6(#Z4D(gJQNs4p6=nF@u zfjowHCr(=Yg7i@;?b&kvyS`s%d@DIPb|EPt8RS9!{v!U%*D*QsS!cHcQtcdn5>RkcLybp(%9{>46fB62BhE;28JN)-x>kDKT`<` zCaH(LwEqk4WR3s+Yw$!S$?R3zVPnO?H3I$65*&58HoUk9p60~=gY#SeH_m(F2%F=- zQyEanXzn`d*ISq3lL_h;upEPeFAK>DR&#ZwYu@p%w?tK+mBz5iZC_0U z$4%kD6$O@(l;;q}o!bk|YBL(wLWzYF=_< z!J1S2`Qc=WJ--SkTLE5X2JuFVjKp14$A8fJ?*)=%uVHl8xw*+-ttMOtC=<%dlvE8* z)bXU}{s$^r?bH_N`>-SNlK z&;KD9r2mwD(x;gB0!QtWk^HOMq>Ai*{g-+C9|%8k_r!k>2f!z0HS0apDj1W8zC^cZ z#6r^|TBd?rCRBr+osu_O7Z%KiiEL>1%d&K;Plpn|tES+Y=Wl{(g_(PE)aSBbBDVDq zM&=?1GZCudhUBZcpoKl>u}!?orRABWaf8s5dY&JIHs@)MtyHrR&w0x zuI@l)Pf1e%BN}Q;YDTtHwnIEHc4$PzieR3X+S85<=d*Fb@xnmTepkTk$&p(FGkL-w zFTq=r4N54R+Lf#(BTGzfA(@s?1AYl>bzylz&;og(O+~6E4wA#D~?5F9Z2(z@G=RvUUld zP)>qN8$4dhG#t0JvvhuDhJVAc{XcX1JtpYbsy-+W{rQn>#cK1a zLt;ein!}IKG!xpWP!VQPur<&nZP%4hz(D>!fmZk~(6(ULv)5iw};-aNU(IU&sL| z(rS27t7BhYvdLBB)}=*#svPYM`c-z*agenuAxMDU`67rYKxM1%`ynNx)0*d+F~-%a z>p1rN}7>9JFv)CG`d^8(uLr@Q(IspFm)X^qkhjGr)=y-0<$dF`xIb%?ek z+ix!^V6rMVUl{$gvL_;d1X5$PrebY@H63n$b{ZwSv2DiPT1>%A#`4cla(Apl z3(-x7-|pNWVr5qg=~-8vQb@uM~H4M8+{3DL2b% zoXzZx`wPCxHSK@KjC4M-eZ%-k^g!M#YT361h*~5b zIv9h-|8Sdn7&U5b8KXFZ+WKdezTA0*e9K zSunezQrE?#>wrjP#CrdqWW%Z_u@;L90%usScFN0C!u-QJAr5lr*YG(^m23aCK?R3ypx zz#1>L2*XR)T%#FS?$U7A+04B{(kc+Da;W-@?s7%=YV^yYI2MlG>hVDFJHtZ4m#ieS z>Ms14g$e!5^`RjLAk&cXf|h~8<`+0xjLbu<4u8-WnhMf{Gpm#jB`i08nCyret)zc0 zzo|RA<_vV*n&MZt;rRd+>JH-Tu$X!wXJl6UW2qRE`^MRnG%o6nWP%AF_$5N#SA7>w z^LG`zF0kInfi-QB(c-_xc}!8pl2Tva{kUOdKi~2;A`B{>Q?kf}2xs%nRp9bHy64tU zZLX5=?LB#avg%s=(lVjmufK2df34|fesV=ou=NQg2U?a*PF~rW>-J64$)p8_%G?P7 z7xCYPBoF^F6lF3q5h+M@J(flY{_<@6L?GR$W@SKu%enZv`h0zliexiN;K-=ln3_ZI z(7^KmCN>CF;rRERed5h~J+K8gc38!>1LfB)XEa6G1uLdH7W=ari!5V)!C#|my!V$kmrQjSWVI_x4@N0&O|45t5Z~t( zFI5Sm~SUCp8698<33=IvRrN zFxLcRac}Q;?NBy=yNq|vL45n0dD#xv4UxCq8KIuQL3c&B*GA>mbs|OAWmpAI{oyap zgg!U*L>9kXTY!!cb>?T{ZcD5jITpuSQ^~}830r9I)6-T=og#6LpPtC!$$+pm7HfRM zhom`fP$_wZb_Y4%RR}|byn){D?&ZfR6VI7%0bSV9pFK2^zfp2DBSyu`o%(E|YA4=y zclUn$T&)A!dCp?zhAJ2sFkqvQpmb4lO+NvgL1lg~cpH8Na|*8quday8YC!2aHA(t%UpY`Q%8MBwE=L&-xwji#Ykl?D&S|s!6y*ODpg@~McG#SkQI7obYj)}2W6T2S&+c; zsO2=OCu}i&45EP+lR;98z$3Ss(j88KKJ07E z16b|mV|f*OFJVg{r=#|RrENrP%mIsFPBgBAV`<&(b{<)6Hs1n~As`i=_e+J->_4u2qlZwcR*=A4SF*Ab^akJ830t3Ud-Fxu^m}T)w`M>tTza&@65WN6= z2Y-TfQg-|ZvD#5Xn;($)K5Oou5F2ejV-PxGV2EM^SuVfAFp;YedL_o$0I3vYeFe== zUeJcxE@zG}$`~*OpD`r@Kaeo8S8F=^9OjtododgWvk~Xv%WxE3HsM7w@Riwe2^620 zqx^ge5cYLsazmVvrPaYLPJccy<-WjB1w}Z4$=3sfr67-FbQxCo9=XWFjQhtC&iu)PM)nNC*tfD|Yf@q?#n_6Ngpf64HyBxF=9_uH<@&Dg`{VudUd}o9eLc@} zp65R2cRl9-w);)7u2~KDja-?Gw@iG{aR+Vxwb3kSN)+Aj2G@8~frYOTr+>K%8`o)V z=3HoMIdxJl)M@jEP>e&mJx4iK00%e&*fT^V!3#SMx(w+}G01dyPk-D({&IyF8hPm* z!Ll>Pz{{ypqaoOzZaY5qav{^q;nS@mg+=Gi-*3QvSwGcIAw>8RhxFPd9C_MhL~Yc` zK2PwQgg40<`d7b5*kZC=yziv9VcBfo1-xy^8Mjc8AX^>lOp{n4s!U~lox7GRe(z&} z7p(p@I$m!kX-N9Tf$y2(cH!x#HXz`9BeqTD+M5vW=$o}Tbr0Z1sludEw_1F+Q97zH zv!uLbY{s9pyb6Pd-@;e-6FGx9fvFa@Hrt*Zi@bdt)$QIDETj*~-8}iFE>jFg0^Ue; zs50`IE^@gWBpNO?mLyzw0eK4gIW`cCl`o7^>U5fh5deJM4?|Ro=0gTY; z1;lmM>yVSx4bdo3*Ao`T9ox`ZHSTULuZimx2AnO!eQ|@45An}B!L9EDIr801BGV+6 z_pJ#oYl|)o#i^`KL1j1y!KV8{S|rwma=zZ)y35mmK=(lczuwxsCc4U5Q8h0j5k8#~ z8As;RT98W&tWVB@_dw_MCaoGSV=x$yuaC*uWdPu4ML_Rwk7o2eE9*N?p10d+3RsYj zQYr;pc77l{tIAyz`0cPP)q#)v>NaB@gsJvSHU%CJ2%e5j3lCVPwKg9?UN)<;>+4|P zwrtKBVDe0^w$>(u(}qYnGgP zqdks~Hz}f8GhahL9vfJ8V^*>A<Azf;nRmZd*`-z$Ggjxo3$EGNII%D#AWvgf0ZX{YH6z;O7Tn6GzD7DWmrtF zoeyy=-pViJ98KfhFj{aX4*m{r&H#N0^=aC)`{|lx{wts?|5nY?`Ewz#G}zl%L!Z`S zmT*%kwXj-D#HntKs#qF%4(zZMUkgbpcec0{7@K?1b*6T#W=8@C$-}_j232bK&B>u) zo|mD*cxz7VN@Fxp{i1APyBm2E7oO|sk?!c;s@djkaOb=JZiRJ1*YNvZkN0(=`?g|AQcvKMvGC`(&q5R$JMw8c1tYIR2Fy;x$9{zTO|Wd zob=M~#bmH1r(tx4dR&bmp4w@O{J$;zT7>lsa{-$REox#b`urDIW_-4=0hGkr&7J_h zwl-7c+e&1!yjWUr+c#3{@KU8l$j-6qRLURL8f_t&+x637Kg)ua9@;f$2c)ZkXeXWU zCRIB5f%OKVjC!ZeF2@+@EHMm8pP<9qacv+~Vmktl2W<~dlPt1NBbuL?brjqvr8J6L zOnh6Iai0_g%gQLDf9IHO4*Z8u(x+ymGec9QY=6rh$xRXmAr6Ut6Ka91tnS7Z`!`dW zl6@)s1CduEXj>QU&{RwgCo@)d%=o|ny#KSeWQz$l87q*Q0Zw*r*FN?=VoGQmA8GXWN+Vdc&xr=(jL`rBP1R@Av~(Fu<< z3E@(SvZ13*WPx%Z6J9HTA#H=|nrIOKW;VSIV^yfuVz|1gZ37(kTX+li}Zd0G6v-sZD>=elh!OcRhwiE6u^n35iqJ!^-!+@h9 z{jyE=S7QkK{IreUh2WjW=Lx0i>3odqOd9RP1Dpzflc1X*Xn0m{)xjB4Ojru;tUt*i zx1`ejk`?UWL>@r~w5@MSKlCn-YyOzTwxZ!Xm1H?V$bv4N3kEKg?V(srY-u@~R42K~ zCH|>`T%O9scNqHadZUD~tyjcTkykW%y=tLZv4bPKFo*W7UY{vPNOofCJo-WO0>m=@ zPz|KnQKJgsZ_B7da3@ktK^9os4V>t|dT*G8FYgyPT+ z+ba@bR`va=urKK5PL8e04ZNHTgn5I#P4ZwI`bbxqhd!R}U!xgrCCRy0pU+wH(5B-= ze3!kXuPlo;xCkL*^9e}sws`B~f#f4f2EW{Yv6AenPzBG_X=b5atp>s0Inh+%@f{7Q zT?&VASHnF(-rgXJLYRoX?3^_U{z&G#J@ayJ=%-y9->$~zx&~fL3>zX8R+HDKRk#Ad zpS;lA4y~t2@X0{hQqCys4cu^rw4pG3jmWv!`B|&Ro}#n% zdqO`K2H}%%Y$D^v4;|=EEh^+jeyyAB-PWh)4sqs_V8DDYJ$TS zZ~pT(2*d8coD3H@s`NzgV)$&!sN1226{J6==E2brs9ks7ZMOjOz}vm0n}{w7O>BSQ z3$NDnD%zzxPWs*Cp{qXZ5OcK}3)A{e_a74)-*LZ`B%9SydOjb>t5FxV-1jO`Lcd`t_a<$&UNE?NTAOpC^@qc)mM~x zm(C{L(yixkN7ac5k{bI6(503bK%jW4O4WVJ z50Z-9v+R3M_q=0j@Iq1BTx#YrWfyUeg21w$%jO_83cygzz$NP|xDU`=PkwFHu&~;q z3ra($eWIB*bo?uzda-{DLeDuXyeLKF8hsb%C@bya`{C|kdI=qwqqj%VU!j_`XJiS4 zd?;bC0Q`pYe*>@nuW$Auq7t^ELR+Gku3cPt)j%|yYtO@t;a8mBXHtaN&EON599HJ* zc%iK@V+7{ts5MW@R3nwSG$UW^XZaV2wLmatB!ZxLhm0PRkWbx zurZ~xIZ3o(6-G*jU5;w}n_WjejMC7BVM^UiKCaG9L{_Qt?fs?Xq`VW`hrpp*l6!j# z=b8TCafX<9Hh|^fa2%qS*Dj2buMCaqz0XxA&QQz1U;Y2d@y7U-Qp5ftc;%>n{)^Q8 i-_8DQ^61~$CMvxfN=PbeEjY%khi<56s#~S)_~;*;tJ>56 literal 0 HcmV?d00001 diff --git a/addons/web/doc/images/tests3.png b/addons/web/doc/images/tests3.png new file mode 100644 index 0000000000000000000000000000000000000000..10b45eddedcbaf300d54245b85134efb2736c3cb GIT binary patch literal 37827 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 literal 0 HcmV?d00001 diff --git a/addons/web/doc/index.rst b/addons/web/doc/index.rst index 6a0d8dee2d4..a0c66be12fd 100644 --- a/addons/web/doc/index.rst +++ b/addons/web/doc/index.rst @@ -24,6 +24,8 @@ Contents: guides/client-action + testing + Indices and tables ================== diff --git a/addons/web/doc/testing.rst b/addons/web/doc/testing.rst new file mode 100644 index 00000000000..34826201dc9 --- /dev/null +++ b/addons/web/doc/testing.rst @@ -0,0 +1,479 @@ +.. 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 `_. 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:`~openerp.testing.test`, 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, test cases get 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 +scrartchpad your code can do whatever it wants:: + + // test/demo.js + test('DOM content', function (instance, $scratchpad) { + $scratchpad.html('

'); + 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. + +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 + + + + + +

+
+
+
+ +:: + + // 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 +`, and thus test cases need to be async-aware. + +This is done by returning a :js:class:`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 introduces an options object to the test case. In +this case, it's used to specify the number of assertions the test 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. + +.. 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 + ` + +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 most 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. Hander 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'}).pipe(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).pipe(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) + +Actual RPC +++++++++++ + +.. TODO:: rpc to database (implement & document) + +Testing API +----------- + +.. todo:: implement options on sections + +.. js:class:: TestOptions + + the various options which can be passed to + :js:func:`~openerp.testing.section` or + :js:func:`~openerp.testing.case` + + .. 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 + + .. todo:: implement & document setup (async?) + + .. js:attribute:: TestOptions.teardown + + .. todo:: implement & document teardown (async?) + + .. 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. + +Running through Python +---------------------- + +.. todo:: make that work and document it + +.. _qunit: http://qunitjs.com/ + +.. _qunit assertions: http://api.qunitjs.com/category/assert/ diff --git a/addons/web/http.py b/addons/web/http.py index 65630768737..b1d50597bf1 100644 --- a/addons/web/http.py +++ b/addons/web/http.py @@ -537,7 +537,7 @@ class Root(object): :rtype: ``Controller | None`` """ if l: - ps = '/' + '/'.join(l) + ps = '/' + '/'.join(filter(None, l)) meth = 'index' while ps: c = controllers_path.get(ps) diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 3cb4437d1c3..80f965a6200 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -296,14 +296,14 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty(); var fetch_db = this.rpc("/web/database/get_list", {}).pipe( function(result) { - self.db_list = result.db_list; + self.db_list = result; }, function (_, ev) { ev.preventDefault(); self.db_list = null; }); var fetch_langs = this.rpc("/web/session/get_lang_list", {}).then(function(result) { - self.lang_list = result.lang_list; + self.lang_list = result; }); return $.when(fetch_db, fetch_langs).then(self.do_render); }, diff --git a/addons/web/static/src/js/testing.js b/addons/web/static/src/js/testing.js new file mode 100644 index 00000000000..4eadc3a5415 --- /dev/null +++ b/addons/web/static/src/js/testing.js @@ -0,0 +1,179 @@ +// Test support structures and methods for OpenERP +openerp.testing = {}; +(function (testing) { + var dependencies = { + corelib: [], + coresetup: ['corelib'], + data: ['corelib', 'coresetup'], + dates: [], + formats: ['coresetup', 'dates'], + chrome: ['corelib', 'coresetup'], + views: ['corelib', 'coresetup', 'data', 'chrome'], + search: ['data', 'coresetup', 'formats'], + list: ['views', 'data'], + form: ['data', 'views', 'list', 'formats'], + list_editable: ['list', 'form', 'data'], + }; + + testing.dependencies = window['oe_all_dependencies'] || []; + testing.current_module = null; + testing.templates = { }; + testing.add_template = function (name) { + var xhr = QWeb2.Engine.prototype.get_xhr(); + xhr.open('GET', name, false); + xhr.send(null); + (testing.templates[testing.current_module] = + testing.templates[testing.current_module] || []) + .push(xhr.responseXML); + }; + /** + * Function which does not do anything + */ + testing.noop = function () { }; + /** + * Alter provided instance's ``session`` attribute to make response + * mockable: + * + * * The ``responses`` parameter can be used to provide a map of (RPC) + * paths (e.g. ``/web/view/load``) to a function returning a response + * to the query. + * * ``instance.session`` grows a ``responses`` attribute which is + * a map of the same (and is in fact initialized to the ``responses`` + * parameter if one is provided) + * + * Note that RPC requests to un-mocked URLs will be rejected with an + * error message: only explicitly specified urls will get a response. + * + * Mocked sessions will *never* perform an actual RPC connection. + * + * @param instance openerp instance being initialized + * @param {Object} [responses] + */ + testing.mockifyRPC = function (instance, responses) { + var session = instance.session; + session.responses = responses || {}; + session.rpc_function = function (url, payload) { + var fn, params; + var needle = payload.params.model + ':' + payload.params.method; + if (url.url === '/web/dataset/call_kw' + && needle in this.responses) { + fn = this.responses[needle]; + params = [ + payload.params.args || [], + payload.params.kwargs || {} + ]; + } else { + fn = this.responses[url.url]; + params = [payload]; + } + + if (!fn) { + return $.Deferred().reject({}, 'failed', + _.str.sprintf("Url %s not found in mock responses, with arguments %s", + url.url, JSON.stringify(payload.params)) + ).promise(); + } + try { + return $.when(fn.apply(null, params)).pipe(function (result) { + // Wrap for RPC layer unwrapper thingy + return {result: result}; + }); + } catch (e) { + // not sure why this looks like that + return $.Deferred().reject({}, 'failed', String(e)); + } + }; + }; + + var _load = function (instance, module, loaded) { + if (!loaded) { loaded = []; } + + var deps = dependencies[module]; + if (!deps) { throw new Error("Unknown dependencies for " + module); } + + var to_load = _.difference(deps, loaded); + while (!_.isEmpty(to_load)) { + _load(instance, to_load[0], loaded); + to_load = _.difference(deps, loaded); + } + openerp.web[module](instance); + loaded.push(module); + }; + + testing.section = function (name, body) { + QUnit.module(testing.current_module + '.' + name); + body(testing.case); + }; + testing.case = function (name, options, callback) { + if (_.isFunction(options)) { + callback = options; + options = {}; + } + + var module = testing.current_module; + var module_index = _.indexOf(testing.dependencies, module); + var module_deps = testing.dependencies.slice( + // If module not in deps (because only tests, no JS) -> indexOf + // returns -1 -> index becomes 0 -> replace with ``undefined`` so + // Array#slice returns a full copy + 0, module_index + 1 || undefined); + QUnit.test(name, function (env) { + var instance = openerp.init(module_deps); + if (_.isNumber(options.asserts)) { + expect(options.asserts) + } + + if (options.templates) { + for(var i=0; i - - - - OpenERP Web Test Suite - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - - - - - - - - diff --git a/addons/web/static/test/testing.js b/addons/web/static/test/testing.js deleted file mode 100644 index 1be8e94e74b..00000000000 --- a/addons/web/static/test/testing.js +++ /dev/null @@ -1,97 +0,0 @@ -// Test support structures and methods for OpenERP -openerp.testing = (function () { - var xhr = QWeb2.Engine.prototype.get_xhr(); - xhr.open('GET', '/web/static/src/xml/base.xml', false); - xhr.send(null); - var doc = xhr.responseXML; - - var dependencies = { - corelib: [], - coresetup: ['corelib'], - data: ['corelib', 'coresetup'], - dates: [], - formats: ['coresetup', 'dates'], - chrome: ['corelib', 'coresetup'], - views: ['corelib', 'coresetup', 'data', 'chrome'], - search: ['data', 'coresetup', 'formats'], - list: ['views', 'data'], - form: ['data', 'views', 'list', 'formats'], - list_editable: ['list', 'form', 'data'], - }; - - return { - /** - * Function which does not do anything - */ - noop: function () { }, - /** - * Loads 'base.xml' template file into qweb for the provided instance - * - * @param instance openerp instance being initialized, to load the template file in - */ - loadTemplate: function (instance) { - instance.web.qweb.add_template(doc); - }, - /** - * Alter provided instance's ``session`` attribute to make response - * mockable: - * - * * The ``responses`` parameter can be used to provide a map of (RPC) - * paths (e.g. ``/web/view/load``) to a function returning a response - * to the query. - * * ``instance.session`` grows a ``responses`` attribute which is - * a map of the same (and is in fact initialized to the ``responses`` - * parameter if one is provided) - * - * Note that RPC requests to un-mocked URLs will be rejected with an - * error message: only explicitly specified urls will get a response. - * - * Mocked sessions will *never* perform an actual RPC connection. - * - * @param instance openerp instance being initialized - * @param {Object} [responses] - */ - mockifyRPC: function (instance, responses) { - var session = instance.session; - session.responses = responses || {}; - session.rpc_function = function (url, payload) { - var fn = this.responses[url.url + ':' + payload.params.method] - || this.responses[url.url]; - - if (!fn) { - return $.Deferred().reject({}, 'failed', - _.str.sprintf("Url %s not found in mock responses, with arguments %s", - url.url, JSON.stringify(payload.params)) - ).promise(); - } - return $.when(fn(payload)); - }; - }, - /** - * Creates an openerp web instance loading the specified module after - * all of its dependencies. - * - * @param {String} module - * @returns OpenERP Web instance - */ - instanceFor: function (module) { - var instance = openerp.init([]); - this._load(instance, module); - return instance; - }, - _load: function (instance, module, loaded) { - if (!loaded) { loaded = []; } - - var deps = dependencies[module]; - if (!deps) { throw new Error("Unknown dependencies for " + module); } - - var to_load = _.difference(deps, loaded); - while (!_.isEmpty(to_load)) { - this._load(instance, to_load[0], loaded); - to_load = _.difference(deps, loaded); - } - openerp.web[module](instance); - loaded.push(module); - } - } -})(); diff --git a/addons/web_graph/static/lib/flotr2/lib/bean.js b/addons/web_graph/static/lib/flotr2/lib/bean.js index 6e6e3ef4eb5..1a854771006 100644 --- a/addons/web_graph/static/lib/flotr2/lib/bean.js +++ b/addons/web_graph/static/lib/flotr2/lib/bean.js @@ -9,9 +9,7 @@ */ /*global module:true, define:true*/ !function (name, context, definition) { - if (typeof module !== 'undefined') module.exports = definition(name, context); - else if (typeof define === 'function' && typeof define.amd === 'object') define(definition); - else context[name] = definition(name, context); + context[name] = definition(name, context); }('bean', this, function (name, context) { var win = window , old = context[name] diff --git a/addons/web_tests_demo/__init__.py b/addons/web_tests_demo/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/addons/web_tests_demo/__openerp__.py b/addons/web_tests_demo/__openerp__.py new file mode 100644 index 00000000000..7404ef6b4fc --- /dev/null +++ b/addons/web_tests_demo/__openerp__.py @@ -0,0 +1,8 @@ +{ + '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'], +} diff --git a/addons/web_tests_demo/static/src/js/demo.js b/addons/web_tests_demo/static/src/js/demo.js new file mode 100644 index 00000000000..b35b44b80f8 --- /dev/null +++ b/addons/web_tests_demo/static/src/js/demo.js @@ -0,0 +1,11 @@ +// static/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; + } + }) + }; +}; diff --git a/addons/web_tests_demo/static/src/xml/demo.xml b/addons/web_tests_demo/static/src/xml/demo.xml new file mode 100644 index 00000000000..1bd3862d70e --- /dev/null +++ b/addons/web_tests_demo/static/src/xml/demo.xml @@ -0,0 +1,7 @@ + +
+ +

+
+
+
diff --git a/addons/web_tests_demo/static/test/demo.js b/addons/web_tests_demo/static/test/demo.js new file mode 100644 index 00000000000..0bf5a1a58b6 --- /dev/null +++ b/addons/web_tests_demo/static/test/demo.js @@ -0,0 +1,87 @@ +openerp.testing.section('basic section', function (test) { + test('my first test', function () { + ok(true, "this test has run"); + }); + 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"); + }); + test('DOM content', function (instance, $scratchpad) { + $scratchpad.html('
ok
'); + 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"); + }); + + 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']); + }); + + test('asynchronous', { + asserts: 1 + }, function () { + var d = $.Deferred(); + setTimeout(function () { + ok(true); + d.resolve(); + }, 100); + return d; + }); + test('unfail rejection', { + asserts: 1, + fail_on_rejection: false + }, function () { + var d = $.Deferred(); + setTimeout(function () { + ok(true); + d.reject(); + }, 100); + return d; + }); + + test('XML-RPC', {rpc: 'mock', asserts: 3}, function (instance, $s, mock) { + mock('people.famous:name_search', function (args, kwargs) { + strictEqual(kwargs.name, 'bob'); + return [ + [1, "Microsoft Bob"], + [2, "Bob the Builder"], + [3, "Silent Bob"] + ]; + }); + return new instance.web.Model('people.famous') + .call('name_search', {name: 'bob'}).pipe(function (result) { + strictEqual(result.length, 3, "shoud return 3 people"); + strictEqual(result[0][1], "Microsoft Bob", + "the most famous bob should be Microsoft Bob"); + }); + }); + 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).pipe(function () { + ok(fetched_dbs, "should have fetched databases"); + ok(fetched_langs, "should have fetched languages"); + deepEqual(dbm.db_list, ['foo', 'bar', 'baz']); + }); + }); +}); From 04282ff00e8fa70c3eb0e9a48ba0cd32aa0e1cd7 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 26 Oct 2012 10:45:43 +0200 Subject: [PATCH 004/125] [IMP] port search test to new framework, add handling of web.{submodule} dependencies bzr revid: xmo@openerp.com-20121026084543-fobkc3ta5q2jc3q6 --- addons/web/doc/testing.rst | 35 +- addons/web/static/src/js/boot.js | 10 +- addons/web/static/src/js/testing.js | 111 +++-- addons/web/static/src/js/view_list.js | 2 +- addons/web/static/src/js/views.js | 3 +- addons/web/static/test/search.js | 655 +++++++++++--------------- 6 files changed, 395 insertions(+), 421 deletions(-) diff --git a/addons/web/doc/testing.rst b/addons/web/doc/testing.rst index 34826201dc9..582960846e4 100644 --- a/addons/web/doc/testing.rst +++ b/addons/web/doc/testing.rst @@ -417,26 +417,49 @@ Actual RPC Testing API ----------- -.. todo:: implement options on sections +.. js:function:: openerp.testing.section(name[, options], body) + + :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) + + :param String name: + :param TestOptions options: + :param callback: + :type callback: Function> .. js:class:: TestOptions the various options which can be passed to :js:func:`~openerp.testing.section` or - :js:func:`~openerp.testing.case` + :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 - .. todo:: implement & document setup (async?) + 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 - .. todo:: implement & document teardown (async?) + 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 diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js index 05ce709e80a..631a7f8c6f5 100644 --- a/addons/web/static/src/js/boot.js +++ b/addons/web/static/src/js/boot.js @@ -19,12 +19,14 @@ /** * OpenERP instance constructor * - * @param {Array} modules list of modules to initialize + * @param {Array|String} modules list of modules to initialize */ init: function(modules) { - // By default only web will be loaded, the rest will be by loaded - // by openerp.web.Session on the first session_authenticate - modules = _.union(['web'], modules || []); + if (modules === "fuck your shit, don't load anything you cunt") { + modules = []; + } else { + modules = _.union(['web'], modules || []); + } var new_instance = { // links to the global openerp _openerp: openerp, diff --git a/addons/web/static/src/js/testing.js b/addons/web/static/src/js/testing.js index 4eadc3a5415..e4f327bf0c3 100644 --- a/addons/web/static/src/js/testing.js +++ b/addons/web/static/src/js/testing.js @@ -85,23 +85,17 @@ openerp.testing = {}; }; }; - var _load = function (instance, module, loaded) { - if (!loaded) { loaded = []; } - - var deps = dependencies[module]; - if (!deps) { throw new Error("Unknown dependencies for " + module); } - - var to_load = _.difference(deps, loaded); - while (!_.isEmpty(to_load)) { - _load(instance, to_load[0], loaded); - to_load = _.difference(deps, loaded); + testing.section = function (name, options, body) { + if (_.isFunction(options)) { + body = options; + options = {}; } - openerp.web[module](instance); - loaded.push(module); - }; + _.defaults(options, { + setup: testing.noop, + teardown: testing.noop + }); - testing.section = function (name, body) { - QUnit.module(testing.current_module + '.' + name); + QUnit.module(testing.current_module + '.' + name, {_oe: options}); body(testing.case); }; testing.case = function (name, options, callback) { @@ -109,6 +103,10 @@ openerp.testing = {}; callback = options; options = {}; } + _.defaults(options, { + setup: testing.noop, + teardown: testing.noop + }); var module = testing.current_module; var module_index = _.indexOf(testing.dependencies, module); @@ -117,13 +115,66 @@ openerp.testing = {}; // returns -1 -> index becomes 0 -> replace with ``undefined`` so // Array#slice returns a full copy 0, module_index + 1 || undefined); - QUnit.test(name, function (env) { - var instance = openerp.init(module_deps); - if (_.isNumber(options.asserts)) { - expect(options.asserts) + QUnit.test(name, function () { + // module testing environment + var self = this; + var opts = _.defaults({ + // section setup + // case setup + // test + // case teardown + // section teardown + setup: function () { + if (self._oe.setup.apply(null, arguments)) { + throw new Error("Asynchronous setup not implemented"); + } + if (options.setup.apply(null, arguments)) { + throw new Error("Asynchronous setup not implemented"); + } + }, + teardown: function () { + if (options.teardown.apply(null, arguments)) { + throw new Error("Asynchronous teardown not implemented"); + } + if (self._oe.teardown(null, arguments)) { + throw new Error("Asynchronous teardown not implemented"); + } + } + }, options, this._oe); + + var instance; + if (!opts.dependencies) { + instance = openerp.init(module_deps); + } else { + // empty-but-specified dependencies actually allow running + // without loading any module into the instance + + // TODO: clean up this mess + var d = opts.dependencies.slice(); + var di = 0; + while (di < d.length) { + var m = /^web\.(\w+)$/.exec(d[di]); + if (m) { + d[di] = m[1]; + } + d.splice.apply(d, [di+1, 0].concat( + _(dependencies[d[di]]).reverse())); + ++di; + } + + instance = openerp.init("fuck your shit, don't load anything you cunt"); + _(d).chain() + .reverse() + .uniq() + .each(function (module) { + openerp.web[module](instance); + }); + } + if (_.isNumber(opts.asserts)) { + expect(opts.asserts); } - if (options.templates) { + if (opts.templates) { for(var i=0; i Date: Fri, 26 Oct 2012 11:21:44 +0200 Subject: [PATCH 005/125] [IMP] port basic/existing tests to new framework, apart from editor stuff bzr revid: xmo@openerp.com-20121026092144-0arxx2v9oi28e3d6 --- addons/web/static/src/js/testing.js | 3 + addons/web/static/test/Widget.js | 158 +++++++++++-------- addons/web/static/test/class.js | 67 ++++---- addons/web/static/test/evals.js | 39 ++--- addons/web/static/test/form.js | 40 ++--- addons/web/static/test/formats.js | 199 +++++++++++------------- addons/web/static/test/list-utils.js | 223 +++++++++++---------------- addons/web/static/test/registry.js | 77 +++++---- addons/web/static/test/rpc.js | 48 +++--- 9 files changed, 395 insertions(+), 459 deletions(-) diff --git a/addons/web/static/src/js/testing.js b/addons/web/static/src/js/testing.js index e4f327bf0c3..45728902e7f 100644 --- a/addons/web/static/src/js/testing.js +++ b/addons/web/static/src/js/testing.js @@ -151,6 +151,9 @@ openerp.testing = {}; // TODO: clean up this mess var d = opts.dependencies.slice(); + // dependencies list should be in deps order, reverse to make + // loading order from last + d.reverse(); var di = 0; while (di < d.length) { var m = /^web\.(\w+)$/.exec(d[di]); diff --git a/addons/web/static/test/Widget.js b/addons/web/static/test/Widget.js index 3e6cc262565..44b7cfe0dfa 100644 --- a/addons/web/static/test/Widget.js +++ b/addons/web/static/test/Widget.js @@ -1,32 +1,7 @@ -$(document).ready(function () { - var $fix = $('#qunit-fixture'); - var mod = { - setup: function () { - instance = window.openerp.init([]); - window.openerp.web.corelib(instance); - - instance.web.qweb = new QWeb2.Engine(); - instance.web.qweb.add_template( - '' + - '' + - '
    ' + - '
  1. ' + - '' + - '' + - '
  2. ' + - '
' + - '
' + - '' + - '

' + - '
' + - '
'); - } - }; - var instance; - - module('Widget.proxy', mod); - test('(String)', function () { +openerp.testing.section('Widget.proxy', { + dependencies: ['web.corelib'] +}, function (test) { + test('(String)', function (instance) { var W = instance.web.Widget.extend({ exec: function () { this.executed = true; @@ -37,7 +12,7 @@ $(document).ready(function () { fn(); ok(w.executed, 'should execute the named method in the right context'); }); - test('(String)(*args)', function () { + test('(String)(*args)', function (instance) { var W = instance.web.Widget.extend({ exec: function (arg) { this.executed = arg; @@ -49,7 +24,7 @@ $(document).ready(function () { ok(w.executed, "should execute the named method in the right context"); equal(w.executed, 42, "should be passed the proxy's arguments"); }); - test('(String), include', function () { + test('(String), include', function (instance) { // the proxy function should handle methods being changed on the class // and should always proxy "by name", to the most recent one var W = instance.web.Widget.extend({ @@ -67,23 +42,43 @@ $(document).ready(function () { equal(w.executed, 2, "should be lazily resolved"); }); - test('(Function)', function () { + test('(Function)', function (instance) { var w = new (instance.web.Widget.extend({ })); var fn = w.proxy(function () { this.executed = true; }); fn(); ok(w.executed, "should set the function's context (like Function#bind)"); }); - test('(Function)(*args)', function () { + test('(Function)(*args)', function (instance) { var w = new (instance.web.Widget.extend({ })); var fn = w.proxy(function (arg) { this.executed = arg; }); fn(42); equal(w.executed, 42, "should be passed the proxy's arguments"); }); - - module('Widget.renderElement', mod); - test('no template, default', function () { +}); +openerp.testing.section('Widget.renderElement', { + dependencies: ['web.corelib'], + setup: function (instance) { + instance.web.qweb = new QWeb2.Engine(); + instance.web.qweb.add_template( + '' + + '' + + '
    ' + + '
  1. ' + + '' + + '' + + '
  2. ' + + '
' + + '
' + + '' + + '

' + + '
' + + '
'); + } +}, function (test) { + test('no template, default', function (instance) { var w = new (instance.web.Widget.extend({ })); var $original = w.$el; @@ -98,7 +93,7 @@ $(document).ready(function () { equal(w.el.attributes.length, 0, "should not have generated any attribute"); ok(_.isEmpty(w.$el.html(), "should not have generated any content")); }); - test('no template, custom tag', function () { + test('no template, custom tag', function (instance) { var w = new (instance.web.Widget.extend({ tagName: 'ul' })); @@ -106,7 +101,7 @@ $(document).ready(function () { equal(w.el.nodeName, 'UL', "should have generated the custom element tag"); }); - test('no template, @id', function () { + test('no template, @id', function (instance) { var w = new (instance.web.Widget.extend({ id: 'foo' })); @@ -116,7 +111,7 @@ $(document).ready(function () { equal(w.$el.attr('id'), 'foo', "should have generated the id attribute"); equal(w.el.id, 'foo', "should also be available via property"); }); - test('no template, @className', function () { + test('no template, @className', function (instance) { var w = new (instance.web.Widget.extend({ className: 'oe_some_class' })); @@ -125,7 +120,7 @@ $(document).ready(function () { equal(w.el.className, 'oe_some_class', "should have the right property"); equal(w.$el.attr('class'), 'oe_some_class', "should have the right attribute"); }); - test('no template, bunch of attributes', function () { + test('no template, bunch of attributes', function (instance) { var w = new (instance.web.Widget.extend({ attributes: { 'id': 'some_id', @@ -152,7 +147,7 @@ $(document).ready(function () { equal(w.$el.attr('spoiler'), 'snape kills dumbledore'); }); - test('template', function () { + test('template', function (instance) { var w = new (instance.web.Widget.extend({ template: 'test.widget.template' })); @@ -162,9 +157,41 @@ $(document).ready(function () { equal(w.$el.children().length, 5); equal(w.el.textContent, '01234'); }); - - module('Widget.$', mod); - test('basic-alias', function () { + test('repeated', { asserts: 4 }, function (instance, $fix) { + var w = new (instance.web.Widget.extend({ + template: 'test.widget.template-value' + })); + w.value = 42; + return w.appendTo($fix) + .done(function () { + equal($fix.find('p').text(), '42', "DOM fixture should contain initial value"); + equal(w.$el.text(), '42', "should set initial value"); + w.value = 36; + w.renderElement(); + equal($fix.find('p').text(), '36', "DOM fixture should use new value"); + equal(w.$el.text(), '36', "should set new value"); + }); + }); +}); +openerp.testing.section('Widget.$', { + dependencies: ['web.corelib'], + setup: function (instance) { + instance.web.qweb = new QWeb2.Engine(); + instance.web.qweb.add_template( + '' + + '' + + '
    ' + + '
  1. ' + + '' + + '' + + '
  2. ' + + '
' + + '
' + + '
'); + } +}, function (test) { + test('basic-alias', function (instance) { var w = new (instance.web.Widget.extend({ template: 'test.widget.template' })); @@ -173,9 +200,26 @@ $(document).ready(function () { ok(w.$('li:eq(3)').is(w.$el.find('li:eq(3)')), "should do the same thing as calling find on the widget root"); }); - - module('Widget.events', mod); - test('delegate', function () { +}); +openerp.testing.section('Widget.events', { + dependencies: ['web.corelib'], + setup: function (instance) { + instance.web.qweb = new QWeb2.Engine(); + instance.web.qweb.add_template( + '' + + '' + + '
    ' + + '
  1. ' + + '' + + '' + + '
  2. ' + + '
' + + '
' + + '
'); + } +}, function (test) { + test('delegate', function (instance) { var a = []; var w = new (instance.web.Widget.extend({ template: 'test.widget.template', @@ -199,7 +243,7 @@ $(document).ready(function () { ok(a[i], "should pass test " + i); } }); - test('undelegate', function () { + test('undelegate', function (instance) { var clicked = false, newclicked = false; var w = new (instance.web.Widget.extend({ template: 'test.widget.template', @@ -218,22 +262,4 @@ $(document).ready(function () { ok(!clicked, "undelegate should unbind events delegated"); ok(newclicked, "undelegate should only unbind events it created"); }); - - module('Widget.renderElement', mod); - asyncTest('repeated', 4, function () { - var w = new (instance.web.Widget.extend({ - template: 'test.widget.template-value' - })); - w.value = 42; - w.appendTo($fix) - .always(start) - .done(function () { - equal($fix.find('p').text(), '42', "DOM fixture should contain initial value"); - equal(w.$el.text(), '42', "should set initial value"); - w.value = 36; - w.renderElement(); - equal($fix.find('p').text(), '36', "DOM fixture should use new value"); - equal(w.$el.text(), '36', "should set new value"); - }); - }); }); diff --git a/addons/web/static/test/class.js b/addons/web/static/test/class.js index faad4421e31..bb3fb7e12b9 100644 --- a/addons/web/static/test/class.js +++ b/addons/web/static/test/class.js @@ -1,30 +1,25 @@ -$(document).ready(function () { - var openerp; - module('web-class', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - } - }); - test('Basic class creation', function () { - var C = openerp.web.Class.extend({ +openerp.testing.section('class', { + dependencies: ['web.corelib'] +}, function (test) { + test('Basic class creation', function (instance) { + var C = instance.web.Class.extend({ foo: function () { return this.somevar; } }); - var instance = new C(); - instance.somevar = 3; + var i = new C(); + i.somevar = 3; - ok(instance instanceof C); - strictEqual(instance.foo(), 3); + ok(i instanceof C); + strictEqual(i.foo(), 3); }); - test('Class initialization', function () { - var C1 = openerp.web.Class.extend({ + test('Class initialization', function (instance) { + var C1 = instance.web.Class.extend({ init: function () { this.foo = 3; } }); - var C2 = openerp.web.Class.extend({ + var C2 = instance.web.Class.extend({ init: function (arg) { this.foo = arg; } @@ -36,8 +31,8 @@ $(document).ready(function () { strictEqual(i1.foo, 3); strictEqual(i2.foo, 42); }); - test('Inheritance', function () { - var C0 = openerp.web.Class.extend({ + test('Inheritance', function (instance) { + var C0 = instance.web.Class.extend({ foo: function () { return 1; } @@ -57,8 +52,8 @@ $(document).ready(function () { strictEqual(new C1().foo(), 2); strictEqual(new C2().foo(), 3); }); - test('In-place extension', function () { - var C0 = openerp.web.Class.extend({ + test('In-place extension', function (instance) { + var C0 = instance.web.Class.extend({ foo: function () { return 3; }, @@ -83,8 +78,8 @@ $(document).ready(function () { strictEqual(new C0().foo(), 5); strictEqual(new C0().qux(), 5); }); - test('In-place extension and inheritance', function () { - var C0 = openerp.web.Class.extend({ + test('In-place extension and inheritance', function (instance) { + var C0 = instance.web.Class.extend({ foo: function () { return 1; }, bar: function () { return 1; } }); @@ -101,24 +96,24 @@ $(document).ready(function () { strictEqual(new C1().foo(), 4); strictEqual(new C1().bar(), 2); }); - test('In-place extensions alter existing instances', function () { - var C0 = openerp.web.Class.extend({ + test('In-place extensions alter existing instances', function (instance) { + var C0 = instance.web.Class.extend({ foo: function () { return 1; }, bar: function () { return 1; } }); - var instance = new C0(); - strictEqual(instance.foo(), 1); - strictEqual(instance.bar(), 1); + var i = new C0(); + strictEqual(i.foo(), 1); + strictEqual(i.bar(), 1); C0.include({ foo: function () { return 2; }, bar: function () { return 2 + this._super(); } }); - strictEqual(instance.foo(), 2); - strictEqual(instance.bar(), 3); + strictEqual(i.foo(), 2); + strictEqual(i.bar(), 3); }); - test('In-place extension of subclassed types', function () { - var C0 = openerp.web.Class.extend({ + test('In-place extension of subclassed types', function (instance) { + var C0 = instance.web.Class.extend({ foo: function () { return 1; }, bar: function () { return 1; } }); @@ -126,13 +121,13 @@ $(document).ready(function () { foo: function () { return 1 + this._super(); }, bar: function () { return 1 + this._super(); } }); - var instance = new C1(); - strictEqual(instance.foo(), 2); + var i = new C1(); + strictEqual(i.foo(), 2); C0.include({ foo: function () { return 2; }, bar: function () { return 2 + this._super(); } }); - strictEqual(instance.foo(), 3); - strictEqual(instance.bar(), 4); + strictEqual(i.foo(), 3); + strictEqual(i.bar(), 4); }); }); diff --git a/addons/web/static/test/evals.js b/addons/web/static/test/evals.js index 757d98c5bcc..6eec615b199 100644 --- a/addons/web/static/test/evals.js +++ b/addons/web/static/test/evals.js @@ -1,18 +1,11 @@ -$(document).ready(function () { - var openerp; - - module("eval.contexts", { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - } - }); - test('context_sequences', function () { +openerp.testing.section('eval.contexts', { + dependencies: ['web.coresetup'] +}, function (test) { + test('context_sequences', function (instance) { // Context n should have base evaluation context + all of contexts // 0..n-1 in its own evaluation context var active_id = 4; - var result = openerp.session.test_eval_contexts([ + var result = instance.session.test_eval_contexts([ { "__contexts": [ { @@ -55,8 +48,8 @@ $(document).ready(function () { record_id: active_id }); }); - test('non-literal_eval_contexts', function () { - var result = openerp.session.test_eval_contexts([{ + test('non-literal_eval_contexts', function (instance) { + var result = instance.session.test_eval_contexts([{ "__ref": "compound_context", "__contexts": [ {"__ref": "context", "__debug": "{'type':parent.type}", @@ -133,17 +126,15 @@ $(document).ready(function () { }]); deepEqual(result, {type: 'out_invoice'}); }); - module('eval.domains', { - setup: function () { - openerp = window.openerp.testing.instanceFor('coresetup'); - window.openerp.web.dates(openerp); - } - }); - test('current_date', function () { - var current_date = openerp.web.date_to_str(new Date()); - var result = openerp.session.test_eval_domains( +}); +openerp.testing.section('eval.contexts', { + dependencies: ['web.coresetup', 'web.dates'] +}, function (test) { + test('current_date', function (instance) { + var current_date = instance.web.date_to_str(new Date()); + var result = instance.session.test_eval_domains( [[],{"__ref":"domain","__debug":"[('name','>=',current_date),('name','<=',current_date)]","__id":"5dedcfc96648"}], - openerp.session.test_eval_get_context()); + instance.session.test_eval_get_context()); deepEqual(result, [ ['name', '>=', current_date], ['name', '<=', current_date] diff --git a/addons/web/static/test/form.js b/addons/web/static/test/form.js index aace926a96a..0c8107ce9bc 100644 --- a/addons/web/static/test/form.js +++ b/addons/web/static/test/form.js @@ -1,33 +1,21 @@ -$(document).ready(function () { - var openerp; - module("form.widget", { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.chrome(openerp); - // views loader stuff - window.openerp.web.data(openerp); - window.openerp.web.views(openerp); - window.openerp.web.list(openerp); - window.openerp.web.form(openerp); - } - }); - test("compute_domain", function () { +openerp.testing.section('compute_domain', { + dependencies: ['web.form'] +}, function (test) { + test("basic", function (instance) { var fields = { 'a': {value: 3}, 'group_method': {value: 'line'}, 'select1': {value: 'day'}, 'rrule_type': {value: 'monthly'} }; - ok(openerp.web.form.compute_domain( + ok(instance.web.form.compute_domain( [['a', '=', 3]], fields)); - ok(openerp.web.form.compute_domain( + ok(instance.web.form.compute_domain( [['group_method','!=','count']], fields)); - ok(openerp.web.form.compute_domain( + ok(instance.web.form.compute_domain( [['select1','=','day'], ['rrule_type','=','monthly']], fields)); }); - test("compute_domain or", function () { + test("or", function (instance) { var web = { 'section_id': {value: null}, 'user_id': {value: null}, @@ -38,22 +26,22 @@ $(document).ready(function () { '|', ['user_id','=',3], ['member_ids', 'in', [3]]]; - ok(openerp.web.form.compute_domain(domain, _.extend( + ok(instance.web.form.compute_domain(domain, _.extend( {}, web, {'section_id': {value: 42}}))); - ok(openerp.web.form.compute_domain(domain, _.extend( + ok(instance.web.form.compute_domain(domain, _.extend( {}, web, {'user_id': {value: 3}}))); - ok(openerp.web.form.compute_domain(domain, _.extend( + ok(instance.web.form.compute_domain(domain, _.extend( {}, web, {'member_ids': {value: 3}}))); }); - test("compute_domain not", function () { + test("not", function (instance) { var fields = { 'a': {value: 5}, 'group_method': {value: 'line'} }; - ok(openerp.web.form.compute_domain( + ok(instance.web.form.compute_domain( ['!', ['a', '=', 3]], fields)); - ok(openerp.web.form.compute_domain( + ok(instance.web.form.compute_domain( ['!', ['group_method','=','count']], fields)); }); }); diff --git a/addons/web/static/test/formats.js b/addons/web/static/test/formats.js index 4fe07ff0a9c..4536501d71d 100644 --- a/addons/web/static/test/formats.js +++ b/addons/web/static/test/formats.js @@ -1,16 +1,8 @@ -$(document).ready(function () { - var openerp; - - module('server-formats', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.dates(openerp); - } - }); - test('Parse server datetime', function () { - var date = openerp.web.str_to_datetime("2009-05-04 12:34:23"); +openerp.testing.section('server-formats', { + dependencies: ['web.coresetup', 'web.dates'] +}, function (test) { + test('Parse server datetime', function (instance) { + var date = instance.web.str_to_datetime("2009-05-04 12:34:23"); deepEqual( [date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()], @@ -20,92 +12,86 @@ $(document).ready(function () { date.getHours(), date.getMinutes(), date.getSeconds()], [2009, 5 - 1, 4, 12 - (date.getTimezoneOffset() / 60), 34, 23]); - var date2 = openerp.web.str_to_datetime('2011-12-10 00:00:00'); + var date2 = instance.web.str_to_datetime('2011-12-10 00:00:00'); deepEqual( [date2.getUTCFullYear(), date2.getUTCMonth(), date2.getUTCDate(), date2.getUTCHours(), date2.getUTCMinutes(), date2.getUTCSeconds()], [2011, 12 - 1, 10, 0, 0, 0]); }); - test('Parse server date', function () { - var date = openerp.web.str_to_date("2009-05-04"); + test('Parse server date', function (instance) { + var date = instance.web.str_to_date("2009-05-04"); deepEqual( [date.getFullYear(), date.getMonth(), date.getDate()], [2009, 5 - 1, 4]); }); - test('Parse server time', function () { - var date = openerp.web.str_to_time("12:34:23"); + test('Parse server time', function (instance) { + var date = instance.web.str_to_time("12:34:23"); deepEqual( [date.getHours(), date.getMinutes(), date.getSeconds()], [12, 34, 23]); }); - - module('web-formats', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.dates(openerp); - window.openerp.web.formats(openerp); - } - }); - test("format_datetime", function () { - var date = openerp.web.str_to_datetime("2009-05-04 12:34:23"); - var str = openerp.web.format_value(date, {type:"datetime"}); +}); +openerp.testing.section('web-formats', { + dependencies: ['web.formats'] +}, function (test) { + test("format_datetime", function (instance) { + var date = instance.web.str_to_datetime("2009-05-04 12:34:23"); + var str = instance.web.format_value(date, {type:"datetime"}); equal(str, date.toString("MM/dd/yyyy HH:mm:ss")); }); - test("format_date", function () { - var date = openerp.web.str_to_datetime("2009-05-04 12:34:23"); - var str = openerp.web.format_value(date, {type:"date"}); + test("format_date", function (instance) { + var date = instance.web.str_to_datetime("2009-05-04 12:34:23"); + var str = instance.web.format_value(date, {type:"date"}); equal(str, date.toString("MM/dd/yyyy")); }); - test("format_time", function () { - var date = openerp.web.str_to_datetime("2009-05-04 12:34:23"); - var str = openerp.web.format_value(date, {type:"time"}); + test("format_time", function (instance) { + var date = instance.web.str_to_datetime("2009-05-04 12:34:23"); + var str = instance.web.format_value(date, {type:"time"}); equal(str, date.toString("HH:mm:ss")); }); - test("format_float_time", function () { + test("format_float_time", function (instance) { strictEqual( - openerp.web.format_value(1.0, {type:'float', widget:'float_time'}), + instance.web.format_value(1.0, {type:'float', widget:'float_time'}), '01:00'); strictEqual( - openerp.web.format_value(0.9853, {type:'float', widget:'float_time'}), + instance.web.format_value(0.9853, {type:'float', widget:'float_time'}), '00:59'); strictEqual( - openerp.web.format_value(0.0085, {type:'float', widget:'float_time'}), + instance.web.format_value(0.0085, {type:'float', widget:'float_time'}), '00:01'); strictEqual( - openerp.web.format_value(-1.0, {type:'float', widget:'float_time'}), + instance.web.format_value(-1.0, {type:'float', widget:'float_time'}), '-01:00'); strictEqual( - openerp.web.format_value(-0.9853, {type:'float', widget:'float_time'}), + instance.web.format_value(-0.9853, {type:'float', widget:'float_time'}), '-00:59'); strictEqual( - openerp.web.format_value(-0.0085, {type:'float', widget:'float_time'}), + instance.web.format_value(-0.0085, {type:'float', widget:'float_time'}), '-00:01'); }); - test("format_float", function () { + test("format_float", function (instance) { var fl = 12.1234; - var str = openerp.web.format_value(fl, {type:"float"}); + var str = instance.web.format_value(fl, {type:"float"}); equal(str, "12.12"); - equal(openerp.web.format_value(12.02, {type: 'float'}), + equal(instance.web.format_value(12.02, {type: 'float'}), '12.02'); - equal(openerp.web.format_value(0.0002, {type: 'float', digits: [1, 3]}), + equal(instance.web.format_value(0.0002, {type: 'float', digits: [1, 3]}), '0.000'); - equal(openerp.web.format_value(0.0002, {type: 'float', digits: [1, 4]}), + equal(instance.web.format_value(0.0002, {type: 'float', digits: [1, 4]}), '0.0002'); - equal(openerp.web.format_value(0.0002, {type: 'float', digits: [1, 6]}), + equal(instance.web.format_value(0.0002, {type: 'float', digits: [1, 6]}), '0.000200'); - equal(openerp.web.format_value(1, {type: 'float', digits: [1, 6]}), + equal(instance.web.format_value(1, {type: 'float', digits: [1, 6]}), '1.000000'); - equal(openerp.web.format_value(1, {type: 'float'}), + equal(instance.web.format_value(1, {type: 'float'}), '1.00'); - equal(openerp.web.format_value(-11.25, {type: 'float'}), + equal(instance.web.format_value(-11.25, {type: 'float'}), "-11.25"); - openerp.web._t.database.parameters.grouping = [1, 2, -1]; - equal(openerp.web.format_value(1111111.25, {type: 'float'}), + instance.web._t.database.parameters.grouping = [1, 2, -1]; + equal(instance.web.format_value(1111111.25, {type: 'float'}), "1111,11,1.25"); - openerp.web._t.database.parameters.grouping = [1, 0]; - equal(openerp.web.format_value(-11.25, {type: 'float'}), + instance.web._t.database.parameters.grouping = [1, 0]; + equal(instance.web.format_value(-11.25, {type: 'float'}), "-1,1.25"); }); // test("parse_datetime", function () { @@ -123,29 +109,29 @@ $(document).ready(function () { // var res = openerp.web.parse_value(val.toString("HH:mm:ss"), {type:"time"}); // equal(val.toString("HH:mm:ss"), res.toString("HH:mm:ss")); // }); - test('parse_integer', function () { - var val = openerp.web.parse_value('123,456', {type: 'integer'}); + test('parse_integer', function (instance) { + var val = instance.web.parse_value('123,456', {type: 'integer'}); equal(val, 123456); - openerp.web._t.database.parameters.thousands_sep = '|'; - var val2 = openerp.web.parse_value('123|456', {type: 'integer'}); + instance.web._t.database.parameters.thousands_sep = '|'; + var val2 = instance.web.parse_value('123|456', {type: 'integer'}); equal(val2, 123456); }); - test("parse_float", function () { + test("parse_float", function (instance) { var str = "134,112.1234"; - var val = openerp.web.parse_value(str, {type:"float"}); + var val = instance.web.parse_value(str, {type:"float"}); equal(val, 134112.1234); var str = "-134,112.1234"; - var val = openerp.web.parse_value(str, {type:"float"}); + var val = instance.web.parse_value(str, {type:"float"}); equal(val, -134112.1234); - _.extend(openerp.web._t.database.parameters, { + _.extend(instance.web._t.database.parameters, { decimal_point: ',', thousands_sep: '.' }); - var val3 = openerp.web.parse_value('123.456,789', {type: 'float'}); + var val3 = instance.web.parse_value('123.456,789', {type: 'float'}); equal(val3, 123456.789); }); - test('intersperse', function () { - var g = openerp.web.intersperse; + test('intersperse', function (instance) { + var g = instance.web.intersperse; equal(g("", []), ""); equal(g("0", []), "0"); equal(g("012", []), "012"); @@ -176,60 +162,61 @@ $(document).ready(function () { equal(g("12345678", [3,3,3,3], '.'), '12.345.678'); equal(g("12345678", [3,0], '.'), '12.345.678'); }); - test('format_integer', function () { - openerp.web._t.database.parameters.grouping = [3, 3, 3, 3]; - equal(openerp.web.format_value(1000000, {type: 'integer'}), + test('format_integer', function (instance) { + instance.web._t.database.parameters.grouping = [3, 3, 3, 3]; + equal(instance.web.format_value(1000000, {type: 'integer'}), '1,000,000'); - openerp.web._t.database.parameters.grouping = [3, 2, -1]; - equal(openerp.web.format_value(106500, {type: 'integer'}), + instance.web._t.database.parameters.grouping = [3, 2, -1]; + equal(instance.web.format_value(106500, {type: 'integer'}), '1,06,500'); - openerp.web._t.database.parameters.grouping = [1, 2, -1]; - equal(openerp.web.format_value(106500, {type: 'integer'}), + instance.web._t.database.parameters.grouping = [1, 2, -1]; + equal(instance.web.format_value(106500, {type: 'integer'}), '106,50,0'); }); - test('format_float', function () { - openerp.web._t.database.parameters.grouping = [3, 3, 3, 3]; - equal(openerp.web.format_value(1000000, {type: 'float'}), + test('format_float', function (instance) { + instance.web._t.database.parameters.grouping = [3, 3, 3, 3]; + equal(instance.web.format_value(1000000, {type: 'float'}), '1,000,000.00'); - openerp.web._t.database.parameters.grouping = [3, 2, -1]; - equal(openerp.web.format_value(106500, {type: 'float'}), + instance.web._t.database.parameters.grouping = [3, 2, -1]; + equal(instance.web.format_value(106500, {type: 'float'}), '1,06,500.00'); - openerp.web._t.database.parameters.grouping = [1, 2, -1]; - equal(openerp.web.format_value(106500, {type: 'float'}), + instance.web._t.database.parameters.grouping = [1, 2, -1]; + equal(instance.web.format_value(106500, {type: 'float'}), '106,50,0.00'); - _.extend(openerp.web._t.database.parameters, { + _.extend(instance.web._t.database.parameters, { grouping: [3, 0], decimal_point: ',', thousands_sep: '.' }); - equal(openerp.web.format_value(6000, {type: 'float'}), + equal(instance.web.format_value(6000, {type: 'float'}), '6.000,00'); }); - module('custom-date-formats', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.dates(openerp); - window.openerp.web.formats(openerp); - } +}); +openerp.testing.section('web-formats', { + dependencies: ['web.formats'] +}, function (test) { + test('format stripper', function (instance) { + strictEqual(instance.web.strip_raw_chars('%a, %Y %b %d'), + '%a, %Y %b %d'); + strictEqual(instance.web.strip_raw_chars('%a, %Y.eko %bren %da'), + '%a, %Y. %b %d'); }); - test('format stripper', function () { - strictEqual(openerp.web.strip_raw_chars('%a, %Y %b %d'), '%a, %Y %b %d'); - strictEqual(openerp.web.strip_raw_chars('%a, %Y.eko %bren %da'), '%a, %Y. %b %d'); + test('ES date format', function (instance) { + instance.web._t.database.parameters.date_format = '%a, %Y %b %d'; + var date = instance.web.str_to_date("2009-05-04"); + strictEqual(instance.web.format_value(date, {type:"date"}), + 'Mon, 2009 May 04'); + strictEqual(instance.web.parse_value('Mon, 2009 May 04', {type: 'date'}), + '2009-05-04'); }); - test('ES date format', function () { - openerp.web._t.database.parameters.date_format = '%a, %Y %b %d'; - var date = openerp.web.str_to_date("2009-05-04"); - strictEqual(openerp.web.format_value(date, {type:"date"}), 'Mon, 2009 May 04'); - strictEqual(openerp.web.parse_value('Mon, 2009 May 04', {type: 'date'}), '2009-05-04'); - }); - test('extended ES date format', function () { - openerp.web._t.database.parameters.date_format = '%a, %Y.eko %bren %da'; - var date = openerp.web.str_to_date("2009-05-04"); - strictEqual(openerp.web.format_value(date, {type:"date"}), 'Mon, 2009. May 04'); - strictEqual(openerp.web.parse_value('Mon, 2009. May 04', {type: 'date'}), '2009-05-04'); + test('extended ES date format', function (instance) { + instance.web._t.database.parameters.date_format = '%a, %Y.eko %bren %da'; + var date = instance.web.str_to_date("2009-05-04"); + strictEqual(instance.web.format_value(date, {type:"date"}), + 'Mon, 2009. May 04'); + strictEqual(instance.web.parse_value('Mon, 2009. May 04', {type: 'date'}), + '2009-05-04'); }); }); diff --git a/addons/web/static/test/list-utils.js b/addons/web/static/test/list-utils.js index 6200ba788ce..84661138315 100644 --- a/addons/web/static/test/list-utils.js +++ b/addons/web/static/test/list-utils.js @@ -1,45 +1,34 @@ -$(document).ready(function () { - var openerp, - create = function (o) { - if (typeof Object.create === 'function') { - return Object.create(o); - } - function Cls() {} - Cls.prototype = o; - return new Cls; - }; - module('list-events', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.chrome(openerp); - // views loader stuff - window.openerp.web.data(openerp); - window.openerp.web.views(openerp); - window.openerp.web.list(openerp); +openerp.testing.section('list.events', { + dependencies: ['web.list'] +}, function (test) { + var create = function (o) { + if (typeof Object.create === 'function') { + return Object.create(o); } - }); - test('Simple event triggering', function () { - var e = create(openerp.web.list.Events), passed = false; + function Cls() {} + Cls.prototype = o; + return new Cls; + }; + test('Simple event triggering', function (instance) { + var e = create(instance.web.list.Events), passed = false; e.bind('foo', function () { passed = true; }); e.trigger('foo'); ok(passed); }); - test('Bind all', function () { - var e = create(openerp.web.list.Events), event = null; + test('Bind all', function (instance) { + var e = create(instance.web.list.Events), event = null; e.bind(null, function (ev) { event = ev; }); e.trigger('foo'); strictEqual(event, 'foo'); }); - test('Propagate trigger params', function () { - var e = create(openerp.web.list.Events), p = false; + test('Propagate trigger params', function (instance) { + var e = create(instance.web.list.Events), p = false; e.bind(null, function (_, param) { p = param }); e.trigger('foo', true); strictEqual(p, true) }); - test('Bind multiple callbacks', function () { - var e = create(openerp.web.list.Events), count; + test('Bind multiple callbacks', function (instance) { + var e = create(instance.web.list.Events), count; e.bind('foo', function () { count++; }) .bind('bar', function () { count++; }) .bind(null, function () { count++; }) @@ -59,20 +48,20 @@ $(document).ready(function () { e.trigger('baz'); strictEqual(count, 3); }); - test('Mixin events', function () { - var cls = openerp.web.Class.extend({ + test('Mixin events', function (instance) { + var cls = instance.web.Class.extend({ method: function () { this.trigger('e'); } }); - cls.include(openerp.web.list.Events); - var instance = new cls, triggered = false; + cls.include(instance.web.list.Events); + var i = new cls, triggered = false; - instance.bind('e', function () { triggered = true; }); - instance.method(); + i.bind('e', function () { triggered = true; }); + i.method(); ok(triggered); }); - test('Unbind all handlers', function () { - var e = create(openerp.web.list.Events), passed = 0; + test('Unbind all handlers', function (instance) { + var e = create(instance.web.list.Events), passed = 0; e.bind('foo', function () { passed++; }); e.trigger('foo'); strictEqual(passed, 1); @@ -80,8 +69,8 @@ $(document).ready(function () { e.trigger('foo'); strictEqual(passed, 1); }); - test('Unbind one handler', function () { - var e = create(openerp.web.list.Events), p1 = 0, p2 = 0, + test('Unbind one handler', function (instance) { + var e = create(instance.web.list.Events), p1 = 0, p2 = 0, h1 = function () { p1++; }, h2 = function () { p2++; }; e.bind('foo', h1); e.bind('foo', h2); @@ -93,29 +82,20 @@ $(document).ready(function () { strictEqual(p1, 1); strictEqual(p2, 2); }); - - module('list-records', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.chrome(openerp); - // views loader stuff - window.openerp.web.data(openerp); - window.openerp.web.views(openerp); - window.openerp.web.list(openerp); - } - }); - test('Basic record initialization', function () { - var r = new openerp.web.list.Record({qux: 3}); +}); +openerp.testing.section('list.records', { + dependencies: ['web.list'] +}, function (test) { + test('Basic record initialization', function (instance) { + var r = new instance.web.list.Record({qux: 3}); r.set('foo', 1); r.set('bar', 2); strictEqual(r.get('foo'), 1); strictEqual(r.get('bar'), 2); strictEqual(r.get('qux'), 3); }); - test('Change all the things', function () { - var r = new openerp.web.list.Record(), changed = false, field; + test('Change all the things', function (instance) { + var r = new instance.web.list.Record(), changed = false, field; r.bind('change', function () { changed = true; }); r.bind(null, function (e) { field = field || e.split(':')[1]}); r.set('foo', 1); @@ -123,8 +103,8 @@ $(document).ready(function () { ok(changed); strictEqual(field, 'foo'); }); - test('Change single field', function () { - var r = new openerp.web.list.Record(), changed = 0; + test('Change single field', function (instance) { + var r = new instance.web.list.Record(), changed = 0; r.bind('change:foo', function () { changed++; }); r.set('foo', 1); r.set('bar', 1); @@ -132,21 +112,12 @@ $(document).ready(function () { strictEqual(r.get('bar'), 1); strictEqual(changed, 1); }); - - module('list-collections', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.chrome(openerp); - // views loader stuff - window.openerp.web.data(openerp); - window.openerp.web.views(openerp); - window.openerp.web.list(openerp); - } - }); - test('degenerate-fetch', function () { - var c = new openerp.web.list.Collection(); +}); +openerp.testing.section('list.collections', { + dependencies: ['web.list'] +}, function (test) { + test('degenerate-fetch', function (instance) { + var c = new instance.web.list.Collection(); strictEqual(c.length, 0); c.add({id: 1, value: 2}); c.add({id: 2, value: 3}); @@ -155,16 +126,16 @@ $(document).ready(function () { strictEqual(c.length, 4); var r = c.at(2), r2 = c.get(1); - ok(r instanceof openerp.web.list.Record); + ok(r instanceof instance.web.list.Record); strictEqual(r.get('id'), 3); strictEqual(r.get('value'), 5); - ok(r2 instanceof openerp.web.list.Record); + ok(r2 instanceof instance.web.list.Record); strictEqual(r2.get('id'), 1); strictEqual(r2.get('value'), 2); }); - test('degenerate-indexed-add', function () { - var c = new openerp.web.list.Collection([ + test('degenerate-indexed-add', function (instance) { + var c = new instance.web.list.Collection([ {id: 1, value: 5}, {id: 2, value: 10}, {id: 3, value: 20} @@ -175,8 +146,8 @@ $(document).ready(function () { strictEqual(c.at(1).get('value'), 55); strictEqual(c.at(3).get('value'), 20); }); - test('degenerate-remove', function () { - var c = new openerp.web.list.Collection([ + test('degenerate-remove', function (instance) { + var c = new instance.web.list.Collection([ {id: 1, value: 5}, {id: 2, value: 10}, {id: 3, value: 20} @@ -188,9 +159,9 @@ $(document).ready(function () { equal(c.get(2), undefined); strictEqual(c.at(1).get('value'), 20); }); - test('degenerate-remove-bound', function () { + test('degenerate-remove-bound', function (instance) { var changed = false, - c = new openerp.web.list.Collection([ {id: 1, value: 5} ]); + c = new instance.web.list.Collection([ {id: 1, value: 5} ]); c.bind('change', function () { changed = true; }); var record = c.get(1); c.remove(record); @@ -198,8 +169,8 @@ $(document).ready(function () { ok(!changed, 'removed records should not trigger events in their ' + 'parent collection'); }); - test('degenerate-reset', function () { - var event, obj, c = new openerp.web.list.Collection([ + test('degenerate-reset', function (instance) { + var event, obj, c = new instance.web.list.Collection([ {id: 1, value: 5}, {id: 2, value: 10}, {id: 3, value: 20} @@ -218,9 +189,9 @@ $(document).ready(function () { strictEqual(c.length, 1); strictEqual(c.get(42).get('value'), 55); }); - test('degenerate-reset-bound', function () { + test('degenerate-reset-bound', function (instance) { var changed = false, - c = new openerp.web.list.Collection([ {id: 1, value: 5} ]); + c = new instance.web.list.Collection([ {id: 1, value: 5} ]); c.bind('change', function () { changed = true; }); var record = c.get(1); c.reset(); @@ -229,9 +200,9 @@ $(document).ready(function () { 'parent collection'); }); - test('degenerate-propagations', function () { + test('degenerate-propagations', function (instance) { var values = []; - var c = new openerp.web.list.Collection([ + var c = new instance.web.list.Collection([ {id: 1, value: 5}, {id: 2, value: 10}, {id: 3, value: 20} @@ -244,8 +215,8 @@ $(document).ready(function () { c.get(3).set('value', 21); deepEqual(values, [6, 11, 21]); }); - test('BTree', function () { - var root = new openerp.web.list.Collection(), + test('BTree', function (instance) { + var root = new instance.web.list.Collection(), c = root.proxy('admin'), total = 0; c.add({id: 1, name: "Administrator", login: 'admin'}); @@ -260,8 +231,8 @@ $(document).ready(function () { c.at(1).set('wealth', 5); strictEqual(total, 47); }); - test('degenerate-successor', function () { - var root = new openerp.web.list.Collection([ + test('degenerate-successor', function (instance) { + var root = new instance.web.list.Collection([ {id: 1, value: 1}, {id: 2, value: 2}, {id: 3, value: 3}, @@ -282,8 +253,8 @@ $(document).ready(function () { root.at(3).attributes, "wraparound should have no effect if not succ(last_record)"); }); - test('successor', function () { - var root = new openerp.web.list.Collection(); + test('successor', function (instance) { + var root = new instance.web.list.Collection(); root.proxy('first').add([{id: 1, value: 1}, {id: 2, value: 2}]); root.proxy('second').add([{id: 3, value: 3}, {id: 4, value: 5}]); root.proxy('third').add([{id: 5, value: 8}, {id: 6, value: 13}]); @@ -298,8 +269,8 @@ $(document).ready(function () { root.get(3).attributes, "should wraparound within a collection"); }); - test('degenerate-predecessor', function () { - var root = new openerp.web.list.Collection([ + test('degenerate-predecessor', function (instance) { + var root = new instance.web.list.Collection([ {id: 1, value: 1}, {id: 2, value: 2}, {id: 3, value: 3}, @@ -320,8 +291,8 @@ $(document).ready(function () { root.at(0).attributes, "wraparound should have no effect if not pred(first_record)"); }); - test('predecessor', function () { - var root = new openerp.web.list.Collection(); + test('predecessor', function (instance) { + var root = new instance.web.list.Collection(); root.proxy('first').add([{id: 1, value: 1}, {id: 2, value: 2}]); root.proxy('second').add([{id: 3, value: 3}, {id: 4, value: 5}]); root.proxy('third').add([{id: 5, value: 8}, {id: 6, value: 13}]); @@ -336,21 +307,12 @@ $(document).ready(function () { root.get(4).attributes, "should wraparound within a collection"); }); - - module('list-hofs', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.chrome(openerp); - // views loader stuff - window.openerp.web.data(openerp); - window.openerp.web.views(openerp); - window.openerp.web.list(openerp); - } - }); - test('each, degenerate', function () { - var c = new openerp.web.list.Collection([ +}); +openerp.testing.section('list.collections.hom', { + dependencies: ['web.list'] +}, function (test) { + test('each, degenerate', function (instance) { + var c = new instance.web.list.Collection([ {id: 1, value: 5}, {id: 2, value: 10}, {id: 3, value: 20} @@ -362,8 +324,8 @@ $(document).ready(function () { ids, [1, 2, 3], 'degenerate collections should be iterated in record order'); }); - test('each, deep', function () { - var root = new openerp.web.list.Collection(), + test('each, deep', function (instance) { + var root = new instance.web.list.Collection(), ids = []; root.proxy('foo').add([ {id: 1, value: 5}, @@ -382,8 +344,8 @@ $(document).ready(function () { ids, [1, 2, 3, 10, 20, 30], 'tree collections should be deeply iterated'); }); - test('map, degenerate', function () { - var c = new openerp.web.list.Collection([ + test('map, degenerate', function (instance) { + var c = new instance.web.list.Collection([ {id: 1, value: 5}, {id: 2, value: 10}, {id: 3, value: 20} @@ -395,8 +357,8 @@ $(document).ready(function () { ids, [1, 2, 3], 'degenerate collections should be iterated in record order'); }); - test('map, deep', function () { - var root = new openerp.web.list.Collection(); + test('map, deep', function (instance) { + var root = new instance.web.list.Collection(); root.proxy('foo').add([ {id: 1, value: 5}, {id: 2, value: 10}, @@ -414,29 +376,20 @@ $(document).ready(function () { ids, [1, 2, 3, 10, 20, 30], 'tree collections should be deeply iterated'); }); - - module("list-weirds", { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.chrome(openerp); - // views loader stuff - window.openerp.web.data(openerp); - window.openerp.web.views(openerp); - window.openerp.web.list(openerp); - } - }); - test('set-from-noid', function () { - var root = new openerp.web.list.Collection(); +}); +openerp.testing.section('list.collection.weirdoes', { + dependencies: ['web.list'] +}, function (test) { + test('set-from-noid', function (instance) { + var root = new instance.web.list.Collection(); root.add({v: 3}); root.at(0).set('id', 42); var record = root.get(42); equal(root.length, 1); equal(record.get('v'), 3, "should have fetched the original record"); }); - test('set-from-previd', function () { - var root = new openerp.web.list.Collection(); + test('set-from-previd', function (instance) { + var root = new instance.web.list.Collection(); root.add({id: 1, v: 2}); root.get(1).set('id', 42); var record = root.get(42); diff --git a/addons/web/static/test/registry.js b/addons/web/static/test/registry.js index ca689a351e0..2ad69a541bc 100644 --- a/addons/web/static/test/registry.js +++ b/addons/web/static/test/registry.js @@ -1,58 +1,55 @@ -$(document).ready(function () { - var openerp; - module('Registry', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - openerp.web.Foo = {}; - openerp.web.Bar = {}; - openerp.web.Foo2 = {}; - } - }); - test('key set', function () { - var reg = new openerp.web.Registry(); +openerp.testing.section('registry', { + dependencies: ['web.corelib'], + setup: function (instance) { + instance.web.Foo = {}; + instance.web.Bar = {}; + instance.web.Foo2 = {}; + } +}, function (test) { + test('key set', function (instance) { + var reg = new instance.web.Registry(); - reg.add('foo', 'openerp.web.Foo') - .add('bar', 'openerp.web.Bar'); - strictEqual(reg.get_object('bar'), openerp.web.Bar); + reg.add('foo', 'instance.web.Foo') + .add('bar', 'instance.web.Bar'); + strictEqual(reg.get_object('bar'), instance.web.Bar); }); - test('extension', function () { - var reg = new openerp.web.Registry({ - foo: 'openerp.web.Foo', - bar: 'openerp.web.Bar' + test('extension', function (instance) { + var reg = new instance.web.Registry({ + foo: 'instance.web.Foo', + bar: 'instance.web.Bar' }); - var reg2 = reg.extend({ 'foo': 'openerp.web.Foo2' }); - strictEqual(reg.get_object('foo'), openerp.web.Foo); - strictEqual(reg2.get_object('foo'), openerp.web.Foo2); + var reg2 = reg.extend({ 'foo': 'instance.web.Foo2' }); + strictEqual(reg.get_object('foo'), instance.web.Foo); + strictEqual(reg2.get_object('foo'), instance.web.Foo2); }); - test('remain-linked', function () { - var reg = new openerp.web.Registry({ - foo: 'openerp.web.Foo', - bar: 'openerp.web.Bar' + test('remain-linked', function (instance) { + var reg = new instance.web.Registry({ + foo: 'instance.web.Foo', + bar: 'instance.web.Bar' }); var reg2 = reg.extend(); - reg.add('foo2', 'openerp.web.Foo2'); - strictEqual(reg.get_object('foo2'), openerp.web.Foo2); - strictEqual(reg2.get_object('foo2'), openerp.web.Foo2); + reg.add('foo2', 'instance.web.Foo2'); + strictEqual(reg.get_object('foo2'), instance.web.Foo2); + strictEqual(reg2.get_object('foo2'), instance.web.Foo2); }); - test('multiget', function () { - var reg = new openerp.web.Registry({ - foo: 'openerp.web.Foo', - bar: 'openerp.web.Bar' + test('multiget', function (instance) { + var reg = new instance.web.Registry({ + foo: 'instance.web.Foo', + bar: 'instance.web.Bar' }); strictEqual(reg.get_any(['qux', 'grault', 'bar', 'foo']), - openerp.web.Bar); + instance.web.Bar); }); - test('extended-multiget', function () { - var reg = new openerp.web.Registry({ - foo: 'openerp.web.Foo', - bar: 'openerp.web.Bar' + test('extended-multiget', function (instance) { + var reg = new instance.web.Registry({ + foo: 'instance.web.Foo', + bar: 'instance.web.Bar' }); var reg2 = reg.extend(); strictEqual(reg2.get_any(['qux', 'grault', 'bar', 'foo']), - openerp.web.Bar); + instance.web.Bar); }); }); diff --git a/addons/web/static/test/rpc.js b/addons/web/static/test/rpc.js index fbe7147f2e6..84b4afa8baf 100644 --- a/addons/web/static/test/rpc.js +++ b/addons/web/static/test/rpc.js @@ -1,16 +1,8 @@ -$(document).ready(function () { - var openerp; - - module('Misordered resolution management', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.data(openerp); - } - }); - test('Resolve all correctly ordered, sync', function () { - var dm = new openerp.web.DropMisordered(), flag = false; +openerp.testing.section('misordered resolution managemeng', { + dependencies: ['web.data'] +}, function (test) { + test('Resolve all correctly ordered, sync', function (instance) { + var dm = new instance.web.DropMisordered(), flag = false; var d1 = $.Deferred(), d2 = $.Deferred(), r1 = dm.add(d1), r2 = dm.add(d2); @@ -23,8 +15,8 @@ $(document).ready(function () { ok(flag); }); - test("Don't resolve mis-ordered, sync", function () { - var dm = new openerp.web.DropMisordered(), + test("Don't resolve mis-ordered, sync", function (instance) { + var dm = new instance.web.DropMisordered(), done1 = false, done2 = false, fail1 = false, fail2 = false; @@ -44,8 +36,8 @@ $(document).ready(function () { ok(done2); ok(!fail2); }); - test('Fail mis-ordered flag, sync', function () { - var dm = new openerp.web.DropMisordered(true), + test('Fail mis-ordered flag, sync', function (instance) { + var dm = new instance.web.DropMisordered(true), done1 = false, done2 = false, fail1 = false, fail2 = false; @@ -66,8 +58,8 @@ $(document).ready(function () { ok(!fail2); }); - asyncTest('Resolve all correctly ordered, async', 1, function () { - var dm = new openerp.web.DropMisordered(); + test('Resolve all correctly ordered, async', {asserts: 1}, function (instance) { + var dm = new instance.web.DropMisordered(); var d1 = $.Deferred(), d2 = $.Deferred(), r1 = dm.add(d1), r2 = dm.add(d2); @@ -75,13 +67,12 @@ $(document).ready(function () { setTimeout(function () { d1.resolve(); }, 100); setTimeout(function () { d2.resolve(); }, 200); - $.when(r1, r2).done(function () { - start(); + return $.when(r1, r2).done(function () { ok(true); }); }); - asyncTest("Don't resolve mis-ordered, async", 4, function () { - var dm = new openerp.web.DropMisordered(), + test("Don't resolve mis-ordered, async", {asserts: 4}, function (instance) { + var dm = new instance.web.DropMisordered(), done1 = false, done2 = false, fail1 = false, fail2 = false; @@ -94,18 +85,20 @@ $(document).ready(function () { setTimeout(function () { d1.resolve(); }, 200); setTimeout(function () { d2.resolve(); }, 100); + var done = $.Deferred(); setTimeout(function () { - start(); // d1 is in limbo ok(!done1); ok(!fail1); // d2 is resolved ok(done2); ok(!fail2); + done.resolve(); }, 400); + return $.when(d1, d2, done); }); - asyncTest('Fail mis-ordered flag, async', 4, function () { - var dm = new openerp.web.DropMisordered(true), + test('Fail mis-ordered flag, async', {asserts: 4}, function (instance) { + var dm = new instance.web.DropMisordered(true), done1 = false, done2 = false, fail1 = false, fail2 = false; @@ -118,6 +111,7 @@ $(document).ready(function () { setTimeout(function () { d1.resolve(); }, 200); setTimeout(function () { d2.resolve(); }, 100); + var done = $.Deferred(); setTimeout(function () { start(); // d1 is failed @@ -126,6 +120,8 @@ $(document).ready(function () { // d2 is resolved ok(done2); ok(!fail2); + done.resolve(); }, 400); + return $.when(d1, d2, done) }); }); From 6abc1134f5193d0a80066834a17f344ce24b09d8 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 26 Oct 2012 12:04:52 +0200 Subject: [PATCH 006/125] [IMP] port list-edition tests to new framework bzr revid: xmo@openerp.com-20121026100452-8s4j9g0mxzfuy834 --- addons/web/static/test/list-editable.js | 260 ++++++++++-------------- 1 file changed, 113 insertions(+), 147 deletions(-) diff --git a/addons/web/static/test/list-editable.js b/addons/web/static/test/list-editable.js index 0388e56048d..0fc930793f2 100644 --- a/addons/web/static/test/list-editable.js +++ b/addons/web/static/test/list-editable.js @@ -1,16 +1,13 @@ -$(document).ready(function () { - var $fix = $('#qunit-fixture'); - - var instance; - var baseSetup = function () { - instance = openerp.testing.instanceFor('list_editable'); - - openerp.testing.loadTemplate(instance); - - openerp.testing.mockifyRPC(instance); - }; - - +openerp.testing.section('editor', { + dependencies: ['web.list_editable'], + rpc: 'mock', + templates: true, + setup: function (instance, $s, mock) { + mock('test.model:create', function () { + return 42; + }); + } +}, function (test) { /** * * @param {String} name @@ -30,7 +27,7 @@ $(document).ready(function () { } /** - * @param {Array} fields + * @param {Array} [fields] * @return {Object} */ function makeFormView(fields) { @@ -67,46 +64,37 @@ $(document).ready(function () { }; } - module('editor', { - setup: baseSetup - }); - asyncTest('base-state', 2, function () { + test('base-state', {asserts: 2}, function (instance, $fix) { var e = new instance.web.list.Editor({ dataset: {ids: []}, edition_view: function () { return makeFormView(); } }); - e.appendTo($fix) - .always(start) - .fail(function (error) { ok(false, error && error.message); }) + return e.appendTo($fix) .done(function () { ok(!e.is_editing(), "should not be editing"); ok(e.form instanceof instance.web.FormView, "should use default form type"); }); }); - asyncTest('toggle-edition-save', 4, function () { - instance.session.responses['/web/dataset/call_kw:create'] = function () { - return { result: 42 }; - }; - instance.session.responses['/web/dataset/call_kw:read'] = function () { - return { result: [{ - id: 42, - a: false, - b: false, - c: false - }]}; - }; + test('toggle-edition-save', { + asserts: 4, + setup: function (instance, $s, mock) { + mock('test.model:read', function () { + return [{id: 42, a: false, b: false, c: false}]; + }); + } + }, function (instance, $fix) { var e = new instance.web.list.Editor({ - dataset: new instance.web.DataSetSearch(), + dataset: new instance.web.DataSetSearch(null, 'test.model'), prepends_on_create: function () { return false; }, edition_view: function () { return makeFormView([ field('a'), field('b'), field('c') ]); } }); var counter = 0; - e.appendTo($fix) + return e.appendTo($fix) .pipe(function () { return e.edit({}, function () { ++counter; @@ -117,26 +105,21 @@ $(document).ready(function () { equal(counter, 3, "should have configured all fields"); return e.save(); }) - .always(start) - .fail(function (error) { ok(false, error && error.message); }) .done(function (record) { ok(!e.is_editing(), "should have stopped editing"); equal(record.id, 42, "should have newly created id"); }) }); - asyncTest('toggle-edition-cancel', 2, function () { - instance.session.responses['/web/dataset/call_kw:create'] = function () { - return { result: 42 }; - }; + test('toggle-edition-cancel', { asserts: 2 }, function (instance, $fix) { var e = new instance.web.list.Editor({ - dataset: new instance.web.DataSetSearch(), + dataset: new instance.web.DataSetSearch(null, 'test.model'), prepends_on_create: function () { return false; }, edition_view: function () { return makeFormView([ field('a'), field('b'), field('c') ]); } }); var counter = 0; - e.appendTo($fix) + return e.appendTo($fix) .pipe(function () { return e.edit({}, function () { ++counter; @@ -145,22 +128,20 @@ $(document).ready(function () { .pipe(function (form) { return e.cancel(); }) - .always(start) - .fail(function (error) { ok(false, error && error.message); }) .done(function (record) { ok(!e.is_editing(), "should have stopped editing"); ok(!record.id, "should have no id"); }) }); - asyncTest('toggle-save-required', 2, function () { - instance.session.responses['/web/dataset/call_kw:create'] = function () { - return { result: 42 }; - }; + test('toggle-save-required', { + asserts: 2, + fail_on_rejection: false + }, function (instance, $fix) { var e = new instance.web.list.Editor({ do_warn: function () { warnings++; }, - dataset: new instance.web.DataSetSearch(), + dataset: new instance.web.DataSetSearch(null, 'test.model'), prepends_on_create: function () { return false; }, edition_view: function () { return makeFormView([ @@ -169,7 +150,7 @@ $(document).ready(function () { }); var counter = 0; var warnings = 0; - e.appendTo($fix) + return e.appendTo($fix) .pipe(function () { return e.edit({}, function () { ++counter; @@ -178,78 +159,73 @@ $(document).ready(function () { .pipe(function (form) { return e.save(); }) - .always(start) .done(function () { ok(false, "cancel should not succeed"); }) .fail(function () { equal(warnings, 1, "should have been warned"); ok(e.is_editing(), "should have kept editing"); - }) + }); }); - - module('list-edition', { - setup: function () { - baseSetup(); - - var records = {}; - _.extend(instance.session.responses, { - '/web/view/load': function () { - return {result: { - type: 'tree', - fields: { - a: {type: 'char', string: "A"}, - b: {type: 'char', string: "B"}, - c: {type: 'char', string: "C"} - }, - arch: { - tag: 'tree', - attrs: {}, - children: [ - {tag: 'field', attrs: {name: 'a'}}, - {tag: 'field', attrs: {name: 'b'}}, - {tag: 'field', attrs: {name: 'c'}} - ] - } - }}; +}); +openerp.testing.section('list.edition', { + dependencies: ['web.list_editable'], + rpc: 'mock', + templates: true, + setup: function (instance, $s, mock) { + var records = {}; + mock('demo:create', function (args) { + records[42] = _.extend({}, args[0]); + return 42; + }); + mock('demo:read', function (args) { + var id = args[0][0]; + if (id in records) { + return [records[id]]; + } + return []; + }); + mock('/web/view/load', function () { + return { + type: 'tree', + fields: { + a: {type: 'char', string: "A"}, + b: {type: 'char', string: "B"}, + c: {type: 'char', string: "C"} }, - '/web/dataset/call_kw:create': function (params) { - records[42] = _.extend({}, params.params.args[0]); - return {result: 42}; - }, - '/web/dataset/call_kw:read': function (params) { - var id = params.params.args[0][0]; - if (id in records) { - return {result: [records[id]]}; - } - return {result: []}; + arch: { + tag: 'tree', + attrs: {}, + children: [ + {tag: 'field', attrs: {name: 'a'}}, + {tag: 'field', attrs: {name: 'b'}}, + {tag: 'field', attrs: {name: 'c'}} + ] } - }) - } - }); - asyncTest('newrecord', 6, function () { + }; + }); + } +}, function (test) { + test('newrecord', {asserts: 6}, function (instance, $fix, mock) { var got_defaults = false; - instance.session.responses['/web/dataset/call_kw:default_get'] = function (params) { - var fields = params.params.args[0]; + mock('demo:default_get', function (args) { + var fields = args[0]; deepEqual( fields, ['a', 'b', 'c'], "should ask defaults for all fields"); got_defaults = true; - return {result: { - a: "qux", - b: "quux" - }}; - }; + return { a: "qux", b: "quux" }; + }); var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]); var l = new instance.web.ListView({}, ds, false, {editable: 'top'}); - l.appendTo($fix) + return l.appendTo($fix) .pipe(l.proxy('reload_content')) .pipe(function () { return l.start_edition(); }) - .always(start) .pipe(function () { ok(got_defaults, "should have fetched default values for form"); + return l.save_edition(); }) .pipe(function (result) { @@ -260,45 +236,39 @@ $(document).ready(function () { "should have used default values"); ok(!result.record.get('c'), "should have no value if there was no default"); - }) - .fail(function (e) { ok(false, e && e.message || e); }); - }); - - module('list-edition-events', { - setup: function () { - baseSetup(); - _.extend(instance.session.responses, { - '/web/view/load': function () { - return {result: { - type: 'tree', - fields: { - a: {type: 'char', string: "A"}, - b: {type: 'char', string: "B"}, - c: {type: 'char', string: "C"} - }, - arch: { - tag: 'tree', - attrs: {}, - children: [ - {tag: 'field', attrs: {name: 'a'}}, - {tag: 'field', attrs: {name: 'b'}}, - {tag: 'field', attrs: {name: 'c'}} - ] - } - }}; - }, - '/web/dataset/call_kw:read': function (params) { - return {result: [{ - id: 1, - a: 'foo', - b: 'bar', - c: 'baz' - }]}; - } }); - } }); - asyncTest('edition events', 4, function () { +}); +openerp.testing.section('list.edition.events', { + dependencies: ['web.list_editable'], + rpc: 'mock', + templates: true, + setup: function (instance, $s, mock) { + mock('demo:read', function () { + return [{ id: 1, a: 'foo', b: 'bar', c: 'baz' }]; + }); + mock('/web/view/load', function () { + return { + type: 'tree', + fields: { + a: {type: 'char', string: "A"}, + b: {type: 'char', string: "B"}, + c: {type: 'char', string: "C"} + }, + arch: { + tag: 'tree', + attrs: {}, + children: [ + {tag: 'field', attrs: {name: 'a'}}, + {tag: 'field', attrs: {name: 'b'}}, + {tag: 'field', attrs: {name: 'c'}} + ] + } + }; + }); + } +}, function (test) { + test('edition events', {asserts: 4}, function (instance, $fix) { var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]); var o = { counter: 0, @@ -306,9 +276,8 @@ $(document).ready(function () { }; var l = new instance.web.ListView({}, ds, false, {editable: 'top'}); l.on('edit:before edit:after', o, o.onEvent); - l.appendTo($fix) + return l.appendTo($fix) .pipe(l.proxy('reload_content')) - .always(start) .pipe(function () { ok(l.options.editable, "should be editable"); equal(o.counter, 0, "should have seen no event yet"); @@ -317,11 +286,10 @@ $(document).ready(function () { .pipe(function () { ok(l.editor.is_editing(), "should be editing"); equal(o.counter, 2, "should have seen two edition events"); - }) - .fail(function (e) { ok(false, e && e.message); }); + }); }); - asyncTest('edition events: cancelling', 3, function () { + test('edition events: cancelling', {asserts: 3}, function (instance, $fix) { var edit_after = false; var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]); var l = new instance.web.ListView({}, ds, false, {editable: 'top'}); @@ -331,9 +299,8 @@ $(document).ready(function () { l.on('edit:after', {}, function () { edit_after = true; }); - l.appendTo($fix) + return l.appendTo($fix) .pipe(l.proxy('reload_content')) - .always(start) .pipe(function () { ok(l.options.editable, "should be editable"); return l.start_edition(); @@ -343,7 +310,6 @@ $(document).ready(function () { ok(!l.editor.is_editing(), "should not be editing"); ok(!edit_after, "should not have fired the edit:after event"); return $.when(); - }) - .fail(function (e) { ok(false, e && e.message || e); }); + }); }); }); From bf4a26d3e8613c24b3a360013525cfef28458176 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 29 Oct 2012 12:01:54 +0100 Subject: [PATCH 007/125] [ADD] actual-RPC tests, with UI. Bump QUnit timeout to 10s so RPC can run at all bzr revid: xmo@openerp.com-20121029110154-5927gaix8k0ijl0c --- addons/web/controllers/testing.py | 2 +- addons/web/doc/images/db-query.png | Bin 0 -> 7142 bytes addons/web/doc/images/tests2.png | Bin 37769 -> 20114 bytes addons/web/doc/images/tests3.png | Bin 37827 -> 20382 bytes addons/web/doc/testing.rst | 86 +++++++++- addons/web/static/src/js/testing.js | 182 ++++++++++++++++------ addons/web_tests_demo/__init__.py | 14 ++ addons/web_tests_demo/static/test/demo.js | 15 ++ 8 files changed, 242 insertions(+), 57 deletions(-) create mode 100644 addons/web/doc/images/db-query.png diff --git a/addons/web/controllers/testing.py b/addons/web/controllers/testing.py index 0cfc633a492..9947f1e1ed5 100644 --- a/addons/web/controllers/testing.py +++ b/addons/web/controllers/testing.py @@ -53,7 +53,7 @@ TESTING = Template(u""" diff --git a/addons/web/doc/images/db-query.png b/addons/web/doc/images/db-query.png new file mode 100644 index 0000000000000000000000000000000000000000..e063b724001871f3846ead587810069347a8eab6 GIT binary patch literal 7142 zcmaJ^byU>Ny9WdWlw1_)bm>@0NhuLox;vH>7o;SlyE~R#kp=U=QC$QloVueama8`P*8AX-$|*Upr8gL#|SJmWN*zpJA{Hl zUMwpmuI4iRCp9y%NSHX_TB`ApV)K))zqQy(h)9Bd-Q=?nW2*bL7!kYxqoV-`f9qmF zwQ7nQC|TAA#I9AH=SCdtCm9-O9r5l7CA!2gBSHuyAawrG=Mh^HShNc`|Naw@0cia@ zq_AjtfD%{_bCzI#S%-Y}EabOkhx)(ay-cCa@LcO||w3 zw-17Un{V85uDrtaE@GxRv(tsX?+EthnEFW$W2+>4a7L*!EHNdEq9#-DFdVd0COSdj z(7$!<89jpGm+%-PrcuBeocNkk=X7lS5dDWl%v^`5vcCHd0pmkA^{zd=bP4BVA!(
TQGpC;T(@|%Ds=~e*YvUZ7-rEljGYl;GTnMKK#n-M6 zxEd3?MP543?p)@Uy0(A@{rA~oR!E86n;5;C<;W*7pKfuuF75Z+Z82w0eE2z4~&)-uvVdqe9I)(j)xcj7{jNLsrSXmBjr5t@eyoq|Ep7> zyr^(l4H<6I9N{nP&l4UgD3`N`2|Jo!#x*j9Evu#Vb2pD-apxV?lLuxNO{hk#IMr`* z=rMZg#j9(Zc53m^vs}bFEcd@QE?JlCuWJM(kX55f)TnQ#>lEaP;-+06q4e()_S6>| zp2S`98FXR>ji1%0RVJDz8TU~5fLhAVtS&ylCs(af*7AxJWcG_6aeftM!dk`9c81vd zR>CVkD$ci%4aKo>p?WJJZlCS|AS$k(F381H_o>n^BC7BkSf~h3d1R7Y5$!^WWW-XF zOMPgn^7Y^-Q-7%UP`&VtUa4;PsHl?nx-)jwPjY)zGcnAG0}TWIWIppTr&~Srhd3n# z9@x)xiF|bFg?S%+rHLjnFYaI<%L?kT`5@en20+0+omsf@Fakr?$O1;xg4Gz_y4A$* z|HSQ1dW1bSmZv&Z)a#te>3wlOtBEvX_`OuK9`d7|%vwZMw@vgaigC0_HFa z5!sknuC^2>XHsKaPaXZU%~|DyI&>qNnK)qK>r~1;UUXuFJF{c2C|8wZ^fJ~l#yN*C zh2abY2(MC*k=0zphQKCe&ow`{i~B?Fw6^w9r`ZLuQp;ilFt=trRA%W zaE*)NE(J0-Rgnb9h0^xvMq~U18BI|?8f(2LrJv5RF`w+m`ph~s$xg%iy5F{LSG-y4ny0O+FIT{rA~^-s#Sa@v06vX0Me=@~ zD|d~FwXR&K8QAUT_6rw`%vV#K9j83+nLF*K14o${bE__t#6+V#*n9ii#FAT5_ z-E2rOk91_88IUXrb(BA9O7>3Z#3*Ylo#E2NEFVI50TO$4wILGd|MKMn&1QlK|DjnV z75_`U$k79v{vWIV5c9*f2V#C;@PC9)f|`!4$|#vzT||{Vg09dn(X=g?QN@`E+82MZ zVfLB#VUa0SgjRG3Lu36=M&hsjParjArBRsXKkgAevmsocqSCV$Jg7mcPZ3g8V8`MGTEE&`l696-hvQ&&6ckTT9#RUz zM_P&*6RtLg9CH|t9NX+p_$}^C^q&JM>eH>9o!vp^)y#9Q^{2$TsdBuqPf_$UUV4}C zq1_ygH5ULYeMyHIX)2a$Y-JvPfXAm1<%`)#MXU1_PgTVejKh*e7nfQKCWx(!Qh20* zjHC7@t-l}Bn;G1-uW`hHmUl#J@^Lb`Y*0<@ay&Edl0(^=e5#>bpCIDw?Npw4+L)l_ z4=OS1@3DOd@t&Mn4jaUw(67oUFOu@^4Z{096x`RY#8t-BwZ{Q~T!!-|O5>VPp;Nmp z_v}_Jq6DzkQE%*(R-#P%=6IaE(*?m}$f@Sw-oOaqMm6H~5q#%`>?eY&RGVGxIh&a| zQbR}S_b1w746+m?bN2BYnHkg`#9fn{d6$_#fO3*aP7v_YqD2avWqF zyrXg@yBm zE5SxHjN)>lyGktjpwZ^>t|x(EOK9^9z!WbX|K#Q{2#AR%`5o~#)3#%-MZo?9o_eFW zd8QJvRW>Y;aS<13!YaJ(qb#7n(7_h|wXw9E>7WdKSLo`6jqxp4_%|jii@i_>tcgbg zDW_q48Y!{w5k!8G&tMI&HzwFK^V2v|0ylS3*L31>)A-8VEaj-h+WCG&WLW@ASmci@ zxGxP8`IMa**-6qs>}9+_#LBc@xQ%m%*w9pQtA=Xs1Xgtu-U-ho&9EL%*_Y|I1jm0mm2mqKzHkw;=ap&rAs|f15>_)2j7$!H=T?F^H{xjpr>d z7I3|d2zqGI99q&3@tri6?Pv?VoY|7Pi!Q zshN0gI)(wV5|YY{!^3i$?N@kZ_J-BaZ&@_ZX~770#h{NQcL&tTc0vOXXd*tdNaPA1 z5CbnbJ*DtwWr!i<+vFWWXjjGI)j+Q>c7J*VTg9&Ff?NbaK%)pykO#QE&`!r82lxWq zqC@QIt%@}9d+k^=1&=TcW%g+2!b!=?z7R0wzx`}`^C+ECkO>kiC0n>-HZ4c7RdDp% zU)JEccE0s+*_Y|x&o;gyC^x*u*oBKyE9UT5cKE#+hIfM`=?XBnH*IalLoOjNa!$WW{J*#divvmLTcj`(2xEXmFqlDVRM z^ZNN2-=Dxp`hl5nW~@63|P|MV|yLAGiyV9P!~B$An5wjPsvl5a6-IE6u22 z`d9m!2zM>umO>T^DK(F_B!6~?XE9*ce9F$)kMR0hAs3V*E8^!FYE?=vVtXXcV0-DY zyBX{-w19V9Q=fT|X0!L%xy_=!jYyp%eIY+@3X2;E*jW@e%^*C1Q-^2y-8n?xW6%8r zE?ly|NZ{h&_yQBDLd0&Vm(}|TE!R@lL}Dk}1G!i9X45ywpX>#MCsB1iFDff13-S9|YJbRIj+s4LFD@@|W11+@Dil#Kdzxo~Iuqay~&mlWxAxYU1vW z8koO~@rok$gyEtz!;m-D-wyKdjC$z8v5|xSbX-8>-$VDmNaM)U`{{+uI;W==|U2%3%F7WMs25(OjN{p$LPAAYkvfe2V}#Ve>D z)nnm2eQb*cL8GEwD7)vnm5z+V_dM)97rh{*7<7Gy)S==vg+qcgu9i*{aO9yI8EqTA z?{Pe{a6j(6=#nd0$j+P8BX?`Ys)w{{F?4n-+8XC_?{<#*=HM27bJMyF`HwaKt?67~ z^@9Xn6!lvBe&8TSvjgo|Vv5{a&RzL44wM%Fln(TV<_$tM0vmyyet$ZGeRi8F(RbX?)evUFHh~n1W%vfCVLu{NC;e7D_LonMKed0cHyv zBC<-KkmmsxoyY+@Wg9uIe2GDQ2r@wG_Rv69tawAGn$$4B-szUub(%Uw`q_c9Vfe8P z6ZRR;Sf6_LsQ&~P>e)@&)`h#x=ff2r{j>uLxeeQk?uLwKk;6TFtKL0Fl_+XqiqI?-n!fBg2H8n*+ z?VfsO8{VGvhe3Ru~`s zBI$64sRiiPQ)Q)ShFz7EMW*3na9!j`pxlCWg4qRwY@C3sYK6egBnb%EN}(UYd@Qmm z!w~vQDWIjhvaYE7cf1q>GYKj;GL&4onxqt*?EaML!RJWdKHN7g~gV8e>;$D3dR2eBuE-UDd(WzCx;?Yg= zc?T#ARNu9zxQ!i(>P;SY+8a?SE>fFPDQY+V4WeBw_DIJNjmh{N3^V;iJP}_kse3UY zZzIm?OHh#Lq`zLk+-0t%yGEoK7ex_{jci*^q$5;_6h1}m`d`m8^OI}J>39@5p+18aI+5jc6V=3RZZ{M9s8%%Qr=O?~!h1xS083m~ zsmpZJXj2Kg9Ehd!lsTg^t!uGezcHAAT}N$bS(<>bQkdwp@NQuweeDyYeyFN)%0kp5 z=0QbKnNMO25X8bLPNLWy1*RtAras~_c|&HPI?X}jhZMLuW#Ri}1dITXKVSb7U~N~gTCINqyfU-+^LCr z-c0^S2CdB>sm(+J>TWf; z0!U%5JeYYe1+RL{ri6EmT!kc&VC%H4WTcf=uKf!e(^`%4tkKO2rj@DuE#c8Ob=q2= zCux9f=oMjG4W z2yQ03NtzVUFL?=kh{lMpNxiTG4|}fCAkm+@>6cr1NkPRmo49tKW}{M1MCOh?<{&uV zzS}T%^uL5@Rwq0keWh`#z~?HI5<%80x%T_W-Lw94U-9g%J3teRw6h+mpVQ>Js(fIBENX!NayVVNa0ZU$irwD|gQBxPvanv(hG0V^KodUU`l zCul`>bkhOE$toj%ijCc(%DMr&iy!H_Ka47irF^$%OF9}RUC4MOCHbsioV(~~(!JPU z&FQ<%My##GLn=ky|Iz8?!Y0|^^$9wXV&T5*(G9DU>-;#;Nn4eD$Ty{}{RW?-F~M3l zMf&Rm{N=*m+H&*PArb#9uetBuYk%Dv7bC2knp|$<`0jMv{(Q=yT73K@Ny;??DhyJe zPPb$o`B>hnuVLOTF_9Wc`7a<;*B{Jj_K+17^k6mW(nd|H6eQ& z=drXR^~DBgAL z4EuQVv$O7ZH`=$7s|2r=@b*y_8#YoyWGj!BT7&p)=NsKGhn?ma6*F!Q#=1;jOX0=0 zuF0+z2%@|JlZ48aIG2=`!o|F(q?HrR=#muSqPLd>{P?&%rmt6Tug^;j5uRs%l7$n5 zjURi}IE#seq@lj(TtHaN-5a%E+qmtX&1O+Kji}y=w_rIXRmy8bjCFk5GLFlAJknO1 zB0ZO%&9Pf34>e}T!s70V#L^;9ndF3g1vNdbmv?g!)#j%^5Ow(SYF~&mD>yWS`s+yD zOnbXD4;d~x+Lq(h;0*2d!NJh}`Cxy)-S_15;Lt5#yHqM02S=bprbMPv>BOLIfv3Vd zjwEtQ%9n`;a)ipD%q*uIFp&Gzt6wL(tE<@>MPoZR4IMpsss)wmC11oPs&uNLLWd9& zOUvL-#rz@pFhN#ep{cR4v8SgfeDE3j4zx}WTA*%S^{c4)OD@#tb87(~)N9&-|Hl%Q z-~#R}uL#^iH)~*EK)~&+eQTbyLIRo7r{@$1_PQK`1*O)h3PiTPS@-kzeq(~YL9Am% zqzn={7L8gdl8kxBkPKFNi9ynCS~uU;5m9%%JE$>pyeI+@b=#$yC6Br#d!nNLThp~D zXOnLA;sTaJpZG~3b1DsKkTr;x3^y^++`^)+-s+t2Gw z&KLKE*(H7M#(1{6KhKK-+~|dYB@m#@fuS~OB*!D!sz#c<-*Su)`nb5Q^foZCn^T@*s{8)3Fnwq-1yZ!w9ir$+{ zb=j4f$43N&ABqeLLJ#x4Go9`wrfGF;gta|sFeKN2Wpt$NOrJ{q8673g8+sP^pn(_2 zCmGnfBt<9wQu%GdbDI)%mg`5vgOA~qTF)A_;Y&66Sz>mLRA){e<@4@#%za@Vzo}U! z2&HMt{2g$LK@X{|W$i2+IypJ%>A|rE#}l4kr|Kr_@vbgYpTnwU44Z_VYM9$Qr#KGE z*QY(sf@{!vyNhTSA4!1{boKRLB3&s#x)Ne?6k0r;nwr|$+Bz$grRwqyWr4o#ikjl< z>bzOSLdFWdZSb;0h}JJsCuTa9+{n+K4#jOGYcPj0lYSF|(#qR4RLSYir*&=tOlTo5 z^^OGypQ)K!Vt(ZJYEFQc)@~z!LD@zclLP z{QR+>cw*Y5Z(CK=Il5>c0p_bTf^S%(9H@lPMxgM{wn}T{XKrr(=~MJ$?o;IF))7Sd zS6h5Zf|MF~e;qrKO>L&Szy9>@PL!N}CF9UIkSlu0-%?+8Ri*vJ27ZbA{6})IzRTZz zyH5!SZxe`-#S{{p50jE=XQ2CLLPUtnN^qzwQXLEMe_87C(gD-a-Q)ke=yJw?|2*O- U?IK$f`NBkzl~#}{kudW8FIjHf#{d8T literal 0 HcmV?d00001 diff --git a/addons/web/doc/images/tests2.png b/addons/web/doc/images/tests2.png index 59835b257b5e18f236303a34c66ede42f0bbc29e..c8a6f8ae9eefbeff46a9b45e3a9948676d820714 100644 GIT binary patch literal 20114 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 diff --git a/addons/web/doc/images/tests3.png b/addons/web/doc/images/tests3.png index 10b45eddedcbaf300d54245b85134efb2736c3cb..247f70716af614d48ed6255832cb2d71dce5d9f2 100644 GIT binary patch literal 20382 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 diff --git a/addons/web/doc/testing.rst b/addons/web/doc/testing.rst index 582960846e4..61077072ad2 100644 --- a/addons/web/doc/testing.rst +++ b/addons/web/doc/testing.rst @@ -297,8 +297,8 @@ 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 + asynchronous test cases also have a 10 seconds timeout: if the + test does not finish within 10 seconds, it will be considered failed. This pretty much always means the test will not resolve. .. note:: @@ -328,8 +328,8 @@ a test case (or its containing test suite) through Mock RPC ++++++++ -The preferred (and most fastest from a setup and execution time point -of view) way to do RPC during tests is to mock the RPC calls: while +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. @@ -412,7 +412,83 @@ To do this, set the :js:attr:`rpc option <~TestOptions.rpc>` to Actual RPC ++++++++++ -.. TODO:: rpc to database (implement & document) +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"}]) + .pipe(function (id) { + return Model.call('read', [[id]]); + }).pipe(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 ----------- diff --git a/addons/web/static/src/js/testing.js b/addons/web/static/src/js/testing.js index 45728902e7f..744bd9fa2cd 100644 --- a/addons/web/static/src/js/testing.js +++ b/addons/web/static/src/js/testing.js @@ -85,6 +85,7 @@ openerp.testing = {}; }; }; + var db = window['oe_db_info'] || undefined; testing.section = function (name, options, body) { if (_.isFunction(options)) { body = options; @@ -115,32 +116,69 @@ openerp.testing = {}; // returns -1 -> index becomes 0 -> replace with ``undefined`` so // Array#slice returns a full copy 0, module_index + 1 || undefined); + + // Serialize options for this precise test case + // WARNING: typo is from jquery, do not fix! + var env = QUnit.config.currentModuleTestEnviroment; + var opts = _.defaults({ + // section setup + // case setup + // test + // case teardown + // section teardown + setup: function () { + var args = [].slice.call(arguments); + return $.when(env._oe.setup.apply(null, args)) + .pipe(function () { + return options.setup.apply(null, args); + }); + }, + teardown: function () { + var args = [].slice.call(arguments); + return $.when(options.teardown.apply(null, args)) + .pipe(function () { + return env._oe.teardown.apply(null, args); + }); + } + }, options, env._oe); + // FIXME: if this test is ignored, will still query + if (opts.rpc === 'rpc' && !db) { + QUnit.config.autostart = false; + db = { + source: null, + supadmin: null, + password: null + }; + var $msg = $('

') + .append('

A test needs to clone a database

') + .append('

Please provide the source clone information

') + .append(' Source DB: ').append('').append('
') + .append(' DB Password: ').append('').append('
') + .append('Admin Password: ').append('').append('
') + .append('') + .submit(function (e) { + e.preventDefault(); + e.stopPropagation(); + db.source = $msg.find('input[name=source]').val(); + db.supadmin = $msg.find('input[name=supadmin]').val(); + db.password = $msg.find('input[name=password]').val(); + QUnit.start(); + $.unblockUI(); + }); + $.blockUI({ + message: $msg, + css: { + fontFamily: 'monospace', + textAlign: 'left', + whiteSpace: 'pre-wrap', + cursor: 'default' + } + }); + } + QUnit.test(name, function () { // module testing environment var self = this; - var opts = _.defaults({ - // section setup - // case setup - // test - // case teardown - // section teardown - setup: function () { - if (self._oe.setup.apply(null, arguments)) { - throw new Error("Asynchronous setup not implemented"); - } - if (options.setup.apply(null, arguments)) { - throw new Error("Asynchronous setup not implemented"); - } - }, - teardown: function () { - if (options.teardown.apply(null, arguments)) { - throw new Error("Asynchronous teardown not implemented"); - } - if (self._oe.teardown(null, arguments)) { - throw new Error("Asynchronous teardown not implemented"); - } - } - }, options, this._oe); var instance; if (!opts.dependencies) { @@ -202,38 +240,80 @@ openerp.testing = {}; break; case 'rpc': async = true; + (function (setup, teardown) { + // Bunch of random base36 characters + var dbname = 'test_' + Math.random().toString(36).slice(2); + opts.setup = function (instance, $s) { + // FIXME hack: don't want the session to go through shitty loading process of everything + instance.session.session_init = testing.noop; + instance.session.load_modules = testing.noop; + instance.session.session_bind(); + return instance.session.rpc('/web/database/duplicate', { + fields: [ + {name: 'super_admin_pwd', value: db.supadmin}, + {name: 'db_original_name', value: db.source}, + {name: 'db_name', value: dbname} + ] + }).pipe(function (result) { + if (result.error) { + return $.Deferred().reject(result.error).promise(); + } + return instance.session.session_authenticate( + dbname, 'admin', db.password, true); + }).pipe(function () { + return setup(instance, $s); + }); + }; + opts.teardown = function (instance, $s) { + return $.when(teardown(instance, $s)).pipe(function () { + return instance.session.session_logout() + }).pipe(function () { + return instance.session.rpc('/web/database/drop', { + fields: [ + {name: 'drop_pwd', value: db.supadmin}, + {name: 'drop_db', value: db.dbname} + ] + }); + }).pipe(function (result) { + if (result.error) { + return $.Deferred().reject(result.error).promise(); + } + return result; + }); + }; + })(opts.setup, opts.teardown); } - // TODO: async setup/teardown - opts.setup(instance, $fixture, mock); - - var result = callback(instance, $fixture, mock); - - // TODO: cleanup which works on errors - if (!(result && _.isFunction(result.then))) { - if (async) { - ok(false, "asynchronous test cases must return a promise"); - } - opts.teardown(instance, $fixture, mock); - return; - } - + // Always execute tests asynchronously stop(); - if (!_.isNumber(opts.asserts)) { - ok(false, "asynchronous test cases must specify the " - + "number of assertions they expect"); - } - result.always(function () { - start(); - opts.teardown(instance, $fixture, mock); - }).fail(function (error) { - if (options.fail_on_rejection === false) { - return; + $.when(opts.setup(instance, $fixture, mock)) + .pipe(function () { + var result = callback(instance, $fixture, mock); + if (!(result && _.isFunction(result.then))) { + if (async) { + ok(false, "asynchronous test cases must return a promise"); + } + } else { + if (!_.isNumber(opts.asserts)) { + ok(false, "asynchronous test cases must specify the " + + "number of assertions they expect"); + } } - ok(false, typeof error === 'object' && error.message - ? error.message - : JSON.stringify([].slice.call(arguments))); - }) + return $.when(result).fail(function (error) { + if (options.fail_on_rejection === false) { + return; + } + ok(false, typeof error === 'object' && error.message + ? error.message + : JSON.stringify([].slice.call(arguments))); + }) + }).pipe(function () { + return opts.teardown(instance, $fixture, mock); + }, function () { + return opts.teardown(instance, $fixture, mock); + }).always(function () { + start(); + }); }); }; })(openerp.testing); diff --git a/addons/web_tests_demo/__init__.py b/addons/web_tests_demo/__init__.py index e69de29bb2d..137d472ec74 100644 --- a/addons/web_tests_demo/__init__.py +++ b/addons/web_tests_demo/__init__.py @@ -0,0 +1,14 @@ +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" + } + diff --git a/addons/web_tests_demo/static/test/demo.js b/addons/web_tests_demo/static/test/demo.js index 0bf5a1a58b6..62cecf6f088 100644 --- a/addons/web_tests_demo/static/test/demo.js +++ b/addons/web_tests_demo/static/test/demo.js @@ -84,4 +84,19 @@ openerp.testing.section('basic section', function (test) { deepEqual(dbm.db_list, ['foo', 'bar', 'baz']); }); }); + + 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"}]) + .pipe(function (id) { + return Model.call('read', [[id]]); + }).pipe(function (records) { + strictEqual(records.length, 1); + var record = records[0]; + strictEqual(record.name, "Bob"); + strictEqual(record.thing, false); + // default value + strictEqual(record.other, 'bob'); + }); + }); }); From 21e776a56b9fedb25c48b1ea362227ed76de6321 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 29 Oct 2012 12:09:02 +0100 Subject: [PATCH 008/125] [IMP] document test runner global conf keys bzr revid: xmo@openerp.com-20121029110902-ob21050di74vcyqc --- addons/web/doc/testing.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/addons/web/doc/testing.rst b/addons/web/doc/testing.rst index 61077072ad2..4760f0780eb 100644 --- a/addons/web/doc/testing.rst +++ b/addons/web/doc/testing.rst @@ -409,6 +409,8 @@ To do this, set the :js:attr:`rpc option <~TestOptions.rpc>` to handler containing assertions, it multiplies the effective number of assertions) +.. _testing-rpc-rpc: + Actual RPC ++++++++++ @@ -568,6 +570,19 @@ Testing API 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 + ` tests, to avoid a prompt being displayed + (especially for headless situations). + Running through Python ---------------------- From a9f1489fd0dc32901823fbe09fc323479a444048 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 29 Oct 2012 13:00:04 +0100 Subject: [PATCH 009/125] [FIX] typos bzr revid: xmo@openerp.com-20121029120004-d8tv0hks9d6684az --- addons/web/doc/testing.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/web/doc/testing.rst b/addons/web/doc/testing.rst index 4760f0780eb..2d5876776a3 100644 --- a/addons/web/doc/testing.rst +++ b/addons/web/doc/testing.rst @@ -199,7 +199,7 @@ testing them. Thus, test cases get 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 -scrartchpad your code can do whatever it wants:: +scratchpad your code can do whatever it wants:: // test/demo.js test('DOM content', function (instance, $scratchpad) { @@ -334,7 +334,7 @@ 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 +To do this, set the :js:attr:`rpc option ` to ``mock``. This will add a third parameter to the test case callback: .. js:function:: mock(rpc_spec, handler) @@ -349,8 +349,8 @@ To do this, set the :js:attr:`rpc option <~TestOptions.rpc>` to In that case, ``handler`` should be a function taking two arguments ``args`` and ``kwargs``, matching the corresponding - arguments on the server side. Hander should simply return the - value as if it were returned by the Python XMLRPC handler:: + 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 From e7fbd7290f430dd67e9b3f093aea837bdef6f81d Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 29 Oct 2012 13:04:26 +0100 Subject: [PATCH 010/125] [IMP] add a doc blurb to testing.section and testing.case bzr revid: xmo@openerp.com-20121029120426-xdk03e5gvv1n03jw --- addons/web/doc/testing.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/addons/web/doc/testing.rst b/addons/web/doc/testing.rst index 2d5876776a3..e870ec091b7 100644 --- a/addons/web/doc/testing.rst +++ b/addons/web/doc/testing.rst @@ -497,6 +497,13 @@ 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: @@ -504,6 +511,10 @@ Testing API .. 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: From 523f4a6e03e909156082f528acf4e50024e7b486 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 30 Oct 2012 08:43:25 +0100 Subject: [PATCH 011/125] [FIX] db name being used to drop databases after rpc tests xmo@openerp.com-20121029110154-5927gaix8k0ijl0c introduced a regression which wasn't caught by deferred piping (?) and caused databases to not be dropped after rpc tests bzr revid: xmo@openerp.com-20121030074325-wh39s75vj2zob9e0 --- addons/web/static/src/js/testing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web/static/src/js/testing.js b/addons/web/static/src/js/testing.js index 744bd9fa2cd..de6b7aab1a4 100644 --- a/addons/web/static/src/js/testing.js +++ b/addons/web/static/src/js/testing.js @@ -271,7 +271,7 @@ openerp.testing = {}; return instance.session.rpc('/web/database/drop', { fields: [ {name: 'drop_pwd', value: db.supadmin}, - {name: 'drop_db', value: db.dbname} + {name: 'drop_db', value: dbname} ] }); }).pipe(function (result) { From d2cf844e7b2e447ce0c1502aa3a514056a6d7fb1 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 30 Oct 2012 08:46:15 +0100 Subject: [PATCH 012/125] [IMP] allow providing db-cloning info directly in URL for test runner bzr revid: xmo@openerp.com-20121030074615-lt517w5t9fd9ppvh --- addons/web/controllers/testing.py | 9 ++++++++- addons/web/static/src/js/testing.js | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/addons/web/controllers/testing.py b/addons/web/controllers/testing.py index 9947f1e1ed5..95121f5b192 100644 --- a/addons/web/controllers/testing.py +++ b/addons/web/controllers/testing.py @@ -51,6 +51,7 @@ TESTING = Template(u""" diff --git a/addons/web/doc/testing.rst b/addons/web/doc/testing.rst index 302ebda9485..84ce8bceb22 100644 --- a/addons/web/doc/testing.rst +++ b/addons/web/doc/testing.rst @@ -297,9 +297,11 @@ test architecture was not warned about asynchronous operations. .. note:: - Asynchronous test cases also have a 10 seconds timeout: if the - test does not finish within 10 seconds, it will be considered - failed. This pretty much always means the test will not resolve. + 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:: diff --git a/addons/web/static/src/js/testing.js b/addons/web/static/src/js/testing.js index e124897f063..7686499b939 100644 --- a/addons/web/static/src/js/testing.js +++ b/addons/web/static/src/js/testing.js @@ -286,6 +286,7 @@ openerp.testing = {}; // Always execute tests asynchronously stop(); + var timeout; var teardown = function () { return opts.teardown(instance, $fixture, mock) }; @@ -302,8 +303,25 @@ openerp.testing = {}; + "number of assertions they expect"); } } - return $.when(result).pipe(teardown, teardown); + var d = $.Deferred(); + $.when(result).then(function () { + d.resolve.apply(d, arguments) + }, function () { + d.reject.apply(d, arguments); + }); + if (async || (result && result.then)) { + // async test can be either implicit async (rpc) or + // promise-returning + timeout = setTimeout(function () { + QUnit.config.semaphore = 1; + d.reject({message: "Test timed out"}); + }, 2000); + } + return d.pipe(teardown, teardown); }).always(function () { + if (timeout) { + clearTimeout(timeout); + } start(); }).fail(function (error) { if (options.fail_on_rejection === false) { @@ -320,6 +338,7 @@ openerp.testing = {}; } } + ok(false, "failed!") ok(false, message); }); }); diff --git a/addons/web/tests/test_js.py b/addons/web/tests/test_js.py index d52e2f43806..a126ce4b2f3 100644 --- a/addons/web/tests/test_js.py +++ b/addons/web/tests/test_js.py @@ -14,7 +14,7 @@ class WebSuite(QUnitSuite): password=tools.config['admin_passwd'] or 'admin'), '' ]) - super(WebSuite, self).__init__(url, 20000) + super(WebSuite, self).__init__(url, 50000) def run(self, result): if sql_db._Pool is not None: sql_db._Pool.close_all(sql_db.dsn(tools.config['db_name'])) diff --git a/addons/web_tests_demo/static/test/demo.js b/addons/web_tests_demo/static/test/demo.js index 62cecf6f088..6b80c039107 100644 --- a/addons/web_tests_demo/static/test/demo.js +++ b/addons/web_tests_demo/static/test/demo.js @@ -85,6 +85,15 @@ openerp.testing.section('basic section', function (test) { }); }); + test('timeouting', {asserts: 1}, function () { + var d = $.Deferred(); + setTimeout(function () { + ok(true, "yeah"); + d.resolve('ok') + }, 10000); + return d.promise(); + }); + 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"}]) From 739775253f2d0f9e4fc9bdfd197369a14d72223a Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Thu, 15 Nov 2012 10:21:49 +0100 Subject: [PATCH 039/125] [FIX] web: FieldOne2ManyBinaryMultiFiles fix the loading for default ids files list bzr revid: chm@openerp.com-20121115092149-lsbxb9ougxwkkktn --- addons/web/static/src/js/view_form.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 867569e2ea5..1be42849a96 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -5017,6 +5017,19 @@ instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractFiel this._super(this); this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change ); }, + set_value: function(value_) { + var self = this; + if (value_ && typeof value_[0] != 'object') { + this.ds_file.call('read', [value_, ['name', 'datas_fname']]).done(function(data) { + _.each(data, function (val) { + val.no_unlink = true; + }); + self.set_value(data); + }); + } else { + this._super(value_); + } + }, get_value: function() { return _.map(this.get('value'), function (value) { return commands.link_to( value.id ); }); }, @@ -5111,7 +5124,7 @@ instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractFiel if(file_id != this.get('value')[i].id){ files.push(this.get('value')[i]); } - else { + else if(!this.get('value')[i].no_unlink) { this.ds_file.unlink([file_id]); } } From edb1e07948416d12351dbc9e5d4b9896a53f0de7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 15 Nov 2012 10:53:09 +0100 Subject: [PATCH 040/125] [FIX] composer: default content subtype is html (note that plaintext management will be removed asap), name_get is now using an id list instead of a single id. bzr revid: tde@openerp.com-20121115095309-zu4m6nzndlpmfe5b --- addons/mail/wizard/mail_compose_message.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 45082b9be4d..839a3a6970e 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -114,7 +114,7 @@ class mail_compose_message(osv.TransientModel): _defaults = { 'composition_mode': 'comment', - 'content_subtype': lambda self, cr, uid, ctx={}: 'plain', + 'content_subtype': lambda self, cr, uid, ctx={}: 'html', 'body_text': lambda self, cr, uid, ctx={}: False, 'body': lambda self, cr, uid, ctx={}: '', 'subject': lambda self, cr, uid, ctx={}: False, @@ -135,7 +135,7 @@ class mail_compose_message(osv.TransientModel): related to. :param int res_id: id of the document record this mail is related to """ - doc_name_get = self.pool.get(model).name_get(cr, uid, res_id, context=context) + doc_name_get = self.pool.get(model).name_get(cr, uid, [res_id], context=context) if doc_name_get: record_name = doc_name_get[0][1] else: From 71c2be227cd1a8188db6f961d7d358099873e37a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 15 Nov 2012 10:53:29 +0100 Subject: [PATCH 041/125] [IMP] mail: updated tests to fit the new composer behavior. bzr revid: tde@openerp.com-20121115095329-whh6z7ckto87yven --- addons/mail/tests/test_mail.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/mail/tests/test_mail.py b/addons/mail/tests/test_mail.py index 1cdb966ccbc..02b55cef9f7 100644 --- a/addons/mail/tests/test_mail.py +++ b/addons/mail/tests/test_mail.py @@ -463,7 +463,8 @@ class test_mail(test_mail_mockup.TestMailMockups): # 1. Comment group_pigs with body_text and subject compose_id = mail_compose.create(cr, uid, {'subject': _subject, 'body_text': _body_text, 'partner_ids': [(4, p_c_id), (4, p_d_id)]}, - {'default_composition_mode': 'comment', 'default_model': 'mail.group', 'default_res_id': self.group_pigs_id}) + {'default_composition_mode': 'comment', 'default_model': 'mail.group', 'default_res_id': self.group_pigs_id, + 'default_content_subtype': 'plaintext'}) compose = mail_compose.browse(cr, uid, compose_id) # Test: mail.compose.message: composition_mode, model, res_id self.assertEqual(compose.composition_mode, 'comment', 'mail.compose.message incorrect composition_mode') From 25c684b1712796fad879596d19fa27cafbe03846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 15 Nov 2012 10:54:00 +0100 Subject: [PATCH 042/125] [FIX] Chatter: fixed posting on user/partner. Cleaned a bit default options of mail.js. bzr revid: tde@openerp.com-20121115095400-u7h6rw8ltp0xprtt --- addons/mail/mail_thread_view.xml | 2 +- addons/mail/res_partner.py | 3 ++- addons/mail/res_users.py | 11 ++++------- addons/mail/static/src/js/mail.js | 8 ++------ 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/addons/mail/mail_thread_view.xml b/addons/mail/mail_thread_view.xml index cdeade04d84..3a37218c813 100644 --- a/addons/mail/mail_thread_view.xml +++ b/addons/mail/mail_thread_view.xml @@ -7,7 +7,7 @@ mail.message { 'default_model': 'res.users', - 'default_res_id': uid + 'default_res_id': uid, } Date: Thu, 15 Nov 2012 11:05:33 +0100 Subject: [PATCH 043/125] [FIX] Chatter: fixed rpely to a lonely message, that was badly indented. bzr revid: tde@openerp.com-20121115100533-ly99op6fpfrzp9jw --- addons/mail/static/src/js/mail.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 75852a9d9b1..37090543b8c 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -598,16 +598,15 @@ openerp.mail = function (session) { this.parent_thread.context ]).done(function (record) { var thread = self.parent_thread; - if (self.options.display_indented_thread < self.thread_level && thread.parent_message) { - thread = thread.parent_message.parent_thread; + var hread = thread.parent_message.parent_thread; } + var root = thread == self.options.root_thread; // create object and attach to the thread object thread.message_fetch([['id', 'child_of', [self.id]]], false, [record], function (arg, data) { - data[0].no_sorted = true; var message = thread.create_message_object( data[0] ); // insert the message on dom - thread.insert_message( message, self.$el ); + thread.insert_message( message, root ? undefined : self.$el, root ); if (thread.parent_message) { self.$el.remove(); self.parent_thread.compose_message = null; @@ -1272,7 +1271,7 @@ openerp.mail = function (session) { * The sort is define by the thread_level (O for newer on top). * @param : {object} ThreadMessage object */ - insert_message: function (message, dom_insert_after) { + insert_message: function (message, dom_insert_after, prepend) { var self=this; if (this.options.show_compact_message > this.thread_level) { this.instantiate_compose_message(); @@ -1284,6 +1283,8 @@ openerp.mail = function (session) { if (dom_insert_after) { message.insertAfter(dom_insert_after); + }if (prepend) { + message.prependTo(self.$el); } else { message.appendTo(self.$el); } From 2cfef36846745dbd6b03288f9e223ce9977195ca Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Thu, 15 Nov 2012 11:07:40 +0100 Subject: [PATCH 044/125] [MERGE] improved welcome messages and mail css bzr revid: fp@tinyerp.com-20121115100740-3pbot8zfu64s4ods --- addons/account_accountant/account_accountant_data.xml | 4 ++-- addons/crm/crm_data.xml | 2 +- addons/hr_evaluation/hr_evaluation_data.xml | 4 ++-- addons/hr_recruitment/hr_recruitment_data.xml | 2 +- addons/mail/data/mail_group_data.xml | 4 ++-- addons/mail/static/src/css/mail.css | 5 ++++- addons/mrp/mrp_data.xml | 2 +- addons/project/project_data.xml | 2 +- addons/project_gtd/project_gtd_data.xml | 2 +- addons/project_issue/project_issue_data.xml | 4 ++-- addons/sale/sale_data.xml | 2 +- 11 files changed, 18 insertions(+), 15 deletions(-) diff --git a/addons/account_accountant/account_accountant_data.xml b/addons/account_accountant/account_accountant_data.xml index 14d8f59a5fe..c7d6eb80bc6 100644 --- a/addons/account_accountant/account_accountant_data.xml +++ b/addons/account_accountant/account_accountant_data.xml @@ -28,8 +28,8 @@ notification Accounting and Finance application installed! - With OpenERP's accounting, you get instant access to your financial data, and can setup analytic accounting, forecast taxes, control budgets, easily create and send invoices, record bank statements, etc.

-

The accounting features are fully integrated with other OpenERP applications to automate all your processes: creation of customer invoices, control of supplier invoices, point-of-sale integration, automated follow-ups, etc.

]]>
+ With OpenERP's accounting, you get instant access to your financial data, and can setup analytic accounting, forecast taxes, control budgets, easily create and send invoices, record bank statements, etc.

+

The accounting features are fully integrated with other OpenERP applications to automate all your processes: creation of customer invoices, control of supplier invoices, point-of-sale integration, automated follow-ups, etc.

]]>
diff --git a/addons/crm/crm_data.xml b/addons/crm/crm_data.xml index 91117d2d83e..f2bf56e34ac 100644 --- a/addons/crm/crm_data.xml +++ b/addons/crm/crm_data.xml @@ -61,7 +61,7 @@ CRM application installed! From the top Sales menu you can track leads and opportunities, get accurate forecast on your sales pipeline, plan meetings and phonecalls, get realtime statistics and efficiently organize the communication with your prospects.

-
To manage quotations and sale orders, install the "Sales Management" application.

]]>
+

To manage quotations and sale orders, install the "Sales Management" application.

]]>
diff --git a/addons/hr_evaluation/hr_evaluation_data.xml b/addons/hr_evaluation/hr_evaluation_data.xml index 45100b14e2a..2afb5200091 100644 --- a/addons/hr_evaluation/hr_evaluation_data.xml +++ b/addons/hr_evaluation/hr_evaluation_data.xml @@ -811,8 +811,8 @@ Once the form had been filled, the employee send it to his supervisor. The comment you entered is in an invalid format. * His direct reports will be invited through OpenERP to express a feedback on their supervisor's leadership and to give their opinion about their own engagement and effectiveness, the continuous improvement and openness in action in the company, ... -* The employees will send back their anonymous answers to OpenERP. The data will be handled by the HR manager and a brief summary of the data will be sent to the concerned supervisor, to his team and to the supervisor's supervisor. -* The appraiser should rate the employee’s major work accomplishments and performance according to the metric provided below : +* The employees will send back their anonymous answers to OpenERP. The data will be handled by the HR manager and a brief summary of the data will be sent to the concerned supervisor, to his team and to the supervisor's supervisor. +* The appraiser should rate the employee’s major work accomplishments and performance according to the metric provided below : 1 - Significantly exceeds standards and expectations required of the position 2 - Exceeds standards and expectations diff --git a/addons/hr_recruitment/hr_recruitment_data.xml b/addons/hr_recruitment/hr_recruitment_data.xml index f1c8ed46aca..adaeed81205 100644 --- a/addons/hr_recruitment/hr_recruitment_data.xml +++ b/addons/hr_recruitment/hr_recruitment_data.xml @@ -8,7 +8,7 @@ notification Recruitment Process application installed! - Manage job positions and your company's recruitment process. This application is integrated with the Survey application to help you define interviews for different jobs.

+ Manage job positions and your company's recruitment process. This application is integrated with the Survey application to help you define interviews for different jobs.

You can automatically receive job application though an email gateway, see the Human Resources settings.

]]>
diff --git a/addons/mail/data/mail_group_data.xml b/addons/mail/data/mail_group_data.xml index aae30226bf2..6d3e5395eda 100644 --- a/addons/mail/data/mail_group_data.xml +++ b/addons/mail/data/mail_group_data.xml @@ -19,8 +19,8 @@ notification Welcome to OpenERP! - Your homepage is a summary of messages you received and key information about documents you follow.
-The top menu bar contains all applications you installed. You can use this <i>Settings</i> menu to install more applications, activate others features or give access to new users.
+ Your homepage is a summary of messages you received and key information about documents you follow.

+The top menu bar contains all applications you installed. You can use this Settings menu to install more applications, activate others features or give access to new users.

To setup your preferences (name, email signature, avatar), click on the top right corner.

]]>
diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css index 0bcfd0d3733..b0ce6d499b5 100644 --- a/addons/mail/static/src/css/mail.css +++ b/addons/mail/static/src/css/mail.css @@ -87,8 +87,11 @@ margin-bottom: 0px; margin-top: 2px; } -.openerp .oe_mail .oe_msg .oe_msg_content .oe_msg_body p{ +.openerp .oe_mail .oe_msg .oe_msg_content .oe_msg_body p:first-of-type { margin-top: 0px; +} + +.openerp .oe_mail .oe_msg .oe_msg_content .oe_msg_body p { margin-bottom: 0px; } diff --git a/addons/mrp/mrp_data.xml b/addons/mrp/mrp_data.xml index 1d92609eb7e..a6b65b7c475 100644 --- a/addons/mrp/mrp_data.xml +++ b/addons/mrp/mrp_data.xml @@ -8,7 +8,7 @@ notification MRP application installed! - Manage your manufacturing process with OpenERP by defining your bills of materials (BoM), routings and work centers.
+ Manage your manufacturing process with OpenERP by defining your bills of materials (BoM), routings and work centers.

This application supports complete integration and production scheduling for stockable goods, consumables, and services.

From the Manufacturing Settings, you can choose to compute production schedules periodically or just-in-time.

]]>
diff --git a/addons/project/project_data.xml b/addons/project/project_data.xml index c5980ecddd2..08981bd126a 100644 --- a/addons/project/project_data.xml +++ b/addons/project/project_data.xml @@ -128,7 +128,7 @@ Project Management application installed! Manage multi-level projects and tasks. You can delegate tasks, track task work, and review your planning.

-

You can manage todo lists on tasks by installing the "Todo Lists" application, supporting the Getting Things Done (GTD) methodology.

+

You can manage todo lists on tasks by installing the Todo Lists application, supporting the Getting Things Done (GTD) methodology.

You can also manage issues/bugs in projects by installing the "Issue Tracker" application.

]]>
diff --git a/addons/project_gtd/project_gtd_data.xml b/addons/project_gtd/project_gtd_data.xml index 1536ac4be3f..9d8c99f23ff 100644 --- a/addons/project_gtd/project_gtd_data.xml +++ b/addons/project_gtd/project_gtd_data.xml @@ -32,7 +32,7 @@ notification Todo Lists application installed! - Add todo items on project tasks, to help you organize your work. + Add todo items on project tasks, to help you organize your work.

This application supports the Getting Things Done (GTD) methodology, based on David Allen's book.

]]>
diff --git a/addons/project_issue/project_issue_data.xml b/addons/project_issue/project_issue_data.xml index d70cd71e240..c1460167097 100644 --- a/addons/project_issue/project_issue_data.xml +++ b/addons/project_issue/project_issue_data.xml @@ -38,8 +38,8 @@ notification Issue Tracker application installed! - Manage the issues you might face in a project, such as bugs in a system, client complaints or material breakdowns. -You can record issues, assign them to a responsible person, and keep track of their status as they evolve over time. + Manage the issues you might face in a project, such as bugs in a system, client complaints or material breakdowns.

+You can record issues, assign them to a responsible person, and keep track of their status as they evolve over time.

Access all issues from the top Project menu, and access the issues of a specific project via the projects gallery view.

]]>
diff --git a/addons/sale/sale_data.xml b/addons/sale/sale_data.xml index d5ed8e810d6..3fe03966da2 100644 --- a/addons/sale/sale_data.xml +++ b/addons/sale/sale_data.xml @@ -41,7 +41,7 @@ Sales Management application installed! This application lets you create and send quotations and process your sales orders; from delivery to invoicing.

-

If you need to manage your sales pipeline (leads, opportunities, phonecalls), the <i>CRM</i> application may be useful. Use the Settings menu to install it.

]]>
+

If you need to manage your sales pipeline (leads, opportunities, phonecalls), the CRM application may be useful. Use the Settings menu to install it.

]]>
From bd494ad7f41862662950d813f6b4aaea94277526 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Thu, 15 Nov 2012 11:13:29 +0100 Subject: [PATCH 045/125] [FIX] project_issue: missing module prefix when updating project demo data Fixes test breakage by FP - commit 8045 revision-id: fp@openerp.com-20121114224452-4djljq2ny7f1lumx bzr revid: odo@openerp.com-20121115101329-3d2epoctqwkq9mlq --- addons/project_issue/project_issue_demo.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/addons/project_issue/project_issue_demo.xml b/addons/project_issue/project_issue_demo.xml index 86e4c59b571..63fa37c84c3 100644 --- a/addons/project_issue/project_issue_demo.xml +++ b/addons/project_issue/project_issue_demo.xml @@ -2,19 +2,19 @@ - + - + - + - + - + From d530905b93493430aaea0d359a52140659490f1d Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Thu, 15 Nov 2012 11:22:23 +0100 Subject: [PATCH 046/125] [IMP] Add default filter on groups bzr revid: fp@tinyerp.com-20121115102223-i2jgjkgqf9pxez08 --- addons/mail/mail_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py index d4c9c565bf3..b163882577b 100644 --- a/addons/mail/mail_group.py +++ b/addons/mail/mail_group.py @@ -130,7 +130,7 @@ class mail_group(osv.Model): params = { 'search_view_id': search_ref and search_ref[1] or False, 'domain': [('model', '=', 'mail.group'), ('res_id', '=', mail_group_id)], - 'context': {'default_model': 'mail.group', 'default_res_id': mail_group_id}, + 'context': {'default_model': 'mail.group', 'default_res_id': mail_group_id, 'search_default_message_unread': True}, 'res_model': 'mail.message', 'thread_level': 1, } From 8fe036423c78467c9edce38abbdd0acbf60d07d9 Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Thu, 15 Nov 2012 11:25:22 +0100 Subject: [PATCH 047/125] [FIX] show of image groups bzr revid: fp@tinyerp.com-20121115102522-jquurlypugesxu1e --- addons/mail/static/src/css/mail_group.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/mail/static/src/css/mail_group.css b/addons/mail/static/src/css/mail_group.css index 467bc53d80f..c1cefee9070 100644 --- a/addons/mail/static/src/css/mail_group.css +++ b/addons/mail/static/src/css/mail_group.css @@ -52,7 +52,7 @@ border-collapse: separate; -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); - box-shadow: 0 1px 4px 3px rgba(0, 0, 0, 0.4); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); -o-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); } From d3cf004baeba4e1d0b26a6d4f0ce1cb0e2138575 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Thu, 15 Nov 2012 11:27:41 +0100 Subject: [PATCH 048/125] [IMP] web_linkedin: adapted to new linkedin api bzr revid: nicolas.vanhoren@openerp.com-20121115102741-9vzb8qprvrh1lsmz --- addons/web_linkedin/static/src/js/linkedin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/web_linkedin/static/src/js/linkedin.js b/addons/web_linkedin/static/src/js/linkedin.js index ec58c3736f4..d6237e48d51 100644 --- a/addons/web_linkedin/static/src/js/linkedin.js +++ b/addons/web_linkedin/static/src/js/linkedin.js @@ -20,7 +20,7 @@ openerp.web_linkedin = function(instance) { var tag = document.createElement('script'); tag.type = 'text/javascript'; tag.src = "http://platform.linkedin.com/in.js"; - tag.innerHTML = 'api_key : ' + self.api_key + '\nauthorize : true'; + tag.innerHTML = 'api_key : ' + self.api_key + '\nauthorize : true\nscope: r_network r_contactinfo'; document.getElementsByTagName('head')[0].appendChild(tag); self.linkedin_added = true; $(tag).load(function() { @@ -107,7 +107,7 @@ openerp.web_linkedin = function(instance) { } to_change.website = entity.websiteUrl; to_change.phone = false; - _.each(entity.locations.values || [], function(el) { + _.each((entity.locations || {}).values || [], function(el) { to_change.phone = el.contactInfo.phone1; }); var children_def = $.Deferred(); From 29e0ab965f899f6c68681640bed4a658177d59f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 15 Nov 2012 11:34:12 +0100 Subject: [PATCH 049/125] [IMP] res_users: removed hack about redirecting the fields view get. Added a simplified contact form the the user, used for example when viewing a salesman profile. bzr revid: tde@openerp.com-20121115103412-jl0o40t8d3d76ysd --- openerp/addons/base/res/res_users.py | 11 ------ openerp/addons/base/res/res_users_view.xml | 43 ++++++++++++++++++++++ 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py index f975d39b300..3aee2ed630b 100644 --- a/openerp/addons/base/res/res_users.py +++ b/openerp/addons/base/res/res_users.py @@ -250,17 +250,6 @@ class res_users(osv.osv): 'image': lambda self, cr, uid, ctx={}: self.pool.get('res.partner')._get_default_image(cr, uid, False, ctx, colorize=True), } - def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): - """ Override of res.users fields_view_get. - - if the view is specified: resume with normal behavior - - else: the default view is overrided and redirected to the partner - view - """ - #made a lot of views crash because methods of open chatter are not available on users - #if not view_id and view_type == 'form': - # return self.pool.get('res.partner').fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu) - return super(res_users, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu) - # User can write on a few of his own fields (but not his groups for example) SELF_WRITEABLE_FIELDS = ['password', 'signature', 'action_id', 'company_id', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz'] # User can read a few of his own fields diff --git a/openerp/addons/base/res/res_users_view.xml b/openerp/addons/base/res/res_users_view.xml index cfab4b7d341..b5c88e4c24b 100644 --- a/openerp/addons/base/res/res_users_view.xml +++ b/openerp/addons/base/res/res_users_view.xml @@ -83,6 +83,49 @@ + + res.users.simplified.form + res.users + 1 + + + + +
+
+
+

+ +

+ +
+ + + + + + + + + + +
+ +
+
res.users.form res.users From 15b23a76aeded2cff747d1dedaeab6ae6357eb27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 15 Nov 2012 11:35:01 +0100 Subject: [PATCH 050/125] [REM] Chatter: removed unused template in xml. bzr revid: tde@openerp.com-20121115103501-wt837acx8kj1ycht --- addons/mail/static/src/xml/mail.xml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml index 2ba1733043a..89fbe45dc36 100644 --- a/addons/mail/static/src/xml/mail.xml +++ b/addons/mail/static/src/xml/mail.xml @@ -95,14 +95,6 @@ - -
- - This email is private. - I wrote for contacts and all my followers. -
-
- From c6b5ca0a078c83f211dc469f3ca764400f3586e6 Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Thu, 15 Nov 2012 11:39:47 +0100 Subject: [PATCH 051/125] [FIX] typo in a message bzr revid: fp@tinyerp.com-20121115103947-zre3ja8ghnojesd1 --- addons/mail/data/mail_group_data.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/mail/data/mail_group_data.xml b/addons/mail/data/mail_group_data.xml index 6d3e5395eda..64d989f9e9d 100644 --- a/addons/mail/data/mail_group_data.xml +++ b/addons/mail/data/mail_group_data.xml @@ -20,7 +20,7 @@ Welcome to OpenERP! Your homepage is a summary of messages you received and key information about documents you follow.

-The top menu bar contains all applications you installed. You can use this Settings menu to install more applications, activate others features or give access to new users.

+The top menu bar contains all applications you installed. You can use the Settings menu to install more applications, activate others features or give access to new users.

To setup your preferences (name, email signature, avatar), click on the top right corner.

]]>
From c2667f13adadbe9f0cbb0f65c7283dd5b1a71dfa Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Thu, 15 Nov 2012 11:43:45 +0100 Subject: [PATCH 052/125] [IMP] Inverting personnal tasks and notes importancies bzr revid: fp@tinyerp.com-20121115104345-1dkpg3rrk2j84wnj --- addons/note/__openerp__.py | 1 + addons/project_gtd/__openerp__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/note/__openerp__.py b/addons/note/__openerp__.py index d87ee70086d..797270637e2 100644 --- a/addons/note/__openerp__.py +++ b/addons/note/__openerp__.py @@ -38,6 +38,7 @@ Notes can be found in the 'Home' menu. 'author': 'OpenERP SA', 'website': 'http://openerp.com', 'summary': 'Sticky notes, Collaborative, Memos', + 'sequence': 9, 'depends': [ 'mail', ], diff --git a/addons/project_gtd/__openerp__.py b/addons/project_gtd/__openerp__.py index 441f8859dc6..c99535f63fd 100644 --- a/addons/project_gtd/__openerp__.py +++ b/addons/project_gtd/__openerp__.py @@ -24,7 +24,7 @@ 'name': 'Todo Lists', 'version': '1.0', 'category': 'Project Management', - 'sequence': 9, + 'sequence': 100, 'summary': 'Personal Tasks, Contexts, Timeboxes', 'description': """ Implement concepts of the "Getting Things Done" methodology From 08a21157540bd0dc423697c9d0f7fa78ab56e9bf Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 15 Nov 2012 12:57:59 +0100 Subject: [PATCH 053/125] [IMP] removing some warnings (those one are generated by tests). bzr revid: vmt@openerp.com-20121115115759-toz6g9jcephwxmin --- openerp/tests/test_orm.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openerp/tests/test_orm.py b/openerp/tests/test_orm.py index c114064cd5f..1dde83d026c 100644 --- a/openerp/tests/test_orm.py +++ b/openerp/tests/test_orm.py @@ -109,6 +109,7 @@ class TestInherits(common.TransactionCase): self.assertEqual(foo.name, 'Foo') self.assertEqual(foo.partner_id.id, par_id) + @mute_logger('openerp.osv.orm') def test_read(self): """ inherited fields should be read without any indirection """ foo_id = self.user.create(self.cr, UID, {'name': 'Foo', 'login': 'foo', 'password': 'foo'}) @@ -120,6 +121,7 @@ class TestInherits(common.TransactionCase): foo = self.user.browse(self.cr, UID, foo_id) self.assertEqual(foo.name, foo.partner_id.name) + @mute_logger('openerp.osv.orm') def test_copy(self): """ copying a user should automatically copy its partner, too """ foo_id = self.user.create(self.cr, UID, {'name': 'Foo', 'login': 'foo', 'password': 'foo'}) @@ -134,6 +136,7 @@ class TestInherits(common.TransactionCase): self.assertNotEqual(foo.id, bar.id) self.assertNotEqual(foo.partner_id.id, bar.partner_id.id) + @mute_logger('openerp.osv.orm') def test_copy_with_ancestor(self): """ copying a user with 'parent_id' in defaults should not duplicate the partner """ foo_id = self.user.create(self.cr, UID, {'name': 'Foo', 'login': 'foo', 'password': 'foo'}) From d1f49ab27ae25c38e2322b3844a43d6e8635d9c5 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 15 Nov 2012 12:59:16 +0100 Subject: [PATCH 054/125] [IMP] removing some warnings (those one are generated by tests). bzr revid: vmt@openerp.com-20121115115916-6i2ljxn7s9qz4rlv --- addons/mail/security/ir.model.access.csv | 2 ++ addons/mail/tests/test_mail_access_rights.py | 6 +++++- addons/sale/sale_demo.xml | 1 - 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/addons/mail/security/ir.model.access.csv b/addons/mail/security/ir.model.access.csv index b87c323b53d..37c6952717c 100644 --- a/addons/mail/security/ir.model.access.csv +++ b/addons/mail/security/ir.model.access.csv @@ -17,3 +17,5 @@ access_mail_alias_system,mail.alias.system,model_mail_alias,base.group_system,1, access_mail_message_subtype_all,mail.message.subtype.all,model_mail_message_subtype,,1,0,0,0 access_mail_vote_all,mail.vote.all,model_mail_vote,,1,1,1,1 access_mail_favorite_all,mail.favorite.all,model_mail_favorite,,1,1,1,1 +access_mail_thread_all,mail.thread.all,model_mail_thread,,1,1,1,1 +access_publisher_warranty_contract_all,publisher.warranty.contract.all,model_publisher_warranty_contract,,1,1,1,1 diff --git a/addons/mail/tests/test_mail_access_rights.py b/addons/mail/tests/test_mail_access_rights.py index ec08d257efe..7594de87faf 100644 --- a/addons/mail/tests/test_mail_access_rights.py +++ b/addons/mail/tests/test_mail_access_rights.py @@ -20,7 +20,8 @@ ############################################################################## from openerp.addons.mail.tests import test_mail_mockup -from osv.orm import except_orm +from openerp.osv.orm import except_orm +from openerp.tools.misc import mute_logger class test_mail_access_rights(test_mail_mockup.TestMailMockups): @@ -84,6 +85,7 @@ class test_mail_access_rights(test_mail_mockup.TestMailMockups): msg_ids = self.mail_message.search(cr, uid, [('subject', 'like', '_Test')]) self.assertEqual(set([msg_id1, msg_id2, msg_id3, msg_id4, msg_id5, msg_id6, msg_id7, msg_id8]), set(msg_ids), 'mail_message search failed') + @mute_logger('openerp.osv.orm') def test_05_mail_message_read_access_rights(self): """ Test basic mail_message read access rights. """ cr, uid = self.cr, self.uid @@ -131,6 +133,8 @@ class test_mail_access_rights(test_mail_mockup.TestMailMockups): self.assertRaises(except_orm, self.mail_message.read, cr, user_bert_id, message_id) + @mute_logger('openerp.addons.base.ir.ir_model') + @mute_logger('openerp.osv.orm') def test_10_mail_flow_access_rights(self): """ Test a Chatter-looks alike flow. """ cr, uid = self.cr, self.uid diff --git a/addons/sale/sale_demo.xml b/addons/sale/sale_demo.xml index ba75b1571cb..f5566e68365 100644 --- a/addons/sale/sale_demo.xml +++ b/addons/sale/sale_demo.xml @@ -329,7 +329,6 @@ Thanks!
service 150.0 100.0 - produce From 6227e4b5c04b9ee861511a45f7b50806e27cf33c Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 15 Nov 2012 13:03:31 +0100 Subject: [PATCH 055/125] [FIX] CrashManager do not have destroy() method bzr revid: chs@openerp.com-20121115120331-ltujwzobh19muygl --- addons/web/static/src/js/chrome.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 2753cb45d1e..f5fc7b8f632 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -190,7 +190,14 @@ instance.web.Dialog = instance.web.Widget.extend({ }); instance.web.CrashManager = instance.web.Class.extend({ + init: function() { + this.active = true; + }, + rpc_error: function(error) { + if (!this.active) { + return; + } if (error.data.fault_code) { var split = ("" + error.data.fault_code).split('\n')[0].split(' -- '); if (split.length > 1) { @@ -205,6 +212,9 @@ instance.web.CrashManager = instance.web.Class.extend({ } }, show_warning: function(error) { + if (!this.active) { + return; + } instance.web.dialog($('
' + QWeb.render('CrashManager.warning', {error: error}) + '
'), { title: "OpenERP " + _.str.capitalize(error.type), buttons: [ @@ -213,7 +223,9 @@ instance.web.CrashManager = instance.web.Class.extend({ }); }, show_error: function(error) { - var self = this; + if (!this.active) { + return; + } var buttons = {}; buttons[_t("Ok")] = function() { $(this).dialog("close"); @@ -642,7 +654,7 @@ instance.web.client_actions.add("login", "instance.web.Login"); instance.web.redirect = function(url, wait) { // Dont display a dialog if some xmlhttprequest are in progress if (instance.client && instance.client.crashmanager) { - instance.client.crashmanager.destroy(); + instance.client.crashmanager.active = false; } var wait_server = function() { @@ -658,7 +670,7 @@ instance.web.redirect = function(url, wait) { } else { window.location = url; } -} +}; /** * Client action to reload the whole interface. From 81d4467c8fb4e15129d5dd6b8242fa1a8d4e5562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 15 Nov 2012 13:24:06 +0100 Subject: [PATCH 056/125] [REV] mail.compose.message using email_template: _reopen is back, as the spec of a wizard should be that hitting a button close it. Therefore _reopen is necessaru to reopen the wizard with its previous value, to continue working on it. bzr revid: tde@openerp.com-20121115122406-d88ozw5iclsqdyff --- .../wizard/mail_compose_message.py | 28 ++++++++++++++----- .../wizard/mail_compose_message_view.xml | 3 +- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/addons/email_template/wizard/mail_compose_message.py b/addons/email_template/wizard/mail_compose_message.py index 129c32722f5..ba4cef83bdb 100644 --- a/addons/email_template/wizard/mail_compose_message.py +++ b/addons/email_template/wizard/mail_compose_message.py @@ -25,6 +25,21 @@ from osv import osv from osv import fields +def _reopen(self, res_id, model): + return {'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'view_type': 'form', + 'res_id': res_id, + 'res_model': self._name, + 'target': 'new', + # save original model in context, because selecting the list of available + # templates requires a model in context + 'context': { + 'default_model': model, + }, + } + + class mail_compose_message(osv.TransientModel): _inherit = 'mail.compose.message' @@ -42,10 +57,8 @@ class mail_compose_message(osv.TransientModel): else: model = context.get('default_model', context.get('active_model')) - if model: - record_ids = email_template_obj.search(cr, uid, [('model', '=', model)], context=context) - return email_template_obj.name_get(cr, uid, record_ids, context) + [(False, '')] - return [] + record_ids = email_template_obj.search(cr, uid, [('model', '=', model)], context=context) + return email_template_obj.name_get(cr, uid, record_ids, context) + [(False, '')] def default_get(self, cr, uid, fields, context=None): if context is None: @@ -109,7 +122,7 @@ class mail_compose_message(osv.TransientModel): onchange_res['partner_ids'] = [(4, partner_id) for partner_id in onchange_res.pop('partner_ids', [])] onchange_res['attachment_ids'] = [(4, attachment_id) for attachment_id in onchange_res.pop('attachment_ids', [])] record.write(onchange_res) - return True + return _reopen(self, record.id, record.model) def onchange_use_template(self, cr, uid, ids, use_template, template_id, composition_mode, model, res_id, context=None): """ onchange_use_template (values: True or False). If use_template is @@ -141,8 +154,9 @@ class mail_compose_message(osv.TransientModel): 'attachment_ids': [(6, 0, [att.id for att in record.attachment_ids])] } template_id = email_template.create(cr, uid, values, context=context) - record.write({'template_id': template_id, 'use_template': True}) - return True + # record.write({'template_id': template_id, 'use_template': True}) + record.write(record.onchange_template_id(True, template_id, record.composition_mode, record.model, record.res_id)['value']) + return _reopen(self, record.id, record.model) #------------------------------------------------------ # Wizard validation and send diff --git a/addons/email_template/wizard/mail_compose_message_view.xml b/addons/email_template/wizard/mail_compose_message_view.xml index 0a530299d4a..5afaf9745e1 100644 --- a/addons/email_template/wizard/mail_compose_message_view.xml +++ b/addons/email_template/wizard/mail_compose_message_view.xml @@ -14,12 +14,11 @@ -
- diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index bd0ade31b68..c72af5eadaf 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -20,6 +20,7 @@ ############################################################################## import logging +import pdb import tools from email.header import decode_header @@ -50,8 +51,9 @@ class mail_message(osv.Model): _description = 'Message' _inherit = ['ir.needaction_mixin'] _order = 'id desc' + _rec_name = 'record_name' - _message_read_limit = 10 + _message_read_limit = 30 _message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'to_read', 'email_from', 'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name', 'favorite_user_ids'] _message_record_name_length = 18 @@ -120,21 +122,26 @@ class mail_message(osv.Model): "message, comment for other messages such as user replies"), 'email_from': fields.char('From', help="Email address of the sender. This field is set when no matching partner is found for incoming emails."), - 'author_id': fields.many2one('res.partner', 'Author', + 'author_id': fields.many2one('res.partner', 'Author', select=1, + ondelete='set null', help="Author of the message. If not set, email_from may hold an email address that did not match any partner."), 'partner_ids': fields.many2many('res.partner', string='Recipients'), 'notified_partner_ids': fields.many2many('res.partner', 'mail_notification', - 'message_id', 'partner_id', 'Recipients'), + 'message_id', 'partner_id', 'Notified partners', + help='Partners that have a notification pushing this message in their mailboxes'), 'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel', 'message_id', 'attachment_id', 'Attachments'), - 'parent_id': fields.many2one('mail.message', 'Parent Message', select=True, ondelete='set null', help="Initial thread message."), + 'parent_id': fields.many2one('mail.message', 'Parent Message', select=True, + ondelete='set null', help="Initial thread message."), 'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'), 'model': fields.char('Related Document Model', size=128, select=1), 'res_id': fields.integer('Related Document ID', select=1), 'record_name': fields.function(_get_record_name, type='char', store=True, string='Message Record Name', help="Name get of the related document."), - 'notification_ids': fields.one2many('mail.notification', 'message_id', 'Notifications'), + 'notification_ids': fields.one2many('mail.notification', 'message_id', + string='Notifications', + help='Technical field holding the message notifications. Use notified_partner_ids to access notified partners.'), 'subject': fields.char('Subject'), 'date': fields.datetime('Date'), 'message_id': fields.char('Message-Id', help='Message unique identifier', select=1, readonly=1), @@ -142,7 +149,8 @@ class mail_message(osv.Model): 'to_read': fields.function(_get_to_read, fnct_search=_search_to_read, type='boolean', string='To read', help='Functional field to search for messages the current user has to read'), - 'subtype_id': fields.many2one('mail.message.subtype', 'Subtype'), + 'subtype_id': fields.many2one('mail.message.subtype', 'Subtype', + ondelete='set null', select=1,), 'vote_user_ids': fields.many2many('res.users', 'mail_vote', 'message_id', 'user_id', string='Votes', help='Users that voted for this message'), @@ -200,67 +208,99 @@ class mail_message(osv.Model): # Message loading for web interface #------------------------------------------------------ - def _message_get_dict(self, cr, uid, message, context=None): - """ Return a dict representation of the message. This representation is - used in the JS client code, to display the messages. + def _message_read_dict_postprocess(self, cr, uid, messages, message_tree, context=None): + """ Post-processing on values given by message_read. This method will + handle partners in batch to avoid doing numerous queries. - :param dict message: read result of a mail.message + :param list messages: list of message, as get_dict result + :param dict message_tree: {[msg.id]: msg browse record} """ - # TDE note: this method should be optimized, to lessen the number of queries, will be done ASAP - is_author = False - if message['author_id']: - is_author = message['author_id'][0] == self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0] - author_id = message['author_id'] - elif message['email_from']: - author_id = (0, message['email_from']) + res_partner_obj = self.pool.get('res.partner') + ir_attachment_obj = self.pool.get('ir.attachment') + pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0] - has_voted = False - if uid in message.get('vote_user_ids'): - has_voted = True + # 1. Aggregate partners (author_id and partner_ids) and attachments + partner_ids = set() + attachment_ids = set() + for key, message in message_tree.iteritems(): + if message.author_id: + partner_ids |= set([message.author_id.id]) + if message.partner_ids: + partner_ids |= set([partner.id for partner in message.partner_ids]) + if message.attachment_ids: + attachment_ids |= set([attachment.id for attachment in message.attachment_ids]) - is_favorite = False - if uid in message.get('favorite_user_ids'): - is_favorite = True + # Filter author_ids uid can see + # partner_ids = self.pool.get('res.partner').search(cr, uid, [('id', 'in', partner_ids)], context=context) + partners = res_partner_obj.name_get(cr, uid, list(partner_ids), context=context) + partner_tree = dict((partner[0], partner) for partner in partners) - is_private = True - if message.get('model') and message.get('res_id'): - is_private = False + # 2. Attachments + attachments = ir_attachment_obj.read(cr, uid, list(attachment_ids), ['id', 'datas_fname'], context=context) + attachments_tree = dict((attachment['id'], {'id': attachment['id'], 'filename': attachment['datas_fname']}) for attachment in attachments) - try: - attachment_ids = [{'id': attach[0], 'name': attach[1]} for attach in self.pool.get('ir.attachment').name_get(cr, uid, message['attachment_ids'], context=context)] - except (orm.except_orm, osv.except_osv): - attachment_ids = [] - - # TDE note: should we send partner_ids ? - # TDE note: shouldn't we separated followers and other partners ? costly to compute maybe , - try: - partner_ids = self.pool.get('res.partner').name_get(cr, uid, message['partner_ids'], context=context) - except (orm.except_orm, osv.except_osv): + # 3. Update message dictionaries + for message_dict in messages: + message_id = message_dict.get('id') + message = message_tree[message_id] + if message.author_id: + author = partner_tree[message.author_id.id] + else: + author = (0, message.email_from) partner_ids = [] + for partner in message.partner_ids: + if partner.id in partner_tree: + partner_ids.append(partner_tree[partner.id]) + attachment_ids = [] + for attachment in message.attachment_ids: + if attachment.id in attachments_tree: + attachment_ids.append(attachments_tree[attachment.id]) + message_dict.update({ + 'is_author': pid == author[0], + 'author_id': author, + 'partner_ids': partner_ids, + 'attachment_ids': attachment_ids, + }) + return True - return { - 'id': message['id'], - 'type': message['type'], - 'attachment_ids': attachment_ids, - 'body': message['body'], - 'model': message['model'], - 'res_id': message['res_id'], - 'record_name': message['record_name'], - 'subject': message['subject'], - 'date': message['date'], - 'author_id': author_id, - 'is_author': is_author, - 'partner_ids': partner_ids, - 'parent_id': False, - 'vote_nb': len(message['vote_user_ids']), - 'has_voted': has_voted, - 'is_private': is_private, - 'is_favorite': is_favorite, - 'to_read': message['to_read'], - } + def _message_read_dict(self, cr, uid, message, parent_id=False, context=None): + """ Return a dict representation of the message. This representation is + used in the JS client code, to display the messages. Partners and + attachments related stuff will be done in post-processing in batch. - def _message_read_add_expandables(self, cr, uid, message_list, read_messages, - thread_level=0, message_loaded_ids=[], domain=[], parent_id=False, context=None, limit=None): + :param dict message: mail.message browse record + """ + # private message: no model, no res_id + is_private = False + if not message.model or not message.res_id: + is_private = True + # votes and favorites: res.users ids, no prefetching should be done + vote_nb = len(message.vote_user_ids) + has_voted = uid in [user.id for user in message.vote_user_ids] + is_favorite = uid in [user.id for user in message.favorite_user_ids] + + return {'id': message.id, + 'type': message.type, + 'body': message.body, + 'model': message.model, + 'res_id': message.res_id, + 'record_name': message.record_name, + 'subject': message.subject, + 'date': message.date, + 'to_read': message.to_read, + 'parent_id': parent_id, + 'is_private': is_private, + 'author_id': False, + 'is_author': False, + 'partner_ids': [], + 'vote_nb': vote_nb, + 'has_voted': has_voted, + 'is_favorite': is_favorite, + 'attachment_ids': [], + } + + def _message_read_add_expandables(self, cr, uid, messages, message_tree, parent_tree, + message_unload_ids=[], thread_level=0, domain=[], parent_id=False, context=None): """ Create expandables for message_read, to load new messages. 1. get the expandable for new threads if display is flat (thread_level == 0): @@ -275,96 +315,82 @@ class mail_message(osv.Model): for each hole in the child list based on message displayed, create an expandable - :param list message_list:list of message structure for the Chatter + :param list messages: list of message structure for the Chatter widget to which expandables are added - :param dict read_messages: dict [id]: read result of the messages to - easily have access to their values, given their ID + :param dict message_tree: dict [id]: browse record of this message + :param dict parent_tree: dict [parent_id]: [child_ids] + :param list message_unload_ids: list of message_ids we do not want + to load :return bool: True """ - def _get_expandable(domain, message_nb, parent_id, id, model): + def _get_expandable(domain, message_nb, parent_id, max_limit): return { 'domain': domain, 'nb_messages': message_nb, 'type': 'expandable', 'parent_id': parent_id, - 'id': id, - # TDE note: why do we need model sometimes, and sometimes not ??? - 'model': model, + 'max_limit': max_limit, } - # all_not_loaded_ids = [] - id_list = sorted(read_messages.keys()) - if not id_list: - return message_list + if not messages: + return True + message_ids = sorted(message_tree.keys()) # 1. get the expandable for new threads if thread_level == 0: - exp_domain = domain + [('id', '<', min(message_loaded_ids + id_list))] + exp_domain = domain + [('id', '<', min(message_unload_ids + message_ids))] else: - exp_domain = domain + ['!', ('id', 'child_of', message_loaded_ids + id_list)] + exp_domain = domain + ['!', ('id', 'child_of', message_unload_ids + parent_tree.keys())] ids = self.search(cr, uid, exp_domain, context=context, limit=1) if ids: - message_list.append(_get_expandable(exp_domain, -1, parent_id, -1, None)) + # inside a thread: prepend + if parent_id: + messages.insert(0, _get_expandable(exp_domain, -1, parent_id, True)) + # new threads: append + else: + messages.append(_get_expandable(exp_domain, -1, parent_id, True)) # 2. get the expandables for new messages inside threads if display is not flat if thread_level == 0: return True - for message_id in id_list: - message = read_messages[message_id] + for message_id in message_ids: + message = message_tree[message_id] - # message is not a thread header (has a parent_id) - # TDE note: parent_id is false is there is a parent we can not see -> ok - if message.get('parent_id'): + # generate only for thread header messages (TDE note: parent_id may be False is uid cannot see parent_id, seems ok) + if message.parent_id: continue - # TDE note: check search is correctly implemented in mail.message - not_loaded_ids = self.search(cr, uid, [ - ('id', 'child_of', message['id']), - ('id', 'not in', message_loaded_ids), - ], context=context, limit=self._message_read_more_limit) - if not not_loaded_ids: + # check there are message for expandable + child_ids = set([child.id for child in message.child_ids]) - set(message_unload_ids) + child_ids = sorted(list(child_ids), reverse=True) + if not child_ids: continue - # all_not_loaded_ids += not_loaded_ids - # group childs not read - id_min, id_max, nb = max(not_loaded_ids), 0, 0 - for not_loaded_id in not_loaded_ids: - if not read_messages.get(not_loaded_id): + # make groups of unread messages + id_min, id_max, nb = max(child_ids), 0, 0 + for child_id in child_ids: + if not child_id in message_ids: nb += 1 - if id_min > not_loaded_id: - id_min = not_loaded_id - if id_max < not_loaded_id: - id_max = not_loaded_id + if id_min > child_id: + id_min = child_id + if id_max < child_id: + id_max = child_id elif nb > 0: exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)] - message_list.append(_get_expandable(exp_domain, nb, message_id, id_min, message.get('model'))) - id_min, id_max, nb = max(not_loaded_ids), 0, 0 + messages.append(_get_expandable(exp_domain, nb, message_id, False)) + id_min, id_max, nb = max(child_ids), 0, 0 else: - id_min, id_max, nb = max(not_loaded_ids), 0, 0 + id_min, id_max, nb = max(child_ids), 0, 0 if nb > 0: exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)] - message_list.append(_get_expandable(exp_domain, nb, message_id, id_min, message.get('model'))) - - # message_loaded_ids = list(set(message_loaded_ids + read_messages.keys() + all_not_loaded_ids)) + idx = [msg.get('id') for msg in messages].index(message_id) + 1 + # messages.append(_get_expandable(exp_domain, nb, message_id, id_min)) + messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False)) return True - def _get_parent(self, cr, uid, message, context=None): - """ Tools method that tries to get the parent of a mail.message. If - no parent, or if uid has no access right on the parent, False - is returned. - - :param dict message: read result of a mail.message - """ - if not message['parent_id']: - return False - parent_id = message['parent_id'][0] - try: - return self.read(cr, uid, parent_id, self._message_read_fields, context=context) - except (orm.except_orm, osv.except_osv): - return False - - def message_read(self, cr, uid, ids=None, domain=None, message_unload_ids=None, thread_level=0, context=None, parent_id=False, limit=None): + def message_read(self, cr, uid, ids=None, domain=None, message_unload_ids=None, + thread_level=0, context=None, parent_id=False, limit=None): """ Read messages from mail.message, and get back a list of structured messages to be displayed as discussion threads. If IDs is set, fetch these records. Otherwise use the domain to fetch messages. @@ -388,46 +414,56 @@ class mail_message(osv.Model): ancestors and expandables :return list: list of message structure for the Chatter widget """ - # print 'message_read', ids, domain, message_unload_ids, thread_level, context, parent_id, limit assert thread_level in [0, 1], 'message_read() thread_level should be 0 (flat) or 1 (1 level of thread); given %s.' % thread_level domain = domain if domain is not None else [] message_unload_ids = message_unload_ids if message_unload_ids is not None else [] if message_unload_ids: domain += [('id', 'not in', message_unload_ids)] limit = limit or self._message_read_limit - read_messages = {} + message_tree = {} message_list = [] + parent_tree = {} # no specific IDS given: fetch messages according to the domain, add their parents if uid has access to if ids is None: ids = self.search(cr, uid, domain, context=context, limit=limit) - for message in self.read(cr, uid, ids, self._message_read_fields, context=context): - message_id = message['id'] - # if not in tree and not in message_loaded list - if not message_id in read_messages and not message_id in message_unload_ids: - read_messages[message_id] = message - message_list.append(self._message_get_dict(cr, uid, message, context=context)) + # fetch parent if threaded, sort messages + for message in self.browse(cr, uid, ids, context=context): + message_id = message.id + if message_id in message_tree: + continue + message_tree[message_id] = message - # get the older ancestor the user can read, update its ancestor field - if not thread_level: - message_list[-1]['parent_id'] = parent_id - continue - parent = self._get_parent(cr, uid, message, context=context) - while parent and parent.get('id') != parent_id: - message_list[-1]['parent_id'] = parent.get('id') - message = parent - parent = self._get_parent(cr, uid, message, context=context) - # if in thread: add its ancestor to the list of messages - if not message['id'] in read_messages and not message['id'] in message_unload_ids: - read_messages[message['id']] = message - message_list.append(self._message_get_dict(cr, uid, message, context=context)) + # find parent_id + if thread_level == 0: + tree_parent_id = parent_id + else: + tree_parent_id = message_id + parent = message + while parent.parent_id and parent.parent_id.id != parent_id: + parent = parent.parent_id + tree_parent_id = parent.id + if not parent.id in message_tree: + message_tree[parent.id] = parent + # newest messages first + parent_tree.setdefault(tree_parent_id, []) + if tree_parent_id != message_id: + parent_tree[tree_parent_id].append(self._message_read_dict(cr, uid, message_tree[message_id], parent_id=tree_parent_id, context=context)) + + if thread_level: + for key, message_id_list in parent_tree.iteritems(): + message_id_list.sort(key=lambda item: item['id']) + message_id_list.insert(0, self._message_read_dict(cr, uid, message_tree[key], context=context)) + + parent_list = parent_tree.items() + parent_list = sorted(parent_list, key=lambda item: max([msg.get('id') for msg in item[1]]) if item[1] else item[0], reverse=True) + message_list = [message for (key, msg_list) in parent_list for message in msg_list] # get the child expandable messages for the tree - message_list = sorted(message_list, key=lambda k: k['id']) - self._message_read_add_expandables(cr, uid, message_list, read_messages, thread_level=thread_level, - message_loaded_ids=message_unload_ids, domain=domain, parent_id=parent_id, context=context, limit=limit) - + self._message_read_dict_postprocess(cr, uid, message_list, message_tree, context=context) + self._message_read_add_expandables(cr, uid, message_list, message_tree, parent_tree, + thread_level=thread_level, message_unload_ids=message_unload_ids, domain=domain, parent_id=parent_id, context=context) return message_list # TDE Note: do we need this ? @@ -461,7 +497,6 @@ class mail_message(osv.Model): - otherwise: remove the id """ # Rules do not apply to administrator - # print '_search', uid, args if uid == SUPERUSER_ID: return super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count, access_rights_uid=access_rights_uid) @@ -592,11 +627,14 @@ class mail_message(osv.Model): other_ids = other_ids - set(document_related_ids) if not other_ids: return + raise orm.except_orm(_('Access Denied'), _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \ (self._description, operation)) def create(self, cr, uid, values, context=None): + if 'default_res_model' in context: + values['model']=context.get('default_res_model') if not values.get('message_id') and values.get('res_id') and values.get('model'): values['message_id'] = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values) newid = super(mail_message, self).create(cr, uid, values, context) diff --git a/addons/mail/mail_message_view.xml b/addons/mail/mail_message_view.xml index 93b35797fd5..7d2e9483773 100644 --- a/addons/mail/mail_message_view.xml +++ b/addons/mail/mail_message_view.xml @@ -59,21 +59,26 @@ + + - + + @@ -84,7 +89,6 @@ form tree,form - {'search_default_to_read_message':True} diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index f3203336615..ee00b88dbd9 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -74,17 +74,17 @@ class mail_thread(osv.AbstractModel): - message_unread: has uid unread message for the document - message_summary: html snippet summarizing the Chatter for kanban views """ res = dict((id, dict(message_unread=False, message_summary='')) for id in ids) + user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0] - # search for unread messages, by reading directly mail.notification, as SUPERUSER - notif_obj = self.pool.get('mail.notification') - notif_ids = notif_obj.search(cr, SUPERUSER_ID, [ - ('partner_id.user_ids', 'in', [uid]), - ('message_id.res_id', 'in', ids), - ('message_id.model', '=', self._name), - ('read', '=', False) - ], context=context) - for notif in notif_obj.browse(cr, SUPERUSER_ID, notif_ids, context=context): - res[notif.message_id.res_id]['message_unread'] = True + # search for unread messages, directly in SQL to improve performances + cr.execute(""" SELECT m.res_id FROM mail_message m + RIGHT JOIN mail_notification n + ON (n.message_id = m.id AND n.partner_id = %s AND n.read = False) + WHERE m.model = %s AND m.res_id in %s""", + (user_pid, self._name, tuple(ids),)) + msg_ids = [result[0] for result in cr.fetchall()] + for msg_id in msg_ids: + res[msg_id]['message_unread'] = True for thread in self.browse(cr, uid, ids, context=context): cls = res[thread.id]['message_unread'] and ' class="oe_kanban_mail_new"' or '' diff --git a/addons/mail/mail_thread_view.xml b/addons/mail/mail_thread_view.xml index 07b24725b05..cdeade04d84 100644 --- a/addons/mail/mail_thread_view.xml +++ b/addons/mail/mail_thread_view.xml @@ -4,48 +4,126 @@ Inbox mail.wall - + mail.message + { + 'default_model': 'res.users', + 'default_res_id': uid + } + + +

+ Good Job! Your inbox is empty. +

+ Your inbox contains private messages or emails sent to you + as well as information related to documents or people you + follow. +

+
To: me mail.wall - + mail.message + { + 'default_model': 'res.users', + 'default_res_id': uid, + 'search_default_message_unread': True + } + + +

+ No private message. +

+ This list contains messages sent to you. +

+
- Favorites + Todo mail.wall - + mail.message + { + 'default_model': 'res.users', + 'default_res_id': uid, + 'search_default_message_unread': True + } + + +

+ No todo! +

+ When you process messages in your inbox, you can mark some + as todo. From this menu, you can process all your todo. +

+
Archives mail.wall - + { + 'default_model': 'res.users', + 'default_res_id': uid, + 'search_default_message_read': True + } + + +

+ No message found. +

+
Sent mail.wall - + { + 'default_model': 'res.users', + 'default_res_id': uid + } + + +

+ No message sent yet. +

+ Click on the top-right icon to compose a message. This + message will be sent by email if it's an internal contact. +

+
- - + @@ -61,7 +139,7 @@ - Favorites + Todo diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css index af3cc3ed359..18d19a848cc 100644 --- a/addons/mail/static/src/css/mail.css +++ b/addons/mail/static/src/css/mail.css @@ -1,7 +1,36 @@ +/* ------------ TOPBAR MAIL BUTTON --------------- */ + +/* FIXME this css is not very pretty because it uses a + * 'button' element wich comes with a lot of inappropriate + * styling. Entypo is also a headache to center properly + * */ + +.openerp .oe_topbar_item.oe_topbar_compose_full_email{ + padding: 0px; + width: 32px; + height: 32px; +} +.openerp .oe_topbar_item.oe_topbar_compose_full_email button{ + position: relative; + top: -3px; /* centering entypo ... urgh */ + box-sizing: border-box; + border: none; + box-shadow: none; + color: white; + background: none; + text-shadow: 0px 1px 2px black; + width: 32px; + height: 32px; + padding: 0px; + margin: 0px + border-radius: 0px; +} /* ------------ MAIL WIDGET --------------- */ .openerp .oe_mail, .openerp .oe_mail *{ - box-sizing: border-box; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } .openerp .oe_mail { display: block; @@ -42,9 +71,10 @@ } .openerp .oe_mail .oe_msg .oe_msg_footer{ padding-left: 4px; + padding-top: 3px; overflow: hidden; - opacity:0.8; - -webkit-transition: opacity 0.2s linear; + margin-bottom: 4px; + font-size: 11px; } .openerp .oe_mail .oe_msg .oe_msg_content{ display: block; @@ -80,18 +110,24 @@ .openerp .oe_mail .oe_msg.oe_msg_indented .oe_msg_content{ padding-top:2px; } +.openerp .oe_mail .oe_msg.oe_msg_indented .oe_msg_footer{ + margin-bottom: 0px; +} + /* b) Votes (likes) */ + .openerp .oe_mail .oe_mail_vote_count{ display: inline; position: relative; - background: #7C7BAD; - color: white; + background: white; + box-shadow: 0px 0px 0px 1px rgba(124, 123, 173, 0.36) inset; + color: #7c7bad; text-shadow: none; border-radius: 3px; margin: 0px; padding-left: 3px; - padding-right: 18px; - margin-right: 3px; + padding-right: 15px; + margin-right: 5px; } .openerp .oe_mail .oe_mail_vote_count .oe_e{ position: absolute; @@ -102,12 +138,6 @@ /* c) Message action icons */ -.openerp .oe_mail .oe_msg.oe_msg_unread .oe_unread{ - display:none; -} -.openerp .oe_mail .oe_msg.oe_msg_read .oe_read{ - display:none; -} .openerp .oe_mail .oe_msg .oe_msg_icons{ float: right; margin-top: 4px; @@ -115,6 +145,9 @@ margin-left: 8px; height: 24px; -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .openerp .oe_mail .oe_msg .oe_msg_icons span{ float:right; @@ -128,10 +161,16 @@ color: #FFF; text-shadow: 0px 1px #AAA,0px -1px #AAA, -1px 0px #AAA, 1px 0px #AAA, 0px 3px 3px rgba(0,0,0,0.1); -webkit-transition: all 0.2s linear; + -moz-transition: all 0.2s linear; + -o-transition: all 0.2s linear; + transition: all 0.2s linear; } .openerp .oe_mail .oe_msg:hover .oe_msg_icons a{ opacity: 1; -webkit-transition: all 0.1s linear; + -moz-transition: all 0.1s linear; + -o-transition: all 0.1s linear; + transition: all 0.1s linear; } .openerp .oe_mail .oe_msg .oe_msg_icons .oe_star:hover a{ color: #FFF6C0; @@ -155,7 +194,7 @@ } .openerp .oe_mail .oe_msg .oe_msg_content textarea{ width: 100%; - height: 32px; + height: 64px; margin: 0px; padding: 0px; resize: vertical; @@ -171,6 +210,150 @@ width: 100%; } +/* --------------------- ATTACHMENTS --------------------- */ + +.openerp .oe_mail .oe_msg_attachment_list{ + display: none; + margin-top: 12px; + margin-bottom: 12px; +} +.openerp .oe_mail .oe_msg_composer .oe_msg_attachment_list{ + display: block; +} +.openerp .oe_mail .oe_attachment{ + display: inline-block; + width: 100px; + margin: 2px; + min-height: 80px; + position: relative; + border-radius: 3px; + text-align: center; + vertical-align: top; +} +.openerp .oe_mail .oe_attachment .oe_name{ + display: inline-block; + max-width: 100%; + padding: 1px 3px; + margin-top: 50px; + margin-bottom: 5px; + background: rgba(124, 123, 173, 0.13); + overflow: hidden; + color: #4c4c4c; + text-shadow: none; + border-radius: 3px; +} + +.openerp .oe_mail .oe_attachment.oe_preview{ + background: url( data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAJ0lEQVQYV2MsLS39z4AGLCws0IUYGIeCwrVr12J45sSJE5ieGQIKAbuZKf/EMCs7AAAAAElFTkSuQmCC ); +} +.openerp .oe_mail .oe_attachment .oe_progress_bar{ + display: none; + position: absolute; + top: 18px; + left: 16px; + right: 16px; + height: 17px; + line-height: 13px; + padding: 0px; + background: #4BBD00; + color: white; + text-align: center; + border-radius: 3px; + border: solid 1px rgba(0,0,0,0.2); + box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.34); + -webkit-animation: oe_mail_attach_loading_anim 0.75s infinite linear; + -moz-animation: oe_mail_attach_loading_anim 0.75s infinite linear; + -o-animation: oe_mail_attach_loading_anim 0.75s infinite linear; + animation: oe_mail_attach_loading_anim 0.75s infinite linear; +} +.openerp .oe_mail .oe_attachment.oe_uploading .oe_progress_bar{ + display: block; +} +@-webkit-keyframes oe_mail_attach_loading_anim{ + 0% { background: #4BBD00 } + 50% { background: #009123 } + 100% { background: #4BBD00 } +} +@-moz-keyframes oe_mail_attach_loading_anim{ + 0% { background: #4BBD00 } + 50% { background: #009123 } + 100% { background: #4BBD00 } +} +@-o-keyframes oe_mail_attach_loading_anim{ + 0% { background: #4BBD00 } + 50% { background: #009123 } + 100% { background: #4BBD00 } +} +@keyframes oe_mail_attach_loading_anim{ + 0% { background: #4BBD00 } + 50% { background: #009123 } + 100% { background: #4BBD00 } +} +.openerp .oe_mail .oe_attachment.oe_preview .oe_name{ + position: absolute; + bottom: 0px; + margin: 0px; + left: 0px; + right: 0px; + max-height: 64px; + background: rgba(0,0,0,0.8); + color: white; + border-top-left-radius: 0px; + border-top-right-radius: 0px; + opacity: 0; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; +} +.openerp .oe_mail .oe_attachment.oe_preview:hover .oe_name{ + opacity: 1; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; +} +.openerp .oe_mail .oe_attachment img{ + position: absolute; + width: 48px; + height: 48px; + top: 0px; + left: 50%; + margin-left: -24px; +} +.openerp .oe_mail .oe_attachment.oe_preview img{ + display: block; + position: relative; + margin:0px; + width: 100px; + height: 100px; + border-radius: 3px; + margin-left: -50px; +} +.openerp .oe_mail .oe_attachment .oe_delete{ + display: none; +} +.openerp .oe_mail .oe_msg_composer .oe_attachment .oe_delete{ + display: block; + position: absolute; + top: -7px; + right: 0px; + color: black; + text-shadow: 1px 0px white, -1px 0px white, 0px 1px white, 0px -1px white; + cursor: pointer; + opacity: 0; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; +} +.openerp .oe_mail .oe_msg_composer .oe_attachment:hover .oe_delete{ + opacity: 1; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; +} /* ---------------- MESSAGE QUICK COMPOSER --------------- */ .openerp .oe_mail .oe_msg_composer .oe_msg_footer{ @@ -178,37 +361,6 @@ padding-top: 2px; padding-bottom:6px; } -.openerp .oe_mail .oe_msg_attachments.oe_hidden, -.openerp .oe_mail .oe_msg_images.oe_hidden{ - margin:0px; - border: none; - display: none; -} -.openerp .oe_mail .oe_msg_attachments{ - margin-bottom: 4px; - margin-right: 0px; - font-size: 12px; - border-radius: 2px; - border: solid 1px rgba(124,123,173,0.14); -} -.openerp .oe_mail .oe_msg_attachments .oe_attachment{ - padding: 2px; - padding-left: 4px; - padding-right: 4px; -} -.openerp .oe_mail .oe_msg_attachments .oe_attachment .oe_e{ - font-size: 23px; - margin-top: -5px; -} -.openerp .oe_mail .oe_msg_attachments .oe_attachment .oe_e:hover{ - text-decoration: none; -} -.openerp .oe_mail .oe_msg_attachments .oe_attachment:nth-child(odd){ - background:white; -} -.openerp .oe_mail .oe_msg_attachments .oe_attachment:nth-child(even){ - background: #F4F5FA; -} .openerp .oe_mail .oe_msg_images { display: block; } @@ -269,6 +421,7 @@ .openerp .oe_mail .oe_msg_content.oe_msg_more_message{ text-align: right; + cursor: pointer; } .openerp .oe_mail .oe_msg_content.oe_msg_more_message .oe_separator{ height: 0; @@ -280,7 +433,7 @@ } .openerp .oe_mail .oe_msg_more_message .oe_msg_fetch_more { background: white; - margin-right: 280px; + margin-right: 210px; padding-left: 8px; padding-right: 8px; text-decoration: none; @@ -298,6 +451,7 @@ padding-top: 5px; width: 160px; float: right; + margin-right: 16px; } /* a) THE FOLLOW BUTTON */ @@ -308,9 +462,22 @@ width:100%; } .openerp .oe_followers button.oe_follower.oe_following{ + color: white; background-color: #3465A4; background-image: -webkit-linear-gradient(top, #729FCF, #3465A4); + background-image: -moz-linear-gradient(top, #729FCF, #3465A4); + background-image: -ms-linear-gradient(top, #729FCF, #3465A4); + background-image: -o-linear-gradient(top, #729FCF, #3465A4); + background-image: linear-gradient(to bottom, #729FCF, #3465A4); +} +.openerp .oe_followers button.oe_follower.oe_following:hover{ color: white; + background-color: #A21A1A; + background-image: -webkit-linear-gradient(top, #DF3F3F, #A21A1A); + background-image: -moz-linear-gradient(top, #DF3F3F, #A21A1A); + background-image: -ms-linear-gradient(top, #DF3F3F, #A21A1A); + background-image: -o-linear-gradient(top, #DF3F3F, #A21A1A); + background-image: linear-gradient(to bottom, #DF3F3F, #A21A1A); } .openerp .oe_followers button.oe_follower .oe_follow, @@ -371,12 +538,17 @@ .openerp .oe_record_thread{ display: block; - margin-right: 180px; + margin-left: 16px; + margin-right: 212px; } /* ----------- INBOX INTEGRATION ----------- */ .openerp .oe_mail_wall .oe_mail{ margin: 16px; - width: 720px; + width: 600px; +} + +.openerp .oe_mail .oe_view_nocontent > p { + padding-left: 15px; } diff --git a/addons/mail/static/src/css/mail_group.css b/addons/mail/static/src/css/mail_group.css index b700cb8c80f..3146247c1bc 100644 --- a/addons/mail/static/src/css/mail_group.css +++ b/addons/mail/static/src/css/mail_group.css @@ -68,30 +68,31 @@ min-height: 120px; } -.oe_group_details a, .oe_group_details a:hover { - font-weight: bold; - color: #4c4c4c; -} - .oe_group_details h4 { margin: 0; font-size: 13px; } -.oe_group_details h4 a { - color: #4c4c4c; -} - -.oe_group_details h4 a:hover { - text-decoration: underline; -} - .oe_group_details ul { margin: 3px 0 5px; padding: 0; list-style: none; } -.oe_group_details li { +.openerp .oe_group_details li { margin: 2px 0; } + +.openerp .oe_group_button { + padding-top: 7px; +} + +.openerp .oe_group_button .oe_group_join { + color: white; + background-color: #3465A4; + background-image: -webkit-linear-gradient(top, #729FCF, #3465A4); + background-image: -moz-linear-gradient(top, #729FCF, #3465A4); + background-image: -ms-linear-gradient(top, #729FCF, #3465A4); + background-image: -o-linear-gradient(top, #729FCF, #3465A4); + background-image: linear-gradient(to bottom, #729FCF, #3465A4); +} diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 1eb3c29e556..e214168cb25 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -26,7 +26,8 @@ openerp.mail = function (session) { 'default_use_template', 'default_partner_ids', 'default_model', 'default_res_id', 'default_content_subtype', , 'default_subject', 'default_body', 'active_id', 'lang', 'bin_raw', 'tz', - 'active_model', 'edi_web_url_view', 'active_ids'] + 'active_model', 'edi_web_url_view', 'active_ids', + 'default_attachment_ids'] for (var key in action.context) { if (_.indexOf(context_keys, key) == -1) { action.context[key] = null; @@ -54,8 +55,8 @@ openerp.mail = function (session) { mail.ChatterUtils = { /* Get an image in /web/binary/image?... */ - get_image: function (session, model, field, id) { - return session.prefix + '/web/binary/image?session_id=' + session.session_id + '&model=' + model + '&field=' + field + '&id=' + (id || ''); + get_image: function (session, model, field, id, resize) { + return session.prefix + '/web/binary/image?session_id=' + session.session_id + '&model=' + model + '&field=' + field + '&id=' + (id || '') + '&resize=' + (resize ? encodeURIComponent(resize) : ''); }, /* Get the url of an attachment {'id': id} */ @@ -111,10 +112,265 @@ openerp.mail = function (session) { } return domain; - } + }, + + // inserts zero width space between each letter of a string so that + // the word will correctly wrap in html boxes smaller than the text + breakword: function(str){ + var out = ''; + if (!str) { + return str; + } + for(var i = 0, len = str.length; i < len; i++){ + out += _.str.escapeHTML(str[i]) + '​'; + } + return out; + }, + + // returns the file type of a file based on its extension + // As it only looks at the extension it is quite approximative. + filetype: function(url){ + url = url.filename || url; + var tokens = url.split('.'); + if(tokens.length <= 1){ + return 'unknown'; + } + var extension = tokens[tokens.length -1]; + if(extension.length === 0){ + return 'unknown'; + }else{ + extension = extension.toLowerCase(); + } + var filetypes = { + 'webimage': ['png','jpg','jpeg','jpe','gif'], // those have browser preview + 'image': ['tif','tiff','tga', + 'bmp','xcf','psd','ppm','pbm','pgm','pnm','mng', + 'xbm','ico','icon','exr','webp','psp','pgf','xcf', + 'jp2','jpx','dng','djvu','dds'], + 'vector': ['ai','svg','eps','vml','cdr','xar','cgm','odg','sxd'], + 'print': ['dvi','pdf','ps'], + 'document': ['doc','docx','odm','odt'], + 'presentation': ['key','keynote','odp','pps','ppt'], + 'font': ['otf','ttf','woff','eot'], + 'archive': ['zip','7z','ace','apk','bzip2','cab','deb','dmg','gzip','jar', + 'rar','tar','gz','pak','pk3','pk4','lzip','lz','rpm'], + 'certificate': ['cer','key','pfx','p12','pem','crl','der','crt','csr'], + 'audio': ['aiff','wav','mp3','ogg','flac','wma','mp2','aac', + 'm4a','ra','mid','midi'], + 'video': ['asf','avi','flv','mkv','m4v','mpeg','mpg','mpe','wmv','mp4','ogm'], + 'text': ['txt','rtf','ass'], + 'html': ['html','xhtml','xml','htm','css'], + 'disk': ['iso','nrg','img','ccd','sub','cdi','cue','mds','mdx'], + 'script': ['py','js','c','cc','cpp','cs','h','java','bat','sh', + 'd','rb','pl','as','cmd','coffee','m','r','vbs','lisp'], + 'spreadsheet': ['123','csv','ods','numbers','sxc','xls','vc','xlsx'], + 'binary': ['exe','com','bin','app'], + }; + for(filetype in filetypes){ + var ext_list = filetypes[filetype]; + for(var i = 0, len = ext_list.length; i < len; i++){ + if(extension === ext_list[i]){ + return filetype; + } + } + } + return 'unknown'; + }, + }; + /** + * ------------------------------------------------------------ + * MessageCommon + * ------------------------------------------------------------ + * + * Common base for expandables, chatter messages and composer. It manages + * the various variables common to those models. + */ + + mail.MessageCommon = session.web.Widget.extend({ + + /** + * ------------------------------------------------------------ + * FIXME: this comment was moved as is from the ThreadMessage Init as + * part of a refactoring. Check that it is still correct + * ------------------------------------------------------------ + * This widget handles the display of a messages in a thread. + * Displays a record and performs some formatting on the record : + * - record.date: formatting according to the user timezone + * - record.timerelative: relative time givein by timeago lib + * - record.avatar: image url + * - record.attachment_ids[].url: url of each attachmentThe + * thread view : + * - root thread + * - - sub message (parent_id = root message) + * - - - sub thread + * - - - - sub sub message (parent id = sub thread) + * - - sub message (parent_id = root message) + * - - - sub thread + */ + + init: function (parent, datasets, options) { + this._super(parent, options); + + // record options + this.options = datasets.options || options || {}; + // record domain and context + this.domain = datasets.domain || options.domain || []; + this.context = _.extend({ + default_model: false, + default_res_id: 0, + default_parent_id: false }, options.context || {}); + + // data of this message + this.id = datasets.id || false, + this.last_id = this.id, + this.model = datasets.model || this.context.default_model || false, + this.res_id = datasets.res_id || this.context.default_res_id || false, + this.parent_id = datasets.parent_id || false, + this.type = datasets.type || false, + this.is_author = datasets.is_author || false, + this.is_private = datasets.is_private || false, + this.subject = datasets.subject || false, + this.name = datasets.name || false, + this.record_name = datasets.record_name || false, + this.body = datasets.body || false, + this.vote_nb = datasets.vote_nb || 0, + this.has_voted = datasets.has_voted || false, + this.is_favorite = datasets.is_favorite || false, + this.thread_level = datasets.thread_level || 0, + this.to_read = datasets.to_read || false, + this.author_id = datasets.author_id || false, + this.attachment_ids = datasets.attachment_ids || [], + this.partner_ids = datasets.partner_ids || []; + this._date = datasets.date; + + this.format_data(); + + // record options and data + this.show_record_name = this.record_name && !this.thread_level && this.model != 'res.partner'; + this.options.show_read = false; + this.options.show_unread = false; + if (this.options.show_read_unread_button) { + if (this.options.read_action == 'read') this.options.show_read = true; + else if (this.options.read_action == 'unread') this.options.show_unread = true; + else { + this.options.show_read = this.to_read; + this.options.show_unread = !this.to_read; + this.options.rerender = true; + this.options.toggle_read = true; + } + } + this.parent_thread = parent.messages != undefined ? parent : this.options.root_thread; + this.thread = false; + }, + + /* Convert date, timerelative and avatar in displayable data. */ + format_data: function () { + + //formating and add some fields for render + if (this._date) { + this.date = session.web.format_value(this._date, {type:"datetime"}); + this.timerelative = $.timeago(this.date); + } + if (this.type == 'email' && (!this.author_id || !this.author_id[0])) { + this.avatar = ('/mail/static/src/img/email_icon.png'); + } else if (this.author_id && this.template != 'mail.compose_message') { + this.avatar = mail.ChatterUtils.get_image(this.session, 'res.partner', 'image_small', this.author_id[0]); + } else { + this.avatar = mail.ChatterUtils.get_image(this.session, 'res.users', 'image_small', this.session.uid); + } + + }, + + + /* upload the file on the server, add in the attachments list and reload display + */ + display_attachments: function () { + for (var l in this.attachment_ids) { + var attach = this.attachment_ids[l]; + if (!attach.formating) { + attach.url = mail.ChatterUtils.get_attachment_url(this.session, attach); + attach.filetype = mail.ChatterUtils.filetype(attach.filename); + attach.name = mail.ChatterUtils.breakword(attach.name || attach.filename); + attach.formating = true; + } + } + this.$(".oe_msg_attachment_list").html( session.web.qweb.render('mail.thread.message.attachments', {'widget': this}) ); + }, + + /* return the link to resized image + */ + attachments_resize_image: function (id, resize) { + return mail.ChatterUtils.get_image(this.session, 'ir.attachment', 'datas', id, resize); + }, + + /* get all child message id linked. + * @return array of id + */ + get_child_ids: function () { + return _.map(this.get_childs(), function (val) { return val.id; }); + }, + + /* get all child message linked. + * @return array of message object + */ + get_childs: function (nb_thread_level) { + var res=[]; + if (arguments[1] && this.id) res.push(this); + if ((isNaN(nb_thread_level) || nb_thread_level>0) && this.thread) { + _(this.thread.messages).each(function (val, key) { + res = res.concat( val.get_childs((isNaN(nb_thread_level) ? undefined : nb_thread_level-1), true) ); + }); + } + return res; + }, + + /** + * search a message in all thread and child thread. + * This method return an object message. + * @param {object}{int} option.id + * @param {object}{string} option.model + * @param {object}{boolean} option._go_thread_wall + * private for check the top thread + * @return thread object + */ + browse_message: function (options) { + // goto the wall thread for launch browse + if (!options._go_thread_wall) { + options._go_thread_wall = true; + for (var i in this.options.root_thread.messages) { + var res=this.options.root_thread.messages[i].browse_message(options); + if (res) return res; + } + } + + if (this.id==options.id) + return this; + + for (var i in this.thread.messages) { + if (this.thread.messages[i].thread) { + var res=this.thread.messages[i].browse_message(options); + if (res) return res; + } + } + + return false; + }, + + /** + * call on_message_delete on his parent thread + */ + destroy: function () { + + this._super(); + this.parent_thread.on_message_detroy(this); + + } + + }); + /** * ------------------------------------------------------------ * ComposeMessage widget @@ -126,7 +382,7 @@ openerp.mail = function (session) { * When the user focuses the textarea, the compose message is instantiated. */ - mail.ThreadComposeMessage = session.web.Widget.extend({ + mail.ThreadComposeMessage = mail.MessageCommon.extend({ template: 'mail.compose_message', /** @@ -138,45 +394,22 @@ openerp.mail = function (session) { */ init: function (parent, datasets, options) { - var self = this; - this._super(parent); - this.context = options.context || {}; - this.options = options.options; - + this._super(parent, datasets, options); this.show_compact_message = false; - - // data of this compose message - this.attachment_ids = []; - this.id = datasets.id; - this.model = datasets.model; - this.res_model = datasets.res_model; - this.is_private = datasets.is_private || false; - this.partner_ids = datasets.partner_ids || []; - this.avatar = mail.ChatterUtils.get_image(this.session, 'res.users', 'image_small', this.session.uid); - this.thread_level = datasets.thread_level; - this.parent_thread= parent.messages!= undefined ? parent : false; - - this.ds_attachment = new session.web.DataSetSearch(this, 'ir.attachment'); this.show_delete_attachment = true; - - this.fileupload_id = _.uniqueId('oe_fileupload_temp'); - $(window).on(self.fileupload_id, self.on_attachment_loaded); }, start: function () { + this._super.apply(this, arguments); + + this.ds_attachment = new session.web.DataSetSearch(this, 'ir.attachment'); + this.fileupload_id = _.uniqueId('oe_fileupload_temp'); + $(window).on(this.fileupload_id, this.on_attachment_loaded); + this.display_attachments(); this.bind_events(); }, - /* upload the file on the server, add in the attachments list and reload display - */ - display_attachments: function () { - this.$(".oe_msg_attachment_list").html( - session.web.qweb.render('mail.thread.message.attachments', {'widget': this}) ); - // event: delete an attachment - this.$(".oe_msg_attachment_list").on('click', '.oe_mail_attachment_delete', this.on_attachment_delete); - }, - /* when a user click on the upload button, send file read on_attachment_loaded */ on_attachment_change: function (event) { @@ -263,24 +496,31 @@ openerp.mail = function (session) { this.$('textarea.oe_compact').on('focus', _.bind( this.on_compose_expandable, this)); // set the function called when attachments are added - this.$el.on('change', 'input.oe_form_binary_file', _.bind( this.on_attachment_change, this) ); + this.$('input.oe_form_binary_file').on('change', _.bind( this.on_attachment_change, this) ); - this.$el.on('click', '.oe_cancel', _.bind( this.on_cancel, this) ); - this.$el.on('click', '.oe_post', _.bind( this.on_message_post, this) ); - this.$el.on('click', '.oe_full', _.bind( this.on_compose_fullmail, this, 'reply') ); + this.$('.oe_cancel').on('click', _.bind( this.on_cancel, this) ); + this.$('.oe_post').on('click', _.bind( this.on_message_post, this) ); + this.$('.oe_full').on('click', _.bind( this.on_compose_fullmail, this, this.id ? 'reply' : 'comment') ); /* stack for don't close the compose form if the user click on a button */ - this.$el.on('mousedown', '.oe_msg_footer', _.bind( function () { this.stay_open = true; }, this)); - this.$('textarea:not(.oe_compact):first').on('focus, mouseup, keydown', _.bind( function () { this.stay_open = false; }, this)); - this.$('textarea:not(.oe_compact):first').autosize(); + this.$('.oe_msg_footer').on('mousedown', _.bind( function () { this.stay_open = true; }, this)); + var ev_stay = {}; + ev_stay.mouseup = ev_stay.keydown = ev_stay.focus = function () { self.stay_open = false; }; + this.$('textarea:not(.oe_compact)').on(ev_stay); + this.$('textarea:not(.oe_compact)').autosize(); // auto close - this.$el.on('blur', 'textarea:not(.oe_compact):first', _.bind( this.on_compose_expandable, this)); + this.$('textarea:not(.oe_compact)').on('blur', _.bind( this.on_compose_expandable, this)); + + // event: delete child attachments off the oe_msg_attachment_list box + this.$(".oe_msg_attachment_list").on('click', '.oe_delete', this.on_attachment_delete); }, on_compose_fullmail: function (default_composition_mode) { if (default_composition_mode == 'reply') { var context = { + 'default_model': this.context.default_model, + 'default_res_id': this.context.default_res_id, 'default_composition_mode': default_composition_mode, 'default_parent_id': this.id, 'default_body': mail.ChatterUtils.get_text2html(this.$el ? (this.$el.find('textarea:not(.oe_compact)').val() || '') : ''), @@ -357,10 +597,15 @@ openerp.mail = function (session) { this.context.default_parent_id, attachments, this.parent_thread.context - ]).then(function (record) { + ]).done(function (record) { var thread = self.parent_thread; + + if (self.options.display_indented_thread < self.thread_level && thread.parent_message) { + thread = thread.parent_message.parent_thread; + } // create object and attach to the thread object - thread.message_fetch(false, false, [record], function (arg, data) { + thread.message_fetch([['id', 'child_of', [self.id]]], false, [record], function (arg, data) { + data[0].no_sorted = true; var message = thread.create_message_object( data[0] ); // insert the message on dom thread.insert_message( message, self.$el ); @@ -419,30 +664,16 @@ openerp.mail = function (session) { * - - visible message * - - expandable */ - mail.ThreadExpandable = session.web.Widget.extend({ + mail.ThreadExpandable = mail.MessageCommon.extend({ template: 'mail.thread.expandable', - init: function (parent, datasets, context) { - this._super(parent); - this.domain = datasets.domain || []; - this.options = datasets.options; - this.context = _.extend({ - default_model: 'mail.thread', - default_res_id: 0, - default_parent_id: false }, context || {}); - - // data of this expandable message - this.id = datasets.id || -1, - this.model = datasets.model || false, - this.parent_id = datasets.parent_id || false, - this.nb_messages = datasets.nb_messages || 0, - this.thread_level = datasets.thread_level || 0, - this.type = 'expandable', - this.max_limit = this.id < 0 || false, - this.flag_used = false, - this.parent_thread= parent.messages!= undefined ? parent : this.options._parents[0]; + init: function (parent, datasets, options) { + this._super(parent, datasets, options); + this.type = 'expandable'; + this.max_limit = datasets.max_limit; + this.nb_messages = datasets.nb_messages; + this.flag_used = false; }, - start: function () { this._super.apply(this, arguments); @@ -460,7 +691,7 @@ openerp.mail = function (session) { * Bind events in the widget. Each event is slightly described * in the function. */ bind_events: function () { - this.$el.on('click', 'a.oe_msg_fetch_more', this.on_expandable); + this.$('.oe_msg_more_message').on('click', this.on_expandable); }, animated_destroy: function (fadeTime) { @@ -480,154 +711,36 @@ openerp.mail = function (session) { } this.flag_used = true; - this.animated_destroy(200); - this.parent_thread.message_fetch(this.domain, this.context); + var self = this; + + // read messages + self.parent_thread.message_fetch(this.domain, this.context, false, function (arg, data) { + // insert the message on dom after this message + self.id = false; + self.parent_thread.switch_new_message( data, self.$el ); + self.animated_destroy(200); + }); + return false; }, - /** - * call on_message_delete on his parent thread - */ - destroy: function () { - - this._super(); - this.parent_thread.on_message_detroy(this); - - } }); - /** - * ------------------------------------------------------------ - * Thread Message Widget - * ------------------------------------------------------------ - * This widget handles the display of a messages in a thread. - * Displays a record and performs some formatting on the record : - * - record.date: formatting according to the user timezone - * - record.timerelative: relative time givein by timeago lib - * - record.avatar: image url - * - record.attachment_ids[].url: url of each attachmentThe - * thread view : - * - root thread - * - - sub message (parent_id = root message) - * - - - sub thread - * - - - - sub sub message (parent id = sub thread) - * - - sub message (parent_id = root message) - * - - - sub thread - */ - mail.ThreadMessage = session.web.Widget.extend({ + mail.ThreadMessage = mail.MessageCommon.extend({ template: 'mail.thread.message', - /** - * @param {Object} parent parent - * @param {Array} [domain] - * @param {Object} [context] context of the thread. It should - contain at least default_model, default_res_id. Please refer to - the ComposeMessage widget for more information about it. - * @param {Object} [options] - * @param {Object} [thread] read obout mail.Thread object - * @param {Object} [message] - * @param {Number} [truncate_limit=250] number of character to - * display before having a "show more" link; note that the text - * will not be truncated if it does not have 110% of the parameter - * @param {Boolean} [show_record_name] - *... @param {int} [show_reply_button] number thread level to display the reply button - *... @param {int} [show_read_unread_button] number thread level to display the read/unread button - */ - init: function (parent, datasets, context) { - this._super(parent); - - // record domain and context - this.domain = datasets.domain || []; - this.context = _.extend({ - default_model: 'mail.thread', - default_res_id: 0, - default_parent_id: false }, context || {}); - - // record options - this.options = datasets.options; - - // data of this message - this.id = datasets.id || -1, - this.model = datasets.model || false, - this.parent_id = datasets.parent_id || false, - this.res_id = datasets.res_id || false, - this.type = datasets.type || false, - this.is_author = datasets.is_author || false, - this.is_private = datasets.is_private || false, - this.subject = datasets.subject || false, - this.name = datasets.name || false, - this.record_name = datasets.record_name || false, - this.body = datasets.body || false, - this.vote_nb = datasets.vote_nb || 0, - this.has_voted = datasets.has_voted || false, - this.is_favorite = datasets.is_favorite || false, - this.thread_level = datasets.thread_level || 0, - this.to_read = datasets.to_read || false, - this.author_id = datasets.author_id || [], - this.attachment_ids = datasets.attachment_ids || [], - this._date = datasets.date; - - - this.show_reply_button = this.options.show_compose_message && this.options.show_reply_button > this.thread_level; - this.show_read_unread_button = this.options.show_read_unread_button > this.thread_level; - - // record options and data - this.parent_thread= parent.messages!= undefined ? parent : this.options._parents[0]; - this.thread = false; - - if ( this.id > 0 ) { - this.formating_data(); - } - - this.ds_notification = new session.web.DataSetSearch(this, 'mail.notification'); - this.ds_message = new session.web.DataSetSearch(this, 'mail.message'); - this.ds_follow = new session.web.DataSetSearch(this, 'mail.followers'); - }, - - /* Convert date, timerelative and avatar in displayable data. */ - formating_data: function () { - - //formating and add some fields for render - this.date = session.web.format_value(this._date, {type:"datetime"}); - this.timerelative = $.timeago(this.date); - if (this.type == 'email') { - this.avatar = ('/mail/static/src/img/email_icon.png'); - } else { - this.avatar = mail.ChatterUtils.get_image(this.session, 'res.partner', 'image_small', this.author_id[0]); - } - for (var l in this.attachment_ids) { - var attach = this.attachment_ids[l]; - attach['url'] = mail.ChatterUtils.get_attachment_url(this.session, attach); - - if ((attach.filename || attach.name).match(/[.](jpg|jpg|gif|png|tif|svg)$/i)) { - attach.is_image = true; - attach['url'] = mail.ChatterUtils.get_image(this.session, 'ir.attachment', 'datas', attach.id); - } - } - }, start: function () { this._super.apply(this, arguments); this.expender(); - this.$el.hide().fadeIn(750, function () {$(this).css('display', '');}); - this.resize_img(); this.bind_events(); if(this.thread_level < this.options.display_indented_thread) { this.create_thread(); } this.$('.oe_msg_attachments, .oe_msg_images').addClass("oe_hidden"); - }, - resize_img: function () { - var resize = function () { - var h = $(this).height(); - var w = $(this).width(); - if ( h > 100 || w >100 ) { - var ratio = 100 / (h > w ? h : w); - $(this).attr("width", parseInt( w*ratio )).attr("height", parseInt( h*ratio )); - } - }; - this.$("img").load(resize).each(resize); + this.ds_notification = new session.web.DataSetSearch(this, 'mail.notification'); + this.ds_message = new session.web.DataSetSearch(this, 'mail.message'); }, /** @@ -635,30 +748,15 @@ openerp.mail = function (session) { * in the function. */ bind_events: function () { var self = this; + // header icons bindings + this.$('.oe_read').on('click', this.on_message_read); + this.$('.oe_unread').on('click', this.on_message_unread); + this.$('.oe_msg_delete').on('click', this.on_message_delete); + this.$('.oe_reply').on('click', this.on_message_reply); + this.$('.oe_star').on('click', this.on_star); + this.$('.oe_msg_vote').on('click', this.on_vote); + this.$('.oe_view_attachments').on('click', this.on_view_attachments); - // event: click on 'Attachment(s)' in msg - this.$('.oe_mail_msg_view_attachments').on('click', function (event) { - var attach = self.$('.oe_msg_attachments:first, .oe_msg_images:first'); - if ( self.$('.oe_msg_attachments:first').hasClass("oe_hidden") ) { - attach.removeClass("oe_hidden"); - } else { - attach.addClass("oe_hidden"); - } - self.resize_img(); - }); - // event: click on icone 'Read' in header - this.$el.on('click', '.oe_read', this.on_message_read_unread); - // event: click on icone 'UnRead' in header - this.$el.on('click', '.oe_unread', this.on_message_read_unread); - // event: click on 'Delete' in msg side menu - this.$el.on('click', '.oe_msg_delete', this.on_message_delete); - - // event: click on 'Reply' in msg - this.$el.on('click', '.oe_reply', this.on_message_reply); - // event: click on 'Vote' button - this.$el.on('click', '.oe_msg_vote', this.on_vote); - // event: click on 'starred/favorite' button - this.$el.on('click', '.oe_star', this.on_star); }, /* Call the on_compose_message on the thread of this message. */ @@ -673,7 +771,7 @@ openerp.mail = function (session) { this.$('.oe_msg_body:first').expander({ slicePoint: this.options.truncate_limit, expandText: 'read more', - userCollapseText: '[^]', + userCollapseText: 'read less', detailClass: 'oe_msg_tail', moreClass: 'oe_mail_expand', lessClass: 'oe_mail_reduce', @@ -717,6 +815,17 @@ openerp.mail = function (session) { } }, + /* Call the on_compose_message on the thread of this message. */ + on_view_attachments:function (event) { + event.stopPropagation(); + var self = this; + if (!this.toggle_attachment) { + self.display_attachments(); + this.toggle_attachment = true; + } + this.$('.oe_msg_attachment_list').toggle(200); + }, + /** * Wait a confirmation for delete the message on the DB. * Make an animate destroy @@ -732,81 +841,90 @@ openerp.mail = function (session) { return false; }, + /* Check if the message must be destroy and detroy it or check for re render widget + * @param {callback} apply function + */ + check_for_rerender: function () { + var self = this; + + var messages = [this].concat(this.get_childs()); + var message_ids = _.map(messages, function (msg) { return msg.id;}); + var domain = mail.ChatterUtils.expand_domain( this.options.root_thread.domain ) + .concat([["id", "in", message_ids ]]); + + return this.parent_thread.ds_message.call('message_read', [undefined, domain, [], !!this.parent_thread.options.display_indented_thread, this.context, this.parent_thread.id]) + .then( function (records) { + // remove message not loaded + _.map(messages, function (msg) { + if(!_.find(records, function (record) { return record.id == msg.id; })) { + msg.animated_destroy(150); + } else { + msg.renderElement(); + msg.start() + } + }); + + }); + }, + + on_message_read: function (event) { + event.stopPropagation(); + this.on_message_read_unread(true); + return false; + }, + + on_message_unread: function (event) { + event.stopPropagation(); + this.on_message_read_unread(false); + return false; + }, + /*The selected thread and all childs (messages/thread) became read * @param {object} mouse envent */ - on_message_read_unread: function (event) { - event.stopPropagation(); - var self=this; + on_message_read_unread: function (read_value) { + var self = this; + var messages = [this].concat(this.get_childs()); - if ( (this.to_read && this.options.typeof_thread == 'inbox') || - (!this.to_read && this.options.typeof_thread == 'archives')) { - this.animated_destroy(150); - } - - // if this message is read, all childs message display is read - this.ds_notification.call('set_message_read', [ [this.id].concat( this.get_child_ids() ) , this.to_read, this.context]).pipe(function () { - self.$el.removeClass(self.to_read ? 'oe_msg_unread':'oe_msg_read').addClass(self.to_read ? 'oe_msg_read':'oe_msg_unread'); - self.to_read = !self.to_read; - }); - return false; - }, - - /** - * search a message in all thread and child thread. - * This method return an object message. - * @param {object}{int} option.id - * @param {object}{string} option.model - * @param {object}{boolean} option._go_thread_wall - * private for check the top thread - * @return thread object - */ - browse_message: function (options) { - // goto the wall thread for launch browse - if (!options._go_thread_wall) { - options._go_thread_wall = true; - for (var i in this.options._parents[0].messages) { - var res=this.options._parents[0].messages[i].browse_message(options); - if (res) return res; - } - } - - if (this.id==options.id) - return this; - - for (var i in this.thread.messages) { - if (this.thread.messages[i].thread) { - var res=this.thread.messages[i].browse_message(options); - if (res) return res; + // inside the inbox, when the user mark a message as read/done, don't apply this value + // for the stared/favorite message + if (this.options.view_inbox && read_value) { + var messages = _.filter(messages, function (val) { return !val.is_favorite && val.id; }); + if (!messages.length) { + this.check_for_rerender(); + return false; } } + var message_ids = _.map(messages, function (val) { return val.id; }); + this.ds_notification.call('set_message_read', [message_ids, read_value, this.context]) + .then(function () { + // apply modification + _.each(messages, function (msg) { + msg.to_read = !read_value; + if (msg.options.toggle_read) { + msg.options.show_read = msg.to_read; + msg.options.show_unread = !msg.to_read; + } + }); + // check if the message must be display, destroy or rerender + self.check_for_rerender(); + }); return false; }, - /* get all child message id linked. - * @return array of id - */ - get_child_ids: function () { - var res=[] - if (arguments[0]) res.push(this.id); - if (this.thread) { - res = res.concat( this.thread.get_child_ids(true) ); - } - return res; - }, - /** * add or remove a vote for a message and display the result */ on_vote: function (event) { event.stopPropagation(); - var self=this; - return this.ds_message.call('vote_toggle', [[self.id]]).pipe(function (vote) { - self.has_voted = vote; - self.vote_nb += self.has_voted ? 1 : -1; - self.display_vote(); - }); + this.ds_message.call('vote_toggle', [[this.id]]) + .then( + _.bind(function (vote) { + this.has_voted = vote; + this.vote_nb += this.has_voted ? 1 : -1; + this.display_vote(); + }, this)); return false; }, @@ -814,10 +932,10 @@ openerp.mail = function (session) { * Display the render of this message's vote */ display_vote: function () { - var self = this; - var vote_element = session.web.qweb.render('mail.thread.message.vote', {'widget': self}); - self.$(".oe_msg_vote:first").remove(); - self.$(".oe_mail_vote_count:first").replaceWith(vote_element); + var vote_element = session.web.qweb.render('mail.thread.message.vote', {'widget': this}); + this.$(".oe_msg_footer:first .oe_mail_vote_count").remove(); + this.$(".oe_msg_footer:first .oe_msg_vote").replaceWith(vote_element); + this.$('.oe_msg_vote').on('click', this.on_vote); }, /** @@ -827,34 +945,29 @@ openerp.mail = function (session) { event.stopPropagation(); var self=this; var button = self.$('.oe_star:first'); - return this.ds_message.call('favorite_toggle', [[self.id]]).pipe(function (star) { - self.is_favorite=star; - if (self.is_favorite) { - button.addClass('oe_starred'); - } else { - button.removeClass('oe_starred'); - if ( self.options.typeof_thread == 'stared' ) { - self.animated_destroy(150); + + this.ds_message.call('favorite_toggle', [[self.id]]) + .then(function (star) { + self.is_favorite=star; + if (self.is_favorite) { + button.addClass('oe_starred'); + } else { + button.removeClass('oe_starred'); } - } - }); + + if (self.options.view_inbox && self.is_favorite) { + self.on_message_read_unread(true); + } else { + self.check_for_rerender(); + } + }); return false; }, - /** - * call on_message_delete on his parent thread - */ - destroy: function () { - - this._super(); - this.parent_thread.on_message_detroy(this); - - } - }); /** - * ------------------------------------------------------------ + * ------------------------------------------------------------ * Thread Widget * ------------------------------------------------------------ * @@ -881,14 +994,11 @@ openerp.mail = function (session) { * @param {Object} [thread] * @param {int} [display_indented_thread] number thread level to indented threads. * other are on flat mode - * @param {Select} [typeof_thread] inbox/archives/stared/sent - * type of thread and option for user application like animate - * destroy for read/unread * @param {Array} [parents] liked with the parents thread * use with browse, fetch... [O]= top parent */ init: function (parent, datasets, options) { - this._super(parent); + this._super(parent, options); this.domain = options.domain || []; this.context = _.extend({ default_model: 'mail.thread', @@ -896,21 +1006,24 @@ openerp.mail = function (session) { default_parent_id: false }, options.context || {}); this.options = options.options; - this.options._parents = (options.options._parents != undefined ? options.options._parents : []).concat( [this] ); - + this.options.root_thread = (options.options.root_thread != undefined ? options.options.root_thread : this); + this.options.show_compose_message = this.options.show_compose_message && (this.options.display_indented_thread >= this.thread_level || !this.thread_level); + // record options and data this.parent_message= parent.thread!= undefined ? parent : false ; // data of this thread this.id = datasets.id || false, - this.model = datasets.model || false, + this.last_id = datasets.last_id || false, this.parent_id = datasets.parent_id || false, + this.is_private = datasets.is_private || false, this.author_id = datasets.author_id || false, this.thread_level = (datasets.thread_level+1) || 0, this.partner_ids = _.filter(datasets.partner_ids, function (partner) { return partner[0]!=datasets.author_id[0]; } ) this.messages = []; - this.show_compose_message = this.options.show_compose_message && (this.options.show_reply_button > this.thread_level || !this.thread_level); + + this.options.flat_mode = !!(this.options.display_indented_thread > this.thread_level ? this.options.display_indented_thread - this.thread_level : 0); // object compose message this.compose_message = false; @@ -934,13 +1047,10 @@ openerp.mail = function (session) { 'context': this.context, 'options': this.options, }); - if (!this.thread_level) { - // root view + if (!this.thread_level || this.thread_level > this.options.display_indented_thread) { this.compose_message.insertBefore(this.$el); - } else if (this.thread_level > this.options.display_indented_thread) { - this.compose_message.insertAfter(this.$el); } else { - this.compose_message.appendTo(this.$el); + this.compose_message.prependTo(this.$el); } } }, @@ -948,21 +1058,20 @@ openerp.mail = function (session) { /* When the expandable object is visible on screen (with scrolling) * then the on_expandable function is launch */ - on_scroll: function (event) { - if (event)event.stopPropagation(); - this.$('.oe_msg_expandable:last'); - - var message = this.messages[this.messages.length-1]; - if (message && message.type=="expandable" && message.max_limit) { - var pos = message.$el.position(); + on_scroll: function () { + var expandables = + _.each( _.filter(this.messages, function (val) {return val.max_limit && !val.parent_id;}), function (val) { + var pos = val.$el.position(); if (pos.top) { /* bottom of the screen */ var bottom = $(window).scrollTop()+$(window).height()+200; if (bottom > pos.top) { - message.on_expandable(); + val.on_expandable(); + // load only one time + val.loading = true; } } - } + }); }, /** @@ -970,8 +1079,8 @@ openerp.mail = function (session) { * in the function. */ bind_events: function () { var self = this; - self.$el.on('click', '.oe_mail_list_recipients .oe_more', self.on_show_recipients); - self.$el.on('click', '.oe_mail_compose_textarea .oe_more_hidden', self.on_hide_recipients); + self.$('.oe_mail_list_recipients .oe_more').on('click', self.on_show_recipients); + self.$('.oe_mail_compose_textarea .oe_more_hidden').on('click', self.on_hide_recipients); }, /** @@ -981,6 +1090,7 @@ openerp.mail = function (session) { var p=$(this).parent(); p.find('.oe_more_hidden, .oe_hidden').show(); p.find('.oe_more').hide(); + return false; }, /** @@ -990,15 +1100,14 @@ openerp.mail = function (session) { var p=$(this).parent(); p.find('.oe_more_hidden, .oe_hidden').hide(); p.find('.oe_more').show(); + return false; }, /* get all child message/thread id linked. * @return array of id */ get_child_ids: function () { - var res=[]; - _(this.get_childs()).each(function (val, key) { res.push(val.id); }); - return res; + return _.map(this.get_childs(), function (val) { return val.id; }); }, /* get all child message/thread linked. @@ -1033,17 +1142,17 @@ openerp.mail = function (session) { // goto the wall thread for launch browse if (!options._go_thread_wall) { options._go_thread_wall = true; - return this.options._parents[0].browse_thread(options); + return this.options.root_thread.browse_thread(options); } - if (this.id==options.id) { + if (this.id == options.id) { return this; } if (options.id) { for (var i in this.messages) { if (this.messages[i].thread) { - var res=this.messages[i].thread.browse_thread({'id':options.id, '_go_thread_wall':true}); + var res = this.messages[i].thread.browse_thread({'id':options.id, '_go_thread_wall':true}); if (res) return res; } } @@ -1067,8 +1176,8 @@ openerp.mail = function (session) { * @return message object */ browse_message: function (options) { - if (this.options._parents[0].messages[0]) - return this.options._parents[0].messages[0].browse_message(options); + if (this.options.root_thread.messages[0]) + return this.options.root_thread.messages[0].browse_message(options); }, /** @@ -1079,6 +1188,7 @@ openerp.mail = function (session) { on_compose_message: function () { this.instantiate_compose_message(); this.compose_message.on_compose_expandable(); + return false; }, /** @@ -1086,8 +1196,8 @@ openerp.mail = function (session) { */ no_message: function () { var no_message = $(session.web.qweb.render('mail.wall_no_message', {})); - if (this.options.no_message) { - no_message.html(this.options.no_message); + if (this.options.help) { + no_message.html(this.options.help); } no_message.appendTo(this.$el); }, @@ -1101,18 +1211,20 @@ openerp.mail = function (session) { * @param {Array} ids read (if the are some ids, the method don't use the domain) */ message_fetch: function (replace_domain, replace_context, ids, callback) { - var self = this; - - // domain and context: options + additional - fetch_domain = replace_domain ? replace_domain : this.domain; - fetch_context = replace_context ? replace_context : this.context; - var message_loaded_ids = this.id ? [this.id].concat( self.get_child_ids() ) : self.get_child_ids(); - - // CHM note : option for sending in flat mode by server - var thread_level = this.options.display_indented_thread > this.thread_level ? this.options.display_indented_thread - this.thread_level : 0; - - return this.ds_message.call('message_read', [ids, fetch_domain, message_loaded_ids, thread_level, fetch_context, this.context.default_parent_id || undefined]) - .then(callback ? _.bind(callback, this, arguments) : this.proxy('switch_new_message')); + return this.ds_message.call('message_read', [ + // ids force to read + ids == false ? undefined : ids, + // domain + additional + (replace_domain ? replace_domain : this.domain), + // ids allready loaded + (this.id ? [this.id].concat( this.get_child_ids() ) : this.get_child_ids()), + // option for sending in flat mode by server + this.options.flat_mode, + // context + additional + (replace_context ? replace_context : this.context), + // parent_id + this.context.default_parent_id || undefined + ]).done(callback ? _.bind(callback, this, arguments) : this.proxy('switch_new_message')); }, /** @@ -1128,22 +1240,23 @@ openerp.mail = function (session) { data.options = _.extend(self.options, data.options); if (data.type=='expandable') { - var message = new mail.ThreadExpandable(self, data, { + var message = new mail.ThreadExpandable(self, data, {'context':{ 'default_model': data.model || self.context.default_model, 'default_res_id': data.res_id || self.context.default_res_id, 'default_parent_id': self.id, - }); + }}); } else { - var message = new mail.ThreadMessage(self, data, { + var message = new mail.ThreadMessage(self, data, {'context':{ 'default_model': data.model, 'default_res_id': data.res_id, 'default_parent_id': data.id, - }); + }}); } // check if the message is already create for (var i in self.messages) { if (self.messages[i] && self.messages[i].id == message.id) { + console.log('Reload message', message.id); self.messages[i].destroy(); } } @@ -1163,74 +1276,18 @@ openerp.mail = function (session) { */ insert_message: function (message, dom_insert_after) { var self=this; - - if (this.show_compose_message && this.options.show_compact_message) { + if (this.options.show_compact_message > this.thread_level) { this.instantiate_compose_message(); this.compose_message.do_show_compact(); } - this.$('.oe_wall_no_message').remove(); + this.$('.oe_view_nocontent').remove(); if (dom_insert_after) { message.insertAfter(dom_insert_after); - return message - } - - // check older and newer message for insertion - var message_newer = false; - var message_older = false; - if (message.id > 0) { - for (var i in self.messages) { - if (self.messages[i].id > message.id) { - if (!message_newer || message_newer.id > self.messages[i].id) { - message_newer = self.messages[i]; - } - } else if (self.messages[i].id > 0 && self.messages[i].id < message.id) { - if (!message_older || message_older.id < self.messages[i].id) { - message_older = self.messages[i]; - } - } - } - } - - var sort = (!!self.thread_level || message.id<0); - - if (sort) { - if (message_older) { - - message.insertAfter(message_older.thread ? (message_older.thread.compose_message ? message_older.thread.compose_message.$el : message_older.thread.$el) : message_older.$el); - - } else if (message_newer) { - - message.insertBefore(message_newer.$el); - - } else if (message.id < 0) { - - message.appendTo(self.$el); - - } else { - - message.prependTo(self.$el); - } } else { - if (message_older) { - - message.insertBefore(message_older.$el); - - } else if (message_newer) { - - message.insertAfter(message_newer.thread ? (message_newer.thread.compose_message ? message_newer.thread.compose_message.$el : message_newer.thread.$el) : message_newer.$el ); - - } else if (message.id < 0) { - - message.prependTo(self.$el); - - } else { - - message.appendTo(self.$el); - - } + message.appendTo(self.$el); } return message @@ -1241,7 +1298,7 @@ openerp.mail = function (session) { * Each message is send to his parent object (or parent thread flat mode) for creating the object message. * @param : {Array} datas from calling RPC to "message_read" */ - switch_new_message: function (records) { + switch_new_message: function (records, dom_insert_after) { var self=this; _(records).each(function (record) { var thread = self.browse_thread({ @@ -1251,7 +1308,7 @@ openerp.mail = function (session) { // create object and attach to the thread object var message = thread.create_message_object( record ); // insert the message on dom - thread.insert_message( message ); + thread.insert_message( message, typeof dom_insert_after == 'object' ? dom_insert_after : false); }); }, @@ -1262,6 +1319,10 @@ openerp.mail = function (session) { on_message_detroy: function (message) { this.messages = _.filter(this.messages, function (val) { return !val.isDestroyed(); }); + if (this.options.root_thread == this && !this.messages.length) { + this.no_message(); + } + return false; }, @@ -1323,7 +1384,6 @@ openerp.mail = function (session) { } else { // create a expandable message var expandable = new mail.ThreadExpandable(this, { - 'id': message.id, 'model': message.model, 'parent_id': message.parent_id, 'nb_messages': 1, @@ -1332,9 +1392,11 @@ openerp.mail = function (session) { 'domain': message_dom, 'options': message.options, }, { - 'default_model': message.model || this.context.default_model, - 'default_res_id': message.res_id || this.context.default_res_id, - 'default_parent_id': this.id, + 'context':{ + 'default_model': message.model || this.context.default_model, + 'default_res_id': message.res_id || this.context.default_res_id, + 'default_parent_id': this.id, + } }); // add object on array and DOM @@ -1350,7 +1412,7 @@ openerp.mail = function (session) { }); /** - * ------------------------------------------------------------ + * ------------------------------------------------------------ * mail : root Widget * ------------------------------------------------------------ * @@ -1360,7 +1422,7 @@ openerp.mail = function (session) { */ session.web.client_actions.add('mail.Widget', 'session.mail.Widget'); mail.Widget = session.web.Widget.extend({ - template: 'mail.Widget', + template: 'mail.Root', /** * @param {Object} parent parent @@ -1368,15 +1430,13 @@ openerp.mail = function (session) { * @param {Object} [context] context of the thread. It should * contain at least default_model, default_res_id. Please refer to * the compose_message widget for more information about it. - * ... @param {Select} [typeof_thread=(mail|stared|archives|send|other)] - * options for destroy message when the user click on a button * @param {Object} [options] *... @param {Number} [truncate_limit=250] number of character to * display before having a "show more" link; note that the text * will not be truncated if it does not have 110% of the parameter *... @param {Boolean} [show_record_name] display the name and link for do action - *... @param {int} [show_reply_button] number thread level to display the reply button - *... @param {int} [show_read_unread_button] number thread level to display the read/unread button + *... @param {boolean} [show_reply_button] display the reply button + *... @param {boolean} [show_read_unread_button] display the read/unread button *... @param {int} [display_indented_thread] number thread level to indented threads. * other are on flat mode *... @param {Boolean} [show_compose_message] allow to display the composer @@ -1387,35 +1447,25 @@ openerp.mail = function (session) { * @param {String} [no_message] Message to display when there are no message */ init: function (parent, action) { - var options = action.params || {}; - this._super(parent); - this.domain = options.domain || []; - this.context = options.context || {}; - this.search_results = {'domain': [], 'context': {}, 'groupby': {}}; + this._super(parent, action); + var self = this; + this.action = _.clone(action); + this.domain = this.action.domain || this.action.params.domain || []; + this.context = this.action.context || this.action.params.context || {}; - this.options = _.extend({ - 'typeof_thread' : 'inbox', + this.action.params = _.extend({ 'display_indented_thread' : -1, - 'show_reply_button' : -1, - 'show_read_unread_button' : -1, + 'show_reply_button' : false, + 'show_read_unread_button' : false, 'truncate_limit' : 250, 'show_record_name' : false, 'show_compose_message' : false, 'show_compact_message' : false, + 'view_inbox': false, 'message_ids': undefined, - 'no_message': false - }, options); + }, this.action.params); - if (this.display_indented_thread === false) { - this.display_indented_thread = -1; - } - if (this.show_reply_button === false) { - this.show_reply_button = -1; - } - if (this.show_read_unread_button === false) { - this.show_read_unread_button = -1; - } - + this.action.params.help = this.action.help || false; }, start: function (options) { @@ -1423,7 +1473,6 @@ openerp.mail = function (session) { this.message_render(); this.bind_events(); }, - /** *Create the root thread and display this object in the DOM. @@ -1435,35 +1484,32 @@ openerp.mail = function (session) { this.thread = new mail.Thread(this, {}, { 'domain' : this.domain, 'context' : this.context, - 'options': this.options, + 'options': this.action.params, }); this.thread.appendTo( this.$el ); this.thread.no_message(); - this.thread.message_fetch(null, null, this.options.message_ids); - if (this.options.show_compose_message) { + if (this.action.params.show_compose_message) { this.thread.instantiate_compose_message(); - if (this.options.show_compact_message) { - this.thread.compose_message.do_show_compact(); - } else { - this.thread.compose_message.do_hide_compact(); - } + this.thread.compose_message.do_show_compact(); } + + this.thread.message_fetch(null, null, this.action.params.message_ids); + }, bind_events: function () { - if (this.context['typeof_thread']!='other') { - $(document).scroll( this.thread.on_scroll ); - $(window).resize( this.thread.on_scroll ); - window.setTimeout( this.thread.on_scroll, 500 ); - } + $(document).scroll( _.bind(this.thread.on_scroll, this.thread) ); + $(window).resize( _.bind(this.thread.on_scroll, this.thread) ); + this.$el.resize( _.bind(this.thread.on_scroll, this.thread) ); + window.setTimeout( _.bind(this.thread.on_scroll, this.thread), 500 ); } }); /** - * ------------------------------------------------------------ + * ------------------------------------------------------------ * mail_thread Widget * ------------------------------------------------------------ * @@ -1476,10 +1522,19 @@ openerp.mail = function (session) { mail.RecordThread = session.web.form.AbstractField.extend({ template: 'mail.record_thread', - init: function () { + init: function (parent, node) { this._super.apply(this, arguments); - this.options.domain = this.options.domain || []; - this.options.context = {'default_model': 'mail.thread', 'default_res_id': false}; + this.node = _.clone(node); + + this.node.params = _.extend({ + 'display_indented_thread': -1, + 'show_reply_button': false, + 'show_read_unread_button': false, + 'show_compose_message': this.view.is_action_enabled('edit'), + 'show_compact_message': 1, + }, this.node.params); + + this.domain = this.node.params && this.node.params.domain || []; }, start: function () { @@ -1492,42 +1547,32 @@ openerp.mail = function (session) { _check_visibility: function () { this.$el.toggle(this.view.get("actual_mode") !== "create"); }, + render_value: function () { var self = this; + if (! this.view.datarecord.id || session.web.BufferedDataSet.virtual_id_regex.test(this.view.datarecord.id)) { this.$('oe_mail_thread').hide(); return; } - // update context - _.extend(this.options.context, { - default_res_id: this.view.datarecord.id, - default_model: this.view.model, - default_is_private: false }); - // update domain - var domain = this.options.domain.concat([['model', '=', this.view.model], ['res_id', '=', this.view.datarecord.id]]); - var show_compose_message = this.view.is_action_enabled('edit') || - (this.getParent().fields.message_is_follower && this.getParent().fields.message_is_follower.get_value()); - - var message_ids = this.getParent().fields.message_ids && this.getParent().fields.message_ids.get_value(); + this.node.params = _.extend({ + 'message_ids': this.getParent().fields.message_ids ? this.getParent().fields.message_ids.get_value() : undefined, + }, this.node.params); + this.node.context = { + 'default_res_id': this.view.datarecord.id || false, + 'default_model': this.view.model || false, + }; if (this.root) { + $('').insertAfter(this.root.$el); this.root.destroy(); } // create and render Thread widget - this.root = new mail.Widget(this, { params: { - 'domain' : domain, - 'context' : this.options.context, - 'typeof_thread': this.options.context['typeof_thread'] || 'other', - 'display_indented_thread': -1, - 'show_reply_button': 0, - 'show_read_unread_button': -1, - 'show_compose_message': show_compose_message, - 'message_ids': message_ids, - 'show_compact_message': true, - 'no_message': this.node.attrs.help - }} - ); + this.root = new mail.Widget(this, _.extend(this.node, { + 'domain' : (this.domain || []).concat([['model', '=', this.view.model], ['res_id', '=', this.view.datarecord.id]]), + + })); return this.root.replace(this.$('.oe_mail-placeholder')); }, @@ -1535,7 +1580,7 @@ openerp.mail = function (session) { /** - * ------------------------------------------------------------ + * ------------------------------------------------------------ * Wall Widget * ------------------------------------------------------------ * @@ -1543,6 +1588,7 @@ openerp.mail = function (session) { * use is to receive a context and a domain, and to delegate the message * fetching and displaying to the Thread widget. */ + session.web.client_actions.add('mail.wall', 'session.mail.Wall'); mail.Wall = session.web.Widget.extend({ template: 'mail.wall', @@ -1555,35 +1601,53 @@ openerp.mail = function (session) { * contain default_model, default_res_id, to give it to the threads. */ init: function (parent, action) { - this._super(parent); - var options = action.params || {}; - this.options = options; - this.options.domain = options.domain || []; - this.options.context = options.context || {}; - this.search_results = {'domain': [], 'context': {}, 'groupby': {}} - this.ds_msg = new session.web.DataSetSearch(this, 'mail.message'); + this._super(parent, action); + + this.action = _.clone(action); + this.domain = this.action.params.domain || this.action.domain || []; + this.context = this.action.params.context || this.action.context || {}; + + this.defaults = {}; + for (var key in this.context) { + if (key.match(/^search_default_/)) { + this.defaults[key.replace(/^search_default_/, '')] = this.context[key]; + } + } + + this.action.params = _.extend({ + 'display_indented_thread': 1, + 'show_reply_button': true, + 'show_read_unread_button': true, + 'show_compose_message': true, + 'show_compact_message': this.action.params.view_mailbox ? false : 1, + 'view_inbox': false, + }, this.action.params); }, start: function () { - this._super.apply(this, arguments); - var searchview_ready = this.load_searchview({}, false); - var thread_displayed = this.message_render(); - this.options.domain = this.options.domain.concat(this.search_results['domain']); + this._super.apply(this); this.bind_events(); - return $.when(searchview_ready, thread_displayed); + var searchview_loaded = this.load_searchview(this.defaults); + if (! this.searchview.has_defaults) { + this.message_render(); + } + }, /** * Load the mail.message search view * @param {Object} defaults ?? - * @param {Boolean} hidden some kind of trick we do not care here */ - load_searchview: function (defaults, hidden) { + load_searchview: function (defaults) { var self = this; - this.searchview = new session.web.SearchView(this, this.ds_msg, false, defaults || {}, hidden || false); - return this.searchview.appendTo(this.$('.oe_view_manager_view_search')).then(function () { - self.searchview.on('search_data', self, self.do_searchview_search); - }); + var ds_msg = new session.web.DataSetSearch(this, 'mail.message'); + this.searchview = new session.web.SearchView(this, ds_msg, false, defaults || {}, false); + this.searchview.appendTo(this.$('.oe_view_manager_view_search')) + .then(function () { self.searchview.on('search_data', self, self.do_searchview_search); }); + if (this.searchview.has_defaults) { + this.searchview.ready.then(this.searchview.do_search); + } + return this.searchview }, /** @@ -1600,32 +1664,25 @@ openerp.mail = function (session) { contexts: contexts || [], group_by_seq: groupbys || [] }).then(function (results) { - self.search_results['context'] = results.context; - self.search_results['domain'] = results.domain; - self.root.destroy(); - return self.message_render(); + if(self.root) { + $('').insertAfter(self.root.$el); + self.root.destroy(); + } + return self.message_render(results); }); }, - /** - *Create the root thread widget and display this object in the DOM - */ + * Create the root thread widget and display this object in the DOM + */ message_render: function (search) { - var domain = this.options.domain.concat(this.search_results['domain']); - var context = _.extend(this.options.context, search&&search.search_results['context'] ? search.search_results['context'] : {}); - this.root = new mail.Widget(this, { params: { + var domain = this.domain.concat(search && search['domain'] ? search['domain'] : []); + var context = _.extend(this.context, search && search['context'] ? search['context'] : {}); + + this.root = new mail.Widget(this, _.extend(this.action, { 'domain' : domain, 'context' : context, - 'typeof_thread': context['typeof_thread'] || 'other', - 'display_indented_thread': 1, - 'show_reply_button': 10, - 'show_read_unread_button': 11, - 'show_compose_message': true, - 'show_compact_message': false, - }} - ); - + })); return this.root.replace(this.$('.oe_mail-placeholder')); }, @@ -1653,19 +1710,18 @@ openerp.mail = function (session) { /** - * ------------------------------------------------------------ + * ------------------------------------------------------------ * UserMenu * ------------------------------------------------------------ * * Add a link on the top user bar for write a full mail */ session.web.ComposeMessageTopButton = session.web.Widget.extend({ - template:'mail.compose_message.button_top_bar', + template:'mail.ComposeMessageTopButton', - start: function (parent, params) { - var self = this; - this.$el.on('click', 'button', self.on_compose_message ); - this._super(parent, params); + start: function () { + this.$('button').on('click', this.on_compose_message ); + this._super(); }, on_compose_message: function (event) { @@ -1675,26 +1731,22 @@ openerp.mail = function (session) { res_model: 'mail.compose.message', view_mode: 'form', view_type: 'form', - action_from: 'mail.ThreadComposeMessage', views: [[false, 'form']], target: 'new', - context: { - 'default_model': '', - 'default_res_id': false, - 'default_content_subtype': 'html', - }, + context: { 'default_content_subtype': 'html' }, }; session.client.action_manager.do_action(action); }, - }); - session.web.UserMenu = session.web.UserMenu.extend({ - start: function (parent, params) { - var render = new session.web.ComposeMessageTopButton(); - render.insertAfter(this.$el); - this._super(parent, params); - } + session.web.UserMenu.include({ + do_update: function(){ + var self = this; + this._super.apply(this, arguments); + this.update_promise.then(function() { + var mail_button = new session.web.ComposeMessageTopButton(); + mail_button.appendTo(session.webclient.$el.find('.oe_systray')); + }); + }, }); - }; diff --git a/addons/mail/static/src/js/mail_followers.js b/addons/mail/static/src/js/mail_followers.js index a13d2048bf3..7bb019cde4f 100644 --- a/addons/mail/static/src/js/mail_followers.js +++ b/addons/mail/static/src/js/mail_followers.js @@ -30,6 +30,8 @@ openerp_mail_followers = function(session, mail) { this.ds_model = new session.web.DataSetSearch(this, this.view.model); this.ds_follow = new session.web.DataSetSearch(this, this.field.relation); this.ds_users = new session.web.DataSetSearch(this, 'res.users'); + + this.value = []; }, start: function() { @@ -41,6 +43,11 @@ openerp_mail_followers = function(session, mail) { this._super(); }, + set_value: function(_value) { + this.value = _value; + this._super(_value); + }, + _check_visibility: function() { this.$el.toggle(this.view.get("actual_mode") !== "create"); }, @@ -85,22 +92,23 @@ openerp_mail_followers = function(session, mail) { read_value: function () { var self = this; - return this.ds_model.read_ids([this.view.datarecord.id], ['message_follower_ids']).pipe(function (results) { - self.set_value(results[0].message_follower_ids); + return this.ds_model.read_ids([this.view.datarecord.id], ['message_follower_ids']).then(function (results) { + self.value = results[0].message_follower_ids; + self.render_value(); }); }, render_value: function () { this.reinit(); - return this.fetch_followers(this.get("value")); + return this.fetch_followers(this.value); }, fetch_followers: function (value_) { this.value = value_ || {}; return this.ds_follow.call('read', [this.value, ['name', 'user_ids']]) - .pipe(this.proxy('display_followers'), this.proxy('fetch_generic')) - .pipe(this.proxy('display_buttons')) - .pipe(this.proxy('fetch_subtypes')); + .then(this.proxy('display_followers'), this.proxy('fetch_generic')) + .then(this.proxy('display_buttons')) + .then(this.proxy('fetch_subtypes')); }, /** Read on res.partner failed: fall back on a generic case @@ -109,10 +117,10 @@ openerp_mail_followers = function(session, mail) { fetch_generic: function (error, event) { var self = this; event.preventDefault(); - return this.ds_users.call('read', [this.session.uid, ['partner_id']]).pipe(function (results) { + return this.ds_users.call('read', [this.session.uid, ['partner_id']]).then(function (results) { var pid = results['partner_id'][0]; - self.message_is_follower = (_.indexOf(self.get('value'), pid) != -1); - }).pipe(self.proxy('display_generic')); + self.message_is_follower = (_.indexOf(self.value, pid) != -1); + }).then(self.proxy('display_generic')); }, _format_followers: function(count){ // TDE note: why redefining _t ? @@ -131,7 +139,7 @@ openerp_mail_followers = function(session, mail) { display_generic: function () { var self = this; var node_user_list = this.$('.oe_follower_list').empty(); - this.$('.oe_follower_title').html(this._format_followers(this.get('value').length)); + this.$('.oe_follower_title').html(this._format_followers(this.value.length)); }, /** Display the followers */ @@ -179,7 +187,7 @@ openerp_mail_followers = function(session, mail) { var subtype_list_ul = this.$('.oe_subtype_list').empty(); if (! this.message_is_follower) return; var context = new session.web.CompoundContext(this.build_context(), {}); - this.ds_model.call('message_get_subscription_data', [[this.view.datarecord.id], context]).pipe(this.proxy('display_subtypes')); + this.ds_model.call('message_get_subscription_data', [[this.view.datarecord.id], context]).then(this.proxy('display_subtypes')); }, /** Display subtypes: {'name': default, followed} */ @@ -206,7 +214,8 @@ openerp_mail_followers = function(session, mail) { $(record).attr('checked',false); }); var context = new session.web.CompoundContext(this.build_context(), {}); - return this.ds_model.call('message_unsubscribe_users', [[this.view.datarecord.id], [this.session.uid], context]).pipe(this.proxy('read_value')); + return this.ds_model.call('message_unsubscribe_users', [[this.view.datarecord.id], [this.session.uid], context]) + .then(this.proxy('read_value')); }, do_update_subscription: function (event) { @@ -220,8 +229,8 @@ openerp_mail_followers = function(session, mail) { }); var context = new session.web.CompoundContext(this.build_context(), {}); - return this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], [this.session.uid], this.message_is_follower ? checklist:undefined, context]) - .pipe(this.proxy('read_value')); + return this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], [this.session.uid], this.message_is_follower ? checklist : undefined, context]) + .then(this.proxy('read_value')); }, }); }; diff --git a/addons/mail/static/src/js/many2many_tags_email.js b/addons/mail/static/src/js/many2many_tags_email.js index 52dcbf7aeb5..cc701e8a585 100644 --- a/addons/mail/static/src/js/many2many_tags_email.js +++ b/addons/mail/static/src/js/many2many_tags_email.js @@ -46,7 +46,7 @@ instance.web.form.FieldMany2ManyTagsEmail = instance.web.form.FieldMany2ManyTags ["email", "=", false], ["notification_email_send", "in", ['all', 'comment']] ]], {context: this.build_context()}) - .pipe(function (record_ids) { + .then(function (record_ids) { // valid partner var valid_partner = _.difference(ids, record_ids); self.values = self.values.concat(valid_partner); diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml index caa85cd3deb..2ba1733043a 100644 --- a/addons/mail/static/src/xml/mail.xml +++ b/addons/mail/static/src/xml/mail.xml @@ -1,8 +1,9 @@
Use template +
Use template
- or
+ +