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, model_factory=OpenERPModel):
83 self._password = False
84 self.model_factory = model_factory
85 self._locale = 'en_US'
87 self.contexts_store = {}
88 self.domains_store = {}
90 self.remote_timezone = 'utc'
91 self.client_timezone = False
93 def proxy(self, service):
94 s = xmlrpclib.ServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service))
97 def bind(self, db, uid, password):
100 self._password = password
102 def login(self, db, login, password):
103 uid = self.proxy('common').login(db, login, password)
104 self.bind(db, uid, password)
107 if uid: self.get_context()
110 def execute(self, model, func, *l, **d):
111 if not (self._db and self._uid and self._password):
112 raise OpenERPUnboundException()
113 r = self.proxy('object').execute(self._db, self._uid, self._password, model, func, *l, **d)
116 def exec_workflow(self, model, id, signal):
117 if not (self._db and self._uid and self._password):
118 raise OpenERPUnboundException()
119 r = self.proxy('object').exec_workflow(self._db, self._uid, self._password, model, signal, id)
122 def model(self, model):
123 """ Get an RPC proxy for the object ``model``, bound to this session.
125 :param model: an OpenERP model name
127 :rtype: :class:`openerpweb.openerpweb.OpenERPModel`
129 return self.model_factory(self, model)
131 def get_context(self):
132 """ Re-initializes the current user's session context (based on
133 his preferences) by calling res.users.get_context() with the old
136 :returns: the new context
138 assert self._uid, "The user needs to be logged-in to initialize his context"
139 self.context = self.model('res.users').context_get(self.context)
140 self.context = self.context or {}
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
177 d = dict(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
185 the context. Also merges the evaluated context with the session's context.
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
198 self.base_eval_context,
201 # adding the context of the session to send to the openerp server
202 ccontext = nonliterals.CompoundContext(self.context, context_to_eval or {})
203 ccontext.session = self
204 return ccontext.evaluate(ctx)
206 def eval_domain(self, domain, context=None):
207 """ Evaluates the provided domain using the provided context
208 (merged with the session's evaluation context)
210 :param domain: an OpenERP domain as a list or as a
211 :class:`openerpweb.nonliterals.Domain` instance
213 In the second case, it will be evaluated and returned.
214 :type domain: openerpweb.nonliterals.Domain
215 :param dict context: the context to use in the evaluation, if any.
216 :returns: the evaluated domain
219 :raises: ``TypeError`` if ``domain`` is neither a list nor a Domain
221 if isinstance(domain, list):
224 cdomain = nonliterals.CompoundDomain(domain)
225 cdomain.session = self
226 return cdomain.evaluate(context or {})
228 #----------------------------------------------------------
229 # OpenERP Web RequestHandler
230 #----------------------------------------------------------
231 class JsonRequest(object):
232 """ JSON-RPC2 over HTTP.
236 --> {"jsonrpc": "2.0",
238 "params": {"session_id": "SID",
243 <-- {"jsonrpc": "2.0",
244 "result": { "res1": "val1" },
247 Request producing a error::
249 --> {"jsonrpc": "2.0",
251 "params": {"session_id": "SID",
256 <-- {"jsonrpc": "2.0",
258 "message": "End user error message.",
259 "data": {"code": "codestring",
260 "debug": "traceback" } },
265 def parse(self, request):
266 self.request = request
267 self.params = request.get("params", {})
268 self.applicationsession = applicationsession
269 self.httprequest = cherrypy.request
270 self.httpresponse = cherrypy.response
271 self.httpsession = cherrypy.session
272 self.httpsession_id = "cookieid"
273 self.httpsession = cherrypy.session
274 self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex
275 host = cherrypy.config['openerp.server.host']
276 port = cherrypy.config['openerp.server.port']
277 self.session = self.httpsession.setdefault(self.session_id, OpenERPSession(host, port))
278 self.context = self.params.pop('context', None)
281 def dispatch(self, controller, method, requestf=None, request=None):
282 """ Calls the method asked for by the JSON-RPC2 request
284 :param controller: the instance of the controller which received the request
285 :type controller: type
286 :param method: the method which received the request
287 :type method: callable
288 :param requestf: a file-like object containing an encoded JSON-RPC2 request
289 :type requestf: <read() -> bytes>
290 :param request: an encoded JSON-RPC2 request
293 :returns: a string-encoded JSON-RPC2 reply
296 # Read POST content or POST Form Data named "request"
298 request = simplejson.load(requestf, object_hook=nonliterals.non_literal_decoder)
300 request = simplejson.loads(request, object_hook=nonliterals.non_literal_decoder)
302 response = {"jsonrpc": "2.0", "id": request.get('id')}
304 print "--> %s.%s %s" % (controller.__class__.__name__, method.__name__, request)
307 response["result"] = method(controller, self, **self.params)
308 except OpenERPUnboundException:
311 'message': "OpenERP Session Invalid",
313 'type': 'session_invalid',
314 'debug': traceback.format_exc()
317 except xmlrpclib.Fault, e:
320 'message': "OpenERP Server Error",
322 'type': 'server_exception',
323 'fault_code': e.faultCode,
324 'debug': "Client %s\nServer %s" % (
325 "".join(traceback.format_exception("", None, sys.exc_traceback)), e.faultString)
329 cherrypy.log("An error occured while handling a json request",
330 severity=logging.ERROR, traceback=True)
333 'message': "OpenERP WebClient Error",
335 'type': 'client_exception',
336 'debug': "Client %s" % traceback.format_exc()
340 response["error"] = error
342 print "<--", response
345 content = simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder)
346 cherrypy.response.headers['Content-Type'] = 'application/json'
347 cherrypy.response.headers['Content-Length'] = len(content)
353 def json_handler(controller):
354 return JsonRequest().dispatch(controller, f, requestf=cherrypy.request.body)
358 class HttpRequest(object):
359 """ Regular GET/POST request
361 def dispatch(self, controller, f, request, **kw):
362 self.request = request
363 self.applicationsession = applicationsession
364 self.httpsession_id = "cookieid"
365 self.httpsession = cherrypy.session
366 self.context = kw.get('context', {})
367 host = cherrypy.config['openerp.server.host']
368 port = cherrypy.config['openerp.server.port']
369 self.session = self.httpsession.setdefault(kw.get('session_id'), OpenERPSession(host, port))
371 if request.method == 'GET':
372 print "GET --> %s.%s %s %r" % (controller.__class__.__name__, f.__name__, request, kw)
374 akw = dict([(key, kw[key] if isinstance(kw[key], basestring) else type(kw[key])) for key in kw.keys()])
375 print "POST --> %s.%s %s %r" % (controller.__class__.__name__, f.__name__, request, akw)
376 r = f(controller, self, **kw)
382 # check cleaner wrapping:
383 # functools.wraps(f)(lambda x: JsonRequest().dispatch(x, f))
384 def http_handler(self,*l, **kw):
385 return HttpRequest().dispatch(self, f, cherrypy.request, **kw)
386 http_handler.exposed = 1
389 #-----------------------------------------------------------
391 #-----------------------------------------------------------
393 class ControllerType(type):
394 def __init__(cls, name, bases, attrs):
395 super(ControllerType, cls).__init__(name, bases, attrs)
396 controllers_class["%s.%s" % (cls.__module__, cls.__name__)] = cls
398 class Controller(object):
399 __metaclass__ = ControllerType
406 def _load_addons(self):
407 if path_addons not in sys.path:
408 sys.path.insert(0, path_addons)
409 for i in os.listdir(path_addons):
410 if i not in addons_module:
411 manifest_path = os.path.join(path_addons, i, '__openerp__.py')
412 if os.path.isfile(manifest_path):
413 manifest = eval(open(manifest_path).read())
417 addons_manifest[i] = manifest
418 for k, v in controllers_class.items():
419 if k not in controllers_object:
421 controllers_object[k] = o
422 if hasattr(o, '_cp_path'):
423 controllers_path[o._cp_path] = o
425 def default(self, *l, **kw):
426 #print "default",l,kw
427 # handle static files
428 if len(l) > 2 and l[1] == 'static':
430 p = os.path.normpath(os.path.join(*l))
431 return cherrypy.lib.static.serve_file(os.path.join(path_addons, p))
433 for i in range(len(l), 1, -1):
434 ps = "/" + "/".join(l[0:i])
435 if ps in controllers_path:
436 c = controllers_path[ps]
437 rest = l[i:] or ['index']
440 if getattr(m, 'exposed', 0):
441 print "Calling", ps, c, meth, m
443 raise cherrypy.NotFound('/' + '/'.join(l))
444 elif l and l[0] == 'mobile':
445 #for the mobile web client we are supposed to use a different url to just add '/mobile'
446 raise cherrypy.HTTPRedirect('/web_mobile/static/src/web_mobile.html', 301)
448 raise cherrypy.HTTPRedirect('/base/webclient/home', 301)
449 default.exposed = True
452 # change the timezone of the program to the OpenERP server's assumed timezone
453 os.environ["TZ"] = "UTC"
456 'server.socket_host': '0.0.0.0',
457 'tools.sessions.on': True,
458 'tools.sessions.storage_type': 'file',
459 'tools.sessions.timeout': 60
463 op = optparse.OptionParser()
464 op.add_option("-p", "--port", dest="server.socket_port", default=8002, help="listening port", type="int", metavar="NUMBER")
465 op.add_option("-s", "--session-path", dest="tools.sessions.storage_path", default=os.path.join(tempfile.gettempdir(), "cpsessions"), help="directory used for session storage", metavar="DIR")
466 op.add_option("--server-host", dest="openerp.server.host", default='127.0.0.1', help="OpenERP server hostname", metavar="HOST")
467 op.add_option("--server-port", dest="openerp.server.port", default=8069, help="OpenERP server port", type="int", metavar="NUMBER")
468 op.add_option("--db-filter", dest="openerp.dbfilter", default='.*', help="Filter listed database", metavar="REGEXP")
469 (o, args) = op.parse_args(argv[1:])
475 # Setup and run cherrypy
476 cherrypy.tree.mount(Root())
478 cherrypy.config.update(config=DEFAULT_CONFIG)
479 if os.path.exists(os.path.join(path_root,'openerp-web.cfg')):
480 cherrypy.config.update(os.path.join(path_root,'openerp-web.cfg'))
481 if os.path.exists(os.path.expanduser('~/.openerp_webrc')):
482 cherrypy.config.update(os.path.expanduser('~/.openerp_webrc'))
483 cherrypy.config.update(o)
485 if not os.path.exists(cherrypy.config['tools.sessions.storage_path']):
486 os.makedirs(cherrypy.config['tools.sessions.storage_path'], 0700)
488 cherrypy.server.subscribe()
489 cherrypy.engine.start()
490 cherrypy.engine.block()