3 import dateutil.relativedelta
15 import cherrypy.lib.static
21 #-----------------------------------------------------------
23 #-----------------------------------------------------------
25 path_root = os.path.dirname(os.path.dirname(os.path.normpath(__file__)))
26 path_addons = os.path.join(path_root, 'addons')
29 #-----------------------------------------------------------
30 # Per Database Globals (might move into a pool if needed)
31 #-----------------------------------------------------------
33 applicationsession = {}
36 controllers_class = {}
37 controllers_object = {}
40 #----------------------------------------------------------
41 # OpenERP Client Library
42 #----------------------------------------------------------
43 class OpenERPUnboundException(Exception):
46 class OpenERPConnector(object):
49 class OpenERPAuth(object):
52 class OpenERPModel(object):
53 def __init__(self, session, model):
54 self._session = session
57 def __getattr__(self, name):
58 return lambda *l:self._session.execute(self._model, name, *l)
60 class OpenERPSession(object):
62 An OpenERP RPC session, a given user can own multiple such sessions
65 .. attribute:: context
67 The session context, a ``dict``. Can be reloaded by calling
68 :meth:`openerpweb.openerpweb.OpenERPSession.get_context`
70 .. attribute:: domains_store
72 A ``dict`` matching domain keys to evaluable (but non-literal) domains.
74 Used to store references to non-literal domains which need to be
75 round-tripped to the client browser.
77 def __init__(self, server='127.0.0.1', port=8069,
78 model_factory=OpenERPModel):
84 self._password = False
85 self.model_factory = model_factory
86 self._locale = 'en_US'
88 self.contexts_store = {}
89 self.domains_store = {}
91 self.remote_timezone = 'utc'
92 self.client_timezone = False
94 def proxy(self, service):
95 s = xmlrpclib.ServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service))
98 def bind(self, db, uid, password):
101 self._password = password
103 def login(self, db, login, password):
104 uid = self.proxy('common').login(db, login, password)
105 self.bind(db, uid, password)
108 if uid: self.get_context()
111 def execute(self, model, func, *l, **d):
112 if not (self._db and self._uid and self._password):
113 raise OpenERPUnboundException()
114 r = self.proxy('object').execute(self._db, self._uid, self._password, model, func, *l, **d)
117 def exec_workflow(self, model, id, signal):
118 if not (self._db and self._uid and self._password):
119 raise OpenERPUnboundException()
120 r = self.proxy('object').exec_workflow(self._db, self._uid, self._password, model, signal, id)
123 def model(self, model):
124 """ Get an RPC proxy for the object ``model``, bound to this session.
126 :param model: an OpenERP model name
128 :rtype: :class:`openerpweb.openerpweb.OpenERPModel`
130 return self.model_factory(self, model)
132 def get_context(self):
133 """ Re-initializes the current user's session context (based on
134 his preferences) by calling res.users.get_context() with the old
137 :returns: the new context
139 assert self._uid, "The user needs to be logged-in to initialize his context"
140 self.context = self.model('res.users').context_get(self.context)
142 self.client_timezone = self.context.get("tz", False)
143 # invalid code, anyway we decided the server will be in UTC
144 #if self.client_timezone:
145 # self.remote_timezone = self.execute('common', 'timezone_get')
147 self._locale = self.context.get('lang','en_US')
148 lang_ids = self.execute('res.lang','search', [('code', '=', self._locale)])
150 self._lang = self.execute('res.lang', 'read',lang_ids[0], [])
154 def base_eval_context(self):
155 """ Default evaluation context for the session.
157 Used to evaluate contexts and domains.
161 current_date=datetime.date.today().strftime('%Y-%m-%d'),
164 relativedelta=dateutil.relativedelta.relativedelta
166 base.update(self.context)
169 def evaluation_context(self, context=None):
170 """ Returns the session's evaluation context, augmented with the
171 provided context if any.
173 :param dict context: to add merge in the session's base eval context
174 :returns: the augmented context
178 d.update(self.base_eval_context)
183 def eval_context(self, context_to_eval, context=None):
184 """ Evaluates the provided context_to_eval in the context (haha) of
187 :param context_to_eval: a context to evaluate. Must be a dict or a
188 non-literal context. If it's a dict, will be
190 :type context_to_eval: openerpweb.nonliterals.Context
191 :returns: the evaluated context
194 :raises: ``TypeError`` if ``context_to_eval`` is neither a dict nor
197 if not isinstance(context_to_eval, (dict, nonliterals.Domain)):
198 raise TypeError("Context %r is not a dict or a nonliteral Context",
201 if isinstance(context_to_eval, dict):
202 return context_to_eval
204 ctx = dict(context or {})
207 # if the domain was unpacked from JSON, it needs the current
208 # OpenERPSession for its data retrieval
209 context_to_eval.session = self
210 return context_to_eval.evaluate(ctx)
212 def eval_contexts(self, contexts, context=None):
213 """ Evaluates a sequence of contexts to build a single final result
215 :param list contexts: a list of Context or dict contexts
216 :param dict context: a base context, if needed
217 :returns: the final combination of all provided contexts
220 # This is the context we use to evaluate stuff
221 current_context = dict(
222 self.base_eval_context,
224 # this is our result, it should not contain the values
225 # of the base context above
228 # evaluate the current context in the sequence, merge it into
230 final_context.update(
232 ctx, current_context))
233 # update the current evaluation context so that future
234 # evaluations can use the results we just gathered
235 current_context.update(final_context)
238 def eval_domain(self, domain, context=None):
239 """ Evaluates the provided domain using the provided context
240 (merged with the session's evaluation context)
242 :param domain: an OpenERP domain as a list or as a
243 :class:`openerpweb.nonliterals.Domain` instance
245 In the second case, it will be evaluated and returned.
246 :type domain: openerpweb.nonliterals.Domain
247 :param dict context: the context to use in the evaluation, if any.
248 :returns: the evaluated domain
251 :raises: ``TypeError`` if ``domain`` is neither a list nor a Domain
253 if not isinstance(domain, (list, nonliterals.Domain)):
254 raise TypeError("Domain %r is not a list or a nonliteral Domain",
257 if isinstance(domain, list):
260 ctx = dict(context or {})
263 # if the domain was unpacked from JSON, it needs the current
264 # OpenERPSession for its data retrieval
265 domain.session = self
266 return domain.evaluate(ctx)
268 def eval_domains(self, domains, context=None):
269 """ Evaluates and concatenates the provided domains using the
270 provided context for all of them.
272 Returns the final, concatenated result.
274 :param list domains: a list of Domain or list domains
275 :param dict context: the context in which the domains
276 should be evaluated (if evaluations need
278 :returns: the final combination of all domains in the sequence
282 for domain in domains:
284 self.eval_domain(domain, context))
287 #----------------------------------------------------------
288 # OpenERP Web RequestHandler
289 #----------------------------------------------------------
290 class JsonRequest(object):
291 """ JSON-RPC2 over HTTP POST using non standard POST encoding.
292 Difference with the standard:
294 * the json string is passed as a form parameter named "request"
295 * method is currently ignored
299 --> {"jsonrpc": "2.0",
301 "params": {"session_id": "SID",
306 <-- {"jsonrpc": "2.0",
307 "result": { "res1": "val1" },
310 Request producing a error::
312 --> {"jsonrpc": "2.0",
314 "params": {"session_id": "SID",
319 <-- {"jsonrpc": "2.0",
321 "message": "End user error message.",
322 "data": {"code": "codestring",
323 "debug": "traceback" } },
328 def parse(self, request):
329 self.request = request
330 self.params = request.get("params", {})
331 self.applicationsession = applicationsession
332 self.httpsession_id = "cookieid"
333 self.httpsession = cherrypy.session
334 self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex
335 self.session = self.httpsession.setdefault(self.session_id, OpenERPSession())
336 self.context = self.params.pop('context', None)
339 def dispatch(self, controller, method, requestf=None, request=None):
340 ''' Calls the method asked for by the JSON-RPC2 request
342 :param controller: the instance of the controller which received the request
343 :type controller: type
344 :param method: the method which received the request
345 :type method: callable
346 :param requestf: a file-like object containing an encoded JSON-RPC2 request
347 :type requestf: <read() -> bytes>
348 :param request: an encoded JSON-RPC2 request
351 :returns: a string-encoded JSON-RPC2 reply
354 # Read POST content or POST Form Data named "request"
356 request = simplejson.load(requestf, object_hook=nonliterals.non_literal_decoder)
358 request = simplejson.loads(request, object_hook=nonliterals.non_literal_decoder)
360 print "--> %s.%s %s" % (controller.__class__.__name__, method.__name__, request)
363 result = method(controller, self, **self.params)
364 except OpenERPUnboundException:
367 'message': "OpenERP Session Invalid",
369 'type': 'session_invalid',
370 'debug': traceback.format_exc()
373 except xmlrpclib.Fault, e:
376 'message': "OpenERP Server Error",
378 'type': 'server_exception',
379 'fault_code': e.faultCode,
380 'debug': "Client %s\nServer %s" % (
381 "".join(traceback.format_exception("", None, sys.exc_traceback)), e.faultString)
385 cherrypy.log("An error occured while handling a json request",
386 severity=logging.ERROR, traceback=True)
389 'message': "OpenERP WebClient Error",
391 'type': 'client_exception',
392 'debug': "Client %s" % traceback.format_exc()
395 response = {"jsonrpc": "2.0", "id": request.get('id')}
397 response["error"] = error
399 response["result"] = result
401 print "<--", response
404 content = simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder)
405 cherrypy.response.headers['Content-Type'] = 'application/json'
406 cherrypy.response.headers['Content-Length'] = len(content)
412 def json_handler(controller):
413 return JsonRequest().dispatch(controller, f, requestf=cherrypy.request.body)
417 class HttpRequest(object):
418 """ Regular GET/POST request
420 def dispatch(self, controller, f, request, **kw):
421 self.request = request
422 self.applicationsession = applicationsession
423 self.httpsession_id = "cookieid"
424 self.httpsession = cherrypy.session
425 self.context = kw.get('context', {})
426 self.session = self.httpsession.setdefault(kw.get('session_id', None), OpenERPSession())
428 if request.method == 'GET':
429 print "GET --> %s.%s %s %r" % (controller.__class__.__name__, f.__name__, request, kw)
431 akw = dict([(key, kw[key] if isinstance(kw[key], basestring) else type(kw[key])) for key in kw.keys()])
432 print "POST --> %s.%s %s %r" % (controller.__class__.__name__, f.__name__, request, akw)
433 r = f(controller, self, **kw)
439 # check cleaner wrapping:
440 # functools.wraps(f)(lambda x: JsonRequest().dispatch(x, f))
441 def http_handler(self,*l, **kw):
442 return HttpRequest().dispatch(self, f, cherrypy.request, **kw)
443 http_handler.exposed = 1
446 #-----------------------------------------------------------
448 #-----------------------------------------------------------
450 class ControllerType(type):
451 def __init__(cls, name, bases, attrs):
452 super(ControllerType, cls).__init__(name, bases, attrs)
453 controllers_class["%s.%s" % (cls.__module__, cls.__name__)] = cls
455 class Controller(object):
456 __metaclass__ = ControllerType
463 def _load_addons(self):
464 if path_addons not in sys.path:
465 sys.path.insert(0, path_addons)
466 for i in os.listdir(path_addons):
467 if i not in sys.modules:
468 manifest_path = os.path.join(path_addons, i, '__openerp__.py')
469 if os.path.isfile(manifest_path):
470 manifest = eval(open(manifest_path).read())
474 addons_manifest[i] = manifest
475 for k, v in controllers_class.items():
476 if k not in controllers_object:
478 controllers_object[k] = o
479 if hasattr(o, '_cp_path'):
480 controllers_path[o._cp_path] = o
482 def default(self, *l, **kw):
483 #print "default",l,kw
484 # handle static files
485 if len(l) > 2 and l[1] == 'static':
487 p = os.path.normpath(os.path.join(*l))
488 return cherrypy.lib.static.serve_file(os.path.join(path_addons, p))
490 for i in range(len(l), 1, -1):
491 ps = "/" + "/".join(l[0:i])
492 if ps in controllers_path:
493 c = controllers_path[ps]
494 rest = l[i:] or ['index']
497 if getattr(m, 'exposed', 0):
498 print "Calling", ps, c, meth, m
500 raise cherrypy.NotFound('/' + '/'.join(l))
502 raise cherrypy.HTTPRedirect('/base/static/src/base.html', 301)
503 default.exposed = True
506 # change the timezone of the program to the OpenERP server's assumed timezone
507 os.environ["TZ"] = "UTC"
510 'server.socket_port': 8002,
511 'server.socket_host': '0.0.0.0',
512 'tools.sessions.on': True,
513 'tools.sessions.storage_type': 'file',
514 'tools.sessions.storage_path': os.path.join(tempfile.gettempdir(), "cpsessions"),
515 'tools.sessions.timeout': 60
519 op = optparse.OptionParser()
520 op.add_option("-p", "--port", dest="server.socket_port", help="listening port",
521 type="int", metavar="NUMBER")
522 op.add_option("-s", "--session-path", dest="tools.sessions.storage_path",
523 help="directory used for session storage", metavar="DIR")
524 (o, args) = op.parse_args(argv[1:])
530 # Setup and run cherrypy
531 cherrypy.tree.mount(Root())
533 cherrypy.config.update(config=DEFAULT_CONFIG)
534 if os.path.exists(os.path.join(os.path.dirname(
535 os.path.dirname(__file__)),'openerp-web.cfg')):
536 cherrypy.config.update(os.path.join(os.path.dirname(
537 os.path.dirname(__file__)),'openerp-web.cfg'))
538 if os.path.exists(os.path.expanduser('~/.openerp_webrc')):
539 cherrypy.config.update(os.path.expanduser('~/.openerp_webrc'))
540 cherrypy.config.update(o)
542 if not os.path.exists(cherrypy.config['tools.sessions.storage_path']):
543 os.mkdir(cherrypy.config['tools.sessions.storage_path'], 0700)
545 cherrypy.server.subscribe()
546 cherrypy.engine.start()
547 cherrypy.engine.block()