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 assert_valid(self):
112 Ensures this session is valid (logged into the openerp server)
114 if not (self._db and self._uid and self._password):
115 raise OpenERPUnboundException()
117 def execute(self, model, func, *l, **d):
119 r = self.proxy('object').execute(self._db, self._uid, self._password, model, func, *l, **d)
122 def exec_workflow(self, model, id, signal):
124 r = self.proxy('object').exec_workflow(self._db, self._uid, self._password, model, signal, id)
127 def model(self, model):
128 """ Get an RPC proxy for the object ``model``, bound to this session.
130 :param model: an OpenERP model name
132 :rtype: :class:`openerpweb.openerpweb.OpenERPModel`
134 return self.model_factory(self, model)
136 def get_context(self):
137 """ Re-initializes the current user's session context (based on
138 his preferences) by calling res.users.get_context() with the old
141 :returns: the new context
143 assert self._uid, "The user needs to be logged-in to initialize his context"
144 self.context = self.model('res.users').context_get(self.context)
145 self.context = self.context or {}
147 self.client_timezone = self.context.get("tz", False)
148 # invalid code, anyway we decided the server will be in UTC
149 #if self.client_timezone:
150 # self.remote_timezone = self.execute('common', 'timezone_get')
152 self._locale = self.context.get('lang','en_US')
153 lang_ids = self.execute('res.lang','search', [('code', '=', self._locale)])
155 self._lang = self.execute('res.lang', 'read',lang_ids[0], [])
159 def base_eval_context(self):
160 """ Default evaluation context for the session.
162 Used to evaluate contexts and domains.
166 current_date=datetime.date.today().strftime('%Y-%m-%d'),
169 relativedelta=dateutil.relativedelta.relativedelta
171 base.update(self.context)
174 def evaluation_context(self, context=None):
175 """ Returns the session's evaluation context, augmented with the
176 provided context if any.
178 :param dict context: to add merge in the session's base eval context
179 :returns: the augmented context
182 d = dict(self.base_eval_context)
188 def eval_context(self, context_to_eval, context=None):
189 """ Evaluates the provided context_to_eval in the context (haha) of
190 the context. Also merges the evaluated context with the session's context.
192 :param context_to_eval: a context to evaluate. Must be a dict or a
193 non-literal context. If it's a dict, will be
195 :type context_to_eval: openerpweb.nonliterals.Context
196 :returns: the evaluated context
199 :raises: ``TypeError`` if ``context_to_eval`` is neither a dict nor
203 self.base_eval_context,
206 # adding the context of the session to send to the openerp server
207 ccontext = nonliterals.CompoundContext(self.context, context_to_eval or {})
208 ccontext.session = self
209 return ccontext.evaluate(ctx)
211 def eval_domain(self, domain, context=None):
212 """ Evaluates the provided domain using the provided context
213 (merged with the session's evaluation context)
215 :param domain: an OpenERP domain as a list or as a
216 :class:`openerpweb.nonliterals.Domain` instance
218 In the second case, it will be evaluated and returned.
219 :type domain: openerpweb.nonliterals.Domain
220 :param dict context: the context to use in the evaluation, if any.
221 :returns: the evaluated domain
224 :raises: ``TypeError`` if ``domain`` is neither a list nor a Domain
226 if isinstance(domain, list):
229 cdomain = nonliterals.CompoundDomain(domain)
230 cdomain.session = self
231 return cdomain.evaluate(context or {})
233 #----------------------------------------------------------
234 # OpenERP Web RequestHandler
235 #----------------------------------------------------------
236 class JsonRequest(object):
237 """ JSON-RPC2 over HTTP.
241 --> {"jsonrpc": "2.0",
243 "params": {"session_id": "SID",
248 <-- {"jsonrpc": "2.0",
249 "result": { "res1": "val1" },
252 Request producing a error::
254 --> {"jsonrpc": "2.0",
256 "params": {"session_id": "SID",
261 <-- {"jsonrpc": "2.0",
263 "message": "End user error message.",
264 "data": {"code": "codestring",
265 "debug": "traceback" } },
270 def parse(self, request):
271 self.request = request
272 self.params = request.get("params", {})
273 self.applicationsession = applicationsession
274 self.httprequest = cherrypy.request
275 self.httpresponse = cherrypy.response
276 self.httpsession = cherrypy.session
277 self.httpsession_id = "cookieid"
278 self.httpsession = cherrypy.session
279 self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex
280 host = cherrypy.config['openerp.server.host']
281 port = cherrypy.config['openerp.server.port']
282 self.session = self.httpsession.setdefault(self.session_id, OpenERPSession(host, port))
283 self.context = self.params.pop('context', None)
286 def dispatch(self, controller, method, requestf=None, request=None):
287 """ Calls the method asked for by the JSON-RPC2 request
289 :param controller: the instance of the controller which received the request
290 :type controller: type
291 :param method: the method which received the request
292 :type method: callable
293 :param requestf: a file-like object containing an encoded JSON-RPC2 request
294 :type requestf: <read() -> bytes>
295 :param request: an encoded JSON-RPC2 request
298 :returns: a string-encoded JSON-RPC2 reply
301 # Read POST content or POST Form Data named "request"
303 request = simplejson.load(requestf, object_hook=nonliterals.non_literal_decoder)
305 request = simplejson.loads(request, object_hook=nonliterals.non_literal_decoder)
307 response = {"jsonrpc": "2.0", "id": request.get('id')}
309 print "--> %s.%s %s" % (controller.__class__.__name__, method.__name__, request)
312 response["result"] = method(controller, self, **self.params)
313 except OpenERPUnboundException:
316 'message': "OpenERP Session Invalid",
318 'type': 'session_invalid',
319 'debug': traceback.format_exc()
322 except xmlrpclib.Fault, e:
325 'message': "OpenERP Server Error",
327 'type': 'server_exception',
328 'fault_code': e.faultCode,
329 'debug': "Client %s\nServer %s" % (
330 "".join(traceback.format_exception("", None, sys.exc_traceback)), e.faultString)
334 cherrypy.log("An error occured while handling a json request",
335 severity=logging.ERROR, traceback=True)
338 'message': "OpenERP WebClient Error",
340 'type': 'client_exception',
341 'debug': "Client %s" % traceback.format_exc()
345 response["error"] = error
347 print "<--", response
350 content = simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder)
351 cherrypy.response.headers['Content-Type'] = 'application/json'
352 cherrypy.response.headers['Content-Length'] = len(content)
358 def json_handler(controller):
359 return JsonRequest().dispatch(controller, f, requestf=cherrypy.request.body)
363 class HttpRequest(object):
364 """ Regular GET/POST request
366 def dispatch(self, controller, f, request, **kw):
367 self.request = request
368 self.applicationsession = applicationsession
369 self.httpsession_id = "cookieid"
370 self.httpsession = cherrypy.session
371 self.context = kw.get('context', {})
372 host = cherrypy.config['openerp.server.host']
373 port = cherrypy.config['openerp.server.port']
374 self.session = self.httpsession.setdefault(kw.pop('session_id', None), OpenERPSession(host, port))
376 if request.method == 'GET':
377 print "GET --> %s.%s %s %r" % (controller.__class__.__name__, f.__name__, request, kw)
379 akw = dict([(key, kw[key] if isinstance(kw[key], basestring) else type(kw[key])) for key in kw.keys()])
380 print "POST --> %s.%s %s %r" % (controller.__class__.__name__, f.__name__, request, akw)
381 r = f(controller, self, **kw)
382 if isinstance(r, str):
383 print "<--", len(r), 'bytes'
385 print "<--", len(r), 'characters'
390 # check cleaner wrapping:
391 # functools.wraps(f)(lambda x: JsonRequest().dispatch(x, f))
392 def http_handler(self,*l, **kw):
393 return HttpRequest().dispatch(self, f, cherrypy.request, **kw)
394 http_handler.exposed = 1
397 #-----------------------------------------------------------
399 #-----------------------------------------------------------
401 class ControllerType(type):
402 def __init__(cls, name, bases, attrs):
403 super(ControllerType, cls).__init__(name, bases, attrs)
404 controllers_class["%s.%s" % (cls.__module__, cls.__name__)] = cls
406 class Controller(object):
407 __metaclass__ = ControllerType
414 def _load_addons(self):
415 if path_addons not in sys.path:
416 sys.path.insert(0, path_addons)
417 for i in os.listdir(path_addons):
418 if i not in addons_module:
419 manifest_path = os.path.join(path_addons, i, '__openerp__.py')
420 if os.path.isfile(manifest_path):
421 manifest = eval(open(manifest_path).read())
425 addons_manifest[i] = manifest
426 for k, v in controllers_class.items():
427 if k not in controllers_object:
429 controllers_object[k] = o
430 if hasattr(o, '_cp_path'):
431 controllers_path[o._cp_path] = o
433 def default(self, *l, **kw):
434 #print "default",l,kw
435 # handle static files
436 if len(l) > 2 and l[1] == 'static':
438 p = os.path.normpath(os.path.join(*l))
439 return cherrypy.lib.static.serve_file(os.path.join(path_addons, p))
441 for i in range(len(l), 1, -1):
442 ps = "/" + "/".join(l[0:i])
443 if ps in controllers_path:
444 c = controllers_path[ps]
445 rest = l[i:] or ['index']
448 if getattr(m, 'exposed', 0):
449 print "Calling", ps, c, meth, m
451 raise cherrypy.NotFound('/' + '/'.join(l))
452 elif l and l[0] == 'mobile':
453 #for the mobile web client we are supposed to use a different url to just add '/mobile'
454 raise cherrypy.HTTPRedirect('/web_mobile/static/src/web_mobile.html', 301)
456 raise cherrypy.HTTPRedirect('/base/webclient/home', 301)
457 default.exposed = True
460 # change the timezone of the program to the OpenERP server's assumed timezone
461 os.environ["TZ"] = "UTC"
464 'server.socket_host': '0.0.0.0',
465 'tools.sessions.on': True,
466 'tools.sessions.storage_type': 'file',
467 'tools.sessions.timeout': 60
471 op = optparse.OptionParser()
472 op.add_option("-p", "--port", dest="server.socket_port", default=8002, help="listening port", type="int", metavar="NUMBER")
473 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")
474 op.add_option("--server-host", dest="openerp.server.host", default='127.0.0.1', help="OpenERP server hostname", metavar="HOST")
475 op.add_option("--server-port", dest="openerp.server.port", default=8069, help="OpenERP server port", type="int", metavar="NUMBER")
476 op.add_option("--db-filter", dest="openerp.dbfilter", default='.*', help="Filter listed database", metavar="REGEXP")
477 (o, args) = op.parse_args(argv[1:])
483 # Setup and run cherrypy
484 cherrypy.tree.mount(Root())
486 cherrypy.config.update(config=DEFAULT_CONFIG)
487 if os.path.exists(os.path.join(path_root,'openerp-web.cfg')):
488 cherrypy.config.update(os.path.join(path_root,'openerp-web.cfg'))
489 if os.path.exists(os.path.expanduser('~/.openerp_webrc')):
490 cherrypy.config.update(os.path.expanduser('~/.openerp_webrc'))
491 cherrypy.config.update(o)
493 if not os.path.exists(cherrypy.config['tools.sessions.storage_path']):
494 os.makedirs(cherrypy.config['tools.sessions.storage_path'], 0700)
496 cherrypy.server.subscribe()
497 cherrypy.engine.start()
498 cherrypy.engine.block()