[MERGE] ir_translation: lang field selection only displays installed languages.
[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,
78                  model_factory=OpenERPModel):
79         self._server = server
80         self._port = port
81         self._db = False
82         self._uid = False
83         self._login = False
84         self._password = False
85         self.model_factory = model_factory
86         self._locale = 'en_US'
87         self.context = {}
88         self.contexts_store = {}
89         self.domains_store = {}
90         self._lang = {}
91         self.remote_timezone = 'utc'
92         self.client_timezone = False
93
94     def proxy(self, service):
95         s = xmlrpclib.ServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service))
96         return s
97
98     def bind(self, db, uid, password):
99         self._db = db
100         self._uid = uid
101         self._password = password
102
103     def login(self, db, login, password):
104         uid = self.proxy('common').login(db, login, password)
105         self.bind(db, uid, password)
106         self._login = login
107         
108         if uid: self.get_context()
109         return uid
110
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)
115         return r
116
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)
121         return r
122
123     def model(self, model):
124         """ Get an RPC proxy for the object ``model``, bound to this session.
125
126         :param model: an OpenERP model name
127         :type model: str
128         :rtype: :class:`openerpweb.openerpweb.OpenERPModel`
129         """
130         return self.model_factory(self, model)
131
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
135         context
136
137         :returns: the new context
138         """
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)
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 = {}
178         d.update(self.base_eval_context)
179         if context:
180             d.update(context)
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.
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         if not isinstance(context_to_eval, (dict, nonliterals.Domain)):
198             raise TypeError("Context %r is not a dict or a nonliteral Context",
199                              context_to_eval)
200
201         if isinstance(context_to_eval, dict):
202             return context_to_eval
203
204         ctx = dict(context or {})
205         ctx['context'] = ctx
206
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)
211
212     def eval_contexts(self, contexts, context=None):
213         """ Evaluates a sequence of contexts to build a single final result
214
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
218         :rtype: dict
219         """
220         # This is the context we use to evaluate stuff
221         current_context = dict(
222             self.base_eval_context,
223             **(context or {}))
224         # this is our result, it should not contain the values
225         # of the base context above
226         final_context = {}
227         for ctx in contexts:
228             # evaluate the current context in the sequence, merge it into
229             # the result
230             final_context.update(
231                 self.eval_context(
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)
236         return final_context
237
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)
241
242         :param domain: an OpenERP domain as a list or as a
243                        :class:`openerpweb.nonliterals.Domain` instance
244
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
249         :rtype: list
250
251         :raises: ``TypeError`` if ``domain`` is neither a list nor a Domain
252         """
253         if not isinstance(domain, (list, nonliterals.Domain)):
254             raise TypeError("Domain %r is not a list or a nonliteral Domain",
255                              domain)
256
257         if isinstance(domain, list):
258             return domain
259
260         ctx = dict(context or {})
261         ctx['context'] = ctx
262
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)
267
268     def eval_domains(self, domains, context=None):
269         """ Evaluates and concatenates the provided domains using the
270         provided context for all of them.
271
272         Returns the final, concatenated result.
273
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
277                              to happen)
278         :returns: the final combination of all domains in the sequence
279         :rtype: list
280         """
281         final_domain = []
282         for domain in domains:
283             final_domain.extend(
284                 self.eval_domain(domain, context))
285         return final_domain
286
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:
293
294     * the json string is passed as a form parameter named "request"
295     * method is currently ignored
296
297     Sucessful request::
298
299       --> {"jsonrpc": "2.0",
300            "method": "call",
301            "params": {"session_id": "SID",
302                       "context": {},
303                       "arg1": "val1" },
304            "id": null}
305
306       <-- {"jsonrpc": "2.0",
307            "result": { "res1": "val1" },
308            "id": null}
309
310     Request producing a error::
311
312       --> {"jsonrpc": "2.0",
313            "method": "call",
314            "params": {"session_id": "SID",
315                       "context": {},
316                       "arg1": "val1" },
317            "id": null}
318
319       <-- {"jsonrpc": "2.0",
320            "error": {"code": 1,
321                      "message": "End user error message.",
322                      "data": {"code": "codestring",
323                               "debug": "traceback" } },
324            "id": null}
325
326     """
327
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)
337         return self.params
338
339     def dispatch(self, controller, method, requestf=None, request=None):
340         ''' Calls the method asked for by the JSON-RPC2 request
341
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
349         :type request: bytes
350
351         :returns: a string-encoded JSON-RPC2 reply
352         :rtype: bytes
353         '''
354         # Read POST content or POST Form Data named "request"
355         if requestf:
356             request = simplejson.load(requestf, object_hook=nonliterals.non_literal_decoder)
357         else:
358             request = simplejson.loads(request, object_hook=nonliterals.non_literal_decoder)
359         try:
360             print "--> %s.%s %s" % (controller.__class__.__name__, method.__name__, request)
361             error = None
362             self.parse(request)
363             result = method(controller, self, **self.params)
364         except OpenERPUnboundException:
365             error = {
366                 'code': 100,
367                 'message': "OpenERP Session Invalid",
368                 'data': {
369                     'type': 'session_invalid',
370                     'debug': traceback.format_exc()
371                 }
372             }
373         except xmlrpclib.Fault, e:
374             error = {
375                 'code': 200,
376                 'message': "OpenERP Server Error",
377                 'data': {
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)
382                 }
383             }
384         except Exception:
385             cherrypy.log("An error occured while handling a json request",
386                          severity=logging.ERROR, traceback=True)
387             error = {
388                 'code': 300,
389                 'message': "OpenERP WebClient Error",
390                 'data': {
391                     'type': 'client_exception',
392                     'debug': "Client %s" % traceback.format_exc()
393                 }
394             }
395         response = {"jsonrpc": "2.0", "id": request.get('id')}
396         if error:
397             response["error"] = error
398         else:
399             response["result"] = result
400
401         print "<--", response
402         print
403
404         content = simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder)
405         cherrypy.response.headers['Content-Type'] = 'application/json'
406         cherrypy.response.headers['Content-Length'] = len(content)
407         return content
408
409 def jsonrequest(f):
410     @cherrypy.expose
411     @functools.wraps(f)
412     def json_handler(controller):
413         return JsonRequest().dispatch(controller, f, requestf=cherrypy.request.body)
414
415     return json_handler
416
417 class HttpRequest(object):
418     """ Regular GET/POST request
419     """
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())
427         self.result = ""
428         if request.method == 'GET':
429             print "GET --> %s.%s %s %r" % (controller.__class__.__name__, f.__name__, request, kw)
430         else:
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)
434         print "<--", r
435         print
436         return r
437
438 def httprequest(f):
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
444     return http_handler
445
446 #-----------------------------------------------------------
447 # Cherrypy stuff
448 #-----------------------------------------------------------
449
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
454
455 class Controller(object):
456     __metaclass__ = ControllerType
457
458 class Root(object):
459     def __init__(self):
460         self.addons = {}
461         self._load_addons()
462
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())
471                     print "Loading", i
472                     m = __import__(i)
473                     addons_module[i] = m
474                     addons_manifest[i] = manifest
475         for k, v in controllers_class.items():
476             if k not in controllers_object:
477                 o = v()
478                 controllers_object[k] = o
479                 if hasattr(o, '_cp_path'):
480                     controllers_path[o._cp_path] = o
481
482     def default(self, *l, **kw):
483         #print "default",l,kw
484         # handle static files
485         if len(l) > 2 and l[1] == 'static':
486             # sanitize path
487             p = os.path.normpath(os.path.join(*l))
488             return cherrypy.lib.static.serve_file(os.path.join(path_addons, p))
489         elif len(l) > 1:
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']
495                     meth = rest[0]
496                     m = getattr(c, meth)
497                     if getattr(m, 'exposed', 0):
498                         print "Calling", ps, c, meth, m
499                         return m(**kw)
500             raise cherrypy.NotFound('/' + '/'.join(l))
501         else:
502             raise cherrypy.HTTPRedirect('/base/static/src/base.html', 301)
503     default.exposed = True
504
505 def main(argv):
506     # change the timezone of the program to the OpenERP server's assumed timezone
507     os.environ["TZ"] = "UTC"
508     
509     DEFAULT_CONFIG = {
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
516     }
517     
518     # Parse config
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:])
525     o = vars(o)
526     for k in o.keys():
527         if o[k] == None:
528             del(o[k])
529
530     # Setup and run cherrypy
531     cherrypy.tree.mount(Root())
532     
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)
541     
542     if not os.path.exists(cherrypy.config['tools.sessions.storage_path']):
543         os.mkdir(cherrypy.config['tools.sessions.storage_path'], 0700)
544     
545     cherrypy.server.subscribe()
546     cherrypy.engine.start()
547     cherrypy.engine.block()
548