[Merge] Merge from openerp-web.
authorJiten (OpenERP) <jra@tinyerp.com>
Fri, 7 Oct 2011 05:11:13 +0000 (10:41 +0530)
committerJiten (OpenERP) <jra@tinyerp.com>
Fri, 7 Oct 2011 05:11:13 +0000 (10:41 +0530)
bzr revid: jra@tinyerp.com-20111007051113-w33l3jffcxlgz811

40 files changed:
addons/web/__init__.py
addons/web/__openerp__.py
addons/web/common/__init__.py
addons/web/common/ast.py [deleted file]
addons/web/common/dates.py [deleted file]
addons/web/common/dispatch.py [deleted file]
addons/web/common/http.py
addons/web/common/session.py
addons/web/common/xml2json.py [new file with mode: 0644]
addons/web/controllers/main.py
addons/web/static/src/css/base.css
addons/web/static/src/js/core.js
addons/web/static/src/js/formats.js
addons/web/static/src/js/search.js
addons/web/static/src/js/view_form.js
addons/web/static/src/js/view_list.js
addons/web/static/src/js/view_list_editable.js
addons/web/static/src/js/views.js
addons/web/static/src/xml/base.xml
addons/web_calendar/__openerp__.py
addons/web_calendar/static/src/js/calendar.js
addons/web_chat/__openerp__.py
addons/web_chat/controllers/main.py
addons/web_dashboard/__openerp__.py
addons/web_dashboard/controllers.py
addons/web_default_home/__openerp__.py
addons/web_default_home/static/src/js/home.js
addons/web_diagram/__openerp__.py
addons/web_diagram/controllers/main.py
addons/web_gantt/__openerp__.py
addons/web_gantt/static/src/js/gantt.js
addons/web_graph/__openerp__.py
addons/web_graph/static/src/js/graph.js
addons/web_hello/__openerp__.py
addons/web_kanban/__openerp__.py
addons/web_kanban/static/src/js/kanban.js
addons/web_mobile/__openerp__.py
addons/web_rpc/__openerp__.py
addons/web_tests/__openerp__.py
openerp-web.py

index 8802cb5..06e7fa1 100644 (file)
@@ -1,6 +1,5 @@
 import common
 import controllers
-import common.dispatch
 import logging
 import optparse
 
@@ -22,6 +21,6 @@ def wsgi_postload():
     o.serve_static = True
     o.backend = 'local'
 
-    app = common.dispatch.Root(o)
+    app = common.http.Root(o)
     openerp.wsgi.register_wsgi_handler(app)
 
