[MERGE] Sync with trunk

bzr revid: tde@openerp.com-20130624160023-lhg92flym8svgo0l
This commit is contained in:
Thibault Delavallée 2013-06-24 18:00:23 +02:00
commit d7e5eb67bc
6 changed files with 999 additions and 569 deletions

File diff suppressed because it is too large Load Diff

View File

@ -86,9 +86,8 @@ TESTING = Template(u"""<!DOCTYPE html>
""", default_filters=['h'])
class TestRunnerController(http.Controller):
_cp_path = '/web/tests'
@http.httprequest
@http.route('/web/tests', type='http', auth="none")
def index(self, req, mod=None, **kwargs):
ms = module.get_modules()
manifests = dict(

View File

@ -6,32 +6,49 @@
Welcome to OpenERP Web's documentation!
=======================================
Contents:
Basics
------
.. toctree::
:maxdepth: 1
module
widget
changelog-7.0
Server-Side Web Framework
-------------------------
async
rpc
qweb
client_action
.. toctree::
:maxdepth: 1
web_controllers
Javascript
----------
.. toctree::
:maxdepth: 1
guidelines
widget
rpc
async
qweb
client_action
testing
Views
-----
.. toctree::
:maxdepth: 1
search_view
list_view
form_view
changelog-7.0
Indices and tables
==================
------------------
* :ref:`genindex`
* :ref:`modindex`

View File

@ -0,0 +1,166 @@
Web Controllers
===============
Web controllers are classes in OpenERP able to catch the http requests sent by any browser. They allow to generate
html pages to be served like any web server, implement new methods to be used by the Javascript client, etc...
Controllers File
----------------
By convention the controllers should be placed in the controllers directory of the module. Example:
.. code-block:: text
web_example
├── controllers
│ ├── __init__.py
│ └── my_controllers.py
├── __init__.py
└── __openerp__.py
In ``__init__.py`` you must add:
::
import controllers
And here is the content of ``controllers/__init__.py``:
::
import my_controllers
Now you can put the following content in ``controllers/my_controllers.py``:
::
import openerp.addons.web.http as http
from openerp.addons.web.http import request
Controller Declaration
----------------------
In your controllers file, you can now declare a controller this way:
::
class MyController(http.Controller):
@http.route('/my_url/some_html', type="http")
def some_html(self):
return "<h1>This is a test</h1>"
@http.route('/my_url/some_json', type="json")
def some_json(self):
return {"sample_dictionary": "This is a sample JSON dictionary"}
A controller must inherit from ``http.Controller``. Each time you define a method with ``@http.route()`` it defines a
url to match. As example, the ``some_html()`` method will be called a client query the ``/my_url/some_html`` url.
Pure HTTP Requests
------------------
You can define methods to get any normal http requests by passing ``'http'`` to the ``type`` argument of
``http.route()``. When doing so, you get the HTTP parameters as named parameters of the method:
::
@http.route('/say_hello', type="http")
def say_hello(self, name):
return "<h1>Hello %s</h1>" % name
This url could be contacted by typing this url in a browser: ``http://localhost:8069/say_hello?name=Nicolas``.
JSON Requests
-------------
Methods that received JSON can be defined by passing ``'json'`` to the ``type`` argument of ``http.route()``. The
OpenERP Javascript client can contact these methods using the JSON-RPC protocol. JSON methods must return JSON. Like the
HTTP methods they receive arguments as named parameters (except these arguments are JSON-RPC parameters).
::
@http.route('/division', type="json")
def division(self, i, j):
return i / j # returns a number
URL Patterns
------------
Any URL passed to ``http.route()`` can contain patterns. Example:
::
@http.route('/files/<path:file_path>', type="http")
def files(self, file_path):
... # return a file identified by the path store in the 'my_path' variable
When such patterns are used, the method will received additional parameters that correspond to the parameters defined in
the url. For exact documentation about url patterns, see Werkzeug's documentation:
http://werkzeug.pocoo.org/docs/routing/ .
Also note you can pass multiple urls to ``http.route()``:
::
@http.route(['/files/<path:file_path>', '/other_url/<path:file_path>'], type="http")
def files(self, file_path):
...
Contacting Models
-----------------
To use the database you must access the OpenERP models. The global ``request`` object provides the necessary objects:
::
@http.route('/my_name', type="http")
def my_name(self):
my_user_record = request.registry.get("res.users").browse(request.cr, request.uid, request.uid)
return "<h1>Your name is %s</h1>" % my_user_record.name
``request.registry`` is the registry that gives you access to the models. It is the equivalent of ``self.pool`` when
working inside OpenERP models.
``request.cr`` is the cursor object. This is the ``cr`` parameter you have to pass as first argument of every model
method in OpenERP.
``request.uid`` is the id of the current logged in user. This is the ``uid`` parameter you have to pass as second
argument of every model method in OpenERP.
Authorization Levels
--------------------
By default, all methods can only be used by users logged into OpenERP (OpenERP uses cookies to track logged users).
There are some cases when you need to enable not-logged in users to access some methods. To do so, add the ``'db'``
value to the ``auth`` parameter of ``http.route()``:
::
@http.route('/hello', type="http", auth="db")
def hello(self):
return "<div>Hello unknown user!</div>"
Please note the ``request.uid`` user id will be ``None`` inside this method call. This is due to the fact no user was
authenticated.
Overriding Controllers
----------------------
Existing routes can be overridden. To do so, create a controller that inherit the controller containing the route you
want to override. Example that redefine the home page of your OpenERP application.
::
import openerp.addons.web.controllers.main as main
class Home2(main.Home):
@http.route('/', type="http", auth="db")
def index(self):
return "<div>This is my new home page.</div>"
By re-defining the ``index()`` method, you change the behavior of the original ``Home`` class. Now the ``'/'`` route
will match the new ``index()`` method in ``Home2``.

View File

@ -20,6 +20,7 @@ import traceback
import urlparse
import uuid
import errno
import re
import babel.core
import simplejson
@ -29,10 +30,11 @@ import werkzeug.exceptions
import werkzeug.utils
import werkzeug.wrappers
import werkzeug.wsgi
import werkzeug.routing as routing
import urllib2
import openerp
import session
import openerp.service.security as security
import inspect
import functools
@ -82,21 +84,48 @@ class WebRequest(object):
.. attribute:: debug
``bool``, indicates whether the debug mode is active on the client
.. attribute:: db
``str``, the name of the database linked to the current request. Can be ``None``
if the current request uses the ``none`` authentication.
.. attribute:: uid
``int``, the id of the user related to the current request. Can be ``None``
if the current request uses the ``none`` or the ``db`` authenticatoin.
"""
def __init__(self, request):
self.httprequest = request
def __init__(self, httprequest):
self.httprequest = httprequest
self.httpresponse = None
self.httpsession = request.session
self.httpsession = httprequest.session
self.db = None
self.uid = None
self.func = None
self.auth_method = None
self._cr_cm = None
self._cr = None
self.func_request_type = None
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_id = self.params.pop("session_id", None)
if not self.session_id:
i0 = self.httprequest.cookies.get("instance0|session_id", None)
if i0:
self.session_id = simplejson.loads(urllib2.unquote(i0))
else:
self.session_id = uuid.uuid4().hex
self.session = self.httpsession.get(self.session_id)
if not self.session:
self.session = session.OpenERPSession()
self.session = OpenERPSession()
self.httpsession[self.session_id] = self.session
with set_request(self):
self.db = self.session._db or db_monodb()
# TODO: remove this
# set db/uid trackers - they're cleaned up at the WSGI
# dispatching phase in openerp.service.wsgi_server.application
if self.session._db:
@ -121,12 +150,100 @@ class WebRequest(object):
# we use _ as seprator where RFC2616 uses '-'
self.lang = lang.replace('-', '_')
@contextlib.contextmanager
def registry_cr(self):
dbname = self.session._db or openerp.addons.web.controllers.main.db_monodb(self)
registry = openerp.modules.registry.RegistryManager.get(dbname.lower())
with registry.cursor() as cr:
yield (registry, cr)
def _authenticate(self):
if self.auth_method == "none":
self.db = None
self.uid = None
elif self.auth_method == "db":
self.db = self.session._db or db_monodb()
if not self.db:
raise SessionExpiredException("No valid database for request %s" % self.httprequest)
self.uid = None
else: # auth
try:
self.session.check_security()
except SessionExpiredException, e:
raise SessionExpiredException("Session expired for request %s" % self.httprequest)
self.db = self.session._db
self.uid = self.session._uid
@property
def registry(self):
"""
The registry to the database linked to this request. Can be ``None`` if the current request uses the
``none'' authentication.
"""
return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None
@property
def cr(self):
"""
The cursor initialized for the current method call. If the current request uses the ``none`` authentication
trying to access this property will raise an exception.
"""
# some magic to lazy create the cr
if not self._cr_cm:
self._cr_cm = self.registry.cursor()
self._cr = self._cr_cm.__enter__()
return self._cr
def _call_function(self, *args, **kwargs):
self._authenticate()
try:
# ugly syntax only to get the __exit__ arguments to pass to self._cr
request = self
class with_obj(object):
def __enter__(self):
pass
def __exit__(self, *args):
if request._cr_cm:
request._cr_cm.__exit__(*args)
request._cr_cm = None
request._cr = None
with with_obj():
if self.func_request_type != self._request_type:
raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
% (self.func, self.httprequest.path, self.func_request_type, self._request_type))
return self.func(*args, **kwargs)
finally:
# just to be sure no one tries to re-use the request
self.db = None
self.uid = None
def route(route, type="http", auth="user"):
"""
Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
of ``Controller``.
Decorator to put on a controller method to inform it does not require a user to be logged. When this decorator
is used, ``request.uid`` will be ``None``. The request will still try to detect the database and an exception
will be launched if there is no way to guess it.
:param route: string or array. The route part that will determine which http requests will match the decorated
method. Can be a single string or an array of strings. See werkzeug's routing documentation for the format of
route expression ( http://werkzeug.pocoo.org/docs/routing/ ).
:param type: The type of request, can be ``'http'`` or ``'json'``.
:param auth: The type of authentication method, can on of the following:
* ``auth``: The user must be authenticated.
* ``db``: There is no need for the user to be authenticated but there must be a way to find the current
database.
* ``none``: The method is always active, even if there is no database. Mainly used by the framework and
authentication modules.
"""
assert type in ["http", "json"]
assert auth in ["user", "db", "none"]
def decorator(f):
if isinstance(route, list):
f.routes = route
else:
f.routes = [route]
f.exposed = type
if getattr(f, "auth", None) is None:
f.auth = auth
return f
return decorator
def reject_nonliteral(dct):
if '__ref' in dct:
@ -167,26 +284,30 @@ class JsonRequest(WebRequest):
"id": null}
"""
def dispatch(self, method):
""" Calls the method asked for by the JSON-RPC2 or JSONP request
_request_type = "json"
:param method: the method which received the request
def __init__(self, *args):
super(JsonRequest, self).__init__(*args)
self.jsonp_handler = None
:returns: an utf8 encoded JSON-RPC2 or JSONP reply
"""
args = self.httprequest.args
jsonp = args.get('jsonp')
requestf = None
self.jsonp = jsonp
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
def handler():
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
self.jsonp_handler = handler
return
elif jsonp and args.get('r'):
# jsonp method GET
request = args.get('r')
@ -196,22 +317,28 @@ class JsonRequest(WebRequest):
request = self.session.jsonp_requests.pop(request_id, "")
else:
# regular jsonrpc2
requestf = self.httprequest.stream
request = self.httprequest.stream.read()
# Read POST content or POST Form Data named "request"
self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral)
self.init(self.jsonrequest.get("params", {}))
def dispatch(self):
""" Calls the method asked for by the JSON-RPC2 or JSONP request
:returns: an utf8 encoded JSON-RPC2 or JSONP reply
"""
if self.jsonp_handler:
return self.jsonp_handler()
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=reject_nonliteral)
else:
self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral)
self.init(self.jsonrequest.get("params", {}))
#if _logger.isEnabledFor(logging.DEBUG):
# _logger.debug("--> %s.%s\n%s", method.im_class.__name__, method.__name__, pprint.pformat(self.jsonrequest))
# _logger.debug("--> %s.%s\n%s", func.im_class.__name__, func.__name__, pprint.pformat(self.jsonrequest))
response['id'] = self.jsonrequest.get('id')
response["result"] = method(**self.params)
except session.AuthenticationError, e:
response["result"] = self._call_function(**self.params)
except AuthenticationError, e:
_logger.exception("Exception during JSON request handling.")
se = serialize_exception(e)
error = {
'code': 100,
@ -219,6 +346,7 @@ class JsonRequest(WebRequest):
'data': se
}
except Exception, e:
_logger.exception("Exception during JSON request handling.")
se = serialize_exception(e)
error = {
'code': 200,
@ -231,13 +359,13 @@ class JsonRequest(WebRequest):
if _logger.isEnabledFor(logging.DEBUG):
_logger.debug("<--\n%s", pprint.pformat(response))
if jsonp:
if self.jsonp:
# If we use jsonp, that's mean we are called from another host
# Some browser (IE and Safari) do no allow third party cookies
# We need then to manage http sessions manually.
response['httpsessionid'] = self.httpsession.sid
mime = 'application/javascript'
body = "%s(%s);" % (jsonp, simplejson.dumps(response),)
body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),)
else:
mime = 'application/json'
body = simplejson.dumps(response)
@ -285,26 +413,34 @@ def jsonrequest(f):
the ``session_id``, ``context`` and ``debug`` keys (which are stripped out
beforehand)
"""
f.exposed = 'json'
return f
f.combine = True
base = f.__name__
if f.__name__ == "index":
base = ""
return route([base, os.path.join(base, "<path:_ignored_path>")], type="json", auth="user")(f)
class HttpRequest(WebRequest):
""" Regular GET/POST request
"""
def dispatch(self, method):
_request_type = "http"
def __init__(self, *args):
super(HttpRequest, self).__init__(*args)
params = dict(self.httprequest.args)
params.update(self.httprequest.form)
params.update(self.httprequest.files)
self.init(params)
def dispatch(self):
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, method.im_class.__name__, method.__name__, akw)
#_logger.debug("%s --> %s.%s %r", self.httprequest.func, func.im_class.__name__, func.__name__, akw)
try:
r = method(**self.params)
r = self._call_function(**self.params)
except werkzeug.exceptions.HTTPException, e:
r = e
except Exception, e:
@ -360,8 +496,11 @@ def httprequest(f):
merged in the same dictionary), apart from the ``session_id``, ``context``
and ``debug`` keys (which are stripped out beforehand)
"""
f.exposed = 'http'
return f
f.combine = True
base = f.__name__
if f.__name__ == "index":
base = ""
return route([base, os.path.join(base, "<path:_ignored_path>")], type="http", auth="user")(f)
#----------------------------------------------------------
# Local storage of requests
@ -378,6 +517,9 @@ def set_request(request):
_request_stack.pop()
return with_obj()
"""
A global proxy that always redirect to the current request object.
"""
request = _request_stack()
#----------------------------------------------------------
@ -385,11 +527,7 @@ request = _request_stack()
#----------------------------------------------------------
addons_module = {}
addons_manifest = {}
controllers_class = []
controllers_class_path = {}
controllers_object = {}
controllers_object_path = {}
controllers_path = {}
controllers_per_module = {}
class ControllerType(type):
def __init__(cls, name, bases, attrs):
@ -408,47 +546,268 @@ class ControllerType(type):
# store the controller in the controllers list
name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
controllers_class.append(name_class)
path = attrs.get('_cp_path')
if path not in controllers_class_path:
controllers_class_path[path] = name_class
class_path = name_class[0].split(".")
if not class_path[:2] == ["openerp", "addons"]:
return
# we want to know all modules that have controllers
module = class_path[2]
controllers_per_module.setdefault(module, [])
# but we only store controllers directly inheriting from Controller
if not "Controller" in globals() or not Controller in bases:
return
controllers_per_module.setdefault(module, []).append(name_class)
class Controller(object):
__metaclass__ = ControllerType
def __new__(cls, *args, **kwargs):
subclasses = [c for c in cls.__subclasses__() if c._cp_path == cls._cp_path]
if subclasses:
name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
cls = type(name, tuple(reversed(subclasses)), {})
return object.__new__(cls)
def get_wrapped_method(self, name):
if name in self.__class__._methods_wrapper:
return functools.partial(self.__class__._methods_wrapper[name], self)
else:
return getattr(self, name)
#############################
# OpenERP Sessions #
#############################
class AuthenticationError(Exception):
pass
class SessionExpiredException(Exception):
pass
class Service(object):
"""
.. deprecated:: 8.0
Use ``openerp.netsvc.dispatch_rpc()`` instead.
"""
def __init__(self, session, service_name):
self.session = session
self.service_name = service_name
def __getattr__(self, method):
def proxy_method(*args):
result = openerp.netsvc.dispatch_rpc(self.service_name, method, args)
return result
return proxy_method
class Model(object):
"""
.. deprecated:: 8.0
Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
"""
def __init__(self, session, model):
self.session = session
self.model = model
self.proxy = self.session.proxy('object')
def __getattr__(self, method):
self.session.assert_valid()
def proxy(*args, **kw):
# Can't provide any retro-compatibility for this case, so we check it and raise an Exception
# to tell the programmer to adapt his code
if not request.db or not request.uid or self.session._db != request.db \
or self.session._uid != request.uid:
raise Exception("Trying to use Model with badly configured database or user.")
mod = request.registry.get(self.model)
meth = getattr(mod, method)
cr = request.cr
result = meth(cr, request.uid, *args, **kw)
# reorder read
if method == "read":
if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
index = {}
for r in result:
index[r['id']] = r
result = [index[x] for x in args[0] if x in index]
return result
return proxy
class OpenERPSession(object):
"""
An OpenERP RPC session, a given user can own multiple such sessions
in a web session.
.. attribute:: context
The session context, a ``dict``. Can be reloaded by calling
:meth:`openerpweb.openerpweb.OpenERPSession.get_context`
.. attribute:: domains_store
A ``dict`` matching domain keys to evaluable (but non-literal) domains.
Used to store references to non-literal domains which need to be
round-tripped to the client browser.
"""
def __init__(self):
self._creation_time = time.time()
self._db = False
self._uid = False
self._login = False
self._password = False
self._suicide = False
self.context = {}
self.jsonp_requests = {} # FIXME use a LRU
def authenticate(self, db, login=None, password=None, env=None, uid=None):
"""
Authenticate the current user with the given db, login and password. If successful, store
the authentication parameters in the current session and request.
:param uid: If not None, that user id will be used instead the login to authenticate the user.
"""
if uid is None:
uid = openerp.netsvc.dispatch_rpc('common', 'authenticate', [db, login, password, env])
else:
security.check(db, uid, password)
self._db = db
self._uid = uid
self._login = login
self._password = password
request.db = db
request.uid = uid
if uid: self.get_context()
return uid
def check_security(self):
"""
Chech the current authentication parameters to know if those are still valid. This method
should be called at each request. If the authentication fails, a ``SessionExpiredException``
is raised.
"""
if not self._db or not self._uid:
raise SessionExpiredException("Session expired")
security.check(self._db, self._uid, self._password)
def get_context(self):
"""
Re-initializes the current user's session context (based on
his preferences) by calling res.users.get_context() with the old
context.
:returns: the new context
"""
assert self._uid, "The user needs to be logged-in to initialize his context"
self.context = request.registry.get('res.users').context_get(request.cr, request.uid) or {}
self.context['uid'] = self._uid
self._fix_lang(self.context)
return self.context
def _fix_lang(self, context):
""" OpenERP provides languages which may not make sense and/or may not
be understood by the web client's libraries.
Fix those here.
:param dict context: context to fix
"""
lang = context['lang']
# inane OpenERP locale
if lang == 'ar_AR':
lang = 'ar'
# lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
if lang in babel.core.LOCALE_ALIASES:
lang = babel.core.LOCALE_ALIASES[lang]
context['lang'] = lang or 'en_US'
def send(self, service_name, method, *args):
"""
.. deprecated:: 8.0
Use ``openerp.netsvc.dispatch_rpc()`` instead.
"""
return openerp.netsvc.dispatch_rpc(service_name, method, args)
def proxy(self, service):
"""
.. deprecated:: 8.0
Use ``openerp.netsvc.dispatch_rpc()`` instead.
"""
return Service(self, service)
def assert_valid(self, force=False):
"""
.. deprecated:: 8.0
Use ``check_security()`` instead.
Ensures this session is valid (logged into the openerp server)
"""
if self._uid and not force:
return
# TODO use authenticate instead of login
self._uid = self.proxy("common").login(self._db, self._login, self._password)
if not self._uid:
raise AuthenticationError("Authentication failure")
def ensure_valid(self):
"""
.. deprecated:: 8.0
Use ``check_security()`` instead.
"""
if self._uid:
try:
self.assert_valid(True)
except Exception:
self._uid = None
def execute(self, model, func, *l, **d):
"""
.. deprecated:: 8.0
Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
"""
model = self.model(model)
r = getattr(model, func)(*l, **d)
return r
def exec_workflow(self, model, id, signal):
"""
.. deprecated:: 8.0
Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
"""
self.assert_valid()
r = self.proxy('object').exec_workflow(self._db, self._uid, self._password, model, signal, id)
return r
def model(self, model):
"""
.. deprecated:: 8.0
Use the resistry and cursor in ``openerp.addons.web.http.request`` instead.
Get an RPC proxy for the object ``model``, bound to this session.
:param model: an OpenERP model name
:type model: str
:rtype: a model object
"""
if self._db == False:
raise SessionExpiredException("Session expired")
return Model(self, model)
#----------------------------------------------------------
# Session context manager
#----------------------------------------------------------
@contextlib.contextmanager
def session_context(request, session_store, session_lock, sid):
def session_context(httprequest, session_store, session_lock, sid):
with session_lock:
if sid:
request.session = session_store.get(sid)
httprequest.session = session_store.get(sid)
else:
request.session = session_store.new()
httprequest.session = session_store.new()
try:
yield request.session
yield httprequest.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):
for key, value in httprequest.session.items():
if not isinstance(value, OpenERPSession):
continue
if getattr(value, '_suicide', False) or (
not value._uid
@ -457,7 +816,7 @@ def session_context(request, session_store, session_lock, sid):
and value._creation_time + (60*5) < time.time()):
_logger.debug('remove session %s', key)
removed_sessions.add(key)
del request.session[key]
del httprequest.session[key]
with session_lock:
if sid:
@ -474,9 +833,9 @@ def session_context(request, session_store, session_lock, sid):
# 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():
for k, v in httprequest.session.iteritems():
stored = in_store.get(k)
if stored and isinstance(v, session.OpenERPSession):
if stored and isinstance(v, OpenERPSession):
if hasattr(v, 'contexts_store'):
del v.contexts_store
if hasattr(v, 'domains_store'):
@ -488,10 +847,10 @@ def session_context(request, session_store, session_lock, sid):
# 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
if k not in httprequest.session and k not in removed_sessions:
httprequest.session[k] = v
session_store.save(request.session)
session_store.save(httprequest.session)
def session_gc(session_store):
if random.random() < 0.001:
@ -563,6 +922,9 @@ class Root(object):
self.addons = {}
self.statics = {}
self.db_routers = {}
self.db_routers_lock = threading.Lock()
self.load_addons()
# Setup http sessions
@ -571,6 +933,7 @@ class Root(object):
self.session_lock = threading.Lock()
_logger.debug('HTTP sessions stored in: %s', path)
def __call__(self, environ, start_response):
""" Handle a WSGI request
"""
@ -583,23 +946,33 @@ class Root(object):
Call the object directly.
"""
request = werkzeug.wrappers.Request(environ)
request.parameter_storage_class = werkzeug.datastructures.ImmutableDict
request.app = self
try:
httprequest = werkzeug.wrappers.Request(environ)
httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
httprequest.app = self
handler = self.find_handler(*(request.path.split('/')[1:]))
if not handler:
response = werkzeug.exceptions.NotFound()
else:
sid = request.cookies.get('sid')
sid = httprequest.cookies.get('sid')
if not sid:
sid = request.args.get('sid')
sid = httprequest.args.get('sid')
session_gc(self.session_store)
with session_context(request, self.session_store, self.session_lock, sid) as session:
result = handler(request)
with session_context(httprequest, self.session_store, self.session_lock, sid) as session:
request = self._build_request(httprequest)
db = request.db
if db:
updated = openerp.modules.registry.RegistryManager.check_registry_signaling(db)
if updated:
with self.db_routers_lock:
del self.db_routers[db]
with set_request(request):
self.find_handler()
result = request.dispatch()
if db:
openerp.modules.registry.RegistryManager.signal_caches_change(db)
if isinstance(result, basestring):
headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
@ -610,7 +983,22 @@ class Root(object):
if hasattr(response, 'set_cookie'):
response.set_cookie('sid', session.sid)
return response(environ, start_response)
return response(environ, start_response)
except werkzeug.exceptions.HTTPException, e:
return e(environ, start_response)
def _build_request(self, httprequest):
if httprequest.args.get('jsonp'):
return JsonRequest(httprequest)
content = httprequest.stream.read()
import cStringIO
httprequest.stream = cStringIO.StringIO(content)
try:
simplejson.loads(content)
return JsonRequest(httprequest)
except:
return HttpRequest(httprequest)
def load_addons(self):
""" Load all addons from addons patch containg static files and
@ -633,57 +1021,129 @@ class Root(object):
addons_manifest[module] = manifest
self.statics['/%s/static' % module] = path_static
for k, v in controllers_class_path.items():
if k not in controllers_object_path and hasattr(v[1], '_cp_path'):
o = v[1]()
controllers_object[v[0]] = o
controllers_object_path[k] = o
if hasattr(o, '_cp_path'):
controllers_path[o._cp_path] = o
app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, self.statics)
self.dispatch = DisableCacheMiddleware(app)
def find_handler(self, *l):
def _build_router(self, db):
_logger.info("Generating routing configuration for database %s" % db)
routing_map = routing.Map()
def gen(modules, nodb_only):
for module in modules:
for v in controllers_per_module[module]:
cls = v[1]
subclasses = cls.__subclasses__()
subclasses = [c for c in subclasses if c.__module__.split(".")[:2] == ["openerp", "addons"] and \
cls.__module__.split(".")[2] in modules]
if subclasses:
name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
cls = type(name, tuple(reversed(subclasses)), {})
o = cls()
members = inspect.getmembers(o)
for mk, mv in members:
if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and \
nodb_only == (getattr(mv, "auth", None) == "none"):
function = (o.get_wrapped_method(mk), mv)
for url in mv.routes:
if getattr(mv, "combine", False):
url = os.path.join(o._cp_path, url)
if url.endswith("/") and len(url) > 1:
url = url[: -1]
routing_map.add(routing.Rule(url, endpoint=function))
modules_set = set(controllers_per_module.keys())
modules_set -= set("web")
# building all none methods
gen(["web"] + sorted(modules_set), True)
if not db:
return routing_map
registry = openerp.modules.registry.RegistryManager.get(db)
with registry.cursor() as cr:
m = registry.get('ir.module.module')
ids = m.search(cr, openerp.SUPERUSER_ID, [('state','=','installed')])
installed = set([x['name'] for x in m.read(cr, 1, ids, ['name'])])
modules_set = modules_set.intersection(set(installed))
modules = ["web"] + sorted(modules_set)
# building all other methods
gen(["web"] + sorted(modules_set), False)
return routing_map
def get_db_router(self, db):
with self.db_routers_lock:
router = self.db_routers.get(db)
if not router:
router = self._build_router(db)
with self.db_routers_lock:
self.db_routers[db] = router
return router
def find_handler(self):
"""
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``
:param path: path to match
:returns: a callable matching the path sections
:rtype: ``Controller | None``
"""
if l:
ps = '/' + '/'.join(filter(None, l))
method_name = 'index'
while ps:
c = controllers_path.get(ps)
if c:
method = getattr(c, method_name, None)
if method:
exposed = getattr(method, 'exposed', False)
method = c.get_wrapped_method(method_name)
if exposed == 'json':
_logger.debug("Dispatch json to %s %s %s", ps, c, method_name)
def fct(_request):
_req = JsonRequest(_request)
with set_request(_req):
return request.dispatch(method)
return fct
elif exposed == 'http':
_logger.debug("Dispatch http to %s %s %s", ps, c, method_name)
def fct(_request):
_req = HttpRequest(_request)
with set_request(_req):
return request.dispatch(method)
return fct
if method_name != "index":
method_name = "index"
continue
ps, _slash, method_name = ps.rpartition('/')
if not ps and method_name:
ps = '/'
return None
path = request.httprequest.path
urls = self.get_db_router(request.db).bind("")
matched, arguments = urls.match(path)
arguments = dict([(k, v) for k, v in arguments.items() if not k.startswith("_ignored_")])
func, original = matched
def nfunc(*args, **kwargs):
kwargs.update(arguments)
return func(*args, **kwargs)
request.func = nfunc
request.auth_method = getattr(original, "auth", "user")
request.func_request_type = original.exposed
def db_list(force=False):
proxy = request.session.proxy("db")
dbs = proxy.list(force)
h = request.httprequest.environ['HTTP_HOST'].split(':')[0]
d = h.split('.')[0]
r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
dbs = [i for i in dbs if re.match(r, i)]
return dbs
def db_monodb():
db = None
# 1 try the db in the url
db_url = request.params.get('db')
if db_url:
return db_url
try:
dbs = db_list()
except Exception:
# ignore access denied
dbs = []
# 2 use the database from the cookie if it's listable and still listed
cookie_db = request.httprequest.cookies.get('last_used_database')
if cookie_db in dbs:
db = cookie_db
# 3 use the first db
if dbs and not db:
db = dbs[0]
return db.lower() if db else db
class JsonRpcController(Controller):
@route('/jsonrpc', type='json', auth="none")
def jsonrpc(self, service, method, args):
""" Method used by client APIs to contact OpenERP. """
return openerp.netsvc.dispatch_rpc(service, method, args)
def wsgi_postload():
openerp.wsgi.register_wsgi_handler(Root())

