summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/web/__init__.py3
-rw-r--r--addons/web/common/__init__.py6
-rw-r--r--addons/web/common/ast.py48
-rw-r--r--addons/web/common/dates.py88
-rw-r--r--addons/web/common/dispatch.py428
-rw-r--r--addons/web/common/http.py431
-rw-r--r--addons/web/common/session.py3
-rw-r--r--addons/web/controllers/main.py81
-rw-r--r--addons/web_chat/controllers/main.py2
-rw-r--r--addons/web_dashboard/controllers.py2
-rw-r--r--addons/web_diagram/controllers/main.py2
-rwxr-xr-xopenerp-web.py4
12 files changed, 460 insertions, 638 deletions
diff --git a/addons/web/__init__.py b/addons/web/__init__.py
index 8802cb548c3..06e7fa1f0e3 100644
--- a/addons/web/__init__.py
+++ b/addons/web/__init__.py
@@ -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)
diff --git a/addons/web/common/__init__.py b/addons/web/common/__init__.py
index 9257f51d037..53bf3e3ed08 100644
--- a/addons/web/common/__init__.py
+++ b/addons/web/common/__init__.py
@@ -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
index 2fd565aa30d..00000000000
--- a/addons/web/common/ast.py
+++ /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
index caa7f83c84a..00000000000
--- a/addons/web/common/dates.py
+++ /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
index cb998caf891..00000000000
--- a/addons/web/common/dispatch.py
+++ /dev/null
@@ -1,428 +0,0 @@
-#!/usr/bin/python
-from __future__ import with_statement
-
-import functools
-import logging
-import urllib
-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'
- 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 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
diff --git a/addons/web/common/http.py b/addons/web/common/http.py
index 0186fd985cc..453484acee7 100644
--- a/addons/web/common/http.py
+++ b/addons/web/common/http.py
@@ -1,13 +1,286 @@
# -*- 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
+
+#
diff --git a/addons/web/common/session.py b/addons/web/common/session.py
index 2ed50bd6131..e8027db8ea4 100644
--- a/addons/web/common/session.py
+++ b/addons/web/common/session.py
@@ -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/controllers/main.py b/addons/web/controllers/main.py
index 30128444fb2..2646cbb4dfc 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -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 web.common.xml2json.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:
@@ -398,8 +355,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 []):
@@ -816,7 +773,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'):
@@ -881,7 +838,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
@@ -891,14 +848,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
@@ -908,9 +865,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,
@@ -998,10 +955,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
@@ -1393,7 +1350,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 = {}
diff --git a/addons/web_chat/controllers/main.py b/addons/web_chat/controllers/main.py
index e9a026759b1..87d64806180 100644
--- a/addons/web_chat/controllers/main.py
+++ b/addons/web_chat/controllers/main.py
@@ -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__)
diff --git a/addons/web_dashboard/controllers.py b/addons/web_dashboard/controllers.py
index 80235492c0f..37fe039366c 100644
--- a/addons/web_dashboard/controllers.py
+++ b/addons/web_dashboard/controllers.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-import web.common as openerpweb
+import web.common.http as openerpweb
WIDGET_CONTENT_PATTERN = """<!DOCTYPE html>
<html>
diff --git a/addons/web_diagram/controllers/main.py b/addons/web_diagram/controllers/main.py
index c443b048ea6..51861c17b5f 100644
--- a/addons/web_diagram/controllers/main.py
+++ b/addons/web_diagram/controllers/main.py
@@ -1,4 +1,4 @@
-import web.common as openerpweb
+import web.common.http as openerpweb
from web.controllers.main import View
class DiagramView(View):
diff --git a/openerp-web.py b/openerp-web.py
index c4e6012fb1f..fe33585a233 100755
--- a/openerp-web.py
+++ b/openerp-web.py
@@ -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)