3 import dateutil.relativedelta
16 import cherrypy.lib.static
23 #-----------------------------------------------------------
25 #-----------------------------------------------------------
27 path_root = os.path.dirname(os.path.dirname(os.path.normpath(__file__)))
28 path_addons = os.path.join(path_root, 'addons')
31 #-----------------------------------------------------------
32 # Per Database Globals (might move into a pool if needed)
33 #-----------------------------------------------------------
35 applicationsession = {}
38 controllers_class = {}
39 controllers_object = {}
42 #----------------------------------------------------------
43 # OpenERP Client Library
44 #----------------------------------------------------------
45 class OpenERPUnboundException(Exception):
48 class OpenERPConnector(object):
51 class OpenERPAuth(object):
54 class OpenERPModel(object):
55 def __init__(self, session, model):
56 self._session = session
59 def __getattr__(self, name):
60 return lambda *l:self._session.execute(self._model, name, *l)
62 class OpenERPSession(object):
64 An OpenERP RPC session, a given user can own multiple such sessions
67 .. attribute:: context
69 The session context, a ``dict``. Can be reloaded by calling
70 :meth:`openerpweb.openerpweb.OpenERPSession.get_context`
72 .. attribute:: domains_store
74 A ``dict`` matching domain keys to evaluable (but non-literal) domains.
76 Used to store references to non-literal domains which need to be
77 round-tripped to the client browser.
79 def __init__(self, server='127.0.0.1', port=8069,
80 model_factory=OpenERPModel):
86 self._password = False
87 self.model_factory = model_factory
88 self._locale = 'en_US'
90 self.contexts_store = {}
91 self.domains_store = {}
93 self.remote_timezone = 'utc'
94 self.client_timezone = False
96 def proxy(self, service):
97 s = xmlrpctimeout.TimeoutServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service), timeout=5)
100 def bind(self, db, uid, password):
103 self._password = password
105 def login(self, db, login, password):
106 uid = self.proxy('common').login(db, login, password)
107 self.bind(db, uid, password)
110 if uid: self.get_context()
113 def execute(self, model, func, *l, **d):
114 if not (self._db and self._uid and self._password):
115 raise OpenERPUnboundException()
116 r = self.proxy('object').execute(self._db, self._uid, self._password, model, func, *l, **d)
119 def model(self, model):
120 """ Get an RPC proxy for the object ``model``, bound to this session.
122 :param model: an OpenERP model name
124 :rtype: :class:`openerpweb.openerpweb.OpenERPModel`
126 return self.model_factory(self, model)
128 def get_context(self):
129 """ Re-initializes the current user's session context (based on
130 his preferences) by calling res.users.get_context() with the old
133 :returns: the new context
135 assert self._uid, "The user needs to be logged-in to initialize his context"
136 self.context = self.model('res.users').context_get(self.context)
138 self.client_timezone = self.context.get("tz", False)
139 if self.client_timezone:
140 self.remote_timezone = self.execute('common', 'timezone_get')
142 self._locale = self.context.get('lang','en_US')
143 lang_ids = self.execute('res.lang','search', [('code', '=', self._locale)])
145 self._lang = self.execute('res.lang', 'read',lang_ids[0], [])
149 def base_eval_context(self):
150 """ Default evaluation context for the session.
152 Used to evaluate contexts and domains.
156 current_date=datetime.date.today().strftime('%Y-%m-%d'),
159 relativedelta=dateutil.relativedelta.relativedelta
161 base.update(self.context)
164 def evaluation_context(self, context=None):
165 """ Returns the session's evaluation context, augmented with the
166 provided context if any.
168 :param dict context: to add merge in the session's base eval context
169 :returns: the augmented context
173 d.update(self.base_eval_context)
178 def eval_context(self, context_to_eval, context=None):
179 """ Evaluates the provided context_to_eval in the context (haha) of
182 :param context_to_eval: a context to evaluate. Must be a dict or a
183 non-literal context. If it's a dict, will be
185 :type context_to_eval: openerpweb.nonliterals.Context
186 :returns: the evaluated context
189 :raises: ``TypeError`` if ``context_to_eval`` is neither a dict nor
192 if not isinstance(context_to_eval, (dict, nonliterals.Domain)):
193 raise TypeError("Context %r is not a dict or a nonliteral Context",
196 if isinstance(context_to_eval, dict):
197 return context_to_eval
199 ctx = dict(context or {})
202 # if the domain was unpacked from JSON, it needs the current
203 # OpenERPSession for its data retrieval
204 context_to_eval.session = self
205 return context_to_eval.evaluate(ctx)
207 def eval_contexts(self, contexts, context=None):
208 """ Evaluates a sequence of contexts to build a single final result
210 :param list contexts: a list of Context or dict contexts
211 :param dict context: a base context, if needed
212 :returns: the final combination of all provided contexts
215 # This is the context we use to evaluate stuff
216 current_context = dict(
217 self.base_eval_context,
219 # this is our result, it should not contain the values
220 # of the base context above
223 # evaluate the current context in the sequence, merge it into
225 final_context.update(
227 ctx, current_context))
228 # update the current evaluation context so that future
229 # evaluations can use the results we just gathered
230 current_context.update(final_context)
233 def eval_domain(self, domain, context=None):
234 """ Evaluates the provided domain using the provided context
235 (merged with the session's evaluation context)
237 :param domain: an OpenERP domain as a list or as a
238 :class:`openerpweb.nonliterals.Domain` instance
240 In the second case, it will be evaluated and returned.
241 :type domain: openerpweb.nonliterals.Domain
242 :param dict context: the context to use in the evaluation, if any.
243 :returns: the evaluated domain
246 :raises: ``TypeError`` if ``domain`` is neither a list nor a Domain
248 if not isinstance(domain, (list, nonliterals.Domain)):
249 raise TypeError("Domain %r is not a list or a nonliteral Domain",
252 if isinstance(domain, list):
255 ctx = dict(context or {})
258 # if the domain was unpacked from JSON, it needs the current
259 # OpenERPSession for its data retrieval
260 domain.session = self
261 return domain.evaluate(ctx)
263 def eval_domains(self, domains, context=None):
264 """ Evaluates and concatenates the provided domains using the
265 provided context for all of them.
267 Returns the final, concatenated result.
269 :param list domains: a list of Domain or list domains
270 :param dict context: the context in which the domains
271 should be evaluated (if evaluations need
273 :returns: the final combination of all domains in the sequence
277 for domain in domains:
279 self.eval_domain(domain, context))
282 #----------------------------------------------------------
283 # OpenERP Web RequestHandler
284 #----------------------------------------------------------
285 class JsonRequest(object):
286 """ JSON-RPC2 over HTTP POST using non standard POST encoding.
287 Difference with the standard:
289 * the json string is passed as a form parameter named "request"
290 * method is currently ignored
293 --> {"jsonrpc": "2.0", "method": "call", "params": {"session_id": "SID", "context": {}, "arg1": "val1" }, "id": null}
294 <-- {"jsonrpc": "2.0", "result": { "res1": "val1" }, "id": null}
296 Request producing a error:
297 --> {"jsonrpc": "2.0", "method": "call", "params": {"session_id": "SID", "context": {}, "arg1": "val1" }, "id": null}
298 <-- {"jsonrpc": "2.0", "error": {"code": 1, "message": "End user error message.", "data": {"code": "codestring", "debug": "traceback" } }, "id": null}
302 def parse(self, request):
303 self.request = request
304 self.params = request.get("params", {})
305 self.applicationsession = applicationsession
306 self.httpsession_id = "cookieid"
307 self.httpsession = cherrypy.session
308 self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex
309 self.session = self.httpsession.setdefault(self.session_id, OpenERPSession())
310 self.context = self.params.pop('context', None)
313 def dispatch(self, controller, method, requestf=None, request=None):
314 ''' Calls the method asked for by the JSON-RPC2 request
316 :param controller: the instance of the controller which received the request
317 :type controller: type
318 :param method: the method which received the request
319 :type method: callable
320 :param requestf: a file-like object containing an encoded JSON-RPC2 request
321 :type requestf: <read() -> bytes>
322 :param request: an encoded JSON-RPC2 request
325 :returns: a string-encoded JSON-RPC2 reply
328 # Read POST content or POST Form Data named "request"
330 request = simplejson.load(requestf, object_hook=nonliterals.non_literal_decoder)
332 request = simplejson.loads(request, object_hook=nonliterals.non_literal_decoder)
334 print "--> %s.%s %s" % (controller.__class__.__name__, method.__name__, request)
337 result = method(controller, self, **self.params)
338 except OpenERPUnboundException:
341 'message': "OpenERP Session Invalid",
343 'type': 'session_invalid',
344 'debug': traceback.format_exc()
347 except xmlrpclib.Fault, e:
350 'message': "OpenERP Server Error",
352 'type': 'server_exception',
353 'fault_code': e.faultCode,
354 'debug': "Client %s\nServer %s" % (
355 "".join(traceback.format_exception("", None, sys.exc_traceback)), e.faultString)
359 cherrypy.log("An error occured while handling a json request",
360 severity=logging.ERROR, traceback=True)
363 'message': "OpenERP WebClient Error",
365 'type': 'client_exception',
366 'debug': "Client %s" % traceback.format_exc()
369 response = {"jsonrpc": "2.0", "id": request.get('id')}
371 response["error"] = error
373 response["result"] = result
375 print "<--", response
378 content = simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder)
379 cherrypy.response.headers['Content-Type'] = 'application/json'
380 cherrypy.response.headers['Content-Length'] = len(content)
386 def json_handler(controller):
387 return JsonRequest().dispatch(controller, f, requestf=cherrypy.request.body)
391 class HttpRequest(object):
392 """ Regular GET/POST request
394 def dispatch(self, controller, f, request, **kw):
395 self.request = request
396 self.applicationsession = applicationsession
397 self.httpsession_id = "cookieid"
398 self.httpsession = cherrypy.session
400 print "GET/POST --> %s.%s %s %r" % (controller.__class__.__name__, f.__name__, request, kw)
401 r = f(controller, self, **kw)
407 # check cleaner wrapping:
408 # functools.wraps(f)(lambda x: JsonRequest().dispatch(x, f))
409 def http_handler(self,*l, **kw):
410 return HttpRequest().dispatch(self, f, cherrypy.request, **kw)
411 http_handler.exposed = 1
414 #-----------------------------------------------------------
416 #-----------------------------------------------------------
418 class ControllerType(type):
419 def __init__(cls, name, bases, attrs):
420 super(ControllerType, cls).__init__(name, bases, attrs)
421 controllers_class["%s.%s" % (cls.__module__, cls.__name__)] = cls
423 class Controller(object):
424 __metaclass__ = ControllerType
431 def _load_addons(self):
432 if path_addons not in sys.path:
433 sys.path.insert(0, path_addons)
434 for i in os.listdir(path_addons):
435 if i not in sys.modules:
436 manifest_path = os.path.join(path_addons, i, '__openerp__.py')
437 if os.path.isfile(manifest_path):
438 manifest = eval(open(manifest_path).read())
439 if manifest.get('active', True):
443 addons_manifest[i] = manifest
444 for k, v in controllers_class.items():
445 if k not in controllers_object:
447 controllers_object[k] = o
448 if hasattr(o, '_cp_path'):
449 controllers_path[o._cp_path] = o
451 def default(self, *l, **kw):
452 #print "default",l,kw
453 # handle static files
454 if len(l) > 2 and l[1] == 'static':
456 p = os.path.normpath(os.path.join(*l))
457 return cherrypy.lib.static.serve_file(os.path.join(path_addons, p))
459 for i in range(len(l), 1, -1):
460 ps = "/" + "/".join(l[0:i])
461 if ps in controllers_path:
462 c = controllers_path[ps]
463 rest = l[i:] or ['index']
466 if getattr(m, 'exposed', 0):
467 print "Calling", ps, c, meth, m
469 raise cherrypy.NotFound('/' + '/'.join(l))
471 raise cherrypy.HTTPRedirect('/base/static/src/base.html', 301)
472 default.exposed = True
476 op = optparse.OptionParser()
477 op.add_option("-p", "--port", dest="socket_port", help="listening port", metavar="NUMBER", default=8002)
478 op.add_option("-s", "--session-path", dest="storage_path",
479 help="directory used for session storage", metavar="DIR",
480 default=os.path.join(tempfile.gettempdir(), "cpsessions"))
481 (o, args) = op.parse_args(argv[1:])
483 # Prepare cherrypy config from options
484 if not os.path.exists(o.storage_path):
485 os.mkdir(o.storage_path, 0700)
487 'server.socket_port': int(o.socket_port),
488 'server.socket_host': '0.0.0.0',
489 #'server.thread_pool' = 10,
490 'tools.sessions.on': True,
491 'tools.sessions.storage_type': 'file',
492 'tools.sessions.storage_path': o.storage_path,
493 'tools.sessions.timeout': 60
496 # Setup and run cherrypy
497 cherrypy.tree.mount(Root())
498 cherrypy.config.update(config)
499 cherrypy.server.subscribe()
500 cherrypy.engine.start()
501 cherrypy.engine.block()