index b611eb9..0afd431 100644 (file)
@@ -1,5 +1,6 @@
 {
     "name" : "web",
+    "category" : "Hidden",
     "depends" : [],
     'active': True,
     'post_load' : 'wsgi_postload',
index 9257f51..53bf3e3 100644 (file)
@@ -1,2 +1,6 @@
 #!/usr/bin/python
-from dispatch import *
+import http
+import nonliterals
+import release
+import session
+import xml2json
diff --git a/addons/web/common/ast.py b/addons/web/common/ast.py
deleted file mode 100644 (file)
index 2fd565a..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-# -*- coding: utf-8 -*-
-""" Backport of Python 2.6's ast.py for Python 2.5
-"""
-__all__ = ['literal_eval']
-try:
-    from ast import literal_eval
-except ImportError:
-    from _ast import *
-    from _ast import __version__
-
-
-    def parse(expr, filename='<unknown>', mode='exec'):
-        """
-        Parse an expression into an AST node.
-        Equivalent to compile(expr, filename, mode, PyCF_ONLY_AST).
-        """
-        return compile(expr, filename, mode, PyCF_ONLY_AST)
-
-
-    def literal_eval(node_or_string):
-        """
-        Safely evaluate an expression node or a string containing a Python
-        expression.  The string or node provided may only consist of the
-        following Python literal structures: strings, numbers, tuples, lists,
-        dicts, booleans, and None.
-        """
-        _safe_names = {'None': None, 'True': True, 'False': False}
-        if isinstance(node_or_string, basestring):
-            node_or_string = parse(node_or_string, mode='eval')
-        if isinstance(node_or_string, Expression):
-            node_or_string = node_or_string.body
-        def _convert(node):
-            if isinstance(node, Str):
-                return node.s
-            elif isinstance(node, Num):
-                return node.n
-            elif isinstance(node, Tuple):
-                return tuple(map(_convert, node.elts))
-            elif isinstance(node, List):
-                return list(map(_convert, node.elts))
-            elif isinstance(node, Dict):
-                return dict((_convert(k), _convert(v)) for k, v
-                            in zip(node.keys, node.values))
-            elif isinstance(node, Name):
-                if node.id in _safe_names:
-                    return _safe_names[node.id]
-            raise ValueError('malformed string')
-        return _convert(node_or_string)
diff --git a/addons/web/common/dates.py b/addons/web/common/dates.py
deleted file mode 100644 (file)
index caa7f83..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-#    OpenERP, Open Source Management Solution
-#    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
-#    Copyright (C) 2010 OpenERP s.a. (<http://openerp.com>).
-#
-#    This program is free software: you can redistribute it and/or modify
-#    it under the terms of the GNU Affero General Public License as
-#    published by the Free Software Foundation, either version 3 of the
-#    License, or (at your option) any later version.
-#
-#    This program is distributed in the hope that it will be useful,
-#    but WITHOUT ANY WARRANTY; without even the implied warranty of
-#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#    GNU Affero General Public License for more details.
-#
-#    You should have received a copy of the GNU Affero General Public License
-#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-##############################################################################
-
-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/dispatch.py b/addons/web/common/dispatch.py
deleted file mode 100644 (file)
index 245624f..0000000
+++ /dev/null
@@ -1,425 +0,0 @@
-#!/usr/bin/python
-from __future__ import with_statement
-
-import functools
-import logging
-import os
-import pprint
-import sys
-import traceback
-import uuid
-import xmlrpclib
-
-import simplejson
-import werkzeug.datastructures
-import werkzeug.exceptions
-import werkzeug.utils
-import werkzeug.wrappers
-import werkzeug.wsgi
-
-import ast
-import nonliterals
-import http
-import session
-import openerplib
-
-__all__ = ['Root', 'jsonrequest', 'httprequest', 'Controller',
-           'WebRequest', 'JsonRequest', 'HttpRequest']
-
-_logger = logging.getLogger(__name__)
-
-#-----------------------------------------------------------
-# Globals (wont move into a pool)
-#-----------------------------------------------------------
-
-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:: 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:`session.OpenERPSession` instance of
-        the current request
-
-    .. attribute:: session
-
-        :class:`~session.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.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, session.OpenERPSession())
-        self.session.config = self.config
-        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 _logger.isEnabledFor(logging.DEBUG):
-                _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 openerplib.AuthenticationError:
-            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(__name__ + '.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 _logger.isEnabledFor(logging.DEBUG):
-            _logger.debug("<--\n%s", pprint.pformat(response))
-        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):
-        params = dict(self.httprequest.args)
-        params.update(self.httprequest.form)
-        params.update(self.httprequest.files)
-        self.init(params)
-        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)
-        _logger.debug("%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):
-                _logger.debug('<-- %s', r)
-            else:
-                _logger.debug("<-- size: %s", len(r))
-        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 = '/web/webclient/home?debug=1'
-        self.config = options
-
-        if self.config.backend == 'local':
-            conn = openerplib.get_connector(protocol='local')
-        else:
-            conn = openerplib.get_connector(hostname=self.config.server_host,
-                   port=self.config.server_port)
-        self.config.connector = conn
-
-        self.session_cookie = 'sessionid'
-        self.addons = {}
-
-        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, 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 = {}
-        for addons_path in self.config.addons_path:
-            if addons_path not in sys.path:
-                sys.path.insert(0, addons_path)
-            for module in os.listdir(addons_path):
-                if module not in addons_module:
-                    manifest_path = os.path.join(addons_path, module, '__openerp__.py')
-                    path_static = os.path.join(addons_path, module, 'static')
-                    if os.path.isfile(manifest_path) and os.path.isdir(path_static):
-                        manifest = ast.literal_eval(open(manifest_path).read())
-                        manifest['addons_path'] = addons_path
-                        _logger.info("Loading %s", module)
-                        m = __import__(module)
-                        addons_module[module] = m
-                        addons_manifest[module] = manifest
-                        statics['/%s/static' % module] = path_static
-        for k, v in controllers_class.items():
-            if k not in controllers_object:
-                o = v()
-                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):
-                        _logger.debug("Dispatching to %s %s %s", ps, c, meth)
-                        return m
-        return None
index 0186fd9..453484a 100644 (file)
 # -*- coding: utf-8 -*-
-
+#----------------------------------------------------------
+# OpenERP Web HTTP layer
+#----------------------------------------------------------
+import ast
 import contextlib
+import functools
+import logging
+import urllib
+import os
+import pprint
+import sys
+import traceback
+import uuid
+import xmlrpclib
 
+import simplejson
 import werkzeug.contrib.sessions
+import werkzeug.datastructures
+import werkzeug.exceptions
+import werkzeug.utils
+import werkzeug.wrappers
+import werkzeug.wsgi
+
+import nonliterals
+import session
+import openerplib
+
+__all__ = ['Root', 'jsonrequest', 'httprequest', 'Controller',
+           'WebRequest', 'JsonRequest', 'HttpRequest']
+
+_logger = logging.getLogger(__name__)
+
+#----------------------------------------------------------
+# 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:: 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:`session.OpenERPSession` instance of
+        the current request
+
+    .. attribute:: session
+
+        :class:`~session.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.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, session.OpenERPSession())
+        self.session.config = self.config
+        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 _logger.isEnabledFor(logging.DEBUG):
+                _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 openerplib.AuthenticationError:
+            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(__name__ + '.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 _logger.isEnabledFor(logging.DEBUG):
+            _logger.debug("<--\n%s", pprint.pformat(response))
+        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):
+        params = dict(self.httprequest.args)
+        params.update(self.httprequest.form)
+        params.update(self.httprequest.files)
+        self.init(params)
+        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)
+        _logger.debug("%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):
+                _logger.debug('<-- %s', r)
+            else:
+                _logger.debug("<-- size: %s", len(r))
+        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
+
+#----------------------------------------------------------
+# OpenERP Web werkzeug Session Managment wraped using with
+#----------------------------------------------------------
 STORES = {}
 
 @contextlib.contextmanager
-def session(request, storage_path, session_cookie='sessionid'):
+def session_context(request, storage_path, session_cookie='sessionid'):
     session_store = STORES.get(storage_path)
     if not session_store:
         session_store = werkzeug.contrib.sessions.FilesystemSessionStore(
@@ -24,3 +297,157 @@ def session(request, storage_path, session_cookie='sessionid'):
         yield request.session
     finally:
         session_store.save(request.session)
+
+#----------------------------------------------------------
+# OpenERP Web Module/Controller Loading and URL Routing
+#----------------------------------------------------------
+addons_module = {}
+addons_manifest = {}
+controllers_class = {}
+controllers_object = {}
+controllers_path = {}
+
+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 = '/web/webclient/home'
+        self.config = options
+
+        if self.config.backend == 'local':
+            conn = openerplib.get_connector(protocol='local')
+        else:
+            conn = openerplib.get_connector(hostname=self.config.server_host,
+                   port=self.config.server_port)
+        self.config.connector = conn
+
+        self.session_cookie = 'sessionid'
+        self.addons = {}
+
+        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 == '/':
+            params = urllib.urlencode(dict(request.args, debug=''))
+            return werkzeug.utils.redirect(self.root + '?' + params, 301)(
+                environ, start_response)
+        elif request.path == '/mobile':
+            return werkzeug.utils.redirect(
+                '/web_mobile/static/src/web_mobile.html', 301)(environ, start_response)
+
+        handler = self.find_handler(*(request.path.split('/')[1:]))
+
+        if not handler:
+            response = werkzeug.exceptions.NotFound()
+        else:
+            with session_context(request, self.session_storage, self.session_cookie) as session:
+                result = handler( request, self.config)
+
+                if isinstance(result, basestring):
+                    headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
+                    response = werkzeug.wrappers.Response(result, headers=headers)
+                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 = {}
+        for addons_path in self.config.addons_path:
+            if addons_path not in sys.path:
+                sys.path.insert(0, addons_path)
+            for module in os.listdir(addons_path):
+                if module not in addons_module:
+                    manifest_path = os.path.join(addons_path, module, '__openerp__.py')
+                    path_static = os.path.join(addons_path, module, 'static')
+                    if os.path.isfile(manifest_path) and os.path.isdir(path_static):
+                        manifest = ast.literal_eval(open(manifest_path).read())
+                        manifest['addons_path'] = addons_path
+                        _logger.info("Loading %s", module)
+                        m = __import__(module)
+                        addons_module[module] = m
+                        addons_manifest[module] = manifest
+                        statics['/%s/static' % module] = path_static
+        for k, v in controllers_class.items():
+            if k not in controllers_object:
+                o = v()
+                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):
+                        _logger.debug("Dispatching to %s %s %s", ps, c, meth)
+                        return m
+        return None
+
+#
index 2ed50bd..e8027db 100644 (file)
@@ -1,17 +1,16 @@
 #!/usr/bin/python
 import datetime
 import dateutil.relativedelta
+import logging
 import time
 import openerplib
 
 import nonliterals
 
-import logging
 _logger = logging.getLogger(__name__)
 #----------------------------------------------------------
 # OpenERPSession RPC openerp backend access
 #----------------------------------------------------------
-
 class OpenERPSession(object):
     """
     An OpenERP RPC session, a given user can own multiple such sessions
diff --git a/addons/web/common/xml2json.py b/addons/web/common/xml2json.py
new file mode 100644 (file)
index 0000000..1602262
--- /dev/null
@@ -0,0 +1,38 @@
+# xml2json-direct
+# Simple and straightforward XML-to-JSON converter in Python
+# New BSD Licensed
+#
+# URL: http://code.google.com/p/xml2json-direct/
+
+class Xml2Json(object):
+    @staticmethod
+    def convert_to_json(s):
+        return simplejson.dumps(
+            Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
+
+    @staticmethod
+    def convert_to_structure(s):
+        root = ElementTree.fromstring(s)
+        return Xml2Json.convert_element(root)
+
+    @staticmethod
+    def convert_element(el, skip_whitespaces=True):
+        res = {}
+        if el.tag[0] == "{":
+            ns, name = el.tag.rsplit("}", 1)
+            res["tag"] = name
+            res["namespace"] = ns[1:]
+        else:
+            res["tag"] = el.tag
+        res["attrs"] = {}
+        for k, v in el.items():
+            res["attrs"][k] = v
+        kids = []
+        if el.text and (not skip_whitespaces or el.text.strip() != ''):
+            kids.append(el.text)
+        for kid in el:
+            kids.append(Xml2Json.convert_element(kid))
+            if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
+                kids.append(kid.tail)
+        res["children"] = kids
+        return res
index 9be0794..7ca80fb 100644 (file)
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 
+import ast
 import base64
 import csv
 import glob
@@ -9,60 +10,16 @@ import os
 import re
 import simplejson
 import textwrap
-import xmlrpclib
 import time
+import xmlrpclib
 import zlib
 from xml.etree import ElementTree
 from cStringIO import StringIO
 
-from babel.messages.pofile import read_po
-
-import web.common.dispatch as openerpweb
-import web.common.ast
-import web.common.nonliterals
-import web.common.release
-openerpweb.ast = web.common.ast
-openerpweb.nonliterals = web.common.nonliterals
-
-
-# Should move to openerpweb.Xml2Json
-class Xml2Json:
-    # xml2json-direct
-    # Simple and straightforward XML-to-JSON converter in Python
-    # New BSD Licensed
-    #
-    # URL: http://code.google.com/p/xml2json-direct/
-    @staticmethod
-    def convert_to_json(s):
-        return simplejson.dumps(
-            Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
-
-    @staticmethod
-    def convert_to_structure(s):
-        root = ElementTree.fromstring(s)
-        return Xml2Json.convert_element(root)
-
-    @staticmethod
-    def convert_element(el, skip_whitespaces=True):
-        res = {}
-        if el.tag[0] == "{":
-            ns, name = el.tag.rsplit("}", 1)
-            res["tag"] = name
-            res["namespace"] = ns[1:]
-        else:
-            res["tag"] = el.tag
-        res["attrs"] = {}
-        for k, v in el.items():
-            res["attrs"][k] = v
-        kids = []
-        if el.text and (not skip_whitespaces or el.text.strip() != ''):
-            kids.append(el.text)
-        for kid in el:
-            kids.append(Xml2Json.convert_element(kid))
-            if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
-                kids.append(kid.tail)
-        res["children"] = kids
-        return res
+import babel.messages.pofile
+
+import web.common
+openerpweb = web.common.http
 
 #----------------------------------------------------------
 # OpenERP Web web Controllers
@@ -200,7 +157,7 @@ class WebClient(openerpweb.Controller):
                     continue
                 try:
                     with open(f_name) as t_file:
-                        po = read_po(t_file)
+                        po = babel.messages.pofile.read_po(t_file)
                 except:
                     continue
                 for x in po:
@@ -358,13 +315,22 @@ class Session(openerpweb.Controller):
 
     @openerpweb.jsonrequest
     def modules(self, req):
-        # TODO query server for installed web modules
-        mods = []
-        for name, manifest in openerpweb.addons_manifest.items():
-            # TODO replace by ir.module.module installed web
-            if name not in req.config.server_wide_modules and manifest.get('active', True):
-                mods.append(name)
-        return mods
+        # Compute available candidates module
+        loadable = openerpweb.addons_manifest.iterkeys()
+        loaded = req.config.server_wide_modules
+        candidates = [mod for mod in loadable if mod not in loaded]
+
+        # Compute active true modules that might be on the web side only
+        active = set(name for name in candidates
+                     if openerpweb.addons_manifest[name].get('active'))
+
+        # Retrieve database installed modules
+        Modules = req.session.model('ir.module.module')
+        installed = set(module['name'] for module in Modules.search_read(
+            [('state','=','installed'), ('name','in', candidates)], ['name']))
+
+        # Merge both
+        return list(active | installed)
 
     @openerpweb.jsonrequest
     def eval_domain_and_context(self, req, contexts, domains,
@@ -399,8 +365,8 @@ class Session(openerpweb.Controller):
                 no group by should be performed)
         """
         context, domain = eval_context_and_domain(req.session,
-                                                  openerpweb.nonliterals.CompoundContext(*(contexts or [])),
-                                                  openerpweb.nonliterals.CompoundDomain(*(domains or [])))
+                                                  web.common.nonliterals.CompoundContext(*(contexts or [])),
+                                                  web.common.nonliterals.CompoundDomain(*(domains or [])))
 
         group_by_sequence = []
         for candidate in (group_by_seq or []):
