[IMP]: remove stage type in lead and opportunity
[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 assert_valid(self):
111         """
112         Ensures this session is valid (logged into the openerp server)
113         """
114         if not (self._db and self._uid and self._password):
115             raise OpenERPUnboundException()
116
117     def execute(self, model, func, *l, **d):
118         self.assert_valid()
119         r = self.proxy('object').execute(self._db, self._uid, self._password, model, func, *l, **d)
120         return r
121
122     def exec_workflow(self, model, id, signal):
123         self.assert_valid()
124         r = self.proxy('object').exec_workflow(self._db, self._uid, self._password, model, signal, id)
125         return r
126
127     def model(self, model):
128         """ Get an RPC proxy for the object ``model``, bound to this session.
129
130         :param model: an OpenERP model name
131         :type model: str
132         :rtype: :class:`openerpweb.openerpweb.OpenERPModel`
133         """
134         return self.model_factory(self, model)
135
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
139         context
140
141         :returns: the new context
142         """
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 {}
146
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')
151
152         self._locale = self.context.get('lang','en_US')
153         lang_ids = self.execute('res.lang','search', [('code', '=', self._locale)])
154         if lang_ids:
155             self._lang = self.execute('res.lang', 'read',lang_ids[0], [])
156         return self.context
157
158     @property
159     def base_eval_context(self):
160         """ Default evaluation context for the session.
161
162         Used to evaluate contexts and domains.
163         """
164         base = dict(
165             uid=self._uid,
166             current_date=datetime.date.today().strftime('%Y-%m-%d'),
167             time=time,
168             datetime=datetime,
169             relativedelta=dateutil.relativedelta.relativedelta
170         )
171         base.update(self.context)
172         return base
173
174     def evaluation_context(self, context=None):
175         """ Returns the session's evaluation context, augmented with the
176         provided context if any.
177
178         :param dict context: to add merge in the session's base eval context
179         :returns: the augmented context
180         :rtype: dict
181         """
182         d = dict(self.base_eval_context)
183         if context:
184             d.update(context)
185         d['context'] = d
186         return d
187
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.
191
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
194                                 returned as-is
195         :type context_to_eval: openerpweb.nonliterals.Context
196         :returns: the evaluated context
197         :rtype: dict
198
199         :raises: ``TypeError`` if ``context_to_eval`` is neither a dict nor
200                  a Context
201         """
202         ctx = dict(
203             self.base_eval_context,
204             **(context or {}))
205         
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)
210
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)
214
215         :param domain: an OpenERP domain as a list or as a
216                        :class:`openerpweb.nonliterals.Domain` instance
217
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
222         :rtype: list
223
224         :raises: ``TypeError`` if ``domain`` is neither a list nor a Domain
225         """
226         if isinstance(domain, list):
227             return domain
228
229         cdomain = nonliterals.CompoundDomain(domain)
230         cdomain.session = self
231         return cdomain.evaluate(context or {})
232
233 #----------------------------------------------------------
234 # OpenERP Web RequestHandler
235 #----------------------------------------------------------
236 class JsonRequest(object):
237     """ JSON-RPC2 over HTTP.
238
239     Sucessful request::
240
241       --> {"jsonrpc": "2.0",
242            "method": "call",
243            "params": {"session_id": "SID",
244                       "context": {},
245                       "arg1": "val1" },
246            "id": null}
247
248       <-- {"jsonrpc": "2.0",
249            "result": { "res1": "val1" },
250            "id": null}
251
252     Request producing a error::
253
254       --> {"jsonrpc": "2.0",
255            "method": "call",
256            "params": {"session_id": "SID",
257                       "context": {},
258                       "arg1": "val1" },
259            "id": null}
260
261       <-- {"jsonrpc": "2.0",
262            "error": {"code": 1,
263                      "message": "End user error message.",
264                      "data": {"code": "codestring",
265                               "debug": "traceback" } },
266            "id": null}
267
268     """
269
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)
284         return self.params
285
286     def dispatch(self, controller, method, requestf=None, request=None):
287         """ Calls the method asked for by the JSON-RPC2 request
288
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
296         :type request: bytes
297
298         :returns: a string-encoded JSON-RPC2 reply
299         :rtype: bytes
300         """
301         # Read POST content or POST Form Data named "request"
302         if requestf:
303             request = simplejson.load(requestf, object_hook=nonliterals.non_literal_decoder)
304         else:
305             request = simplejson.loads(request, object_hook=nonliterals.non_literal_decoder)
306
307         response = {"jsonrpc": "2.0", "id": request.get('id')}
308         try:
309             print "--> %s.%s %s" % (controller.__class__.__name__, method.__name__, request)
310             error = None
311             self.parse(request)
312             response["result"] = method(controller, self, **self.params)
313         except OpenERPUnboundException:
314             error = {
315                 'code': 100,
316                 'message': "OpenERP Session Invalid",
317                 'data': {
318                     'type': 'session_invalid',
319                     'debug': traceback.format_exc()
320                 }
321             }
322         except xmlrpclib.Fault, e:
323             error = {
324                 'code': 200,
325                 'message': "OpenERP Server Error",
326                 'data': {
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)
331                 }
332             }
333         except Exception:
334             cherrypy.log("An error occured while handling a json request",
335                          severity=logging.ERROR, traceback=True)
336             error = {
337                 'code': 300,
338                 'message': "OpenERP WebClient Error",
339                 'data': {
340                     'type': 'client_exception',
341                     'debug': "Client %s" % traceback.format_exc()
342                 }
343             }
344         if error:
345             response["error"] = error
346
347         print "<--", response
348         print
349
350         content = simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder)
351         cherrypy.response.headers['Content-Type'] = 'application/json'
352         cherrypy.response.headers['Content-Length'] = len(content)
353         return content
354
355 def jsonrequest(f):
356     @cherrypy.expose
357     @functools.wraps(f)
358     def json_handler(controller):
359         return JsonRequest().dispatch(controller, f, requestf=cherrypy.request.body)
360
361     return json_handler
362
363 class HttpRequest(object):
364     """ Regular GET/POST request
365     """
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))
375         self.result = ""
376         if request.method == 'GET':
377             print "GET --> %s.%s %s %r" % (controller.__class__.__name__, f.__name__, request, kw)
378         else:
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'
384         else:
385             print "<--", len(r), 'characters'
386         print
387         return r
388
389 def httprequest(f):
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
395     return http_handler
396
397 #-----------------------------------------------------------
398 # Cherrypy stuff
399 #-----------------------------------------------------------
400
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
405
406 class Controller(object):
407     __metaclass__ = ControllerType
408
409 class Root(object):
410     def __init__(self):
411         self.addons = {}
412         self._load_addons()
413
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())
422                     print "Loading", i
423                     m = __import__(i)
424                     addons_module[i] = m
425                     addons_manifest[i] = manifest
426         for k, v in controllers_class.items():
427             if k not in controllers_object:
428                 o = v()
429                 controllers_object[k] = o
430                 if hasattr(o, '_cp_path'):
431                     controllers_path[o._cp_path] = o
432
433     def default(self, *l, **kw):
434         #print "default",l,kw
435         # handle static files
436         if len(l) > 2 and l[1] == 'static':
437             # sanitize path
438             p = os.path.normpath(os.path.join(*l))
439             return cherrypy.lib.static.serve_file(os.path.join(path_addons, p))
440         elif len(l) > 1:
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']
446                     meth = rest[0]
447                     m = getattr(c, meth)
448                     if getattr(m, 'exposed', 0):
449                         print "Calling", ps, c, meth, m
450                         return m(**kw)
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)
455         else:
456             raise cherrypy.HTTPRedirect('/base/webclient/home', 301)
457     default.exposed = True
458
459 def main(argv):
460     # change the timezone of the program to the OpenERP server's assumed timezone
461     os.environ["TZ"] = "UTC"
462
463     DEFAULT_CONFIG = {
464         'server.socket_host': '0.0.0.0',
465         'tools.sessions.on': True,
466         'tools.sessions.storage_type': 'file',
467         'tools.sessions.timeout': 60
468     }
469
470     # Parse config
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:])
478     o = vars(o)
479     for k in o.keys():
480         if o[k] is None:
481             del(o[k])
482
483     # Setup and run cherrypy
484     cherrypy.tree.mount(Root())
485
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)
492
493     if not os.path.exists(cherrypy.config['tools.sessions.storage_path']):
494         os.makedirs(cherrypy.config['tools.sessions.storage_path'], 0700)
495
496     cherrypy.server.subscribe()
497     cherrypy.engine.start()
498     cherrypy.engine.block()
499