merge upstream
authorChristophe Simonis <chs@openerp.com>
Thu, 11 Oct 2012 09:32:52 +0000 (11:32 +0200)
committerChristophe Simonis <chs@openerp.com>
Thu, 11 Oct 2012 09:32:52 +0000 (11:32 +0200)
bzr revid: chs@openerp.com-20121011093252-ke83ln88kw9em5y6

1  2 
addons/web/__init__.py
addons/web/controllers/main.py
addons/web/doc/conf.py
addons/web/http.py
addons/web/static/src/js/chrome.js

@@@ -1,32 -1,4 +1,5 @@@
- import logging
- from . import common
- from . import controllers
+ import http
+ import controllers
 +from . import ir_module
  
- _logger = logging.getLogger(__name__)
- class Options(object):
-     pass
- def wsgi_postload():
-     import openerp
-     import os
-     import tempfile
-     import getpass
-     _logger.info("embedded mode")
-     o = Options()
-     o.dbfilter = openerp.tools.config['dbfilter']
-     o.server_wide_modules = openerp.conf.server_wide_modules or ['web']
-     try:
-         username = getpass.getuser()
-     except Exception:
-         username = "unknown"
-     o.session_storage = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
-     o.addons_path = openerp.modules.module.ad_paths
-     o.serve_static = True
-     o.backend = 'local'
-     app = common.http.Root(o)
-     openerp.wsgi.register_wsgi_handler(app)
+ wsgi_postload = http.wsgi_postload
Simple merge
index 0000000,ac5b890..83fc969
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,257 +1,257 @@@
+ # -*- coding: utf-8 -*-
+ #
+ # OpenERP Technical Documentation configuration file, created by
+ # sphinx-quickstart on Fri Feb 17 16:14:06 2012.
+ #
+ # This file is execfile()d with the current directory set to its containing dir.
+ #
+ # Note that not all possible configuration values are present in this
+ # autogenerated file.
+ #
+ # All configuration values have a default; values that are commented out
+ # serve to show the default.
+ import sys, os
+ # If extensions (or modules to document with autodoc) are in another directory,
+ # add these directories to sys.path here. If the directory is relative to the
+ # documentation root, use os.path.abspath to make it absolute, like shown here.
 -#sys.path.insert(0, os.path.abspath('.'))
+ sys.path.append(os.path.abspath('_themes'))
 -sys.path.append(os.path.abspath('..'))
 -sys.path.append(os.path.abspath('../openerp'))
++sys.path.insert(0, os.path.abspath('../addons'))
++sys.path.insert(0, os.path.abspath('..'))
+ # -- General configuration -----------------------------------------------------
+ # If your documentation needs a minimal Sphinx version, state it here.
+ #needs_sphinx = '1.0'
+ # Add any Sphinx extension module names here, as strings. They can be extensions
+ # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.viewcode']
+ # Add any paths that contain templates here, relative to this directory.
+ templates_path = ['_templates']
+ # The suffix of source filenames.
+ source_suffix = '.rst'
+ # The encoding of source files.
+ #source_encoding = 'utf-8-sig'
+ # The master toctree document.
+ master_doc = 'index'
+ # General information about the project.
 -project = u'OpenERP Server Developers Documentation'
++project = u'OpenERP Web Developers Documentation'
+ copyright = u'2012, OpenERP s.a.'
+ # The version info for the project you're documenting, acts as replacement for
+ # |version| and |release|, also used in various other places throughout the
+ # built documents.
+ #
+ # The short X.Y version.
+ version = '7.0'
+ # The full version, including alpha/beta/rc tags.
 -release = '7.0b'