@@ -817,7 +783,7 @@ class View(openerpweb.Controller):
             xml = self.transform_view(arch, session, evaluation_context)
         else:
             xml = ElementTree.fromstring(arch)
-        fvg['arch'] = Xml2Json.convert_element(xml)
+        fvg['arch'] = web.common.xml2json.Xml2Json.convert_element(xml)
 
         for field in fvg['fields'].itervalues():
             if field.get('views'):
@@ -882,7 +848,7 @@ class View(openerpweb.Controller):
 
     def parse_domain(self, domain, session):
         """ Parses an arbitrary string containing a domain, transforms it
-        to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
+        to either a literal domain or a :class:`web.common.nonliterals.Domain`
 
         :param domain: the domain to parse, if the domain is not a string it
                        is assumed to be a literal domain and is returned as-is
@@ -892,14 +858,14 @@ class View(openerpweb.Controller):
         if not isinstance(domain, (str, unicode)):
             return domain
         try:
-            return openerpweb.ast.literal_eval(domain)
+            return ast.literal_eval(domain)
         except ValueError:
             # not a literal
-            return openerpweb.nonliterals.Domain(session, domain)
+            return web.common.nonliterals.Domain(session, domain)
 
     def parse_context(self, context, session):
         """ Parses an arbitrary string containing a context, transforms it
-        to either a literal context or a :class:`openerpweb.nonliterals.Context`
+        to either a literal context or a :class:`web.common.nonliterals.Context`
 
         :param context: the context to parse, if the context is not a string it
                is assumed to be a literal domain and is returned as-is
@@ -909,9 +875,9 @@ class View(openerpweb.Controller):
         if not isinstance(context, (str, unicode)):
             return context
         try:
-            return openerpweb.ast.literal_eval(context)
+            return ast.literal_eval(context)
         except ValueError:
-            return openerpweb.nonliterals.Context(session, context)
+            return web.common.nonliterals.Context(session, context)
 
     def parse_domains_and_contexts(self, elem, session):
         """ Converts domains and contexts from the view into Python objects,
@@ -999,10 +965,10 @@ class SearchView(View):
     @openerpweb.jsonrequest
     def save_filter(self, req, model, name, context_to_save, domain):
         Model = req.session.model("ir.filters")
-        ctx = openerpweb.nonliterals.CompoundContext(context_to_save)
+        ctx = web.common.nonliterals.CompoundContext(context_to_save)
         ctx.session = req.session
         ctx = ctx.evaluate()
-        domain = openerpweb.nonliterals.CompoundDomain(domain)
+        domain = web.common.nonliterals.CompoundDomain(domain)
         domain.session = req.session
         domain = domain.evaluate()
         uid = req.session._uid
@@ -1025,16 +991,17 @@ class Binary(openerpweb.Controller):
 
         try:
             if not id:
-                res = Model.default_get([field], context).get(field, '')
+                res = Model.default_get([field], context).get(field)
             else:
-                res = Model.read([int(id)], [field], context)[0].get(field, '')
+                res = Model.read([int(id)], [field], context)[0].get(field)
             image_data = base64.b64decode(res)
         except (TypeError, xmlrpclib.Fault):
             image_data = self.placeholder(req)
         return req.make_response(image_data, [
             ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
     def placeholder(self, req):
-        return open(os.path.join(req.addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
+        addons_path = openerpweb.addons_manifest['web']['addons_path']
+        return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
 
     @openerpweb.httprequest
     def saveas(self, req, model, id, field, fieldname, **kw):
@@ -1393,7 +1360,7 @@ class Reports(View):
 
         report_srv = req.session.proxy("report")
         context = req.session.eval_context(
-            openerpweb.nonliterals.CompoundContext(
+            web.common.nonliterals.CompoundContext(
                 req.context or {}, action[ "context"]))
 
         report_data = {}
@@ -1432,7 +1399,6 @@ class Reports(View):
                  ('Content-Length', len(report))],
              cookies={'fileToken': int(token)})
 
-
 class Import(View):
     _cp_path = "/web/import"
 
index 8c99823..7bcf65e 100644 (file)
@@ -855,7 +855,7 @@ label.error {
     cursor: help;
 }
 
-.openerp .oe_form_field label.oe_label, .openerp .oe_form_field label.oe_label_help {
+.openerp .oe_forms label.oe_label, .openerp .oe_forms label.oe_label_help {
     text-align: right;
     margin: 3px 0 0 10px;
 }
@@ -943,7 +943,7 @@ label.error {
     position: relative;
 }
 .openerp input.oe-binary-file {
-    z-index: 2;
+    z-index: 0;
     line-height: 0;
     font-size: 50px;
     position: absolute;
index 2e9294d..da9c21b 100644 (file)
@@ -480,14 +480,12 @@ openerp.web.Session = openerp.web.CallbackEnabled.extend( /** @lends openerp.web
     session_login: function(db, login, password, success_callback) {
         var self = this;
         var params = { db: db, login: login, password: password };
-        this.rpc("/web/session/login", params, function(result) {
+        return this.rpc("/web/session/login", params, function(result) {
             self.session_id = result.session_id;
             self.uid = result.uid;
             self.user_context = result.context;
             self.db = result.db;
             self.session_save();
-            if (self.uid)
-                self.on_session_valid();
             return true;
         }).then(success_callback);
     },
index e7feedf..d3d88bc 100644 (file)
@@ -179,9 +179,13 @@ openerp.web.auto_date_to_str = function(value, type) {
  * @param {String} [column.string] button label
  * @param {String} [column.icon] button icon
  * @param {String} [value_if_empty=''] what to display if the field's value is ``false``
+ * @param {Boolean} [process_modifiers=true] should the modifiers be computed ?
  */
-openerp.web.format_cell = function (row_data, column, value_if_empty) {
-    var attrs = column.modifiers_for(row_data);
+openerp.web.format_cell = function (row_data, column, value_if_empty, process_modifiers) {
+    var attrs = {};
+    if (process_modifiers !== false) {
+        attrs = column.modifiers_for(row_data);
+    }
     if (attrs.invisible) { return ''; }
     if (column.tag === 'button') {
         return [
index db91d6b..5f88b9a 100644 (file)
@@ -13,24 +13,35 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
      * @param view_id
      * @param defaults
      */
-    init: function(parent, dataset, view_id, defaults) {
+    init: function(parent, dataset, view_id, defaults, hidden) {
         this._super(parent);
         this.dataset = dataset;
         this.model = dataset.model;
         this.view_id = view_id;
 
         this.defaults = defaults || {};
+        this.has_defaults = !_.isEmpty(this.defaults);
 
         this.inputs = [];
         this.enabled_filters = [];
 
         this.has_focus = false;
 
+        this.hidden = !!hidden;
+        this.headless = this.hidden && !this.has_defaults;
+
         this.ready = $.Deferred();
     },
     start: function() {
         this._super();
-        this.rpc("/web/searchview/load", {"model": this.model, "view_id":this.view_id}, this.on_loaded);
+        if (this.hidden) {
+            this.$element.hide();
+        }
+        if (this.headless) {
+            this.ready.resolve();
+        } else {
+            this.rpc("/web/searchview/load", {"model": this.model, "view_id":this.view_id}, this.on_loaded);
+        }
         return this.ready.promise();
     },
     show: function () {
@@ -105,8 +116,9 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
      */
     make_field: function (item, field) {
         try {
-            return new (openerp.web.search.fields.get_object(field.type))
-                        (item, field, this);
+            return new (openerp.web.search.fields.get_any(
+                    [item.attrs.widget, field.type]))
+                (item, field, this);
         } catch (e) {
             if (! e instanceof openerp.web.KeyNotFound) {
                 throw e;
@@ -137,10 +149,7 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
             'defaults': this.defaults
         });
 
-        // We don't understand why the following commented line does not work in Chrome but
-        // the non-commented line does. As far as we investigated, only God knows.
-        //this.$element.html(render);
-        jQuery(render).appendTo(this.$element);
+        this.$element.html(render);
         this.$element.find(".oe_search-view-custom-filter-btn").click(ext.on_activate);
 
         var f = this.$element.find('form');
@@ -246,10 +255,13 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
      * @param e jQuery event object coming from the "Search" button
      */
     do_search: function (e) {
+        if (this.headless && !this.has_defaults) {
+            return this.on_search([], [], []);
+        }
         // reset filters management
         var select = this.$element.find(".oe_search-view-filters-management");
         select.val("_filters");
-        
+
         if (e && e.preventDefault) { e.preventDefault(); }
 
         var data = this.build_search_data();
@@ -327,7 +339,7 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
         this.notification.notify("Invalid Search", "triggered from search view");
     },
     do_clear: function () {
-        this.$element.find('.filter_label').removeClass('enabled');
+        this.$element.find('.filter_label, .filter_icon').removeClass('enabled');
         this.enabled_filters.splice(0);
         var string = $('a.searchview_group_string');
         _.each(string, function(str){
@@ -748,9 +760,30 @@ openerp.web.search.FloatField = openerp.web.search.NumberField.extend(/** @lends
  * @extends openerp.web.search.Field
  */
 openerp.web.search.SelectionField = openerp.web.search.Field.extend(/** @lends openerp.web.search.SelectionField# */{
+    // This implementation is a basic <select> field, but it may have to be
+    // altered to be more in line with the GTK client, which uses a combo box
+    // (~ jquery.autocomplete):
+    // * If an option was selected in the list, behave as currently
+    // * If something which is not in the list was entered (via the text input),
+    //   the default domain should become (`ilike` string_value) but **any
+    //   ``context`` or ``filter_domain`` becomes falsy, idem if ``@operator``
+    //   is specified. So at least get_domain needs to be quite a bit
+    //   overridden (if there's no @value and there is no filter_domain and
+    //   there is no @operator, return [[name, 'ilike', str_val]]
     template: 'SearchView.field.selection',
+    init: function () {
+        this._super.apply(this, arguments);
+        // prepend empty option if there is no empty option in the selection list
+        this.prepend_empty = !_(this.attrs.selection).detect(function (item) {
+            return !item[1];
+        });
+    },
     get_value: function () {
-        return this.$element.val();
+        var index = parseInt(this.$element.val(), 10);
+        if (isNaN(index)) { return null; }
+        var value = this.attrs.selection[index][0];
+        if (value === false) { return null; }
+        return value;
     }
 });
 openerp.web.search.BooleanField = openerp.web.search.SelectionField.extend(/** @lends openerp.web.search.BooleanField# */{
index 11ebad8..b4eba50 100644 (file)
@@ -463,9 +463,6 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
             return $.Deferred().then(success).resolve(_.extend(r, {created: true}));
         }
     },
-    do_search: function (domains, contexts, groupbys) {
-        console.debug("Searching form");
-    },
     on_action: function (action) {
         console.debug('Executing action', action);
     },
@@ -688,7 +685,8 @@ openerp.web.form.Widget = openerp.web.Widget.extend(/** @lends openerp.web.form.
         this.width = this.node.attrs.width;
     },
     start: function() {
-        this.$element = this.view.$element.find('.' + this.element_class);
+        this.$element = this.view.$element.find(
+            '.' + this.element_class.replace(/[^\r\n\f0-9A-Za-z_-]/g, "\\$&"));
     },
     process_modifiers: function() {
         var compute_domain = openerp.web.form.compute_domain;
@@ -792,7 +790,7 @@ openerp.web.form.WidgetNotebook = openerp.web.form.Widget.extend({
         for (var i = 0; i < node.children.length; i++) {
             var n = node.children[i];
             if (n.tag == "page") {
-                var page = new openerp.web.form.WidgetNotebookPage(
+                var page = new (this.view.registry.get_object('notebookpage'))(
                         this.view, n, this, this.pages.length);
                 this.pages.push(page);
             }
@@ -898,11 +896,11 @@ openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({
                             self.on_confirmed().then(function() {
                                 def.resolve();
                             });
-                            $(self).dialog("close");
+                            $(this).dialog("close");
                         },
                         Cancel: function() {
                             def.resolve();
-                            $(self).dialog("close");
+                            $(this).dialog("close");
                         }
                     }
                 });
@@ -911,7 +909,7 @@ openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({
                 return self.on_confirmed();
             }
         };
-        if ((!this.node.attrs.special && this.view.dirty_for_user) || !this.view.datarecord.id) {
+        if (!this.node.attrs.special && (this.view.dirty_for_user || !this.view.datarecord.id)) {
             return this.view.recursive_save().pipe(exec_action);
         } else {
             return exec_action();
@@ -1787,6 +1785,9 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({
             result.result.context = _.extend(result.result.context || {}, additional_context);
             self.do_action(result.result);
         });
+    },
+    focus: function () {
+        this.$input.focus();
     }
 });
 
@@ -2243,11 +2244,11 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope
                 });
         this.searchview.on_search.add(function(domains, contexts, groupbys) {
             if (self.initial_ids) {
-                self.view_list.do_search.call(self, domains.concat([[["id", "in", self.initial_ids]], self.domain]),
+                self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
                     contexts, groupbys);
                 self.initial_ids = undefined;
             } else {
-                self.view_list.do_search.call(self, domains.concat([self.domain]), contexts, groupbys);
+                self.do_search(domains.concat([self.domain]), contexts, groupbys);
             }
         });
         this.searchview.on_loaded.add_last(function () {
@@ -2274,10 +2275,19 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope
             }).pipe(function() {
                 self.searchview.do_search();
             });
-            
         });
         this.searchview.appendTo($("#" + this.element_id + "_search"));
     },
+    do_search: function(domains, contexts, groupbys) {
+        var self = this;
+        this.rpc('/web/session/eval_domain_and_context', {
+            domains: domains || [],
+            contexts: contexts || [],
+            group_by_seq: groupbys || []
+        }, function (results) {
+            self.view_list.do_search(results.domain, results.context, results.group_by);
+        });
+    },
     create_row: function(data) {
         var self = this;
         var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain);
@@ -2781,6 +2791,7 @@ openerp.web.form.widgets = new openerp.web.Registry({
     'frame' : 'openerp.web.form.WidgetFrame',
     'group' : 'openerp.web.form.WidgetFrame',
     'notebook' : 'openerp.web.form.WidgetNotebook',
+    'notebookpage' : 'openerp.web.form.WidgetNotebookPage',
     'separator' : 'openerp.web.form.WidgetSeparator',
     'label' : 'openerp.web.form.WidgetLabel',
     'button' : 'openerp.web.form.WidgetButton',
index 8a9deaa..bb7b502 100644 (file)
@@ -391,9 +391,6 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
         if (this.sidebar) {
             this.sidebar.$element.show();
         }
-        if (!_(this.dataset.ids).isEmpty()) {
-            this.reload_content();
-        }
     },
     do_hide: function () {
         this.$element.hide();
@@ -443,40 +440,21 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
             }));
     },
     /**
-     * Event handler for a search, asks for the computation/folding of domains
-     * and contexts (and group-by), then reloads the view's content.
-     *
-     * @param {Array} domains a sequence of literal and non-literal domains
-     * @param {Array} contexts a sequence of literal and non-literal contexts
-     * @param {Array} groupbys a sequence of literal and non-literal group-by contexts
-     * @returns {$.Deferred} fold request evaluation promise
-     */
-    do_search: function (domains, contexts, groupbys) {
-        return this.rpc('/web/session/eval_domain_and_context', {
-            domains: _([this.dataset.get_domain()].concat(domains)).compact(),
-            contexts: _([this.dataset.get_context()].concat(contexts)).compact(),
-            group_by_seq: groupbys
-        }, $.proxy(this, 'do_actual_search'));
-    },
-    /**
      * Handler for the result of eval_domain_and_context, actually perform the
      * searching
      *
      * @param {Object} results results of evaluating domain and process for a search
      */
-    do_actual_search: function (results) {
+    do_search: function (domain, context, group_by) {
         this.groups.datagroup = new openerp.web.DataGroup(
-            this, this.model,
-            results.domain,
-            results.context,
-            results.group_by);
+            this, this.model, domain, context, group_by);
         this.groups.datagroup.sort = this.dataset._sort;
 
-        if (_.isEmpty(results.group_by) && !results.context['group_by_no_leaf']) {
-            results.group_by = null;
+        if (_.isEmpty(group_by) && !context['group_by_no_leaf']) {
+            group_by = null;
         }
 
-        this.reload_view(!!results.group_by, results.context).then(
+        this.reload_view(!!group_by, context).then(
             $.proxy(this, 'reload_content'));
     },
     /**
@@ -505,7 +483,13 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
     do_select: function (ids, records) {
         this.$element.find('.oe-list-delete')
             .attr('disabled', !ids.length);
-
+        if (this.sidebar) {
+            if (ids.length) {
+                this.sidebar.do_unfold();
+            } else {
+                this.sidebar.do_fold();
+            }
+        }
         if (!records.length) {
             this.compute_aggregates();
             return;
@@ -536,14 +520,8 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
      * @param {openerp.web.DataSet} dataset dataset in which the record is available (may not be the listview's dataset in case of nested groups)
      */
     do_activate_record: function (index, id, dataset) {
-        var self = this;
-        // TODO is it needed ?
-        this.dataset.read_slice([],{
-                context: dataset.get_context(),
-                domain: dataset.get_domain()
-            }, function () {
-                self.select_record(index);
-        });
+        this.dataset.ids = dataset.ids;
+        this.select_record(index);
     },
     /**
      * Handles signal for the addition of a new record (can be a creation,
@@ -642,7 +620,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
             }
 
             $footer_cells.filter(_.sprintf('[data-field=%s]', column.id))
-                .html(openerp.web.format_cell(aggregation, column));
+                .html(openerp.web.format_cell(aggregation, column, undefined, false));
         });
     },
     get_selected_ids: function() {
@@ -837,7 +815,9 @@ openerp.web.ListView.List = openerp.web.Class.extend( /** @lends openerp.web.Lis
         cells.push('</tr>');
 
         var row = cells.join('');
-        this.$current.append(new Array(count - this.records.length + 1).join(row));
+        this.$current
+            .children('tr:not([data-id])').remove().end()
+            .append(new Array(count - this.records.length + 1).join(row));
         this.refresh_zebra(this.records.length);
     },
     /**
@@ -918,6 +898,7 @@ openerp.web.ListView.List = openerp.web.Class.extend( /** @lends openerp.web.Lis
             options: this.options,
             record: record,
             row_parity: (index % 2 === 0) ? 'even' : 'odd',
+            view: this.view,
             render_cell: openerp.web.format_cell
         });
     },
index beb2c3a..dc49c20 100644 (file)
@@ -56,11 +56,11 @@ openerp.web.list_editable = function (openerp) {
                     || this.defaults.editable);
         },
         /**
-         * Replace do_actual_search to handle editability process
+         * Replace do_search to handle editability process
          */
-        do_actual_search: function (results) {
-            this.set_editable(results.context['set_editable']);
-            this._super(results);
+        do_search: function(domain, context, group_by) {
+            this.set_editable(context['set_editable']);
+            this._super.apply(this, arguments);
         },
         /**
          * Replace do_add_record to handle editability (and adding new record
@@ -120,6 +120,7 @@ openerp.web.list_editable = function (openerp) {
                 delete self.edition_id;
                 delete self.edition;
             });
+            this.pad_table_to(5);
             return cancelled.promise();
         },
         /**
@@ -147,7 +148,7 @@ openerp.web.list_editable = function (openerp) {
                 var $new_row = $('<tr>', {
                         id: _.uniqueId('oe-editable-row-'),
                         'data-id': record_id,
-                        'class': $(row).attr('class') + ' oe_forms',
+                        'class': row ? $(row).attr('class') : '' + ' oe_forms',
                         click: function (e) {e.stopPropagation();}
                     })
                     .delegate('button.oe-edit-row-save', 'click', function () {
@@ -173,10 +174,22 @@ openerp.web.list_editable = function (openerp) {
                     });
                 if (row) {
                     $new_row.replaceAll(row);
-                } else if (self.options.editable === 'top') {
-                    self.$current.prepend($new_row);
                 } else if (self.options.editable) {
-                    self.$current.append($new_row);
+                    var $last_child = self.$current.children('tr:last');
+                    if (self.records.length) {
+                        if (self.options.editable === 'top') {
+                            $new_row.insertBefore(
+                                self.$current.children('[data-id]:first'));
+                        } else {
+                            $new_row.insertAfter(
+                                self.$current.children('[data-id]:last'));
+                        }
+                    } else {
+                        $new_row.prependTo(self.$current);
+                    }
+                    if ($last_child.is(':not([data-id])')) {
+                        $last_child.remove();
+                    }
                 }
                 self.edition = true;
                 self.edition_id = record_id;
@@ -325,11 +338,13 @@ openerp.web.list_editable = function (openerp) {
                 this.$element.children().css('visibility', '');
                 if (this.modifiers.tree_invisible) {
                     var old_invisible = this.invisible;
-                    this.invisible = !!this.modifiers.tree_invisible;
+                    this.invisible = true;
                     this._super();
                     this.invisible = old_invisible;
                 } else if (this.invisible) {
                     this.$element.children().css('visibility', 'hidden');
+                } else {
+                    this._super();
                 }
             }
         });
index 4a9e57e..46091fa 100644 (file)
@@ -116,6 +116,9 @@ db.web.ActionManager = db.web.Widget.extend({
         */
     },
     ir_actions_act_window_close: function (action, on_closed) {
+        if (!this.dialog && on_closed) {
+            on_closed();
+        }
         this.dialog_stop();
     },
     ir_actions_server: function (action, on_closed) {
@@ -166,6 +169,7 @@ db.web.ViewManager =  db.web.Widget.extend(/** @lends db.web.ViewManager# */{
         this.model = dataset ? dataset.model : undefined;
         this.dataset = dataset;
         this.searchview = null;
+        this.last_search = false;
         this.active_view = null;
         this.views_src = _.map(views, function(x) {return x instanceof Array? {view_id: x[0], view_type: x[1]} : x;});
         this.views = {};
@@ -224,35 +228,21 @@ db.web.ViewManager =  db.web.Widget.extend(/** @lends db.web.ViewManager# */{
                 controller.set_embedded_view(view.embedded_view);
             }
             controller.do_switch_view.add_last(this.on_mode_switch);
-            if (view_type === 'list' && this.flags.search_view === false && this.action && this.action['auto_search']) {
-                // In case the search view is not instantiated: manually call ListView#search
-                var domains = !_(self.action.domain).isEmpty()
-                                ? [self.action.domain] : [],
-                   contexts = !_(self.action.context).isEmpty()
-                                ? [self.action.context] : [];
-                controller.on_loaded.add({
-                    callback: function () {
-                        controller.do_search(domains, contexts, []);
-                    },
-                    position: 'last',
-                    unique: true
-                });
-            }
             var container = $("#" + this.element_id + '_view_' + view_type);
             view_promise = controller.appendTo(container);
+            this.views[view_type].controller = controller;
             $.when(view_promise).then(function() {
                 self.on_controller_inited(view_type, controller);
+                if (self.searchview && view.controller.searchable !== false) {
+                    self.do_searchview_search();
+                }
             });
-            this.views[view_type].controller = controller;
+        } else if (this.searchview && view.controller.searchable !== false) {
+            self.do_searchview_search();
         }
 
-
         if (this.searchview) {
-            if (view.controller.searchable === false) {
-                this.searchview.hide();
-            } else {
-                this.searchview.show();
-            }
+            this.searchview[(view.controller.searchable === false || this.searchview.hidden) ? 'hide' : 'show']();
         }
 
         this.$element
@@ -273,14 +263,6 @@ db.web.ViewManager =  db.web.Widget.extend(/** @lends db.web.ViewManager# */{
         return view_promise;
     },
     /**
-     * Event launched when a controller has been inited.
-     *
-     * @param {String} view_type type of view
-     * @param {String} view the inited controller
-     */
-    on_controller_inited: function(view_type, view) {
-    },
-    /**
      * Sets up the current viewmanager's search view.
      *
      * @param {Number|false} view_id the view to use or false for a default one
@@ -293,14 +275,37 @@ db.web.ViewManager =  db.web.Widget.extend(/** @lends db.web.ViewManager# */{
         }
         this.searchview = new db.web.SearchView(
                 this, this.dataset,
-                view_id, search_defaults);
+                view_id, search_defaults, this.flags.search_view === false);
 
-        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);
-        });
+        this.searchview.on_search.add(this.do_searchview_search);
         return this.searchview.appendTo($("#" + this.element_id + "_search"));
     },
+    do_searchview_search: function(domains, contexts, groupbys) {
+        var self = this,
+            controller = this.views[this.active_view].controller;
+        if (domains || contexts) {
+            this.rpc('/web/session/eval_domain_and_context', {
+                domains: [this.action.domain || []].concat(domains || []),
+                contexts: [this.action.context || {}].concat(contexts || []),
+                group_by_seq: groupbys || []
+            }, function (results) {
+                self.dataset.context = results.context;
+                self.dataset.domain = results.domain;
+                self.last_search = [results.domain, results.context, results.group_by];
+                controller.do_search(results.domain, results.context, results.group_by);
+            });
+        } else if (this.last_search) {
+            controller.do_search.apply(controller, this.last_search);
+        }
+    },
+    /**
+     * Event launched when a controller has been inited.
+     *
+     * @param {String} view_type type of view
+     * @param {String} view the inited controller
+     */
+    on_controller_inited: function(view_type, view) {
+    },
     /**
      * Called when one of the view want to execute an action
      */
@@ -360,28 +365,24 @@ db.web.ViewManagerAction = db.web.ViewManager.extend(/** @lends oepnerp.web.View
      * 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 = {};
-            _.each(this.action.context, function (value, key) {
-                var match = /^search_default_(.*)$/.exec(key);
-                if (match) {
-                    search_defaults[match[1]] = value;
-                }
-            });
-            // init search view
-            var searchview_id = this.action['search_view_id'] && this.action['search_view_id'][0];
+        var self = this,
+            searchview_loaded,
+            search_defaults = {};
+        _.each(this.action.context, function (value, key) {
+            var match = /^search_default_(.*)$/.exec(key);
+            if (match) {
+                search_defaults[match[1]] = value;
+            }
+        });
+        // init search view
+        var searchview_id = this.action['search_view_id'] && this.action['search_view_id'][0];
 
-            searchview_loaded = this.setup_search_view(
-                    searchview_id || false, search_defaults);
-        }
+        searchview_loaded = this.setup_search_view(searchview_id || false, search_defaults);
 
         var main_view_loaded = this._super();
 
         var manager_ready = $.when(searchview_loaded, main_view_loaded);
-        if (searchview_loaded && this.action['auto_search']) {
+        if (searchview_loaded && this.action['auto_search'] !== false) {
             // schedule auto_search
             manager_ready.then(this.searchview.do_search);
         }
@@ -816,6 +817,8 @@ db.web.View = db.web.Widget.extend(/** @lends db.web.View# */{
     },
     do_switch_view: function(view) {
     },
+    do_search: function(view) {
+    },
     set_common_sidebar_sections: function(sidebar) {
         sidebar.add_section('customize', "Customize", [
             {
index f8519c1..0e5c97c 100644 (file)
     <input type="text" size="1"
         t-att-name="widget.name"
         t-att-id="widget.element_id"
-        t-attf-class="field_#{widget.type} #{widget.element_class}"
+        t-attf-class="field_#{widget.type}"
         t-attf-style="width: #{widget.field.translate ? '99' : '100'}%"
     />
     <img class="oe_field_translate" t-if="widget.field.translate" src="/web/static/src/img/icons/terp-translate.png" width="16" height="16" border="0"/>
 <t t-name="FieldChar.readonly">
     <div
         t-att-id="widget.element_id"
-        t-attf-class="field_#{widget.type} #{widget.element_class}"
+        t-attf-class="field_#{widget.type}"
         t-attf-style="width: #{widget.field.translate ? '99' : '100'}%">
     </div>
 </t>
     <textarea rows="6"
         t-att-name="widget.name"
         t-att-id="widget.element_id"
-        t-attf-class="field_#{widget.type} #{widget.element_class}"
+        t-attf-class="field_#{widget.type}"
         t-attf-style="width: #{widget.field.translate ? '99' : '100'}%"
     ></textarea>
     <img class="oe_field_translate" t-if="widget.field.translate" src="/web/static/src/img/icons/terp-translate.png" width="16" height="16" border="0"/>
 <t t-name="FieldSelection">
     <select
         t-att-name="widget.name"
-        t-att-id="widget.element_id + '_field'"
-        t-attf-class="field_#{widget.type} #{widget.element_class}"
+        t-att-id="widget.element_id"
+        t-attf-class="field_#{widget.type}"
         style="width: 100%">
             <t t-foreach="widget.values" t-as="option">
                 <option><t t-esc="option[1]"/></option>
     </select>
 </t>
 <t t-name="FieldMany2One">
-    <div t-att-class="widget.element_class" class="oe-m2o">
-        <input type="text" size="1" style="width: 100%;"/>
+    <div class="oe-m2o">
+        <input type="text" size="1" style="width: 100%;"
+                t-att-id="widget.element_id"/>
         <span class="oe-m2o-drop-down-button">
             <img src="/web/static/src/img/down-arrow.png" /></span>
         <span class="oe-m2o-cm-button" t-att-id="widget.name + '_open'">
 <t t-name="FieldBoolean">
     <input type="checkbox"
         t-att-name="widget.name"
-        t-att-id="widget.element_id + '_field'"
-        t-attf-class="field_#{widget.type} #{widget.element_class}"/>
+        t-att-id="widget.element_id"
+        t-attf-class="field_#{widget.type}"/>
 </t>
 <t t-name="FieldProgressBar">
     <div t-opentag="true" class="oe-progressbar">
                 t-att-border="widget.readonly ? 0 : 1"
                 t-att-id="widget.element_id + '_field'"
                 t-att-name="widget.name"
-                t-attf-class="field_#{widget.type} #{widget.element_class}"
+                t-attf-class="field_#{widget.type}"
                 t-att-width="widget.node.attrs.img_width || widget.node.attrs.width"
                 t-att-height="widget.node.attrs.img_height || widget.node.attrs.height"
             />
             <input type="text" size="1"
                 t-att-name="widget.name"
                 t-att-id="widget.element_id + '_field'"
-                t-attf-class="field_#{widget.type} #{widget.element_class}" style="width: 100%"
+                t-attf-class="field_#{widget.type}" style="width: 100%"
             />
         </td>
         <td class="oe-binary" nowrap="true">
 </t>
 <t t-name="WidgetButton">
     <button type="button"
-        t-attf-class="#{widget.element_class}"
         t-att-title="widget.help"
         style="width: 100%" class="button">
         <img t-if="widget.node.attrs.icon" t-att-src="'/web/static/src/img/icons/' + widget.node.attrs.icon + '.png'" width="16" height="16"/>
     <div style="white-space: nowrap;">
         <select t-att-name="attrs.name" t-att-id="element_id"
                 t-att-autofocus="attrs.default_focus === '1' || undefined">
-            <option/>
+            <option t-if="prepend_empty"/>
             <t t-foreach="attrs.selection" t-as="option">
                 <t t-set="selected" t-value="defaults[attrs.name] === option[0]"/>
                 <option t-if="selected"
-                        t-att-value="option[0]" selected="selected">
+                        t-att-value="option_index" selected="selected">
                     <t t-esc="option[1]"/>
                 </option>
-                <option t-if="!selected" t-att-value="option[0]">
+                <option t-if="!selected" t-att-value="option_index">
                     <t t-esc="option[1]"/>
                 </option>
             </t>
index ba18ef0..1eba260 100644 (file)
@@ -1,5 +1,6 @@
 {
     "name": "web calendar",
+    "category" : "Hidden",
     "version": "2.0",
     "depends": ['web'],
     "js": [
index 5de601e..da13012 100644 (file)
@@ -13,9 +13,8 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
         this.set_default_options(options);
         this.dataset = dataset;
         this.model = dataset.model;
+        this.fields_view = {};
         this.view_id = view_id;
-        this.domain = this.dataset.domain || [];
-        this.context = this.dataset.context || {};
         this.has_been_loaded = $.Deferred();
         this.creating_event_id = null;
         this.dataset_events = [];
@@ -31,7 +30,7 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
     },
     start: function() {
         this._super();
-        this.rpc("/web/view/load", {"model": this.model, "view_id": this.view_id, "view_type":"calendar", 'toolbar': true}, this.on_loaded);
+        return this.rpc("/web/view/load", {"model": this.model, "view_id": this.view_id, "view_type":"calendar", 'toolbar': true}, this.on_loaded);
     },
     stop: function() {
         scheduler.clearAll();
@@ -47,6 +46,10 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
         this.name = this.fields_view.name || this.fields_view.arch.attrs.string;
         this.view_id = this.fields_view.view_id;
 
+        // mode, one of month, week or day
+        this.mode = this.fields_view.arch.attrs.mode;
+
+        // date_start is mandatory, date_delay and date_stop are optional
         this.date_start = this.fields_view.arch.attrs.date_start;
         this.date_delay = this.fields_view.arch.attrs.date_delay;
         this.date_stop = this.fields_view.arch.attrs.date_stop;
@@ -55,6 +58,10 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
         this.day_length = this.fields_view.arch.attrs.day_length || 8;
         this.color_field = this.fields_view.arch.attrs.color;
         this.fields =  this.fields_view.fields;
+        
+        if (!this.date_start) {
+            throw new Error("Calendar view has not defined 'date_start' attribute.");
+        }
 
         //* Calendar Fields *
         this.calendar_fields.date_start = {'name': this.date_start, 'kind': this.fields[this.date_start].type};
@@ -68,9 +75,6 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
         if (this.date_stop) {
             this.calendar_fields.date_stop = {'name': this.date_stop, 'kind': this.fields[this.date_stop].type};
         }
-        if (!this.date_delay && !this.date_stop) {
-            throw new Error("Calendar view has none of the following attributes : 'date_stop', 'date_delay'");
-        }
 
         for (var fld = 0; fld < this.fields_view.arch.children.length; fld++) {
             this.info_fields.push(this.fields_view.arch.children[fld].attrs.name);
@@ -92,9 +96,6 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
 
         this.init_scheduler();
         this.has_been_loaded.resolve();
-        if (this.dataset.ids.length) {
-            this.dataset.read_ids(this.dataset.ids, _.keys(this.fields), this.on_events_loaded);
-        }
     },
     init_scheduler: function() {
         var self = this;
@@ -111,9 +112,7 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
         scheduler.config.drag_resize = true;
         scheduler.config.drag_create = true;
 
-        // Initialize Sceduler
-        this.mode = this.mode || 'month';
-        scheduler.init('openerp_scheduler', null, this.mode);
+        scheduler.init('openerp_scheduler', null, this.mode || 'month');
 
         scheduler.detachAllEvents();
         scheduler.attachEvent('onEventAdded', this.do_create_event);
@@ -200,7 +199,7 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
     convert_event: function(evt) {
         var date_start = openerp.web.str_to_datetime(evt[this.date_start]),
             date_stop = this.date_stop ? openerp.web.str_to_datetime(evt[this.date_stop]) : null,
-            date_delay = evt[this.date_delay] || null,
+            date_delay = evt[this.date_delay] || 1.0,
             res_text = '',
             res_description = [];
 
@@ -293,26 +292,17 @@ openerp.web_calendar.CalendarView = openerp.web.View.extend({
         }
         return data;
     },
-    do_search: function(domains, contexts, groupbys) {
+    do_search: function(domain, context, group_by) {
         var self = this;
         scheduler.clearAll();
         $.when(this.has_been_loaded).then(function() {
-            self.rpc('/web/session/eval_domain_and_context', {
-                domains: domains,
-                contexts: contexts,
-                group_by_seq: groupbys
-            }, function (results) {
-                // TODO: handle non-empty results.group_by with read_group
-                self.dataset.context = self.context = results.context;
-                self.dataset.domain = self.domain = results.domain;
-                self.dataset.read_slice(_.keys(self.fields), {
-                        offset:0,
-                        limit: self.limit
-                    }, function(events) {
-                        self.dataset_events = events;
-                        self.on_events_loaded(events);
-                    }
-                );
+            // TODO: handle non-empty results.group_by with read_group
+            self.dataset.read_slice(_.keys(self.fields), {
+                offset: 0,
+                limit: self.limit
+            }, function(events) {
+                self.dataset_events = events;
+                self.on_events_loaded(events);
             });
         });
     },
index c5e2fa7..f0525bc 100644 (file)
@@ -1,5 +1,6 @@
 {
     "name": "Web Chat",
+    "category" : "Hidden",
     "version": "2.0",
     "depends": ['web'],
     "js": [
@@ -10,6 +11,6 @@
         'static/src/js/web_chat.js'
     ],
     "css": [],
-#    'active': True,
     'active': False,
+    'installable': False,
 }
index e9a0267..87d6480 100644 (file)
@@ -2,7 +2,7 @@
 import time
 
 import simplejson
-import web.common as openerpweb
+import web.common.http as openerpweb
 import logging
 
 _logger = logging.getLogger(__name__)
index e4bad9c..7a52751 100644 (file)
@@ -1,5 +1,6 @@
 {
     "name": "web Dashboard",
+    "category" : "Hidden",
     "version": "2.0",
     "depends": ['web'],
     "js": [
index 8023549..37fe039 100644 (file)
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-import web.common as openerpweb
+import web.common.http as openerpweb
 
 WIDGET_CONTENT_PATTERN = """<!DOCTYPE html>
 <html>
