diff options
| author | sma (Tiny) <sma@tinyerp.com> | 2011-10-03 16:57:31 +0530 |
|---|---|---|
| committer | sma (Tiny) <sma@tinyerp.com> | 2011-10-03 16:57:31 +0530 |
| commit | 7e9e542418f791b024de401236eeb8e006be42d4 (patch) | |
| tree | 91e49927b0de50c8c690a2eb608b425a12b4ac7c /addons | |
| parent | 3e11ca62990fde34e537e233e3b099d22f8c8373 (diff) | |
| parent | 2dae9519d7c3d0a4c2217b63939bb4eb1d83d39d (diff) | |
[MERGE]
bzr revid: sma@tinyerp.com-20110926105300-72fwa60uvvdf1qgg
bzr revid: sma@tinyerp.com-20111003112731-uobxur1ryqb5laxy
Diffstat (limited to 'addons')
50 files changed, 3159 insertions, 1276 deletions
diff --git a/addons/web/__init__.py b/addons/web/__init__.py index 2478edb81b7..8802cb548c3 100644 --- a/addons/web/__init__.py +++ b/addons/web/__init__.py @@ -2,27 +2,26 @@ import common import controllers import common.dispatch import logging +import optparse _logger = logging.getLogger(__name__) +class Options(object): + pass + def wsgi_postload(): - import openerp.wsgi + import openerp import os import tempfile _logger.info("embedded mode") - class Options(object): - pass o = Options() - o.dbfilter = '.*' + o.dbfilter = openerp.tools.config['dbfilter'] + o.server_wide_modules = openerp.conf.server_wide_modules or ['web'] o.session_storage = os.path.join(tempfile.gettempdir(), "oe-sessions") - o.addons_path = os.path.dirname(os.path.dirname(__file__)) + o.addons_path = openerp.modules.module.ad_paths o.serve_static = True o.backend = 'local' app = common.dispatch.Root(o) - #import openerp.wsgi openerp.wsgi.register_wsgi_handler(app) -# TODO -# if we detect that we are imported from the openerp server register common.Root() as a wsgi entry point - diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py index 85464e750cb..990f0b445a0 100644 --- a/addons/web/__openerp__.py +++ b/addons/web/__openerp__.py @@ -2,6 +2,7 @@ "name" : "web", "depends" : [], 'active': True, + 'post_load' : 'wsgi_postload', 'js' : [ "static/lib/datejs/globalization/en-US.js", "static/lib/datejs/core.js", @@ -19,11 +20,13 @@ "static/lib/jquery.ui/js/jquery-ui-1.8.9.custom.min.js", "static/lib/jquery.ui/js/jquery-ui-timepicker-addon.js", "static/lib/jquery.ui.notify/js/jquery.notify.js", + "static/lib/jquery.deferred-queue/jquery.deferred-queue.js", "static/lib/json/json2.js", "static/lib/qweb/qweb2.js", "static/lib/underscore/underscore.js", "static/lib/underscore/underscore.string.js", "static/lib/labjs/LAB.src.js", + "static/lib/py.parse/lib/py.js", "static/src/js/boot.js", "static/src/js/core.js", "static/src/js/dates.js", @@ -32,6 +35,7 @@ "static/src/js/views.js", "static/src/js/data.js", "static/src/js/data_export.js", + "static/src/js/data_import.js", "static/src/js/search.js", "static/src/js/view_form.js", "static/src/js/view_list.js", @@ -45,6 +49,6 @@ "static/lib/jquery.ui.notify/css/ui.notify.css", "static/src/css/base.css", "static/src/css/data_export.css", + "static/src/css/data_import.css", ], - 'post_load' : 'wsgi_postload', } diff --git a/addons/web/common/dispatch.py b/addons/web/common/dispatch.py index 98b948704fa..cb998caf891 100644 --- a/addons/web/common/dispatch.py +++ b/addons/web/common/dispatch.py @@ -3,6 +3,7 @@ from __future__ import with_statement import functools import logging +import urllib import os import pprint import sys @@ -13,7 +14,6 @@ import xmlrpclib import simplejson import werkzeug.datastructures import werkzeug.exceptions -import werkzeug.urls import werkzeug.utils import werkzeug.wrappers import werkzeug.wsgi @@ -21,8 +21,7 @@ import werkzeug.wsgi import ast import nonliterals import http -# import backendlocal as backend -import session as backend +import session import openerplib __all__ = ['Root', 'jsonrequest', 'httprequest', 'Controller', @@ -34,7 +33,6 @@ _logger = logging.getLogger(__name__) # Globals (wont move into a pool) #----------------------------------------------------------- -applicationsession = {} addons_module = {} addons_manifest = {} controllers_class = {} @@ -53,10 +51,6 @@ class WebRequest(object): :type request: :class:`werkzeug.wrappers.BaseRequest` :param config: configuration object - .. attribute:: applicationsession - - an application-wide :class:`~collections.Mapping` - .. attribute:: httprequest the original :class:`werkzeug.wrappers.Request` object provided to the @@ -79,12 +73,12 @@ class WebRequest(object): .. attribute:: session_id - opaque identifier for the :class:`backend.OpenERPSession` instance of + opaque identifier for the :class:`session.OpenERPSession` instance of the current request .. attribute:: session - :class:`~backend.OpenERPSession` instance for the current request + :class:`~session.OpenERPSession` instance for the current request .. attribute:: context @@ -95,18 +89,17 @@ class WebRequest(object): ``bool``, indicates whether the debug mode is active on the client """ def __init__(self, request, config): - self.applicationsession = applicationsession self.httprequest = request self.httpresponse = None self.httpsession = request.session self.config = config + def init(self, params): self.params = dict(params) # OpenERP session setup self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex - self.session = self.httpsession.setdefault( - self.session_id, backend.OpenERPSession( - self.config.server_host, self.config.server_port)) + self.session = self.httpsession.setdefault(self.session_id, session.OpenERPSession()) + self.session.config = self.config self.context = self.params.pop('context', None) self.debug = self.params.pop('debug', False) != False @@ -317,9 +310,16 @@ class Root(object): by the server, will be filtered by this pattern """ def __init__(self, options): - self.root = werkzeug.urls.Href('/web/webclient/home') + self.root = '/web/webclient/home' self.config = options + if self.config.backend == 'local': + conn = openerplib.get_connector(protocol='local') + else: + conn = openerplib.get_connector(hostname=self.config.server_host, + port=self.config.server_port) + self.config.connector = conn + self.session_cookie = 'sessionid' self.addons = {} @@ -349,13 +349,12 @@ class Root(object): request.parameter_storage_class = werkzeug.datastructures.ImmutableDict if request.path == '/': - return werkzeug.utils.redirect( - self.root(dict(request.args, debug='')), 301)( - environ, start_response) + params = urllib.urlencode(dict(request.args, debug='')) + return werkzeug.utils.redirect(self.root + '?' + params, 301)( + environ, start_response) elif request.path == '/mobile': return werkzeug.utils.redirect( - '/web_mobile/static/src/web_mobile.html', 301)( - environ, start_response) + '/web_mobile/static/src/web_mobile.html', 301)(environ, start_response) handler = self.find_handler(*(request.path.split('/')[1:])) @@ -383,21 +382,21 @@ class Root(object): static URLs to the corresponding directories """ statics = {} - addons_path = self.config.addons_path - if addons_path not in sys.path: - sys.path.insert(0, addons_path) - for module in os.listdir(addons_path): - if module not in addons_module: - manifest_path = os.path.join(addons_path, module, '__openerp__.py') - if os.path.isfile(manifest_path): - manifest = ast.literal_eval(open(manifest_path).read()) - _logger.info("Loading %s", module) - m = __import__(module) - addons_module[module] = m - addons_manifest[module] = manifest - - statics['/%s/static' % module] = \ - os.path.join(addons_path, module, 'static') + for addons_path in self.config.addons_path: + if addons_path not in sys.path: + sys.path.insert(0, addons_path) + for module in os.listdir(addons_path): + if module not in addons_module: + manifest_path = os.path.join(addons_path, module, '__openerp__.py') + path_static = os.path.join(addons_path, module, 'static') + if os.path.isfile(manifest_path) and os.path.isdir(path_static): + manifest = ast.literal_eval(open(manifest_path).read()) + manifest['addons_path'] = addons_path + _logger.info("Loading %s", module) + m = __import__(module) + addons_module[module] = m + addons_manifest[module] = manifest + statics['/%s/static' % module] = path_static for k, v in controllers_class.items(): if k not in controllers_object: o = v() diff --git a/addons/web/common/http.py b/addons/web/common/http.py index 3f6c96d1f42..0186fd985cc 100644 --- a/addons/web/common/http.py +++ b/addons/web/common/http.py @@ -20,6 +20,7 @@ def session(request, storage_path, session_cookie='sessionid'): else: request.session = session_store.new() - yield request.session - - session_store.save(request.session) + try: + yield request.session + finally: + session_store.save(request.session) diff --git a/addons/web/common/nonliterals.py b/addons/web/common/nonliterals.py index 937da06ad9e..4db333a5769 100644 --- a/addons/web/common/nonliterals.py +++ b/addons/web/common/nonliterals.py @@ -41,6 +41,9 @@ class NonLiteralEncoder(simplejson.encoder.JSONEncoder): '__eval_context': object.get_eval_context() } raise TypeError('Could not encode unknown non-literal %s' % object) + +_ALLOWED_KEYS = frozenset(['__ref', "__id", '__domains', + '__contexts', '__eval_context', 'own_values']) def non_literal_decoder(dct): """ Decodes JSON dicts into :class:`Domain` and :class:`Context` based on @@ -50,6 +53,9 @@ def non_literal_decoder(dct): ``own_values`` dict key. """ if '__ref' in dct: + for x in dct.keys(): + if not x in _ALLOWED_KEYS: + raise ValueError("'%s' key not allowed in non literal domain/context" % x) if dct['__ref'] == 'domain': domain = Domain(None, key=dct['__id']) if 'own_values' in dct: @@ -231,7 +237,10 @@ class CompoundContext(BaseContext): def evaluate(self, context=None): ctx = dict(context or {}) - ctx.update(self.get_eval_context() or {}) + eval_context = self.get_eval_context() + if eval_context: + eval_context = self.session.eval_context(eval_context) + ctx.update(eval_context) final_context = {} for context_to_eval in self.contexts: if not isinstance(context_to_eval, (dict, BaseContext)): diff --git a/addons/web/common/openerplib/main.py b/addons/web/common/openerplib/main.py index 21f8d48ff63..b09e9f9d72f 100644 --- a/addons/web/common/openerplib/main.py +++ b/addons/web/common/openerplib/main.py @@ -38,6 +38,7 @@ Code repository: https://code.launchpad.net/~niv-openerp/openerp-client-lib/trun import xmlrpclib import logging import socket +import sys try: import cPickle as pickle @@ -70,6 +71,14 @@ class Connector(object): self.hostname = hostname self.port = port + def get_service(self, service_name): + """ + Returns a Service instance to allow easy manipulation of one of the services offered by the remote server. + + :param service_name: The name of the service. + """ + return Service(self, service_name) + class XmlRPCConnector(Connector): """ A type of connector that uses the XMLRPC protocol. @@ -190,6 +199,31 @@ class NetRPCConnector(Connector): socket.disconnect() return result +class LocalConnector(Connector): + """ + A type of connector that uses the XMLRPC protocol. + """ + PROTOCOL = 'local' + + __logger = _getChildLogger(_logger, 'connector.local') + + def __init__(self): + pass + + def send(self, service_name, method, *args): + import openerp + # TODO Exception handling + # This will be changed to be xmlrpc compatible + # OpenERPWarning code 1 + # OpenERPException code 2 + try: + result = openerp.netsvc.dispatch_rpc(service_name, method, args) + except: + exc_type, exc_value, exc_tb = sys.exc_info() + fault = xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value)) + raise fault + return result + class Service(object): """ A class to execute RPC calls on a specific service of the remote server. @@ -295,7 +329,7 @@ class Connection(object): :param service_name: The name of the service. """ - return Service(self.connector, service_name) + return self.connector.get_service(service_name) class AuthenticationError(Exception): """ @@ -342,7 +376,7 @@ class Model(object): index = {} for r in result: index[r['id']] = r - result = [index[x] for x in args[0]] + result = [index[x] for x in args[0] if x in index] self.__logger.debug('result: %r', result) return result return proxy @@ -363,7 +397,7 @@ class Model(object): records = self.read(record_ids, fields or [], context or {}) return records -def get_connector(hostname, protocol="xmlrpc", port="auto"): +def get_connector(hostname=None, protocol="xmlrpc", port="auto"): """ A shortcut method to easily create a connector to a remote server using XMLRPC or NetRPC. @@ -377,10 +411,12 @@ def get_connector(hostname, protocol="xmlrpc", port="auto"): return XmlRPCConnector(hostname, port) elif protocol == "netrpc": return NetRPCConnector(hostname, port) + elif protocol == "local": + return LocalConnector() else: - raise ValueError("You must choose xmlrpc or netrpc") + raise ValueError("You must choose xmlrpc or netrpc or local") -def get_connection(hostname, protocol="xmlrpc", port='auto', database=None, +def get_connection(hostname=None, protocol="xmlrpc", port='auto', database=None, login=None, password=None, user_id=None): """ A shortcut method to easily create a connection to a remote OpenERP server. diff --git a/addons/web/common/session.py b/addons/web/common/session.py index d1997a3f903..2ed50bd6131 100644 --- a/addons/web/common/session.py +++ b/addons/web/common/session.py @@ -5,6 +5,9 @@ import time import openerplib import nonliterals + +import logging +_logger = logging.getLogger(__name__) #---------------------------------------------------------- # OpenERPSession RPC openerp backend access #---------------------------------------------------------- @@ -26,25 +29,26 @@ class OpenERPSession(object): Used to store references to non-literal domains which need to be round-tripped to the client browser. """ - def __init__(self, server='127.0.0.1', port=8069): - self._server = server - self._port = port + def __init__(self): + self.config = None self._db = False self._uid = False self._login = False self._password = False - self._locale = 'en_US' self.context = {} self.contexts_store = {} self.domains_store = {} - self._lang = {} - self.remote_timezone = 'utc' - self.client_timezone = False + def __getstate__(self): + state = dict(self.__dict__) + if "config" in state: + del state['config'] + return state + def build_connection(self): - return openerplib.get_connection(hostname=self._server, port=self._port, - database=self._db, login=self._login, - user_id=self._uid, password=self._password) + conn = openerplib.Connection(self.config.connector, database=self._db, login=self._login, + user_id=self._uid, password=self._password) + return conn def proxy(self, service): return self.build_connection().get_service(service) @@ -99,15 +103,6 @@ class OpenERPSession(object): self.context = self.model('res.users').context_get(self.context) self.context = self.context or {} - self.client_timezone = self.context.get("tz", False) - # invalid code, anyway we decided the server will be in UTC - #if self.client_timezone: - # self.remote_timezone = self.execute('common', 'timezone_get') - - self._locale = self.context.get('lang','en_US') - lang_ids = self.execute('res.lang','search', [('code', '=', self._locale)]) - if lang_ids: - self._lang = self.execute('res.lang', 'read',lang_ids[0], []) return self.context @property diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index d0c8950f9e1..9be0794a8bb 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -68,15 +68,7 @@ class Xml2Json: # OpenERP Web web Controllers #---------------------------------------------------------- -def manifest_glob(addons_path, addons, key): - files = [] - for addon in addons: - globlist = openerpweb.addons_manifest.get(addon, {}).get(key, []) - for pattern in globlist: - for path in glob.glob(os.path.join(addons_path, addon, pattern)): - files.append(path[len(addons_path):]) - return files - +# TODO change into concat_file(addons,key) taking care of addons_path def concat_files(addons_path, file_list): """ Concatenate file content return (concat,timestamp) @@ -104,7 +96,7 @@ home_template = textwrap.dedent("""<!DOCTYPE html> %(javascript)s <script type="text/javascript"> $(function() { - var c = new openerp.init(); + var c = new openerp.init(%(modules)s); var wc = new c.web.WebClient("oe"); wc.start(); }); @@ -116,24 +108,45 @@ home_template = textwrap.dedent("""<!DOCTYPE html> class WebClient(openerpweb.Controller): _cp_path = "/web/webclient" + def server_wide_modules(self, req): + addons = [i for i in req.config.server_wide_modules if i in openerpweb.addons_manifest] + return addons + + def manifest_glob(self, req, addons, key): + if addons==None: + addons = self.server_wide_modules(req) + else: + addons = addons.split(',') + files = [] + for addon in addons: + manifest = openerpweb.addons_manifest.get(addon, None) + if not manifest: + continue + addons_path = manifest['addons_path'] + globlist = manifest.get(key, []) + for pattern in globlist: + for path in glob.glob(os.path.join(addons_path, addon, pattern)): + files.append(path[len(addons_path):]) + return files + @openerpweb.jsonrequest - def csslist(self, req, mods='web'): - return manifest_glob(req.config.addons_path, mods.split(','), 'css') + def csslist(self, req, mods=None): + return self.manifest_glob(req, mods, 'css') @openerpweb.jsonrequest - def jslist(self, req, mods='web'): - return manifest_glob(req.config.addons_path, mods.split(','), 'js') + def jslist(self, req, mods=None): + return self.manifest_glob(req, mods, 'js') @openerpweb.httprequest - def css(self, req, mods='web'): - files = manifest_glob(req.config.addons_path, mods.split(','), 'css') + def css(self, req, mods=None): + files = self.manifest_glob(req, mods, 'css') content,timestamp = concat_files(req.config.addons_path, files) # TODO request set the Date of last modif and Etag return req.make_response(content, [('Content-Type', 'text/css')]) @openerpweb.httprequest - def js(self, req, mods='web'): - files = manifest_glob(req.config.addons_path, mods.split(','), 'js') + def js(self, req, mods=None): + files = self.manifest_glob(req, mods, 'js') content,timestamp = concat_files(req.config.addons_path, files) # TODO request set the Date of last modif and Etag return req.make_response(content, [('Content-Type', 'application/javascript')]) @@ -143,17 +156,19 @@ class WebClient(openerpweb.Controller): # script tags jslist = ['/web/webclient/js'] if req.debug: - jslist = [i + '?debug=' + str(time.time()) for i in manifest_glob(req.config.addons_path, ['web'], 'js')] + jslist = [i + '?debug=' + str(time.time()) for i in self.manifest_glob(req, None, 'js')] js = "\n ".join(['<script type="text/javascript" src="%s"></script>'%i for i in jslist]) # css tags csslist = ['/web/webclient/css'] if req.debug: - csslist = [i + '?debug=' + str(time.time()) for i in manifest_glob(req.config.addons_path, ['web'], 'css')] + csslist = [i + '?debug=' + str(time.time()) for i in self.manifest_glob(req, None, 'css')] css = "\n ".join(['<link rel="stylesheet" href="%s">'%i for i in csslist]) + r = home_template % { 'javascript': js, - 'css': css + 'css': css, + 'modules': simplejson.dumps(self.server_wide_modules(req)), } return r @@ -166,20 +181,21 @@ class WebClient(openerpweb.Controller): "grouping", "decimal_point", "thousands_sep"]) else: lang_obj = None - + if lang.count("_") > 0: separator = "_" else: separator = "@" langs = lang.split(separator) langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)] - + transs = {} for addon_name in mods: transl = {"messages":[]} transs[addon_name] = transl for l in langs: - f_name = os.path.join(req.config.addons_path, addon_name, "po", l + ".po") + addons_path = openerpweb.addons_manifest[addon_name]['addons_path'] + f_name = os.path.join(addons_path, addon_name, "po", l + ".po") if not os.path.exists(f_name): continue try: @@ -240,7 +256,7 @@ class Database(openerpweb.Controller): password, db = operator.itemgetter( 'drop_pwd', 'drop_db')( dict(map(operator.itemgetter('name', 'value'), fields))) - + try: return req.session.proxy("db").drop(password, db) except xmlrpclib.Fault, e: @@ -291,7 +307,7 @@ class Session(openerpweb.Controller): @openerpweb.jsonrequest def login(self, req, db, login, password): req.session.login(db, login, password) - ctx = req.session.get_context() + ctx = req.session.get_context() if req.session._uid else {} return { "session_id": req.session_id, @@ -299,6 +315,7 @@ class Session(openerpweb.Controller): "context": ctx, "db": req.session._db } + @openerpweb.jsonrequest def get_session_info(self, req): req.session.assert_valid(force=True) @@ -307,6 +324,7 @@ class Session(openerpweb.Controller): "context": req.session.get_context() if req.session._uid else False, "db": req.session._db } + @openerpweb.jsonrequest def change_password (self,req,fields): old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')( @@ -322,6 +340,7 @@ class Session(openerpweb.Controller): except: return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'} return {'error': 'Error, password not changed !', 'title': 'Change Password'} + @openerpweb.jsonrequest def sc_list(self, req): return req.session.model('ir.ui.view_sc').get_sc( @@ -336,13 +355,14 @@ class Session(openerpweb.Controller): } except Exception, e: return {"error": e, "title": "Languages"} - + @openerpweb.jsonrequest def modules(self, req): # TODO query server for installed web modules mods = [] for name, manifest in openerpweb.addons_manifest.items(): - if name != 'web' and manifest.get('active', True): + # TODO replace by ir.module.module installed web + if name not in req.config.server_wide_modules and manifest.get('active', True): mods.append(name) return mods @@ -472,6 +492,9 @@ def clean_action(req, action): if isinstance(action.get('domain'), basestring): action['domain'] = eval( action['domain'], eval_ctx ) or [] + if 'type' not in action: + action['type'] = 'ir.actions.act_window_close' + if action['type'] == 'ir.actions.act_window': return fix_view_modes(action) return action @@ -715,7 +738,7 @@ class DataSet(openerpweb.Controller): args[domain_id] = d if context_id and len(args) - 1 >= context_id: args[context_id] = c - + for i in xrange(len(args)): if isinstance(args[i], web.common.nonliterals.BaseContext): args[i] = req.session.eval_context(args[i]) @@ -963,7 +986,7 @@ class SearchView(View): if field.get('context'): field["context"] = self.parse_domain(field["context"], req.session) return {'fields': fields} - + @openerpweb.jsonrequest def get_filters(self, req, model): Model = req.session.model("ir.filters") @@ -972,7 +995,7 @@ class SearchView(View): filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session)) filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session)) return filters - + @openerpweb.jsonrequest def save_filter(self, req, model, name, context_to_save, domain): Model = req.session.model("ir.filters") @@ -1382,7 +1405,7 @@ class Reports(View): report_data['form'] = action['datas']['form'] if 'ids' in action['datas']: report_ids = action['datas']['ids'] - + report_id = report_srv.report( req.session._db, req.session._uid, req.session._password, action["report_name"], report_ids, @@ -1394,6 +1417,7 @@ class Reports(View): req.session._db, req.session._uid, req.session._password, report_id) if report_struct["state"]: break + time.sleep(self.POLLING_DELAY) report = base64.b64decode(report_struct['result']) @@ -1407,3 +1431,108 @@ class Reports(View): ('Content-Type', report_mimetype), ('Content-Length', len(report))], cookies={'fileToken': int(token)}) + + +class Import(View): + _cp_path = "/web/import" + + def fields_get(self, req, model): + Model = req.session.model(model) + fields = Model.fields_get(False, req.session.eval_context(req.context)) + return fields + + @openerpweb.httprequest + def detect_data(self, req, csvfile, csvsep, csvdel, csvcode, jsonp): + try: + data = list(csv.reader( + csvfile, quotechar=str(csvdel), delimiter=str(csvsep))) + except csv.Error, e: + csvfile.seek(0) + return '<script>window.top.%s(%s);</script>' % ( + jsonp, simplejson.dumps({'error': { + 'message': 'Error parsing CSV file: %s' % e, + # decodes each byte to a unicode character, which may or + # may not be printable, but decoding will succeed. + # Otherwise simplejson will try to decode the `str` using + # utf-8, which is very likely to blow up on characters out + # of the ascii range (in range [128, 256)) + 'preview': csvfile.read(200).decode('iso-8859-1')}})) + + try: + return '<script>window.top.%s(%s);</script>' % ( + jsonp, simplejson.dumps( + {'records': data[:10]}, encoding=csvcode)) + except UnicodeDecodeError: + return '<script>window.top.%s(%s);</script>' % ( + jsonp, simplejson.dumps({ + 'message': u"Failed to decode CSV file using encoding %s, " + u"try switching to a different encoding" % csvcode + })) + + @openerpweb.httprequest + def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp, + meta): + modle_obj = req.session.model(model) + skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')( + simplejson.loads(meta)) + + error = None + if not (csvdel and len(csvdel) == 1): + error = u"The CSV delimiter must be a single character" + + if not indices and fields: + error = u"You must select at least one field to import" + + if error: + return '<script>window.top.%s(%s);</script>' % ( + jsonp, simplejson.dumps({'error': {'message': error}})) + + # skip ignored records + data_record = itertools.islice( + csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)), + skip, None) + + # if only one index, itemgetter will return an atom rather than a tuple + if len(indices) == 1: mapper = lambda row: [row[indices[0]]] + else: mapper = operator.itemgetter(*indices) + + data = None + error = None + try: + # decode each data row + data = [ + [record.decode(csvcode) for record in row] + for row in itertools.imap(mapper, data_record) + # don't insert completely empty rows (can happen due to fields + # filtering in case of e.g. o2m content rows) + if any(row) + ] + except UnicodeDecodeError: + error = u"Failed to decode CSV file using encoding %s" % csvcode + except csv.Error, e: + error = u"Could not process CSV file: %s" % e + + # If the file contains nothing, + if not data: + error = u"File to import is empty" + if error: + return '<script>window.top.%s(%s);</script>' % ( + jsonp, simplejson.dumps({'error': {'message': error}})) + + try: + (code, record, message, _nope) = modle_obj.import_data( + fields, data, 'init', '', False, + req.session.eval_context(req.context)) + except xmlrpclib.Fault, e: + error = {"message": u"%s, %s" % (e.faultCode, e.faultString)} + return '<script>window.top.%s(%s);</script>' % ( + jsonp, simplejson.dumps({'error':error})) + + if code != -1: + return '<script>window.top.%s(%s);</script>' % ( + jsonp, simplejson.dumps({'success':True})) + + msg = u"Error during import: %s\n\nTrying to import record %r" % ( + message, record) + return '<script>window.top.%s(%s);</script>' % ( + jsonp, simplejson.dumps({'error': {'message':msg}})) diff --git a/addons/web/static/lib/jquery.deferred-queue/LICENSE b/addons/web/static/lib/jquery.deferred-queue/LICENSE new file mode 100644 index 00000000000..ee8cf44578c --- /dev/null +++ b/addons/web/static/lib/jquery.deferred-queue/LICENSE @@ -0,0 +1,21 @@ +Copyright 2011 Xavier Morel. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY XAVIER MOREL ``AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/addons/web/static/lib/jquery.deferred-queue/README b/addons/web/static/lib/jquery.deferred-queue/README new file mode 100644 index 00000000000..71d980199c9 --- /dev/null +++ b/addons/web/static/lib/jquery.deferred-queue/README @@ -0,0 +1,59 @@ +.. -*- restructuredtext -*- + +In jQuery 1.5, jQuery has introduced a Deferred object in order to +better handle callbacks. + +Along with Deferred, it introduced ``jQuery.when`` which allows, among +other things, for multiplexing deferreds (waiting on multiple +deferreds at the same time). + +While this is very nice if all deferreds are available at the same +point, it doesn't really work if the resolution of a deferred can +generate more deferreds, or for collections of deferreds coming from +multiple sources (which may not be aware of one another). + +Deferred.queue tries to be a solution to this. It is based on the +principle of FIFO multiple-producers multiple-consumers tasks queues, +such as Python's `queue.Queue`_. Any code with a reference to the +queue can add a promise to the queue, and the queue (which is a +promise itself) will only be resolved once all promises within are +resolved. + +Quickstart +---------- + +Deferred.queue has a very simple life cycle: it is dormant when +created, it starts working as soon as promises get added to it (via +``.push``, or by providing them directly to its constructor), and as +soon as the last promise in the queue is resolved it resolves itself. + +If any promise in the queue fails, the queue itself will be rejected +(without waiting for further promises). + +Once a queue has been resolved or rejected, adding new promises to it +results in an error. + +API +--- + +``jQuery.Deferred.queue([promises...])`` + Creates a new deferred queue. Can be primed with a series of promise + objects. + +``.push(promise...)`` + Adds promises to the queue. Returns the queue itself (can be + chained). + + If the queue has already been resolved or rejected, raises an error. + +``.then([doneCallbacks][, failCallbacks])`` + Promise/A ``then`` method. + +``.done(doneCallbacks)`` + jQuery ``done`` extension to promise objects + +``.fail(failCallbacks)`` + jQuery ``fail`` extension to promise objects + +.. _queue.Queue: + http://docs.python.org/dev/library/queue.html diff --git a/addons/web/static/lib/jquery.deferred-queue/jquery.deferred-queue.js b/addons/web/static/lib/jquery.deferred-queue/jquery.deferred-queue.js new file mode 100644 index 00000000000..7ef83342f77 --- /dev/null +++ b/addons/web/static/lib/jquery.deferred-queue/jquery.deferred-queue.js @@ -0,0 +1,34 @@ +(function ($) { + "use strict"; + $.extend($.Deferred, { + queue: function () { + var queueDeferred = $.Deferred(); + var promises = 0; + + function resolve() { + if (--promises > 0) { + return; + } + setTimeout($.proxy(queueDeferred, 'resolve'), 0); + } + + var promise = $.extend(queueDeferred.promise(), { + push: function () { + if (this.isResolved() || this.isRejected()) { + throw new Error("Can not add promises to a resolved " + + "or rejected promise queue"); + } + + promises += 1; + $.when.apply(null, arguments).then( + resolve, $.proxy(queueDeferred, 'reject')); + return this; + } + }); + if (arguments.length) { + promise.push.apply(promise, arguments); + } + return promise; + } + }); +})(jQuery) diff --git a/addons/web/static/lib/py.parse/.hg_archival.txt b/addons/web/static/lib/py.parse/.hg_archival.txt new file mode 100644 index 00000000000..43d5d80a9e4 --- /dev/null +++ b/addons/web/static/lib/py.parse/.hg_archival.txt @@ -0,0 +1,5 @@ +repo: 076b192d0d8ab2b92d1dbcfa3da055382f30eaea +node: 87fb1b67d6a13f10a1a328104ee4d4b2c36801ec +branch: default +latesttag: 0.2 +latesttagdistance: 1 diff --git a/addons/web/static/lib/py.parse/README b/addons/web/static/lib/py.parse/README new file mode 100644 index 00000000000..453f001ee15 --- /dev/null +++ b/addons/web/static/lib/py.parse/README @@ -0,0 +1 @@ +Parser and evaluator of Python expressions diff --git a/addons/web/static/lib/py.parse/TODO.rst b/addons/web/static/lib/py.parse/TODO.rst new file mode 100644 index 00000000000..3638b681ddc --- /dev/null +++ b/addons/web/static/lib/py.parse/TODO.rst @@ -0,0 +1,14 @@ +* Parser + since parsing expressions, try with a pratt parser + http://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/ + http://effbot.org/zone/simple-top-down-parsing.htm + +Evaluator +--------- + +* Stop busyworking trivial binary operator +* Make it *trivial* to build Python type-wrappers +* Implement Python's `data model + protocols<http://docs.python.org/reference/datamodel.html#basic-customization>`_ + for *all* supported operations, optimizations can come later +* Automatically type-wrap everything (for now anyway) diff --git a/addons/web/static/lib/py.parse/lib/py.js b/addons/web/static/lib/py.parse/lib/py.js new file mode 100644 index 00000000000..0f2415f1252 --- /dev/null +++ b/addons/web/static/lib/py.parse/lib/py.js @@ -0,0 +1,546 @@ +var py = {}; +(function (exports) { + var NUMBER = /^\d$/, + NAME_FIRST = /^[a-zA-Z_]$/, + NAME = /^[a-zA-Z0-9_]$/; + + var create = function (o, props) { + function F() {} + F.prototype = o; + var inst = new F; + for(var name in props) { + if(!props.hasOwnProperty(name)) { continue; } + inst[name] = props[name]; + } + return inst; + }; + + var symbols = {}; + var comparators = {}; + var Base = { + nud: function () { throw new Error(this.id + " undefined as prefix"); }, + led: function (led) { throw new Error(this.id + " undefined as infix"); }, + toString: function () { + if (this.id === '(constant)' || this.id === '(number)' || this.id === '(name)' || this.id === '(string)') { + return [this.id.slice(0, this.id.length-1), ' ', this.value, ')'].join(''); + } else if (this.id === '(end)') { + return '(end)'; + } else if (this.id === '(comparator)' ) { + var repr = ['(comparator', this.expressions[0]]; + for (var i=0;i<this.operators.length; ++i) { + repr.push(this.operators[i], this.expressions[i+1]); + } + return repr.join(' ') + ')'; + } + var out = [this.id, this.first, this.second, this.third] + .filter(function (r){return r}).join(' '); + return '(' + out + ')'; + } + }; + function symbol(id, bp) { + bp = bp || 0; + var s = symbols[id]; + if (s) { + if (bp > s.lbp) { + s.lbp = bp; + } + return s; + } + return symbols[id] = create(Base, { + id: id, + lbp: bp + }); + } + function constant(id) { + symbol(id).nud = function () { + this.id = "(constant)"; + this.value = id; + return this; + }; + } + function prefix(id, bp, nud) { + symbol(id).nud = nud || function () { + this.first = expression(bp); + return this + } + } + function infix(id, bp, led) { + symbol(id, bp).led = led || function (left) { + this.first = left; + this.second = expression(bp); + return this; + } + } + function infixr(id, bp) { + symbol(id, bp).led = function (left) { + this.first = left; + this.second = expression(bp - 1); + return this; + } + } + function comparator(id) { + comparators[id] = true; + var bp = 60; + infix(id, bp, function (left) { + this.id = '(comparator)'; + this.operators = [id]; + this.expressions = [left, expression(bp)]; + while (token.id in comparators) { + this.operators.push(token.id); + advance(); + this.expressions.push( + expression(bp)); + } + return this; + }); + } + + constant('None'); constant('False'); constant('True'); + + symbol('(number)').nud = function () { return this; }; + symbol('(name)').nud = function () { return this; }; + symbol('(string)').nud = function () { return this; }; + symbol('(end)'); + + symbol(':'); symbol(')'); symbol(']'); symbol('}'); symbol(','); + symbol('else'); + + symbol('lambda', 20).nud = function () { + this.first = []; + if (token.id !== ':') { + for(;;) { + if (token.id !== '(name)') { + throw new Error('Excepted an argument name'); + } + this.first.push(token); + advance(); + if (token.id !== ',') { + break; + } + advance(','); + } + } + advance(':'); + this.second = expression(); + return this; + }; + infix('if', 20, function (left) { + this.first = left; + this.second = expression(); + advance('else'); + this.third = expression(); + return this; + }); + + infixr('or', 30); infixr('and', 40); prefix('not', 50); + + comparator('in'); comparator('not in'); + comparator('is'); comparator('is not'); + comparator('<'); comparator('<='); + comparator('>'); comparator('>='); + comparator('<>'); comparator('!='); comparator('=='); + + infix('|', 70); infix('^', 80), infix('&', 90); + + infix('<<', 100); infix('>>', 100); + + infix('+', 110); infix('-', 110); + + infix('*', 120); infix('/', 120); + infix('//', 120), infix('%', 120); + + prefix('-', 130); prefix('+', 130); prefix('~', 130); + + infixr('**', 140); + + infix('.', 150, function (left) { + if (token.id !== '(name)') { + throw new Error('Expected attribute name, got ', token.id); + } + this.first = left; + this.second = token; + advance(); + return this; + }); + symbol('(', 150).nud = function () { + this.first = []; + var comma = false; + if (token.id !== ')') { + while (true) { + if (token.id === ')') { + break; + } + this.first.push(expression()); + if (token.id !== ',') { + break; + } + comma = true; + advance(','); + } + } + advance(')'); + if (!this.first.length || comma) { + return this; + } else { + return this.first[0]; + } + }; + symbol('(').led = function (left) { + this.first = left; + this.second = []; + if (token.id !== ")") { + for(;;) { + this.second.push(expression()); + if (token.id !== ',') { + break; + } + advance(','); + } + } + advance(")"); + return this; + + }; + infix('[', 150, function (left) { + this.first = left; + this.second = expression(); + advance("]"); + return this; + }); + symbol('[').nud = function () { + this.first = []; + if (token.id !== ']') { + for (;;) { + if (token.id === ']') { + break; + } + this.first.push(expression()); + if (token.id !== ',') { + break; + } + advance(','); + } + } + advance(']'); + return this; + }; + + symbol('{').nud = function () { + this.first = []; + if (token.id !== '}') { + for(;;) { + if (token.id === '}') { + break; + } + var key = expression(); + advance(':'); + var value = expression(); + this.first.push([key, value]); + if (token.id !== ',') { + break; + } + advance(','); + } + } + advance('}'); + return this; + }; + + var longops = { + '*': ['*'], + '<': ['<', '=', '>'], + '>': ['=', '>'], + '!': ['='], + '=': ['='], + '/': ['/'] + }; + function Tokenizer() { + this.states = ['initial']; + this.tokens = []; + } + Tokenizer.prototype = { + builder: function (empty) { + var key = this.states[0] + '_builder'; + if (empty) { + var value = this[key]; + delete this[key]; + return value; + } else { + return this[key] = this[key] || []; + } + }, + simple: function (type) { + this.tokens.push({type: type}); + }, + push: function (new_state) { + this.states.push(new_state); + }, + pop: function () { + this.states.pop(); + }, + + feed: function (str, index) { + var s = this.states; + return this[s[s.length - 1]](str, index); + }, + + initial: function (str, index) { + var character = str[index]; + + if (character in longops) { + var follow = longops[character]; + for(var i=0, len=follow.length; i<len; ++i) { + if (str[index+1] === follow[i]) { + character += follow[i]; + index++; + break; + } + } + } + + if (character === ' ') { + return index+1; + } else if (character === '\0') { + this.tokens.push(symbols['(end)']); + return index + 1 + } else if (character === '"' || character === "'") { + this.push('string'); + return index + 1; + } else if (NUMBER.test(character)) { + this.push('number'); + return index; + } else if (NAME_FIRST.test(character)) { + this.push('name'); + return index; + } else if (character in symbols) { + this.tokens.push(create(symbols[character])); + return index + 1; + } + throw new Error("Tokenizing failure of <<" + str + ">> at index " + index + + ", character [[" + character + "]]" + + "; parsed so far: " + this.tokens); + }, + string: function (str, index) { + var character = str[index]; + if (character === '"' || character === "'") { + this.tokens.push(create(symbols['(string)'], { + value: this.builder(true).join('') + })); + this.pop(); + return index + 1; + } + this.builder().push(character); + return index + 1; + }, + number: function (str, index) { + var character = str[index]; + if (!NUMBER.test(character)) { + this.tokens.push(create(symbols['(number)'], { + value: parseFloat(this.builder(true).join('')) + })); + this.pop(); + return index; + } + this.builder().push(character); + return index + 1; + }, + name: function (str, index) { + var character = str[index]; + if (!NAME.test(character)) { + var name = this.builder(true).join(''); + var symbol = symbols[name]; + if (symbol) { + if (name === 'in' && this.tokens[this.tokens.length-1].id === 'not') { + symbol = symbols['not in']; + this.tokens.pop(); + } else if (name === 'not' && this.tokens[this.tokens.length-1].id === 'is') { + symbol = symbols['is not']; + this.tokens.pop(); + } + this.tokens.push(create(symbol)); + } else { + this.tokens.push(create(symbols['(name)'], { + value: name + })); + } + this.pop(); + return index; + } + this.builder().push(character); + return index + 1; + } + }; + + exports.tokenize = function tokenize(str) { + var index = 0, + tokenizer = new Tokenizer(str); + str += '\0'; + + do { + index = tokenizer.feed(str, index); + } while (index !== str.length); + return tokenizer.tokens; + }; + + var token, next; + function expression(rbp) { + rbp = rbp || 0; + var t = token; + token = next(); + var left = t.nud(); + while (rbp < token.lbp) { + t = token; + token = next(); + left = t.led(left); + } + return left; + } + function advance(id) { + if (id && token.id !== id) { + throw new Error( + 'Expected "' + id + '", got "' + token.id + '"'); + } + token = next(); + } + + exports.object = create({}, {}); + exports.bool = function (arg) { return !!arg; }; + exports.tuple = create(exports.object, { + __contains__: function (value) { + for(var i=0, len=this.values.length; i<len; ++i) { + if (this.values[i] === value) { + return true; + } + } + return false; + }, + toJSON: function () { + return this.values; + } + }); + exports.list = exports.tuple; + exports.dict = create(exports.object, { + toJSON: function () { + return this.values; + } + }); + + exports.parse = function (toks) { + var index = 0; + token = toks[0]; + next = function () { return toks[++index]; }; + return expression(); + }; + var evaluate_operator = function (operator, a, b) { + switch (operator) { + case '==': case 'is': return a === b; + case '!=': case 'is not': return a !== b; + case '<': return a < b; + case '<=': return a <= b; + case '>': return a > b; + case '>=': return a >= b; + case 'in': + if (typeof b === 'string') { + return b.indexOf(a) !== -1; + } + return b.__contains__(a); + case 'not in': + if (typeof b === 'string') { + return b.indexOf(a) === -1; + } + return !b.__contains__(a); + } + throw new Error('SyntaxError: unknown comparator [[' + operator + ']]'); + }; + exports.evaluate = function (expr, context) { + switch (expr.id) { + case '(name)': + var val = context[expr.value]; + if (val === undefined) { + throw new Error("NameError: name '" + expr.value + "' is not defined"); + } + return val; + case '(string)': + case '(number)': + return expr.value; + case '(constant)': + if (expr.value === 'None') + return null; + else if (expr.value === 'False') + return false; + else if (expr.value === 'True') + return true; + throw new Error("SyntaxError: unknown constant '" + expr.value + "'"); + case '(comparator)': + var result, left = exports.evaluate(expr.expressions[0], context); + for(var i=0; i<expr.operators.length; ++i) { + result = evaluate_operator( + expr.operators[i], + left, + left = exports.evaluate(expr.expressions[i+1], context)); + if (!result) { return false; } + } + return true; + case '-': + if (expr.second) { + throw new Error('SyntaxError: binary [-] not implemented yet'); + } + return -(exports.evaluate(expr.first, context)); + case 'not': + return !(exports.evaluate(expr.first, context)); + case 'and': + return (exports.evaluate(expr.first, context) + && exports.evaluate(expr.second, context)); + case 'or': + return (exports.evaluate(expr.first, context) + || exports.evaluate(expr.second, context)); + case '(': + if (expr.second) { + var fn = exports.evaluate(expr.first, context), args=[]; + for (var jj=0; jj<expr.second.length; ++jj) { + args.push(exports.evaluate( + expr.second[jj], context)); + } + return fn.apply(null, args); + } + var tuple_exprs = expr.first, + tuple_values = []; + for (var j=0, len=tuple_exprs.length; j<len; ++j) { + tuple_values.push(exports.evaluate( + tuple_exprs[j], context)); + } + return create(exports.tuple, {values: tuple_values}); + case '[': + if (expr.second) { + throw new Error('SyntaxError: indexing not implemented yet'); + } + var list_exprs = expr.first, list_values = []; + for (var k=0; k<list_exprs.length; ++k) { + list_values.push(exports.evaluate( + list_exprs[k], context)); + } + return create(exports.list, {values: list_values}); + case '{': + var dict_exprs = expr.first, dict_values = {}; + for(var l=0; l<dict_exprs.length; ++l) { + dict_values[exports.evaluate(dict_exprs[l][0], context)] = + exports.evaluate(dict_exprs[l][1], context); + } + return create(exports.dict, {values: dict_values}); + case '.': + if (expr.second.id !== '(name)') { + throw new Error('SyntaxError: ' + expr); + } + return exports.evaluate(expr.first, context)[expr.second.value]; + default: + throw new Error('SyntaxError: Unknown node [[' + expr.id + ']]'); + } + }; + exports.eval = function (str, context) { + return exports.evaluate( + exports.parse( + exports.tokenize( + str)), + context); + } +})(typeof exports === 'undefined' ? py : exports); diff --git a/addons/web/static/lib/py.parse/test/test.js b/addons/web/static/lib/py.parse/test/test.js new file mode 100644 index 00000000000..0b4ddc1b623 --- /dev/null +++ b/addons/web/static/lib/py.parse/test/test.js @@ -0,0 +1,90 @@ +var py = require('../lib/py.js'), + assert = require('assert'); + +// Literals +assert.strictEqual(py.eval('1'), 1); +assert.strictEqual(py.eval('None'), null); +assert.strictEqual(py.eval('False'), false); +assert.strictEqual(py.eval('True'), true); +assert.strictEqual(py.eval('"somestring"'), 'somestring'); +assert.strictEqual(py.eval("'somestring'"), 'somestring'); +assert.deepEqual(py.eval("()").toJSON(), []); +assert.deepEqual(py.eval("[]").toJSON(), []); +assert.deepEqual(py.eval("{}").toJSON(), {}); +assert.deepEqual(py.eval("(None, True, False, 0, 1, 'foo')").toJSON(), + [null, true, false, 0, 1, 'foo']); +assert.deepEqual(py.eval("[None, True, False, 0, 1, 'foo']").toJSON(), + [null, true, false, 0, 1, 'foo']); +assert.deepEqual(py.eval("{'foo': 1, foo: 2}", {foo: 'bar'}).toJSON(), + {foo: 1, bar: 2}); + +// Equality tests +assert.ok(py.eval( + "foo == 'foo'", {foo: 'foo'})); +// Inequality +assert.ok(py.eval( + "foo != bar", {foo: 'foo', bar: 'bar'})); + +// Comparisons +assert.ok(py.eval('3 < 5')); +assert.ok(py.eval('5 >= 3')); +assert.ok(py.eval('3 >= 3')); +assert.ok(!py.eval('5 < 3')); +assert.ok(py.eval('1 < 3 < 5')); +assert.ok(py.eval('5 > 3 > 1')); +assert.ok(py.eval('1 < 3 > 2 == 2 > -2 not in (0, 1, 2)')); +// string rich comparisons +assert.ok(py.eval( + 'date >= current', {date: '2010-06-08', current: '2010-06-05'})); + +// Boolean operators +assert.ok(py.eval( + "foo == 'foo' or foo == 'bar'", {foo: 'bar'})); +assert.ok(py.eval( + "foo == 'foo' and bar == 'bar'", {foo: 'foo', bar: 'bar'})); +// - lazyness, second clauses NameError if not short-circuited +assert.ok(py.eval( + "foo == 'foo' or bar == 'bar'", {foo: 'foo'})); +assert.ok(!py.eval( + "foo == 'foo' and bar == 'bar'", {foo: 'bar'})); + +// contains (in) +assert.ok(py.eval( + "foo in ('foo', 'bar')", {foo: 'bar'})); +assert.ok(py.eval('1 in (1, 2, 3, 4)')); +assert.ok(!py.eval('1 in (2, 3, 4)')); +assert.ok(py.eval('type in ("url",)', {type: 'url'})); +assert.ok(!py.eval('type in ("url",)', {type: 'ur'})); +assert.ok(py.eval('1 not in (2, 3, 4)')); +assert.ok(py.eval('type not in ("url",)', {type: 'ur'})); + +assert.ok(py.eval( + "foo in ['foo', 'bar']", {foo: 'bar'})); +// string contains +assert.ok(py.eval('type in "view"', {type: 'view'})); +assert.ok(!py.eval('type in "view"', {type: 'bob'})); +assert.ok(py.eval('type in "url"', {type: 'ur'})); + +// Literals +assert.strictEqual(py.eval('False'), false); +assert.strictEqual(py.eval('True'), true); +assert.strictEqual(py.eval('None'), null); +assert.ok(py.eval('foo == False', {foo: false})); +assert.ok(!py.eval('foo == False', {foo: true})); + +// conversions +assert.strictEqual( + py.eval('bool(date_deadline)', {bool: py.bool, date_deadline: '2008'}), + true); + +// getattr +assert.ok(py.eval('foo.bar', {foo: {bar: true}})); +assert.ok(!py.eval('foo.bar', {foo: {bar: false}})); + +// complex expressions +assert.ok(py.eval( + "state=='pending' and not(date_deadline and (date_deadline < current_date))", + {state: 'pending', date_deadline: false})); +assert.ok(py.eval( + "state=='pending' and not(date_deadline and (date_deadline < current_date))", + {state: 'pending', date_deadline: '2010-05-08', current_date: '2010-05-08'}));; diff --git a/addons/web/static/lib/underscore/underscore.string.js b/addons/web/static/lib/underscore/underscore.string.js index 4da2b26ec1c..d15c1f1f7ce 100644 --- a/addons/web/static/lib/underscore/underscore.string.js +++ b/addons/web/static/lib/underscore/underscore.string.js @@ -4,301 +4,436 @@ // Documentation: https://github.com/edtsech/underscore.string // Some code is borrowed from MooTools and Alexandru Marasteanu. -// Version 1.1.4 +// Version 1.1.6 -(function(){ - // ------------------------- Baseline setup --------------------------------- - // Establish the root object, "window" in the browser, or "global" on the server. - var root = this; +(function(root){ + 'use strict'; - var nativeTrim = String.prototype.trim; + if (typeof _ != 'undefined') { + var _reverse = _().reverse, + _include = _.include; + } - function str_repeat(i, m) { - for (var o = []; m > 0; o[--m] = i); - return o.join(''); - } + // Defining helper functions. - function defaultToWhiteSpace(characters){ - if (characters) { - return _s.escapeRegExp(characters); - } - return '\\s'; - } + var nativeTrim = String.prototype.trim; - var _s = { + var parseNumber = function(source) { return source * 1 || 0; }; - isBlank: function(str){ - return !!str.match(/^\s*$/); - }, + var strRepeat = function(i, m) { + for (var o = []; m > 0; o[--m] = i); + return o.join(''); + }; - capitalize : function(str) { - return str.charAt(0).toUpperCase() + str.substring(1).toLowerCase(); - }, + var slice = function(a){ + return Array.prototype.slice.call(a); + }; - chop: function(str, step){ - step = step || str.length; - var arr = []; - for (var i = 0; i < str.length;) { - arr.push(str.slice(i,i + step)); - i = i + step; - } - return arr; - }, - - clean: function(str){ - return _s.strip(str.replace(/\s+/g, ' ')); - }, - - count: function(str, substr){ - var count = 0, index; - for (var i=0; i < str.length;) { - index = str.indexOf(substr, i); - index >= 0 && count++; - i = i + (index >= 0 ? index : 0) + substr.length; - } - return count; - }, - - chars: function(str) { - return str.split(''); - }, - - escapeHTML: function(str) { - return String(str||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') - .replace(/"/g, '"').replace(/'/g, "'"); - }, - - unescapeHTML: function(str) { - return String(str||'').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') - .replace(/"/g, '"').replace(/'/g, "'"); - }, - - escapeRegExp: function(str){ - // From MooTools core 1.2.4 - return String(str||'').replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1'); - }, - - insert: function(str, i, substr){ - var arr = str.split(''); - arr.splice(i, 0, substr); - return arr.join(''); - }, - - includes: function(str, needle){ - return str.indexOf(needle) !== -1; - }, - - join: function(sep) { - // TODO: Could this be faster by converting - // arguments to Array and using array.join(sep)? - sep = String(sep); - var str = ""; - for (var i=1; i < arguments.length; i += 1) { - str += String(arguments[i]); - if ( i !== arguments.length-1 ) { - str += sep; - } - } - return str; - }, - - lines: function(str) { - return str.split("\n"); - }, - -// reverse: function(str){ -// return Array.prototype.reverse.apply(str.split('')).join(''); -// }, - - splice: function(str, i, howmany, substr){ - var arr = str.split(''); - arr.splice(i, howmany, substr); - return arr.join(''); - }, - - startsWith: function(str, starts){ - return str.length >= starts.length && str.substring(0, starts.length) === starts; - }, - - endsWith: function(str, ends){ - return str.length >= ends.length && str.substring(str.length - ends.length) === ends; - }, - - succ: function(str){ - var arr = str.split(''); - arr.splice(str.length-1, 1, String.fromCharCode(str.charCodeAt(str.length-1) + 1)); - return arr.join(''); - }, - - titleize: function(str){ - var arr = str.split(' '), - word; - for (var i=0; i < arr.length; i++) { - word = arr[i].split(''); - if(typeof word[0] !== 'undefined') word[0] = word[0].toUpperCase(); - i+1 === arr.length ? arr[i] = word.join('') : arr[i] = word.join('') + ' '; - } - return arr.join(''); - }, - - camelize: function(str){ - return _s.trim(str).replace(/(\-|_|\s)+(.)?/g, function(match, separator, chr) { - return chr ? chr.toUpperCase() : ''; - }); - }, - - underscored: function(str){ - return _s.trim(str).replace(/([a-z\d])([A-Z]+)/g, '$1_$2').replace(/\-|\s+/g, '_').toLowerCase(); - }, - - dasherize: function(str){ - return _s.trim(str).replace(/([a-z\d])([A-Z]+)/g, '$1-$2').replace(/^([A-Z]+)/, '-$1').replace(/\_|\s+/g, '-').toLowerCase(); - }, - - trim: function(str, characters){ - if (!characters && nativeTrim) { - return nativeTrim.call(str); - } - characters = defaultToWhiteSpace(characters); - return str.replace(new RegExp('\^[' + characters + ']+|[' + characters + ']+$', 'g'), ''); - }, - - ltrim: function(str, characters){ - characters = defaultToWhiteSpace(characters); - return str.replace(new RegExp('\^[' + characters + ']+', 'g'), ''); - }, - - rtrim: function(str, characters){ - characters = defaultToWhiteSpace(characters); - return str.replace(new RegExp('[' + characters + ']+$', 'g'), ''); - }, - - truncate: function(str, length, truncateStr){ - truncateStr = truncateStr || '...'; - return str.slice(0,length) + truncateStr; - }, - - words: function(str, delimiter) { - delimiter = delimiter || " "; - return str.split(delimiter); - }, - - - pad: function(str, length, padStr, type) { - - var padding = ''; - var padlen = 0; - - if (!padStr) { padStr = ' '; } - else if (padStr.length > 1) { padStr = padStr[0]; } - switch(type) { - case "right": - padlen = (length - str.length); - padding = str_repeat(padStr, padlen); - str = str+padding; - break; - case "both": - padlen = (length - str.length); - padding = { - 'left' : str_repeat(padStr, Math.ceil(padlen/2)), - 'right': str_repeat(padStr, Math.floor(padlen/2)) - }; - str = padding.left+str+padding.right; - break; - default: // "left" - padlen = (length - str.length); - padding = str_repeat(padStr, padlen);; - str = padding+str; + var defaultToWhiteSpace = function(characters){ + if (characters) { + return _s.escapeRegExp(characters); + } + return '\\s'; + }; + + var sArgs = function(method){ + return function(){ + var args = slice(arguments); + for(var i=0; i<args.length; i++) + args[i] = args[i] == null ? '' : '' + args[i]; + return method.apply(null, args); + }; + }; + + // sprintf() for JavaScript 0.7-beta1 + // http://www.diveintojavascript.com/projects/javascript-sprintf + // + // Copyright (c) Alexandru Marasteanu <alexaholic [at) gmail (dot] com> + // All rights reserved. + + var sprintf = (function() { + function get_type(variable) { + return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase(); + } + + var str_repeat = strRepeat; + + var str_format = function() { + if (!str_format.cache.hasOwnProperty(arguments[0])) { + str_format.cache[arguments[0]] = str_format.parse(arguments[0]); + } + return str_format.format.call(null, str_format.cache[arguments[0]], arguments); + }; + + str_format.format = function(parse_tree, argv) { + var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length; + for (i = 0; i < tree_length; i++) { + node_type = get_type(parse_tree[i]); + if (node_type === 'string') { + output.push(parse_tree[i]); + } + else if (node_type === 'array') { + match = parse_tree[i]; // convenience purposes only + if (match[2]) { // keyword argument + arg = argv[cursor]; + for (k = 0; k < match[2].length; k++) { + if (!arg.hasOwnProperty(match[2][k])) { + throw(sprintf('[_.sprintf] property "%s" does not exist', match[2][k])); + } + arg = arg[match[2][k]]; } - return str; - }, - - lpad: function(str, length, padStr) { - return _s.pad(str, length, padStr); - }, - - rpad: function(str, length, padStr) { - return _s.pad(str, length, padStr, 'right'); - }, - - lrpad: function(str, length, padStr) { - return _s.pad(str, length, padStr, 'both'); - }, - - - /** - * Credits for this function goes to - * http://www.diveintojavascript.com/projects/sprintf-for-javascript - * - * Copyright (c) Alexandru Marasteanu <alexaholic [at) gmail (dot] com> - * All rights reserved. - * */ - sprintf: function(){ - - var i = 0, a, f = arguments[i++], o = [], m, p, c, x, s = ''; - while (f) { - if (m = /^[^\x25]+/.exec(f)) { - o.push(m[0]); - } - else if (m = /^\x25{2}/.exec(f)) { - o.push('%'); + } else if (match[1]) { // positional argument (explicit) + arg = argv[match[1]]; + } + else { // positional argument (implicit) + arg = argv[cursor++]; + } + + if (/[^s]/.test(match[8]) && (get_type(arg) != 'number')) { + throw(sprintf('[_.sprintf] expecting number but found %s', get_type(arg))); + } + switch (match[8]) { + case 'b': arg = arg.toString(2); break; + case 'c': arg = String.fromCharCode(arg); break; + case 'd': arg = parseInt(arg, 10); break; + case 'e': arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential(); break; + case 'f': arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg); break; + case 'o': arg = arg.toString(8); break; + case 's': arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg); break; + case 'u': arg = Math.abs(arg); break; + case 'x': arg = arg.toString(16); break; + case 'X': arg = arg.toString(16).toUpperCase(); break; + } + arg = (/[def]/.test(match[8]) && match[3] && arg >= 0 ? '+'+ arg : arg); + pad_character = match[4] ? match[4] == '0' ? '0' : match[4].charAt(1) : ' '; + pad_length = match[6] - String(arg).length; + pad = match[6] ? str_repeat(pad_character, pad_length) : ''; + output.push(match[5] ? arg + pad : pad + arg); + } + } + return output.join(''); + }; + + str_format.cache = {}; + + str_format.parse = function(fmt) { + var _fmt = fmt, match = [], parse_tree = [], arg_names = 0; + while (_fmt) { + if ((match = /^[^\x25]+/.exec(_fmt)) !== null) { + parse_tree.push(match[0]); + } + else if ((match = /^\x25{2}/.exec(_fmt)) !== null) { + parse_tree.push('%'); + } + else if ((match = /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(_fmt)) !== null) { + if (match[2]) { + arg_names |= 1; + var field_list = [], replacement_field = match[2], field_match = []; + if ((field_match = /^([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) { + field_list.push(field_match[1]); + while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') { + if ((field_match = /^\.([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) { + field_list.push(field_match[1]); } - else if (m = /^\x25(?:(\d+)\$)?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(f)) { - if (((a = arguments[m[1] || i++]) == null) || (a == undefined)) { - throw('Too few arguments.'); - } - if (/[^s]/.test(m[7]) && (typeof(a) != 'number')) { - throw('Expecting number but found ' + typeof(a)); - } - switch (m[7]) { - case 'b': a = a.toString(2); break; - case 'c': a = String.fromCharCode(a); break; - case 'd': a = parseInt(a); break; - case 'e': a = m[6] ? a.toExponential(m[6]) : a.toExponential(); break; - case 'f': a = m[6] ? parseFloat(a).toFixed(m[6]) : parseFloat(a); break; - case 'o': a = a.toString(8); break; - case 's': a = ((a = String(a)) && m[6] ? a.substring(0, m[6]) : a); break; - case 'u': a = Math.abs(a); break; - case 'x': a = a.toString(16); break; - case 'X': a = a.toString(16).toUpperCase(); break; - } - a = (/[def]/.test(m[7]) && m[2] && a >= 0 ? '+'+ a : a); - c = m[3] ? m[3] == '0' ? '0' : m[3].charAt(1) : ' '; - x = m[5] - String(a).length - s.length; - p = m[5] ? str_repeat(c, x) : ''; - o.push(s + (m[4] ? a + p : p + a)); + else if ((field_match = /^\[(\d+)\]/.exec(replacement_field)) !== null) { + field_list.push(field_match[1]); } else { - throw('Huh ?!'); + throw('[_.sprintf] huh?'); } - f = f.substring(m[0].length); + } } - return o.join(''); + else { + throw('[_.sprintf] huh?'); + } + match[2] = field_list; + } + else { + arg_names |= 2; + } + if (arg_names === 3) { + throw('[_.sprintf] mixing positional and named placeholders is not (yet) supported'); + } + parse_tree.push(match); } - } - - // Aliases - - _s.strip = _s.trim; - _s.lstrip = _s.ltrim; - _s.rstrip = _s.rtrim; - _s.center = _s.lrpad - _s.ljust = _s.lpad - _s.rjust = _s.rpad - - // CommonJS module is defined - if (typeof window === 'undefined' && typeof module !== 'undefined') { - // Export module - module.exports = _s; - - // Integrate with Underscore.js - } else if (typeof root._ !== 'undefined') { - root._.mixin(_s); - - // Or define it - } else { - root._ = _s; - } - -}()); + else { + throw('[_.sprintf] huh?'); + } + _fmt = _fmt.substring(match[0].length); + } + return parse_tree; + }; + + return str_format; + })(); + + + + // Defining underscore.string + + var _s = { + + isBlank: sArgs(function(str){ + return (/^\s*$/).test(str); + }), + + stripTags: sArgs(function(str){ + return str.replace(/<\/?[^>]+>/ig, ''); + }), + + capitalize : sArgs(function(str) { + return str.charAt(0).toUpperCase() + str.substring(1).toLowerCase(); + }), + + chop: sArgs(function(str, step){ + step = parseNumber(step) || str.length; + var arr = []; + for (var i = 0; i < str.length;) { + arr.push(str.slice(i,i + step)); + i = i + step; + } + return arr; + }), + + clean: sArgs(function(str){ + return _s.strip(str.replace(/\s+/g, ' ')); + }), + + count: sArgs(function(str, substr){ + var count = 0, index; + for (var i=0; i < str.length;) { + index = str.indexOf(substr, i); + index >= 0 && count++; + i = i + (index >= 0 ? index : 0) + substr.length; + } + return count; + }), + + chars: sArgs(function(str) { + return str.split(''); + }), + + escapeHTML: sArgs(function(str) { + return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') + .replace(/"/g, '"').replace(/'/g, "'"); + }), + + unescapeHTML: sArgs(function(str) { + return str.replace(/</g, '<').replace(/>/g, '>') + .replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, '&'); + }), + + escapeRegExp: sArgs(function(str){ + // From MooTools core 1.2.4 + return str.replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1'); + }), + + insert: sArgs(function(str, i, substr){ + var arr = str.split(''); + arr.splice(parseNumber(i), 0, substr); + return arr.join(''); + }), + + includes: sArgs(function(str, needle){ + return str.indexOf(needle) !== -1; + }), + + include: function(obj, needle) { + if (!_include || (/string|number/).test(typeof obj)) { + return this.includes(obj, needle); + } else { + return _include(obj, needle); + } + }, + + join: sArgs(function(sep) { + var args = slice(arguments); + return args.join(args.shift()); + }), + + lines: sArgs(function(str) { + return str.split("\n"); + }), + + reverse: function(obj){ + if (!_reverse || (/string|number/).test(typeof obj)) { + return Array.prototype.reverse.apply(String(obj).split('')).join(''); + } else { + return _reverse.call(_(obj)); + } + }, + + splice: sArgs(function(str, i, howmany, substr){ + var arr = str.split(''); + arr.splice(parseNumber(i), parseNumber(howmany), substr); + return arr.join(''); + }), + + startsWith: sArgs(function(str, starts){ + return str.length >= starts.length && str.substring(0, starts.length) === starts; + }), + + endsWith: sArgs(function(str, ends){ + return str.length >= ends.length && str.substring(str.length - ends.length) === ends; + }), + + succ: sArgs(function(str){ + var arr = str.split(''); + arr.splice(str.length-1, 1, String.fromCharCode(str.charCodeAt(str.length-1) + 1)); + return arr.join(''); + }), + + titleize: sArgs(function(str){ + var arr = str.split(' '), + word; + for (var i=0; i < arr.length; i++) { + word = arr[i].split(''); + if(typeof word[0] !== 'undefined') word[0] = word[0].toUpperCase(); + i+1 === arr.length ? arr[i] = word.join('') : arr[i] = word.join('') + ' '; + } + return arr.join(''); + }), + + camelize: sArgs(function(str){ + return _s.trim(str).replace(/(\-|_|\s)+(.)?/g, function(match, separator, chr) { + return chr ? chr.toUpperCase() : ''; + }); + }), + + underscored: function(str){ + return _s.trim(str).replace(/([a-z\d])([A-Z]+)/g, '$1_$2').replace(/\-|\s+/g, '_').toLowerCase(); + }, + + dasherize: function(str){ + return _s.trim(str).replace(/([a-z\d])([A-Z]+)/g, '$1-$2').replace(/^([A-Z]+)/, '-$1').replace(/\_|\s+/g, '-').toLowerCase(); + }, + + trim: sArgs(function(str, characters){ + if (!characters && nativeTrim) { + return nativeTrim.call(str); + } + characters = defaultToWhiteSpace(characters); + return str.replace(new RegExp('\^[' + characters + ']+|[' + characters + ']+$', 'g'), ''); + }), + + ltrim: sArgs(function(str, characters){ + characters = defaultToWhiteSpace(characters); + return str.replace(new RegExp('\^[' + characters + ']+', 'g'), ''); + }), + + rtrim: sArgs(function(str, characters){ + characters = defaultToWhiteSpace(characters); + return str.replace(new RegExp('[' + characters + ']+$', 'g'), ''); + }), + + truncate: sArgs(function(str, length, truncateStr){ + truncateStr = truncateStr || '...'; + length = parseNumber(length); + return str.length > length ? str.slice(0,length) + truncateStr : str; + }), + + words: function(str, delimiter) { + return String(str).split(delimiter || " "); + }, + + pad: sArgs(function(str, length, padStr, type) { + var padding = '', + padlen = 0; + + length = parseNumber(length); + + if (!padStr) { padStr = ' '; } + else if (padStr.length > 1) { padStr = padStr.charAt(0); } + switch(type) { + case 'right': + padlen = (length - str.length); + padding = strRepeat(padStr, padlen); + str = str+padding; + break; + case 'both': + padlen = (length - str.length); + padding = { + 'left' : strRepeat(padStr, Math.ceil(padlen/2)), + 'right': strRepeat(padStr, Math.floor(padlen/2)) + }; + str = padding.left+str+padding.right; + break; + default: // 'left' + padlen = (length - str.length); + padding = strRepeat(padStr, padlen);; + str = padding+str; + } + return str; + }), + + lpad: function(str, length, padStr) { + return _s.pad(str, length, padStr); + }, + + rpad: function(str, length, padStr) { + return _s.pad(str, length, padStr, 'right'); + }, + + lrpad: function(str, length, padStr) { + return _s.pad(str, length, padStr, 'both'); + }, + + sprintf: sprintf, + + vsprintf: function(fmt, argv){ + argv.unshift(fmt); + return sprintf.apply(null, argv); + }, + + toNumber: function(str, decimals) { + var num = parseNumber(parseNumber(str).toFixed(parseNumber(decimals))); + return (!(num === 0 && (str !== "0" && str !== 0))) ? num : Number.NaN; + }, + + strRight: sArgs(function(sourceStr, sep){ + var pos = (!sep) ? -1 : sourceStr.indexOf(sep); + return (pos != -1) ? sourceStr.slice(pos+sep.length, sourceStr.length) : sourceStr; + }), + + strRightBack: sArgs(function(sourceStr, sep){ + var pos = (!sep) ? -1 : sourceStr.lastIndexOf(sep); + return (pos != -1) ? sourceStr.slice(pos+sep.length, sourceStr.length) : sourceStr; + }), + + strLeft: sArgs(function(sourceStr, sep){ + var pos = (!sep) ? -1 : sourceStr.indexOf(sep); + return (pos != -1) ? sourceStr.slice(0, pos) : sourceStr; + }), + + strLeftBack: sArgs(function(sourceStr, sep){ + var pos = sourceStr.lastIndexOf(sep); + return (pos != -1) ? sourceStr.slice(0, pos) : sourceStr; + }) + + }; + + // Aliases + + _s.strip = _s.trim; + _s.lstrip = _s.ltrim; + _s.rstrip = _s.rtrim; + _s.center = _s.lrpad; + _s.ljust = _s.lpad; + _s.rjust = _s.rpad; + + // CommonJS module is defined + if (typeof module !== 'undefined' && module.exports) { + // Export module + module.exports = _s; + + // Integrate with Underscore.js + } else if (typeof root._ !== 'undefined') { + root._.mixin(_s); + + // Or define it + } else { + root._ = _s; + } + +}(this || window)); diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index 5d4da8175dd..7f5b7f5f41b 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -125,7 +125,7 @@ body.openerp, .openerp textarea, .openerp input, .openerp select, .openerp optio margin-top: 5px; text-align: center; } -.openerp .login.login_invalid .login_error_message { +.openerp .login .login_invalid .login_error_message { display: block; } @@ -221,17 +221,11 @@ label.error { height: 100%; background: #f0eeee; } - -/* Menu */ -.openerp .sf-menu { - margin-bottom: 0; -} -/* -.sf-menu a { - padding: 5px 5px; +.openerp .oe-application-container { + height: 100%; } -*/ +/* Menu */ .openerp .menu { height: 34px; background: #cc4e45; /* Old browsers */ @@ -253,7 +247,7 @@ label.error { height: 20px; margin: 3px 2px; padding: 0 8px; - + background: #bd5e54; /* Old browsers */ background: -moz-linear-gradient(top, #bd5e54 0%, #90322a 60%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#bd5e54), color-stop(60%,#90322a)); /* Chrome,Safari4+ */ @@ -291,13 +285,38 @@ label.error { background: linear-gradient(top, #c6c6c6 0%,#5c5c5c 7%,#969595 86%); /* W3C */ /* for ie */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#5c5c5c', endColorstr='#969595',GradientType=0 ); /* IE6-9 */ - color: #fff; } -.openerp .oe-application-container { - height: 100%; -} /* Secondary Menu */ +.openerp .secondary_menu .oe_toggle_secondary_menu { + position: absolute; + cursor: pointer; + border-left: 1px solid #282828; + width: 21px; + height: 21px; + z-index: 10; + background: transparent; + color: white; + text-shadow: 0 1px 0 #333; + text-align: center; + font-size: 18px; + line-height: 14px; + right: 0; +} +.openerp .secondary_menu.oe_folded .oe_toggle_secondary_menu { + position: static; + border-left: none; + border-bottom: 1px solid #282828; + width: 21px; + height: 21px; + background: #818181; +} +.openerp .secondary_menu.oe_folded .oe_toggle_secondary_menu span.oe_menu_fold { + display: none; +} +.openerp .secondary_menu.oe_unfolded .oe_toggle_secondary_menu span.oe_menu_unfold { + display: none; +} .openerp .secondary_menu { width: 200px; min-width: 200px; @@ -306,15 +325,34 @@ label.error { background: #5A5858; vertical-align: top; height: 100%; + position: relative; } -.openerp .secondary_menu .menu_content { - padding: 0; - border: none; - background: none; - overflow: hidden; +.openerp .secondary_menu.oe_folded { + width: 20px; + min-width: 20px; + position: static; +} +.openerp .secondary_menu.oe_folded .oe_secondary_menu.active { + position: absolute; + z-index: 100; + border: 4px solid #585858; + border: 4px solid rgba(88, 88, 88, .5); + border-radius: 4px; + min-width: 200px; } -.openerp .secondary_menu h3 { - padding: 0 0 2px; +.openerp .secondary_menu a { + display: block; + padding: 0 5px 2px 5px; + line-height: 20px; + text-decoration: none; + white-space: nowrap; + color: white; + text-shadow: 0 1px 0 #333; +} +.openerp .oe_secondary_submenu { + background: #5A5858; +} +.openerp .secondary_menu a.oe_secondary_menu_item { background: #949292; /* Old browsers */ background: -moz-linear-gradient(top, #949292 0%, #6d6b6b 87%, #282828 99%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#949292), color-stop(87%,#6d6b6b), color-stop(99%,#282828)); /* Chrome,Safari4+ */ @@ -325,34 +363,13 @@ label.error { background: linear-gradient(top, #949292 0%,#6d6b6b 87%,#282828 99%); /* W3C */ /* for ie9 */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#949292', endColorstr='#5B5A5A',GradientType=0 ); /* IE6-9 */ - border: none; - /* overriding jquery ui */ - -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; -} -.openerp .secondary_menu h4 { - padding: 0 0 2px 10px; - border: none; - background: none; -} -.openerp .secondary_menu h3 span, .openerp .secondary_menu h4 span { - left: 0 !important; -} -.openerp .secondary_menu a { - display: block; - height: 20px; - padding: 0 5px; - line-height: 20px; white-space: nowrap; color: white; - text-decoration: none; text-shadow: 0 1px 0 #333; } -.openerp .secondary_menu a.leaf:hover, -.openerp .secondary_menu a.leaf:active, -.openerp .secondary_menu a.leaf.active, -.openerp .secondary_menu h4:hover, -.openerp .secondary_menu h4:active, -.openerp .secondary_menu h4.active { +.openerp a.oe_secondary_submenu_item:hover, +.openerp a.oe_secondary_submenu_item.leaf.active { + display: block; background: #ffffff; /* Old browsers */ background: -moz-linear-gradient(top, #ffffff 0%, #d8d8d8 11%, #afafaf 86%, #333333 91%, #5a5858 96%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(11%,#d8d8d8), color-stop(86%,#afafaf), color-stop(91%,#333333), color-stop(96%,#5a5858)); /* Chrome,Safari4+ */ @@ -361,22 +378,17 @@ label.error { background: -ms-linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%,#5a5858 96%); /* IE10+ */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#FFFFFF', endColorstr='#5A5858',GradientType=0 ); /* IE6-9 */ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%,#5a5858 96%); /* W3C */ - /* overriding jquery ui */ - -moz-border-radius-topright: 0; -webkit-border-top-right-radius: 0; border-top-right-radius: 0; + padding: 0 5px 2px 5px; + line-height: 20px; color: #3f3d3d; + text-decoration: none; text-shadow: #fff 0 1px 0; - border: none !important; } - -.openerp .secondary_menu h4:hover a, -.openerp .secondary_menu h4:active a, -.openerp .secondary_menu h4.active a { - color: #3f3d3d; - text-shadow: #fff 0 1px 0; - border: none !important; +.openerp a.oe_secondary_submenu_item.submenu.opened span:before { + content: "\25be"; } -.openerp div.submenu_accordion div.menu_content a span { - padding-left: 20px; +.openerp a.oe_secondary_submenu_item.submenu span:before { + content: "\25b8"; } /* Header */ @@ -880,6 +892,9 @@ label.error { .openerp .oe_forms input.field_datetime { min-width: 11em; } +.openerp .oe_forms.oe_frame .oe_datepicker_root { + width: 100%; +} .openerp .oe_forms .button { color: #4c4c4c; white-space: nowrap; @@ -892,7 +907,14 @@ label.error { position: absolute; cursor: pointer; right: 5px; - top: 5px; + top: 3px; +} +.openerp .oe_datepicker_root { + position: relative; + display: inline-block; +} +.openerp .oe_datepicker_root input[type="text"] { + min-width: 160px; } .openerp .oe_input_icon_disabled { position: absolute; diff --git a/addons/web/static/src/css/data_import.css b/addons/web/static/src/css/data_import.css new file mode 100644 index 00000000000..1fb3b22aa45 --- /dev/null +++ b/addons/web/static/src/css/data_import.css @@ -0,0 +1,30 @@ +.openerp .oe_import_grid { + border: none; + border-collapse: collapse; +} +.openerp .oe_import_grid-header .oe_import_grid-cell { + background: url(../img/gradientlinebg.gif) repeat-x #CCCCCC; + border-bottom: 1px solid #E3E3E3; + font-weight: bold; + text-align: left; +} +.openerp .oe_import_grid-row .oe_import_grid-cell { + border-bottom: 1px solid #E3E3E3; +} +.openerp .separator.horizontal { + font-weight: bold; + border-bottom-width: 1px; + margin: 6px 4px 6px 1px; + height: 20px; +} +.openerp .duplicate_fld{ + background-color:#FF6666; +} +.openerp .select_fld{ + background: none repeat scroll 0 0 white; +} +.openerp .ui-autocomplete { + max-height: 300px; + overflow-y: auto; + padding-right: 20px; +} diff --git a/addons/web/static/src/img/gradientlinebg.gif b/addons/web/static/src/img/gradientlinebg.gif Binary files differnew file mode 100644 index 00000000000..f40bd9c0f6a --- /dev/null +++ b/addons/web/static/src/img/gradientlinebg.gif diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js index 38f8eaa1e7c..f951d160866 100644 --- a/addons/web/static/src/js/boot.js +++ b/addons/web/static/src/js/boot.js @@ -19,9 +19,9 @@ /** * OpenERP instance constructor * - * @param {Boolean} skip_init if true, skips the built-in initialization + * @param {Array} modules list of modules to initialize */ - init: function(skip_init) { + init: function(modules) { var new_instance = { // links to the global openerp _openerp: openerp, @@ -35,8 +35,9 @@ web_mobile: {} }; openerp.sessions[new_instance._session_id] = new_instance; - if (!skip_init){ - openerp.web(new_instance); + modules = modules || ["web"]; + for(var i=0; i < modules.length; i++) { + openerp[modules[i]](new_instance); } return new_instance; } diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index d114f485c62..1955298ba27 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -1,7 +1,6 @@ /*--------------------------------------------------------- * OpenERP Web chrome *---------------------------------------------------------*/ - openerp.web.chrome = function(openerp) { var QWeb = openerp.web.qweb; @@ -129,6 +128,7 @@ openerp.web.Dialog = openerp.web.OldWidget.extend(/** @lends openerp.web.Dialog# // Destroy widget this.close(); this.$dialog.dialog('destroy'); + this._super(); } }); @@ -218,9 +218,8 @@ openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database this.$element.closest(".openerp") .removeClass("login-mode") .addClass("database_block"); - + var self = this; - var fetch_db = this.rpc("/web/database/get_list", {}, function(result) { self.db_list = result.db_list; }); @@ -232,7 +231,7 @@ openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database self.lang_list = result.lang_list; }); $.when(fetch_db, fetch_langs).then(function () {self.do_create();}); - + this.$element.find('#db-create').click(this.do_create); this.$element.find('#db-drop').click(this.do_drop); this.$element.find('#db-backup').click(this.do_backup); @@ -254,7 +253,7 @@ openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database .removeClass("database_block") .end() .empty(); - + this._super(); }, /** * Converts a .serializeArray() result into a dict. Does not bother folding @@ -300,9 +299,9 @@ openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database var admin = result[1][0]; setTimeout(function () { - self.stop(); self.widget_parent.do_login( info.db, admin.login, admin.password); + self.stop(); $.unblockUI(); }); }); @@ -399,7 +398,7 @@ openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database do_restore: function() { var self = this; self.$option_id.html(QWeb.render("RestoreDB", self)); - + self.$option_id.find("form[name=restore_db_form]").validate({ submitHandler: function (form) { $.blockUI({message:'<img src="/web/static/src/img/throbber2.gif">'}); @@ -464,15 +463,19 @@ openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database openerp.web.Login = openerp.web.Widget.extend(/** @lends openerp.web.Login# */{ remember_creditentials: true, + + template: "Login", + identifier_prefix: 'oe-app-login-', /** * @constructs openerp.web.Login * @extends openerp.web.Widget - * + * * @param parent * @param element_id */ - init: function(parent, element_id) { - this._super(parent, element_id); + + init: function(parent) { + this._super(parent); this.has_local_storage = typeof(localStorage) != 'undefined'; this.selected_db = null; this.selected_login = null; @@ -487,17 +490,6 @@ openerp.web.Login = openerp.web.Widget.extend(/** @lends openerp.web.Login# */{ }, start: function() { var self = this; - this.rpc("/web/database/get_list", {}, function(result) { - self.db_list = result.db_list; - self.display(); - }, function() { - self.display(); - }); - }, - display: function() { - var self = this; - - this.$element.html(QWeb.render("Login", this)); this.database = new openerp.web.Database( this, "oe_database", "oe_db_options"); @@ -506,6 +498,17 @@ openerp.web.Login = openerp.web.Widget.extend(/** @lends openerp.web.Login# */{ }); this.$element.find("form").submit(this.on_submit); + + this.rpc("/web/database/get_list", {}, function(result) { + var tpl = openerp.web.qweb.render('Login_dblist', {db_list: result.db_list, selected_db: self.selected_db}); + self.$element.find("input[name=db]").replaceWith(tpl) + }, + function(error, event) { + if (error.data.fault_code === 'AccessDenied') { + event.preventDefault(); + } + }); + }, on_login_invalid: function() { this.$element.closest(".openerp").addClass("login-mode"); @@ -574,14 +577,13 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# * /** * @constructs openerp.web.Header * @extends openerp.web.Widget - * + * * @param parent */ init: function(parent) { this._super(parent); this.qs = "?" + jQuery.param.querystring(); this.$content = $(); - console.debug("initializing header with id", this.element_id); this.update_promise = $.Deferred().resolve(); }, start: function() { @@ -666,7 +668,7 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# * }); }); }, - + on_action: function(action) { }, on_preferences: function(){ @@ -702,10 +704,11 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# * }, Save: function(){ var inner_viewmanager = action_manager.inner_viewmanager; - inner_viewmanager.views[inner_viewmanager.active_view].controller.do_save(function(){ - inner_viewmanager.start(); + inner_viewmanager.views[inner_viewmanager.active_view].controller.do_save() + .then(function() { + self.dialog.stop(); + window.location.reload(); }); - $(this).dialog('destroy') } } }); @@ -713,7 +716,7 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# * action_manager.appendTo(this.dialog); action_manager.render(this.dialog); }, - + change_password :function() { var self = this; this.dialog = new openerp.web.Dialog(this,{ @@ -728,21 +731,13 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# * submitHandler: function (form) { self.rpc("/web/session/change_password",{ 'fields': $(form).serializeArray() - }, function(result) { - if (result.error) { - self.display_error(result); + }, function(result) { + if (result.error) { + self.display_error(result); return; - } - else { - if (result.new_password) { - self.session.password = result.new_password; - var session = new openerp.web.Session(self.session.server, self.session.port); - session.start(); - session.session_login(self.session.db, self.session.login, self.session.password) - } - } - self.notification.notify("Changed Password", "Password has been changed successfully"); - self.dialog.close(); + } else { + self.session.logout(); + } }); } }); @@ -766,7 +761,7 @@ openerp.web.Menu = openerp.web.Widget.extend(/** @lends openerp.web.Menu# */{ /** * @constructs openerp.web.Menu * @extends openerp.web.Widget - * + * * @param parent * @param element_id * @param secondary_menu_id @@ -776,74 +771,132 @@ openerp.web.Menu = openerp.web.Widget.extend(/** @lends openerp.web.Menu# */{ this.secondary_menu_id = secondary_menu_id; this.$secondary_menu = $("#" + secondary_menu_id).hide(); this.menu = false; + this.folded = false; + if (window.localStorage) { + this.folded = localStorage.getItem('oe_menu_folded') === 'true'; + } + this.float_timeout = 700; }, start: function() { + this.$secondary_menu.addClass(this.folded ? 'oe_folded' : 'oe_unfolded'); + this.reload(); + }, + reload: function() { this.rpc("/web/menu/load", {}, this.on_loaded); }, on_loaded: function(data) { this.data = data; - this.$element.html(QWeb.render("Menu", this.data)); - for (var i = 0; i < this.data.data.children.length; i++) { - var v = { menu : this.data.data.children[i] }; - this.$secondary_menu.append(QWeb.render("Menu.secondary", v)); - } - this.$secondary_menu.find("div.menu_accordion").accordion({ - animated : false, - autoHeight : false, - icons : false - }); - this.$secondary_menu.find("div.submenu_accordion").accordion({ - animated : false, - autoHeight : false, - active: false, - collapsible: true, - header: 'h4' - }); - + this.$element.html(QWeb.render("Menu", { widget : this })); + this.$secondary_menu.html(QWeb.render("Menu.secondary", { widget : this })); this.$element.add(this.$secondary_menu).find("a").click(this.on_menu_click); + this.$secondary_menu.find('.oe_toggle_secondary_menu').click(this.on_toggle_fold); + }, + on_toggle_fold: function() { + this.$secondary_menu.toggleClass('oe_folded').toggleClass('oe_unfolded'); + if (this.folded) { + this.$secondary_menu.find('.oe_secondary_menu.active').show(); + } else { + this.$secondary_menu.find('.oe_secondary_menu').hide(); + } + this.folded = !this.folded; + if (window.localStorage) { + localStorage.setItem('oe_menu_folded', this.folded.toString()); + } }, on_menu_click: function(ev, id) { id = id || 0; - var $menu, $parent, $secondary; + var $clicked_menu, manual = false; if (id) { // We can manually activate a menu with it's id (for hash url mapping) - $menu = this.$element.find('a[data-menu=' + id + ']'); - if (!$menu.length) { - $menu = this.$secondary_menu.find('a[data-menu=' + id + ']'); + manual = true; + $clicked_menu = this.$element.find('a[data-menu=' + id + ']'); + if (!$clicked_menu.length) { + $clicked_menu = this.$secondary_menu.find('a[data-menu=' + id + ']'); } } else { - $menu = $(ev.currentTarget); - id = $menu.data('menu'); - } - if (this.$secondary_menu.has($menu).length) { - $secondary = $menu.parents('.menu_accordion'); - $parent = this.$element.find('a[data-menu=' + $secondary.data('menu-parent') + ']'); - } else { - $parent = $menu; - $secondary = this.$secondary_menu.find('.menu_accordion[data-menu-parent=' + $menu.attr('data-menu') + ']'); + $clicked_menu = $(ev.currentTarget); + id = $clicked_menu.data('menu'); } - this.$secondary_menu.find('.menu_accordion').hide(); - // TODO: ui-accordion : collapse submenus and expand the good one - $secondary.show(); - - if (id) { + if (this.do_menu_click($clicked_menu, manual) && id) { this.session.active_id = id; - this.rpc('/web/menu/action', {'menu_id': id}, - this.on_menu_action_loaded); + this.rpc('/web/menu/action', {'menu_id': id}, this.on_menu_action_loaded); } + ev.stopPropagation(); + return false; + }, + do_menu_click: function($clicked_menu, manual) { + var $sub_menu, $main_menu, + active = $clicked_menu.is('.active'), + sub_menu_visible = false; + + if (this.$secondary_menu.has($clicked_menu).length) { + $sub_menu = $clicked_menu.parents('.oe_secondary_menu'); + $main_menu = this.$element.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']'); + } else { + $sub_menu = this.$secondary_menu.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']'); + $main_menu = $clicked_menu; + } + + sub_menu_visible = $sub_menu.is(':visible'); + this.$secondary_menu.find('.oe_secondary_menu').hide(); $('.active', this.$element.add(this.$secondary_menu.show())).removeClass('active'); - $parent.addClass('active'); - $menu.addClass('active'); - $menu.parent('h4').addClass('active'); + $main_menu.add($clicked_menu).add($sub_menu).addClass('active'); - if (this.$secondary_menu.has($menu).length) { - return !$menu.is(".leaf"); + if (!(this.folded && manual)) { + this.do_show_secondary($sub_menu, $main_menu); + } + + if ($main_menu != $clicked_menu) { + if ($clicked_menu.is('.submenu')) { + $sub_menu.find('.submenu.opened').each(function() { + if (!$(this).next().has($clicked_menu).length && !$(this).is($clicked_menu)) { + $(this).removeClass('opened').next().hide(); + } + }); + $clicked_menu.toggleClass('opened').next().toggle(); + } else if ($clicked_menu.is('.leaf')) { + $sub_menu.toggle(!this.folded); + return true; + } + } else if (this.folded) { + if (active && sub_menu_visible) { + $sub_menu.hide(); + return true; + } } else { - return false; + return true; } + return false; + }, + do_show_secondary: function($sub_menu, $main_menu) { + var self = this; + if (this.folded) { + var css = $main_menu.position(), + fold_width = this.$secondary_menu.width() + 2, + window_width = $(window).width(); + css.top += 33; + css.left -= Math.round(($sub_menu.width() - $main_menu.width()) / 2); + css.left = css.left < fold_width ? fold_width : css.left; + if ((css.left + $sub_menu.width()) > window_width) { + delete(css.left); + css.right = 1; + } + $sub_menu.css(css); + $sub_menu.mouseenter(function() { + clearTimeout($sub_menu.data('timeoutId')); + }).mouseleave(function(evt) { + var timeoutId = setTimeout(function() { + if (self.folded) { + $sub_menu.hide(); + } + }, self.float_timeout); + $sub_menu.data('timeoutId', timeoutId); + }); + } + $sub_menu.show(); }, on_menu_action_loaded: function(data) { var self = this; @@ -860,7 +913,7 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie /** * @constructs openerp.web.WebClient * @extends openerp.web.Widget - * + * * @param element_id */ init: function(element_id) { @@ -883,7 +936,7 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie openerp.web.Widget.prototype.notification = new openerp.web.Notification(this, "oe_notification"); this.header = new openerp.web.Header(this); - this.login = new openerp.web.Login(this, "oe_login"); + this.login = new openerp.web.Login(this); this.header.on_logout.add(this.login.on_logout); this.header.on_action.add(this.on_menu_action); @@ -903,9 +956,8 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie start: function() { this.header.appendTo($("#oe_header")); this.session.start(); - this.login.start(); + this.login.appendTo($('#oe_login')); this.menu.start(); - console.debug("The openerp client has been initialized."); }, on_logged: function() { if(this.action_manager) @@ -951,7 +1003,7 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie self.execute_home_action(home_action[0], ds); }) }, - default_home: function () { + default_home: function () { }, /** * Bundles the execution of the home action @@ -976,7 +1028,6 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie }, do_url_set_hash: function(url) { if(!this.url_external_hashchange) { - console.log("url set #hash to",url); this.url_internal_hashchange = true; jQuery.bbq.pushState(url); } @@ -984,10 +1035,8 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie on_url_hashchange: function() { if(this.url_internal_hashchange) { this.url_internal_hashchange = false; - console.log("url jump to FLAG OFF"); } else { var url = jQuery.deparam.fragment(); - console.log("url jump to",url); this.url_external_hashchange = true; this.action_manager.on_url_hashchange(url); this.url_external_hashchange = false; diff --git a/addons/web/static/src/js/core.js b/addons/web/static/src/js/core.js index dba47ee758c..9d4d2124008 100644 --- a/addons/web/static/src/js/core.js +++ b/addons/web/static/src/js/core.js @@ -1,7 +1,10 @@ /*--------------------------------------------------------- * OpenERP Web core *--------------------------------------------------------*/ - +var console; +if (!console) { + console = {log: function () {}}; +} if (!console.debug) { console.debug = console.log; } @@ -483,7 +486,6 @@ openerp.web.Session = openerp.web.CallbackEnabled.extend( /** @lends openerp.web self.user_context = result.context; self.db = result.db; self.session_save(); - self.on_session_valid(); return true; }).then(success_callback); }, @@ -772,7 +774,6 @@ openerp.web.SessionAware = openerp.web.CallbackEnabled.extend(/** @lends openerp * // stuff that you want to init before the rendering * }, * start: function() { - * this._super(); * // stuff you want to make after the rendering, `this.$element` holds a correct value * this.$element.find(".my_button").click(/* an example of event binding * /); * @@ -916,6 +917,8 @@ openerp.web.Widget = openerp.web.SessionAware.extend(/** @lends openerp.web.Widg * @returns {jQuery.Deferred} */ start: function() { + /* The default implementation is only useful for retro-compatibility, it is + not necessary to call it using _super() when using Widget for new components. */ if (!this.$element) { var tmp = document.getElementById(this.element_id); this.$element = tmp ? $(tmp) : undefined; @@ -923,7 +926,7 @@ openerp.web.Widget = openerp.web.SessionAware.extend(/** @lends openerp.web.Widg return $.Deferred().done().promise(); }, /** - * Destroys the current widget, also destory all its children before destroying itself. + * Destroys the current widget, also destroy all its children before destroying itself. */ stop: function() { _.each(_.clone(this.widget_children), function(el) { @@ -944,7 +947,6 @@ openerp.web.Widget = openerp.web.SessionAware.extend(/** @lends openerp.web.Widg * If that's not the case this method will simply return `false`. */ do_action: function(action, on_finished) { - console.log('Widget.do_action', action, on_finished); if (this.widget_parent) { return this.widget_parent.do_action(action, on_finished); } diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index e5d323c8a96..fcf6a65dcf5 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -233,6 +233,7 @@ openerp.web.StaticDataGroup = openerp.web.GrouplessDataGroup.extend( /** @lends }); openerp.web.DataSet = openerp.web.Widget.extend( /** @lends openerp.web.DataSet# */{ + identifier_prefix: "dataset", /** * DateaManagement interface between views and the collection of selected * OpenERP records (represents the view's state?) @@ -242,16 +243,12 @@ openerp.web.DataSet = openerp.web.Widget.extend( /** @lends openerp.web.DataSet * * @param {String} model the OpenERP model this dataset will manage */ - init: function(source_controller, model, context) { - // we don't want the dataset to be a child of anything! - this._super(null); - this.session = source_controller ? source_controller.session : undefined; + init: function(parent, model, context) { + this._super(parent); this.model = model; this.context = context || {}; this.index = null; }, - start: function() { - }, previous: function () { this.index -= 1; if (this.index < 0) { @@ -549,13 +546,11 @@ openerp.web.DataSetSearch = openerp.web.DataSet.extend(/** @lends openerp.web.D sort: this.sort(), offset: offset, limit: options.limit || false - }, function (result) { + }).pipe(function (result) { self.ids = result.ids; self.offset = offset; - if (callback) { - callback(result.records); - } - }); + return result.records; + }).then(callback); }, get_domain: function (other_domain) { if (other_domain) { @@ -603,20 +598,27 @@ openerp.web.DataSetSearch = openerp.web.DataSet.extend(/** @lends openerp.web.D }); openerp.web.BufferedDataSet = openerp.web.DataSetStatic.extend({ virtual_id_prefix: "one2many_v_id_", - virtual_id_regex: /one2many_v_id_.*/, debug_mode: true, init: function() { this._super.apply(this, arguments); this.reset_ids([]); + this.last_default_get = {}; + }, + default_get: function(fields, callback) { + return this._super(fields).then(this.on_default_get).then(callback); + }, + on_default_get: function(res) { + this.last_default_get = res; }, create: function(data, callback, error_callback) { - var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data}; + var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data, + defaults: this.last_default_get}; this.to_create.push(cached); this.cache.push(cached); this.on_change(); - var to_return = $.Deferred().then(callback); - to_return.resolve({result: cached.id}); - return to_return.promise(); + var prom = $.Deferred().then(callback); + setTimeout(function() {prom.resolve({result: cached.id});}, 0); + return prom.promise(); }, write: function (id, data, options, callback) { var self = this; @@ -676,7 +678,8 @@ openerp.web.BufferedDataSet = openerp.web.DataSetStatic.extend({ var cached = _.detect(self.cache, function(x) {return x.id === id;}); var created = _.detect(self.to_create, function(x) {return x.id === id;}); if (created) { - _.each(fields, function(x) {if (cached.values[x] === undefined) cached.values[x] = false;}); + _.each(fields, function(x) {if (cached.values[x] === undefined) + cached.values[x] = created.defaults[x] || false;}); } else { if (!cached || !_.all(fields, function(x) {return cached.values[x] !== undefined})) to_get.push(id); @@ -715,7 +718,13 @@ openerp.web.BufferedDataSet = openerp.web.DataSetStatic.extend({ return completion.promise(); } }); +openerp.web.BufferedDataSet.virtual_id_regex = /^one2many_v_id_.*$/; + openerp.web.ReadOnlyDataSetSearch = openerp.web.DataSetSearch.extend({ + default_get: function(fields, callback) { + return this._super(fields, callback).then(this.on_default_get); + }, + on_default_get: function(result) {}, create: function(data, callback, error_callback) { this.on_create(data); var to_return = $.Deferred().then(callback); diff --git a/addons/web/static/src/js/data_import.js b/addons/web/static/src/js/data_import.js new file mode 100644 index 00000000000..9c686846751 --- /dev/null +++ b/addons/web/static/src/js/data_import.js @@ -0,0 +1,289 @@ +openerp.web.data_import = function(openerp) { +var QWeb = openerp.web.qweb; +/** + * Safari does not deal well at all with raw JSON data being returned. As a + * result, we're going to cheat by using a pseudo-jsonp: instead of getting + * JSON data in the iframe, we're getting a ``script`` tag which consists of a + * function call and the returned data (the json dump). + * + * The function is an auto-generated name bound to ``window``, which calls + * back into the callback provided here. + * + * @param {Object} form the form element (DOM or jQuery) to use in the call + * @param {Object} attributes jquery.form attributes object + * @param {Function} callback function to call with the returned data + */ +function jsonp(form, attributes, callback) { + attributes = attributes || {}; + var options = {jsonp: _.uniqueId('import_callback_')}; + window[options.jsonp] = function () { + delete window[options.jsonp]; + callback.apply(null, arguments); + }; + if ('data' in attributes) { + _.extend(attributes.data, options); + } else { + _.extend(attributes, {data: options}); + } + $(form).ajaxSubmit(attributes); +} + +openerp.web.DataImport = openerp.web.Dialog.extend({ + template: 'ImportDataView', + dialog_title: "Import Data", + init: function(parent, dataset){ + var self = this; + this._super(parent, {}); + this.model = parent.model; + this.fields = []; + this.all_fields = []; + this.required_fields = null; + + var convert_fields = function (root, prefix) { + prefix = prefix || ''; + _(root.fields).each(function (f) { + self.all_fields.push(prefix + f.name); + if (f.fields) { + convert_fields(f, prefix + f.id + '/'); + } + }); + }; + this.ready = $.Deferred.queue().then(function () { + self.required_fields = _(self.fields).chain() + .filter(function (field) { return field.required; }) + .pluck('name') + .value(); + convert_fields(self); + self.all_fields.sort(); + }); + }, + start: function() { + var self = this; + this._super(); + this.open({ + modal: true, + width: '70%', + height: 'auto', + position: 'top', + buttons: [ + {text: "Close", click: function() { self.stop(); }}, + {text: "Import File", click: function() { self.do_import(); }, 'class': 'oe-dialog-import-button'} + ], + close: function(event, ui) { + self.stop(); + } + }); + this.toggle_import_button(false); + this.$element.find('#csvfile').change(this.on_autodetect_data); + this.$element.find('fieldset').change(this.on_autodetect_data); + this.$element.find('fieldset legend').click(function() { + $(this).next().toggle(); + }); + this.ready.push(new openerp.web.DataSet(this, this.model).call( + 'fields_get', [], function (fields) { + self.graft_fields(fields); + })); + }, + graft_fields: function (fields, parent, level) { + parent = parent || this; + level = level || 0; + + var self = this; + _(fields).each(function (field, field_name) { + var f = { + id: field_name, + name: field_name, + string: field.string, + required: field.required + }; + + switch (field.type) { + case 'many2many': + case 'many2one': + f.name += '/id'; + break; + case 'one2many': + f.name += '/id'; + f.fields = []; + // only fetch sub-fields to a depth of 2 levels + if (level < 2) { + self.ready.push(new openerp.web.DataSet(self, field.relation).call( + 'fields_get', [], function (fields) { + self.graft_fields(fields, f, level+1); + })); + } + break; + } + parent.fields.push(f); + }); + }, + toggle_import_button: function (newstate) { + this.$dialog.dialog('widget') + .find('.oe-dialog-import-button') + .button('option', 'disabled', !newstate); + }, + do_import: function() { + if(!this.$element.find('#csvfile').val()) { return; } + var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10); + var with_headers = this.$element.find('#file_has_headers').prop('checked'); + if (!lines_to_skip && with_headers) { + lines_to_skip = 1; + } + var indices = [], fields = []; + this.$element.find(".sel_fields").each(function(index, element) { + var val = element.value; + if (!val) { + return; + } + indices.push(index); + fields.push(val); + }); + + jsonp(this.$element.find('#import_data'), { + url: '/web/import/import_data', + data: { + model: this.model, + meta: JSON.stringify({ + skip: lines_to_skip, + indices: indices, + fields: fields + }) + } + }, this.on_import_results); + }, + on_autodetect_data: function() { + if(!this.$element.find('#csvfile').val()) { return; } + jsonp(this.$element.find('#import_data'), { + url: '/web/import/detect_data' + }, this.on_import_results); + }, + on_import_results: function(results) { + this.$element.find('#result').empty(); + var headers, result_node = this.$element.find("#result"); + + if (results['records']) { + var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10), + with_headers = this.$element.find('#file_has_headers').prop('checked'); + headers = with_headers ? results.records[0] : null; + + result_node.append(QWeb.render('ImportView.result', { + 'headers': headers, + 'records': lines_to_skip ? results.records.slice(lines_to_skip) + : with_headers ? results.records.slice(1) + : results.records + })); + } else if (results['error']) { + result_node.append(QWeb.render('ImportView.error', { + 'error': results['error']})); + } else if (results['success']) { + if (this.widget_parent.widget_parent.active_view == "list") { + this.widget_parent.reload_content(); + } + this.stop(); + return; + } + + var self = this; + this.ready.then(function () { + var $fields = self.$element.find('.sel_fields').bind('blur', function () { + if (this.value && !_(self.all_fields).contains(this.value)) { + this.value = ''; + } + }).autocomplete({ + minLength: 0, + source: self.all_fields, + change: self.on_check_field_values + }).focus(function () { + $(this).autocomplete('search'); + }); + // Column auto-detection + _(headers).each(function (header, index) { + var f =_(self.fields).detect(function (field) { + // TODO: levenshtein between header and field.string + return field.name === header || field.string.toLowerCase() === header; + }); + if (f) { + $fields.eq(index).val(f.name); + } + }); + self.on_check_field_values(); + }); + }, + /** + * Looks through all the field selections, and tries to find if two + * (or more) columns were matched to the same model field. + * + * Returns a map of the multiply-mapped fields to an array of offending + * columns (not actually columns, but the inputs containing the same field + * names). + * + * Also has the side-effect of marking the discovered inputs with the class + * ``duplicate_fld``. + * + * @returns {Object<String, Array<String>>} map of duplicate field matches to same-valued inputs + */ + find_duplicate_fields: function() { + // Maps values to DOM nodes, in order to discover duplicates + var values = {}, duplicates = {}; + this.$element.find(".sel_fields").each(function(index, element) { + var value = element.value; + var $element = $(element).removeClass('duplicate_fld'); + if (!value) { return; } + + if (!(value in values)) { + values[value] = element; + } else { + var same_valued_field = values[value]; + if (value in duplicates) { + duplicates[value].push(element); + } else { + duplicates[value] = [same_valued_field, element]; + } + $element.add(same_valued_field).addClass('duplicate_fld'); + } + }); + return duplicates; + }, + on_check_field_values: function () { + this.$element.find("#message, #msg").remove(); + + var required_valid = this.check_required(); + + var duplicates = this.find_duplicate_fields(); + if (_.isEmpty(duplicates)) { + this.toggle_import_button(required_valid); + } else { + var $err = $('<div id="msg" style="color: red;">Destination fields should only be selected once, some fields are selected more than once:</div>').insertBefore(this.$element.find('#result')); + var $dupes = $('<dl>').appendTo($err); + _(duplicates).each(function(elements, value) { + $('<dt>').text(value).appendTo($dupes); + _(elements).each(function(element) { + var cell = $(element).closest('td'); + $('<dd>').text(cell.parent().children().index(cell)).appendTo($dupes); + }); + }); + this.toggle_import_button(false); + } + + }, + check_required: function() { + if (!this.required_fields.length) { return true; } + + var selected_fields = _(this.$element.find('.sel_fields').get()).chain() + .pluck('value') + .compact() + .value(); + + var missing_fields = _.difference(this.required_fields, selected_fields); + if (missing_fields.length) { + this.$element.find("#result").before('<div id="message" style="color:red">*Required Fields are not selected : ' + missing_fields + '.</div>'); + return false; + } + return true; + }, + stop: function() { + $(this.$dialog).remove(); + this._super(); + } +}); +}; diff --git a/addons/web/static/src/js/formats.js b/addons/web/static/src/js/formats.js index b0dc4fcc289..e7feedf26a5 100644 --- a/addons/web/static/src/js/formats.js +++ b/addons/web/static/src/js/formats.js @@ -82,7 +82,12 @@ openerp.web.parse_value = function (value, descriptor, value_if_empty) { } switch (descriptor.widget || descriptor.type) { case 'integer': - var tmp = Number(value); + var tmp; + do { + tmp = value; + value = value.replace(openerp.web._t.database.parameters.thousands_sep, ""); + } while(tmp !== value); + tmp = Number(value); if (isNaN(tmp)) throw value + " is not a correct integer"; return tmp; @@ -113,26 +118,26 @@ openerp.web.parse_value = function (value, descriptor, value_if_empty) { var tmp = Date.parseExact(value, _.sprintf("%s %s", Date.CultureInfo.formatPatterns.shortDate, Date.CultureInfo.formatPatterns.longTime)); if (tmp !== null) - return tmp; + return openerp.web.datetime_to_str(tmp); tmp = Date.parse(value); if (tmp !== null) - return tmp; + return openerp.web.datetime_to_str(tmp); throw value + " is not a valid datetime"; case 'date': var tmp = Date.parseExact(value, Date.CultureInfo.formatPatterns.shortDate); if (tmp !== null) - return tmp; + return openerp.web.date_to_str(tmp); tmp = Date.parse(value); if (tmp !== null) - return tmp; + return openerp.web.date_to_str(tmp); throw value + " is not a valid date"; case 'time': var tmp = Date.parseExact(value, Date.CultureInfo.formatPatterns.longTime); if (tmp !== null) - return tmp; + return openerp.web.time_to_str(tmp); tmp = Date.parse(value); if (tmp !== null) - return tmp; + return openerp.web.time_to_str(tmp); throw value + " is not a valid time"; } return value; diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index 5db66ab7260..db91d6bfd27 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -327,7 +327,8 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search this.notification.notify("Invalid Search", "triggered from search view"); }, do_clear: function () { - $('.filter_label').removeClass('enabled'); + this.$element.find('.filter_label').removeClass('enabled'); + this.enabled_filters.splice(0); var string = $('a.searchview_group_string'); _.each(string, function(str){ $(str).closest('div.searchview_group').removeClass("expanded").addClass('folded'); @@ -542,6 +543,7 @@ openerp.web.search.FilterGroup = openerp.web.search.Input.extend(/** @lends open var domains = _(this.filters).chain() .filter(function (filter) { return filter.is_enabled(); }) .map(function (filter) { return filter.attrs.domain; }) + .reject(_.isEmpty) .value(); if (!domains.length) { return; } @@ -720,7 +722,11 @@ openerp.web.search.NumberField = openerp.web.search.Field.extend(/** @lends open openerp.web.search.IntegerField = openerp.web.search.NumberField.extend(/** @lends openerp.web.search.IntegerField# */{ error_message: "not a valid integer", parse: function (value) { - return parseInt(value, 10); + try { + return openerp.web.parse_value(value, {'widget': 'integer'}); + } catch (e) { + return NaN; + } } }); /** @@ -730,7 +736,11 @@ openerp.web.search.IntegerField = openerp.web.search.NumberField.extend(/** @len openerp.web.search.FloatField = openerp.web.search.NumberField.extend(/** @lends openerp.web.search.FloatField# */{ error_message: "not a valid number", parse: function (value) { - return parseFloat(value); + try { + return openerp.web.parse_value(value, {'widget': 'float'}); + } catch (e) { + return NaN; + } } }); /** @@ -785,20 +795,18 @@ openerp.web.search.BooleanField = openerp.web.search.SelectionField.extend(/** @ * @extends openerp.web.search.DateField */ openerp.web.search.DateField = openerp.web.search.Field.extend(/** @lends openerp.web.search.DateField# */{ - /** - * enables date picker on the HTML widgets - */ + template: "SearchView.date", start: function () { this._super(); - this.$element.addClass('field_date').datepicker({ - dateFormat: 'yy-mm-dd' - }); - }, - stop: function () { - this.$element.datepicker('destroy'); + this.datewidget = new openerp.web.DateWidget(this); + this.datewidget.prependTo(this.$element); + this.datewidget.$element.find("input").attr("size", 15); + this.datewidget.$element.find("input").attr("autofocus", + this.attrs.default_focus === '1' ? 'autofocus' : undefined); + this.datewidget.set_value(this.defaults[this.attrs.name] || false); }, get_value: function () { - return this.$element.val(); + return this.datewidget.get_value() || null; } }); /** @@ -1113,7 +1121,7 @@ openerp.web.search.ExtendedSearchProposition.Char = openerp.web.OldWidget.extend } }); openerp.web.search.ExtendedSearchProposition.DateTime = openerp.web.OldWidget.extend({ - template: 'SearchView.extended_search.proposition.datetime', + template: 'SearchView.extended_search.proposition.empty', identifier_prefix: 'extended-search-proposition-datetime', operators: [ {value: "=", text: "is equal to"}, @@ -1124,18 +1132,16 @@ openerp.web.search.ExtendedSearchProposition.DateTime = openerp.web.OldWidget.ex {value: "<=", text: "less or equal than"} ], get_value: function() { - return this.$element.val(); + return this.datewidget.get_value(); }, start: function() { this._super(); - this.$element.datetimepicker({ - dateFormat: 'yy-mm-dd', - timeFormat: 'hh:mm:ss' - }); + this.datewidget = new openerp.web.DateTimeWidget(this); + this.datewidget.prependTo(this.$element); } }); openerp.web.search.ExtendedSearchProposition.Date = openerp.web.OldWidget.extend({ - template: 'SearchView.extended_search.proposition.date', + template: 'SearchView.extended_search.proposition.empty', identifier_prefix: 'extended-search-proposition-date', operators: [ {value: "=", text: "is equal to"}, @@ -1146,14 +1152,12 @@ openerp.web.search.ExtendedSearchProposition.Date = openerp.web.OldWidget.extend {value: "<=", text: "less or equal than"} ], get_value: function() { - return this.$element.val(); + return this.datewidget.get_value(); }, start: function() { this._super(); - this.$element.datepicker({ - dateFormat: 'yy-mm-dd', - timeFormat: 'hh:mm:ss' - }); + this.datewidget = new openerp.web.DateWidget(this); + this.datewidget.prependTo(this.$element); } }); openerp.web.search.ExtendedSearchProposition.Integer = openerp.web.OldWidget.extend({ @@ -1168,11 +1172,11 @@ openerp.web.search.ExtendedSearchProposition.Integer = openerp.web.OldWidget.ext {value: "<=", text: "less or equal than"} ], get_value: function() { - var value = parseFloat(this.$element.val()); - if(value != 0 && !value) { + try { + return openerp.web.parse_value(this.$element.val(), {'widget': 'integer'}); + } catch (e) { return ""; } - return Math.round(value); } }); openerp.web.search.ExtendedSearchProposition.Float = openerp.web.OldWidget.extend({ @@ -1187,11 +1191,11 @@ openerp.web.search.ExtendedSearchProposition.Float = openerp.web.OldWidget.exten {value: "<=", text: "less or equal than"} ], get_value: function() { - var value = parseFloat(this.$element.val()); - if(value != 0 && !value) { + try { + return openerp.web.parse_value(this.$element.val(), {'widget': 'float'}); + } catch (e) { return ""; } - return value; } }); openerp.web.search.ExtendedSearchProposition.Selection = openerp.web.OldWidget.extend({ diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index d3293b4f2ad..11ebad8d093 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -26,7 +26,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# this.set_default_options(options); this.dataset = dataset; this.model = dataset.model; - this.view_id = view_id; + this.view_id = view_id || false; this.fields_view = {}; this.widgets = {}; this.widgets_counter = 0; @@ -41,7 +41,8 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# this.has_been_loaded = $.Deferred(); this.$form_header = null; this.translatable_fields = []; - _.defaults(this.options, {"always_show_new_button": true}); + _.defaults(this.options, {"always_show_new_button": true, + "not_interactible_on_create": false}); }, start: function() { this._super(); @@ -72,17 +73,25 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# _.each(this.widgets, function(w) { w.stop(); }); + this._super(); + }, + reposition: function ($e) { + this.$element = $e; + this.on_loaded(); }, on_loaded: function(data) { var self = this; - this.fields_view = data; - var frame = new (this.registry.get_object('frame'))(this, this.fields_view.arch); + if (data) { + this.fields_view = data; + var frame = new (this.registry.get_object('frame'))(this, this.fields_view.arch); - this.$element.html(QWeb.render(this.form_template, { 'frame': frame, 'view': this })); + this.rendered = QWeb.render(this.form_template, { 'frame': frame, 'view': this }); + } + this.$element.html(this.rendered); _.each(this.widgets, function(w) { w.start(); }); - this.$form_header = this.$element.find('#' + this.element_id + '_header'); + this.$form_header = this.$element.find('.oe_form_header:first'); this.$form_header.find('div.oe_form_pager button[data-pager-action]').click(function() { var action = $(this).data('pager-action'); self.on_pager_action(action); @@ -93,6 +102,17 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# this.$form_header.find('button.oe_form_button_cancel').click(this.do_cancel); this.$form_header.find('button.oe_form_button_new').click(this.on_button_new); this.$form_header.find('button.oe_form_button_duplicate').click(this.on_button_duplicate); + this.$form_header.find('button.oe_form_button_toggle').click(function () { + self.translatable_fields = []; + self.widgets = {}; + self.fields = {}; + self.$form_header.find('button').unbind('click'); + self.registry = self.registry === openerp.web.form.widgets + ? openerp.web.form.readonly + : openerp.web.form.widgets; + self.on_loaded(self.fields_view); + self.reload(); + }); if (this.options.sidebar && this.options.sidebar_id) { this.sidebar = new openerp.web.Sidebar(this, this.options.sidebar_id); @@ -196,7 +216,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# } }, do_update_pager: function(hide_index) { - var $pager = this.$element.find('#' + this.element_id + '_header div.oe_form_pager'); + var $pager = this.$form_header.find('div.oe_form_pager'); var index = hide_index ? '-' : this.dataset.index + 1; $pager.find('span.oe_pager_index').html(index); $pager.find('span.oe_pager_count').html(this.dataset.ids.length); @@ -304,9 +324,15 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# var def = $.Deferred(); $.when(this.has_been_loaded).then(function() { if (self.can_be_discarded()) { - self.dataset.default_get(_.keys(self.fields_view.fields)).then(self.on_record_loaded).then(function() { + var keys = _.keys(self.fields_view.fields); + if (keys.length) { + self.dataset.default_get(keys).then(self.on_record_loaded).then(function() { + def.resolve(); + }); + } else { + self.on_record_loaded({}); def.resolve(); - }); + } } }); return def.promise(); @@ -339,7 +365,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# do_save: function(success, prepend_on_create) { var self = this; if (!this.ready) { - return false; + return $.Deferred().reject(); } var form_dirty = false, form_invalid = false, @@ -361,23 +387,18 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# if (form_invalid) { first_invalid_field.focus(); this.on_invalid(); - return false; - } else if (form_dirty) { + return $.Deferred().reject(); + } else { console.log("About to save", values); if (!this.datarecord.id) { - return this.dataset.create(values, function(r) { - self.on_created(r, success, prepend_on_create); - }); + return this.dataset.create(values).pipe(function(r) { + return self.on_created(r, undefined, prepend_on_create); + }).then(success); } else { - return this.dataset.write(this.datarecord.id, values, {}, function(r) { - self.on_saved(r, success); - }); + return this.dataset.write(this.datarecord.id, values, {}).pipe(function(r) { + return self.on_saved(r); + }).then(success); } - } else { - setTimeout(function() { - self.on_saved({ result: true }, success); - }); - return true; } }, do_save_edit: function() { @@ -401,11 +422,10 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# on_saved: function(r, success) { if (!r.result) { // should not happen in the server, but may happen for internal purpose + return $.Deferred().reject(); } else { - if (success) { - success(r); - } this.reload(); + return $.Deferred().then(success).resolve(r); } }, /** @@ -424,6 +444,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# on_created: function(r, success, prepend_on_create) { if (!r.result) { // should not happen in the server, but may happen for internal purpose + return $.Deferred().reject(); } else { this.datarecord.id = r.result; if (!prepend_on_create) { @@ -438,10 +459,8 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# this.sidebar.attachments.do_update(); } console.debug("The record has been created with id #" + this.datarecord.id); - if (success) { - success(_.extend(r, {created: true})); - } this.reload(); + return $.Deferred().then(success).resolve(_.extend(r, {created: true})); } }, do_search: function (domains, contexts, groupbys) { @@ -471,6 +490,24 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# get_selected_ids: function() { var id = this.dataset.ids[this.dataset.index]; return id ? [id] : []; + }, + recursive_save: function() { + var self = this; + return $.when(this.do_save()).pipe(function(res) { + if (self.dataset.parent_view) + return self.dataset.parent_view.recursive_save(); + }); + }, + is_interactible_record: function() { + var id = this.datarecord.id; + if (!id) { + if (this.options.not_interactible_on_create) + return false; + } else if (typeof(id) === "string") { + if(openerp.web.BufferedDataSet.virtual_id_regex.test(id)) + return false; + } + return true; } }); openerp.web.FormDialog = openerp.web.Dialog.extend({ @@ -618,6 +655,7 @@ openerp.web.form.compute_domain = function(expr, fields) { openerp.web.form.Widget = openerp.web.Widget.extend(/** @lends openerp.web.form.Widget# */{ template: 'Widget', + identifier_prefix: 'formview-widget-', /** * @constructs openerp.web.form.Widget * @extends openerp.web.Widget @@ -631,11 +669,13 @@ openerp.web.form.Widget = openerp.web.Widget.extend(/** @lends openerp.web.form. this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}'); this.type = this.type || node.tag; this.element_name = this.element_name || this.type; - this.element_id = [this.view.element_id, this.element_name, this.view.widgets_counter++].join("_"); + this.element_class = [ + 'formview', this.view.view_id, this.element_name, + this.view.widgets_counter++].join("_"); - this._super(view, this.element_id); + this._super(view); - this.view.widgets[this.element_id] = this; + this.view.widgets[this.element_class] = this; this.children = node.children; this.colspan = parseInt(node.attrs.colspan || 1, 10); this.decrease_max_width = 0; @@ -648,12 +688,7 @@ openerp.web.form.Widget = openerp.web.Widget.extend(/** @lends openerp.web.form. this.width = this.node.attrs.width; }, start: function() { - this.$element = $('#' + this.element_id); - }, - stop: function() { - if (this.$element) { - this.$element.remove(); - } + this.$element = this.view.$element.find('.' + this.element_class); }, process_modifiers: function() { var compute_domain = openerp.web.form.compute_domain; @@ -757,13 +792,24 @@ openerp.web.form.WidgetNotebook = openerp.web.form.Widget.extend({ for (var i = 0; i < node.children.length; i++) { var n = node.children[i]; if (n.tag == "page") { - var page = new openerp.web.form.WidgetNotebookPage(this.view, n, this, this.pages.length); + var page = new openerp.web.form.WidgetNotebookPage( + this.view, n, this, this.pages.length); this.pages.push(page); } } }, start: function() { + var self = this; this._super.apply(this, arguments); + this.$element.find('> ul > li').each(function (index, tab_li) { + var page = self.pages[index], + id = _.uniqueId(self.element_name + '-'); + page.element_id = id; + $(tab_li).find('a').attr('href', '#' + id); + }); + this.$element.find('> div').each(function (index, page) { + page.id = self.pages[index].element_id; + }); this.$element.tabs(); this.view.on_button_new.add_last(this.do_select_first_visible_tab); }, @@ -785,11 +831,11 @@ openerp.web.form.WidgetNotebookPage = openerp.web.form.WidgetFrame.extend({ this.index = index; this.element_name = 'page_' + index; this._super(view, node); - this.element_tab_id = this.element_id + '_tab'; }, start: function() { this._super.apply(this, arguments); - this.$element_tab = $('#' + this.element_tab_id); + this.$element_tab = this.notebook.$element.find( + '> ul > li:eq(' + this.index + ')'); }, update_dom: function() { if (this.invisible && this.index === this.notebook.$element.tabs('option', 'selected')) { @@ -801,9 +847,9 @@ openerp.web.form.WidgetNotebookPage = openerp.web.form.WidgetFrame.extend({ }); openerp.web.form.WidgetSeparator = openerp.web.form.Widget.extend({ + template: 'WidgetSeparator', init: function(view, node) { this._super(view, node); - this.template = "WidgetSeparator"; this.orientation = node.attrs.orientation || 'horizontal'; if (this.orientation === 'vertical') { this.width = '1'; @@ -813,9 +859,10 @@ openerp.web.form.WidgetSeparator = openerp.web.form.Widget.extend({ }); openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({ + template: 'WidgetButton', init: function(view, node) { this._super(view, node); - this.template = "WidgetButton"; + this.force_disabled = false; if (this.string) { // We don't have button key bindings in the webclient this.string = this.string.replace(/_/g, ''); @@ -827,45 +874,74 @@ openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({ }, start: function() { this._super.apply(this, arguments); - this.$element.click(this.on_click); + this.$element.find("button").click(this.on_click); }, - on_click: function(saved) { + on_click: function() { var self = this; - if ((!this.node.attrs.special && this.view.dirty_for_user && saved !== true) || !this.view.datarecord.id) { - this.view.do_save(function() { - self.on_click(true); - }); - } else { - if (this.node.attrs.confirm) { - var dialog = $('<div>' + this.node.attrs.confirm + '</div>').dialog({ + this.force_disabled = true; + this.check_disable(); + this.execute_action().always(function() { + self.force_disabled = false; + self.check_disable(); + }); + }, + execute_action: function() { + var self = this; + var exec_action = function() { + if (self.node.attrs.confirm) { + var def = $.Deferred(); + var dialog = $('<div>' + self.node.attrs.confirm + '</div>').dialog({ title: 'Confirm', modal: true, buttons: { Ok: function() { - self.on_confirmed(); - $(this).dialog("close"); + self.on_confirmed().then(function() { + def.resolve(); + }); + $(self).dialog("close"); }, Cancel: function() { - $(this).dialog("close"); + def.resolve(); + $(self).dialog("close"); } } }); + return def.promise(); } else { - this.on_confirmed(); + return self.on_confirmed(); } + }; + if ((!this.node.attrs.special && this.view.dirty_for_user) || !this.view.datarecord.id) { + return this.view.recursive_save().pipe(exec_action); + } else { + return exec_action(); } }, on_confirmed: function() { var self = this; - this.view.do_execute_action( + return this.view.do_execute_action( this.node.attrs, this.view.dataset, this.view.datarecord.id, function () { self.view.reload(); }); + }, + update_dom: function() { + this._super(); + this.check_disable(); + }, + check_disable: function() { + if (this.force_disabled || !this.view.is_interactible_record()) { + this.$element.find("button").attr("disabled", "disabled"); + this.$element.find("button").css("color", "grey"); + } else { + this.$element.find("button").removeAttr("disabled"); + this.$element.find("button").css("color", ""); + } } }); openerp.web.form.WidgetLabel = openerp.web.form.Widget.extend({ + template: 'WidgetLabel', init: function(view, node) { this.element_name = 'label_' + node.attrs.name; @@ -876,7 +952,6 @@ openerp.web.form.WidgetLabel = openerp.web.form.Widget.extend({ this.template = "WidgetParagraph"; this.colspan = parseInt(this.node.attrs.colspan || 1, 10); } else { - this.template = "WidgetLabel"; this.colspan = 1; this.width = '1%'; this.decrease_max_width = 1; @@ -895,7 +970,7 @@ openerp.web.form.WidgetLabel = openerp.web.form.Widget.extend({ var self = this; this.$element.find("label").dblclick(function() { var widget = self['for'] || self; - console.log(widget.element_id , widget); + console.log(widget.element_class , widget); window.w = widget; }); } @@ -960,7 +1035,7 @@ openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.f return !this.invalid; }, is_dirty: function() { - return this.dirty; + return this.dirty && !this.readonly; }, get_on_change_value: function() { return this.get_value(); @@ -995,37 +1070,40 @@ openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.f focus: function() { }, _build_view_fields_values: function() { - var a_dataset = this.view.dataset || {}; + var a_dataset = this.view.dataset; var fields_values = this.view.get_fields_values(); var parent_values = a_dataset.parent_view ? a_dataset.parent_view.get_fields_values() : {}; fields_values.parent = parent_values; return fields_values; }, + _build_eval_context: function() { + var a_dataset = this.view.dataset; + return new openerp.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values()); + }, /** * Builds a new context usable for operations related to fields by merging * the fields'context with the action's context. */ build_context: function() { - // I previously belevied contexts should be herrited, but now I doubt it - //var a_context = this.view.dataset.get_context() || {}; var f_context = this.field.context || null; // maybe the default_get should only be used when we do a default_get? - var v_context1 = this.node.attrs.default_get || {}; - var v_context2 = this.node.attrs.context || {}; - var v_context = new openerp.web.CompoundContext(v_context1, v_context2); - if (v_context1.__ref || v_context2.__ref || true) { //TODO niv: remove || true - var fields_values = this._build_view_fields_values(); + var v_contexts = _.compact([this.node.attrs.default_get || null, + this.node.attrs.context || null]); + var v_context = new openerp.web.CompoundContext(); + _.each(v_contexts, function(x) {v_context.add(x);}); + if (_.detect(v_contexts, function(x) {return !!x.__ref;})) { + var fields_values = this._build_eval_context(); v_context.set_eval_context(fields_values); } // if there is a context on the node, overrides the model's context - var ctx = f_context || v_context; + var ctx = v_contexts.length > 0 ? v_context : f_context; return ctx; }, build_domain: function() { var f_domain = this.field.domain || null; var v_domain = this.node.attrs.domain || []; if (!(v_domain instanceof Array) || true) { //TODO niv: remove || true - var fields_values = this._build_view_fields_values(); + var fields_values = this._build_eval_context(); v_domain = new openerp.web.CompoundDomain(v_domain).set_eval_context(fields_values); } // if there is a domain on the node, overrides the model's domain @@ -1034,10 +1112,7 @@ openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.f }); openerp.web.form.FieldChar = openerp.web.form.Field.extend({ - init: function(view, node) { - this._super(view, node); - this.template = "FieldChar"; - }, + template: 'FieldChar', start: function() { this._super.apply(this, arguments); this.$element.find('input').change(this.on_ui_change); @@ -1046,6 +1121,7 @@ openerp.web.form.FieldChar = openerp.web.form.Field.extend({ this._super.apply(this, arguments); var show_value = openerp.web.format_value(value, this, ''); this.$element.find('input').val(show_value); + return show_value; }, update_dom: function() { this._super.apply(this, arguments); @@ -1070,10 +1146,7 @@ openerp.web.form.FieldChar = openerp.web.form.Field.extend({ }); openerp.web.form.FieldEmail = openerp.web.form.FieldChar.extend({ - init: function(view, node) { - this._super(view, node); - this.template = "FieldEmail"; - }, + template: 'FieldEmail', start: function() { this._super.apply(this, arguments); this.$element.find('button').click(this.on_button_clicked); @@ -1084,18 +1157,11 @@ openerp.web.form.FieldEmail = openerp.web.form.FieldChar.extend({ } else { location.href = 'mailto:' + this.value; } - }, - set_value: function(value) { - this._super.apply(this, arguments); - this.$element.find('a').attr('href', 'mailto:' + this.$element.find('input').val()); } }); openerp.web.form.FieldUrl = openerp.web.form.FieldChar.extend({ - init: function(view, node) { - this._super(view, node); - this.template = "FieldUrl"; - }, + template: 'FieldUrl', start: function() { this._super.apply(this, arguments); this.$element.find('button').click(this.on_button_clicked); @@ -1120,16 +1186,13 @@ openerp.web.form.FieldFloat = openerp.web.form.FieldChar.extend({ } }); -openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({ - init: function(view, node) { - this._super(view, node); - this.template = "FieldDate"; - this.jqueryui_object = 'datetimepicker'; - }, +openerp.web.DateTimeWidget = openerp.web.Widget.extend({ + template: "web.datetimepicker", + jqueryui_object: 'datetimepicker', + type_of_date: "datetime", start: function() { var self = this; - this._super.apply(this, arguments); - this.$element.find('input').change(this.on_ui_change); + this.$element.find('input').change(this.on_change); this.picker({ onSelect: this.on_picker_select, changeMonth: true, @@ -1147,6 +1210,8 @@ openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({ this.$element.find('button.oe_datepicker_close').click(function() { self.$element.find('.oe_datepicker').hide(); }); + this.set_readonly(false); + this.value = false; }, picker: function() { return $.fn[this.jqueryui_object].apply(this.$element.find('.oe_datepicker_container'), arguments); @@ -1156,69 +1221,99 @@ openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({ this.$element.find('input').val(date ? this.format_client(date) : '').change(); }, set_value: function(value) { - value = this.parse(value); - this._super(value); + this.value = value; this.$element.find('input').val(value ? this.format_client(value) : ''); }, get_value: function() { - return this.format(this.value); + return this.value; }, set_value_from_ui: function() { var value = this.$element.find('input').val() || false; this.value = this.parse_client(value); - this._super(); }, - update_dom: function() { - this._super.apply(this, arguments); + set_readonly: function(readonly) { + this.readonly = readonly; this.$element.find('input').attr('disabled', this.readonly); - this.$element.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', this.readonly); + this.$element.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly); }, - validate: function() { - this.invalid = false; + is_valid: function(required) { var value = this.$element.find('input').val(); if (value === "") { - this.invalid = this.required; + return !required; } else { try { this.parse_client(value); - this.invalid = false; + return true; } catch(e) { - this.invalid = true; + return false; } } }, focus: function() { this.$element.find('input').focus(); }, - parse: openerp.web.auto_str_to_date, parse_client: function(v) { - return openerp.web.parse_value(v, this.field); - }, - format: function(val) { - return openerp.web.auto_date_to_str(val, this.field.type); + return openerp.web.parse_value(v, {"widget": this.type_of_date}); }, format_client: function(v) { - return openerp.web.format_value(v, this.field); + return openerp.web.format_value(v, {"widget": this.type_of_date}); + }, + on_change: function() { + if (this.is_valid()) { + this.set_value_from_ui(); + } } }); -openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({ - init: function(view, node) { - this._super(view, node); - this.jqueryui_object = 'datepicker'; - }, +openerp.web.DateWidget = openerp.web.DateTimeWidget.extend({ + jqueryui_object: 'datepicker', + type_of_date: "date", on_picker_select: function(text, instance) { this._super(text, instance); this.$element.find('.oe_datepicker').hide(); } }); -openerp.web.form.FieldText = openerp.web.form.Field.extend({ - init: function(view, node) { - this._super(view, node); - this.template = "FieldText"; +openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({ + template: "EmptyComponent", + build_widget: function() { + return new openerp.web.DateTimeWidget(this); }, start: function() { + var self = this; + this._super.apply(this, arguments); + this.datewidget = this.build_widget(); + this.datewidget.on_change.add(this.on_ui_change); + this.datewidget.appendTo(this.$element); + }, + set_value: function(value) { + this._super(value); + this.datewidget.set_value(value); + }, + get_value: function() { + return this.datewidget.get_value(); + }, + update_dom: function() { + this._super.apply(this, arguments); + this.datewidget.set_readonly(this.readonly); + }, + validate: function() { + this.invalid = !this.datewidget.is_valid(this.required); + }, + focus: function() { + this.datewidget.focus(); + } +}); + +openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({ + build_widget: function() { + return new openerp.web.DateWidget(this); + } +}); + +openerp.web.form.FieldText = openerp.web.form.Field.extend({ + template: 'FieldText', + start: function() { this._super.apply(this, arguments); this.$element.find('textarea').change(this.on_ui_change); }, @@ -1250,10 +1345,7 @@ openerp.web.form.FieldText = openerp.web.form.Field.extend({ }); openerp.web.form.FieldBoolean = openerp.web.form.Field.extend({ - init: function(view, node) { - this._super(view, node); - this.template = "FieldBoolean"; - }, + template: 'FieldBoolean', start: function() { var self = this; this._super.apply(this, arguments); @@ -1284,10 +1376,7 @@ openerp.web.form.FieldBoolean = openerp.web.form.Field.extend({ }); openerp.web.form.FieldProgressBar = openerp.web.form.Field.extend({ - init: function(view, node) { - this._super(view, node); - this.template = "FieldProgressBar"; - }, + template: 'FieldProgressBar', start: function() { this._super.apply(this, arguments); this.$element.find('div').progressbar({ @@ -1310,10 +1399,10 @@ openerp.web.form.FieldTextXml = openerp.web.form.Field.extend({ }); openerp.web.form.FieldSelection = openerp.web.form.Field.extend({ + template: 'FieldSelection', init: function(view, node) { var self = this; this._super(view, node); - this.template = "FieldSelection"; this.values = this.field.selection; _.each(this.values, function(v, i) { if (v[0] === false && v[1] === '') { @@ -1420,9 +1509,9 @@ openerp.web.form.dialog = function(content, options) { }; openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ + template: 'FieldMany2One', init: function(view, node) { this._super(view, node); - this.template = "FieldMany2One"; this.limit = 7; this.value = null; this.cm_id = _.uniqueId('m2o_cm_'); @@ -1435,7 +1524,7 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ this.$input = this.$element.find("input"); this.$drop_down = this.$element.find(".oe-m2o-drop-down-button"); this.$menu_btn = this.$element.find(".oe-m2o-cm-button"); - + // context menu var init_context_menu_def = $.Deferred().then(function(e) { var rdataset = new openerp.web.DataSetStatic(self, "ir.values", self.build_context()); @@ -1443,7 +1532,7 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ [[self.field.relation, false]], false, rdataset.get_context()], false, 0) .then(function(result) { self.related_entries = result; - + var $cmenu = $("#" + self.cm_id); $cmenu.append(QWeb.render("FieldMany2One.context_menu", {widget: self})); var bindings = {}; @@ -1747,10 +1836,10 @@ var commands = { } }; openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ + template: 'FieldOne2Many', multi_selection: false, init: function(view, node) { this._super(view, node); - this.template = "FieldOne2Many"; this.is_started = $.Deferred(); this.form_last_update = $.Deferred(); this.disable_utility_classes = true; @@ -1781,6 +1870,8 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ } if(view.view_type === "list") { view.options.selectable = self.multi_selection; + } else if (view.view_type === "form") { + view.options.not_interactible_on_create = true; } views.push(view); }); @@ -1840,6 +1931,7 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ switch (command[0]) { case commands.CREATE: obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix); + obj.defaults = {}; self.dataset.to_create.push(obj); self.dataset.cache.push(_.clone(obj)); ids.push(obj.id); @@ -1869,6 +1961,7 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ _.each(value, function(command) { var obj = {values: command}; obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix); + obj.defaults = {}; self.dataset.to_create.push(obj); self.dataset.cache.push(_.clone(obj)); ids.push(obj.id); @@ -1913,11 +2006,7 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ var view = this.viewmanager.views[this.viewmanager.active_view].controller; if (this.viewmanager.active_view === "form") { var res = $.when(view.do_save()); - if (res === false) { - // ignore - } else if (res.isRejected()) { - throw "Save or create on one2many dataset is not supposed to fail."; - } else if (!res.isResolved()) { + if (!res.isResolved() && !res.isRejected()) { throw "Asynchronous get_value() is not supported in form view."; } return res; @@ -1931,9 +2020,8 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ }, validate: function() { this.invalid = false; - var self = this; - var view = self.viewmanager.views[self.viewmanager.active_view].controller; - if (self.viewmanager.active_view === "form") { + var view = this.viewmanager.views[this.viewmanager.active_view].controller; + if (this.viewmanager.active_view === "form") { for (var f in view.fields) { f = view.fields[f]; if (!f.is_valid()) { @@ -1970,6 +2058,7 @@ openerp.web.form.One2ManyListView = openerp.web.ListView.extend({ } else { var self = this; var pop = new openerp.web.form.SelectCreatePopup(this); + pop.on_default_get.add(self.dataset.on_default_get); pop.select_element(self.o2m.field.relation,{ initial_view: "form", alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, @@ -1979,7 +2068,8 @@ openerp.web.form.One2ManyListView = openerp.web.ListView.extend({ self.o2m.dataset.on_change(); }); }, - parent_view: self.o2m.view + parent_view: self.o2m.view, + form_view_options: {'not_interactible_on_create':true} }, self.o2m.build_domain(), self.o2m.build_context()); pop.on_select_elements.add_last(function() { self.o2m.reload_current_view(); @@ -1995,7 +2085,8 @@ openerp.web.form.One2ManyListView = openerp.web.ListView.extend({ parent_view: self.o2m.view, read_function: function() { return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments); - } + }, + form_view_options: {'not_interactible_on_create':true} }); pop.on_write.add(function(id, data) { self.o2m.dataset.write(id, data, {}, function(r) { @@ -2006,10 +2097,10 @@ openerp.web.form.One2ManyListView = openerp.web.ListView.extend({ }); openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({ + template: 'FieldMany2Many', multi_selection: false, init: function(view, node) { this._super(view, node); - this.template = "FieldMany2Many"; this.list_id = _.uniqueId("many2many"); this.is_started = $.Deferred(); }, @@ -2110,6 +2201,8 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope * - alternative_form_view * - create_function (defaults to a naive saving behavior) * - parent_view + * - form_view_options + * - list_view_options */ select_element: function(model, options, domain, context) { var self = this; @@ -2131,6 +2224,7 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope this.dataset = new openerp.web.ReadOnlyDataSetSearch(this, this.model, this.context); this.dataset.parent_view = this.options.parent_view; + this.dataset.on_default_get.add(this.on_default_get); if (this.options.initial_view == "search") { this.setup_search_view(); } else { // "form" @@ -2173,7 +2267,7 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope }); self.view_list = new openerp.web.form.SelectCreateListView(self, self.dataset, false, - {'deletable': false}); + _.extend({'deletable': false}, self.options.list_view_options || {})); self.view_list.popup = self; self.view_list.appendTo($("#" + self.element_id + "_view_list")).pipe(function() { self.view_list.do_show(); @@ -2209,7 +2303,7 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope this.view_list.$element.hide(); } this.dataset.index = null; - this.view_form = new openerp.web.FormView(this, this.dataset, false); + this.view_form = new openerp.web.FormView(this, this.dataset, false, self.options.form_view_options); if (this.options.alternative_form_view) { this.view_form.set_embedded_view(this.options.alternative_form_view); } @@ -2253,7 +2347,8 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope this.on_select_elements(this.created_elements); } this.stop(); - } + }, + on_default_get: function(res) {} }); openerp.web.form.SelectCreateListView = openerp.web.ListView.extend({ @@ -2283,6 +2378,7 @@ openerp.web.form.FormOpenPopup = openerp.web.OldWidget.extend(/** @lends openerp * - auto_write (default true) * - read_function * - parent_view + * - form_view_options */ show_element: function(model, row_id, context, options) { this.model = model; @@ -2318,7 +2414,7 @@ openerp.web.form.FormOpenPopup = openerp.web.OldWidget.extend(/** @lends openerp on_write_completed: function() {}, setup_form_view: function() { var self = this; - this.view_form = new openerp.web.FormView(this, this.dataset, false); + this.view_form = new openerp.web.FormView(this, this.dataset, false, self.options.form_view_options); if (this.options.alternative_form_view) { this.view_form.set_embedded_view(this.options.alternative_form_view); } @@ -2351,9 +2447,9 @@ openerp.web.form.FormOpenDataset = openerp.web.ReadOnlyDataSetSearch.extend({ }); openerp.web.form.FieldReference = openerp.web.form.Field.extend({ + template: 'FieldReference', init: function(view, node) { this._super(view, node); - this.template = "FieldReference"; this.fields_view = { fields: { selection: { @@ -2488,10 +2584,7 @@ openerp.web.form.FieldBinary = openerp.web.form.Field.extend({ }); openerp.web.form.FieldBinaryFile = openerp.web.form.FieldBinary.extend({ - init: function(view, node) { - this._super(view, node); - this.template = "FieldBinaryFile"; - }, + template: 'FieldBinaryFile', set_value: function(value) { this._super.apply(this, arguments); var show_value = (value != null && value !== false) ? value : ''; @@ -2519,10 +2612,7 @@ openerp.web.form.FieldBinaryFile = openerp.web.form.FieldBinary.extend({ }); openerp.web.form.FieldBinaryImage = openerp.web.form.FieldBinary.extend({ - init: function(view, node) { - this._super(view, node); - this.template = "FieldBinaryImage"; - }, + template: 'FieldBinaryImage', start: function() { this._super.apply(this, arguments); this.$image = this.$element.find('img.oe-binary-image'); @@ -2597,6 +2687,93 @@ openerp.web.form.FieldStatus = openerp.web.form.Field.extend({ } }); +openerp.web.form.WidgetNotebookReadonly = openerp.web.form.WidgetNotebook.extend({ + template: 'WidgetNotebook.readonly' +}); +openerp.web.form.FieldReadonly = openerp.web.form.Field.extend({ + +}); +openerp.web.form.FieldCharReadonly = openerp.web.form.FieldReadonly.extend({ + template: 'FieldChar.readonly', + set_value: function (value) { + this._super.apply(this, arguments); + var show_value = openerp.web.format_value(value, this, ''); + this.$element.find('div').text(show_value); + return show_value; + } +}); +openerp.web.form.FieldURIReadonly = openerp.web.form.FieldCharReadonly.extend({ + template: 'FieldURI.readonly', + scheme: null, + set_value: function (value) { + var displayed = this._super.apply(this, arguments); + this.$element.find('a') + .attr('href', this.scheme + ':' + displayed) + .text(displayed); + } +}); +openerp.web.form.FieldEmailReadonly = openerp.web.form.FieldURIReadonly.extend({ + scheme: 'mailto' +}); +openerp.web.form.FieldUrlReadonly = openerp.web.form.FieldURIReadonly.extend({ + set_value: function (value) { + var s = /(\w+):(\.+)/.match(value); + if (!(s[0] === 'http' || s[0] === 'https')) { return; } + this.scheme = s[0]; + this._super(s[1]); + } +}); +openerp.web.form.FieldBooleanReadonly = openerp.web.form.FieldCharReadonly.extend({ + set_value: function (value) { + this._super(value ? '\u2714' : '\u2718'); + } +}); +openerp.web.form.FieldSelectionReadonly = openerp.web.form.FieldReadonly.extend({ + template: 'FieldChar.readonly', + init: function(view, node) { + // lifted straight from r/w version + var self = this; + this._super(view, node); + this.values = this.field.selection; + _.each(this.values, function(v, i) { + if (v[0] === false && v[1] === '') { + self.values.splice(i, 1); + } + }); + this.values.unshift([false, '']); + }, + set_value: function (value) { + value = value === null ? false : value; + value = value instanceof Array ? value[0] : value; + var option = _(this.values) + .detect(function (record) { return record[0] === value; }); + this._super(value); + this.$element.find('div').text(option ? option[1] : this.values[0][1]); + } +}); +openerp.web.form.FieldMany2OneReadonly = openerp.web.form.FieldCharReadonly.extend({ + set_value: function (value) { + value = value || null; + this.invalid = false; + var self = this; + this.tmp_value = value; + self.update_dom(); + self.on_value_changed(); + var real_set_value = function(rval) { + self.$element.find('div').text(rval ? rval[1] : ''); + }; + if(typeof(value) === "number") { + var dataset = new openerp.web.DataSetStatic( + this, this.field.relation, self.build_context()); + dataset.name_get([value], function(data) { + real_set_value(data[0]); + }).fail(function() {self.tmp_value = undefined;}); + } else { + setTimeout(function() {real_set_value(value);}, 0); + } + } +}); + /** * Registry of form widgets, called by :js:`openerp.web.FormView` */ @@ -2629,6 +2806,22 @@ openerp.web.form.widgets = new openerp.web.Registry({ 'binary': 'openerp.web.form.FieldBinaryFile', 'statusbar': 'openerp.web.form.FieldStatus' }); +openerp.web.form.readonly = openerp.web.form.widgets.clone({ + 'notebook': 'openerp.web.form.WidgetNotebookReadonly', + 'char': 'openerp.web.form.FieldCharReadonly', + 'email': 'openerp.web.form.FieldEmailReadonly', + 'url': 'openerp.web.form.FieldUrlReadonly', + 'text': 'openerp.web.form.FieldCharReadonly', + 'text_wiki' : 'openerp.web.form.FieldCharReadonly', + 'date': 'openerp.web.form.FieldCharReadonly', + 'datetime': 'openerp.web.form.FieldCharReadonly', + 'selection' : 'openerp.web.form.FieldSelectionReadonly', + 'many2one': 'openerp.web.form.FieldMany2OneReadonly', + 'boolean': 'openerp.web.form.FieldBooleanReadonly', + 'float': 'openerp.web.form.FieldCharReadonly', + 'integer': 'openerp.web.form.FieldCharReadonly', + 'float_time': 'openerp.web.form.FieldCharReadonly' +}); }; diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index f8a89cd34a1..8a9deaab228 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -49,6 +49,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# this.model = dataset.model; this.view_id = view_id; this.previous_colspan = null; + this.colors = null; this.columns = []; @@ -75,6 +76,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# } self.compute_aggregates(); }); + }, /** * Retrieves the view's number of records per page (|| section) @@ -132,6 +134,31 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# return this.reload_view(null, null, true); }, /** + * Returns the color for the provided record in the current view (from the + * ``@colors`` attribute) + * + * @param {Record} record record for the current row + * @returns {String} CSS color declaration + */ + color_for: function (record) { + if (!this.colors) { return ''; } + var context = _.extend({}, record.attributes, { + uid: this.session.uid, + current_date: new Date().toString('yyyy-MM-dd') + // TODO: time, datetime, relativedelta + }); + for(var i=0, len=this.colors.length; i<len; ++i) { + var pair = this.colors[i], + color = pair[0], + expression = pair[1]; + if (py.evaluate(expression, context)) { + return 'color: ' + color + ';'; + } + // TODO: handle evaluation errors + } + return ''; + }, + /** * Called after loading the list view's description, sets up such things * as the view table's columns, renders the table itself and hooks up the * various table-level and row-level DOM events (action buttons, deletion @@ -159,6 +186,17 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# this.fields_view = data; this.name = "" + this.fields_view.arch.attrs.string; + if (this.fields_view.arch.attrs.colors) { + this.colors = _(this.fields_view.arch.attrs.colors.split(';')).chain() + .compact() + .map(function(color_pair) { + var pair = color_pair.split(':'), + color = pair[0], + expr = pair[1]; + return [color, py.parse(py.tokenize(expr)), expr]; + }).value(); + } + this.setup_columns(this.fields_view.fields, grouped); this.$element.html(QWeb.render("ListView", this)); @@ -415,8 +453,8 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# */ do_search: function (domains, contexts, groupbys) { return this.rpc('/web/session/eval_domain_and_context', { - domains: [this.dataset.get_domain()].concat(domains), - contexts: [this.dataset.get_context()].concat(contexts), + domains: _([this.dataset.get_domain()].concat(domains)).compact(), + contexts: _([this.dataset.get_context()].concat(contexts)).compact(), group_by_seq: groupbys }, $.proxy(this, 'do_actual_search')); }, @@ -541,6 +579,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# if (_.isEmpty(records)) { records = this.groups.get_records(); } + records = _(records).compact(); var count = 0, sums = {}; _(columns).each(function (column) { @@ -1235,6 +1274,9 @@ openerp.web.ListView.Groups = openerp.web.Class.extend( /** @lends openerp.web.L }, get_records: function () { if (_(this.children).isEmpty()) { + if (!this.datagroup.length) { + return; + } return { count: this.datagroup.length, values: this.datagroup.aggregates @@ -1351,10 +1393,10 @@ var Record = openerp.web.Class.extend(/** @lends Record# */{ * @returns {Object} record displayable in a form view */ toForm: function () { - var form_data = {}; - _(this.attributes).each(function (value, key) { - form_data[key] = {value: value}; - }); + var form_data = {}, attrs = this.attributes; + for(var k in attrs) { + form_data[k] = {value: attrs[k]}; + } return {data: form_data}; } diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js index b5a5a3d8c10..beb2c3ac0a9 100644 --- a/addons/web/static/src/js/view_list_editable.js +++ b/addons/web/static/src/js/view_list_editable.js @@ -182,9 +182,11 @@ openerp.web.list_editable = function (openerp) { self.edition_id = record_id; self.edition_form = _.extend(new openerp.web.ListEditableFormView(self, self.dataset, false), { form_template: 'ListView.row.form', - registry: openerp.web.list.form.widgets + registry: openerp.web.list.form.widgets, + $element: $new_row }); - self.edition_form.appendTo($("#" + $new_row.attr('id'))); + // HA HA + self.edition_form.appendTo(); $.when(self.edition_form.on_loaded(self.get_form_fields_view())).then(function () { // put in $.when just in case FormView.on_loaded becomes asynchronous $new_row.find('td') @@ -335,6 +337,9 @@ openerp.web.list_editable = function (openerp) { }); openerp.web.ListEditableFormView = openerp.web.FormView.extend({ - init_view: function() {} + init_view: function() {}, + _render_and_insert: function () { + return this.start(); + } }); }; diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index fa27e59c0af..be47cfc3364 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -86,7 +86,7 @@ db.web.ActionManager = db.web.Widget.extend({ console.log("Action manager can't handle action of type " + action.type, action); return; } - this[type](action, on_close); + return this[type](action, on_close); }, ir_actions_act_window: function (action, on_close) { if (action.target === 'new') { @@ -163,7 +163,7 @@ db.web.ViewManager = db.web.Widget.extend(/** @lends db.web.ViewManager# */{ */ init: function(parent, dataset, views) { this._super(parent); - this.model = dataset.model; + this.model = dataset ? dataset.model : undefined; this.dataset = dataset; this.searchview = null; this.active_view = null; @@ -184,7 +184,6 @@ db.web.ViewManager = db.web.Widget.extend(/** @lends db.web.ViewManager# */{ start: function() { this._super(); var self = this; - this.dataset.start(); this.$element.find('.oe_vm_switch button').click(function() { self.on_mode_switch($(this).data('view-type')); }); @@ -332,6 +331,7 @@ db.web.ViewManagerAction = db.web.ViewManager.extend(/** @lends oepnerp.web.View // dataset initialization will take the session from ``this``, so if we // do not have it yet (and we don't, because we've not called our own // ``_super()``) rpc requests will blow up. + this._super(parent, null, action.views); this.session = parent.session; this.action = action; var dataset = new db.web.DataSetSearch(this, action.res_model, action.context, action.domain); @@ -339,7 +339,7 @@ db.web.ViewManagerAction = db.web.ViewManager.extend(/** @lends oepnerp.web.View dataset.ids.push(action.res_id); dataset.index = 0; } - this._super(parent, dataset, action.views); + this.dataset = dataset; this.flags = this.action.flags || {}; if (action.res_model == 'board.board' && action.views.length == 1 && action.views) { // Not elegant but allows to avoid form chrome (pager, save/new @@ -470,7 +470,7 @@ db.web.ViewManagerAction = db.web.ViewManager.extend(/** @lends oepnerp.web.View * Intercept do_action resolution from children views */ on_action_executed: function () { - new db.web.DataSet(this, 'res.log') + return new db.web.DataSet(this, 'res.log') .call('get', [], this.do_display_log); }, /** @@ -770,28 +770,31 @@ db.web.View = db.web.Widget.extend(/** @lends db.web.View# */{ var self = this; var result_handler = function () { if (on_closed) { on_closed.apply(null, arguments); } - self.widget_parent.on_action_executed.apply(null, arguments); + return self.widget_parent.on_action_executed.apply(null, arguments); }; var handler = function (r) { var action = r.result; if (action && action.constructor == Object) { - action.context = action.context || {}; - _.extend(action.context, { - active_id: record_id || false, - active_ids: [record_id || false], - active_model: dataset.model + return self.rpc('/web/session/eval_domain_and_context', { + contexts: [dataset.get_context(), action.context || {}, { + active_id: record_id || false, + active_ids: [record_id || false], + active_model: dataset.model + }], + domains: [] + }).pipe(function (results) { + action.context = results.context + return self.do_action(action, result_handler); }); - action.context = new db.web.CompoundContext(dataset.get_context(), action.context); - self.do_action(action, result_handler); } else { - result_handler(); + return result_handler(); } }; var context = new db.web.CompoundContext(dataset.get_context(), action_data.context || {}); if (action_data.special) { - handler({result: {"type":"ir.actions.act_window_close"}}); + return handler({result: {"type":"ir.actions.act_window_close"}}); } else if (action_data.type=="object") { return dataset.call_button(action_data.name, [[record_id], context], handler); } else if (action_data.type=="action") { @@ -863,6 +866,8 @@ db.web.View = db.web.Widget.extend(/** @lends db.web.View# */{ console.log('Todo'); }, on_sidebar_import: function() { + var import_view = new db.web.DataImport(this, this.dataset); + import_view.start(); }, on_sidebar_export: function() { var export_view = new db.web.DataExport(this, this.dataset); diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 4d4efbed2a3..3f06270e0e0 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -38,11 +38,17 @@ </td> </tr> <tr> - <td valign="top" id="oe_secondary_menu" class="secondary_menu"> - </td> - <td valign="top" class="oe-application-container"> - <div id="oe_app" class="oe-application"> - </div> + <td colspan="2" valign="top" height="100%"> + <table cellspacing="0" cellpadding="0" border="0" height="100%" width="100%"> + <tr> + <td valign="top" id="oe_secondary_menu" class="secondary_menu"> + </td> + <td valign="top" class="oe-application-container"> + <div id="oe_app" class="oe-application"> + </div> + </td> + </tr> + </table> </td> </tr> <tr> @@ -236,7 +242,23 @@ </table> </form> </t> + +<t t-name="Login_dblist"> + <select name="db"> + <t t-foreach="db_list" t-as="db"> + <t t-if="selected_db === db"> + <option t-att-value="db" selected="true"> + <t t-esc="db"/></option> + </t> + <t t-if="selected_db !== db"> + <option t-att-value="db"><t t-esc="db"/></option> + </t> + </t> + </select> +</t> + <t t-name="Login"> + <div> <form class="oe_forms"> <fieldset> <legend style=""> @@ -247,33 +269,18 @@ <tr> <td><label for="db">Database:</label></td> <td> - <t t-if="!db_list"> - <input type="text" name="db" t-att-value="selected_db || ''" autofocus="true"/> - </t> - <t t-if="db_list"> - <select name="db"> - <t t-foreach="db_list" t-as="db"> - <t t-if="selected_db === db"> - <option t-att-value="db" selected="true"> - <t t-esc="db"/></option> - </t> - <t t-if="selected_db !== db"> - <option t-att-value="db"><t t-esc="db"/></option> - </t> - </t> - </select> - </t> + <input type="text" name="db" t-att-value="widget.selected_db || ''" autofocus="true"/> </td> </tr> <tr> <td><label for="login">User:</label></td> <td><input type="text" name="login" - t-att-value="selected_login || ''" autofocus="true"/></td> + t-att-value="widget.selected_login || ''" autofocus="true"/></td> </tr> <tr> <td><label for="password">Password:</label></td> <td><input type="password" name="password" - t-att-value="selected_password || ''"/></td> + t-att-value="widget.selected_password || ''"/></td> </tr> <tr> <td></td> @@ -324,6 +331,7 @@ </table> </div> + </div> </t> <t t-name="Header"> <div> @@ -342,22 +350,12 @@ <li> <a t-att-href="'/' + widget.qs" title="Home" class="home"><img src="/web/static/src/img/header-home.png" width="16" height="16" border="0"/></a> </li> - <!-- - <li> - <a href="#requests" title="Requests" class="requests"><img src="/web/static/src/img/header-requests.png" width="16" height="16" border="0"/><small>1</small></a> - </li> - --> <li class="preferences"> - <a href="#preferences" title="Preferences" class="preferences"><img src="/web/static/src/img/header-preferences.png" width="16" height="16" border="0"/></a> - </li> - <li> - <a href="#about" title="About" class="about"><img src="/web/static/src/img/header-about.png" width="16" height="16" border="0"/></a> + <a href="javascript:void(0)" title="Preferences" class="preferences"><img src="/web/static/src/img/header-preferences.png" width="16" height="16" border="0"/></a> </li> - <!-- <li> - <a href="http://doc.openerp.com/v6.0/book?version=$version" title="Help" target="_blank" class="help"><img src="/web/static/src/img/header-help.png" width="16" height="16" border="0"/></a> + <a href="javascript:void(0)" title="About" class="about"><img src="/web/static/src/img/header-about.png" width="16" height="16" border="0"/></a> </li> - --> </ul> <div class="block"> <a href="#logout" class="logout">LOGOUT</a> @@ -374,45 +372,39 @@ </ul> <t t-name="Menu"> <table align="center"> - <tr> - <t t-foreach="data.children" t-as="menu"> - <td> - <a href="#" t-att-data-menu="menu.id"> - <t t-esc="menu.name"/> - </a> - </td> - </t> - </tr> + <tr> + <td t-foreach="widget.data.data.children" t-as="menu"> + <a href="#" t-att-data-menu="menu.id"> + <t t-esc="menu.name"/> + </a> + </td> + </tr> </table> </t> <t t-name="Menu.secondary"> - <div style="display: none" class="menu_accordion" t-att-data-menu-parent="menu.id"> + <div t-attf-class="oe_toggle_secondary_menu"> + <span class="oe_menu_fold" title="Fold menu">&laquo;</span> + <span class="oe_menu_unfold" title="Unfold menu">&raquo;</span> + </div> + <div t-foreach="widget.data.data.children" t-as="menu" style="display: none" class="oe_secondary_menu" t-att-data-menu-parent="menu.id"> <t t-foreach="menu.children" t-as="menu"> - <t t-set="header">h3</t> + <t t-set="classname">oe_secondary_menu_item</t> + <t t-set="level" t-value="0"/> <t t-call="Menu.secondary.children"/> </t> </div> </t> <t t-name="Menu.secondary.children"> - <<t t-esc="header"/>> - <a href="#" t-attf-id="menu_#{menu.id}"> - <span><t t-esc="menu.name"/></span> - </a> - </<t t-esc="header"/>> - <div class="menu_content"> + <t t-set="level" t-value="level + 1"/> + <a href="#" t-att-id="menu.children.length ? 'menu_' + menu.id : undefined" + t-att-class="classname + (menu.children.length ? ' submenu' : ' leaf') + (menu_first and level == 1 ? ' opened' : '')" + t-att-data-menu="menu.children.length ? undefined : menu.id"> + <span t-attf-style="padding-left: #{(level - 2) * 20}px"> <t t-esc="menu.name"/></span> + </a> + <div t-attf-class="oe_secondary_submenu" t-if="menu.children.length" t-att-style="menu_first and level == 1 ? undefined : 'display: none'"> <t t-foreach="menu.children" t-as="menu"> - <t t-if="!menu.children.length"> - <a href="#" class="leaf" t-att-data-menu="menu.id"> - <span><t t-esc="menu.name"/></span> - </a> - </t> - <t t-if="menu.children.length"> - <div class="submenu_accordion"> - <t t-call="Menu.secondary.children"> - <t t-set="header">h4</t> - </t> - </div> - </t> + <t t-set="classname">oe_secondary_submenu_item</t> + <t t-call="Menu.secondary.children"/> </t> </div> </t> @@ -624,7 +616,8 @@ </t> </t> <tr t-name="ListView.row" t-att-class="row_parity" - t-att-data-id="record.get('id')"> + t-att-data-id="record.get('id')" + t-att-style="view.color_for(record)"> <t t-foreach="columns" t-as="column"> <td t-if="column.meta"> @@ -658,7 +651,7 @@ <t t-raw="frame.render()"/> </t> <t t-name="FormView"> - <div class="oe_form_header" t-att-id="view.element_id + '_header'"> + <div class="oe_form_header"> <div class="oe_form_buttons" t-if="view.options.action_buttons !== false"> <!--<button type="button" class="oe_form_button_save"> <span class="oe_form_on_update">Save</span> @@ -671,6 +664,7 @@ <!--<button type="button" class="oe_form_button_cancel">Cancel</button>--> <button type="button" class="oe_form_button_new">New</button> <button type="button" class="oe_form_button_duplicate oe_form_on_update">Duplicate</button> + <button type="button" class="oe_form_button_toggle">Readonly/Editable</button> </div> <div class="oe_form_pager" t-if="view.options.pager !== false"> <button type="button" data-pager-action="first">First</button> @@ -730,8 +724,7 @@ t-att-width="td.width" t-att-nowrap="td.nowrap or td.is_field_m2o? 'true' : undefined" t-att-valign="td.table ? 'top' : undefined" - t-att-id="td.element_id" - t-attf-class="oe_form_frame_cell #{td.classname}" + t-attf-class="oe_form_frame_cell #{td.classname} #{td.element_class}" > <t t-raw="td.render()"/> </td> @@ -741,8 +734,8 @@ </t> <t t-name="WidgetNotebook"> <ul> - <li t-foreach="widget.pages" t-as="page" t-att-id="page.element_tab_id"> - <a t-att-href="'#' + page.element_id"> + <li t-foreach="widget.pages" t-as="page"> + <a href="#"> <t t-esc="page.string"/> </a> </li> @@ -752,17 +745,23 @@ </t> </t> <t t-name="WidgetNotebookPage"> - <div t-att-id="widget.element_id"> + <div> <t t-call="WidgetFrame"/> </div> </t> +<t t-name="WidgetNotebook.readonly"> + <t t-foreach="widget.pages" t-as="page"> + <h3><t t-esc="page.string"/></h3> + <t t-raw="page.render()"/> + </t> +</t> <t t-name="WidgetSeparator"> <div t-if="widget.orientation !== 'vertical'" t-att-class="'separator ' + widget.orientation"> <t t-esc="widget.string"/> </div> </t> <t t-name="WidgetLabel"> - <label t-att-for="widget.element_id + '_field'" + <label t-att-for="widget.element_id" t-att-class="'oe_label' + (widget.help ? '_help' : '')" t-att-title="widget.help"> <t t-esc="widget.string"/> @@ -776,12 +775,22 @@ <t t-name="FieldChar"> <input type="text" size="1" t-att-name="widget.name" - t-att-id="widget.element_id + '_field'" - t-att-class="'field_' + widget.type" + t-att-id="widget.element_id" + t-attf-class="field_#{widget.type} #{widget.element_class}" t-attf-style="width: #{widget.field.translate ? '99' : '100'}%" /> <img class="oe_field_translate" t-if="widget.field.translate" src="/web/static/src/img/icons/terp-translate.png" width="16" height="16" border="0"/> </t> +<t t-name="FieldChar.readonly"> + <div + t-att-id="widget.element_id" + t-attf-class="field_#{widget.type} #{widget.element_class}" + t-attf-style="width: #{widget.field.translate ? '99' : '100'}%"> + </div> +</t> +<t t-name="FieldURI.readonly"> + <a href="#">#</a> +</t> <t t-name="FieldEmail"> <table cellpadding="0" cellspacing="0" border="0" width="100%"> <tr> @@ -813,26 +822,28 @@ <t t-name="FieldText"> <textarea rows="6" t-att-name="widget.name" - t-att-id="widget.element_id + '_field'" - t-att-class="'field_' + widget.type" + t-att-id="widget.element_id" + t-attf-class="field_#{widget.type} #{widget.element_class}" t-attf-style="width: #{widget.field.translate ? '99' : '100'}%" ></textarea> <img class="oe_field_translate" t-if="widget.field.translate" src="/web/static/src/img/icons/terp-translate.png" width="16" height="16" border="0"/> </t> -<t t-name="FieldDate"> - <t t-call="FieldChar"/> - <img class="oe_input_icon oe_datepicker_trigger" src="/web/static/src/img/ui/field_calendar.png" - title="Select date" width="16" height="16" border="0"/> - <div class="oe_datepicker ui-widget-content ui-corner-all" style="display: none; position: absolute; z-index: 1;"> - <div class="oe_datepicker_container"/> - <button type="button" class="oe_datepicker_close ui-state-default ui-priority-primary ui-corner-all" style="float: right;">Done</button> +<t t-name="web.datetimepicker"> + <div class="oe_datepicker_root"> + <input type="text" size="1" style="width: 100%"/> + <img class="oe_input_icon oe_datepicker_trigger" src="/web/static/src/img/ui/field_calendar.png" + title="Select date" width="16" height="16" border="0"/> + <div class="oe_datepicker ui-widget-content ui-corner-all" style="display: none; position: absolute; z-index: 1;"> + <div class="oe_datepicker_container"/> + <button type="button" class="oe_datepicker_close ui-state-default ui-priority-primary ui-corner-all" style="float: right;">Done</button> + </div> </div> </t> <t t-name="FieldSelection"> <select t-att-name="widget.name" t-att-id="widget.element_id + '_field'" - t-att-class="'field_' + widget.type" + t-attf-class="field_#{widget.type} #{widget.element_class}" style="width: 100%"> <t t-foreach="widget.values" t-as="option"> <option><t t-esc="option[1]"/></option> @@ -840,9 +851,9 @@ </select> </t> <t t-name="FieldMany2One"> - <div t-att-id="widget.element_id" class="oe-m2o"> - <input t-att-id="widget.element_id + '_input'" type="text" size="1" style="width: 100%;"/> - <span class="oe-m2o-drop-down-button" t-att-id="widget.element_id + '_drop_down'"> + <div t-att-class="widget.element_class" class="oe-m2o"> + <input type="text" size="1" style="width: 100%;"/> + <span class="oe-m2o-drop-down-button"> <img src="/web/static/src/img/down-arrow.png" /></span> <span class="oe-m2o-cm-button" t-att-id="widget.name + '_open'"> <img src="/web/static/src/img/icons/gtk-index.png"/></span> @@ -865,8 +876,6 @@ </ul> </t> <t t-name="FieldOne2Many"> - <div t-att-id="widget.element_id"> - </div> </t> <t t-name="FieldMany2Many"> <div t-att-id="widget.list_id"></div> @@ -874,10 +883,10 @@ <t t-name="FieldReference"> <table border="0" width="100%" cellpadding="0" cellspacing="0" class="oe_frame oe_forms"> <tr> - <td t-att-id="widget.selection.element_id" class="oe_form_frame_cell oe_form_selection"> + <td t-attf-class="oe_form_frame_cell oe_form_selection #{widget.selection.element_class}"> <t t-raw="widget.selection.render()"/> </td> - <td t-att-id="widget.m2o.element_id" class="oe_form_frame_cell oe_form_many2one" nowrap="true"> + <td class="oe_form_frame_cell oe_form_many2one #{widget.selection.element_class}" nowrap="true"> <t t-raw="widget.m2o.render()"/> </td> </tr> @@ -887,7 +896,7 @@ <input type="checkbox" t-att-name="widget.name" t-att-id="widget.element_id + '_field'" - t-att-class="'field_' + widget.type"/> + t-attf-class="field_#{widget.type} #{widget.element_class}"/> </t> <t t-name="FieldProgressBar"> <div t-opentag="true" class="oe-progressbar"> @@ -902,7 +911,7 @@ t-att-border="widget.readonly ? 0 : 1" t-att-id="widget.element_id + '_field'" t-att-name="widget.name" - t-att-class="'field_' + widget.type" + t-attf-class="field_#{widget.type} #{widget.element_class}" t-att-width="widget.node.attrs.img_width || widget.node.attrs.width" t-att-height="widget.node.attrs.img_height || widget.node.attrs.height" /> @@ -950,7 +959,7 @@ <input type="text" size="1" t-att-name="widget.name" t-att-id="widget.element_id + '_field'" - t-att-class="'field_' + widget.type" style="width: 100%" + t-attf-class="field_#{widget.type} #{widget.element_class}" style="width: 100%" /> </td> <td class="oe-binary" nowrap="true"> @@ -995,7 +1004,7 @@ </t> <t t-name="WidgetButton"> <button type="button" - t-att-id="widget.element_id + '_button'" + t-attf-class="#{widget.element_class}" t-att-title="widget.help" style="width: 100%" class="button"> <img t-if="widget.node.attrs.icon" t-att-src="'/web/static/src/img/icons/' + widget.node.attrs.icon + '.png'" width="16" height="16"/> @@ -1070,6 +1079,18 @@ <t t-if="filters.length" t-raw="filters.render(defaults)"/> </div> </t> +<t t-name="SearchView.date"> + <label t-att-class="'oe_label' + (attrs.help ? '_help' : '')" + t-att-title="attrs.help" + t-att-for="element_id"> + <t t-esc="attrs.string || attrs.name"/> + <span t-if="attrs.help">?</span> + </label> + <div style="white-space: nowrap;"> + <span t-att-id="element_id"></span> + <t t-if="filters.length" t-raw="filters.render(defaults)"/> + </div> +</t> <t t-name="SearchView.field.selection"> <label t-att-title="attrs.help" t-att-class="'oe_label' + (attrs.help ? '_help' : '')" @@ -1161,11 +1182,8 @@ <t t-name="SearchView.extended_search.proposition.char"> <input t-att-id="element_id" class="field_char"/> </t> -<t t-name="SearchView.extended_search.proposition.datetime"> - <input t-att-id="element_id" class="field_datetime"/> -</t> -<t t-name="SearchView.extended_search.proposition.date"> - <input t-att-id="element_id" class="field_date"/> +<t t-name="SearchView.extended_search.proposition.empty"> + <span t-att-id="element_id"></span> </t> <t t-name="SearchView.extended_search.proposition.integer"> <input type="number" t-att-id="element_id" class="field_integer" step="1"/> @@ -1437,6 +1455,79 @@ </table> </form> </t> + +<t t-name="ImportView"> + <a id="importview" href="javascript: void(0)" style="text-decoration: none;color: #3D3D3D;">Import</a> +</t> +<t t-name="ImportDataView"> +<form name="import_data" id="import_data" action="" method="post" enctype="multipart/form-data"> + <input type="hidden" name="session_id" t-att-value="session.session_id"/> + <h2 class="separator horizontal">1. Import a .CSV file</h2> + <p>Select a .CSV file to import. If you need a sample of file to import, + you should use the export tool with the "Import Compatible" option. + </p> + <p> + <label for="csvfile">CSV File:</label> + <input type="file" id="csvfile" size="50" name="csvfile"/> + </p> + <h2 class="separator horizontal">2. Check your file format</h2> + <div id="result"></div> + <fieldset> + <legend style="cursor:pointer;">Import Options</legend> + <table style="display:none"> + <tr> + <td colspan="4"> + <label for="file_has_headers">Does your file have titles?</label> + <input type="checkbox" checked="checked" + id="file_has_headers"/> + </td> + </tr> + <tr> + <td><label for="csv_separator">Separator:</label></td> + <td><input type="text" name="csvsep" id="csv_separator" value=","/></td> + <td><label for="csv_delimiter">Delimiter:</label></td> + <td><input type="text" name="csvdel" id="csv_delimiter" value='"'/></td> + </tr> + <tr> + <td><label for="csv_encoding">Encoding:</label></td> + <td> + <select name="csvcode" id="csv_encoding"> + <option value="utf-8">UTF-8</option> + <option value="latin1">Latin 1</option> + </select> + </td> + <td><label for="csv_skip" title="For use if CSV files have titles on multiple lines, skips more than a single line during import"> + Lines to skip<sup>?</sup>:</label></td> + <td><input type="number" id="csv_skip" value="0" min="0"/></td> + </tr> + </table> + </fieldset> +</form> +</t> +<table t-name="ImportView.result" + class="oe_import_grid" width="100%" style="margin: 5px 0;"> + <tr t-if="headers" class="oe_import_grid-header"> + <td t-foreach="headers" t-as="header" class="oe_import_grid-cell"> + <t t-esc="header"/></td> + </tr> + <tr> + <td t-foreach="records[0]" t-as="column"> + <input class="sel_fields"/> + </td> + </tr> + <tr t-foreach="records" t-as="record" class="oe_import_grid-row"> + <td t-foreach="record" t-as="cell" class="oe_import_grid-cell"> + <t t-esc="cell"/></td> + </tr> +</table> +<t t-name="ImportView.error"> + <p style="white-space:pre-line;">The import failed due to:<t t-esc="error.message"/></p> + <t t-if="error.preview"> + <p>Here is a preview of the file we could not import:</p> + <pre><t t-esc="error.preview"/></pre> + </t> +</t> + <t t-name="About-Page"> <div> <h1>OpenERP Web</h1> diff --git a/addons/web_calendar/static/src/js/calendar.js b/addons/web_calendar/static/src/js/calendar.js index e590280b933..5de601e0921 100644 --- a/addons/web_calendar/static/src/js/calendar.js +++ b/addons/web_calendar/static/src/js/calendar.js @@ -35,6 +35,7 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({ }, stop: function() { scheduler.clearAll(); + this._super(); }, on_loaded: function(data) { this.fields_view = data; diff --git a/addons/web_dashboard/static/src/js/dashboard.js b/addons/web_dashboard/static/src/js/dashboard.js index 008d03d1548..4672492821d 100644 --- a/addons/web_dashboard/static/src/js/dashboard.js +++ b/addons/web_dashboard/static/src/js/dashboard.js @@ -301,7 +301,7 @@ openerp.web_dashboard.ConfigOverview = openerp.web.View.extend({ .then(this.on_records_loaded); }, on_records_loaded: function (read_response, progress_response) { - var records = read_response[0].records, + var records = read_response, progress = progress_response[0]; var grouped_todos = _(records).chain() diff --git a/addons/web_default_home/static/src/js/home.js b/addons/web_default_home/static/src/js/home.js index ed8561bc44d..67964351873 100644 --- a/addons/web_default_home/static/src/js/home.js +++ b/addons/web_default_home/static/src/js/home.js @@ -94,8 +94,10 @@ openerp.web_default_home = function (openerp) { }) }, install_module: function (module_name) { + var self = this; var Modules = new openerp.web.DataSetSearch( - this, 'ir.module.module', null, [['name', '=', module_name], ['state', '=', 'uninstalled']]); + this, 'ir.module.module', null, + [['name', '=', module_name], ['state', '=', 'uninstalled']]); var Upgrade = new openerp.web.DataSet(this, 'base.module.upgrade'); $.blockUI({message:'<img src="/web/static/src/img/throbber2.gif">'}); @@ -105,13 +107,21 @@ openerp.web_default_home = function (openerp) { [_.pluck(records, 'id'), 'to install', ['uninstalled']], function () { Upgrade.call('upgrade_module', [[]], function () { - $.unblockUI(); - // TODO: less brutal reloading - window.location.reload(true); + self.run_configuration_wizards(); }); } ) }); + }, + run_configuration_wizards: function () { + var self = this; + new openerp.web.DataSet(this, 'res.config').call('start', [[]], function (action) { + $.unblockUI(); + self.do_action(action, function () { + // TODO: less brutal reloading + window.location.reload(true); + }); + }); } }); }; diff --git a/addons/web_graph/__init__.py b/addons/web_graph/__init__.py index 53e20f29429..e69de29bb2d 100644 --- a/addons/web_graph/__init__.py +++ b/addons/web_graph/__init__.py @@ -1,2 +0,0 @@ -#!/usr/bin/python -import controllers
\ No newline at end of file diff --git a/addons/web_graph/__openerp__.py b/addons/web_graph/__openerp__.py index 4a1c4a6576f..03839744480 100644 --- a/addons/web_graph/__openerp__.py +++ b/addons/web_graph/__openerp__.py @@ -3,7 +3,7 @@ "version": "2.0", "depends": ['web'], "js": [ - "static/lib/dhtmlxGraph/codebase/dhtmlxchart.js", + "static/lib/dhtmlxGraph/codebase/dhtmlxchart_debug.js", "static/src/js/graph.js"], "css": ["static/lib/dhtmlxGraph/codebase/dhtmlxchart.css"], "active": True diff --git a/addons/web_graph/static/src/js/graph.js b/addons/web_graph/static/src/js/graph.js index 2a493b89166..08ceeddc2d9 100644 --- a/addons/web_graph/static/src/js/graph.js +++ b/addons/web_graph/static/src/js/graph.js @@ -19,11 +19,14 @@ openerp.web_graph.GraphView = openerp.web.View.extend({ init: function(parent, dataset, view_id) { this._super(parent); - this.view_manager = parent; this.dataset = dataset; - this.dataset_index = 0; - this.model = this.dataset.model; this.view_id = view_id; + + this.first_field = null; + this.abscissa = null; + this.ordinate = null; + this.columns = []; + this.group_field = null; }, do_show: function () { // TODO: re-trigger search @@ -33,348 +36,265 @@ openerp.web_graph.GraphView = openerp.web.View.extend({ this.$element.hide(); }, start: function() { + var self = this; this._super(); - return this.rpc("/web_graph/graphview/load", {"model": this.model, "view_id": this.view_id}, this.on_loaded); + return $.when( + this.dataset.call('fields_get', []), + this.rpc('/web/view/load', { + model: this.dataset.model, + view_id: this.view_id, + view_type: 'graph' + })).then(function (fields_result, view_result) { + self.fields = fields_result[0]; + self.fields_view = view_result[0]; + self.on_loaded(); + }); + }, + /** + * Returns all object fields involved in the graph view + */ + list_fields: function () { + var fs = [this.abscissa]; + fs.push.apply(fs, _(this.columns).pluck('name')); + if (this.group_field) { + fs.push(this.group_field); + } + return fs; }, - on_loaded: function(data) { - this.all_fields = data.all_fields; - this.fields_view = data.fields_view; - this.name = this.fields_view.name || this.fields_view.arch.attrs.string; - this.view_id = this.fields_view.view_id; + on_loaded: function() { this.chart = this.fields_view.arch.attrs.type || 'pie'; - this.fields = this.fields_view.fields; - this.chart_info_fields = []; - this.operator_field = ''; - this.operator_field_one = ''; - this.operator = []; - this.group_field = []; - this.orientation = this.fields_view.arch.attrs.orientation || ''; - this.elem_id = this.$element[0]['id']; + this.orientation = this.fields_view.arch.attrs.orientation || 'vertical'; _.each(this.fields_view.arch.children, function (field) { - if (field.attrs.operator) { - this.operator.push(field.attrs.name); - } - else if (field.attrs.group) { - this.group_field.push(field.attrs.name); - } - else { - this.chart_info_fields.push(field.attrs.name); + var attrs = field.attrs; + if (attrs.group) { + this.group_field = attrs.name; + } else if(!this.abscissa) { + this.first_field = this.abscissa = attrs.name; + } else { + this.columns.push({ + name: attrs.name, + operator: attrs.operator || '+' + }); } }, this); + this.ordinate = this.columns[0].name; - this.operator_field = this.operator[0]; - if(this.operator.length > 1){ - this.operator_field_one = this.operator[1]; - } - if(this.operator == ''){ - this.operator_field = this.chart_info_fields[1]; - } - this.chart_info = this.chart_info_fields[0]; - this.x_title = this.fields[this.chart_info_fields[0]]['string']; - this.y_title = this.fields[this.operator_field]['string']; - this.load_chart(); + this.dataset.read_slice( + this.list_fields(), {}, $.proxy(this, 'schedule_chart')); }, - - load_chart: function(data) { - var self = this; - var domain = false; - if(data){ - this.x_title = this.all_fields[this.chart_info_fields]['string']; - this.y_title = this.all_fields[this.operator_field]['string']; - self.schedule_chart(data); - }else{ - if(! _.isEmpty(this.view_manager.dataset.domain)){ - domain = this.view_manager.dataset.domain; - }else if(! _.isEmpty(this.view_manager.action.domain)){ - domain = this.view_manager.action.domain; - } - this.dataset.domain = domain; - this.dataset.context = this.view_manager.dataset.context; - this.dataset.read_slice(_(this.fields).keys(),{}, function(res) { - self.schedule_chart(res); - }); - } - }, - schedule_chart: function(results) { - this.$element.html(QWeb.render("GraphView", {"fields_view": this.fields_view, "chart": this.chart,'elem_id': this.elem_id})); + var self = this; + this.$element.html(QWeb.render("GraphView", { + "fields_view": this.fields_view, + "chart": this.chart, + 'element_id': this.element_id + })); - _.each(results, function (result) { - _.each(result, function (field_value, field_name) { - if (typeof field_value == 'object') { - result[field_name] = field_value[field_value.length - 1]; + var fields = _(this.columns).pluck('name').concat([this.abscissa]); + if (this.group_field) { fields.push(this.group_field); } + // transform search result into usable records (convert from OpenERP + // value shapes to usable atomic types + var records = _(results).map(function (result) { + var point = {}; + _(result).each(function (value, field) { + if (!_(fields).contains(field)) { return; } + if (value === false) { point[field] = false; return; } + switch (self.fields[field].type) { + case 'selection': + point[field] = _(self.fields[field].selection).detect(function (choice) { + return choice[0] === value; + })[1]; + break; + case 'many2one': + point[field] = value[1]; + break; + case 'integer': case 'float': case 'char': + case 'date': case 'datetime': + point[field] = value; + break; + default: + throw new Error( + "Unknown field type " + self.fields[field].type + + "for field " + field + " (" + value + ")"); } - if (typeof field_value == 'string') { - var choices = this.all_fields[field_name]['selection']; - _.each(choices, function (choice) { - if (field_value == choice[0]) { - result[field_name] = choice; - } - }); - } - }, this); - }, this); + }); + return point; + }); + // aggregate data, because dhtmlx is crap. Aggregate on abscissa field, + // leave split on group field => max m*n records where m is the # of + // values for the abscissa and n is the # of values for the group field + var graph_data = []; + _(records).each(function (record) { + var abscissa = record[self.abscissa], + group = record[self.group_field]; + var r = _(graph_data).detect(function (potential) { + return potential[self.abscissa] === abscissa + && (!self.group_field + || potential[self.group_field] === group); + }); + var datapoint = r || {}; - var graph_data = {}; - _.each(results, function (result) { - var group_key = []; - if(this.group_field.length){ - _.each(this.group_field, function (res) { - result[res] = (typeof result[res] == 'object') ? result[res][1] : result[res]; - group_key.push(result[res]); - }); - }else{ - group_key.push(result[this.group_field]); - } - var column_key = result[this.chart_info_fields] + "_" + group_key; - var column_descriptor = {}; - if (graph_data[column_key] == undefined) { - column_descriptor[this.operator_field] = result[this.operator_field]; - if (this.operator.length > 1) { - column_descriptor[this.operator_field_one] = result[this.operator_field_one]; - } - column_descriptor[this.chart_info_fields] = result[this.chart_info_fields]; - if(this.group_field.length){ - _.each(this.group_field, function (res) { - column_descriptor[res] = (typeof result[res] == 'object') ? result[res][1] : result[res]; - }); + datapoint[self.abscissa] = abscissa; + if (self.group_field) { datapoint[self.group_field] = group; } + _(self.columns).each(function (column) { + var val = record[column.name], + aggregate = datapoint[column.name]; + switch(column.operator) { + case '+': + datapoint[column.name] = (aggregate || 0) + val; + return; + case '*': + datapoint[column.name] = (aggregate || 1) * val; + return; + case 'min': + datapoint[column.name] = (aggregate || Infinity) > val + ? val + : aggregate; + return; + case 'max': + datapoint[column.name] = (aggregate || -Infinity) < val + ? val + : aggregate; } - } else { - column_descriptor = graph_data[column_key]; - column_descriptor[this.operator_field] += result[this.operator_field]; - if (this.operator.length > 1) { - column_descriptor[this.operator_field_one] += result[this.operator_field_one]; - } - } - graph_data[column_key] = column_descriptor; - }, this); + }); + + if (!r) { graph_data.push(datapoint); } + }); + graph_data = _(graph_data).sortBy(function (point) { + return point[self.abscissa] + '[[--]]' + point[self.group_field]; + }); if (this.chart == 'bar') { - return this.schedule_bar(_.values(graph_data)); + return this.schedule_bar(graph_data); } else if (this.chart == "pie") { - return this.schedule_pie(_.values(graph_data)); + return this.schedule_pie(graph_data); } }, schedule_bar: function(results) { var self = this; - var view_chart = ''; - var group_list = []; - var legend_list = []; - var newkey = '', newkey_one; - var string_legend = ''; - - if((self.group_field.length) && (this.operator.length <= 1)){ - view_chart = self.orientation == 'horizontal'? 'stackedBarH' : 'stackedBar'; - }else{ - view_chart = self.orientation == 'horizontal'? 'barH' : 'bar'; - } - - _.each(results, function (result) { - if ((self.group_field.length) && (this.operator.length <= 1)) { - var legend_key = ''; - _.each(self.group_field, function (res) { - result[res] = (typeof result[res] == 'object') ? result[res][1] : result[res]; - legend_key += result[res]; - }); - newkey = legend_key.replace(/\s+/g,'_').replace(/[^a-zA-Z 0-9]+/g,'_'); - string_legend = legend_key; - } else { - newkey = string_legend = "val"; - } - if (_.contains(group_list, newkey) && _.contains(legend_list, string_legend)) { - return; - } - group_list.push(newkey); - legend_list.push(string_legend); - - if (this.operator.length > 1) { - newkey_one = "val1"; - group_list.push(newkey_one); - legend_list.push(newkey_one); + var group_list, view_chart; + if (!this.group_field) { + view_chart = (this.orientation === 'horizontal') ? 'barH' : 'bar'; + group_list = _(this.columns).map(function (column, index) { + return { + group: column.name, + text: self.fields[column.name].string, + color: COLOR_PALETTE[index % (COLOR_PALETTE.length)] + } + }); + } else { + // dhtmlx handles clustered bar charts (> 1 column per abscissa + // value) and stacked bar charts (basically the same but with the + // columns on top of one another instead of side by side), but it + // does not handle clustered stacked bar charts + if (this.columns.length > 1) { + this.$element.text( + 'OpenERP Web does not support combining grouping and ' + + 'multiple columns in graph at this time.'); + throw new Error( + 'dhtmlx can not handle columns counts of that magnitude'); } - }, this); + // transform series for clustered charts into series for stacked + // charts + view_chart = (this.orientation === 'horizontal') + ? 'stackedBarH' : 'stackedBar'; + group_list = _(results).chain() + .pluck(this.group_field) + .uniq() + .map(function (value, index) { + return { + group: self.ordinate + '_' + + value.toLowerCase().replace(/\s/g, '_'), + text: value, + color: COLOR_PALETTE[index % COLOR_PALETTE.length] + }; + }).value(); - if (group_list.length <=1){ - group_list = []; - legend_list = []; - newkey = string_legend = "val"; - group_list.push(newkey); - legend_list.push(string_legend); + results = _(results).chain() + .groupBy(function (record) { return record[self.abscissa]; }) + .map(function (records) { + var r = {}; + // second argument is coerced to a str, no good for boolean + r[self.abscissa] = records[0][self.abscissa]; + _(records).each(function (record) { + var key = _.sprintf('%s_%s', + self.ordinate, + record[self.group_field].toLowerCase().replace(/\s/g, '_')); + r[key] = record[self.ordinate]; + }); + return r; + }) + .value(); } - var abscissa_data = {}; - _.each(results, function (result) { - var label = result[self.chart_info_fields], - section = {}; - if ((self.group_field.length) && (group_list.length > 1) && (self.operator.length <= 1)){ - var legend_key_two = ''; - _.each(self.group_field, function (res) { - result[res] = (typeof result[res] == 'object') ? result[res][1] : result[res]; - legend_key_two += result[res]; - }); - newkey = legend_key_two.replace(/\s+/g,'_').replace(/[^a-zA-Z 0-9]+/g,'_'); - }else{ - newkey = "val"; - } - if (abscissa_data[label] == undefined){ - section[self.chart_info_fields] = label; - _.each(group_list, function (group) { - section[group] = 0; - }); - } else { - section = abscissa_data[label]; - } - section[newkey] = result[self.operator_field]; - if (self.operator.length > 1){ - section[newkey_one] = result[self.operator_field_one]; - } - abscissa_data[label] = section; - }); - - //for legend color - var grp_color = _.map(legend_list, function (group_legend, index) { - var legend = {color: COLOR_PALETTE[index]}; - if (group_legend == "val"){ - legend['text'] = self.fields[self.operator_field]['string'] - }else if(group_legend == "val1"){ - legend['text'] = self.fields[self.operator_field_one]['string'] - }else{ - legend['text'] = group_legend; - } - return legend; - }); - - //for axis's value and title - var max,min,step; - var maximum,minimum; - if(_.isEmpty(abscissa_data)){ - max = 9; - min = 0; - step=1; - }else{ - var max_min = []; - _.each(abscissa_data, function (abscissa_datas) { - _.each(group_list, function(res){ - max_min.push(abscissa_datas[res]); - }); - }); - maximum = Math.max.apply(Math,max_min); - minimum = Math.min.apply(Math,max_min); - if (maximum == minimum){ - if (maximum == 0){ - max = 9; - min = 0; - step=1; - }else if(maximum > 0){ - max = maximum + (10 - maximum % 10); - min = 0; - step = Math.round(max/10); - }else{ - max = 0; - min = minimum - (10 + minimum % 10); - step = Math.round(Math.abs(min)/10); - } - } - } var abscissa_description = { - template: self.chart_info_fields, - title: "<b>"+self.x_title+"</b>" + title: "<b>" + this.fields[this.abscissa].string + "</b>", + template: function (obj) { + return obj[self.abscissa] || 'Undefined'; + } }; var ordinate_description = { lines: true, - title: "<b>"+self.y_title+"</b>", - start: min, - step: step, - end: max + title: "<b>" + this.fields[this.ordinate].string + "</b>" }; - var x_axis, y_axis, tooltip; - if (self.orientation == 'horizontal'){ - x_axis = ordinate_description; - y_axis = abscissa_description; - }else{ - x_axis = abscissa_description; - y_axis = ordinate_description; + var x_axis, y_axis; + if (self.orientation == 'horizontal') { + x_axis = ordinate_description; + y_axis = abscissa_description; + } else { + x_axis = abscissa_description; + y_axis = ordinate_description; } - tooltip = self.chart_info_fields; - + var bar_chart = new dhtmlXChart({ view: view_chart, - container: self.elem_id+"-barchart", - value:"#"+group_list[0]+"#", + container: this.element_id+"-barchart", + value:"#"+group_list[0].group+"#", gradient: "3d", border: false, width: 1024, tooltip:{ - template:"#"+tooltip+"#"+","+grp_color[0]['text']+"="+"#"+group_list[0]+"#" + template: _.sprintf("#%s#, %s=#%s#", + this.abscissa, group_list[0].text, group_list[0].group) }, radius: 0, - color:grp_color[0]['color'], + color:group_list[0].color, origin:0, - xAxis:{ - template:function(obj){ - if(x_axis['template']){ - var val = obj[x_axis['template']]; - val = (typeof val == 'object')?val[1]:(!val?'Undefined':val); - if(val.length > 12){ - val = val.substring(0,12); - } - return val; - }else{ - return obj; - } - }, - title:x_axis['title'], - lines:x_axis['lines'] - }, - yAxis:{ - template:function(obj){ - if(y_axis['template']){ - var vals = obj[y_axis['template']]; - vals = (typeof vals == 'object')?vals[1]:(!vals?'Undefined':vals); - if(vals.length > 12){ - vals = vals.substring(0,12); - } - return vals; - }else{ - return obj; - } - }, - title:y_axis['title'], - lines: y_axis['lines'], - start:y_axis['start'], - step:y_axis['step'], - end:y_axis['end'] - }, + xAxis: x_axis, + yAxis: y_axis, padding: { left: 75 }, legend: { - values: grp_color, + values: group_list, align:"left", valign:"top", layout: "x", - marker:{ + marker: { type:"round", width:12 } } }); for (var m = 1; m<group_list.length;m++){ + var column = group_list[m]; + if (column.group === this.group_field) { continue; } bar_chart.addSeries({ - value: "#"+group_list[m]+"#", + value: "#"+column.group+"#", tooltip:{ - template:"#"+tooltip+"#"+","+grp_color[m]['text']+"="+"#"+group_list[m]+"#" + template: _.sprintf("#%s#, %s=#%s#", + this.abscissa, column.text, column.group) }, - color: grp_color[m]['color'] + color: column.color }); } - bar_chart.parse(_.values(abscissa_data), "json"); - jQuery("#"+self.elem_id+"-barchart").height(jQuery("#"+self.elem_id+"-barchart").height()+50); + bar_chart.parse(results, "json"); + jQuery("#"+this.element_id+"-barchart").height(jQuery("#"+this.element_id+"-barchart").height()+50); bar_chart.attachEvent("onItemClick", function(id) { self.open_list_view(bar_chart.get(id)); }); @@ -383,15 +303,15 @@ openerp.web_graph.GraphView = openerp.web.View.extend({ var self = this; var chart = new dhtmlXChart({ view:"pie3D", - container:self.elem_id+"-piechart", - value:"#"+self.operator_field+"#", + container:self.element_id+"-piechart", + value:"#"+self.ordinate+"#", pieInnerText:function(obj) { - var sum = chart.sum("#"+self.operator_field+"#"); - var val = obj[self.operator_field] / sum * 100 ; + var sum = chart.sum("#"+self.ordinate+"#"); + var val = obj[self.ordinate] / sum * 100 ; return val.toFixed(1) + "%"; }, tooltip:{ - template:"#"+self.chart_info_fields+"#"+"="+"#"+self.operator_field+"#" + template:"#"+self.abscissa+"#"+"="+"#"+self.ordinate+"#" }, gradient:"3d", height: 20, @@ -406,9 +326,7 @@ openerp.web_graph.GraphView = openerp.web.View.extend({ width:12 }, template:function(obj){ - var val = obj[self.chart_info_fields]; - val = (typeof val == 'object')?val[1]:val; - return val; + return obj[self.abscissa] || 'Undefined'; } } }); @@ -422,20 +340,13 @@ openerp.web_graph.GraphView = openerp.web.View.extend({ if($(".dhx_tooltip").is(":visible")) { $(".dhx_tooltip").remove('div'); } - id = id[this.chart_info_fields]; + id = id[this.abscissa]; if (typeof id == 'object'){ id = id[0]; } - var record_id = ""; - this.dataset.model = this.model; - if (typeof this.chart_info_fields == 'object'){ - record_id = this.chart_info_fields[0]; - }else{ - record_id = this.chart_info_fields; - } - this.dataset.domain = [[record_id, '=', id],['id','in',this.dataset.ids]]; - var modes = !!modes ? modes.split(",") : ["list", "form", "graph"]; + var record_id = this.abscissa; + var modes = ["list", "form", "graph"]; var views = []; _.each(modes, function(mode) { var view = [false, mode]; @@ -446,7 +357,7 @@ openerp.web_graph.GraphView = openerp.web.View.extend({ }); this.do_action({ "res_model" : this.dataset.model, - "domain" : this.dataset.domain, + "domain" : [[record_id, '=', id], ['id','in',this.dataset.ids]], "views" : views, "type" : "ir.actions.act_window", "auto_search" : true, @@ -462,18 +373,18 @@ openerp.web_graph.GraphView = openerp.web.View.extend({ contexts: contexts, group_by_seq: groupbys }, function (results) { - // TODO: handle non-empty results.group_by with read_group - if(results.group_by && results.group_by != ''){ - self.chart_info_fields = results.group_by[0]; - }else{ - self.chart_info_fields = self.chart_info; + // TODO: handle non-empty results.group_by with read_group? + if(!_(results.group_by).isEmpty()){ + self.abscissa = results.group_by[0]; + } else { + self.abscissa = self.first_field; } - self.dataset.context = results.context; - self.dataset.domain = results.domain; - self.dataset.read_slice([],{}, $.proxy(self, 'load_chart')); + self.dataset.read_slice(self.list_fields(), { + context: results.context, + domain: results.domain + }, $.proxy(self, 'schedule_chart')); }); } }); -// here you may tweak globals object, if any, and play with on_* or do_* callbacks on them }; // vim:et fdc=0 fdl=0: diff --git a/addons/web_graph/static/src/xml/web_graph.xml b/addons/web_graph/static/src/xml/web_graph.xml index 4320b9f156e..90349623946 100644 --- a/addons/web_graph/static/src/xml/web_graph.xml +++ b/addons/web_graph/static/src/xml/web_graph.xml @@ -1,4 +1,4 @@ <template> - <div t-name="GraphView" t-att-id="elem_id+'-'+chart+'chart'" + <div t-name="GraphView" t-att-id="element_id+'-'+chart+'chart'" style="height:300px;position:relative;"/> </template>
\ No newline at end of file diff --git a/addons/web_hello/__openerp__.py b/addons/web_hello/__openerp__.py index 7a80645f3b8..498f5129961 100644 --- a/addons/web_hello/__openerp__.py +++ b/addons/web_hello/__openerp__.py @@ -5,4 +5,5 @@ "js": ["static/*/*.js", "static/*/js/*.js"], "css": [], 'active': False, + 'web_preload': False, } diff --git a/addons/web_hello/static/openerp/base_hello.js b/addons/web_hello/static/openerp/base_hello.js index 99c97006eac..91caf263196 100644 --- a/addons/web_hello/static/openerp/base_hello.js +++ b/addons/web_hello/static/openerp/base_hello.js @@ -13,6 +13,13 @@ openerp.web.SearchView = openerp.web.SearchView.extend({ // here you may tweak globals object, if any, and play with on_* or do_* callbacks on them +openerp.web.Login = openerp.web.Login.extend({ + start: function() { + console.log('Hello there'); + this._super.apply(this,arguments); + } +}); + }; // vim:et fdc=0 fdl=0: diff --git a/addons/web_kanban/static/src/css/kanban.css b/addons/web_kanban/static/src/css/kanban.css index afac6cdef47..f92203589b2 100644 --- a/addons/web_kanban/static/src/css/kanban.css +++ b/addons/web_kanban/static/src/css/kanban.css @@ -1,8 +1,21 @@ .openerp .oe_kanban_view .oe_column { - float: left; width: 100%; } +.openerp .oe_vertical_text { + writing-mode:tb-rl; + -webkit-transform:rotate(90deg); + -moz-transform:rotate(90deg); + -o-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); + display:block; + width:30px; + height:20px; + align:center; + font-size:24px; +} + .openerp .oe_kanban_view .ui-sortable-placeholder { border: 1px dotted black; visibility: visible !important; @@ -14,6 +27,10 @@ font-size: 1.5em; font-weight: bold; } +.openerp .oe_kanban_view .fold-columns-icon { + float: left; + padding: 2px 2px 0 0; +} .openerp .oe_kanban_action_button { height: 22px; margin: 0; @@ -200,4 +217,3 @@ .openerp .oe_kanban_color_alert .oe_kanban_color_border { border-color: #c00 !important; } - diff --git a/addons/web_kanban/static/src/img/minus-icon.png b/addons/web_kanban/static/src/img/minus-icon.png Binary files differnew file mode 100644 index 00000000000..cb655d1e503 --- /dev/null +++ b/addons/web_kanban/static/src/img/minus-icon.png diff --git a/addons/web_kanban/static/src/img/plus-icon.png b/addons/web_kanban/static/src/img/plus-icon.png Binary files differnew file mode 100644 index 00000000000..7bb9b1be856 --- /dev/null +++ b/addons/web_kanban/static/src/img/plus-icon.png diff --git a/addons/web_kanban/static/src/js/kanban.js b/addons/web_kanban/static/src/js/kanban.js index 8d6727facaa..46173341361 100644 --- a/addons/web_kanban/static/src/js/kanban.js +++ b/addons/web_kanban/static/src/js/kanban.js @@ -18,6 +18,8 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ this.all_display_data = false; this.groups = []; this.qweb = new QWeb2.Engine(); + this.aggregates = {}; + this.NO_OF_COLUMNS = 3; if (this.options.action_views_ids.form) { this.form_dialog = new openerp.web.FormDialog(this, {}, this.options.action_views_ids.form, dataset).start(); this.form_dialog.on_form_dialog_saved.add_last(this.on_record_saved); @@ -36,8 +38,17 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ } }, add_qweb_template: function() { + var group_operator = ["avg", "max", "min", "sum", "count"] for (var i=0, ii=this.fields_view.arch.children.length; i < ii; i++) { var child = this.fields_view.arch.children[i]; + if (child.tag === "field") { + for(j=0, jj=group_operator.length; j < jj; j++) { + if (child.attrs[group_operator[j]]) { + this.aggregates[child.attrs.name] = child.attrs[group_operator[j]]; + break; + } + } + } if (child.tag === "templates") { this.transform_qweb_template(child); this.qweb.add_template(openerp.web.json_node_to_xml(child)); @@ -118,14 +129,33 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ } } }, - on_show_data: function(data) { + sort_group: function (first, second) { + if (first.header && second.header) + { + first = first.header.toLowerCase(); + second = second.header.toLowerCase(); + if (first > second) return 1; + else if (first < second) return -1; + else return 0; + } + else return 0; + }, + on_show_data: function() { var self = this; - this.$element.html(QWeb.render("KanbanView", {"data": data})); + if (!this.group_by.length) { + this.do_record_group(); + } + self.all_display_data.sort(this.sort_group); + this.$element.html(QWeb.render("KanbanView", {"data": self.all_display_data})); this.on_reload_kanban(); + this.$element.find(".oe_vertical_text").hide(); var drag_handel = false; if (this.$element.find(".oe_kanban_draghandle").length > 0) { drag_handel = ".oe_kanban_draghandle"; } + if (!this.group_by.length) { + drag_handel = true; + } this.$element.find(".oe_column").sortable({ connectWith: ".oe_column", handle : drag_handel, @@ -134,36 +164,44 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ self.source_index['column'] = ui.item.parent().attr('id'); }, stop: self.on_receive_record, + scroll: false }); this.$element.find(".oe_column").disableSelection() this.$element.find('button.oe_kanban_button_new').click(this.do_add_record); + this.$element.find(".fold-columns-icon").click(function(event) { + self.do_fold_unfold_columns(event, this.id); + }); }, - on_button_click: function (button_attrs, record_id) { - var self = this; - if (this.groups.length) { - _.each(this.groups, function (group) { - group.list([], - function (groups) {}, - function (dataset) { - dataset.read_slice([], {}, function(records) { - var index = parseInt(_.indexOf(dataset.ids, record_id)); - if(index >= 0) { - self.on_confirm_click(dataset, button_attrs, index, record_id); - } - }); - } - ); - }); - } else { - var index = parseInt(_.indexOf(self.dataset.ids, record_id)); - if (index >= 0) { - _.extend(self.dataset, {domain: self.domain, context: self.context}); - self.on_confirm_click(self.dataset, button_attrs, index, record_id); + do_fold_unfold_columns: function(event, element_id) { + var column_id = "column_" + element_id; + var column_element = this.$element.find("#" + column_id + " .oe_fold_column"); + if (column_element.is(":hidden")) { + this.$element.find("#" + column_id).find("img.fold-columns-icon").attr('src', '/web_kanban/static/src/img/minus-icon.png'); + column_element.show(); + this.$element.find("#" + column_id + ".oe_table_column").css("width",Math.round(99 / this.all_display_data.length) + "%"); + this.$element.find("#" + column_id + ".oe_vertical_text").hide(); + } + else{ + this.$element.find("#" + column_id).find("img.fold-columns-icon").attr('src', '/web_kanban/static/src/img/plus-icon.png'); + column_element.hide(); + this.$element.find("#" + column_id + ".oe_table_column").css("width","0.5%"); + this.$element.find("#" + column_id + ".oe_vertical_text").show(); + } + + }, + do_record_group: function() { + if (this.NO_OF_COLUMNS && this.all_display_data.length > 0) { + var records = this.all_display_data[0].records; + var record_per_group = Math.round((records).length / this.NO_OF_COLUMNS); + this.all_display_data = []; + for (var i=0, ii=this.NO_OF_COLUMNS; i < ii; i++) { + this.all_display_data.push({'records': records.slice(0,record_per_group), 'value':i, 'header' : false, 'ids':[]}); + records.splice(0,record_per_group); } } }, - on_confirm_click: function (dataset, button_attrs, index, record_id) { - this.on_execute_button_click(dataset, button_attrs, record_id); + on_button_click: function (button_attrs, record_id) { + this.on_execute_button_click(this.dataset, button_attrs, record_id); }, do_add_record: function() { this.dataset.index = null; @@ -179,8 +217,7 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ }, on_record_saved: function(r) { var id = this.form_dialog.form.datarecord.id; - // TODO fme: reload record instead of all. need refactoring - this.do_actual_search(); + this.on_reload_record(id); }, do_change_color: function(record_id, $e) { var self = this, @@ -198,12 +235,41 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ var data = {}; data[$e.data('name')] = $(this).data('color'); self.dataset.write(id, data, {}, function() { - // TODO fme: reload record instead of all. need refactoring - self.do_actual_search(); + self.on_reload_record(id); }); $cpicker.remove(); }); }, + /** + Reload one record in view. + record_id : reload record id. + */ + on_reload_record: function (record_id){ + var self = this; + this.dataset.read_ids([record_id], [], function (records) { + if (records.length > 0) { + for (var i=0, ii=self.all_display_data.length; i < ii; i++) { + for(j=0, jj=self.all_display_data[i].records.length; j < jj; j++) { + if (self.all_display_data[i].records[j].id == record_id) { + _.extend(self.all_display_data[i].records[j], records[0]); + self.$element.find("#main_" + record_id).children().remove(); + self.$element.find("#main_" + record_id).append(self.qweb.render('kanban-box', { + record: self.do_transform_record(self.all_display_data[i].records[j]), + kanban_color: self.kanban_color, + kanban_gravatar: self.kanban_gravatar + })); + break; + } + } + } + self.$element.find("#main_" + record_id + " .oe_kanban_action").click(self.on_action_clicked); + self.$element.find("#main_" + record_id + " .oe_kanban_box_show_onclick_trigger").click(function() { + $(this).parent("#main_" + record_id + " .oe_kanban_box").find(".oe_kanban_box_show_onclick").toggle(); + }); + } + }); + + }, do_delete: function (id) { var self = this; return $.when(this.dataset.unlink([id])).then(function () { @@ -228,16 +294,7 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ this.do_execute_action( button_attrs, dataset, record_id, function () { - var count = 1; - _.each(self.all_display_data, function(data, index) { - self.dataset.read_ids( data.ids, [], function(records){ - self.all_display_data[index].records = records; - if(self.all_display_data.length == count) { - self.do_actual_search(); - } - count++; - }); - }); + self.do_actual_search(); } ); }, @@ -249,9 +306,8 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ if (!ui.item.attr("id")) { return false; } - // TODO fme: check what was this sequence - if (self.fields_view.fields.sequence && (self.source_index.index >= 0 && self.source_index.index != from) || - (self.source_index.column && self.source_index.column != ui.item.parent().attr('id'))) { + if (self.fields_view.fields.sequence != undefined && ((self.source_index.index >= 0 && self.source_index.index != from) || + (self.source_index.column && self.source_index.column != ui.item.parent().attr('id')))) { var child_record = ui.item.parent().children(); var data, sequence = 1, index = to; child_record.splice(0, to); @@ -338,9 +394,6 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ kanban_gravatar: self.kanban_gravatar })); }); - } else { - self.$element.find("#column_" + data.value).remove(); - self.all_display_data.splice(index, 1); } }); this.$element.find('.oe_kanban_action').click(this.on_action_clicked); @@ -377,8 +430,8 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ do_search: function (domains, contexts, group_by) { var self = this; this.rpc('/web/session/eval_domain_and_context', { - domains: domains, - contexts: contexts, + domains: [this.dataset.get_domain()].concat(domains), + contexts: [this.dataset.get_context()].concat(contexts), group_by_seq: group_by }, function (results) { self.domain = results.domain; @@ -392,23 +445,27 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ group_by = self.group_by; if (!group_by.length && this.fields_view.arch.attrs.default_group_by) { group_by = [this.fields_view.arch.attrs.default_group_by]; + self.group_by = group_by; } self.datagroup = new openerp.web.DataGroup(self, self.model, self.domain, self.context, group_by); - self.dataset.context = self.context; - self.dataset.domain = self.domain; - self.datagroup.list([], + self.datagroup.list(_.keys(self.fields_view.fields), function (groups) { self.groups = groups; - self.do_render_group(groups); + if (groups.length) { + self.do_render_group(groups); + } + else { + self.all_display_data = []; + self.on_show_data(); + } }, function (dataset) { - self.domain = dataset.domain; - self.context = dataset.context; self.groups = []; - self.dataset.read_slice([], {}, function(records) { - self.all_display_data = [{'records': records, 'value':false, 'header' : false, 'ids': self.dataset.ids}]; + self.dataset.read_slice([], {'domain': self.domain, 'context': self.context}, function(records) { + if (records.length) self.all_display_data = [{'records': records, 'value':false, 'header' : false, 'ids': self.dataset.ids}]; + else self.all_display_data = []; self.$element.find(".oe_kanban_view").remove(); - self.on_show_data(self.all_display_data); + self.on_show_data(); }); } ); @@ -417,8 +474,6 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ this.all_display_data = []; var self = this; _.each(datagroups, function (group) { - self.dataset.context = group.context; - self.dataset.domain = group.domain; var group_name = group.value; var group_value = group.value; if (!group.value) { @@ -428,11 +483,15 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({ group_name = group.value[1]; group_value = group.value[0]; } - self.dataset.read_slice([], {}, function(records) { - self.all_display_data.push({"value" : group_value, "records": records, 'header':group_name, 'ids': self.dataset.ids}); + var group_aggregates = {}; + _.each(self.aggregates, function(value, key) { + group_aggregates[value] = group.aggregates[key]; + }); + self.dataset.read_slice([], {'domain': group.domain, 'context': group.context}, function(records) { + self.all_display_data.push({"value" : group_value, "records": records, 'header':group_name, 'ids': self.dataset.ids, 'aggregates': group_aggregates}); if (datagroups.length == self.all_display_data.length) { self.$element.find(".oe_kanban_view").remove(); - self.on_show_data(self.all_display_data); + self.on_show_data(); } }); }); diff --git a/addons/web_kanban/static/src/xml/web_kanban.xml b/addons/web_kanban/static/src/xml/web_kanban.xml index aa7bc73190d..8f3aaf457e9 100644 --- a/addons/web_kanban/static/src/xml/web_kanban.xml +++ b/addons/web_kanban/static/src/xml/web_kanban.xml @@ -9,15 +9,22 @@ </td> </tr> <tr> - <td t-foreach="data" t-as="columns" class="oe_table_column oe_column_heading" t-att-id="'column_' + columns.value"> - <t t-if="columns.value" t-esc="columns.header"/> + <td t-foreach="data" t-as="columns" class="oe_table_column" t-att-id="'column_' + columns.value"> + <img t-att-id="columns.value" class="fold-columns-icon" src="/web_kanban/static/src/img/minus-icon.png"/> + <div class="oe_fold_column" t-att-id="'column_' + columns.value"> + <div class="oe_column_heading" t-if="columns.value and columns.header"> + <t t-esc="columns.header"/> + </div> + <div t-foreach="columns.aggregates or {}" t-as="aggregate"> + <i><u><t t-esc="aggregate"/>:</u> <t t-esc="aggregate_value"/></i> + </div> + </div> </td> </tr> <tr> - <td t-foreach="data" t-as="columns" class="oe_table_column" t-att-id="'column_' + columns.value" t-attf-style="width: #{Math.round(99 / data.length)}%"> - <div class="oe_column" t-att-id="'column_' + columns.value"> - <div t-foreach="columns.records" t-as="record" class="oe_kanban_record" t-att-id="'main_' + record.id"/> - </div> + <td t-foreach="data" t-as="columns" class="oe_column oe_table_column" t-att-id="'column_' + columns.value" t-attf-style="width: #{Math.round(99 / data.length)}%"> + <p t-if="columns.header" class="oe_vertical_text" t-att-id="'column_' + columns.value"><t t-esc="columns.header"/></p> + <div t-foreach="columns.records" t-as="record" class="oe_fold_column oe_kanban_record" t-att-id="'main_' + record.id"/> </td> </tr> </table> diff --git a/addons/web_tests/__init__.py b/addons/web_tests/__init__.py new file mode 100644 index 00000000000..40a96afc6ff --- /dev/null +++ b/addons/web_tests/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/addons/web_tests/__openerp__.py b/addons/web_tests/__openerp__.py new file mode 100644 index 00000000000..d6221900f00 --- /dev/null +++ b/addons/web_tests/__openerp__.py @@ -0,0 +1,8 @@ +{ + "name": "Tests", + "version": "2.0", + "depends": [], + "js": ["static/src/js/*.js"], + "css": ['static/src/css/*.css'], + 'active': True, +} diff --git a/addons/web_tests/static/src/css/web_tests.css b/addons/web_tests/static/src/css/web_tests.css new file mode 100644 index 00000000000..b14d6541f43 --- /dev/null +++ b/addons/web_tests/static/src/css/web_tests.css @@ -0,0 +1,3 @@ +.oe-bunchaforms > div { + float: left; +} diff --git a/addons/web_tests/static/src/js/web_tests.js b/addons/web_tests/static/src/js/web_tests.js new file mode 100644 index 00000000000..b21275d1eb7 --- /dev/null +++ b/addons/web_tests/static/src/js/web_tests.js @@ -0,0 +1,37 @@ +openerp.web_tests = function (db) { + db.web.client_actions.add( + 'buncha-forms', 'instance.web_tests.BunchaForms'); + db.web_tests = {}; + db.web_tests.BunchaForms = db.web.Widget.extend({ + init: function (parent) { + this._super(parent); + this.dataset = new db.web.DataSetSearch(this, 'test.listview.relations'); + this.form = new db.web.FormView(this, this.dataset, false, { + action_buttons: false, + pager: false + }); + this.form.registry = db.web.form.readonly; + }, + render: function () { + return '<div class="oe-bunchaforms"></div>'; + }, + start: function () { + $.when( + this.dataset.read_slice(), + this.form.appendTo(this.$element)).then(this.on_everything_loaded); + }, + on_everything_loaded: function (slice) { + var records = slice[0].records; + if (!records.length) { + this.form.on_record_loaded({}); + return; + } + this.form.on_record_loaded(records[0]); + _(records.slice(1)).each(function (record, index) { + this.dataset.index = index+1; + this.form.reposition($('<div>').appendTo(this.$element)); + this.form.on_record_loaded(record); + }, this); + } + }); +}; |