++release = '7.0'
+ # The language for content autogenerated by Sphinx. Refer to documentation
+ # for a list of supported languages.
+ #language = None
+ # There are two options for replacing |today|: either, you set today to some
+ # non-false value, then it is used:
+ #today = ''
+ # Else, today_fmt is used as the format for a strftime call.
+ #today_fmt = '%B %d, %Y'
+ # List of patterns, relative to source directory, that match files and
+ # directories to ignore when looking for source files.
+ exclude_patterns = ['_build']
+ # The reST default role (used for this markup: `text`) to use for all documents.
+ #default_role = None
+ # If true, '()' will be appended to :func: etc. cross-reference text.
+ #add_function_parentheses = True
+ # If true, the current module name will be prepended to all description
+ # unit titles (such as .. function::).
+ #add_module_names = True
+ # If true, sectionauthor and moduleauthor directives will be shown in the
+ # output. They are ignored by default.
+ #show_authors = False
+ # The name of the Pygments (syntax highlighting) style to use.
+ pygments_style = 'sphinx'
+ # A list of ignored prefixes for module index sorting.
+ #modindex_common_prefix = []
+ # -- Options for HTML output ---------------------------------------------------
+ # The theme to use for HTML and HTML Help pages.  See the documentation for
+ # a list of builtin themes.
+ html_theme = 'flask'
+ # Theme options are theme-specific and customize the look and feel of a theme
+ # further.  For a list of options available for each theme, see the
+ # documentation.
+ #html_theme_options = {}
+ # Add any paths that contain custom themes here, relative to this directory.
+ html_theme_path = ['_themes']
+ # The name for this set of Sphinx documents.  If None, it defaults to
+ # "<project> v<release> documentation".
+ #html_title = None
+ # A shorter title for the navigation bar.  Default is the same as html_title.
+ #html_short_title = None
+ # The name of an image file (relative to this directory) to place at the top
+ # of the sidebar.
+ #html_logo = None
+ # The name of an image file (within the static path) to use as favicon of the
+ # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+ # pixels large.
+ #html_favicon = None
+ # Add any paths that contain custom static files (such as style sheets) here,
+ # relative to this directory. They are copied after the builtin static files,
+ # so a file named "default.css" will overwrite the builtin "default.css".
+ html_static_path = ['_static']
+ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+ # using the given strftime format.
+ #html_last_updated_fmt = '%b %d, %Y'
+ # If true, SmartyPants will be used to convert quotes and dashes to
+ # typographically correct entities.
+ #html_use_smartypants = True
+ # Custom sidebar templates, maps document names to template names.
+ html_sidebars = {
+     'index':    ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'],
+     '**':       ['sidebarlogo.html', 'localtoc.html', 'relations.html',
+                  'sourcelink.html', 'searchbox.html']
+ }
+ # Additional templates that should be rendered to pages, maps page names to
+ # template names.
+ #html_additional_pages = {}
+ # If false, no module index is generated.
+ #html_domain_indices = True
+ # If false, no index is generated.
+ #html_use_index = True
+ # If true, the index is split into individual pages for each letter.
+ #html_split_index = False
+ # If true, links to the reST sources are added to the pages.
+ #html_show_sourcelink = True
+ # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+ #html_show_sphinx = True
+ # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+ #html_show_copyright = True
+ # If true, an OpenSearch description file will be output, and all pages will
+ # contain a <link> tag referring to it.  The value of this option must be the
+ # base URL from which the finished HTML is served.
+ #html_use_opensearch = ''
+ # This is the file name suffix for HTML files (e.g. ".xhtml").
+ #html_file_suffix = None
+ # Output file base name for HTML help builder.
 -htmlhelp_basename = 'openerp-server-doc'
