[FIX] survey: data_get code now returns the good type for sequence
[odoo/odoo.git] / openerpweb / openerpweb.py
1 #!/usr/bin/python
2 import datetime
3 import dateutil.relativedelta
4 import functools
5 import optparse
6 import os
7 import sys
8 import tempfile
9 import time
10 import traceback
11 import uuid
12 import xmlrpclib
13
14 import cherrypy
15 import cherrypy.lib.static
16 import simplejson
17
18 import nonliterals
19 import logging
20
21 #-----------------------------------------------------------
22 # Globals
23 #-----------------------------------------------------------
24
25 path_root = os.path.dirname(os.path.dirname(os.path.normpath(__file__)))
26 path_addons = os.path.join(path_root, 'addons')
27 cherrypy_root = None
28
29 #-----------------------------------------------------------
30 # Per Database Globals (might move into a pool if needed)
31 #-----------------------------------------------------------
32
33 applicationsession = {}
34 addons_module = {}
35 addons_manifest = {}
36 controllers_class = {}
37 controllers_object = {}
38 controllers_path = {}
39
40 #----------------------------------------------------------
41 # OpenERP Client Library
42 #----------------------------------------------------------
43 class OpenERPUnboundException(Exception):
44     pass
45
46 class OpenERPConnector(object):
47     pass
48
49 class OpenERPAuth(object):
50     pass
51
52 class OpenERPModel(object):
53     def __init__(self, session, model):
54         self._session = session
55         self._model = model
56
57     def __getattr__(self, name):
58         return lambda *l:self._session.execute(self._model, name, *l)
59
60 class OpenERPSession(object):
61     """
62     An OpenERP RPC session, a given user can own multiple such sessions
63     in a web session.
64
65     .. attribute:: context
66     
67         The session context, a ``dict``. Can be reloaded by calling
68         :meth:`openerpweb.openerpweb.OpenERPSession.get_context`
69
70     .. attribute:: domains_store
71
72         A ``dict`` matching domain keys to evaluable (but non-literal) domains.
73
74         Used to store references to non-literal domains which need to be
75         round-tripped to the client browser.
76     """
77     def __init__(self, server='127.0.0.1', port=8069, model_factory=OpenERPModel):
78         self._server = server
79         self._port = port
80         self._db = False
81         self._uid = False
82         self._login = False
83         self._password = False
84         self.model_factory = model_factory
85         self._locale = 'en_US'
86         self.context = {}
87         self.contexts_store = {}
88         self.domains_store = {}
89         self._lang = {}
90         self.remote_timezone = 'utc'
91         self.client_timezone = False
92
93     def proxy(self, service):
94         s = xmlrpclib.ServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service))
95         return s
96
97     def bind(self, db, uid, password):
98         self._db = db
99         self._uid = uid
100         self._password = password
101
102     def login(self, db, login, password):
103         uid = self.proxy('common').login(db, login, password)
104         self.bind(db, uid, password)
105         self._login = login
106         
107         if uid: self.get_context()
108         return uid
109
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)
114         return r
115
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)
120         return r
121
122     def model(self, model):
123         """ Get an RPC proxy for the object ``model``, bound to this session.
124
125         :param model: an OpenERP model name
126         :type model: str
127         :rtype: :class:`openerpweb.openerpweb.OpenERPModel`
128         """
129         return self.model_factory(self, model)
130
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
134         context
135
136         :returns: the new context
137         """
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 {}
141
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')
146
147         self._locale = self.context.get('lang','en_US')
148         lang_ids = self.execute('res.lang','search', [('code', '=', self._locale)])
149         if lang_ids:
150             self._lang = self.execute('res.lang', 'read',lang_ids[0], [])
151         return self.context
152
153     @property
154     def base_eval_context(self):
155         """ Default evaluation context for the session.
156
157         Used to evaluate contexts and domains.
158         """
159         base = dict(
160             uid=self._uid,
161             current_date=datetime.date.today().strftime('%Y-%m-%d'),
162             time=time,
163             datetime=datetime,
164             relativedelta=dateutil.relativedelta.relativedelta
165         )
166         base.update(self.context)
167         return base
168
169     def evaluation_context(self, context=None):
170         """ Returns the session's evaluation context, augmented with the
171         provided context if any.
172
173         :param dict context: to add merge in the session's base eval context
174         :returns: the augmented context
175         :rtype: dict
176         """
177         d = dict(self.base_eval_context)
178         if context:
179             d.update(context)
180         d['context'] = d
181         return d
182
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.
186
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
189                                 returned as-is
190         :type context_to_eval: openerpweb.nonliterals.Context
191         :returns: the evaluated context
192         :rtype: dict
193
194         :raises: ``TypeError`` if ``context_to_eval`` is neither a dict nor
195                  a Context
196         """
197         ctx = dict(
198             self.base_eval_context,
199             **(context or {}))
200         
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)
205
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)
209
210         :param domain: an OpenERP domain as a list or as a
211                        :class:`openerpweb.nonliterals.Domain` instance
212
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
217         :rtype: list
218
219         :raises: ``TypeError`` if ``domain`` is neither a list nor a Domain
220         """
221         if isinstance(domain, list):
222             return domain
223
224         cdomain = nonliterals.CompoundDomain(domain)
225         cdomain.session = self
226         return cdomain.evaluate(context or {})
227
228 #----------------------------------------------------------
229 # OpenERP Web RequestHandler
230 #----------------------------------------------------------
231 class JsonRequest(object):
232     """ JSON-RPC2 over HTTP.
233
234     Sucessful request::
235
236       --> {"jsonrpc": "2.0",
237            "method": "call",
238            "params": {"session_id": "SID",
239                       "context": {},
240                       "arg1": "val1" },
241            "id": null}
242
243       <-- {"jsonrpc": "2.0",
244            "result": { "res1": "val1" },
245            "id": null}
246
247     Request producing a error::
248
249       --> {"jsonrpc": "2.0",
250            "method": "call",
251            "params": {"session_id": "SID",
252                       "context": {},
253                       "arg1": "val1" },
254            "id": null}
255
256       <-- {"jsonrpc": "2.0",
257            "error": {"code": 1,
258                      "message": "End user error message.",
259                      "data": {"code": "codestring",
260                               "debug": "traceback" } },
261            "id": null}
262
263     """
264
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)
279         return self.params
280
281     def dispatch(self, controller, method, requestf=None, request=None):
282         """ Calls the method asked for by the JSON-RPC2 request
283
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
291         :type request: bytes
292
293         :returns: a string-encoded JSON-RPC2 reply
294         :rtype: bytes
295         """
296         # Read POST content or POST Form Data named "request"
297         if requestf:
298             request = simplejson.load(requestf, object_hook=nonliterals.non_literal_decoder)
299         else:
300             request = simplejson.loads(request, object_hook=nonliterals.non_literal_decoder)
301
302         response = {"jsonrpc": "2.0", "id": request.get('id')}
303         try:
304             print "--> %s.%s %s" % (controller.__class__.__name__, method.__name__, request)
305             error = None
306             self.parse(request)
307             response["result"] = method(controller, self, **self.params)
308         except OpenERPUnboundException:
309             error = {
310                 'code': 100,
311                 'message': "OpenERP Session Invalid",
312                 'data': {
313                     'type': 'session_invalid',
314                     'debug': traceback.format_exc()
315                 }
316             }
317         except xmlrpclib.Fault, e:
318             error = {
319                 'code': 200,
320                 'message': "OpenERP Server Error",
321                 'data': {
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)
326                 }
327             }
328         except Exception:
329             cherrypy.log("An error occured while handling a json request",
330                          severity=logging.ERROR, traceback=True)
331             error = {
332                 'code': 300,
333                 'message': "OpenERP WebClient Error",
334                 'data': {
335                     'type': 'client_exception',
336                     'debug': "Client %s" % traceback.format_exc()
337                 }
338             }
339         if error:
340             response["error"] = error
341
342         print "<--", response
343         print
344
345         content = simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder)
346         cherrypy.response.headers['Content-Type'] = 'application/json'
347         cherrypy.response.headers['Content-Length'] = len(content)
348         return content
349
350 def jsonrequest(f):
351     @cherrypy.expose
352     @functools.wraps(f)
353     def json_handler(controller):
354         return JsonRequest().dispatch(controller, f, requestf=cherrypy.request.body)
355
356     return json_handler
357
358 class HttpRequest(object):
359     """ Regular GET/POST request
360     """
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))
370         self.result = ""
371         if request.method == 'GET':
372             print "GET --> %s.%s %s %r" % (controller.__class__.__name__, f.__name__, request, kw)
373         else:
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)
377         print "<--", r
378         print
379         return r
380
381 def httprequest(f):
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
387     return http_handler
388
389 #-----------------------------------------------------------
390 # Cherrypy stuff
391 #-----------------------------------------------------------
392
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
397
398 class Controller(object):
399     __metaclass__ = ControllerType
400
401 class Root(object):
402     def __init__(self):
403         self.addons = {}
404         self._load_addons()
405
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())
414                     print "Loading", i
415                     m = __import__(i)
416                     addons_module[i] = m
417                     addons_manifest[i] = manifest
418         for k, v in controllers_class.items():
419             if k not in controllers_object:
420                 o = v()
421                 controllers_object[k] = o
422                 if hasattr(o, '_cp_path'):
423                     controllers_path[o._cp_path] = o
424
425     def default(self, *l, **kw):
426         #print "default",l,kw
427         # handle static files
428         if len(l) > 2 and l[1] == 'static':
429             # sanitize path
430             p = os.path.normpath(os.path.join(*l))
431             return cherrypy.lib.static.serve_file(os.path.join(path_addons, p))
432         elif len(l) > 1:
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']
438                     meth = rest[0]
439                     m = getattr(c, meth)
440                     if getattr(m, 'exposed', 0):
441                         print "Calling", ps, c, meth, m
442                         return m(**kw)
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)
447         else:
448             raise cherrypy.HTTPRedirect('/base/webclient/home', 301)
449     default.exposed = True
450
451 def main(argv):
452     # change the timezone of the program to the OpenERP server's assumed timezone
453     os.environ["TZ"] = "UTC"
454
455     DEFAULT_CONFIG = {
456         'server.socket_host': '0.0.0.0',
457         'tools.sessions.on': True,
458         'tools.sessions.storage_type': 'file',
459         'tools.sessions.timeout': 60
460     }
461
462     # Parse config
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:])
470     o = vars(o)
471     for k in o.keys():
472         if o[k] is None:
473             del(o[k])
474
475     # Setup and run cherrypy
476     cherrypy.tree.mount(Root())
477
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)
484
485     if not os.path.exists(cherrypy.config['tools.sessions.storage_path']):
486         os.makedirs(cherrypy.config['tools.sessions.storage_path'], 0700)
487
488     cherrypy.server.subscribe()
489     cherrypy.engine.start()
490     cherrypy.engine.block()
491