[IMP] Added method to make the client lib work with jsonrpc.
[odoo/odoo.git] / addons / web / controllers / main.py
1 # -*- coding: utf-8 -*-
2
3 import ast
4 import base64
5 import csv
6 import glob
7 import itertools
8 import logging
9 import operator
10 import datetime
11 import hashlib
12 import os
13 import re
14 import simplejson
15 import time
16 import urllib2
17 import xmlrpclib
18 import zlib
19 from xml.etree import ElementTree
20 from cStringIO import StringIO
21
22 import babel.messages.pofile
23 import werkzeug.utils
24 import werkzeug.wrappers
25 try:
26     import xlwt
27 except ImportError:
28     xlwt = None
29
30 import openerp
31 import openerp.modules.registry
32 from openerp.tools.translate import _
33
34 from .. import http
35 openerpweb = http
36
37 #----------------------------------------------------------
38 # OpenERP Web helpers
39 #----------------------------------------------------------
40
41 def rjsmin(script):
42     """ Minify js with a clever regex.
43     Taken from http://opensource.perlig.de/rjsmin
44     Apache License, Version 2.0 """
45     def subber(match):
46         """ Substitution callback """
47         groups = match.groups()
48         return (
49             groups[0] or
50             groups[1] or
51             groups[2] or
52             groups[3] or
53             (groups[4] and '\n') or
54             (groups[5] and ' ') or
55             (groups[6] and ' ') or
56             (groups[7] and ' ') or
57             ''
58         )
59
60     result = re.sub(
61         r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
62         r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
63         r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
64         r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
65         r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
66         r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
67         r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
68         r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
69         r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
70         r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
71         r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
72         r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
73         r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
74         r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
75         r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
76         r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
77         r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
78         r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
79         r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
80         r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
81         r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
82     ).strip()
83     return result
84
85 def db_list(req):
86     proxy = req.session.proxy("db")
87     dbs = proxy.list()
88     h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
89     d = h.split('.')[0]
90     r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
91     dbs = [i for i in dbs if re.match(r, i)]
92     return dbs
93
94 def db_monodb(req):
95     # if only one db exists, return it else return False
96     try:
97         dbs = db_list(req)
98         if len(dbs) == 1:
99             return dbs[0]
100     except xmlrpclib.Fault:
101         # ignore access denied
102         pass
103     return False
104
105 def module_topological_sort(modules):
106     """ Return a list of module names sorted so that their dependencies of the
107     modules are listed before the module itself
108
109     modules is a dict of {module_name: dependencies}
110
111     :param modules: modules to sort
112     :type modules: dict
113     :returns: list(str)
114     """
115
116     dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
117     # incoming edge: dependency on other module (if a depends on b, a has an
118     # incoming edge from b, aka there's an edge from b to a)
119     # outgoing edge: other module depending on this one
120
121     # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
122     #L ← Empty list that will contain the sorted nodes
123     L = []
124     #S ← Set of all nodes with no outgoing edges (modules on which no other
125     #    module depends)
126     S = set(module for module in modules if module not in dependencies)
127
128     visited = set()
129     #function visit(node n)
130     def visit(n):
131         #if n has not been visited yet then
132         if n not in visited:
133             #mark n as visited
134             visited.add(n)
135             #change: n not web module, can not be resolved, ignore
136             if n not in modules: return
137             #for each node m with an edge from m to n do (dependencies of n)
138             for m in modules[n]:
139                 #visit(m)
140                 visit(m)
141             #add n to L
142             L.append(n)
143     #for each node n in S do
144     for n in S:
145         #visit(n)
146         visit(n)
147     return L
148
149 def module_installed(req):
150     # Candidates module the current heuristic is the /static dir
151     loadable = openerpweb.addons_manifest.keys()
152     modules = {}
153
154     # Retrieve database installed modules
155     # TODO The following code should move to ir.module.module.list_installed_modules()
156     Modules = req.session.model('ir.module.module')
157     domain = [('state','=','installed'), ('name','in', loadable)]
158     for module in Modules.search_read(domain, ['name', 'dependencies_id']):
159         modules[module['name']] = []
160         deps = module.get('dependencies_id')
161         if deps:
162             deps_read = req.session.model('ir.module.module.dependency').read(deps, ['name'])
163             dependencies = [i['name'] for i in deps_read]
164             modules[module['name']] = dependencies
165
166     sorted_modules = module_topological_sort(modules)
167     return sorted_modules
168
169 def module_installed_bypass_session(dbname):
170     loadable = openerpweb.addons_manifest.keys()
171     modules = {}
172     try:
173         registry = openerp.modules.registry.RegistryManager.get(dbname)
174         with registry.cursor() as cr:
175             m = registry.get('ir.module.module')
176             # TODO The following code should move to ir.module.module.list_installed_modules()
177             domain = [('state','=','installed'), ('name','in', loadable)]
178             ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
179             for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
180                 modules[module['name']] = []
181                 deps = module.get('dependencies_id')
182                 if deps:
183                     deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
184                     dependencies = [i['name'] for i in deps_read]
185                     modules[module['name']] = dependencies
186     except Exception,e:
187         pass
188     sorted_modules = module_topological_sort(modules)
189     return sorted_modules
190
191 def module_boot(req, db=None):
192     server_wide_modules = openerp.conf.server_wide_modules or ['web']
193     serverside = []
194     dbside = []
195     for i in server_wide_modules:
196         if i in openerpweb.addons_manifest:
197             serverside.append(i)
198     monodb = db or db_monodb(req)
199     if monodb:
200         dbside = module_installed_bypass_session(monodb)
201         dbside = [i for i in dbside if i not in serverside]
202     addons = serverside + dbside
203     return addons
204
205 def concat_xml(file_list):
206     """Concatenate xml files
207
208     :param list(str) file_list: list of files to check
209     :returns: (concatenation_result, checksum)
210     :rtype: (str, str)
211     """
212     checksum = hashlib.new('sha1')
213     if not file_list:
214         return '', checksum.hexdigest()
215
216     root = None
217     for fname in file_list:
218         with open(fname, 'rb') as fp:
219             contents = fp.read()
220             checksum.update(contents)
221             fp.seek(0)
222             xml = ElementTree.parse(fp).getroot()
223
224         if root is None:
225             root = ElementTree.Element(xml.tag)
226         #elif root.tag != xml.tag:
227         #    raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
228
229         for child in xml.getchildren():
230             root.append(child)
231     return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
232
233 def concat_files(file_list, reader=None, intersperse=""):
234     """ Concatenates contents of all provided files
235
236     :param list(str) file_list: list of files to check
237     :param function reader: reading procedure for each file
238     :param str intersperse: string to intersperse between file contents
239     :returns: (concatenation_result, checksum)
240     :rtype: (str, str)
241     """
242     checksum = hashlib.new('sha1')
243     if not file_list:
244         return '', checksum.hexdigest()
245
246     if reader is None:
247         def reader(f):
248             with open(f, 'rb') as fp:
249                 return fp.read()
250
251     files_content = []
252     for fname in file_list:
253         contents = reader(fname)
254         checksum.update(contents)
255         files_content.append(contents)
256
257     files_concat = intersperse.join(files_content)
258     return files_concat, checksum.hexdigest()
259
260 concat_js_cache = {}
261
262 def concat_js(file_list):
263     content, checksum = concat_files(file_list, intersperse=';')
264     if checksum in concat_js_cache:
265         content = concat_js_cache[checksum]
266     else:
267         content = rjsmin(content)
268         concat_js_cache[checksum] = content
269     return content, checksum
270
271 def fs2web(path):
272     """convert FS path into web path"""
273     return '/'.join(path.split(os.path.sep))
274
275 def manifest_glob(req, extension, addons=None, db=None):
276     if addons is None:
277         addons = module_boot(req, db=db)
278     else:
279         addons = addons.split(',')
280     r = []
281     for addon in addons:
282         manifest = openerpweb.addons_manifest.get(addon, None)
283         if not manifest:
284             continue
285         # ensure does not ends with /
286         addons_path = os.path.join(manifest['addons_path'], '')[:-1]
287         globlist = manifest.get(extension, [])
288         for pattern in globlist:
289             for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
290                 r.append((path, fs2web(path[len(addons_path):])))
291     return r
292
293 def manifest_list(req, extension, mods=None, db=None):
294     if not req.debug:
295         path = '/web/webclient/' + extension
296         if mods is not None:
297             path += '?mods=' + mods
298         elif db:
299             path += '?db=' + db
300         return [path]
301     files = manifest_glob(req, extension, addons=mods, db=db)
302     i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
303                     req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
304     if i_am_diabetic:
305         return [wp for _fp, wp in files]
306     else:
307         return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
308
309 def get_last_modified(files):
310     """ Returns the modification time of the most recently modified
311     file provided
312
313     :param list(str) files: names of files to check
314     :return: most recent modification time amongst the fileset
315     :rtype: datetime.datetime
316     """
317     files = list(files)
318     if files:
319         return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
320                    for f in files)
321     return datetime.datetime(1970, 1, 1)
322
323 def make_conditional(req, response, last_modified=None, etag=None):
324     """ Makes the provided response conditional based upon the request,
325     and mandates revalidation from clients
326
327     Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
328     setting ``last_modified`` and ``etag`` correctly on the response object
329
330     :param req: OpenERP request
331     :type req: web.common.http.WebRequest
332     :param response: Werkzeug response
333     :type response: werkzeug.wrappers.Response
334     :param datetime.datetime last_modified: last modification date of the response content
335     :param str etag: some sort of checksum of the content (deep etag)
336     :return: the response object provided
337     :rtype: werkzeug.wrappers.Response
338     """
339     response.cache_control.must_revalidate = True
340     response.cache_control.max_age = 0
341     if last_modified:
342         response.last_modified = last_modified
343     if etag:
344         response.set_etag(etag)
345     return response.make_conditional(req.httprequest)
346
347 def login_and_redirect(req, db, login, key, redirect_url='/'):
348     wsgienv = req.httprequest.environ
349     env = dict(
350         base_location=req.httprequest.url_root.rstrip('/'),
351         HTTP_HOST=wsgienv['HTTP_HOST'],
352         REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
353     )
354     req.session.authenticate(db, login, key, env)
355     return set_cookie_and_redirect(req, redirect_url)
356
357 def set_cookie_and_redirect(req, redirect_url):
358     redirect = werkzeug.utils.redirect(redirect_url, 303)
359     redirect.autocorrect_location_header = False
360     cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
361     redirect.set_cookie('instance0|session_id', cookie_val)
362     return redirect
363
364 def load_actions_from_ir_values(req, key, key2, models, meta):
365     Values = req.session.model('ir.values')
366     actions = Values.get(key, key2, models, meta, req.context)
367
368     return [(id, name, clean_action(req, action))
369             for id, name, action in actions]
370
371 def clean_action(req, action):
372     action.setdefault('flags', {})
373     action_type = action.setdefault('type', 'ir.actions.act_window_close')
374     if action_type == 'ir.actions.act_window':
375         return fix_view_modes(action)
376     return action
377
378 # I think generate_views,fix_view_modes should go into js ActionManager
379 def generate_views(action):
380     """
381     While the server generates a sequence called "views" computing dependencies
382     between a bunch of stuff for views coming directly from the database
383     (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
384     to return custom view dictionaries generated on the fly.
385
386     In that case, there is no ``views`` key available on the action.
387
388     Since the web client relies on ``action['views']``, generate it here from
389     ``view_mode`` and ``view_id``.
390
391     Currently handles two different cases:
392
393     * no view_id, multiple view_mode
394     * single view_id, single view_mode
395
396     :param dict action: action descriptor dictionary to generate a views key for
397     """
398     view_id = action.get('view_id') or False
399     if isinstance(view_id, (list, tuple)):
400         view_id = view_id[0]
401
402     # providing at least one view mode is a requirement, not an option
403     view_modes = action['view_mode'].split(',')
404
405     if len(view_modes) > 1:
406         if view_id:
407             raise ValueError('Non-db action dictionaries should provide '
408                              'either multiple view modes or a single view '
409                              'mode and an optional view id.\n\n Got view '
410                              'modes %r and view id %r for action %r' % (
411                 view_modes, view_id, action))
412         action['views'] = [(False, mode) for mode in view_modes]
413         return
414     action['views'] = [(view_id, view_modes[0])]
415
416 def fix_view_modes(action):
417     """ For historical reasons, OpenERP has weird dealings in relation to
418     view_mode and the view_type attribute (on window actions):
419
420     * one of the view modes is ``tree``, which stands for both list views
421       and tree views
422     * the choice is made by checking ``view_type``, which is either
423       ``form`` for a list view or ``tree`` for an actual tree view
424
425     This methods simply folds the view_type into view_mode by adding a
426     new view mode ``list`` which is the result of the ``tree`` view_mode
427     in conjunction with the ``form`` view_type.
428
429     TODO: this should go into the doc, some kind of "peculiarities" section
430
431     :param dict action: an action descriptor
432     :returns: nothing, the action is modified in place
433     """
434     if not action.get('views'):
435         generate_views(action)
436
437     if action.pop('view_type', 'form') != 'form':
438         return action
439
440     if 'view_mode' in action:
441         action['view_mode'] = ','.join(
442             mode if mode != 'tree' else 'list'
443             for mode in action['view_mode'].split(','))
444     action['views'] = [
445         [id, mode if mode != 'tree' else 'list']
446         for id, mode in action['views']
447     ]
448
449     return action
450
451 def _local_web_translations(trans_file):
452     messages = []
453     try:
454         with open(trans_file) as t_file:
455             po = babel.messages.pofile.read_po(t_file)
456     except Exception:
457         return
458     for x in po:
459         if x.id and x.string and "openerp-web" in x.auto_comments:
460             messages.append({'id': x.id, 'string': x.string})
461     return messages
462
463 def xml2json_from_elementtree(el, preserve_whitespaces=False):
464     """ xml2json-direct
465     Simple and straightforward XML-to-JSON converter in Python
466     New BSD Licensed
467     http://code.google.com/p/xml2json-direct/
468     """
469     res = {}
470     if el.tag[0] == "{":
471         ns, name = el.tag.rsplit("}", 1)
472         res["tag"] = name
473         res["namespace"] = ns[1:]
474     else:
475         res["tag"] = el.tag
476     res["attrs"] = {}
477     for k, v in el.items():
478         res["attrs"][k] = v
479     kids = []
480     if el.text and (preserve_whitespaces or el.text.strip() != ''):
481         kids.append(el.text)
482     for kid in el:
483         kids.append(xml2json_from_elementtree(kid, preserve_whitespaces))
484         if kid.tail and (preserve_whitespaces or kid.tail.strip() != ''):
485             kids.append(kid.tail)
486     res["children"] = kids
487     return res
488
489 def content_disposition(filename, req):
490     filename = filename.encode('utf8')
491     escaped = urllib2.quote(filename)
492     browser = req.httprequest.user_agent.browser
493     version = int((req.httprequest.user_agent.version or '0').split('.')[0])
494     if browser == 'msie' and version < 9:
495         return "attachment; filename=%s" % escaped
496     elif browser == 'safari':
497         return "attachment; filename=%s" % filename
498     else:
499         return "attachment; filename*=UTF-8''%s" % escaped
500
501
502 #----------------------------------------------------------
503 # OpenERP Web web Controllers
504 #----------------------------------------------------------
505
506 html_template = """<!DOCTYPE html>
507 <html style="height: 100%%">
508     <head>
509         <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
510         <meta http-equiv="content-type" content="text/html; charset=utf-8" />
511         <title>OpenERP</title>
512         <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
513         <link rel="stylesheet" href="/web/static/src/css/full.css" />
514         %(css)s
515         %(js)s
516         <script type="text/javascript">
517             $(function() {
518                 var s = new openerp.init(%(modules)s);
519                 %(init)s
520             });
521         </script>
522     </head>
523     <body>
524         <!--[if lte IE 8]>
525         <script src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
526         <script>CFInstall.check({mode: "overlay"});</script>
527         <![endif]-->
528     </body>
529 </html>
530 """
531
532 class Home(openerpweb.Controller):
533     _cp_path = '/'
534
535     @openerpweb.httprequest
536     def index(self, req, s_action=None, db=None, **kw):
537         js = "\n        ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, 'js', db=db))
538         css = "\n        ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, 'css', db=db))
539
540         r = html_template % {
541             'js': js,
542             'css': css,
543             'modules': simplejson.dumps(module_boot(req, db=db)),
544             'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
545         }
546         return r
547
548     @openerpweb.httprequest
549     def login(self, req, db, login, key):
550         return login_and_redirect(req, db, login, key)
551
552     @openerpweb.jsonrequest
553     def jsonrpc(self, req, service, method, args):
554         """ Method used by client APIs to contact OpenERP. """
555         return getattr(req.session.proxy(service), method)(*args)
556
557 class WebClient(openerpweb.Controller):
558     _cp_path = "/web/webclient"
559
560     @openerpweb.jsonrequest
561     def csslist(self, req, mods=None):
562         return manifest_list(req, 'css', mods=mods)
563
564     @openerpweb.jsonrequest
565     def jslist(self, req, mods=None):
566         return manifest_list(req, 'js', mods=mods)
567
568     @openerpweb.jsonrequest
569     def qweblist(self, req, mods=None):
570         return manifest_list(req, 'qweb', mods=mods)
571
572     @openerpweb.httprequest
573     def css(self, req, mods=None, db=None):
574         files = list(manifest_glob(req, 'css', addons=mods, db=db))
575         last_modified = get_last_modified(f[0] for f in files)
576         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
577             return werkzeug.wrappers.Response(status=304)
578
579         file_map = dict(files)
580
581         rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
582         rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
583
584         def reader(f):
585             """read the a css file and absolutify all relative uris"""
586             with open(f, 'rb') as fp:
587                 data = fp.read().decode('utf-8')
588
589             path = file_map[f]
590             web_dir = os.path.dirname(path)
591
592             data = re.sub(
593                 rx_import,
594                 r"""@import \1%s/""" % (web_dir,),
595                 data,
596             )
597
598             data = re.sub(
599                 rx_url,
600                 r"""url(\1%s/""" % (web_dir,),
601                 data,
602             )
603             return data.encode('utf-8')
604
605         content, checksum = concat_files((f[0] for f in files), reader)
606
607         return make_conditional(
608             req, req.make_response(content, [('Content-Type', 'text/css')]),
609             last_modified, checksum)
610
611     @openerpweb.httprequest
612     def js(self, req, mods=None, db=None):
613         files = [f[0] for f in manifest_glob(req, 'js', addons=mods, db=db)]
614         last_modified = get_last_modified(files)
615         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
616             return werkzeug.wrappers.Response(status=304)
617
618         content, checksum = concat_js(files)
619
620         return make_conditional(
621             req, req.make_response(content, [('Content-Type', 'application/javascript')]),
622             last_modified, checksum)
623
624     @openerpweb.httprequest
625     def qweb(self, req, mods=None, db=None):
626         files = [f[0] for f in manifest_glob(req, 'qweb', addons=mods, db=db)]
627         last_modified = get_last_modified(files)
628         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
629             return werkzeug.wrappers.Response(status=304)
630
631         content, checksum = concat_xml(files)
632
633         return make_conditional(
634             req, req.make_response(content, [('Content-Type', 'text/xml')]),
635             last_modified, checksum)
636
637     @openerpweb.jsonrequest
638     def bootstrap_translations(self, req, mods):
639         """ Load local translations from *.po files, as a temporary solution
640             until we have established a valid session. This is meant only
641             for translating the login page and db management chrome, using
642             the browser's language. """
643         # For performance reasons we only load a single translation, so for
644         # sub-languages (that should only be partially translated) we load the
645         # main language PO instead - that should be enough for the login screen.
646         lang = req.lang.split('_')[0]
647
648         translations_per_module = {}
649         for addon_name in mods:
650             if openerpweb.addons_manifest[addon_name].get('bootstrap'):
651                 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
652                 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
653                 if not os.path.exists(f_name):
654                     continue
655                 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
656
657         return {"modules": translations_per_module,
658                 "lang_parameters": None}
659
660     @openerpweb.jsonrequest
661     def translations(self, req, mods, lang):
662         res_lang = req.session.model('res.lang')
663         ids = res_lang.search([("code", "=", lang)])
664         lang_params = None
665         if ids:
666             lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
667                                                 "grouping", "decimal_point", "thousands_sep"])
668
669         # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
670         # done server-side when the language is loaded, so we only need to load the user's lang.
671         ir_translation = req.session.model('ir.translation')
672         translations_per_module = {}
673         messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
674                                                ('comments','like','openerp-web'),('value','!=',False),
675                                                ('value','!=','')],
676                                               ['module','src','value','lang'], order='module')
677         for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
678             translations_per_module.setdefault(mod,{'messages':[]})
679             translations_per_module[mod]['messages'].extend({'id': m['src'],
680                                                              'string': m['value']} \
681                                                                 for m in msg_group)
682         return {"modules": translations_per_module,
683                 "lang_parameters": lang_params}
684
685     @openerpweb.jsonrequest
686     def version_info(self, req):
687         return openerp.service.web_services.RPC_VERSION_1
688
689 class Proxy(openerpweb.Controller):
690     _cp_path = '/web/proxy'
691
692     @openerpweb.jsonrequest
693     def load(self, req, path):
694         """ Proxies an HTTP request through a JSON request.
695
696         It is strongly recommended to not request binary files through this,
697         as the result will be a binary data blob as well.
698
699         :param req: OpenERP request
700         :param path: actual request path
701         :return: file content
702         """
703         from werkzeug.test import Client
704         from werkzeug.wrappers import BaseResponse
705
706         return Client(req.httprequest.app, BaseResponse).get(path).data
707
708 class Database(openerpweb.Controller):
709     _cp_path = "/web/database"
710
711     @openerpweb.jsonrequest
712     def get_list(self, req):
713         return db_list(req)
714
715     @openerpweb.jsonrequest
716     def create(self, req, fields):
717         params = dict(map(operator.itemgetter('name', 'value'), fields))
718         return req.session.proxy("db").create_database(
719             params['super_admin_pwd'],
720             params['db_name'],
721             bool(params.get('demo_data')),
722             params['db_lang'],
723             params['create_admin_pwd'])
724
725     @openerpweb.jsonrequest
726     def duplicate(self, req, fields):
727         params = dict(map(operator.itemgetter('name', 'value'), fields))
728         return req.session.proxy("db").duplicate_database(
729             params['super_admin_pwd'],
730             params['db_original_name'],
731             params['db_name'])
732
733     @openerpweb.jsonrequest
734     def duplicate(self, req, fields):
735         params = dict(map(operator.itemgetter('name', 'value'), fields))
736         duplicate_attrs = (
737             params['super_admin_pwd'],
738             params['db_original_name'],
739             params['db_name'],
740         )
741
742         return req.session.proxy("db").duplicate_database(*duplicate_attrs)
743
744     @openerpweb.jsonrequest
745     def drop(self, req, fields):
746         password, db = operator.itemgetter(
747             'drop_pwd', 'drop_db')(
748                 dict(map(operator.itemgetter('name', 'value'), fields)))
749
750         try:
751             return req.session.proxy("db").drop(password, db)
752         except xmlrpclib.Fault, e:
753             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
754                 return {'error': e.faultCode, 'title': 'Drop Database'}
755         return {'error': _('Could not drop database !'), 'title': _('Drop Database')}
756
757     @openerpweb.httprequest
758     def backup(self, req, backup_db, backup_pwd, token):
759         try:
760             db_dump = base64.b64decode(
761                 req.session.proxy("db").dump(backup_pwd, backup_db))
762             filename = "%(db)s_%(timestamp)s.dump" % {
763                 'db': backup_db,
764                 'timestamp': datetime.datetime.utcnow().strftime(
765                     "%Y-%m-%d_%H-%M-%SZ")
766             }
767             return req.make_response(db_dump,
768                [('Content-Type', 'application/octet-stream; charset=binary'),
769                ('Content-Disposition', content_disposition(filename, req))],
770                {'fileToken': int(token)}
771             )
772         except xmlrpclib.Fault, e:
773             return simplejson.dumps([[],[{'error': e.faultCode, 'title': _('Backup Database')}]])
774
775     @openerpweb.httprequest
776     def restore(self, req, db_file, restore_pwd, new_db):
777         try:
778             data = base64.b64encode(db_file.read())
779             req.session.proxy("db").restore(restore_pwd, new_db, data)
780             return ''
781         except xmlrpclib.Fault, e:
782             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
783                 raise Exception("AccessDenied")
784
785     @openerpweb.jsonrequest
786     def change_password(self, req, fields):
787         old_password, new_password = operator.itemgetter(
788             'old_pwd', 'new_pwd')(
789                 dict(map(operator.itemgetter('name', 'value'), fields)))
790         try:
791             return req.session.proxy("db").change_admin_password(old_password, new_password)
792         except xmlrpclib.Fault, e:
793             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
794                 return {'error': e.faultCode, 'title': _('Change Password')}
795         return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
796
797 class Session(openerpweb.Controller):
798     _cp_path = "/web/session"
799
800     def session_info(self, req):
801         req.session.ensure_valid()
802         return {
803             "session_id": req.session_id,
804             "uid": req.session._uid,
805             "user_context": req.session.get_context() if req.session._uid else {},
806             "db": req.session._db,
807             "username": req.session._login,
808         }
809
810     @openerpweb.jsonrequest
811     def get_session_info(self, req):
812         return self.session_info(req)
813
814     @openerpweb.jsonrequest
815     def authenticate(self, req, db, login, password, base_location=None):
816         wsgienv = req.httprequest.environ
817         env = dict(
818             base_location=base_location,
819             HTTP_HOST=wsgienv['HTTP_HOST'],
820             REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
821         )
822         req.session.authenticate(db, login, password, env)
823
824         return self.session_info(req)
825
826     @openerpweb.jsonrequest
827     def change_password (self,req,fields):
828         old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
829                 dict(map(operator.itemgetter('name', 'value'), fields)))
830         if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
831             return {'error':_('You cannot leave any password empty.'),'title': _('Change Password')}
832         if new_password != confirm_password:
833             return {'error': _('The new password and its confirmation must be identical.'),'title': _('Change Password')}
834         try:
835             if req.session.model('res.users').change_password(
836                 old_password, new_password):
837                 return {'new_password':new_password}
838         except Exception:
839             return {'error': _('The old password you provided is incorrect, your password was not changed.'), 'title': _('Change Password')}
840         return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
841
842     @openerpweb.jsonrequest
843     def sc_list(self, req):
844         return req.session.model('ir.ui.view_sc').get_sc(
845             req.session._uid, "ir.ui.menu", req.context)
846
847     @openerpweb.jsonrequest
848     def get_lang_list(self, req):
849         try:
850             return req.session.proxy("db").list_lang() or []
851         except Exception, e:
852             return {"error": e, "title": _("Languages")}
853
854     @openerpweb.jsonrequest
855     def modules(self, req):
856         # return all installed modules. Web client is smart enough to not load a module twice
857         return module_installed(req)
858
859     @openerpweb.jsonrequest
860     def save_session_action(self, req, the_action):
861         """
862         This method store an action object in the session object and returns an integer
863         identifying that action. The method get_session_action() can be used to get
864         back the action.
865
866         :param the_action: The action to save in the session.
867         :type the_action: anything
868         :return: A key identifying the saved action.
869         :rtype: integer
870         """
871         saved_actions = req.httpsession.get('saved_actions')
872         if not saved_actions:
873             saved_actions = {"next":0, "actions":{}}
874             req.httpsession['saved_actions'] = saved_actions
875         # we don't allow more than 10 stored actions
876         if len(saved_actions["actions"]) >= 10:
877             del saved_actions["actions"][min(saved_actions["actions"])]
878         key = saved_actions["next"]
879         saved_actions["actions"][key] = the_action
880         saved_actions["next"] = key + 1
881         return key
882
883     @openerpweb.jsonrequest
884     def get_session_action(self, req, key):
885         """
886         Gets back a previously saved action. This method can return None if the action
887         was saved since too much time (this case should be handled in a smart way).
888
889         :param key: The key given by save_session_action()
890         :type key: integer
891         :return: The saved action or None.
892         :rtype: anything
893         """
894         saved_actions = req.httpsession.get('saved_actions')
895         if not saved_actions:
896             return None
897         return saved_actions["actions"].get(key)
898
899     @openerpweb.jsonrequest
900     def check(self, req):
901         req.session.assert_valid()
902         return None
903
904     @openerpweb.jsonrequest
905     def destroy(self, req):
906         req.session._suicide = True
907
908 class Menu(openerpweb.Controller):
909     _cp_path = "/web/menu"
910
911     @openerpweb.jsonrequest
912     def get_user_roots(self, req):
913         """ Return all root menu ids visible for the session user.
914
915         :param req: A request object, with an OpenERP session attribute
916         :type req: < session -> OpenERPSession >
917         :return: the root menu ids
918         :rtype: list(int)
919         """
920         s = req.session
921         Menus = s.model('ir.ui.menu')
922         # If a menu action is defined use its domain to get the root menu items
923         user_menu_id = s.model('res.users').read([s._uid], ['menu_id'],
924                                                  req.context)[0]['menu_id']
925
926         menu_domain = [('parent_id', '=', False)]
927         if user_menu_id:
928             domain_string = s.model('ir.actions.act_window').read(
929                 [user_menu_id[0]], ['domain'],req.context)[0]['domain']
930             if domain_string:
931                 menu_domain = ast.literal_eval(domain_string)
932
933         return Menus.search(menu_domain, 0, False, False, req.context)
934
935     @openerpweb.jsonrequest
936     def load(self, req):
937         """ Loads all menu items (all applications and their sub-menus).
938
939         :param req: A request object, with an OpenERP session attribute
940         :type req: < session -> OpenERPSession >
941         :return: the menu root
942         :rtype: dict('children': menu_nodes)
943         """
944         Menus = req.session.model('ir.ui.menu')
945
946         fields = ['name', 'sequence', 'parent_id', 'action']
947         menu_root_ids = self.get_user_roots(req)
948         menu_roots = Menus.read(menu_root_ids, fields, req.context) if menu_root_ids else []
949         menu_root = {
950             'id': False,
951             'name': 'root',
952             'parent_id': [-1, ''],
953             'children': menu_roots,
954             'all_menu_ids': menu_root_ids,
955         }
956         if not menu_roots:
957             return menu_root
958
959         # menus are loaded fully unlike a regular tree view, cause there are a
960         # limited number of items (752 when all 6.1 addons are installed)
961         menu_ids = Menus.search([('id', 'child_of', menu_root_ids)], 0, False, False, req.context)
962         menu_items = Menus.read(menu_ids, fields, req.context)
963         # adds roots at the end of the sequence, so that they will overwrite
964         # equivalent menu items from full menu read when put into id:item
965         # mapping, resulting in children being correctly set on the roots.
966         menu_items.extend(menu_roots)
967         menu_root['all_menu_ids'] = menu_ids # includes menu_root_ids!
968
969         # make a tree using parent_id
970         menu_items_map = dict(
971             (menu_item["id"], menu_item) for menu_item in menu_items)
972         for menu_item in menu_items:
973             if menu_item['parent_id']:
974                 parent = menu_item['parent_id'][0]
975             else:
976                 parent = False
977             if parent in menu_items_map:
978                 menu_items_map[parent].setdefault(
979                     'children', []).append(menu_item)
980
981         # sort by sequence a tree using parent_id
982         for menu_item in menu_items:
983             menu_item.setdefault('children', []).sort(
984                 key=operator.itemgetter('sequence'))
985
986         return menu_root
987
988     @openerpweb.jsonrequest
989     def load_needaction(self, req, menu_ids):
990         """ Loads needaction counters for specific menu ids.
991
992             :return: needaction data
993             :rtype: dict(menu_id: {'needaction_enabled': boolean, 'needaction_counter': int})
994         """
995         return req.session.model('ir.ui.menu').get_needaction_data(menu_ids, req.context)
996
997     @openerpweb.jsonrequest
998     def action(self, req, menu_id):
999         # still used by web_shortcut
1000         actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1001                                              [('ir.ui.menu', menu_id)], False)
1002         return {"action": actions}
1003
1004 class DataSet(openerpweb.Controller):
1005     _cp_path = "/web/dataset"
1006
1007     @openerpweb.jsonrequest
1008     def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1009         return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1010     def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1011                        , sort=None):
1012         """ Performs a search() followed by a read() (if needed) using the
1013         provided search criteria
1014
1015         :param req: a JSON-RPC request object
1016         :type req: openerpweb.JsonRequest
1017         :param str model: the name of the model to search on
1018         :param fields: a list of the fields to return in the result records
1019         :type fields: [str]
1020         :param int offset: from which index should the results start being returned
1021         :param int limit: the maximum number of records to return
1022         :param list domain: the search domain for the query
1023         :param list sort: sorting directives
1024         :returns: A structure (dict) with two keys: ids (all the ids matching
1025                   the (domain, context) pair) and records (paginated records
1026                   matching fields selection set)
1027         :rtype: list
1028         """
1029         Model = req.session.model(model)
1030
1031         ids = Model.search(domain, offset or 0, limit or False, sort or False,
1032                            req.context)
1033         if limit and len(ids) == limit:
1034             length = Model.search_count(domain, req.context)
1035         else:
1036             length = len(ids) + (offset or 0)
1037         if fields and fields == ['id']:
1038             # shortcut read if we only want the ids
1039             return {
1040                 'length': length,
1041                 'records': [{'id': id} for id in ids]
1042             }
1043
1044         records = Model.read(ids, fields or False, req.context)
1045         records.sort(key=lambda obj: ids.index(obj['id']))
1046         return {
1047             'length': length,
1048             'records': records
1049         }
1050
1051     @openerpweb.jsonrequest
1052     def load(self, req, model, id, fields):
1053         m = req.session.model(model)
1054         value = {}
1055         r = m.read([id], False, req.context)
1056         if r:
1057             value = r[0]
1058         return {'value': value}
1059
1060     def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1061         return self._call_kw(req, model, method, args, {})
1062
1063     def _call_kw(self, req, model, method, args, kwargs):
1064         # Temporary implements future display_name special field for model#read()
1065         if method == 'read' and kwargs.get('context', {}).get('future_display_name'):
1066             if 'display_name' in args[1]:
1067                 names = dict(req.session.model(model).name_get(args[0], **kwargs))
1068                 args[1].remove('display_name')
1069                 records = req.session.model(model).read(*args, **kwargs)
1070                 for record in records:
1071                     record['display_name'] = \
1072                         names.get(record['id']) or "%s#%d" % (model, (record['id']))
1073                 return records
1074
1075         return getattr(req.session.model(model), method)(*args, **kwargs)
1076
1077     @openerpweb.jsonrequest
1078     def call(self, req, model, method, args, domain_id=None, context_id=None):
1079         return self._call_kw(req, model, method, args, {})
1080
1081     @openerpweb.jsonrequest
1082     def call_kw(self, req, model, method, args, kwargs):
1083         return self._call_kw(req, model, method, args, kwargs)
1084
1085     @openerpweb.jsonrequest
1086     def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1087         action = self._call_kw(req, model, method, args, {})
1088         if isinstance(action, dict) and action.get('type') != '':
1089             return clean_action(req, action)
1090         return False
1091
1092     @openerpweb.jsonrequest
1093     def exec_workflow(self, req, model, id, signal):
1094         return req.session.exec_workflow(model, id, signal)
1095
1096     @openerpweb.jsonrequest
1097     def resequence(self, req, model, ids, field='sequence', offset=0):
1098         """ Re-sequences a number of records in the model, by their ids
1099
1100         The re-sequencing starts at the first model of ``ids``, the sequence
1101         number is incremented by one after each record and starts at ``offset``
1102
1103         :param ids: identifiers of the records to resequence, in the new sequence order
1104         :type ids: list(id)
1105         :param str field: field used for sequence specification, defaults to
1106                           "sequence"
1107         :param int offset: sequence number for first record in ``ids``, allows
1108                            starting the resequencing from an arbitrary number,
1109                            defaults to ``0``
1110         """
1111         m = req.session.model(model)
1112         if not m.fields_get([field]):
1113             return False
1114         # python 2.6 has no start parameter
1115         for i, id in enumerate(ids):
1116             m.write(id, { field: i + offset })
1117         return True
1118
1119 class View(openerpweb.Controller):
1120     _cp_path = "/web/view"
1121
1122     @openerpweb.jsonrequest
1123     def add_custom(self, req, view_id, arch):
1124         CustomView = req.session.model('ir.ui.view.custom')
1125         CustomView.create({
1126             'user_id': req.session._uid,
1127             'ref_id': view_id,
1128             'arch': arch
1129         }, req.context)
1130         return {'result': True}
1131
1132     @openerpweb.jsonrequest
1133     def undo_custom(self, req, view_id, reset=False):
1134         CustomView = req.session.model('ir.ui.view.custom')
1135         vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1136                                     0, False, False, req.context)
1137         if vcustom:
1138             if reset:
1139                 CustomView.unlink(vcustom, req.context)
1140             else:
1141                 CustomView.unlink([vcustom[0]], req.context)
1142             return {'result': True}
1143         return {'result': False}
1144
1145 class TreeView(View):
1146     _cp_path = "/web/treeview"
1147
1148     @openerpweb.jsonrequest
1149     def action(self, req, model, id):
1150         return load_actions_from_ir_values(
1151             req,'action', 'tree_but_open',[(model, id)],
1152             False)
1153
1154 class Binary(openerpweb.Controller):
1155     _cp_path = "/web/binary"
1156
1157     @openerpweb.httprequest
1158     def image(self, req, model, id, field, **kw):
1159         last_update = '__last_update'
1160         Model = req.session.model(model)
1161         headers = [('Content-Type', 'image/png')]
1162         etag = req.httprequest.headers.get('If-None-Match')
1163         hashed_session = hashlib.md5(req.session_id).hexdigest()
1164         id = None if not id else simplejson.loads(id)
1165         if type(id) is list:
1166             id = id[0] # m2o
1167         if etag:
1168             if not id and hashed_session == etag:
1169                 return werkzeug.wrappers.Response(status=304)
1170             else:
1171                 date = Model.read([id], [last_update], req.context)[0].get(last_update)
1172                 if hashlib.md5(date).hexdigest() == etag:
1173                     return werkzeug.wrappers.Response(status=304)
1174
1175         retag = hashed_session
1176         try:
1177             if not id:
1178                 res = Model.default_get([field], req.context).get(field)
1179                 image_base64 = res
1180             else:
1181                 res = Model.read([id], [last_update, field], req.context)[0]
1182                 retag = hashlib.md5(res.get(last_update)).hexdigest()
1183                 image_base64 = res.get(field)
1184
1185             if kw.get('resize'):
1186                 resize = kw.get('resize').split(',')
1187                 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1188                     width = int(resize[0])
1189                     height = int(resize[1])
1190                     # resize maximum 500*500
1191                     if width > 500: width = 500
1192                     if height > 500: height = 500
1193                     image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1194
1195             image_data = base64.b64decode(image_base64)
1196
1197         except (TypeError, xmlrpclib.Fault):
1198             image_data = self.placeholder(req)
1199         headers.append(('ETag', retag))
1200         headers.append(('Content-Length', len(image_data)))
1201         try:
1202             ncache = int(kw.get('cache'))
1203             headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1204         except:
1205             pass
1206         return req.make_response(image_data, headers)
1207
1208     def placeholder(self, req, image='placeholder.png'):
1209         addons_path = openerpweb.addons_manifest['web']['addons_path']
1210         return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', image), 'rb').read()
1211
1212     @openerpweb.httprequest
1213     def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1214         """ Download link for files stored as binary fields.
1215
1216         If the ``id`` parameter is omitted, fetches the default value for the
1217         binary field (via ``default_get``), otherwise fetches the field for
1218         that precise record.
1219
1220         :param req: OpenERP request
1221         :type req: :class:`web.common.http.HttpRequest`
1222         :param str model: name of the model to fetch the binary from
1223         :param str field: binary field
1224         :param str id: id of the record from which to fetch the binary
1225         :param str filename_field: field holding the file's name, if any
1226         :returns: :class:`werkzeug.wrappers.Response`
1227         """
1228         Model = req.session.model(model)
1229         fields = [field]
1230         if filename_field:
1231             fields.append(filename_field)
1232         if id:
1233             res = Model.read([int(id)], fields, req.context)[0]
1234         else:
1235             res = Model.default_get(fields, req.context)
1236         filecontent = base64.b64decode(res.get(field, ''))
1237         if not filecontent:
1238             return req.not_found()
1239         else:
1240             filename = '%s_%s' % (model.replace('.', '_'), id)
1241             if filename_field:
1242                 filename = res.get(filename_field, '') or filename
1243             return req.make_response(filecontent,
1244                 [('Content-Type', 'application/octet-stream'),
1245                  ('Content-Disposition', content_disposition(filename, req))])
1246
1247     @openerpweb.httprequest
1248     def saveas_ajax(self, req, data, token):
1249         jdata = simplejson.loads(data)
1250         model = jdata['model']
1251         field = jdata['field']
1252         data = jdata['data']
1253         id = jdata.get('id', None)
1254         filename_field = jdata.get('filename_field', None)
1255         context = jdata.get('context', {})
1256
1257         Model = req.session.model(model)
1258         fields = [field]
1259         if filename_field:
1260             fields.append(filename_field)
1261         if data:
1262             res = { field: data }
1263         elif id:
1264             res = Model.read([int(id)], fields, context)[0]
1265         else:
1266             res = Model.default_get(fields, context)
1267         filecontent = base64.b64decode(res.get(field, ''))
1268         if not filecontent:
1269             raise ValueError(_("No content found for field '%s' on '%s:%s'") %
1270                 (field, model, id))
1271         else:
1272             filename = '%s_%s' % (model.replace('.', '_'), id)
1273             if filename_field:
1274                 filename = res.get(filename_field, '') or filename
1275             return req.make_response(filecontent,
1276                 headers=[('Content-Type', 'application/octet-stream'),
1277                         ('Content-Disposition', content_disposition(filename, req))],
1278                 cookies={'fileToken': int(token)})
1279
1280     @openerpweb.httprequest
1281     def upload(self, req, callback, ufile):
1282         # TODO: might be useful to have a configuration flag for max-length file uploads
1283         out = """<script language="javascript" type="text/javascript">
1284                     var win = window.top.window;
1285                     win.jQuery(win).trigger(%s, %s);
1286                 </script>"""
1287         try:
1288             data = ufile.read()
1289             args = [len(data), ufile.filename,
1290                     ufile.content_type, base64.b64encode(data)]
1291         except Exception, e:
1292             args = [False, e.message]
1293         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1294
1295     @openerpweb.httprequest
1296     def upload_attachment(self, req, callback, model, id, ufile):
1297         Model = req.session.model('ir.attachment')
1298         out = """<script language="javascript" type="text/javascript">
1299                     var win = window.top.window;
1300                     win.jQuery(win).trigger(%s, %s);
1301                 </script>"""
1302         try:
1303             attachment_id = Model.create({
1304                 'name': ufile.filename,
1305                 'datas': base64.encodestring(ufile.read()),
1306                 'datas_fname': ufile.filename,
1307                 'res_model': model,
1308                 'res_id': int(id)
1309             }, req.context)
1310             args = {
1311                 'filename': ufile.filename,
1312                 'id':  attachment_id
1313             }
1314         except xmlrpclib.Fault, e:
1315             args = {'error':e.faultCode }
1316         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1317
1318     @openerpweb.httprequest
1319     def company_logo(self, req, dbname=None):
1320         # TODO add etag, refactor to use /image code for etag
1321         uid = None
1322         if req.session._db:
1323             dbname = req.session._db
1324             uid = req.session._uid
1325         elif dbname is None:
1326             dbname = db_monodb(req)
1327
1328         if uid is None:
1329             uid = openerp.SUPERUSER_ID
1330
1331         if not dbname:
1332             image_data = self.placeholder(req, 'logo.png')
1333         else:
1334             registry = openerp.modules.registry.RegistryManager.get(dbname)
1335             with registry.cursor() as cr:
1336                 user = registry.get('res.users').browse(cr, uid, uid)
1337                 if user.company_id.logo_web:
1338                     image_data = user.company_id.logo_web.decode('base64')
1339                 else:
1340                     image_data = self.placeholder(req, 'nologo.png')
1341         headers = [
1342             ('Content-Type', 'image/png'),
1343             ('Content-Length', len(image_data)),
1344         ]
1345         return req.make_response(image_data, headers)
1346
1347 class Action(openerpweb.Controller):
1348     _cp_path = "/web/action"
1349
1350     @openerpweb.jsonrequest
1351     def load(self, req, action_id, do_not_eval=False):
1352         Actions = req.session.model('ir.actions.actions')
1353         value = False
1354         try:
1355             action_id = int(action_id)
1356         except ValueError:
1357             try:
1358                 module, xmlid = action_id.split('.', 1)
1359                 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1360                 assert model.startswith('ir.actions.')
1361             except Exception:
1362                 action_id = 0   # force failed read
1363
1364         base_action = Actions.read([action_id], ['type'], req.context)
1365         if base_action:
1366             ctx = {}
1367             action_type = base_action[0]['type']
1368             if action_type == 'ir.actions.report.xml':
1369                 ctx.update({'bin_size': True})
1370             ctx.update(req.context)
1371             action = req.session.model(action_type).read([action_id], False, ctx)
1372             if action:
1373                 value = clean_action(req, action[0])
1374         return value
1375
1376     @openerpweb.jsonrequest
1377     def run(self, req, action_id):
1378         return_action = req.session.model('ir.actions.server').run(
1379             [action_id], req.context)
1380         if return_action:
1381             return clean_action(req, return_action)
1382         else:
1383             return False
1384
1385 class Export(View):
1386     _cp_path = "/web/export"
1387
1388     @openerpweb.jsonrequest
1389     def formats(self, req):
1390         """ Returns all valid export formats
1391
1392         :returns: for each export format, a pair of identifier and printable name
1393         :rtype: [(str, str)]
1394         """
1395         return sorted([
1396             controller.fmt
1397             for path, controller in openerpweb.controllers_path.iteritems()
1398             if path.startswith(self._cp_path)
1399             if hasattr(controller, 'fmt')
1400         ], key=operator.itemgetter("label"))
1401
1402     def fields_get(self, req, model):
1403         Model = req.session.model(model)
1404         fields = Model.fields_get(False, req.context)
1405         return fields
1406
1407     @openerpweb.jsonrequest
1408     def get_fields(self, req, model, prefix='', parent_name= '',
1409                    import_compat=True, parent_field_type=None,
1410                    exclude=None):
1411
1412         if import_compat and parent_field_type == "many2one":
1413             fields = {}
1414         else:
1415             fields = self.fields_get(req, model)
1416
1417         if import_compat:
1418             fields.pop('id', None)
1419         else:
1420             fields['.id'] = fields.pop('id', {'string': 'ID'})
1421
1422         fields_sequence = sorted(fields.iteritems(),
1423             key=lambda field: field[1].get('string', ''))
1424
1425         records = []
1426         for field_name, field in fields_sequence:
1427             if import_compat:
1428                 if exclude and field_name in exclude:
1429                     continue
1430                 if field.get('readonly'):
1431                     # If none of the field's states unsets readonly, skip the field
1432                     if all(dict(attrs).get('readonly', True)
1433                            for attrs in field.get('states', {}).values()):
1434                         continue
1435
1436             id = prefix + (prefix and '/'or '') + field_name
1437             name = parent_name + (parent_name and '/' or '') + field['string']
1438             record = {'id': id, 'string': name,
1439                       'value': id, 'children': False,
1440                       'field_type': field.get('type'),
1441                       'required': field.get('required'),
1442                       'relation_field': field.get('relation_field')}
1443             records.append(record)
1444
1445             if len(name.split('/')) < 3 and 'relation' in field:
1446                 ref = field.pop('relation')
1447                 record['value'] += '/id'
1448                 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1449
1450                 if not import_compat or field['type'] == 'one2many':
1451                     # m2m field in import_compat is childless
1452                     record['children'] = True
1453
1454         return records
1455
1456     @openerpweb.jsonrequest
1457     def namelist(self,req,  model, export_id):
1458         # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1459         export = req.session.model("ir.exports").read([export_id])[0]
1460         export_fields_list = req.session.model("ir.exports.line").read(
1461             export['export_fields'])
1462
1463         fields_data = self.fields_info(
1464             req, model, map(operator.itemgetter('name'), export_fields_list))
1465
1466         return [
1467             {'name': field['name'], 'label': fields_data[field['name']]}
1468             for field in export_fields_list
1469         ]
1470
1471     def fields_info(self, req, model, export_fields):
1472         info = {}
1473         fields = self.fields_get(req, model)
1474         if ".id" in export_fields:
1475             fields['.id'] = fields.pop('id', {'string': 'ID'})
1476
1477         # To make fields retrieval more efficient, fetch all sub-fields of a
1478         # given field at the same time. Because the order in the export list is
1479         # arbitrary, this requires ordering all sub-fields of a given field
1480         # together so they can be fetched at the same time
1481         #
1482         # Works the following way:
1483         # * sort the list of fields to export, the default sorting order will
1484         #   put the field itself (if present, for xmlid) and all of its
1485         #   sub-fields right after it
1486         # * then, group on: the first field of the path (which is the same for
1487         #   a field and for its subfields and the length of splitting on the
1488         #   first '/', which basically means grouping the field on one side and
1489         #   all of the subfields on the other. This way, we have the field (for
1490         #   the xmlid) with length 1, and all of the subfields with the same
1491         #   base but a length "flag" of 2
1492         # * if we have a normal field (length 1), just add it to the info
1493         #   mapping (with its string) as-is
1494         # * otherwise, recursively call fields_info via graft_subfields.
1495         #   all graft_subfields does is take the result of fields_info (on the
1496         #   field's model) and prepend the current base (current field), which
1497         #   rebuilds the whole sub-tree for the field
1498         #
1499         # result: because we're not fetching the fields_get for half the
1500         # database models, fetching a namelist with a dozen fields (including
1501         # relational data) falls from ~6s to ~300ms (on the leads model).
1502         # export lists with no sub-fields (e.g. import_compatible lists with
1503         # no o2m) are even more efficient (from the same 6s to ~170ms, as
1504         # there's a single fields_get to execute)
1505         for (base, length), subfields in itertools.groupby(
1506                 sorted(export_fields),
1507                 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1508             subfields = list(subfields)
1509             if length == 2:
1510                 # subfields is a seq of $base/*rest, and not loaded yet
1511                 info.update(self.graft_subfields(
1512                     req, fields[base]['relation'], base, fields[base]['string'],
1513                     subfields
1514                 ))
1515             else:
1516                 info[base] = fields[base]['string']
1517
1518         return info
1519
1520     def graft_subfields(self, req, model, prefix, prefix_string, fields):
1521         export_fields = [field.split('/', 1)[1] for field in fields]
1522         return (
1523             (prefix + '/' + k, prefix_string + '/' + v)
1524             for k, v in self.fields_info(req, model, export_fields).iteritems())
1525
1526     #noinspection PyPropertyDefinition
1527     @property
1528     def content_type(self):
1529         """ Provides the format's content type """
1530         raise NotImplementedError()
1531
1532     def filename(self, base):
1533         """ Creates a valid filename for the format (with extension) from the
1534          provided base name (exension-less)
1535         """
1536         raise NotImplementedError()
1537
1538     def from_data(self, fields, rows):
1539         """ Conversion method from OpenERP's export data to whatever the
1540         current export class outputs
1541
1542         :params list fields: a list of fields to export
1543         :params list rows: a list of records to export
1544         :returns:
1545         :rtype: bytes
1546         """
1547         raise NotImplementedError()
1548
1549     @openerpweb.httprequest
1550     def index(self, req, data, token):
1551         model, fields, ids, domain, import_compat = \
1552             operator.itemgetter('model', 'fields', 'ids', 'domain',
1553                                 'import_compat')(
1554                 simplejson.loads(data))
1555
1556         Model = req.session.model(model)
1557         ids = ids or Model.search(domain, 0, False, False, req.context)
1558
1559         field_names = map(operator.itemgetter('name'), fields)
1560         import_data = Model.export_data(ids, field_names, req.context).get('datas',[])
1561
1562         if import_compat:
1563             columns_headers = field_names
1564         else:
1565             columns_headers = [val['label'].strip() for val in fields]
1566
1567
1568         return req.make_response(self.from_data(columns_headers, import_data),
1569             headers=[('Content-Disposition',
1570                             content_disposition(self.filename(model), req)),
1571                      ('Content-Type', self.content_type)],
1572             cookies={'fileToken': int(token)})
1573
1574 class CSVExport(Export):
1575     _cp_path = '/web/export/csv'
1576     fmt = {'tag': 'csv', 'label': 'CSV'}
1577
1578     @property
1579     def content_type(self):
1580         return 'text/csv;charset=utf8'
1581
1582     def filename(self, base):
1583         return base + '.csv'
1584
1585     def from_data(self, fields, rows):
1586         fp = StringIO()
1587         writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1588
1589         writer.writerow([name.encode('utf-8') for name in fields])
1590
1591         for data in rows:
1592             row = []
1593             for d in data:
1594                 if isinstance(d, basestring):
1595                     d = d.replace('\n',' ').replace('\t',' ')
1596                     try:
1597                         d = d.encode('utf-8')
1598                     except UnicodeError:
1599                         pass
1600                 if d is False: d = None
1601                 row.append(d)
1602             writer.writerow(row)
1603
1604         fp.seek(0)
1605         data = fp.read()
1606         fp.close()
1607         return data
1608
1609 class ExcelExport(Export):
1610     _cp_path = '/web/export/xls'
1611     fmt = {
1612         'tag': 'xls',
1613         'label': 'Excel',
1614         'error': None if xlwt else "XLWT required"
1615     }
1616
1617     @property
1618     def content_type(self):
1619         return 'application/vnd.ms-excel'
1620
1621     def filename(self, base):
1622         return base + '.xls'
1623
1624     def from_data(self, fields, rows):
1625         workbook = xlwt.Workbook()
1626         worksheet = workbook.add_sheet('Sheet 1')
1627
1628         for i, fieldname in enumerate(fields):
1629             worksheet.write(0, i, fieldname)
1630             worksheet.col(i).width = 8000 # around 220 pixels
1631
1632         style = xlwt.easyxf('align: wrap yes')
1633
1634         for row_index, row in enumerate(rows):
1635             for cell_index, cell_value in enumerate(row):
1636                 if isinstance(cell_value, basestring):
1637                     cell_value = re.sub("\r", " ", cell_value)
1638                 if cell_value is False: cell_value = None
1639                 worksheet.write(row_index + 1, cell_index, cell_value, style)
1640
1641         fp = StringIO()
1642         workbook.save(fp)
1643         fp.seek(0)
1644         data = fp.read()
1645         fp.close()
1646         return data
1647
1648 class Reports(View):
1649     _cp_path = "/web/report"
1650     POLLING_DELAY = 0.25
1651     TYPES_MAPPING = {
1652         'doc': 'application/vnd.ms-word',
1653         'html': 'text/html',
1654         'odt': 'application/vnd.oasis.opendocument.text',
1655         'pdf': 'application/pdf',
1656         'sxw': 'application/vnd.sun.xml.writer',
1657         'xls': 'application/vnd.ms-excel',
1658     }
1659
1660     @openerpweb.httprequest
1661     def index(self, req, action, token):
1662         action = simplejson.loads(action)
1663
1664         report_srv = req.session.proxy("report")
1665         context = dict(req.context)
1666         context.update(action["context"])
1667
1668         report_data = {}
1669         report_ids = context["active_ids"]
1670         if 'report_type' in action:
1671             report_data['report_type'] = action['report_type']
1672         if 'datas' in action:
1673             if 'ids' in action['datas']:
1674                 report_ids = action['datas'].pop('ids')
1675             report_data.update(action['datas'])
1676
1677         report_id = report_srv.report(
1678             req.session._db, req.session._uid, req.session._password,
1679             action["report_name"], report_ids,
1680             report_data, context)
1681
1682         report_struct = None
1683         while True:
1684             report_struct = report_srv.report_get(
1685                 req.session._db, req.session._uid, req.session._password, report_id)
1686             if report_struct["state"]:
1687                 break
1688
1689             time.sleep(self.POLLING_DELAY)
1690
1691         report = base64.b64decode(report_struct['result'])
1692         if report_struct.get('code') == 'zlib':
1693             report = zlib.decompress(report)
1694         report_mimetype = self.TYPES_MAPPING.get(
1695             report_struct['format'], 'octet-stream')
1696         file_name = action.get('name', 'report')
1697         if 'name' not in action:
1698             reports = req.session.model('ir.actions.report.xml')
1699             res_id = reports.search([('report_name', '=', action['report_name']),],
1700                                     0, False, False, context)
1701             if len(res_id) > 0:
1702                 file_name = reports.read(res_id[0], ['name'], context)['name']
1703             else:
1704                 file_name = action['report_name']
1705         file_name = '%s.%s' % (file_name, report_struct['format'])
1706
1707         return req.make_response(report,
1708              headers=[
1709                  ('Content-Disposition', content_disposition(file_name, req)),
1710                  ('Content-Type', report_mimetype),
1711                  ('Content-Length', len(report))],
1712              cookies={'fileToken': int(token)})
1713
1714 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: