diff --git a/addons/web/__init__.py b/addons/web/__init__.py index ec702292ce8..2478edb81b7 100644 --- a/addons/web/__init__.py +++ b/addons/web/__init__.py @@ -5,7 +5,7 @@ import logging _logger = logging.getLogger(__name__) -try: +def wsgi_postload(): import openerp.wsgi import os import tempfile @@ -17,16 +17,12 @@ try: o.session_storage = os.path.join(tempfile.gettempdir(), "oe-sessions") o.addons_path = os.path.dirname(os.path.dirname(__file__)) o.serve_static = True - o.server_host = '127.0.0.1' - o.server_port = 8069 + o.backend = 'local' app = common.dispatch.Root(o) #import openerp.wsgi openerp.wsgi.register_wsgi_handler(app) -except ImportError: - _logger.info("standalone mode") - # 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 2c686a216fe..85464e750cb 100644 --- a/addons/web/__openerp__.py +++ b/addons/web/__openerp__.py @@ -32,10 +32,10 @@ "static/src/js/views.js", "static/src/js/data.js", "static/src/js/data_export.js", - "static/src/js/form.js", - "static/src/js/list.js", - "static/src/js/list-editable.js", "static/src/js/search.js", + "static/src/js/view_form.js", + "static/src/js/view_list.js", + "static/src/js/view_list_editable.js", "static/src/js/view_tree.js", "static/src/js/view_editor.js" ], @@ -46,5 +46,5 @@ "static/src/css/base.css", "static/src/css/data_export.css", ], - 'wsgi' : 'app', + 'post_load' : 'wsgi_postload', } diff --git a/addons/web/common/__init__.py b/addons/web/common/__init__.py index 0628a7fa609..9257f51d037 100644 --- a/addons/web/common/__init__.py +++ b/addons/web/common/__init__.py @@ -1,4 +1,2 @@ #!/usr/bin/python -# TODO if from openerpserver use backendlocal -# from backendlocal import * from dispatch import * diff --git a/addons/web/common/backendlocal.py b/addons/web/common/backendlocal.py deleted file mode 100644 index dac33991c4d..00000000000 --- a/addons/web/common/backendlocal.py +++ /dev/null @@ -1,23 +0,0 @@ -#---------------------------------------------------------- -# OpenERPSession local openerp backend access -#---------------------------------------------------------- -class OpenERPUnboundException(Exception): - pass - -class OpenERPConnector(object): - pass - -class OpenERPAuth(object): - pass - -class OpenERPModel(object): - def __init__(self, session, model): - self._session = session - self._model = model - - def __getattr__(self, name): - return lambda *l:self._session.execute(self._model, name, *l) - -class OpenERPSession(object): - def __init__(self, model_factory=OpenERPModel): - pass diff --git a/addons/web/common/dispatch.py b/addons/web/common/dispatch.py index 2336ca2d426..98b948704fa 100644 --- a/addons/web/common/dispatch.py +++ b/addons/web/common/dispatch.py @@ -22,7 +22,8 @@ import ast import nonliterals import http # import backendlocal as backend -import backendrpc as backend +import session as backend +import openerplib __all__ = ['Root', 'jsonrequest', 'httprequest', 'Controller', 'WebRequest', 'JsonRequest', 'HttpRequest'] @@ -166,7 +167,7 @@ class JsonRequest(WebRequest): _logger.debug("--> %s.%s\n%s", controller.__class__.__name__, method.__name__, pprint.pformat(self.jsonrequest)) response['id'] = self.jsonrequest.get('id') response["result"] = method(controller, self, **self.params) - except backend.OpenERPUnboundException: + except openerplib.AuthenticationError: error = { 'code': 100, 'message': "OpenERP Session Invalid", diff --git a/addons/web/common/openerplib/__init__.py b/addons/web/common/openerplib/__init__.py new file mode 100644 index 00000000000..5c0bd8ec655 --- /dev/null +++ b/addons/web/common/openerplib/__init__.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) Stephane Wirtel +# Copyright (C) 2011 Nicolas Vanhoren +# Copyright (C) 2011 OpenERP s.a. (). +# 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER 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. +# +############################################################################## + +from main import * + diff --git a/addons/web/common/openerplib/dates.py b/addons/web/common/openerplib/dates.py new file mode 100644 index 00000000000..6d4fa9e2f63 --- /dev/null +++ b/addons/web/common/openerplib/dates.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) Stephane Wirtel +# Copyright (C) 2011 Nicolas Vanhoren +# Copyright (C) 2011 OpenERP s.a. (). +# 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER 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. +# +############################################################################## + +import datetime + +DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d" +DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S" +DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % ( + DEFAULT_SERVER_DATE_FORMAT, + DEFAULT_SERVER_TIME_FORMAT) + +def str_to_datetime(str): + """ + Converts a string to a datetime object using OpenERP's + datetime string format (exemple: '2011-12-01 15:12:35'). + + No timezone information is added, the datetime is a naive instance, but + according to OpenERP 6.1 specification the timezone is always UTC. + """ + if not str: + return str + return datetime.datetime.strptime(str, DEFAULT_SERVER_DATETIME_FORMAT) + +def str_to_date(str): + """ + Converts a string to a date object using OpenERP's + date string format (exemple: '2011-12-01'). + """ + if not str: + return str + return datetime.datetime.strptime(str, DEFAULT_SERVER_DATE_FORMAT).date() + +def str_to_time(str): + """ + Converts a string to a time object using OpenERP's + time string format (exemple: '15:12:35'). + """ + if not str: + return str + return datetime.datetime.strptime(str, DEFAULT_SERVER_TIME_FORMAT).time() + +def datetime_to_str(obj): + """ + Converts a datetime object to a string using OpenERP's + datetime string format (exemple: '2011-12-01 15:12:35'). + + The datetime instance should not have an attached timezone and be in UTC. + """ + if not obj: + return False + return obj.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + +def date_to_str(obj): + """ + Converts a date object to a string using OpenERP's + date string format (exemple: '2011-12-01'). + """ + if not obj: + return False + return obj.strftime(DEFAULT_SERVER_DATE_FORMAT) + +def time_to_str(obj): + """ + Converts a time object to a string using OpenERP's + time string format (exemple: '15:12:35'). + """ + if not obj: + return False + return obj.strftime(DEFAULT_SERVER_TIME_FORMAT) + diff --git a/addons/web/common/openerplib/main.py b/addons/web/common/openerplib/main.py new file mode 100644 index 00000000000..21f8d48ff63 --- /dev/null +++ b/addons/web/common/openerplib/main.py @@ -0,0 +1,399 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) Stephane Wirtel +# Copyright (C) 2011 Nicolas Vanhoren +# Copyright (C) 2011 OpenERP s.a. (). +# 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER 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. +# +############################################################################## + +""" +OpenERP Client Library + +Home page: http://pypi.python.org/pypi/openerp-client-lib +Code repository: https://code.launchpad.net/~niv-openerp/openerp-client-lib/trunk +""" + +import xmlrpclib +import logging +import socket + +try: + import cPickle as pickle +except ImportError: + import pickle + +try: + import cStringIO as StringIO +except ImportError: + import StringIO + +_logger = logging.getLogger(__name__) + +def _getChildLogger(logger, subname): + return logging.getLogger(logger.name + "." + subname) + +class Connector(object): + """ + The base abstract class representing a connection to an OpenERP Server. + """ + + __logger = _getChildLogger(_logger, 'connector') + + def __init__(self, hostname, port): + """ + Initilize by specifying an hostname and a port. + :param hostname: Host name of the server. + :param port: Port for the connection to the server. + """ + self.hostname = hostname + self.port = port + +class XmlRPCConnector(Connector): + """ + A type of connector that uses the XMLRPC protocol. + """ + PROTOCOL = 'xmlrpc' + + __logger = _getChildLogger(_logger, 'connector.xmlrpc') + + def __init__(self, hostname, port=8069): + """ + Initialize by specifying the hostname and the port. + :param hostname: The hostname of the computer holding the instance of OpenERP. + :param port: The port used by the OpenERP instance for XMLRPC (default to 8069). + """ + Connector.__init__(self, hostname, port) + self.url = 'http://%s:%d/xmlrpc' % (self.hostname, self.port) + + def send(self, service_name, method, *args): + url = '%s/%s' % (self.url, service_name) + service = xmlrpclib.ServerProxy(url) + return getattr(service, method)(*args) + +class NetRPC_Exception(Exception): + """ + Exception for NetRPC errors. + """ + def __init__(self, faultCode, faultString): + self.faultCode = faultCode + self.faultString = faultString + self.args = (faultCode, faultString) + +class NetRPC(object): + """ + Low level class for NetRPC protocol. + """ + def __init__(self, sock=None): + if sock is None: + self.sock = socket.socket( + socket.AF_INET, socket.SOCK_STREAM) + else: + self.sock = sock + self.sock.settimeout(120) + def connect(self, host, port=False): + if not port: + buf = host.split('//')[1] + host, port = buf.split(':') + self.sock.connect((host, int(port))) + + def disconnect(self): + self.sock.shutdown(socket.SHUT_RDWR) + self.sock.close() + + def mysend(self, msg, exception=False, traceback=None): + msg = pickle.dumps([msg,traceback]) + size = len(msg) + self.sock.send('%8d' % size) + self.sock.send(exception and "1" or "0") + totalsent = 0 + while totalsent < size: + sent = self.sock.send(msg[totalsent:]) + if sent == 0: + raise RuntimeError, "socket connection broken" + totalsent = totalsent + sent + + def myreceive(self): + buf='' + while len(buf) < 8: + chunk = self.sock.recv(8 - len(buf)) + if chunk == '': + raise RuntimeError, "socket connection broken" + buf += chunk + size = int(buf) + buf = self.sock.recv(1) + if buf != "0": + exception = buf + else: + exception = False + msg = '' + while len(msg) < size: + chunk = self.sock.recv(size-len(msg)) + if chunk == '': + raise RuntimeError, "socket connection broken" + msg = msg + chunk + msgio = StringIO.StringIO(msg) + unpickler = pickle.Unpickler(msgio) + unpickler.find_global = None + res = unpickler.load() + + if isinstance(res[0],Exception): + if exception: + raise NetRPC_Exception(str(res[0]), str(res[1])) + raise res[0] + else: + return res[0] + +class NetRPCConnector(Connector): + """ + A type of connector that uses the NetRPC protocol. + """ + + PROTOCOL = 'netrpc' + + __logger = _getChildLogger(_logger, 'connector.netrpc') + + def __init__(self, hostname, port=8070): + """ + Initialize by specifying the hostname and the port. + :param hostname: The hostname of the computer holding the instance of OpenERP. + :param port: The port used by the OpenERP instance for NetRPC (default to 8070). + """ + Connector.__init__(self, hostname, port) + + def send(self, service_name, method, *args): + socket = NetRPC() + socket.connect(self.hostname, self.port) + socket.mysend((service_name, method, )+args) + result = socket.myreceive() + socket.disconnect() + return result + +class Service(object): + """ + A class to execute RPC calls on a specific service of the remote server. + """ + def __init__(self, connector, service_name): + """ + :param connector: A valid Connector instance. + :param service_name: The name of the service on the remote server. + """ + self.connector = connector + self.service_name = service_name + self.__logger = _getChildLogger(_getChildLogger(_logger, 'service'),service_name) + + def __getattr__(self, method): + """ + :param method: The name of the method to execute on the service. + """ + self.__logger.debug('method: %r', method) + def proxy(*args): + """ + :param args: A list of values for the method + """ + self.__logger.debug('args: %r', args) + result = self.connector.send(self.service_name, method, *args) + self.__logger.debug('result: %r', result) + return result + return proxy + +class Connection(object): + """ + A class to represent a connection with authentication to an OpenERP Server. + It also provides utility methods to interact with the server more easily. + """ + __logger = _getChildLogger(_logger, 'connection') + + def __init__(self, connector, + database=None, + login=None, + password=None, + user_id=None): + """ + Initialize with login information. The login information is facultative to allow specifying + it after the initialization of this object. + + :param connector: A valid Connector instance to send messages to the remote server. + :param database: The name of the database to work on. + :param login: The login of the user. + :param password: The password of the user. + :param user_id: The user id is a number identifying the user. This is only useful if you + already know it, in most cases you don't need to specify it. + """ + self.connector = connector + + self.set_login_info(database, login, password, user_id) + + def set_login_info(self, database, login, password, user_id=None): + """ + Set login information after the initialisation of this object. + + :param connector: A valid Connector instance to send messages to the remote server. + :param database: The name of the database to work on. + :param login: The login of the user. + :param password: The password of the user. + :param user_id: The user id is a number identifying the user. This is only useful if you + already know it, in most cases you don't need to specify it. + """ + self.database, self.login, self.password = database, login, password + + self.user_id = user_id + + def check_login(self, force=True): + """ + Checks that the login information is valid. Throws an AuthenticationError if the + authentication fails. + + :param force: Force to re-check even if this Connection was already validated previously. + Default to True. + """ + if self.user_id and not force: + return + + if not self.database or not self.login or self.password is None: + raise AuthenticationError("Creditentials not provided") + + self.user_id = self.get_service("common").login(self.database, self.login, self.password) + if not self.user_id: + raise AuthenticationError("Authentication failure") + self.__logger.debug("Authenticated with user id %s", self.user_id) + + def get_model(self, model_name): + """ + Returns a Model instance to allow easy remote manipulation of an OpenERP model. + + :param model_name: The name of the model. + """ + return Model(self, model_name) + + def get_service(self, service_name): + """ + Returns a Service instance to allow easy manipulation of one of the services offered by the remote server. + Please note this Connection instance does not need to have valid authentication information since authentication + is only necessary for the "object" service that handles models. + + :param service_name: The name of the service. + """ + return Service(self.connector, service_name) + +class AuthenticationError(Exception): + """ + An error thrown when an authentication to an OpenERP server failed. + """ + pass + +class Model(object): + """ + Useful class to dialog with one of the models provided by an OpenERP server. + An instance of this class depends on a Connection instance with valid authentication information. + """ + + def __init__(self, connection, model_name): + """ + :param connection: A valid Connection instance with correct authentication information. + :param model_name: The name of the model. + """ + self.connection = connection + self.model_name = model_name + self.__logger = _getChildLogger(_getChildLogger(_logger, 'object'), model_name) + + def __getattr__(self, method): + """ + Provides proxy methods that will forward calls to the model on the remote OpenERP server. + + :param method: The method for the linked model (search, read, write, unlink, create, ...) + """ + def proxy(*args): + """ + :param args: A list of values for the method + """ + self.connection.check_login(False) + self.__logger.debug(args) + result = self.connection.get_service('object').execute( + self.connection.database, + self.connection.user_id, + self.connection.password, + self.model_name, + method, + *args) + if method == "read": + if isinstance(result, list) and len(result) > 0 and "id" in result[0]: + index = {} + for r in result: + index[r['id']] = r + result = [index[x] for x in args[0]] + self.__logger.debug('result: %r', result) + return result + return proxy + + def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None, context=None): + """ + A shortcut method to combine a search() and a read(). + + :param domain: The domain for the search. + :param fields: The fields to extract (can be None or [] to extract all fields). + :param offset: The offset for the rows to read. + :param limit: The maximum number of rows to read. + :param order: The order to class the rows. + :param context: The context. + :return: A list of dictionaries containing all the specified fields. + """ + record_ids = self.search(domain or [], offset, limit or False, order or False, context or {}) + records = self.read(record_ids, fields or [], context or {}) + return records + +def get_connector(hostname, protocol="xmlrpc", port="auto"): + """ + A shortcut method to easily create a connector to a remote server using XMLRPC or NetRPC. + + :param hostname: The hostname to the remote server. + :param protocol: The name of the protocol, must be "xmlrpc" or "netrpc". + :param port: The number of the port. Defaults to auto. + """ + if port == 'auto': + port = 8069 if protocol=="xmlrpc" else 8070 + if protocol == "xmlrpc": + return XmlRPCConnector(hostname, port) + elif protocol == "netrpc": + return NetRPCConnector(hostname, port) + else: + raise ValueError("You must choose xmlrpc or netrpc") + +def get_connection(hostname, 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. + + :param hostname: The hostname to the remote server. + :param protocol: The name of the protocol, must be "xmlrpc" or "netrpc". + :param port: The number of the port. Defaults to auto. + :param connector: A valid Connector instance to send messages to the remote server. + :param database: The name of the database to work on. + :param login: The login of the user. + :param password: The password of the user. + :param user_id: The user id is a number identifying the user. This is only useful if you + already know it, in most cases you don't need to specify it. + """ + return Connection(get_connector(hostname, protocol, port), database, login, password, user_id) + diff --git a/addons/web/common/backendrpc.py b/addons/web/common/session.py similarity index 84% rename from addons/web/common/backendrpc.py rename to addons/web/common/session.py index e6c59000caf..d1997a3f903 100644 --- a/addons/web/common/backendrpc.py +++ b/addons/web/common/session.py @@ -2,28 +2,12 @@ import datetime import dateutil.relativedelta import time -import xmlrpclib +import openerplib import nonliterals #---------------------------------------------------------- # OpenERPSession RPC openerp backend access #---------------------------------------------------------- -class OpenERPUnboundException(Exception): - pass - -class OpenERPConnector(object): - pass - -class OpenERPAuth(object): - pass - -class OpenERPModel(object): - def __init__(self, session, model): - self._session = session - self._model = model - - def __getattr__(self, name): - return lambda *l:self._session.execute(self._model, name, *l) class OpenERPSession(object): """ @@ -42,14 +26,13 @@ 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, model_factory=OpenERPModel): + def __init__(self, server='127.0.0.1', port=8069): self._server = server self._port = port self._db = False self._uid = False self._login = False self._password = False - self.model_factory = model_factory self._locale = 'en_US' self.context = {} self.contexts_store = {} @@ -57,34 +40,38 @@ class OpenERPSession(object): self._lang = {} self.remote_timezone = 'utc' self.client_timezone = False + + 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) def proxy(self, service): - s = xmlrpclib.ServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service)) - return s + return self.build_connection().get_service(service) - def bind(self, db, uid, password): + def bind(self, db, uid, login, password): self._db = db self._uid = uid + self._login = login self._password = password def login(self, db, login, password): uid = self.proxy('common').login(db, login, password) - self.bind(db, uid, password) - self._login = login + self.bind(db, uid, login, password) if uid: self.get_context() return uid - def assert_valid(self): + def assert_valid(self, force=False): """ Ensures this session is valid (logged into the openerp server) """ - if not (self._db and self._uid and self._password): - raise OpenERPUnboundException() + self.build_connection().check_login(force) def execute(self, model, func, *l, **d): self.assert_valid() - r = self.proxy('object').execute(self._db, self._uid, self._password, model, func, *l, **d) + model = self.build_connection().get_model(model) + r = getattr(model, func)(*l, **d) return r def exec_workflow(self, model, id, signal): @@ -97,9 +84,9 @@ class OpenERPSession(object): :param model: an OpenERP model name :type model: str - :rtype: :class:`openerpweb.openerpweb.OpenERPModel` + :rtype: a model object """ - return self.model_factory(self, model) + return self.build_connection().get_model(model) def get_context(self): """ Re-initializes the current user's session context (based on diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 6ce4bd612f2..d0c8950f9e1 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -301,6 +301,7 @@ class Session(openerpweb.Controller): } @openerpweb.jsonrequest def get_session_info(self, req): + req.session.assert_valid(force=True) return { "uid": req.session._uid, "context": req.session.get_context() if req.session._uid else False, diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index 2cdeb9e89d5..a66bb20d8de 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -821,6 +821,7 @@ label.error { } .openerp td.oe_form_frame_cell { padding: 2px; + position: relative; } .openerp td.oe_form_frame_cell.oe_form_group { padding: 0; @@ -841,8 +842,8 @@ label.error { .openerp label.oe_label_help { cursor: help; } -.openerp label.oe_label, -.openerp label.oe_label_help { + +.openerp .oe_form_field label.oe_label, .openerp .oe_form_field label.oe_label_help { text-align: right; margin: 3px 0 0 10px; } @@ -887,12 +888,19 @@ label.error { position: relative; vertical-align: top; } -.openerp img.ui-datepicker-trigger { - margin-left: -20px; - vertical-align: middle; +.openerp .oe_input_icon { + position: absolute; cursor: pointer; - position: relative; - top: -1px; + right: 5px; + top: 5px; +} +.openerp .oe_input_icon_disabled { + position: absolute; + cursor: default; + opacity: 0.5; + filter:alpha(opacity=50); + right: 5px; + top: 5px; } .openerp img.oe_field_translate { margin-left: -21px; @@ -987,7 +995,53 @@ label.error { .openerp .view-manager-main-content { width: 100%; - padding: 6px; +} +.openerp .oe-view-manager-header { + overflow: auto; +} +.openerp .oe-view-manager-header h2 { + float: left; +} +.openerp .oe-view-manager-header blockquote { + display: none; + font-size: 85%; + margin: 0; + background: #fff; + border-bottom: 1px solid #CECBCB; + padding: 1px 10px; + color: #4C4C4C; +} +.openerp .oe-view-manager-header blockquote p { + margin: 0; + padding: 6px 1px 4px; +} +.openerp .oe-view-manager-header blockquote div { + text-align: right; +} +.openerp .oe-view-manager-header blockquote div button { + border: none; + background: none; + padding: 0 4px; + margin: 0; + display: inline; + text-decoration: underline; + color: inherit; +} +.openerp .oe-view-manager-logs { + clear: both; + font-size: 85%; + margin: 0.25em 0; + background: #fff; + padding: 0 10px; + color: #4C4C4C; + list-style: none; +} +.openerp .oe-view-manager-logs li:before { + content: '→ '; +} +.openerp .oe-view-manager-logs a { + text-decoration: none; + color: inherit; } .openerp .view-manager-main-sidebar { diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index a04456cfaa5..d114f485c62 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -839,7 +839,11 @@ openerp.web.Menu = openerp.web.Widget.extend(/** @lends openerp.web.Menu# */{ $menu.addClass('active'); $menu.parent('h4').addClass('active'); - return !$menu.is(".leaf"); + if (this.$secondary_menu.has($menu).length) { + return !$menu.is(".leaf"); + } else { + return false; + } }, on_menu_action_loaded: function(data) { var self = this; @@ -878,7 +882,6 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie // Do you autorize this ? will be replaced by notify() in controller 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.header.on_logout.add(this.login.on_logout); @@ -891,6 +894,11 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie this.menu = new openerp.web.Menu(this, "oe_menu", "oe_secondary_menu"); this.menu.on_action.add(this.on_menu_action); + + this.url_internal_hashchange = false; + this.url_external_hashchange = false; + jQuery(window).bind('hashchange', this.on_url_hashchange); + }, start: function() { this.header.appendTo($("#oe_header")); @@ -904,6 +912,7 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie this.action_manager.stop(); this.action_manager = new openerp.web.ActionManager(this); this.action_manager.appendTo($("#oe_app")); + this.action_manager.do_url_set_hash.add_last(this.do_url_set_hash); // if using saved actions, load the action and give it to action manager var parameters = jQuery.deparam(jQuery.param.querystring()); @@ -930,7 +939,6 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie load_url_state: function () { var self = this; // TODO: add actual loading if there is url state to unpack, test on window.location.hash - // not logged in if (!this.session.uid) { return; } var ds = new openerp.web.DataSetSearch(this, 'res.users'); @@ -966,6 +974,25 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie self.action_manager.do_action(action); }); }, + 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); + } + }, + 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; + } + }, on_menu_action: function(action) { this.action_manager.do_action(action); }, diff --git a/addons/web/static/src/js/core.js b/addons/web/static/src/js/core.js index 19a915c0f78..dba47ee758c 100644 --- a/addons/web/static/src/js/core.js +++ b/addons/web/static/src/js/core.js @@ -501,6 +501,8 @@ openerp.web.Session = openerp.web.CallbackEnabled.extend( /** @lends openerp.web self.on_session_valid(); else self.on_session_invalid(); + }, function() { + self.on_session_invalid(); }); }, /** @@ -742,6 +744,59 @@ openerp.web.SessionAware = openerp.web.CallbackEnabled.extend(/** @lends openerp } }); +/** + * Base class for all visual components. Provides a lot of functionalities helpful + * for the management of a part of the DOM. + * + * Widget handles: + * - Rendering with QWeb. + * - Life-cycle management and parenting (when a parent is destroyed, all its children are + * destroyed too). + * - Insertion in DOM. + * + * Widget also extends SessionAware for ease of use. + * + * Guide to create implementations of the Widget class: + * ============================================== + * + * Here is a sample child class: + * + * MyWidget = openerp.base.Widget.extend({ + * // the name of the QWeb template to use for rendering + * template: "MyQWebTemplate", + * // identifier prefix, it is useful to put an obvious one for debugging + * identifier_prefix: 'my-id-prefix-', + * + * init: function(parent) { + * this._super(parent); + * // 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 * /); + * + * // if you have some asynchronous operations, it's a good idea to return + * // a promise in start() + * var promise = this.rpc(...); + * return promise; + * } + * }); + * + * Now this class can simply be used with the following syntax: + * + * var my_widget = new MyWidget(this); + * my_widget.appendTo($(".some-div")); + * + * With these two lines, the MyWidget instance was inited, rendered, it was inserted into the + * DOM inside the ".some-div" div and its events were binded. + * + * And of course, when you don't need that widget anymore, just do: + * + * my_widget.stop(); + * + * That will kill the widget in a clean way and erase its content from the dom. + */ openerp.web.Widget = openerp.web.SessionAware.extend(/** @lends openerp.web.Widget# */{ /** * The name of the QWeb template that will be used for rendering. Must be @@ -889,6 +944,7 @@ 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 1eb57241fa6..e5d323c8a96 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -356,7 +356,7 @@ openerp.web.DataSet = openerp.web.Widget.extend( /** @lends openerp.web.DataSet * @returns {$.Deferred} */ write: function (id, data, options, callback, error_callback) { - var options = options || {}; + options = options || {}; return this.rpc('/web/dataset/save', { model: this.model, id: id, diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index f6c53b4ad17..5db66ab7260 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -2,6 +2,7 @@ openerp.web.search = function(openerp) { var QWeb = openerp.web.qweb; openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.SearchView# */{ + template: "EmptyComponent", /** * @constructs openerp.web.SearchView * @extends openerp.web.Widget @@ -12,8 +13,8 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search * @param view_id * @param defaults */ - init: function(parent, element_id, dataset, view_id, defaults) { - this._super(parent, element_id); + init: function(parent, dataset, view_id, defaults) { + this._super(parent); this.dataset = dataset; this.model = dataset.model; this.view_id = view_id; @@ -28,6 +29,7 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search this.ready = $.Deferred(); }, start: function() { + this._super(); this.rpc("/web/searchview/load", {"model": this.model, "view_id":this.view_id}, this.on_loaded); return this.ready.promise(); }, @@ -463,30 +465,6 @@ openerp.web.search.Widget = openerp.web.Widget.extend( /** @lends openerp.web.se })); } }); -openerp.web.search.FilterGroup = openerp.web.search.Widget.extend(/** @lends openerp.web.search.FilterGroup# */{ - template: 'SearchView.filters', - /** - * Inclusive group of filters, creates a continuous "button" with clickable - * sections (the normal display for filters is to be a self-contained button) - * - * @constructs openerp.web.search.FilterGroup - * @extends openerp.web.search.Widget - * - * @param {Array} filters elements of the group - * @param {openerp.web.SearchView} view view in which the filters are contained - */ - init: function (filters, view) { - this._super(view); - this.filters = filters; - this.length = filters.length; - }, - start: function () { - this._super(); - _.each(this.filters, function (filter) { - filter.start(); - }); - } -}); openerp.web.search.add_expand_listener = function($root) { $root.find('a.searchview_group_string').click(function (e) { $root.toggleClass('folded expanded'); @@ -533,6 +511,49 @@ openerp.web.search.Input = openerp.web.search.Widget.extend( /** @lends openerp. "get_domain not implemented for widget " + this.attrs.type); } }); +openerp.web.search.FilterGroup = openerp.web.search.Input.extend(/** @lends openerp.web.search.FilterGroup# */{ + template: 'SearchView.filters', + /** + * Inclusive group of filters, creates a continuous "button" with clickable + * sections (the normal display for filters is to be a self-contained button) + * + * @constructs openerp.web.search.FilterGroup + * @extends openerp.web.search.Input + * + * @param {Array} filters elements of the group + * @param {openerp.web.SearchView} view view in which the filters are contained + */ + init: function (filters, view) { + this._super(view); + this.filters = filters; + this.length = filters.length; + }, + start: function () { + this._super(); + _.each(this.filters, function (filter) { + filter.start(); + }); + }, + get_context: function () { }, + /** + * Handles domains-fetching for all the filters within it: groups them. + */ + get_domain: function () { + var domains = _(this.filters).chain() + .filter(function (filter) { return filter.is_enabled(); }) + .map(function (filter) { return filter.attrs.domain; }) + .value(); + + if (!domains.length) { return; } + if (domains.length === 1) { return domains[0]; } + for (var i=domains.length; --i;) { + domains.unshift(['|']); + } + return _.extend(new openerp.web.CompoundDomain(), { + __domains: domains + }); + } +}); openerp.web.search.Filter = openerp.web.search.Input.extend(/** @lends openerp.web.search.Filter# */{ template: 'SearchView.filter', /** @@ -586,12 +607,11 @@ openerp.web.search.Filter = openerp.web.search.Input.extend(/** @lends openerp.w } return this.attrs.context; }, - get_domain: function () { - if (!this.is_enabled()) { - return; - } - return this.attrs.domain; - } + /** + * Does not return anything: filter domain is handled at the FilterGroup + * level + */ + get_domain: function () { } }); openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.web.search.Field# */ { template: 'SearchView.field', diff --git a/addons/web/static/src/js/form.js b/addons/web/static/src/js/view_form.js similarity index 94% rename from addons/web/static/src/js/form.js rename to addons/web/static/src/js/view_form.js index c4540d591a2..d3293b4f2ad 100644 --- a/addons/web/static/src/js/form.js +++ b/addons/web/static/src/js/view_form.js @@ -10,20 +10,19 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# * view should be displayed (if there is one active). */ searchable: false, - template: "FormView", + form_template: "FormView", /** * @constructs openerp.web.FormView * @extends openerp.web.View * * @param {openerp.web.Session} session the current openerp session - * @param {String} element_id this view's root element id * @param {openerp.web.DataSet} dataset the dataset this view will work with * @param {String} view_id the identifier of the OpenERP view object * * @property {openerp.web.Registry} registry=openerp.web.form.widgets widgets registry for this form view instance */ - init: function(parent, element_id, dataset, view_id, options) { - this._super(parent, element_id); + init: function(parent, dataset, view_id, options) { + this._super(parent); this.set_default_options(options); this.dataset = dataset; this.model = dataset.model; @@ -35,7 +34,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# this.datarecord = {}; this.ready = false; this.show_invalid = true; - this.dirty = false; + this.dirty_for_user = false; this.default_focus_field = null; this.default_focus_button = null; this.registry = openerp.web.form.widgets; @@ -45,6 +44,10 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# _.defaults(this.options, {"always_show_new_button": true}); }, start: function() { + this._super(); + return this.init_view(); + }, + init_view: function() { if (this.embedded_view) { var def = $.Deferred().then(this.on_loaded); var self = this; @@ -75,7 +78,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# this.fields_view = data; var frame = new (this.registry.get_object('frame'))(this, this.fields_view.arch); - this.$element.html(QWeb.render(this.template, { 'frame': frame, 'view': this })); + this.$element.html(QWeb.render(this.form_template, { 'frame': frame, 'view': this })); _.each(this.widgets, function(w) { w.start(); }); @@ -89,12 +92,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# this.$form_header.find('button.oe_form_button_save_edit').click(this.do_save_edit); 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); - - if (this.session.debug) { - this.$form_header.find('button.oe_get_xml_view').click(function() { - $('' + openerp.web.json_node_to_xml(self.fields_view.arch, true) + '').dialog({ width: '95%', height: 600}); - }); - } + this.$form_header.find('button.oe_form_button_duplicate').click(this.on_button_duplicate); if (this.options.sidebar && this.options.sidebar_id) { this.sidebar = new openerp.web.Sidebar(this, this.options.sidebar_id); @@ -141,7 +139,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# this.$form_header.find('.oe_form_on_update').show(); this.$form_header.find('button.oe_form_button_new').show(); } - this.dirty = false; + this.dirty_for_user = false; this.datarecord = record; for (var f in this.fields) { var field = this.fields[f]; @@ -151,7 +149,6 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# } if (!record.id) { // New record: Second pass in order to trigger the onchanges - this.dirty = true; this.show_invalid = false; for (var f in record) { var field = this.fields[f]; @@ -180,21 +177,23 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# } }, on_pager_action: function(action) { - switch (action) { - case 'first': - this.dataset.index = 0; - break; - case 'previous': - this.dataset.previous(); - break; - case 'next': - this.dataset.next(); - break; - case 'last': - this.dataset.index = this.dataset.ids.length - 1; - break; + if (this.can_be_discarded()) { + switch (action) { + case 'first': + this.dataset.index = 0; + break; + case 'previous': + this.dataset.previous(); + break; + case 'next': + this.dataset.next(); + break; + case 'last': + this.dataset.index = this.dataset.ids.length - 1; + break; + } + this.reload(); } - this.reload(); }, do_update_pager: function(hide_index) { var $pager = this.$element.find('#' + this.element_id + '_header div.oe_form_pager'); @@ -276,7 +275,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# processed.push(field.name); if (field.get_value() != value) { field.set_value(value); - field.dirty = true; + field.dirty = this.dirty_for_user = true; if (_.indexOf(processed, field.name) < 0) { this.do_onchange(field, processed); } @@ -304,13 +303,31 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# var self = this; var def = $.Deferred(); $.when(this.has_been_loaded).then(function() { - self.dataset.default_get( - _.keys(self.fields_view.fields)).then(self.on_record_loaded).then(function() { + if (self.can_be_discarded()) { + self.dataset.default_get(_.keys(self.fields_view.fields)).then(self.on_record_loaded).then(function() { def.resolve(); - }); + }); + } }); return def.promise(); }, + on_button_duplicate: function() { + var self = this; + var def = $.Deferred(); + $.when(this.has_been_loaded).then(function() { + if (self.can_be_discarded()) { + self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) { + return self.on_created({ result : new_id }); + }).then(function() { + def.resolve(); + }); + } + }); + return def.promise(); + }, + can_be_discarded: function() { + return !this.dirty_for_user || confirm(_t("Warning, the record has been modified, your changes will be discarded.")); + }, /** * Triggers saving the form's record. Chooses between creating a new * record or saving an existing one depending on whether the record @@ -385,7 +402,6 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# if (!r.result) { // should not happen in the server, but may happen for internal purpose } else { - console.debug(_.sprintf("The record #%s has been saved.", this.datarecord.id)); if (success) { success(r); } @@ -466,11 +482,11 @@ openerp.web.FormDialog = openerp.web.Dialog.extend({ }, start: function() { this._super(); - this.form = new openerp.web.FormView(this, this.element_id, this.dataset, this.view_id, { + this.form = new openerp.web.FormView(this, this.dataset, this.view_id, { sidebar: false, pager: false }); - this.form.start(); + this.form.appendTo(this.$element); this.form.on_created.add_last(this.on_form_dialog_saved); this.form.on_saved.add_last(this.on_form_dialog_saved); return this; @@ -815,7 +831,7 @@ openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({ }, on_click: function(saved) { var self = this; - if (!this.node.attrs.special && this.view.dirty && saved !== true) { + 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); }); @@ -842,7 +858,7 @@ openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({ on_confirmed: function() { var self = this; - this.view.execute_action( + this.view.do_execute_action( this.node.attrs, this.view.dataset, this.view.datarecord.id, function () { self.view.reload(); }); @@ -963,7 +979,7 @@ openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.f } }, on_ui_change: function() { - this.dirty = this.view.dirty = true; + this.dirty = this.view.dirty_for_user = true; this.validate(); if (this.is_valid()) { this.set_value_from_ui(); @@ -1111,37 +1127,51 @@ openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({ this.jqueryui_object = 'datetimepicker'; }, start: function() { + var self = this; this._super.apply(this, arguments); - this.$element.find('input').change(this.on_ui_change)[this.jqueryui_object]({ - dateFormat: 'yy-mm-dd', - timeFormat: 'hh:mm:ss', - showOn: 'button', - buttonImage: '/web/static/src/img/ui/field_calendar.png', - buttonImageOnly: true, - constrainInput: false + this.$element.find('input').change(this.on_ui_change); + this.picker({ + onSelect: this.on_picker_select, + changeMonth: true, + changeYear: true, + showWeek: true, + showButtonPanel: false + }); + this.$element.find('img.oe_datepicker_trigger').click(function() { + if (!self.readonly) { + self.picker('setDate', self.value || new Date()); + self.$element.find('.oe_datepicker').toggle(); + } + }); + this.$element.find('.ui-datepicker-inline').removeClass('ui-widget-content ui-corner-all'); + this.$element.find('button.oe_datepicker_close').click(function() { + self.$element.find('.oe_datepicker').hide(); }); }, + picker: function() { + return $.fn[this.jqueryui_object].apply(this.$element.find('.oe_datepicker_container'), arguments); + }, + on_picker_select: function(text, instance) { + var date = this.picker('getDate'); + this.$element.find('input').val(date ? this.format_client(date) : '').change(); + }, set_value: function(value) { - this._super.apply(this, arguments); - if (!value) { - this.$element.find('input').val(''); - } else { - this.$element.find('input').unbind('change'); - // jQuery UI date picker wrongly call on_change event herebelow - this.$element.find('input')[this.jqueryui_object]('setDate', this.parse(value)); - this.$element.find('input').change(this.on_ui_change); - } + value = this.parse(value); + this._super(value); + this.$element.find('input').val(value ? this.format_client(value) : ''); + }, + get_value: function() { + return this.format(this.value); }, set_value_from_ui: function() { - this.value = this.$element.find('input')[this.jqueryui_object]('getDate') || false; - if (this.value) { - this.value = this.format(this.value); - } + var value = this.$element.find('input').val() || false; + this.value = this.parse_client(value); this._super(); }, update_dom: function() { this._super.apply(this, arguments); - this.$element.find('input').datepicker(this.readonly ? 'disable' : 'enable'); + this.$element.find('input').attr('disabled', this.readonly); + this.$element.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', this.readonly); }, validate: function() { this.invalid = false; @@ -1149,15 +1179,26 @@ openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({ if (value === "") { this.invalid = this.required; } else { - this.invalid = !this.$element.find('input')[this.jqueryui_object]('getDate'); + try { + this.parse_client(value); + this.invalid = false; + } catch(e) { + this.invalid = true; + } } }, 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); + }, + format_client: function(v) { + return openerp.web.format_value(v, this.field); } }); @@ -1165,6 +1206,10 @@ openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({ init: function(view, node) { this._super(view, node); this.jqueryui_object = 'datepicker'; + }, + on_picker_select: function(text, instance) { + this._super(text, instance); + this.$element.find('.oe_datepicker').hide(); } }); @@ -1979,7 +2024,7 @@ openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({ self.on_ui_change(); }); - this.list_view = new openerp.web.form.Many2ManyListView(this, this.list_id, this.dataset, false, { + this.list_view = new openerp.web.form.Many2ManyListView(this, this.dataset, false, { 'addable': 'Add', 'selectable': self.multi_selection }); @@ -1988,7 +2033,7 @@ openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({ self.is_started.resolve(); }); setTimeout(function () { - self.list_view.start(); + self.list_view.appendTo($("#" + self.list_id)); }, 0); }, set_value: function(value) { @@ -2098,7 +2143,7 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope this.searchview.stop(); } this.searchview = new openerp.web.SearchView(this, - this.element_id + "_search", this.dataset, false, { + this.dataset, false, { "selectable": !this.options.disable_multiple_selection, "deletable": false }); @@ -2127,15 +2172,17 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope self.stop(); }); self.view_list = new openerp.web.form.SelectCreateListView(self, - self.element_id + "_view_list", self.dataset, false, + self.dataset, false, {'deletable': false}); self.view_list.popup = self; - self.view_list.do_show(); - self.view_list.start().then(function() { + self.view_list.appendTo($("#" + self.element_id + "_view_list")).pipe(function() { + self.view_list.do_show(); + }).pipe(function() { self.searchview.do_search(); }); + }); - this.searchview.start(); + this.searchview.appendTo($("#" + this.element_id + "_search")); }, create_row: function(data) { var self = this; @@ -2162,11 +2209,11 @@ 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.element_id + "_view_form", this.dataset, false); + this.view_form = new openerp.web.FormView(this, this.dataset, false); if (this.options.alternative_form_view) { this.view_form.set_embedded_view(this.options.alternative_form_view); } - this.view_form.start(); + this.view_form.appendTo(this.$element.find("#" + this.element_id + "_view_form")); this.view_form.on_loaded.add_last(function() { var $buttons = self.view_form.$element.find(".oe_form_buttons"); $buttons.html(QWeb.render("SelectCreatePopup.form.buttons", {widget:self})); @@ -2271,11 +2318,11 @@ 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.element_id + "_view_form", this.dataset, false); + this.view_form = new openerp.web.FormView(this, this.dataset, false); if (this.options.alternative_form_view) { this.view_form.set_embedded_view(this.options.alternative_form_view); } - this.view_form.start(); + this.view_form.appendTo(this.$element.find("#" + this.element_id + "_view_form")); this.view_form.on_loaded.add_last(function() { var $buttons = self.view_form.$element.find(".oe_form_buttons"); $buttons.html(QWeb.render("FormOpenPopup.form.buttons")); @@ -2506,7 +2553,7 @@ openerp.web.form.FieldBinaryImage = openerp.web.form.FieldBinary.extend({ }); openerp.web.form.FieldStatus = openerp.web.form.Field.extend({ - template: "FieldStatus", + template: "EmptyComponent", start: function() { this._super(); this.selected_value = null; diff --git a/addons/web/static/src/js/list.js b/addons/web/static/src/js/view_list.js similarity index 97% rename from addons/web/static/src/js/list.js rename to addons/web/static/src/js/view_list.js index 95a8207144b..f8a89cd34a1 100644 --- a/addons/web/static/src/js/list.js +++ b/addons/web/static/src/js/view_list.js @@ -31,7 +31,6 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# * @extends openerp.web.View * * @param parent parent object - * @param element_id the id of the DOM elements this view should link itself to * @param {openerp.web.DataSet} dataset the dataset the view should work with * @param {String} view_id the listview's identifier, if any * @param {Object} options A set of options used to configure the view @@ -41,12 +40,10 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# * @param {void|String} [options.addable="New"] should the new-record button be displayed, and what should its label be. Use ``null`` to hide the button. * @param {Boolean} [options.sortable=true] is it possible to sort the table by clicking on column headers * @param {Boolean} [options.reorderable=true] is it possible to reorder list rows - * - * @borrows openerp.web.ActionExecutor#execute_action as #execute_action */ - init: function(parent, element_id, dataset, view_id, options) { + init: function(parent, dataset, view_id, options) { var self = this; - this._super(parent, element_id); + this._super(parent); this.set_default_options(_.extend({}, this.defaults, options || {})); this.dataset = dataset; this.model = dataset.model; @@ -130,6 +127,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# * @returns {$.Deferred} loading promise */ start: function() { + this._super(); this.$element.addClass('oe-listview'); return this.reload_view(null, null, true); }, @@ -490,7 +488,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# return field.name === name; }); if (!action) { return; } - this.execute_action(action, this.dataset, id, callback); + this.do_execute_action(action, this.dataset, id, callback); }, /** * Handles the activation of a record (clicking on it) @@ -777,6 +775,31 @@ openerp.web.ListView.List = openerp.web.Class.extend( /** @lends openerp.web.Lis this.$current.empty().append( QWeb.render('ListView.rows', _.extend({ render_cell: openerp.web.format_cell}, this))); + this.pad_table_to(5); + }, + pad_table_to: function (count) { + if (this.records.length >= count || + _(this.columns).any(function(column) { return column.meta; })) { + return; + } + var cells = []; + if (this.options.selectable) { + cells.push(''); + } + _(this.columns).each(function(column) { + if (column.invisible !== '1') { + cells.push(' '); + } + }); + if (this.options.deletable) { + cells.push(''); + } + cells.unshift(''); + cells.push(''); + + var row = cells.join(''); + this.$current.append(new Array(count - this.records.length + 1).join(row)); + this.refresh_zebra(this.records.length); }, /** * Gets the ids of all currently selected records, if any @@ -1139,12 +1162,14 @@ openerp.web.ListView.Groups = openerp.web.Class.extend( /** @lends openerp.web.L } // ondrop, move relevant record & fix sequences list.$current.sortable({ + axis: 'y', + items: '> tr[data-id]', stop: function (event, ui) { var to_move = list.records.get(ui.item.data('id')), target_id = ui.item.prev().data('id'); list.records.remove(to_move); - var to = target_id ? list.records.indexOf(list.records.get(target_id)) : 0; + var to = target_id ? list.records.indexOf(list.records.get(target_id)) + 1 : 0; list.records.add(to_move, { at: to }); // resequencing time! diff --git a/addons/web/static/src/js/list-editable.js b/addons/web/static/src/js/view_list_editable.js similarity index 97% rename from addons/web/static/src/js/list-editable.js rename to addons/web/static/src/js/view_list_editable.js index c38c43a4c9c..b5a5a3d8c10 100644 --- a/addons/web/static/src/js/list-editable.js +++ b/addons/web/static/src/js/view_list_editable.js @@ -180,11 +180,11 @@ openerp.web.list_editable = function (openerp) { } self.edition = true; self.edition_id = record_id; - self.edition_form = _.extend(new openerp.web.FormView( - self, $new_row.attr('id'), self.dataset, false), { - template: 'ListView.row.form', + self.edition_form = _.extend(new openerp.web.ListEditableFormView(self, self.dataset, false), { + form_template: 'ListView.row.form', registry: openerp.web.list.form.widgets }); + self.edition_form.appendTo($("#" + $new_row.attr('id'))); $.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') @@ -333,4 +333,8 @@ openerp.web.list_editable = function (openerp) { }); list_form_widgets.add(key, new_path); }); + + openerp.web.ListEditableFormView = openerp.web.FormView.extend({ + init_view: function() {} + }); }; diff --git a/addons/web/static/src/js/view_tree.js b/addons/web/static/src/js/view_tree.js index 2a314f57fa8..7988d65805c 100644 --- a/addons/web/static/src/js/view_tree.js +++ b/addons/web/static/src/js/view_tree.js @@ -19,13 +19,12 @@ openerp.web.TreeView = openerp.web.View.extend(/** @lends openerp.web.TreeView# * @extends openerp.web.View * * @param parent - * @param element_id * @param dataset * @param view_id * @param options */ - init: function(parent, element_id, dataset, view_id, options) { - this._super(parent, element_id); + init: function(parent, dataset, view_id, options) { + this._super(parent); this.dataset = dataset; this.model = dataset.model; this.view_id = view_id; diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index 5babf74ea2b..5f06297cbbb 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -2,17 +2,21 @@ * OpenERP web library *---------------------------------------------------------*/ -openerp.web.views = function(openerp) { +openerp.web.views = function(db) { -var _t = openerp.web._t; -var QWeb = openerp.web.qweb; +var _t = db.web._t; /** * Registry for all the client actions key: tag value: widget */ -openerp.web.client_actions = new openerp.web.Registry(); +db.web.client_actions = new db.web.Registry(); -openerp.web.ActionManager = openerp.web.Widget.extend({ +/** + * Registry for all the main views + */ +db.web.views = new db.web.Registry(); + +db.web.ActionManager = db.web.Widget.extend({ identifier_prefix: "actionmanager", init: function(parent) { this._super(parent); @@ -20,7 +24,6 @@ openerp.web.ActionManager = openerp.web.Widget.extend({ this.dialog = null; this.dialog_viewmanager = null; this.client_widget = null; - this.url = {} }, render: function() { return "
"; @@ -44,8 +47,11 @@ openerp.web.ActionManager = openerp.web.Widget.extend({ } }, url_update: function(action) { + var url = {}; + if(action.id) + url.action_id = action.id; // this.url = { - // "model": action.model, + // "model": action.res_model, // "domain": action.domain, // }; // action.res_model @@ -56,14 +62,15 @@ openerp.web.ActionManager = openerp.web.Widget.extend({ // action.res_id // mode // menu + this.do_url_set_hash(url); }, - url_stringify: function(action) { + do_url_set_hash: function(url) { }, - url_parse: function(action) { - }, - on_url_update: function(url) { - }, - do_url_action: function(url) { + on_url_hashchange: function(url) { + var self = this; + self.rpc("/web/action/load", { action_id: url.action_id }, function(result) { + self.do_action(result.result); + }); }, do_action: function(action, on_close) { var type = action.type.replace(/\./g,'_'); @@ -84,20 +91,20 @@ openerp.web.ActionManager = openerp.web.Widget.extend({ ir_actions_act_window: function (action, on_close) { if (action.target === 'new') { if (this.dialog == null) { - this.dialog = new openerp.web.Dialog(this, { title: action.name, width: '80%' }); + this.dialog = new db.web.Dialog(this, { title: action.name, width: '80%' }); if(on_close) this.dialog.on_close.add(on_close); this.dialog.start(); } else { this.dialog_viewmanager.stop(); } - this.dialog_viewmanager = new openerp.web.ViewManagerAction(this, action); + this.dialog_viewmanager = new db.web.ViewManagerAction(this, action); this.dialog_viewmanager.appendTo(this.dialog.$element); this.dialog.open(); } else { this.dialog_stop(); this.content_stop(); - this.inner_viewmanager = new openerp.web.ViewManagerAction(this, action); + this.inner_viewmanager = new db.web.ViewManagerAction(this, action); this.inner_viewmanager.appendTo(this.$element); this.url_update(action); } @@ -122,25 +129,33 @@ openerp.web.ActionManager = openerp.web.Widget.extend({ }, ir_actions_client: function (action) { this.content_stop(); - var ClientWidget = openerp.web.client_actions.get_object(action.tag); + var ClientWidget = db.web.client_actions.get_object(action.tag); (this.client_widget = new ClientWidget(this, action.params)).appendTo(this); }, ir_actions_report_xml: function(action) { + var self = this; $.blockUI(); - this.session.get_file({ - url: '/web/report', - data: {action: JSON.stringify(action)}, - complete: $.unblockUI + self.rpc("/web/session/eval_domain_and_context", { + contexts: [action.context], + domains: [] + }).then(function(res) { + action = _.clone(action); + action.context = res.context; + self.session.get_file({ + url: '/web/report', + data: {action: JSON.stringify(action)}, + complete: $.unblockUI + }); }); } }); -openerp.web.ViewManager = openerp.web.Widget.extend(/** @lends openerp.web.ViewManager# */{ +db.web.ViewManager = db.web.Widget.extend(/** @lends db.web.ViewManager# */{ identifier_prefix: "viewmanager", template: "ViewManager", /** - * @constructs openerp.web.ViewManager - * @extends openerp.web.Widget + * @constructs db.web.ViewManager + * @extends db.web.Widget * * @param parent * @param dataset @@ -155,10 +170,13 @@ openerp.web.ViewManager = openerp.web.Widget.extend(/** @lends openerp.web.View this.views_src = _.map(views, function(x) {return x instanceof Array? {view_id: x[0], view_type: x[1]} : x;}); this.views = {}; this.flags = this.flags || {}; - this.registry = openerp.web.views; + this.registry = db.web.views; }, render: function() { - return QWeb.render(this.template, {"prefix": this.element_id, views: this.views_src}) + return db.web.qweb.render(this.template, { + self: this, + prefix: this.element_id, + views: this.views_src}); }, /** * @returns {jQuery.Deferred} initial view loading promise @@ -202,8 +220,7 @@ openerp.web.ViewManager = openerp.web.Widget.extend(/** @lends openerp.web.View if (!view.controller) { // Lazy loading of views var controllerclass = this.registry.get_object(view_type); - var controller = new controllerclass(this, this.element_id + '_view_' + view_type, - this.dataset, view.view_id, view.options); + var controller = new controllerclass(this, this.dataset, view.view_id, view.options); if (view.embedded_view) { controller.set_embedded_view(view.embedded_view); } @@ -222,7 +239,8 @@ openerp.web.ViewManager = openerp.web.Widget.extend(/** @lends openerp.web.View unique: true }); } - view_promise = controller.start(); + var container = $("#" + this.element_id + '_view_' + view_type); + view_promise = controller.appendTo(container); $.when(view_promise).then(function() { self.on_controller_inited(view_type, controller); }); @@ -274,15 +292,15 @@ openerp.web.ViewManager = openerp.web.Widget.extend(/** @lends openerp.web.View if (this.searchview) { this.searchview.stop(); } - this.searchview = new openerp.web.SearchView( - this, this.element_id + "_search", this.dataset, + this.searchview = new db.web.SearchView( + this, this.dataset, view_id, search_defaults); this.searchview.on_search.add(function(domains, contexts, groupbys) { var controller = self.views[self.active_view].controller; controller.do_search.call(controller, domains, contexts, groupbys); }); - return this.searchview.start(); + return this.searchview.appendTo($("#" + this.element_id + "_search")); }, /** * Called when one of the view want to execute an action @@ -294,16 +312,20 @@ openerp.web.ViewManager = openerp.web.Widget.extend(/** @lends openerp.web.View on_remove: function() { }, on_edit: function() { - } + }, + /** + * Called by children view after executing an action + */ + on_action_executed: function () {} }); -openerp.web.ViewManagerAction = openerp.web.ViewManager.extend(/** @lends oepnerp.web.ViewManagerAction# */{ +db.web.ViewManagerAction = db.web.ViewManager.extend(/** @lends oepnerp.web.ViewManagerAction# */{ template:"ViewManagerAction", /** - * @constructs openerp.web.ViewManagerAction - * @extends openerp.web.ViewManager + * @constructs db.web.ViewManagerAction + * @extends db.web.ViewManager * - * @param {openerp.web.ActionManager} parent parent object/widget + * @param {db.web.ActionManager} parent parent object/widget * @param {Object} action descriptor for the action this viewmanager needs to manage its views. */ init: function(parent, action) { @@ -312,7 +334,7 @@ openerp.web.ViewManagerAction = openerp.web.ViewManager.extend(/** @lends oepner // ``_super()``) rpc requests will blow up. this.session = parent.session; this.action = action; - var dataset = new openerp.web.DataSetSearch(this, action.res_model, action.context, action.domain); + var dataset = new db.web.DataSetSearch(this, action.res_model, action.context, action.domain); if (action.res_id) { dataset.ids.push(action.res_id); dataset.index = 0; @@ -324,6 +346,12 @@ openerp.web.ViewManagerAction = openerp.web.ViewManager.extend(/** @lends oepner // buttons, sidebar, ...) displaying this.flags.search_view = this.flags.pager = this.flags.sidebar = this.flags.action_buttons = false; } + + // setup storage for session-wise menu hiding + if (this.session.hidden_menutips) { + return; + } + this.session.hidden_menutips = {} }, /** * Initializes the ViewManagerAction: sets up the searchview (if the @@ -332,6 +360,8 @@ openerp.web.ViewManagerAction = openerp.web.ViewManager.extend(/** @lends oepner * launches an initial search after both views are done rendering. */ start: function() { + var self = this; + var searchview_loaded; if (this.flags.search_view !== false) { var search_defaults = {}; @@ -355,19 +385,55 @@ openerp.web.ViewManagerAction = openerp.web.ViewManager.extend(/** @lends oepner // schedule auto_search manager_ready.then(this.searchview.do_search); } + + this.$element.find('.oe_get_xml_view').click(function () { + // TODO: add search view? + $('
').text(db.web.json_node_to_xml(
+                self.views[self.active_view].controller.fields_view.arch, true))
+                    .dialog({ width: '95%'});
+        });
+        if (this.action.help && !this.flags.low_profile) {
+            var Users = new db.web.DataSet(self, 'res.users'),
+                header = this.$element.find('.oe-view-manager-header');
+            header.delegate('blockquote button', 'click', function() {
+                var $this = $(this);
+                //noinspection FallthroughInSwitchStatementJS
+                switch ($this.attr('name')) {
+                case 'disable':
+                    Users.write(self.session.uid, {menu_tips:false});
+                case 'hide':
+                    $this.closest('blockquote').hide();
+                    self.session.hidden_menutips[self.action.id] = true;
+                }
+            });
+            if (!(self.action.id in self.session.hidden_menutips)) {
+                Users.read_ids([this.session.uid], ['menu_tips'], function(users) {
+                    var user = users[0];
+                    if (!(user && user.id === self.session.uid)) {
+                        return;
+                    }
+                    header.find('blockquote').toggle(user.menu_tips);
+                });
+            }
+        }
+
         return manager_ready;
     },
     on_mode_switch: function (view_type) {
+        var self = this;
         return $.when(
             this._super(view_type),
-            this.shortcut_check(this.views[view_type]));
+            this.shortcut_check(this.views[view_type])).then(function () {
+                var view_id = self.views[self.active_view].controller.fields_view.view_id;
+                self.$element.find('.oe_get_xml_view span').text(view_id);
+        });
     },
     shortcut_check : function(view) {
         var self = this;
         var grandparent = this.widget_parent && this.widget_parent.widget_parent;
         // display shortcuts if on the first view for the action
         var $shortcut_toggle = this.$element.find('.oe-shortcut-toggle');
-        if (!(grandparent instanceof openerp.web.WebClient) ||
+        if (!(grandparent instanceof db.web.WebClient) ||
             !(view.view_type === this.views_src[0].view_type
                 && view.view_id === this.views_src[0].view_id)) {
             $shortcut_toggle.hide();
@@ -399,10 +465,39 @@ openerp.web.ViewManagerAction = openerp.web.ViewManager.extend(/** @lends oepner
                     $shortcut_toggle.addClass("oe-shortcut-remove");
                 }
             });
+    },
+    /**
+     * Intercept do_action resolution from children views
+     */
+    on_action_executed: function () {
+        new db.web.DataSet(this, 'res.log')
+                .call('get', [], this.do_display_log);
+    },
+    /**
+     * @param {Array} log_records
+     */
+    do_display_log: function (log_records) {
+        var self = this,
+            $logs = this.$element.find('ul.oe-view-manager-logs:first').empty();
+        _(log_records).each(function (record) {
+            $(_.sprintf('
  • %s
  • ', record.name)) + .appendTo($logs) + .delegate('a', 'click', function (e) { + self.do_action({ + type: 'ir.actions.act_window', + res_model: record.res_model, + res_id: record.res_id, + // TODO: need to have an evaluated context here somehow + //context: record.context, + views: [[false, 'form']] + }); + return false; + }); + }); } }); -openerp.web.Sidebar = openerp.web.Widget.extend({ +db.web.Sidebar = db.web.Widget.extend({ init: function(parent, element_id) { this._super(parent, element_id); this.items = {}; @@ -411,7 +506,7 @@ openerp.web.Sidebar = openerp.web.Widget.extend({ start: function() { this._super(this); var self = this; - this.$element.html(QWeb.render('Sidebar')); + this.$element.html(db.web.qweb.render('Sidebar')); this.$element.find(".toggle-sidebar").click(function(e) { self.do_toggle(); }); @@ -453,7 +548,7 @@ openerp.web.Sidebar = openerp.web.Widget.extend({ this.items[items[i].element_id] = items[i]; } } - var $section = $(QWeb.render("Sidebar.section", { + var $section = $(db.web.qweb.render("Sidebar.section", { section_id: section_id, name: name, classname: 'oe_sidebar_' + code, @@ -509,7 +604,7 @@ openerp.web.Sidebar = openerp.web.Widget.extend({ } }); -openerp.web.TranslateDialog = openerp.web.Dialog.extend({ +db.web.TranslateDialog = db.web.Dialog.extend({ dialog_title: _t("Translations"), init: function(view) { // TODO fme: should add the language to fields_view_get because between the fields view get @@ -529,14 +624,14 @@ openerp.web.TranslateDialog = openerp.web.Dialog.extend({ this.translatable_fields_keys = _.map(this.view.translatable_fields || [], function(i) { return i.name }); this.languages = null; this.languages_loaded = $.Deferred(); - (new openerp.web.DataSetSearch(this, 'res.lang', this.view.dataset.get_context(), + (new db.web.DataSetSearch(this, 'res.lang', this.view.dataset.get_context(), [['translatable', '=', '1']])).read_slice(['code', 'name'], { sort: 'id' }, this.on_languages_loaded); }, start: function() { var self = this; this._super(); $.when(this.languages_loaded).then(function() { - self.$element.html(QWeb.render('TranslateDialog', { widget: self })); + self.$element.html(db.web.qweb.render('TranslateDialog', { widget: self })); self.$element.tabs(); if (!(self.view.translatable_fields && self.view.translatable_fields.length)) { self.hide_tabs('fields'); @@ -641,11 +736,8 @@ openerp.web.TranslateDialog = openerp.web.Dialog.extend({ } }); -/** - * @class - * @extends openerp.web.Widget - */ -openerp.web.View = openerp.web.Widget.extend(/** @lends openerp.web.View# */{ +db.web.View = db.web.Widget.extend(/** @lends db.web.View# */{ + template: "EmptyComponent", set_default_options: function(options) { this.options = options || {}; _.defaults(this.options, { @@ -658,7 +750,7 @@ openerp.web.View = openerp.web.Widget.extend(/** @lends openerp.web.View# */{ }, open_translate_dialog: function(field) { if (!this.translate_dialog) { - this.translate_dialog = new openerp.web.TranslateDialog(this).start(); + this.translate_dialog = new db.web.TranslateDialog(this).start(); } this.translate_dialog.open(field); }, @@ -670,12 +762,16 @@ openerp.web.View = openerp.web.Widget.extend(/** @lends openerp.web.View# */{ * @param {String} [action_data.special=null] special action handlers (currently: only ``'cancel'``) * @param {String} [action_data.type='workflow'] the action type, if present, one of ``'object'``, ``'action'`` or ``'workflow'`` * @param {Object} [action_data.context=null] additional action context, to add to the current context - * @param {openerp.web.DataSet} dataset a dataset object used to communicate with the server + * @param {db.web.DataSet} dataset a dataset object used to communicate with the server * @param {Object} [record_id] the identifier of the object on which the action is to be applied * @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action) */ - execute_action: function (action_data, dataset, record_id, on_closed) { + do_execute_action: function (action_data, dataset, record_id, on_closed) { var self = this; + var result_handler = function () { + if (on_closed) { on_closed.apply(null, arguments); } + self.widget_parent.on_action_executed.apply(null, arguments); + }; var handler = function (r) { var action = r.result; if (action && action.constructor == Object) { @@ -685,14 +781,14 @@ openerp.web.View = openerp.web.Widget.extend(/** @lends openerp.web.View# */{ active_ids: [record_id || false], active_model: dataset.model }); - action.context = new openerp.web.CompoundContext(dataset.get_context(), action.context); - self.do_action(action, on_closed); - } else if (on_closed) { - on_closed(action); + action.context = new db.web.CompoundContext(dataset.get_context(), action.context); + self.do_action(action, result_handler); + } else { + result_handler(); } }; - var context = new openerp.web.CompoundContext(dataset.get_context(), action_data.context || {}); + var context = new db.web.CompoundContext(dataset.get_context(), action_data.context || {}); if (action_data.special) { handler({result: {"type":"ir.actions.act_window_close"}}); @@ -707,7 +803,7 @@ openerp.web.View = openerp.web.Widget.extend(/** @lends openerp.web.View# */{ /** * Directly set a view to use instead of calling fields_view_get. This method must * be called before start(). When an embedded view is set, underlying implementations - * of openerp.web.View must use the provided view instead of any other one. + * of db.web.View must use the provided view instead of any other one. * * @param embedded_view A view. */ @@ -754,8 +850,7 @@ openerp.web.View = openerp.web.Widget.extend(/** @lends openerp.web.View# */{ }, on_sidebar_manage_view: function() { if (this.fields_view && this.fields_view.arch) { -// $('' + openerp.base.json_node_to_xml(this.fields_view.arch, true) + '').dialog({ width: '95%', height: 600}); - var view_editor = new openerp.web.ViewEditor(this, this.$element, this.dataset, this.fields_view.arch) + var view_editor = new openerp.web.ViewEditor(this, this.$element, this.dataset, this.fields_view.arch); view_editor.start(); } else { this.notification.warn("Manage Views", "Could not find current view declaration"); @@ -770,7 +865,7 @@ openerp.web.View = openerp.web.Widget.extend(/** @lends openerp.web.View# */{ on_sidebar_import: function() { }, on_sidebar_export: function() { - var export_view = new openerp.web.DataExport(this, this.dataset); + var export_view = new db.web.DataExport(this, this.dataset); export_view.start(); }, on_sidebar_translate: function() { @@ -780,12 +875,7 @@ openerp.web.View = openerp.web.Widget.extend(/** @lends openerp.web.View# */{ } }); -/** - * Registry for all the main views - */ -openerp.web.views = new openerp.web.Registry(); - -openerp.web.json_node_to_xml = function(node, single_quote, indent) { +db.web.json_node_to_xml = function(node, single_quote, indent) { // For debugging purpose, this function will convert a json node back to xml // Maybe usefull for xml view editor @@ -814,7 +904,7 @@ openerp.web.json_node_to_xml = function(node, single_quote, indent) { r += '>\n'; var childs = []; for (var i = 0, ii = node.children.length; i < ii; i++) { - childs.push(openerp.web.json_node_to_xml(node.children[i], single_quote, indent + 1)); + childs.push(db.web.json_node_to_xml(node.children[i], single_quote, indent + 1)); } r += childs.join('\n'); r += '\n' + sindent + ''; diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 0283bcc021e..6251b5ba339 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -420,12 +420,14 @@
    -
    - - - +
    +
    + + + +
    @@ -442,9 +444,26 @@ - + +
    +

    +
    + + +
    +
    + href="javascript: void(0)"> +

    + + +

    +
    + +
      @@ -502,7 +521,6 @@
      -

      @@ -641,7 +659,6 @@
      -

      +
      @@ -802,11 +820,13 @@ - + + +