View File

@ -1,178 +0,0 @@
import datetime
import babel
import dateutil.relativedelta
import logging
import time
import traceback
import sys
import openerp
_logger = logging.getLogger(__name__)
#----------------------------------------------------------
# OpenERPSession RPC openerp backend access
#----------------------------------------------------------
class AuthenticationError(Exception):
pass
class SessionExpiredException(Exception):
pass
class Service(object):
def __init__(self, session, service_name):
self.session = session
self.service_name = service_name
def __getattr__(self, method):
def proxy_method(*args):
result = self.session.send(self.service_name, method, *args)
return result
return proxy_method
class Model(object):
def __init__(self, session, model):
self.session = session
self.model = model
self.proxy = self.session.proxy('object')
def __getattr__(self, method):
self.session.assert_valid()
def proxy(*args, **kw):
result = self.proxy.execute_kw(self.session._db, self.session._uid, self.session._password, self.model, method, args, kw)
# reorder read
if method == "read":
if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
index = {}
for r in result:
index[r['id']] = r
result = [index[x] for x in args[0] if x in index]
return result
return proxy
def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None, context=None):
record_ids = self.search(domain or [], offset, limit or False, order or False, context or {})
if not record_ids: return []
records = self.read(record_ids, fields or [], context or {})
return records
class OpenERPSession(object):
"""
An OpenERP RPC session, a given user can own multiple such sessions
in a web session.
.. attribute:: context
The session context, a ``dict``. Can be reloaded by calling
:meth:`openerpweb.openerpweb.OpenERPSession.get_context`
.. attribute:: domains_store
A ``dict`` matching domain keys to evaluable (but non-literal) domains.
Used to store references to non-literal domains which need to be
round-tripped to the client browser.
"""
def __init__(self):
self._creation_time = time.time()
self._db = False
self._uid = False
self._login = False
self._password = False
self._suicide = False
self.context = {}
self.jsonp_requests = {} # FIXME use a LRU
def send(self, service_name, method, *args):
return openerp.netsvc.dispatch_rpc(service_name, method, args)
def proxy(self, service):
return Service(self, service)
def bind(self, db, uid, login, password):
self._db = db
self._uid = uid
self._login = login
self._password = password
def authenticate(self, db, login, password, env=None):
uid = self.proxy('common').authenticate(db, login, password, env)
self.bind(db, uid, login, password)
if uid: self.get_context()
return uid
def assert_valid(self, force=False):
"""
Ensures this session is valid (logged into the openerp server)
"""
if self._uid and not force:
return
# TODO use authenticate instead of login
self._uid = self.proxy("common").login(self._db, self._login, self._password)
if not self._uid:
raise AuthenticationError("Authentication failure")
def ensure_valid(self):
if self._uid:
try:
self.assert_valid(True)
except Exception:
self._uid = None
def execute(self, model, func, *l, **d):
model = self.model(model)
r = getattr(model, func)(*l, **d)
return r
def exec_workflow(self, model, id, signal):
self.assert_valid()
r = self.proxy('object').exec_workflow(self._db, self._uid, self._password, model, signal, id)
return r
def model(self, model):
""" Get an RPC proxy for the object ``model``, bound to this session.
:param model: an OpenERP model name
:type model: str
:rtype: a model object
"""
if self._db == False:
raise SessionExpiredException("Session expired")
return Model(self, model)
def get_context(self):
""" Re-initializes the current user's session context (based on
his preferences) by calling res.users.get_context() with the old
context
:returns: the new context
"""
assert self._uid, "The user needs to be logged-in to initialize his context"
self.context = self.model('res.users').context_get() or {}
self.context['uid'] = self._uid
self._fix_lang(self.context)
return self.context
def _fix_lang(self, context):
""" OpenERP provides languages which may not make sense and/or may not
be understood by the web client's libraries.
Fix those here.
:param dict context: context to fix
"""
lang = context['lang']
# inane OpenERP locale
if lang == 'ar_AR':
lang = 'ar'
# lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
if lang in babel.core.LOCALE_ALIASES:
lang = babel.core.LOCALE_ALIASES[lang]
context['lang'] = lang or 'en_US'
# vim:et:ts=4:sw=4: