diff --git a/addons/base/__init__.py b/addons/base/__init__.py index ee5959455ad..343b53d050d 100644 --- a/addons/base/__init__.py +++ b/addons/base/__init__.py @@ -1 +1,6 @@ +import common import controllers + +# TODO +# if we detect that we are imported from the openerp server register common.Root() as a wsgi entry point + diff --git a/addons/base/common/__init__.py b/addons/base/common/__init__.py new file mode 100644 index 00000000000..60699c86f43 --- /dev/null +++ b/addons/base/common/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/python +# TODO if from openerpserver use backendlocal +# from backendlocal import * +from backendrpc import * +from dispatch import * diff --git a/openerpweb/ast.py b/addons/base/common/ast.py similarity index 100% rename from openerpweb/ast.py rename to addons/base/common/ast.py diff --git a/addons/base/common/backendlocal.py b/addons/base/common/backendlocal.py new file mode 100644 index 00000000000..dac33991c4d --- /dev/null +++ b/addons/base/common/backendlocal.py @@ -0,0 +1,23 @@ +#---------------------------------------------------------- +# 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/base/common/backendrpc.py b/addons/base/common/backendrpc.py new file mode 100644 index 00000000000..e6c59000caf --- /dev/null +++ b/addons/base/common/backendrpc.py @@ -0,0 +1,199 @@ +#!/usr/bin/python +import datetime +import dateutil.relativedelta +import time +import xmlrpclib + +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): + """ + An OpenERP RPC session, a given user can own multiple such sessions + in a web session. + + .. attribute:: context + + The session context, a ``dict``. Can be reloaded by calling + :meth:`openerpweb.openerpweb.OpenERPSession.get_context` + + .. attribute:: domains_store + + A ``dict`` matching domain keys to evaluable (but non-literal) domains. + + 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): + 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 = {} + self.domains_store = {} + self._lang = {} + self.remote_timezone = 'utc' + self.client_timezone = False + + def proxy(self, service): + s = xmlrpclib.ServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service)) + return s + + def bind(self, db, uid, password): + self._db = db + self._uid = uid + 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 + + if uid: self.get_context() + return uid + + def assert_valid(self): + """ + Ensures this session is valid (logged into the openerp server) + """ + if not (self._db and self._uid and self._password): + raise OpenERPUnboundException() + + 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) + return r + + def exec_workflow(self, model, id, signal): + self.assert_valid() + r = self.proxy('object').exec_workflow(self._db, self._uid, self._password, model, signal, id) + return r + + def model(self, model): + """ Get an RPC proxy for the object ``model``, bound to this session. + + :param model: an OpenERP model name + :type model: str + :rtype: :class:`openerpweb.openerpweb.OpenERPModel` + """ + return self.model_factory(self, model) + + def get_context(self): + """ Re-initializes the current user's session context (based on + his preferences) by calling res.users.get_context() with the old + context + + :returns: the new context + """ + assert self._uid, "The user needs to be logged-in to initialize his context" + 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 + def base_eval_context(self): + """ Default evaluation context for the session. + + Used to evaluate contexts and domains. + """ + base = dict( + uid=self._uid, + current_date=datetime.date.today().strftime('%Y-%m-%d'), + time=time, + datetime=datetime, + relativedelta=dateutil.relativedelta.relativedelta + ) + base.update(self.context) + return base + + def evaluation_context(self, context=None): + """ Returns the session's evaluation context, augmented with the + provided context if any. + + :param dict context: to add merge in the session's base eval context + :returns: the augmented context + :rtype: dict + """ + d = dict(self.base_eval_context) + if context: + d.update(context) + d['context'] = d + return d + + def eval_context(self, context_to_eval, context=None): + """ Evaluates the provided context_to_eval in the context (haha) of + the context. Also merges the evaluated context with the session's context. + + :param context_to_eval: a context to evaluate. Must be a dict or a + non-literal context. If it's a dict, will be + returned as-is + :type context_to_eval: openerpweb.nonliterals.Context + :returns: the evaluated context + :rtype: dict + + :raises: ``TypeError`` if ``context_to_eval`` is neither a dict nor + a Context + """ + ctx = dict( + self.base_eval_context, + **(context or {})) + + # adding the context of the session to send to the openerp server + ccontext = nonliterals.CompoundContext(self.context, context_to_eval or {}) + ccontext.session = self + return ccontext.evaluate(ctx) + + def eval_domain(self, domain, context=None): + """ Evaluates the provided domain using the provided context + (merged with the session's evaluation context) + + :param domain: an OpenERP domain as a list or as a + :class:`openerpweb.nonliterals.Domain` instance + + In the second case, it will be evaluated and returned. + :type domain: openerpweb.nonliterals.Domain + :param dict context: the context to use in the evaluation, if any. + :returns: the evaluated domain + :rtype: list + + :raises: ``TypeError`` if ``domain`` is neither a list nor a Domain + """ + if isinstance(domain, list): + return domain + + cdomain = nonliterals.CompoundDomain(domain) + cdomain.session = self + return cdomain.evaluate(context or {}) diff --git a/openerpweb/dates.py b/addons/base/common/dates.py similarity index 100% rename from openerpweb/dates.py rename to addons/base/common/dates.py diff --git a/addons/base/common/dispatch.py b/addons/base/common/dispatch.py new file mode 100644 index 00000000000..37207cc485d --- /dev/null +++ b/addons/base/common/dispatch.py @@ -0,0 +1,426 @@ +#!/usr/bin/python +from __future__ import with_statement + +import functools +import logging +import os +import sys +import traceback +import uuid +import xmlrpclib + +import simplejson +import werkzeug.datastructures +import werkzeug.exceptions +import werkzeug.urls +import werkzeug.utils +import werkzeug.wrappers +import werkzeug.wsgi + +import ast +import nonliterals +import http +# import backendlocal as backend +import backendrpc as backend + +__all__ = ['Root', 'jsonrequest', 'httprequest', 'Controller', + 'WebRequest', 'JsonRequest', 'HttpRequest'] + +#----------------------------------------------------------- +# Globals (wont move into a pool) +#----------------------------------------------------------- + +applicationsession = {} +addons_module = {} +addons_manifest = {} +controllers_class = {} +controllers_object = {} +controllers_path = {} + +#---------------------------------------------------------- +# OpenERP Web RequestHandler +#---------------------------------------------------------- +class WebRequest(object): + """ Parent class for all OpenERP Web request types, mostly deals with + initialization and setup of the request object (the dispatching itself has + to be handled by the subclasses) + + :param request: a wrapped werkzeug Request 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 + request + + .. attribute:: httpsession + + a :class:`~collections.Mapping` holding the HTTP session data for the + current http session + + .. attribute:: config + + config parameter provided to the request object + + .. attribute:: params + + :class:`~collections.Mapping` of request parameters, not generally + useful as they're provided directly to the handler method as keyword + arguments + + .. attribute:: session_id + + opaque identifier for the :class:`backend.OpenERPSession` instance of + the current request + + .. attribute:: session + + :class:`~backend.OpenERPSession` instance for the current request + + .. attribute:: context + + :class:`~collections.Mapping` of context values for the current request + + .. attribute:: debug + + ``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.context = self.params.pop('context', None) + self.debug = self.params.pop('debug', False) != False + +class JsonRequest(WebRequest): + """ JSON-RPC2 over HTTP. + + Sucessful request:: + + --> {"jsonrpc": "2.0", + "method": "call", + "params": {"session_id": "SID", + "context": {}, + "arg1": "val1" }, + "id": null} + + <-- {"jsonrpc": "2.0", + "result": { "res1": "val1" }, + "id": null} + + Request producing a error:: + + --> {"jsonrpc": "2.0", + "method": "call", + "params": {"session_id": "SID", + "context": {}, + "arg1": "val1" }, + "id": null} + + <-- {"jsonrpc": "2.0", + "error": {"code": 1, + "message": "End user error message.", + "data": {"code": "codestring", + "debug": "traceback" } }, + "id": null} + + """ + + def dispatch(self, controller, method, requestf=None, request=None): + """ Calls the method asked for by the JSON-RPC2 request + + :param controller: the instance of the controller which received the request + :param method: the method which received the request + :param requestf: a file-like object containing an encoded JSON-RPC2 request + :param request: a JSON-RPC2 request + + :returns: an utf8 encoded JSON-RPC2 reply + """ + response = {"jsonrpc": "2.0" } + error = None + try: + # Read POST content or POST Form Data named "request" + if requestf: + self.jsonrequest = simplejson.load(requestf, object_hook=nonliterals.non_literal_decoder) + else: + self.jsonrequest = simplejson.loads(request, object_hook=nonliterals.non_literal_decoder) + self.init(self.jsonrequest.get("params", {})) + if self.debug or 1: + print "--> %s.%s %s" % (controller.__class__.__name__, method.__name__, self.jsonrequest) + response['id'] = self.jsonrequest.get('id') + response["result"] = method(controller, self, **self.params) + except backend.OpenERPUnboundException: + error = { + 'code': 100, + 'message': "OpenERP Session Invalid", + 'data': { + 'type': 'session_invalid', + 'debug': traceback.format_exc() + } + } + except xmlrpclib.Fault, e: + error = { + 'code': 200, + 'message': "OpenERP Server Error", + 'data': { + 'type': 'server_exception', + 'fault_code': e.faultCode, + 'debug': "Client %s\nServer %s" % ( + "".join(traceback.format_exception("", None, sys.exc_traceback)), e.faultString) + } + } + except Exception: + logging.getLogger('openerp.JSONRequest.dispatch').exception\ + ("An error occured while handling a json request") + error = { + 'code': 300, + 'message': "OpenERP WebClient Error", + 'data': { + 'type': 'client_exception', + 'debug': "Client %s" % traceback.format_exc() + } + } + if error: + response["error"] = error + + if self.debug or 1: + print "<--", response + print + + content = simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder) + return werkzeug.wrappers.Response( + content, headers=[('Content-Type', 'application/json'), + ('Content-Length', len(content))]) + +def jsonrequest(f): + """ Decorator marking the decorated method as being a handler for a + JSON-RPC request (the exact request path is specified via the + ``$(Controller._cp_path)/$methodname`` combination. + + If the method is called, it will be provided with a :class:`JsonRequest` + instance and all ``params`` sent during the JSON-RPC request, apart from + the ``session_id``, ``context`` and ``debug`` keys (which are stripped out + beforehand) + """ + @functools.wraps(f) + def json_handler(controller, request, config): + return JsonRequest(request, config).dispatch( + controller, f, requestf=request.stream) + json_handler.exposed = True + return json_handler + +class HttpRequest(WebRequest): + """ Regular GET/POST request + """ + def dispatch(self, controller, method): + self.init(dict(self.httprequest.args, **self.httprequest.form)) + akw = {} + for key, value in self.httprequest.args.iteritems(): + if isinstance(value, basestring) and len(value) < 1024: + akw[key] = value + else: + akw[key] = type(value) + if self.debug or 1: + print "%s --> %s.%s %r" % (self.httprequest.method, controller.__class__.__name__, method.__name__, akw) + r = method(controller, self, **self.params) + if self.debug or 1: + if isinstance(r, werkzeug.wrappers.BaseResponse): + print '<--', r + else: + print "<--", 'size:', len(r) + print + return r + + def make_response(self, data, headers=None, cookies=None): + """ Helper for non-HTML responses, or HTML responses with custom + response headers or cookies. + + While handlers can just return the HTML markup of a page they want to + send as a string if non-HTML data is returned they need to create a + complete response object, or the returned data will not be correctly + interpreted by the clients. + + :param basestring data: response body + :param headers: HTTP headers to set on the response + :type headers: ``[(name, value)]`` + :param collections.Mapping cookies: cookies to set on the client + """ + response = werkzeug.wrappers.Response(data, headers=headers) + if cookies: + for k, v in cookies.iteritems(): + response.set_cookie(k, v) + return response + + def not_found(self, description=None): + """ Helper for 404 response, return its result from the method + """ + return werkzeug.exceptions.NotFound(description) + +def httprequest(f): + """ Decorator marking the decorated method as being a handler for a + normal HTTP request (the exact request path is specified via the + ``$(Controller._cp_path)/$methodname`` combination. + + If the method is called, it will be provided with a :class:`HttpRequest` + instance and all ``params`` sent during the request (``GET`` and ``POST`` + merged in the same dictionary), apart from the ``session_id``, ``context`` + and ``debug`` keys (which are stripped out beforehand) + """ + @functools.wraps(f) + def http_handler(controller, request, config): + return HttpRequest(request, config).dispatch(controller, f) + http_handler.exposed = True + return http_handler + +class ControllerType(type): + def __init__(cls, name, bases, attrs): + super(ControllerType, cls).__init__(name, bases, attrs) + controllers_class["%s.%s" % (cls.__module__, cls.__name__)] = cls + +class Controller(object): + __metaclass__ = ControllerType + +class Root(object): + """Root WSGI application for the OpenERP Web Client. + + :param options: mandatory initialization options object, must provide + the following attributes: + + ``server_host`` (``str``) + hostname of the OpenERP server to dispatch RPC to + ``server_port`` (``int``) + RPC port of the OpenERP server + ``serve_static`` (``bool | None``) + whether this application should serve the various + addons's static files + ``storage_path`` (``str``) + filesystem path where HTTP session data will be stored + ``dbfilter`` (``str``) + only used in case the list of databases is requested + by the server, will be filtered by this pattern + """ + def __init__(self, options): + self.root = werkzeug.urls.Href('/base/webclient/home') + self.config = options + + self.session_cookie = 'sessionid' + self.addons = {} + + static_dirs = self._load_addons() + if options.serve_static: + self.dispatch = werkzeug.wsgi.SharedDataMiddleware( + self.dispatch, static_dirs) + + if options.session_storage: + if not os.path.exists(options.session_storage): + os.mkdir(options.session_storage, 0700) + self.session_storage = options.session_storage + + def __call__(self, environ, start_response): + """ Handle a WSGI request + """ + return self.dispatch(environ, start_response) + + def dispatch(self, environ, start_response): + """ + Performs the actual WSGI dispatching for the application, may be + wrapped during the initialization of the object. + + Call the object directly. + """ + request = werkzeug.wrappers.Request(environ) + request.parameter_storage_class = werkzeug.datastructures.ImmutableDict + + if request.path == '/': + return werkzeug.utils.redirect( + self.root(request.args), 301)( + environ, start_response) + elif request.path == '/mobile': + return werkzeug.utils.redirect( + '/web_mobile/static/src/web_mobile.html', 301)( + environ, start_response) + + handler = self.find_handler(*(request.path.split('/')[1:])) + + if not handler: + response = werkzeug.exceptions.NotFound() + else: + with http.session(request, self.session_storage, self.session_cookie) as session: + result = handler( + request, self.config) + + if isinstance(result, basestring): + response = werkzeug.wrappers.Response( + result, headers=[('Content-Type', 'text/html; charset=utf-8'), + ('Content-Length', len(result))]) + else: + response = result + + response.set_cookie(self.session_cookie, session.sid) + + return response(environ, start_response) + + def _load_addons(self): + """ + Loads all addons at the specified addons path, returns a mapping of + 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()) + print "Loading", module + m = __import__(module) + addons_module[module] = m + addons_manifest[module] = manifest + + statics['/%s/static' % module] = \ + os.path.join(addons_path, module, 'static') + for k, v in controllers_class.items(): + if k not in controllers_object: + o = v() + controllers_object[k] = o + if hasattr(o, '_cp_path'): + controllers_path[o._cp_path] = o + return statics + + def find_handler(self, *l): + """ + Tries to discover the controller handling the request for the path + specified by the provided parameters + + :param l: path sections to a controller or controller method + :returns: a callable matching the path sections, or ``None`` + :rtype: ``Controller | None`` + """ + if len(l) > 1: + for i in range(len(l), 1, -1): + ps = "/" + "/".join(l[0:i]) + if ps in controllers_path: + c = controllers_path[ps] + rest = l[i:] or ['index'] + meth = rest[0] + m = getattr(c, meth) + if getattr(m, 'exposed', False): + print "Dispatching to", ps, c, meth, m + return m + return None diff --git a/addons/base/common/http.py b/addons/base/common/http.py new file mode 100644 index 00000000000..3f6c96d1f42 --- /dev/null +++ b/addons/base/common/http.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +import contextlib + +import werkzeug.contrib.sessions + +STORES = {} + +@contextlib.contextmanager +def session(request, storage_path, session_cookie='sessionid'): + session_store = STORES.get(storage_path) + if not session_store: + session_store = werkzeug.contrib.sessions.FilesystemSessionStore( + storage_path) + STORES[storage_path] = session_store + + sid = request.cookies.get(session_cookie) + if sid: + request.session = session_store.get(sid) + else: + request.session = session_store.new() + + yield request.session + + session_store.save(request.session) diff --git a/openerpweb/nonliterals.py b/addons/base/common/nonliterals.py similarity index 99% rename from openerpweb/nonliterals.py rename to addons/base/common/nonliterals.py index 5b61f39fc83..937da06ad9e 100644 --- a/openerpweb/nonliterals.py +++ b/addons/base/common/nonliterals.py @@ -7,8 +7,6 @@ can't be sent there themselves). import binascii import hashlib import simplejson.encoder -import time -import datetime __all__ = ['Domain', 'Context', 'NonLiteralEncoder, non_literal_decoder', 'CompoundDomain', 'CompoundContext'] diff --git a/openerpweb/tests/__init__.py b/addons/base/common/tests/__init__.py similarity index 100% rename from openerpweb/tests/__init__.py rename to addons/base/common/tests/__init__.py diff --git a/openerpweb/tests/test_model.py b/addons/base/common/tests/test_model.py similarity index 100% rename from openerpweb/tests/test_model.py rename to addons/base/common/tests/test_model.py diff --git a/openerpweb/tests/test_nonliterals.py b/addons/base/common/tests/test_nonliterals.py similarity index 100% rename from openerpweb/tests/test_nonliterals.py rename to addons/base/common/tests/test_nonliterals.py diff --git a/openerpweb/tests/test_session.py b/addons/base/common/tests/test_session.py similarity index 100% rename from openerpweb/tests/test_session.py rename to addons/base/common/tests/test_session.py diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py index f650fe3eb47..c015adda052 100644 --- a/addons/base/controllers/main.py +++ b/addons/base/controllers/main.py @@ -9,17 +9,20 @@ import re import simplejson import textwrap import xmlrpclib +import time from xml.etree import ElementTree from cStringIO import StringIO -import cherrypy - -import openerpweb -import openerpweb.ast -import openerpweb.nonliterals +import base.common.dispatch as openerpweb +import base.common.ast +import base.common.nonliterals +openerpweb.ast = base.common.ast +openerpweb.nonliterals = base.common.nonliterals from babel.messages.pofile import read_po +_REPORT_POLLER_DELAY = 0.05 + # Should move to openerpweb.Xml2Json class Xml2Json: # xml2json-direct @@ -63,16 +66,16 @@ class Xml2Json: # OpenERP Web base Controllers #---------------------------------------------------------- -def manifest_glob(addons, key): +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(openerpweb.path_addons, addon, pattern)): - files.append(path[len(openerpweb.path_addons):]) + for path in glob.glob(os.path.join(addons_path, addon, pattern)): + files.append(path[len(addons_path):]) return files -def concat_files(file_list): +def concat_files(addons_path, file_list): """ Concatenate file content return (concat,timestamp) concat: concatenation of file content @@ -81,13 +84,13 @@ def concat_files(file_list): files_content = [] files_timestamp = 0 for i in file_list: - fname = os.path.join(openerpweb.path_addons, i[1:]) + fname = os.path.join(addons_path, i[1:]) ftime = os.path.getmtime(fname) if ftime > files_timestamp: files_timestamp = ftime files_content.append(open(fname).read()) files_concat = "".join(files_content) - return (files_concat,files_timestamp) + return files_concat,files_timestamp home_template = textwrap.dedent(""" @@ -117,40 +120,38 @@ class WebClient(openerpweb.Controller): @openerpweb.jsonrequest def csslist(self, req, mods='base'): - return manifest_glob(mods.split(','), 'css') + return manifest_glob(req.config.addons_path, mods.split(','), 'css') @openerpweb.jsonrequest def jslist(self, req, mods='base'): - return manifest_glob(mods.split(','), 'js') + return manifest_glob(req.config.addons_path, mods.split(','), 'js') @openerpweb.httprequest def css(self, req, mods='base'): - req.httpresponse.headers['Content-Type'] = 'text/css' - files = manifest_glob(mods.split(','), 'css') - content,timestamp = concat_files(files) + files = manifest_glob(req.config.addons_path, mods.split(','), 'css') + content,timestamp = concat_files(req.config.addons_path, files) # TODO request set the Date of last modif and Etag - return content + return req.make_response(content, [('Content-Type', 'text/css')]) @openerpweb.httprequest def js(self, req, mods='base'): - req.httpresponse.headers['Content-Type'] = 'application/javascript' - files = manifest_glob(mods.split(','), 'js') - content,timestamp = concat_files(files) + files = manifest_glob(req.config.addons_path, mods.split(','), 'js') + content,timestamp = concat_files(req.config.addons_path, files) # TODO request set the Date of last modif and Etag - return content + return req.make_response(content, [('Content-Type', 'application/javascript')]) @openerpweb.httprequest def home(self, req, s_action=None, **kw): # script tags jslist = ['/base/webclient/js'] if req.debug: - jslist = manifest_glob(['base'], 'js') + jslist = manifest_glob(req.config.addons_path, ['base'], 'js') js = "\n ".join([''%i for i in jslist]) # css tags csslist = ['/base/webclient/css'] if req.debug: - csslist = manifest_glob(['base'], 'css') + csslist = manifest_glob(req.config.addons_path, ['base'], 'css') css = "\n ".join([''%i for i in csslist]) r = home_template % { 'javascript': js, @@ -180,7 +181,7 @@ class WebClient(openerpweb.Controller): transl = {"messages":[]} transs[addon_name] = transl for l in langs: - f_name = os.path.join(openerpweb.path_addons, addon_name, "po", l + ".po") + f_name = os.path.join(req.config.addons_path, addon_name, "po", l + ".po") if not os.path.exists(f_name): continue try: @@ -203,7 +204,7 @@ class Database(openerpweb.Controller): dbs = proxy.list() h = req.httprequest.headers['Host'].split(':')[0] d = h.split('.')[0] - r = cherrypy.config['openerp.dbfilter'].replace('%h', h).replace('%d', d) + r = req.config.dbfilter.replace('%h', h).replace('%d', d) dbs = [i for i in dbs if re.match(r, i)] return {"db_list": dbs} @@ -248,11 +249,11 @@ class Database(openerpweb.Controller): try: db_dump = base64.decodestring( req.session.proxy("db").dump(backup_pwd, backup_db)) - req.httpresponse.headers['Content-Type'] = "application/octet-stream; charset=binary" - req.httpresponse.headers['Content-Disposition'] = 'attachment; filename="' + backup_db + '.dump"' - req.httpresponse.cookie['fileToken'] = int(token) - req.httpresponse.cookie['fileToken']['path'] = '/' - return db_dump + return req.make_response(db_dump, + [('Content-Type', 'application/octet-stream; charset=binary'), + ('Content-Disposition', 'attachment; filename="' + backup_db + '.dump"')], + {'fileToken': int(token)} + ) except xmlrpclib.Fault, e: if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied': return 'Backup Database|' + e.faultCode @@ -293,7 +294,21 @@ class Session(openerpweb.Controller): "uid": req.session._uid, "context": ctx } - + @openerpweb.jsonrequest + def change_password (self,req,fields): + old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')( + dict(map(operator.itemgetter('name', 'value'), fields))) + if not (old_password.strip() and new_password.strip() and confirm_password.strip()): + return {'error':'All passwords have to be filled.','title': 'Change Password'} + if new_password != confirm_password: + return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'} + try: + if req.session.model('res.users').change_password( + old_password, new_password): + return {'new_password':new_password} + except: + 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( @@ -955,9 +970,9 @@ class Binary(openerpweb.Controller): res = Model.read([int(id)], [field], context)[0].get(field, '') return base64.decodestring(res) except: # TODO: what's the exception here? - return self.placeholder() - def placeholder(self): - return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read() + return self.placeholder(req) + def placeholder(self, req): + return open(os.path.join(req.addons_path, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read() @openerpweb.httprequest def saveas(self, req, model, id, field, fieldname, **kw): @@ -966,18 +981,17 @@ class Binary(openerpweb.Controller): res = Model.read([int(id)], [field, fieldname], context)[0] filecontent = res.get(field, '') if not filecontent: - raise cherrypy.NotFound + return req.not_found() else: - req.httpresponse.headers['Content-Type'] = 'application/octet-stream' filename = '%s_%s' % (model.replace('.', '_'), id) if fieldname: filename = res.get(fieldname, '') or filename - req.httpresponse.headers['Content-Disposition'] = 'attachment; filename=' + filename - return base64.decodestring(filecontent) + return req.make_response(filecontent, + [('Content-Type', 'application/octet-stream'), + ('Content-Disposition', 'attachment; filename=' + filename)]) @openerpweb.httprequest def upload(self, req, callback, ufile=None): - cherrypy.response.timeout = 500 headers = {} for key, val in req.httprequest.headers.iteritems(): headers[key.lower()] = val @@ -1004,7 +1018,6 @@ class Binary(openerpweb.Controller): @openerpweb.httprequest def upload_attachment(self, req, callback, model, id, ufile=None): - cherrypy.response.timeout = 500 context = req.session.eval_context(req.context) Model = req.session.model('ir.attachment') try: diff --git a/addons/base/po/base.pot b/addons/base/po/base.pot index fb5285610d8..e59c507c415 100644 --- a/addons/base/po/base.pot +++ b/addons/base/po/base.pot @@ -365,7 +365,7 @@ msgid "(Any existing filter with the same name will be replaced)" msgstr "" #: addons/base/static/src/xml/base.xml:0 -msgid "(?)" +msgid "?" msgstr "" #: addons/base/static/src/xml/base.xml:0 diff --git a/addons/base/static/lib/jquery.contextmenu/jquery.contextmenu.r2.packed.js b/addons/base/static/lib/jquery.contextmenu/jquery.contextmenu.r2.packed.js index cb456eed14c..2356f3b606b 100644 --- a/addons/base/static/lib/jquery.contextmenu/jquery.contextmenu.r2.packed.js +++ b/addons/base/static/lib/jquery.contextmenu/jquery.contextmenu.r2.packed.js @@ -32,7 +32,7 @@ if(!menu) { menu=$('
').hide().css({ position:'absolute', - zIndex:'500' + zIndex:'2000' }).appendTo('body').bind('click', function(e) { e.stopPropagation() }) diff --git a/addons/base/static/lib/qweb/qweb2.js b/addons/base/static/lib/qweb/qweb2.js index 8987f1dc40e..cfb9c9c461d 100644 --- a/addons/base/static/lib/qweb/qweb2.js +++ b/addons/base/static/lib/qweb/qweb2.js @@ -4,9 +4,9 @@ // TODO: t-set + t-value + children node == scoped variable ? var QWeb2 = { expressions_cache: {}, - reserved_words: 'true,false,NaN,null,undefined,debugger,in,instanceof,new,function,return,this,typeof,eval,Math,RegExp,Array,Object,Date'.split(','), - actions_precedence: 'foreach,if,call,set,esc,escf,raw,rawf,js,debug,log'.split(','), - word_replacement: { + RESERVED_WORDS: 'true,false,NaN,null,undefined,debugger,in,instanceof,new,function,return,this,typeof,eval,Math,RegExp,Array,Object,Date'.split(','), + ACTIONS_PRECEDENCE: 'foreach,if,call,set,esc,escf,raw,rawf,js,debug,log'.split(','), + WORD_REPLACEMENT: { 'and': '&&', 'or': '||', 'gt': '>', @@ -204,6 +204,9 @@ QWeb2.Engine = (function() { this.dict = {}; this.tools = QWeb2.tools; this.jQuery = window.jQuery; + this.reserved_words = QWeb2.RESERVED_WORDS.slice(0); + this.actions_precedence = QWeb2.ACTIONS_PRECEDENCE.slice(0); + this.word_replacement = QWeb2.tools.extend({}, QWeb2.WORD_REPLACEMENT); for (var i = 0; i < arguments.length; i++) { this.add_template(arguments[i]); } @@ -290,6 +293,9 @@ QWeb2.Engine = (function() { xDoc.loadXML(s); return xDoc; }, + has_template : function(template) { + return !!this.templates[template]; + }, get_xhr : function() { if (window.XMLHttpRequest) { return new window.XMLHttpRequest(); @@ -519,8 +525,8 @@ QWeb2.Element = (function() { continue; } else if (c.match(/\W/) && invar.length) { // TODO: Should check for possible spaces before dot - if (chars[invar_pos - 1] !== '.' && QWeb2.tools.arrayIndexOf(QWeb2.reserved_words, invar) < 0) { - invar = QWeb2.word_replacement[invar] || ("dict['" + invar + "']"); + if (chars[invar_pos - 1] !== '.' && QWeb2.tools.arrayIndexOf(this.engine.reserved_words, invar) < 0) { + invar = this.engine.word_replacement[invar] || ("dict['" + invar + "']"); } r += invar; invar = ''; @@ -579,15 +585,18 @@ QWeb2.Element = (function() { return this._bottom.unshift(this.get_indent() + "//@string=" + this.engine.tools.js_escape(s, true) + '\n'); }, compile_element : function() { - for (var i = 0, ilen = QWeb2.actions_precedence.length; i < ilen; i++) { - var a = QWeb2.actions_precedence[i]; + for (var i = 0, ilen = this.engine.actions_precedence.length; i < ilen; i++) { + var a = this.engine.actions_precedence[i]; if (a in this.actions) { var value = this.actions[a]; var key = 'compile_action_' + a; - if (!this[key]) { + if (this[key]) { + this[key](value); + } else if (this.engine[key]) { + this.engine[key].call(this, value); + } else { this.engine.tools.exception("No handler method for action '" + a + "'"); } - this[key](value); } } if (this.tag !== this.engine.prefix) { diff --git a/addons/base/static/src/css/base.css b/addons/base/static/src/css/base.css index 46364c4fa1d..c9894e42cef 100644 --- a/addons/base/static/src/css/base.css +++ b/addons/base/static/src/css/base.css @@ -597,13 +597,13 @@ label.error { background-color: #ccc; } .openerp .searchview_group.folded .searchview_group_string { - background: url("../img/ui/group-folded.png") no-repeat scroll 0 50%; + background: url("/base/static/src/img/ui/group-folded.png") no-repeat scroll 0 50%; } .openerp .searchview_group.folded .searchview_group_content { display: none; } .openerp .searchview_group.expanded .searchview_group_string { - background: url("../img/ui/group-expanded.png") no-repeat scroll 0 50%; + background: url("/base/static/src/img/ui/group-expanded.png") no-repeat scroll 0 50%; } .openerp .searchview_group.expanded .searchview_group_content { display: block; @@ -635,13 +635,13 @@ label.error { } .openerp .oe_search-view-custom-filter-btn span { - background: url(../img/icons/gtk-add.png) repeat-y; + background: url(/base/static/src/img/icons/gtk-add.png) repeat-y; padding-left: 18px; } .openerp .searchview_extended_add_proposition span { font-size: 0.9em; - background: url(../img/icons/gtk-add.png) repeat-y; + background: url(/base/static/src/img/icons/gtk-add.png) repeat-y; padding-left: 18px; } @@ -657,7 +657,7 @@ label.error { .openerp .searchview_extended_delete_group span, .openerp .searchview_extended_delete_prop span { font-size: 0.9em; - background: url(../img/icons/gtk-close.png) repeat-y; + background: url(/base/static/src/img/icons/gtk-close.png) repeat-y; padding-left: 18px; } /* List */ @@ -832,15 +832,17 @@ label.error { float: right; } -.openerp label.oe_form_label_help { +.openerp label.oe_label_help, .openerp label.oe_label { + display: block; +} +.openerp label.oe_label_help { cursor: help; } -.openerp label.oe_form_label, .openerp label.oe_form_label_help { - display: block; +.openerp .oe_form_field label.oe_label, .openerp .oe_form_field label.oe_label_help { text-align: right; margin: 3px 0 0 10px; } -.openerp label.oe_form_label_help span { +.openerp label.oe_label_help span { font-size: 80%; color: darkgreen; vertical-align:top; @@ -895,6 +897,9 @@ label.error { position: relative; top: 4px; } +.openerp .oe_trad_field.touched { + border: 1px solid green !important; +} /* http://www.quirksmode.org/dom/inputfile.html * http://stackoverflow.com/questions/2855589/replace-input-type-file-by-an-image @@ -928,6 +933,9 @@ label.error { margin: 3px 4px 3px 1px; height: 15px; } +.openerp .oe_form_frame_cell.oe_form_separator_vertical { + border-left: 1px solid #666; +} .openerp td.required input, .openerp td.required select { background-color: #D2D2FF !important; } @@ -1088,7 +1096,7 @@ label.error { cursor: pointer; border: 1px solid #D2CFCF; display: block; - background: url(../img/toggle-a-bg.png); + background: url(/base/static/src/img/toggle-a-bg.png); width: 21px; height: 21px; z-index: 10; @@ -1103,7 +1111,7 @@ label.error { } .openerp li.oe_sidebar_print { padding-left: 20px; - background: 1px 3px url(../img/icons/gtk-print.png) no-repeat; + background: 1px 3px url(/base/static/src/img/icons/gtk-print.png) no-repeat; } .openerp.kitten-mode-activated .main_table { diff --git a/addons/base/static/src/js/boot.js b/addons/base/static/src/js/boot.js index fff710a1d68..e6c9fab0831 100644 --- a/addons/base/static/src/js/boot.js +++ b/addons/base/static/src/js/boot.js @@ -50,7 +50,9 @@ openerp.base = function(instance) { openerp.base.core(instance); - openerp.base.dates(instance); + if (openerp.base.dates) { + openerp.base.dates(instance); + } openerp.base.formats(instance); openerp.base.chrome(instance); openerp.base.data(instance); diff --git a/addons/base/static/src/js/chrome.js b/addons/base/static/src/js/chrome.js index cf3f4b0ae00..2f53dca444d 100644 --- a/addons/base/static/src/js/chrome.js +++ b/addons/base/static/src/js/chrome.js @@ -440,8 +440,6 @@ openerp.base.Login = openerp.base.Widget.extend({ this.selected_login = localStorage.getItem('last_login_login_success'); } if (jQuery.deparam(jQuery.param.querystring()).debug != undefined) { - this.selected_db = this.selected_db || "trunk"; - this.selected_login = this.selected_login || "admin"; this.selected_password = this.selected_password || "a"; } }, @@ -525,22 +523,26 @@ openerp.base.Login = openerp.base.Widget.extend({ }); openerp.base.Header = openerp.base.Widget.extend({ - init: function(parent, element_id) { - this._super(parent, element_id); - if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) { - this.qs = '?debug' - } else { - this.qs = '' - } + template: "Header", + identifier_prefix: 'oe-app-header-', + init: function(parent) { + this._super(parent); + this.qs = "?" + jQuery.param.querystring(); + this.$content = $(); }, start: function() { - return this.do_update(); + this._super(); }, do_update: function () { - this.$element.html(QWeb.render("Header", this)); + this.$content = $(QWeb.render("Header-content", {widget: this})); + this.$content.appendTo(this.$element); this.$element.find(".logout").click(this.on_logout); + this.$element.find("a.preferences").click(this.on_preferences); return this.shortcut_load(); }, + do_reset: function() { + this.$content.remove(); + }, shortcut_load :function(){ var self = this, sc = self.session.shortcuts, @@ -594,9 +596,98 @@ openerp.base.Header = openerp.base.Widget.extend({ }); }); }, + on_action: function(action) { }, - + on_preferences: function(){ + var self = this; + var action_manager = new openerp.base.ActionManager(this); + var dataset = new openerp.base.DataSet (this,'res.users',this.context); + dataset.call ('action_get','',function (result){ + self.rpc('/base/action/load', {action_id:result}, function(result){ + action_manager.do_action(_.extend(result['result'], { + res_id: self.session.uid, + res_model: 'res.users', + flags: { + action_buttons: false, + search_view: false, + sidebar: false, + views_switcher: false, + pager: false + } + })); + }); + }); + this.dialog = new openerp.base.Dialog(this,{ + modal: true, + title: 'Preferences', + width: 600, + height: 500, + buttons: { + "Change password": function(){ + self.change_password(); + }, + Cancel: function(){ + $(this).dialog('destroy'); + }, + Save: function(){ + var inner_viewmanager = action_manager.inner_viewmanager; + inner_viewmanager.views[inner_viewmanager.active_view].controller.do_save(function(){ + inner_viewmanager.start(); + }); + $(this).dialog('destroy') + } + } + }); + this.dialog.start().open(); + action_manager.appendTo(this.dialog); + action_manager.render(this.dialog); + }, + + change_password :function() { + var self = this; + this.dialog = new openerp.base.Dialog(this,{ + modal : true, + title : 'Change Password', + width : 'auto', + height : 'auto' + }); + this.dialog.start().open(); + this.dialog.$element.html(QWeb.render("Change_Pwd", self)); + this.dialog.$element.find("form[name=change_password_form]").validate({ + submitHandler: function (form) { + self.rpc("/base/session/change_password",{ + 'fields': $(form).serializeArray() + }, 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.base.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(); + }); + } + }); +}, + display_error: function (error) { + return $('
').dialog({ + modal: true, + title: error.title, + buttons: { + Ok: function() { + $(this).dialog("close"); + } + } + }).html(error.error); + }, on_logout: function() { this.$element.find('.oe-shortcuts ul').empty(); } @@ -710,21 +801,23 @@ openerp.base.WebClient = openerp.base.Widget.extend({ // Do you autorize this ? will be replaced by notify() in controller openerp.base.Widget.prototype.notification = new openerp.base.Notification(this, "oe_notification"); - this.header = new openerp.base.Header(this, "oe_header"); + + this.header = new openerp.base.Header(this); this.login = new openerp.base.Login(this, "oe_login"); this.header.on_logout.add(this.login.on_logout); + this.header.on_action.add(this.on_menu_action); this.session.on_session_invalid.add(this.login.do_ask_login); this.session.on_session_valid.add_last(this.header.do_update); + this.session.on_session_invalid.add_last(this.header.do_reset); this.session.on_session_valid.add_last(this.on_logged); this.menu = new openerp.base.Menu(this, "oe_menu", "oe_secondary_menu"); this.menu.on_action.add(this.on_menu_action); - this.header.on_action.add(this.on_menu_action); }, start: function() { + this.header.appendTo($("#oe_header")); this.session.start(); - this.header.start(); this.login.start(); this.menu.start(); this.notification.notify("OpenERP Client", "The openerp client has been initialized."); diff --git a/addons/base/static/src/js/data.js b/addons/base/static/src/js/data.js index 668898dd66c..e95adaf8870 100644 --- a/addons/base/static/src/js/data.js +++ b/addons/base/static/src/js/data.js @@ -355,12 +355,13 @@ openerp.base.DataSet = openerp.base.Widget.extend( /** @lends openerp.base.Data * @param {Function} error_callback function called in case of write error * @returns {$.Deferred} */ - write: function (id, data, callback, error_callback) { + write: function (id, data, options, callback, error_callback) { + var options = options || {}; return this.rpc('/base/dataset/save', { model: this.model, id: id, data: data, - context: this.get_context() + context: this.get_context(options.context) }, callback, error_callback); }, /** diff --git a/addons/base/static/src/js/form.js b/addons/base/static/src/js/form.js index 212f48717bd..2a3c8ab476e 100644 --- a/addons/base/static/src/js/form.js +++ b/addons/base/static/src/js/form.js @@ -83,9 +83,11 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormVi 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_get_xml_view').click(function() { - $('' + openerp.base.json_node_to_xml(self.fields_view.arch, true) + '').dialog({ width: '95%', height: 600}); - }); + if (this.session.debug) { + this.$form_header.find('button.oe_get_xml_view').click(function() { + $('' + openerp.base.json_node_to_xml(self.fields_view.arch, true) + '').dialog({ width: '95%', height: 600}); + }); + } if (this.options.sidebar && this.options.sidebar_id) { this.sidebar = new openerp.base.Sidebar(this, this.options.sidebar_id); @@ -157,6 +159,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormVi this.do_update_pager(record.id == null); if (this.sidebar) { this.sidebar.attachments.do_update(); + this.sidebar.$element.find('.oe_sidebar_translate').toggleClass('oe_hide', !record.id); } if (this.default_focus_field && !this.embedded_view) { this.default_focus_field.focus(); @@ -342,7 +345,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormVi self.on_created(r, success, prepend_on_create); }); } else { - return this.dataset.write(this.datarecord.id, values, function(r) { + return this.dataset.write(this.datarecord.id, values, {}, function(r) { self.on_saved(r, success); }); } @@ -576,17 +579,23 @@ openerp.base.form.Widget = openerp.base.Widget.extend({ this.view.widgets[this.element_id] = this; this.children = node.children; - this.colspan = parseInt(node.attrs.colspan || 1); + this.colspan = parseInt(node.attrs.colspan || 1, 10); + this.decrease_max_width = 0; this.string = this.string || node.attrs.string; this.help = this.help || node.attrs.help; this.invisible = this.modifiers['invisible'] === true; + this.classname = 'oe_form_' + this.type; + + this.width = this.node.attrs.width; }, start: function() { this.$element = $('#' + this.element_id); }, stop: function() { - this.$element.remove(); + if (this.$element) { + this.$element.remove(); + } }, process_modifiers: function() { var compute_domain = openerp.base.form.compute_domain; @@ -607,7 +616,7 @@ openerp.base.form.WidgetFrame = openerp.base.form.Widget.extend({ template: 'WidgetFrame', init: function(view, node) { this._super(view, node); - this.columns = node.attrs.col || 4; + this.columns = parseInt(node.attrs.col || 4, 10); this.x = 0; this.y = 0; this.table = []; @@ -633,15 +642,21 @@ openerp.base.form.WidgetFrame = openerp.base.form.Widget.extend({ return row; }, set_row_cells_with: function(row) { + var bypass = 0, + max_width = 100; + for (var i = 0; i < row.length; i++) { + bypass += row[i].width === undefined ? 0 : 1; + max_width -= row[i].decrease_max_width; + } + var size_unit = Math.round(max_width / (this.columns - bypass)), + colspan_sum = 0; for (var i = 0; i < row.length; i++) { var w = row[i]; - if (w.is_field_label) { - w.width = "1%"; - if (row[i + 1]) { - row[i + 1].width = Math.round((100 / this.columns) * (w.colspan + 1) - 1) + '%'; - } - } else if (w.width === undefined) { - w.width = Math.round((100 / this.columns) * w.colspan) + '%'; + colspan_sum += w.colspan; + if (w.width === undefined) { + var width = (i === row.length - 1 && colspan_sum === this.columns) ? max_width : Math.round(size_unit * w.colspan); + max_width -= width; + w.width = width + '%'; } } }, @@ -731,6 +746,11 @@ openerp.base.form.WidgetSeparator = openerp.base.form.Widget.extend({ init: function(view, node) { this._super(view, node); this.template = "WidgetSeparator"; + this.orientation = node.attrs.orientation || 'horizontal'; + if (this.orientation === 'vertical') { + this.width = '1'; + } + this.classname += '_' + this.orientation; } }); @@ -794,14 +814,15 @@ openerp.base.form.WidgetLabel = openerp.base.form.Widget.extend({ this._super(view, node); // TODO fme: support for attrs.align - if (this.node.tag == 'label' && this.node.attrs.colspan) { - this.is_field_label = false; + if (this.node.tag == 'label' && (this.node.attrs.colspan || (this.string && this.string.length > 32))) { this.template = "WidgetParagraph"; - this.colspan = this.node.attrs.colspan; + this.colspan = parseInt(this.node.attrs.colspan || 1, 10); } else { - this.is_field_label = true; this.template = "WidgetLabel"; this.colspan = 1; + this.width = '1%'; + this.decrease_max_width = 1; + this.nowrap = true; } }, render: function () { @@ -843,6 +864,8 @@ openerp.base.form.Field = openerp.base.form.Widget.extend({ this.required = this.modifiers['required'] === true; this.invalid = false; this.dirty = false; + + this.classname = 'oe_form_field_' + this.type; }, start: function() { this._super.apply(this, arguments); @@ -879,6 +902,9 @@ openerp.base.form.Field = openerp.base.form.Widget.extend({ }, update_dom: function() { this._super.apply(this, arguments); + if (this.field.translate) { + this.$element.find('.oe_field_translate').toggle(!!this.view.datarecord.id); + } if (!this.disable_utility_classes) { this.$element.toggleClass('disabled', this.readonly); this.$element.toggleClass('required', this.required); @@ -1080,17 +1106,17 @@ openerp.base.form.FieldDatetime = openerp.base.form.Field.extend({ focus: function() { this.$element.find('input').focus(); }, - parse: openerp.base.str_to_datetime, - format: openerp.base.datetime_to_str + parse: openerp.base.auto_str_to_date, + format: function(val) { + return openerp.base.auto_date_to_str(val, this.field.type); + } }); openerp.base.form.FieldDate = openerp.base.form.FieldDatetime.extend({ init: function(view, node) { this._super(view, node); this.jqueryui_object = 'datepicker'; - }, - parse: openerp.base.str_to_date, - format: openerp.base.date_to_str + } }); openerp.base.form.FieldText = openerp.base.form.Field.extend({ @@ -1519,12 +1545,14 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ }, set_value: function(value) { value = value || null; + this.invalid = false; var self = this; - var _super = this._super; this.tmp_value = value; + self.update_dom(); + self.on_value_changed(); var real_set_value = function(rval) { self.tmp_value = undefined; - _super.apply(self, rval); + self.value = rval; self.original_value = undefined; self._change_int_ext_value(rval); }; @@ -1550,7 +1578,8 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ }, validate: function() { this.invalid = false; - if (this.value === null) { + var val = this.tmp_value !== undefined ? this.tmp_value : this.value; + if (val === null) { this.invalid = this.required; } }, @@ -1558,14 +1587,16 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ var self = this; if (!self.value) return; - self.rpc("/base/action/load", { - action_id: related[2].id, - context: { + var additional_context = { active_id: self.value[0], active_ids: [self.value[0]], active_model: self.field.relation - } + }; + self.rpc("/base/action/load", { + action_id: related[2].id, + context: additional_context }, function(result) { + result.result.context = _.extend(result.result.context || {}, additional_context); self.do_action(result.result); }); } @@ -1863,10 +1894,13 @@ openerp.base.form.One2ManyListView = openerp.base.ListView.extend({ pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(),{ auto_write: false, alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, - parent_view: self.o2m.view + parent_view: self.o2m.view, + read_function: function() { + return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments); + } }); pop.on_write.add(function(id, data) { - self.o2m.dataset.write(id, data, function(r) { + self.o2m.dataset.write(id, data, {}, function(r) { self.o2m.reload_current_view(); }); }); @@ -2135,6 +2169,7 @@ openerp.base.form.FormOpenPopup = openerp.base.OldWidget.extend({ * options: * - alternative_form_view * - auto_write (default true) + * - read_function * - parent_view */ show_element: function(model, row_id, context, options) { @@ -2150,7 +2185,8 @@ openerp.base.form.FormOpenPopup = openerp.base.OldWidget.extend({ }, start: function() { this._super(); - this.dataset = new openerp.base.ReadOnlyDataSetSearch(this, this.model, this.context); + this.dataset = new openerp.base.form.FormOpenDataset(this, this.model, this.context); + this.dataset.fop = this; this.dataset.ids = [this.row_id]; this.dataset.index = 0; this.dataset.parent_view = this.options.parent_view; @@ -2163,7 +2199,7 @@ openerp.base.form.FormOpenPopup = openerp.base.OldWidget.extend({ var self = this; var wdataset = new openerp.base.DataSetSearch(this, this.model, this.context, this.domain); wdataset.parent_view = this.options.parent_view; - wdataset.write(id, data, function(r) { + wdataset.write(id, data, {}, function(r) { self.on_write_completed(); }); }, @@ -2192,6 +2228,16 @@ openerp.base.form.FormOpenPopup = openerp.base.OldWidget.extend({ } }); +openerp.base.form.FormOpenDataset = openerp.base.ReadOnlyDataSetSearch.extend({ + read_ids: function() { + if (this.fop.options.read_function) { + return this.fop.options.read_function.apply(null, arguments); + } else { + return this._super.apply(this, arguments); + } + } +}); + openerp.base.form.FieldReference = openerp.base.form.Field.extend({ init: function(view, node) { this._super(view, node); diff --git a/addons/base/static/src/js/formats.js b/addons/base/static/src/js/formats.js index ed25b28c16d..625868eee75 100644 --- a/addons/base/static/src/js/formats.js +++ b/addons/base/static/src/js/formats.js @@ -28,8 +28,10 @@ openerp.base.format_value = function (value, descriptor, value_if_empty) { case 'float': var precision = descriptor.digits ? descriptor.digits[1] : 2; var int_part = Math.floor(value); - var dec_part = Math.floor((value % 1) * Math.pow(10, precision)); - return _.sprintf('%d' + openerp.base._t.database.parameters.decimal_point + '%d', int_part, dec_part); + var dec_part = Math.abs(Math.floor((value % 1) * Math.pow(10, precision))); + return _.sprintf('%d%s%d', + int_part, + openerp.base._t.database.parameters.decimal_point, dec_part); case 'float_time': return _.sprintf("%02d:%02d", Math.floor(value), @@ -43,9 +45,9 @@ openerp.base.format_value = function (value, descriptor, value_if_empty) { return value[1]; case 'datetime': if (typeof(value) == "string") - value = openerp.base.str_to_datetime(value); + value = openerp.base.auto_str_to_date(value); try { - return value.toString(_.sprintf("%s %s", Date.CultureInfo.formatPatterns.shortDate, + return value.toString(_.sprintf("%s %s", Date.CultureInfo.formatPatterns.shortDate, Date.CultureInfo.formatPatterns.longTime)); } catch (e) { return value.format("%m/%d/%Y %H:%M:%S"); @@ -53,7 +55,7 @@ openerp.base.format_value = function (value, descriptor, value_if_empty) { return value; case 'date': if (typeof(value) == "string") - value = openerp.base.str_to_date(value); + value = openerp.base.auto_str_to_date(value); try { return value.toString(Date.CultureInfo.formatPatterns.shortDate); } catch (e) { @@ -61,7 +63,7 @@ openerp.base.format_value = function (value, descriptor, value_if_empty) { } case 'time': if (typeof(value) == "string") - value = openerp.base.str_to_time(value); + value = openerp.base.auto_str_to_date(value); try { return value.toString(Date.CultureInfo.formatPatterns.longTime); } catch (e) { @@ -108,7 +110,7 @@ openerp.base.parse_value = function (value, descriptor, value_if_empty) { case 'progressbar': return openerp.base.parse_value(value, {type: "float"}); case 'datetime': - var tmp = Date.parseExact(value, _.sprintf("%s %s", Date.CultureInfo.formatPatterns.shortDate, + var tmp = Date.parseExact(value, _.sprintf("%s %s", Date.CultureInfo.formatPatterns.shortDate, Date.CultureInfo.formatPatterns.longTime)); if (tmp !== null) return tmp; @@ -117,7 +119,7 @@ openerp.base.parse_value = function (value, descriptor, value_if_empty) { return tmp; throw value + " is not a valid datetime"; case 'date': - var tmp = Date.parseExact(Date.CultureInfo.formatPatterns.shortDate); + var tmp = Date.parseExact(value, Date.CultureInfo.formatPatterns.shortDate); if (tmp !== null) return tmp; tmp = Date.parse(value); @@ -125,7 +127,7 @@ openerp.base.parse_value = function (value, descriptor, value_if_empty) { return tmp; throw value + " is not a valid date"; case 'time': - var tmp = Date.parseExact(Date.CultureInfo.formatPatterns.longTime); + var tmp = Date.parseExact(value, Date.CultureInfo.formatPatterns.longTime); if (tmp !== null) return tmp; tmp = Date.parse(value); @@ -136,6 +138,32 @@ openerp.base.parse_value = function (value, descriptor, value_if_empty) { return value; }; +openerp.base.auto_str_to_date = function(value, type) { + try { + return openerp.base.str_to_datetime(value); + } catch(e) {} + try { + return openerp.base.str_to_date(value); + } catch(e) {} + try { + return openerp.base.str_to_time(value); + } catch(e) {} + throw "'" + value + "' is not a valid date, datetime nor time" +}; + +openerp.base.auto_date_to_str = function(value, type) { + switch(type) { + case 'datetime': + return openerp.base.datetime_to_str(value); + case 'date': + return openerp.base.date_to_str(value); + case 'time': + return openerp.base.time_to_str(value); + default: + throw type + " is not convertible to date, datetime nor time" + } +}; + /** * Formats a provided cell based on its field type * diff --git a/addons/base/static/src/js/search.js b/addons/base/static/src/js/search.js index e3d51e2b439..94a8fa51aa0 100644 --- a/addons/base/static/src/js/search.js +++ b/addons/base/static/src/js/search.js @@ -314,16 +314,25 @@ openerp.base.SearchView = openerp.base.Widget.extend({ on_invalid: function (errors) { this.notification.notify("Invalid Search", "triggered from search view"); }, - do_clear: function (e) { - if (e && e.preventDefault) { e.preventDefault(); } - this.on_clear(); + do_clear: function () { + $('.filter_label').removeClass('enabled'); + var string = $('a.searchview_group_string'); + _.each(string, function(str){ + $(str).closest('div.searchview_group').removeClass("expanded").addClass('folded'); + }); + this.$element.find('table:last').hide(); + + $('.searchview_extended_groups_list').empty(); + setTimeout(this.on_clear); }, /** * Triggered when the search view gets cleared * * @event */ - on_clear: function () { }, + on_clear: function () { + this.do_search(); + }, /** * Called by a filter propagating its state changes * diff --git a/addons/base/static/src/js/views.js b/addons/base/static/src/js/views.js index 689a72ef7f1..1fc04bf8965 100644 --- a/addons/base/static/src/js/views.js +++ b/addons/base/static/src/js/views.js @@ -123,6 +123,14 @@ openerp.base.ActionManager = openerp.base.Widget.extend({ this.content_stop(); var ClientWidget = openerp.base.client_actions.get_object(action.tag); (this.client_widget = new ClientWidget(this, action.params)).appendTo(this); + }, + ir_actions_report_xml: function(action) { + this.rpc('/base/report/get_report', { + action: action, + context: {} + }).then(function(result) { + debugger; + }); } }); @@ -438,14 +446,17 @@ openerp.base.Sidebar = openerp.base.Widget.extend({ //TODO niv: maybe show a warning? return false; } + var additional_context = { + active_id: ids[0], + active_ids: ids, + active_model: self.widget_parent.dataset.model + }; self.rpc("/base/action/load", { action_id: item.action.id, - context: { - active_id: ids[0], - active_ids: ids, - active_model: self.widget_parent.dataset.model - } + context: additional_context }, function(result) { + result.result.context = _.extend(result.result.context || {}, + additional_context); result.result.flags = result.result.flags || {}; result.result.flags.new_window = true; self.do_action(result.result); @@ -472,6 +483,9 @@ openerp.base.Sidebar = openerp.base.Widget.extend({ openerp.base.TranslateDialog = openerp.base.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 + // and the moment the user opens the translation dialog, the user language could have been changed + this.view_language = view.session.user_context.lang; this['on_button' + _t("Save")] = this.on_button_Save; this['on_button' + _t("Close")] = this.on_button_Close; this._super(view, { @@ -500,6 +514,9 @@ openerp.base.TranslateDialog = openerp.base.Dialog.extend({ self.select_tab('view'); } self.$fields_form = self.$element.find('.oe_translation_form'); + self.$fields_form.find('.oe_trad_field').change(function() { + $(this).toggleClass('touched', ($(this).val() != $(this).attr('data-value'))); + }); }); return this; }, @@ -514,19 +531,27 @@ openerp.base.TranslateDialog = openerp.base.Dialog.extend({ _.each(self.languages, function(lg) { var deff = $.Deferred(); deffered.push(deff); - self.rpc('/base/dataset/get', { - model: self.view.dataset.model, - ids: [self.view.datarecord.id], - fields: self.translatable_fields_keys, - context: self.view.dataset.get_context({ - 'lang': lg.code - }) - }, function(values) { + var callback = function(values) { _.each(self.translatable_fields_keys, function(f) { - self.$fields_form.find('.oe_trad_field[name="' + lg.code + '-' + f + '"]').val(values[0][f] || ''); + self.$fields_form.find('.oe_trad_field[name="' + lg.code + '-' + f + '"]').val(values[0][f] || '').attr('data-value', values[0][f] || ''); }); deff.resolve(); - }); + } + if (lg.code === self.view_language) { + var values = {}; + _.each(self.translatable_fields_keys, function(field) { + values[field] = self.view.fields[field].get_value(); + }); + callback([values]); + } else { + self.rpc('/base/dataset/get', { + model: self.view.dataset.model, + ids: [self.view.datarecord.id], + fields: self.translatable_fields_keys, + context: self.view.dataset.get_context({ + 'lang': lg.code + })}, callback); + } }); $.when.apply(null, deffered).then(callback); }, @@ -562,6 +587,24 @@ openerp.base.TranslateDialog = openerp.base.Dialog.extend({ }); }, on_button_Save: function() { + var trads = {}, + self = this; + self.$fields_form.find('.oe_trad_field.touched').each(function() { + var field = $(this).attr('name').split('-'); + if (!trads[field[0]]) { + trads[field[0]] = {}; + } + trads[field[0]][field[1]] = $(this).val(); + }); + _.each(trads, function(data, code) { + if (code === self.view_language) { + _.each(data, function(value, field) { + self.view.fields[field].set_value(value); + }); + } else { + self.view.dataset.write(self.view.datarecord.id, data, { 'lang': code }); + } + }); this.close(); }, on_button_Close: function() { @@ -667,7 +710,7 @@ openerp.base.View = openerp.base.Widget.extend({ }, { label: "Translate", callback: this.on_sidebar_translate, - classname: 'oe_sidebar_translate' + classname: 'oe_sidebar_translate oe_hide' }, { label: "View Log", callback: this.on_sidebar_view_log, diff --git a/addons/base/static/src/xml/base.xml b/addons/base/static/src/xml/base.xml index 76336f447d0..b03d7819954 100644 --- a/addons/base/static/src/xml/base.xml +++ b/addons/base/static/src/xml/base.xml @@ -325,15 +325,19 @@
- -