index 2093e62..9d9d2f8 100644 (file)
@@ -1,5 +1,6 @@
 {
     "name" : "OpenERP Web installer home",
+    "category" : "Hidden",
     "version" : "2.0",
     "depends" : ['web'],
     'active': True,
index ed8561b..eff0567 100644 (file)
@@ -94,24 +94,34 @@ openerp.web_default_home = function (openerp) {
             })
         },
         install_module: function (module_name) {
+            var self = this;
             var Modules = new openerp.web.DataSetSearch(
-                this, 'ir.module.module', null, [['name', '=', module_name], ['state', '=', 'uninstalled']]);
+                this, 'ir.module.module', null,
+                [['name', '=', module_name], ['state', '=', 'uninstalled']]);
             var Upgrade = new openerp.web.DataSet(this, 'base.module.upgrade');
 
             $.blockUI({message:'<img src="/web/static/src/img/throbber2.gif">'});
             Modules.read_slice(['id'], {}, function (records) {
-                if (!(records.length === 1)) { return; }
+                if (!(records.length === 1)) { $.unblockUI(); return; }
                 Modules.call('state_update',
                     [_.pluck(records, 'id'), 'to install', ['uninstalled']],
                     function () {
                         Upgrade.call('upgrade_module', [[]], function () {
-                            $.unblockUI();
-                            // TODO: less brutal reloading
-                            window.location.reload(true);
+                            self.run_configuration_wizards();
                         });
                     }
                 )
             });
