summaryrefslogtreecommitdiff
path: root/addons/web/common/dispatch.py
diff options
context:
space:
mode:
authorAntony Lesuisse <al@openerp.com>2011-09-05 13:03:09 +0200
committerAntony Lesuisse <al@openerp.com>2011-09-05 13:03:09 +0200
commitbedfc48dcfb066e61209337235e339975dd093d0 (patch)
treefd1bab7b9f21380f1aa3ca7675018f13b4d9e652 /addons/web/common/dispatch.py
parent272424894a91aa136952cf88ef206afab5b5505c (diff)
rename base into web part1
bzr revid: al@openerp.com-20110905110309-egx167t1ni1fzho1
Diffstat (limited to 'addons/web/common/dispatch.py')
-rw-r--r--addons/web/common/dispatch.py426
1 files changed, 426 insertions, 0 deletions
diff --git a/addons/web/common/dispatch.py b/addons/web/common/dispatch.py
new file mode 100644
index 00000000000..e4ed31e9947
--- /dev/null
+++ b/addons/web/common/dispatch.py
@@ -0,0 +1,426 @@
+#!/usr/bin/python
+from __future__ import with_statement
+
+import functools
+import logging
+import os
+import sys
+import traceback
+import uuid
+import xmlrpclib
+
+import simplejson
+import werkzeug.datastructures
+import werkzeug.exceptions
+import werkzeug.urls
+import werkzeug.utils
+import werkzeug.wrappers
+import werkzeug.wsgi
+
+import ast
+import nonliterals
+import http
+# import backendlocal as backend
+import backendrpc as backend
+
+__all__ = ['Root', 'jsonrequest', 'httprequest', 'Controller',
+ 'WebRequest', 'JsonRequest', 'HttpRequest']
+
+#-----------------------------------------------------------
+# Globals (wont move into a pool)
+#-----------------------------------------------------------
+
+applicationsession = {}
+addons_module = {}
+addons_manifest = {}
+controllers_class = {}
+controllers_object = {}
+controllers_path = {}
+
+#----------------------------------------------------------
+# OpenERP Web RequestHandler
+#----------------------------------------------------------
+class WebRequest(object):
+ """ Parent class for all OpenERP Web request types, mostly deals with
+ initialization and setup of the request object (the dispatching itself has
+ to be handled by the subclasses)
+
+ :param request: a wrapped werkzeug Request object
+ :type request: :class:`werkzeug.wrappers.BaseRequest`
+ :param config: configuration object
+
+ .. attribute:: applicationsession
+
+ an application-wide :class:`~collections.Mapping`
+
+ .. attribute:: httprequest
+
+ the original :class:`werkzeug.wrappers.Request` object provided to the
+ request
+
+ .. attribute:: httpsession
+
+ a :class:`~collections.Mapping` holding the HTTP session data for the
+ current http session
+
+ .. attribute:: config
+
+ config parameter provided to the request object
+
+ .. attribute:: params
+
+ :class:`~collections.Mapping` of request parameters, not generally
+ useful as they're provided directly to the handler method as keyword
+ arguments
+
+ .. attribute:: session_id
+
+ opaque identifier for the :class:`backend.OpenERPSession` instance of
+ the current request
+
+ .. attribute:: session
+
+ :class:`~backend.OpenERPSession` instance for the current request
+
+ .. attribute:: context
+
+ :class:`~collections.Mapping` of context values for the current request
+
+ .. attribute:: debug
+
+ ``bool``, indicates whether the debug mode is active on the client
+ """
+ def __init__(self, request, config):
+ self.applicationsession = applicationsession
+ self.httprequest = request
+ self.httpresponse = None
+ self.httpsession = request.session
+ self.config = config
+ def init(self, params):
+ self.params = dict(params)
+ # OpenERP session setup
+ self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex
+ self.session = self.httpsession.setdefault(
+ self.session_id, backend.OpenERPSession(
+ self.config.server_host, self.config.server_port))
+ self.context = self.params.pop('context', None)
+ self.debug = self.params.pop('debug', False) != False
+
+class JsonRequest(WebRequest):
+ """ JSON-RPC2 over HTTP.
+
+ Sucessful request::
+
+ --> {"jsonrpc": "2.0",
+ "method": "call",
+ "params": {"session_id": "SID",
+ "context": {},
+ "arg1": "val1" },
+ "id": null}
+
+ <-- {"jsonrpc": "2.0",
+ "result": { "res1": "val1" },
+ "id": null}
+
+ Request producing a error::
+
+ --> {"jsonrpc": "2.0",
+ "method": "call",
+ "params": {"session_id": "SID",
+ "context": {},
+ "arg1": "val1" },
+ "id": null}
+
+ <-- {"jsonrpc": "2.0",
+ "error": {"code": 1,
+ "message": "End user error message.",
+ "data": {"code": "codestring",
+ "debug": "traceback" } },
+ "id": null}
+
+ """
+
+ def dispatch(self, controller, method, requestf=None, request=None):
+ """ Calls the method asked for by the JSON-RPC2 request
+
+ :param controller: the instance of the controller which received the request
+ :param method: the method which received the request
+ :param requestf: a file-like object containing an encoded JSON-RPC2 request
+ :param request: a JSON-RPC2 request
+
+ :returns: an utf8 encoded JSON-RPC2 reply
+ """
+ response = {"jsonrpc": "2.0" }
+ error = None
+ try:
+ # Read POST content or POST Form Data named "request"
+ if requestf:
+ self.jsonrequest = simplejson.load(requestf, object_hook=nonliterals.non_literal_decoder)
+ else:
+ self.jsonrequest = simplejson.loads(request, object_hook=nonliterals.non_literal_decoder)
+ self.init(self.jsonrequest.get("params", {}))
+ if self.debug or 1:
+ print "--> %s.%s %s" % (controller.__class__.__name__, method.__name__, self.jsonrequest)
+ response['id'] = self.jsonrequest.get('id')
+ response["result"] = method(controller, self, **self.params)
+ except backend.OpenERPUnboundException:
+ error = {
+ 'code': 100,
+ 'message': "OpenERP Session Invalid",
+ 'data': {
+ 'type': 'session_invalid',
+ 'debug': traceback.format_exc()
+ }
+ }
+ except xmlrpclib.Fault, e:
+ error = {
+ 'code': 200,
+ 'message': "OpenERP Server Error",
+ 'data': {
+ 'type': 'server_exception',
+ 'fault_code': e.faultCode,
+ 'debug': "Client %s\nServer %s" % (
+ "".join(traceback.format_exception("", None, sys.exc_traceback)), e.faultString)
+ }
+ }
+ except Exception:
+ logging.getLogger('openerp.JSONRequest.dispatch').exception\
+ ("An error occured while handling a json request")
+ error = {
+ 'code': 300,
+ 'message': "OpenERP WebClient Error",
+ 'data': {
+ 'type': 'client_exception',
+ 'debug': "Client %s" % traceback.format_exc()
+ }
+ }
+ if error:
+ response["error"] = error
+
+ if self.debug or 1:
+ print "<--", response
+ print
+
+ content = simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder)
+ return werkzeug.wrappers.Response(
+ content, headers=[('Content-Type', 'application/json'),
+ ('Content-Length', len(content))])
+
+def jsonrequest(f):
+ """ Decorator marking the decorated method as being a handler for a
+ JSON-RPC request (the exact request path is specified via the
+ ``$(Controller._cp_path)/$methodname`` combination.
+
+ If the method is called, it will be provided with a :class:`JsonRequest`
+ instance and all ``params`` sent during the JSON-RPC request, apart from
+ the ``session_id``, ``context`` and ``debug`` keys (which are stripped out
+ beforehand)
+ """
+ @functools.wraps(f)
+ def json_handler(controller, request, config):
+ return JsonRequest(request, config).dispatch(
+ controller, f, requestf=request.stream)
+ json_handler.exposed = True
+ return json_handler
+
+class HttpRequest(WebRequest):
+ """ Regular GET/POST request
+ """
+ def dispatch(self, controller, method):
+ self.init(dict(self.httprequest.args, **self.httprequest.form))
+ akw = {}
+ for key, value in self.httprequest.args.iteritems():
+ if isinstance(value, basestring) and len(value) < 1024:
+ akw[key] = value
+ else:
+ akw[key] = type(value)
+ if self.debug or 1:
+ print "%s --> %s.%s %r" % (self.httprequest.method, controller.__class__.__name__, method.__name__, akw)
+ r = method(controller, self, **self.params)
+ if self.debug or 1:
+ if isinstance(r, werkzeug.wrappers.BaseResponse):
+ print '<--', r
+ else:
+ print "<--", 'size:', len(r)
+ print
+ return r
+
+ def make_response(self, data, headers=None, cookies=None):
+ """ Helper for non-HTML responses, or HTML responses with custom
+ response headers or cookies.
+
+ While handlers can just return the HTML markup of a page they want to
+ send as a string if non-HTML data is returned they need to create a
+ complete response object, or the returned data will not be correctly
+ interpreted by the clients.
+
+ :param basestring data: response body
+ :param headers: HTTP headers to set on the response
+ :type headers: ``[(name, value)]``
+ :param collections.Mapping cookies: cookies to set on the client
+ """
+ response = werkzeug.wrappers.Response(data, headers=headers)
+ if cookies:
+ for k, v in cookies.iteritems():
+ response.set_cookie(k, v)
+ return response
+
+ def not_found(self, description=None):
+ """ Helper for 404 response, return its result from the method
+ """
+ return werkzeug.exceptions.NotFound(description)
+
+def httprequest(f):
+ """ Decorator marking the decorated method as being a handler for a
+ normal HTTP request (the exact request path is specified via the
+ ``$(Controller._cp_path)/$methodname`` combination.
+
+ If the method is called, it will be provided with a :class:`HttpRequest`
+ instance and all ``params`` sent during the request (``GET`` and ``POST``
+ merged in the same dictionary), apart from the ``session_id``, ``context``
+ and ``debug`` keys (which are stripped out beforehand)
+ """
+ @functools.wraps(f)
+ def http_handler(controller, request, config):
+ return HttpRequest(request, config).dispatch(controller, f)
+ http_handler.exposed = True
+ return http_handler
+
+class ControllerType(type):
+ def __init__(cls, name, bases, attrs):
+ super(ControllerType, cls).__init__(name, bases, attrs)
+ controllers_class["%s.%s" % (cls.__module__, cls.__name__)] = cls
+
+class Controller(object):
+ __metaclass__ = ControllerType
+
+class Root(object):
+ """Root WSGI application for the OpenERP Web Client.
+
+ :param options: mandatory initialization options object, must provide
+ the following attributes:
+
+ ``server_host`` (``str``)
+ hostname of the OpenERP server to dispatch RPC to
+ ``server_port`` (``int``)
+ RPC port of the OpenERP server
+ ``serve_static`` (``bool | None``)
+ whether this application should serve the various
+ addons's static files
+ ``storage_path`` (``str``)
+ filesystem path where HTTP session data will be stored
+ ``dbfilter`` (``str``)
+ only used in case the list of databases is requested
+ by the server, will be filtered by this pattern
+ """
+ def __init__(self, options):
+ self.root = werkzeug.urls.Href('/web/webclient/home')
+ self.config = options
+
+ self.session_cookie = 'sessionid'
+ self.addons = {}
+
+ static_dirs = self._load_addons()
+ if options.serve_static:
+ self.dispatch = werkzeug.wsgi.SharedDataMiddleware(
+ self.dispatch, static_dirs)
+
+ if options.session_storage:
+ if not os.path.exists(options.session_storage):
+ os.mkdir(options.session_storage, 0700)
+ self.session_storage = options.session_storage
+
+ def __call__(self, environ, start_response):
+ """ Handle a WSGI request
+ """
+ return self.dispatch(environ, start_response)
+
+ def dispatch(self, environ, start_response):
+ """
+ Performs the actual WSGI dispatching for the application, may be
+ wrapped during the initialization of the object.
+
+ Call the object directly.
+ """
+ request = werkzeug.wrappers.Request(environ)
+ request.parameter_storage_class = werkzeug.datastructures.ImmutableDict
+
+ if request.path == '/':
+ return werkzeug.utils.redirect(
+ self.root(request.args), 301)(
+ environ, start_response)
+ elif request.path == '/mobile':
+ return werkzeug.utils.redirect(
+ '/web_mobile/static/src/web_mobile.html', 301)(
+ environ, start_response)
+
+ handler = self.find_handler(*(request.path.split('/')[1:]))
+
+ if not handler:
+ response = werkzeug.exceptions.NotFound()
+ else:
+ with http.session(request, self.session_storage, self.session_cookie) as session:
+ result = handler(
+ request, self.config)
+
+ if isinstance(result, basestring):
+ response = werkzeug.wrappers.Response(
+ result, headers=[('Content-Type', 'text/html; charset=utf-8'),
+ ('Content-Length', len(result))])
+ else:
+ response = result
+
+ response.set_cookie(self.session_cookie, session.sid)
+
+ return response(environ, start_response)
+
+ def _load_addons(self):
+ """
+ Loads all addons at the specified addons path, returns a mapping of
+ static URLs to the corresponding directories
+ """
+ statics = {}
+ addons_path = self.config.addons_path
+ if addons_path not in sys.path:
+ sys.path.insert(0, addons_path)
+ for module in os.listdir(addons_path):
+ if module not in addons_module:
+ manifest_path = os.path.join(addons_path, module, '__openerp__.py')
+ if os.path.isfile(manifest_path):
+ manifest = ast.literal_eval(open(manifest_path).read())
+ print "Loading", module
+ m = __import__(module)
+ addons_module[module] = m
+ addons_manifest[module] = manifest
+
+ statics['/%s/static' % module] = \
+ os.path.join(addons_path, module, 'static')
+ for k, v in controllers_class.items():
+ if k not in controllers_object:
+ o = v()
+ controllers_object[k] = o
+ if hasattr(o, '_cp_path'):
+ controllers_path[o._cp_path] = o
+ return statics
+
+ def find_handler(self, *l):
+ """
+ Tries to discover the controller handling the request for the path
+ specified by the provided parameters
+
+ :param l: path sections to a controller or controller method
+ :returns: a callable matching the path sections, or ``None``
+ :rtype: ``Controller | None``
+ """
+ if len(l) > 1:
+ for i in range(len(l), 1, -1):
+ ps = "/" + "/".join(l[0:i])
+ if ps in controllers_path:
+ c = controllers_path[ps]
+ rest = l[i:] or ['index']
+ meth = rest[0]
+ m = getattr(c, meth)
+ if getattr(m, 'exposed', False):
+ print "Dispatching to", ps, c, meth, m
+ return m
+ return None