++htmlhelp_basename = 'openerp-web-doc'
+ # -- Options for LaTeX output --------------------------------------------------
+ latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ #'papersize': 'letterpaper',
+ # The font size ('10pt', '11pt' or '12pt').
+ #'pointsize': '10pt',
+ # Additional stuff for the LaTeX preamble.
+ #'preamble': '',
+ }
+ # Grouping the document tree into LaTeX files. List of tuples
+ # (source start file, target name, title, author, documentclass [howto/manual]).
+ latex_documents = [
 -  ('index', 'openerp-server-doc.tex', u'OpenERP Server Developers Documentation',
++  ('index', 'openerp-web-doc.tex', u'OpenERP Web Developers Documentation',
+    u'OpenERP s.a.', 'manual'),
+ ]
+ # The name of an image file (relative to this directory) to place at the top of
+ # the title page.
+ #latex_logo = None
+ # For "manual" documents, if this is true, then toplevel headings are parts,
+ # not chapters.
+ #latex_use_parts = False
+ # If true, show page references after internal links.
+ #latex_show_pagerefs = False
+ # If true, show URL addresses after external links.
+ #latex_show_urls = False
+ # Documents to append as an appendix to all manuals.
+ #latex_appendices = []
+ # If false, no module index is generated.
+ #latex_domain_indices = True
+ # -- Options for manual page output --------------------------------------------
+ # One entry per manual page. List of tuples
+ # (source start file, name, description, authors, manual section).
+ man_pages = [
 -    ('index', 'openerp-server-doc', u'OpenERP Server Developers Documentation',
++    ('index', 'openerp-web-doc', u'OpenERP Web Developers Documentation',
+      [u'OpenERP s.a.'], 1)
+ ]
+ # If true, show URL addresses after external links.
+ #man_show_urls = False
+ # -- Options for Texinfo output ------------------------------------------------
+ # Grouping the document tree into Texinfo files. List of tuples
+ # (source start file, target name, title, author,
+ #  dir menu entry, description, category)
+ texinfo_documents = [
 -  ('index', 'OpenERPServerDocumentation', u'OpenERP Server Developers Documentation',
 -   u'OpenERP s.a.', 'OpenERPServerDocumentation', 'Developers documentation for the openobject-server project.',
++  ('index', 'OpenERPWebDocumentation', u'OpenERP Web Developers Documentation',
++   u'OpenERP s.a.', 'OpenERPWebDocumentation', 'Developers documentation for the openerp-web project.',
+    'Miscellaneous'),
+ ]
+ # Documents to append as an appendix to all manuals.
+ #texinfo_appendices = []
+ # If false, no module index is generated.
+ #texinfo_domain_indices = True
+ # How to display URL addresses: 'footnote', 'no', or 'inline'.
+ #texinfo_show_urls = 'footnote'
++todo_include_todos = True
+ # Example configuration for intersphinx: refer to the Python standard library.
+ intersphinx_mapping = {
+     'python': ('http://docs.python.org/', None),
 -    'openerpweb': ('http://doc.openerp.com/trunk/developers/web', None),
++    'openerpserver': ('http://doc.openerp.com/trunk/developers/server', None),
+     'openerpdev': ('http://doc.openerp.com/trunk/developers', None),
+ }
index 0000000,6563076..13f9977
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,557 +1,562 @@@
+ # -*- coding: utf-8 -*-
+ #----------------------------------------------------------
+ # OpenERP Web HTTP layer
+ #----------------------------------------------------------
+ import ast
+ import cgi
+ import contextlib
+ import functools
+ import getpass
+ import logging
+ import mimetypes
+ import os
+ import pprint
+ import sys
+ import tempfile
+ import threading
+ import time
+ import traceback
+ import urllib
+ import urlparse
+ 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 openerp
+ import nonliterals
+ import session
+ _logger = logging.getLogger(__name__)
+ #----------------------------------------------------------
+ # 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`
+     .. 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:: 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):
+         self.httprequest = request
+         self.httpresponse = None
+         self.httpsession = request.session
+     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.get(self.session_id)
+         if not self.session:
+             self.httpsession[self.session_id] = self.session = session.OpenERPSession()
+         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):
+         """ Calls the method asked for by the JSON-RPC2 or JSONP request
+         :param controller: the instance of the controller which received the request
+         :param method: the method which received the request
+         :returns: an utf8 encoded JSON-RPC2 or JSONP reply
+         """
+         args = self.httprequest.args
+         jsonp = args.get('jsonp')
+         requestf = None
+         request = None
+         request_id = args.get('id')
+         if jsonp and self.httprequest.method == 'POST':
+             # jsonp 2 steps step1 POST: save call
+             self.init(args)
+             self.session.jsonp_requests[request_id] = self.httprequest.form['r']
+             headers=[('Content-Type', 'text/plain; charset=utf-8')]
+             r = werkzeug.wrappers.Response(request_id, headers=headers)
+             return r
+         elif jsonp and args.get('r'):
+             # jsonp method GET
+             request = args.get('r')
+         elif jsonp and request_id:
+             # jsonp 2 steps step2 GET: run and return result
+             self.init(args)
+             request = self.session.jsonp_requests.pop(request_id, "")
+         else:
+             # regular jsonrpc2
+             requestf = self.httprequest.stream
+         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 session.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))
+         if jsonp:
+             mime = 'application/javascript'
+             body = "%s(%s);" % (jsonp, simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder),)
+         else:
+             mime = 'application/json'
+             body = simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder)
+         r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))])
+         return r
+ 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):
+         return JsonRequest(request).dispatch(controller, f)
+     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)
+         try:
+             r = method(controller, self, **self.params)
+         except xmlrpclib.Fault, e:
+             r = werkzeug.exceptions.InternalServerError(cgi.escape(simplejson.dumps({
+                 'code': 200,
+                 'message': "OpenERP Server Error",
+                 'data': {
+                     'type': 'server_exception',
+                     'fault_code': e.faultCode,
+                     'debug': "Server %s\nClient %s" % (
+                         e.faultString, traceback.format_exc())
+                 }
+             })))
+         except Exception:
+             logging.getLogger(__name__ + '.HttpRequest.dispatch').exception(
+                     "An error occurred while handling a json request")
+             r = werkzeug.exceptions.InternalServerError(cgi.escape(simplejson.dumps({
+                 'code': 300,
+                 'message': "OpenERP WebClient Error",
+                 'data': {
+                     'type': 'client_exception',
+                     'debug': "Client %s" % traceback.format_exc()
+                 }
+             })))
+         if self.debug or 1:
+             if isinstance(r, (werkzeug.wrappers.BaseResponse, werkzeug.exceptions.HTTPException)):
+                 _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):
+         return HttpRequest(request).dispatch(controller, f)
+     http_handler.exposed = True
+     return http_handler
+ #----------------------------------------------------------
+ # Controller registration with a metaclass
+ #----------------------------------------------------------
+ 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.append(("%s.%s" % (cls.__module__, cls.__name__), cls))
+ class Controller(object):
+     __metaclass__ = ControllerType
+ #----------------------------------------------------------
+ # Session context manager
+ #----------------------------------------------------------
+ STORES = {}
+ @contextlib.contextmanager
+ def session_context(request, storage_path, session_cookie='httpsessionid'):
+     session_store, session_lock = STORES.get(storage_path, (None, None))
+     if not session_store:
+         session_store = werkzeug.contrib.sessions.FilesystemSessionStore( storage_path)
+         session_lock = threading.Lock()
+         STORES[storage_path] = session_store, session_lock
+     sid = request.cookies.get(session_cookie)
+     with session_lock:
+         if sid:
+             request.session = session_store.get(sid)
+         else:
+             request.session = session_store.new()
+     try:
+         yield request.session
+     finally:
+         # Remove all OpenERPSession instances with no uid, they're generated
+         # either by login process or by HTTP requests without an OpenERP
+         # session id, and are generally noise
+         removed_sessions = set()
+         for key, value in request.session.items():
+             if not isinstance(value, session.OpenERPSession):
+                 continue
+             if getattr(value, '_suicide', False) or (
+                         not value._uid
+                     and not value.jsonp_requests
+                     # FIXME do not use a fixed value
+                     and value._creation_time + (60*5) < time.time()):
+                 _logger.debug('remove session %s', key)
+                 removed_sessions.add(key)
+                 del request.session[key]
+         with session_lock:
+             if sid:
+                 # Re-load sessions from storage and merge non-literal
+                 # contexts and domains (they're indexed by hash of the
+                 # content so conflicts should auto-resolve), otherwise if
+                 # two requests alter those concurrently the last to finish
+                 # will overwrite the previous one, leading to loss of data
+                 # (a non-literal is lost even though it was sent to the
+                 # client and client errors)
+                 #
+                 # note that domains_store and contexts_store are append-only (we
+                 # only ever add items to them), so we can just update one with the
+                 # other to get the right result, if we want to merge the
+                 # ``context`` dict we'll need something smarter
+                 in_store = session_store.get(sid)
+                 for k, v in request.session.iteritems():
+                     stored = in_store.get(k)
+                     if stored and isinstance(v, session.OpenERPSession):
+                         v.contexts_store.update(stored.contexts_store)
+                         v.domains_store.update(stored.domains_store)
+                         if not hasattr(v, 'jsonp_requests'):
+                             v.jsonp_requests = {}
+                         v.jsonp_requests.update(getattr(
+                             stored, 'jsonp_requests', {}))
+                 # add missing keys
+                 for k, v in in_store.iteritems():
+                     if k not in request.session and k not in removed_sessions:
+                         request.session[k] = v
+             session_store.save(request.session)
+ #----------------------------------------------------------
+ # WSGI Application
+ #----------------------------------------------------------
+ # Add potentially missing (older ubuntu) font mime types
+ mimetypes.add_type('application/font-woff', '.woff')
+ mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
+ mimetypes.add_type('application/x-font-ttf', '.ttf')
+ class DisableCacheMiddleware(object):
+     def __init__(self, app):
+         self.app = app
+     def __call__(self, environ, start_response):
+         def start_wrapped(status, headers):
+             referer = environ.get('HTTP_REFERER', '')
+             parsed = urlparse.urlparse(referer)
+             debug = parsed.query.count('debug') >= 1
+             new_headers = []
+             unwanted_keys = ['Last-Modified']
+             if debug:
+                 new_headers = [('Cache-Control', 'no-cache')]
+                 unwanted_keys += ['Expires', 'Etag', 'Cache-Control']
+             for k, v in headers:
+                 if k not in unwanted_keys:
+                     new_headers.append((k, v))
+             start_response(status, new_headers)
+         return self.app(environ, start_wrapped)
+ class Root(object):
+     """Root WSGI application for the OpenERP Web Client.
+     """
+     def __init__(self):
+         self.httpsession_cookie = 'httpsessionid'
+         self.addons = {}
++        self.statics = {}
 -        static_dirs = self._load_addons()
 -        app = werkzeug.wsgi.SharedDataMiddleware( self.dispatch, static_dirs)
 -        self.dispatch = DisableCacheMiddleware(app)