+        },
+        run_configuration_wizards: function () {
+            var self = this;
+            new openerp.web.DataSet(this, 'res.config').call('start', [[]], function (action) {
+                $.unblockUI();
+                self.do_action(action, function () {
+                    // TODO: less brutal reloading
+                    window.location.reload(true);
+                });
+            });
         }
     });
 };
index c1851af..584c0a1 100644 (file)
@@ -1,5 +1,6 @@
 {
     "name" : "OpenERP Web Diagram",
+    "category" : "Hidden",
     "version" : "2.0",
     "depends" : ["base"],
     "js": [
index c443b04..51861c1 100644 (file)
@@ -1,4 +1,4 @@
-import web.common as openerpweb
+import web.common.http as openerpweb
 from web.controllers.main import View
 
 class DiagramView(View):
index 6d857e5..d2b24f9 100644 (file)
@@ -1,5 +1,6 @@
 {
     "name": "web Gantt",
+    "category" : "Hidden",
     "version": "2.0",
     "depends": ['web'],
     "js": [
index a045bae..73e244e 100644 (file)
@@ -509,15 +509,7 @@ init: function(parent, dataset, view_id) {
     do_search: function (domains, contexts, groupbys) {
         var self = this;
         this.grp = groupbys;
-        return this.rpc('/web/session/eval_domain_and_context', {
-            domains: domains,
-            contexts: contexts,
-            group_by_seq: groupbys
-        }, function (results) {
-            self.dataset.context = results.context;
-            self.dataset.domain = results.domain;
-            self.reload_gantt();
-        });
+        self.reload_gantt();
     }
 
 });
index 0383974..68775b7 100644 (file)
@@ -1,5 +1,6 @@
 {
     "name": "web Graph",
+    "category" : "Hidden",
     "version": "2.0",
     "depends": ['web'],
     "js": [
@@ -7,4 +8,4 @@
            "static/src/js/graph.js"],
     "css": ["static/lib/dhtmlxGraph/codebase/dhtmlxchart.css"],
     "active": True
-}
\ No newline at end of file
+}
index 08ceedd..ffdec86 100644 (file)
@@ -29,7 +29,6 @@ openerp.web_graph.GraphView = openerp.web.View.extend({
         this.group_field = null;
     },
     do_show: function () {
-        // TODO: re-trigger search
         this.$element.show();
     },
     do_hide: function () {
@@ -79,9 +78,6 @@ openerp.web_graph.GraphView = openerp.web.View.extend({
             }
         }, this);
         this.ordinate = this.columns[0].name;
-
-        this.dataset.read_slice(
-            this.list_fields(), {}, $.proxy(this, 'schedule_chart'));
     },
     schedule_chart: function(results) {
         var self = this;
@@ -366,24 +362,14 @@ openerp.web_graph.GraphView = openerp.web.View.extend({
         });
     },
 
-    do_search: function(domains, contexts, groupbys) {
-        var self = this;
-        this.rpc('/web/session/eval_domain_and_context', {
-            domains: domains,
-            contexts: contexts,
-            group_by_seq: groupbys
-        }, function (results) {
-            // TODO: handle non-empty results.group_by with read_group?
-            if(!_(results.group_by).isEmpty()){
-                self.abscissa = results.group_by[0];
-            } else {
-                self.abscissa = self.first_field;
-            }
-            self.dataset.read_slice(self.list_fields(), {
-                context: results.context,
-                domain: results.domain
-            }, $.proxy(self, 'schedule_chart'));
-        });
+    do_search: function(domain, context, group_by) {
+        // TODO: handle non-empty group_by with read_group?
+        if (!_(group_by).isEmpty()) {
+            this.abscissa = group_by[0];
+        } else {
+            this.abscissa = this.first_field;
+        }
+        this.dataset.read_slice(this.list_fields(), {}, $.proxy(this, 'schedule_chart'));
     }
 });
 };
index 498f512..5e48d2e 100644 (file)
@@ -1,5 +1,6 @@
 {
     "name": "Hello",
+    "category" : "Hidden",
     "version": "2.0",
     "depends": [],
     "js": ["static/*/*.js", "static/*/js/*.js"],
index 30a6421..f39cd64 100644 (file)
@@ -1,5 +1,6 @@
 {
     "name" : "Base Kanban",
+    "category" : "Hidden",
     "version" : "2.0",
     "depends" : ["web"],
     "js": [
index 4617334..23a0b75 100644 (file)
@@ -9,8 +9,6 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
         this.set_default_options(options);
         this.dataset = dataset;
         this.model = dataset.model;
-        this.domain = dataset.domain;
-        this.context = dataset.context;
         this.view_id = view_id;
         this.fields_view = {};
         this.group_by = [];
@@ -33,9 +31,6 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
         var self = this;
         this.fields_view = data;
         this.add_qweb_template();
-        if (this.qweb.has_template('kanban-box')) {
-            this.do_actual_search();
-        }
     },
     add_qweb_template: function() {
         var group_operator = ["avg", "max", "min", "sum", "count"]
@@ -294,7 +289,7 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
         this.do_execute_action(
             button_attrs, dataset,
             record_id, function () {
-                self.do_actual_search();
+                self.on_reload_record(record_id);
             }
         );
     },
@@ -427,28 +422,18 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
         });
         return new_record;
     },