++        self._load_addons()
+         try:
+             username = getpass.getuser()
+         except Exception:
+             username = "unknown"
+         self.session_storage = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
+         if not os.path.exists(self.session_storage):
+             os.mkdir(self.session_storage, 0700)
+         _logger.debug('HTTP sessions stored in: %s', self.session_storage)
+     def __call__(self, environ, start_response):
+         """ Handle a WSGI request
+         """
+         return self.dispatch(environ, start_response)
 -    def dispatch(self, 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
+         request.app = self
+         handler = self.find_handler(*(request.path.split('/')[1:]))
+         if not handler:
+             response = werkzeug.exceptions.NotFound()
+         else:
+             with session_context(request, self.session_storage, self.httpsession_cookie) as session:
+                 result = handler( request)
+                 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
+                 if hasattr(response, 'set_cookie'):
+                     response.set_cookie(self.httpsession_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 openerp.modules.module.ad_paths:
+             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.debug("Loading %s", module)
+                         if 'openerp.addons' in sys.modules:
+                             m = __import__('openerp.addons.' + module)
+                         else:
+                             m = __import__(module)
+                         addons_module[module] = m
+                         addons_manifest[module] = manifest
 -                        statics['/%s/static' % module] = path_static
++                        self.statics['/%s/static' % module] = path_static
++
+         for k, v in controllers_class:
+             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
++
++        app = self._dispatch
++        if self.config.serve_static:
++            app = werkzeug.wsgi.SharedDataMiddleware(self._dispatch, self.statics)
++
++        self.dispatch = DisableCacheMiddleware(app)
+     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 l:
+             ps = '/' + '/'.join(l)
+             meth = 'index'
+             while ps:
+                 c = controllers_path.get(ps)
+                 if c:
+                     m = getattr(c, meth, None)
+                     if m and getattr(m, 'exposed', False):
+                         _logger.debug("Dispatching to %s %s %s", ps, c, meth)
+                         return m
+                 ps, _slash, meth = ps.rpartition('/')
+                 if not ps and meth:
+                     ps = '/'
+         return None
+ def wsgi_postload():
+     openerp.wsgi.register_wsgi_handler(Root())
+ # vim:et:ts=4:sw=4:
Simple merge