-    do_search: function (domains, contexts, group_by) {
+    do_search: function (domain, context, group_by) {
         var self = this;
-        this.rpc('/web/session/eval_domain_and_context', {
-            domains: [this.dataset.get_domain()].concat(domains),
-            contexts: [this.dataset.get_context()].concat(contexts),
-            group_by_seq: group_by
-        }, function (results) {
-            self.domain = results.domain;
-            self.context = results.context;
-            self.group_by = results.group_by;
-            self.do_actual_search();
-        });
-    },
-    do_actual_search : function () {
+        self.group_by = group_by;
         var self = this,
             group_by = self.group_by;
         if (!group_by.length && this.fields_view.arch.attrs.default_group_by) {
             group_by = [this.fields_view.arch.attrs.default_group_by];
             self.group_by = group_by;
         }
-        self.datagroup = new openerp.web.DataGroup(self, self.model, self.domain, self.context, group_by);
-        self.datagroup.list(_.keys(self.fields_view.fields),
+        self.datagroup = new openerp.web.DataGroup(self, self.model, domain, context, group_by);
+        self.datagroup.list(
+            _.keys(self.fields_view.fields),
             function (groups) {
                 self.groups = groups;
                 if (groups.length) {
@@ -461,9 +446,12 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
             },
             function (dataset) {
                 self.groups = [];
-                self.dataset.read_slice([], {'domain': self.domain, 'context': self.context}, function(records) {
-                    if (records.length) self.all_display_data = [{'records': records, 'value':false, 'header' : false, 'ids': self.dataset.ids}];
-                    else self.all_display_data = [];
+                self.dataset.read_slice([], {}, function(records) {
+                    if (records.length) {
+                        self.all_display_data = [{'records': records, 'value':false, 'header' : false, 'ids': self.dataset.ids}];
+                    } else {
+                        self.all_display_data = [];
+                    }
                     self.$element.find(".oe_kanban_view").remove();
                     self.on_show_data();
                 });
@@ -487,7 +475,7 @@ openerp.web_kanban.KanbanView = openerp.web.View.extend({
             _.each(self.aggregates, function(value, key) {
                 group_aggregates[value] = group.aggregates[key];
             });
-            self.dataset.read_slice([], {'domain': group.domain, 'context': group.context}, function(records) {
+            self.dataset.read_slice([], {'domain': group.domain, 'conext': group.context}, function(records) {
                 self.all_display_data.push({"value" : group_value, "records": records, 'header':group_name, 'ids': self.dataset.ids, 'aggregates': group_aggregates});
                 if (datagroups.length == self.all_display_data.length) {
                     self.$element.find(".oe_kanban_view").remove();
index 8561c9b..ce219df 100644 (file)
@@ -1,5 +1,6 @@
 {
     "name" : "OpenERP Web mobile",
+    "category" : "Hidden",
     "version" : "2.0",
     "depends" : [],
     'active': True,
index f0cc1db..c725489 100644 (file)
@@ -1,5 +1,6 @@
 {
     "name" : "OpenERP Web web",
+    "category" : "Hidden",
     "version" : "2.0",
     "depends" : [],
     'active': False,
index d622190..4d95281 100644 (file)
@@ -1,5 +1,6 @@
 {
     "name": "Tests",
+    "category" : "Hidden",
     "version": "2.0",
     "depends": [],
     "js": ["static/src/js/*.js"],
index 1a349fa..fe33585 100755 (executable)
@@ -40,7 +40,7 @@ server_options.add_option('--no-serve-static', dest='serve_static',
                           default=True, action='store_false',
                           help="Do not serve static files via this server")
 server_options.add_option('--multi-threaded', dest='threaded',
-                          default=True, action='store_true',
+                          default=False, action='store_true',
                           help="Spawn one thread per HTTP request")
 server_options.add_option('--proxy-mode', dest='proxy_mode',
                           default=False, action='store_true',
@@ -55,7 +55,7 @@ logging_opts.add_option("--log-config", dest="log_config", default=os.path.join(
                         help="Logging configuration file", metavar="FILE")
 optparser.add_option_group(logging_opts)
 
-import web.common.dispatch
+import web.common.http
 
 if __name__ == "__main__":
     (options, args) = optparser.parse_args(sys.argv[1:])
@@ -71,7 +71,7 @@ if __name__ == "__main__":
     else:
         logging.basicConfig(level=getattr(logging, options.log_level.upper()))
 
-    app = web.common.dispatch.Root(options)
+    app = web.common.http.Root(options)
 
     if options.proxy_mode:
         app = werkzeug.contrib.fixers.ProxyFix(app)