[IMP] attachment: sidebar rendering of tipsy in html mode, and error fix
[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 from openerp.tools.translate import _
32
33 from .. import http
34 openerpweb = http
35
36 #----------------------------------------------------------
37 # OpenERP Web helpers
38 #----------------------------------------------------------
39
40 def rjsmin(script):
41     """ Minify js with a clever regex.
42     Taken from http://opensource.perlig.de/rjsmin
43     Apache License, Version 2.0 """
44     def subber(match):
45         """ Substitution callback """
46         groups = match.groups()
47         return (
48             groups[0] or
49             groups[1] or
50             groups[2] or
51             groups[3] or
52             (groups[4] and '\n') or
53             (groups[5] and ' ') or
54             (groups[6] and ' ') or
55             (groups[7] and ' ') or
56             ''
57         )
58
59     result = re.sub(
60         r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
61         r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
62         r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
63         r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
64         r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
65         r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
66         r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
67         r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
68         r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
69         r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
70         r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
71         r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
72         r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
73         r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
74         r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
75         r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
76         r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
77         r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
78         r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
79         r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
80         r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
81     ).strip()
82     return result
83
84 def db_list(req):
85     proxy = req.session.proxy("db")
86     dbs = proxy.list()
87     h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
88     d = h.split('.')[0]
89     r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
90     dbs = [i for i in dbs if re.match(r, i)]
91     return dbs
92
93 def db_monodb(req):
94     # if only one db exists, return it else return False
95     try:
96         dbs = db_list(req)
97         if len(dbs) == 1:
98             return dbs[0]
99     except xmlrpclib.Fault:
100         # ignore access denied
101         pass
102     return False
103
104 def module_topological_sort(modules):
105     """ Return a list of module names sorted so that their dependencies of the
106     modules are listed before the module itself
107
108     modules is a dict of {module_name: dependencies}
109
110     :param modules: modules to sort
111     :type modules: dict
112     :returns: list(str)
113     """
114
115     dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
116     # incoming edge: dependency on other module (if a depends on b, a has an
117     # incoming edge from b, aka there's an edge from b to a)
118     # outgoing edge: other module depending on this one
119
120     # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
121     #L ← Empty list that will contain the sorted nodes
122     L = []
123     #S ← Set of all nodes with no outgoing edges (modules on which no other
124     #    module depends)
125     S = set(module for module in modules if module not in dependencies)
126
127     visited = set()
128     #function visit(node n)
129     def visit(n):
130         #if n has not been visited yet then
131         if n not in visited:
132             #mark n as visited
133             visited.add(n)
134             #change: n not web module, can not be resolved, ignore
135             if n not in modules: return
136             #for each node m with an edge from m to n do (dependencies of n)
137             for m in modules[n]:
138                 #visit(m)
139                 visit(m)
140             #add n to L
141             L.append(n)
142     #for each node n in S do
143     for n in S:
144         #visit(n)
145         visit(n)
146     return L
147
148 def module_installed(req):
149     # Candidates module the current heuristic is the /static dir
150     loadable = openerpweb.addons_manifest.keys()
151     modules = {}
152
153     # Retrieve database installed modules
154     # TODO The following code should move to ir.module.module.list_installed_modules()
155     Modules = req.session.model('ir.module.module')
156     domain = [('state','=','installed'), ('name','in', loadable)]
157     for module in Modules.search_read(domain, ['name', 'dependencies_id']):
158         modules[module['name']] = []
159         deps = module.get('dependencies_id')
160         if deps:
161             deps_read = req.session.model('ir.module.module.dependency').read(deps, ['name'])
162             dependencies = [i['name'] for i in deps_read]
163             modules[module['name']] = dependencies
164
165     sorted_modules = module_topological_sort(modules)
166     return sorted_modules
167
168 def module_installed_bypass_session(dbname):
169     loadable = openerpweb.addons_manifest.keys()
170     modules = {}
171     try:
172         import openerp.modules.registry
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>
527             var test = function() {
528                 CFInstall.check({
529                     mode: "overlay"
530                 });
531             };
532             if (window.localStorage && false) {
533                 if (! localStorage.getItem("hasShownGFramePopup")) {
534                     test();
535                     localStorage.setItem("hasShownGFramePopup", true);
536                 }
537             } else {
538                 test();
539             }
540         </script>
541         <![endif]-->
542     </body>
543 </html>
544 """
545
546 class Home(openerpweb.Controller):
547     _cp_path = '/'
548
549     @openerpweb.httprequest
550     def index(self, req, s_action=None, db=None, **kw):
551         js = "\n        ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, 'js', db=db))
552         css = "\n        ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, 'css', db=db))
553
554         r = html_template % {
555             'js': js,
556             'css': css,
557             'modules': simplejson.dumps(module_boot(req, db=db)),
558             'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
559         }
560         return r
561
562     @openerpweb.httprequest
563     def login(self, req, db, login, key):
564         return login_and_redirect(req, db, login, key)
565
566 class WebClient(openerpweb.Controller):
567     _cp_path = "/web/webclient"
568
569     @openerpweb.jsonrequest
570     def csslist(self, req, mods=None):
571         return manifest_list(req, 'css', mods=mods)
572
573     @openerpweb.jsonrequest
574     def jslist(self, req, mods=None):
575         return manifest_list(req, 'js', mods=mods)
576
577     @openerpweb.jsonrequest
578     def qweblist(self, req, mods=None):
579         return manifest_list(req, 'qweb', mods=mods)
580
581     @openerpweb.httprequest
582     def css(self, req, mods=None, db=None):
583         files = list(manifest_glob(req, 'css', addons=mods, db=db))
584         last_modified = get_last_modified(f[0] for f in files)
585         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
586             return werkzeug.wrappers.Response(status=304)
587
588         file_map = dict(files)
589
590         rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
591         rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
592
593         def reader(f):
594             """read the a css file and absolutify all relative uris"""
595             with open(f, 'rb') as fp:
596                 data = fp.read().decode('utf-8')
597
598             path = file_map[f]
599             web_dir = os.path.dirname(path)
600
601             data = re.sub(
602                 rx_import,
603                 r"""@import \1%s/""" % (web_dir,),
604                 data,
605             )
606
607             data = re.sub(
608                 rx_url,
609                 r"""url(\1%s/""" % (web_dir,),
610                 data,
611             )
612             return data.encode('utf-8')
613
614         content, checksum = concat_files((f[0] for f in files), reader)
615
616         return make_conditional(
617             req, req.make_response(content, [('Content-Type', 'text/css')]),
618             last_modified, checksum)
619
620     @openerpweb.httprequest
621     def js(self, req, mods=None, db=None):
622         files = [f[0] for f in manifest_glob(req, 'js', addons=mods, db=db)]
623         last_modified = get_last_modified(files)
624         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
625             return werkzeug.wrappers.Response(status=304)
626
627         content, checksum = concat_js(files)
628
629         return make_conditional(
630             req, req.make_response(content, [('Content-Type', 'application/javascript')]),
631             last_modified, checksum)
632
633     @openerpweb.httprequest
634     def qweb(self, req, mods=None, db=None):
635         files = [f[0] for f in manifest_glob(req, 'qweb', addons=mods, db=db)]
636         last_modified = get_last_modified(files)
637         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
638             return werkzeug.wrappers.Response(status=304)
639
640         content, checksum = concat_xml(files)
641
642         return make_conditional(
643             req, req.make_response(content, [('Content-Type', 'text/xml')]),
644             last_modified, checksum)
645
646     @openerpweb.jsonrequest
647     def bootstrap_translations(self, req, mods):
648         """ Load local translations from *.po files, as a temporary solution
649             until we have established a valid session. This is meant only
650             for translating the login page and db management chrome, using
651             the browser's language. """
652         # For performance reasons we only load a single translation, so for
653         # sub-languages (that should only be partially translated) we load the
654         # main language PO instead - that should be enough for the login screen.
655         lang = req.lang.split('_')[0]
656
657         translations_per_module = {}
658         for addon_name in mods:
659             if openerpweb.addons_manifest[addon_name].get('bootstrap'):
660                 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
661                 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
662                 if not os.path.exists(f_name):
663                     continue
664                 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
665
666         return {"modules": translations_per_module,
667                 "lang_parameters": None}
668
669     @openerpweb.jsonrequest
670     def translations(self, req, mods, lang):
671         res_lang = req.session.model('res.lang')
672         ids = res_lang.search([("code", "=", lang)])
673         lang_params = None
674         if ids:
675             lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
676                                                 "grouping", "decimal_point", "thousands_sep"])
677
678         # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
679         # done server-side when the language is loaded, so we only need to load the user's lang.
680         ir_translation = req.session.model('ir.translation')
681         translations_per_module = {}
682         messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
683                                                ('comments','like','openerp-web'),('value','!=',False),
684                                                ('value','!=','')],
685                                               ['module','src','value','lang'], order='module') 
686         for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
687             translations_per_module.setdefault(mod,{'messages':[]})
688             translations_per_module[mod]['messages'].extend({'id': m['src'],
689                                                              'string': m['value']} \
690                                                                 for m in msg_group)
691         return {"modules": translations_per_module,
692                 "lang_parameters": lang_params}
693
694     @openerpweb.jsonrequest
695     def version_info(self, req):
696         return {
697             "version": openerp.release.version
698         }
699
700 class Proxy(openerpweb.Controller):
701     _cp_path = '/web/proxy'
702
703     @openerpweb.jsonrequest
704     def load(self, req, path):
705         """ Proxies an HTTP request through a JSON request.
706
707         It is strongly recommended to not request binary files through this,
708         as the result will be a binary data blob as well.
709
710         :param req: OpenERP request
711         :param path: actual request path
712         :return: file content
713         """
714         from werkzeug.test import Client
715         from werkzeug.wrappers import BaseResponse
716
717         return Client(req.httprequest.app, BaseResponse).get(path).data
718
719 class Database(openerpweb.Controller):
720     _cp_path = "/web/database"
721
722     @openerpweb.jsonrequest
723     def get_list(self, req):
724         return db_list(req)
725
726     @openerpweb.jsonrequest
727     def create(self, req, fields):
728         params = dict(map(operator.itemgetter('name', 'value'), fields))
729         return req.session.proxy("db").create_database(
730             params['super_admin_pwd'],
731             params['db_name'],
732             bool(params.get('demo_data')),
733             params['db_lang'],
734             params['create_admin_pwd'])
735
736     @openerpweb.jsonrequest
737     def duplicate(self, req, fields):
738         params = dict(map(operator.itemgetter('name', 'value'), fields))
739         return req.session.proxy("db").duplicate_database(
740             params['super_admin_pwd'],
741             params['db_original_name'],
742             params['db_name'])
743
744     @openerpweb.jsonrequest
745     def duplicate(self, req, fields):
746         params = dict(map(operator.itemgetter('name', 'value'), fields))
747         duplicate_attrs = (
748             params['super_admin_pwd'],
749             params['db_original_name'],
750             params['db_name'],
751         )
752
753         return req.session.proxy("db").duplicate_database(*duplicate_attrs)
754
755     @openerpweb.jsonrequest
756     def drop(self, req, fields):
757         password, db = operator.itemgetter(
758             'drop_pwd', 'drop_db')(
759                 dict(map(operator.itemgetter('name', 'value'), fields)))
760
761         try:
762             return req.session.proxy("db").drop(password, db)
763         except xmlrpclib.Fault, e:
764             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
765                 return {'error': e.faultCode, 'title': 'Drop Database'}
766         return {'error': _('Could not drop database !'), 'title': _('Drop Database')}
767
768     @openerpweb.httprequest
769     def backup(self, req, backup_db, backup_pwd, token):
770         try:
771             db_dump = base64.b64decode(
772                 req.session.proxy("db").dump(backup_pwd, backup_db))
773             filename = "%(db)s_%(timestamp)s.dump" % {
774                 'db': backup_db,
775                 'timestamp': datetime.datetime.utcnow().strftime(
776                     "%Y-%m-%d_%H-%M-%SZ")
777             }
778             return req.make_response(db_dump,
779                [('Content-Type', 'application/octet-stream; charset=binary'),
780                ('Content-Disposition', content_disposition(filename, req))],
781                {'fileToken': int(token)}
782             )
783         except xmlrpclib.Fault, e:
784             return simplejson.dumps([[],[{'error': e.faultCode, 'title': _('Backup Database')}]])
785
786     @openerpweb.httprequest
787     def restore(self, req, db_file, restore_pwd, new_db):
788         try:
789             data = base64.b64encode(db_file.read())
790             req.session.proxy("db").restore(restore_pwd, new_db, data)
791             return ''
792         except xmlrpclib.Fault, e:
793             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
794                 raise Exception("AccessDenied")
795
796     @openerpweb.jsonrequest
797     def change_password(self, req, fields):
798         old_password, new_password = operator.itemgetter(
799             'old_pwd', 'new_pwd')(
800                 dict(map(operator.itemgetter('name', 'value'), fields)))
801         try:
802             return req.session.proxy("db").change_admin_password(old_password, new_password)
803         except xmlrpclib.Fault, e:
804             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
805                 return {'error': e.faultCode, 'title': _('Change Password')}
806         return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
807
808 class Session(openerpweb.Controller):
809     _cp_path = "/web/session"
810
811     def session_info(self, req):
812         req.session.ensure_valid()
813         return {
814             "session_id": req.session_id,
815             "uid": req.session._uid,
816             "context": req.session.get_context() if req.session._uid else {},
817             "db": req.session._db,
818             "login": req.session._login,
819         }
820
821     @openerpweb.jsonrequest
822     def get_session_info(self, req):
823         return self.session_info(req)
824
825     @openerpweb.jsonrequest
826     def authenticate(self, req, db, login, password, base_location=None):
827         wsgienv = req.httprequest.environ
828         env = dict(
829             base_location=base_location,
830             HTTP_HOST=wsgienv['HTTP_HOST'],
831             REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
832         )
833         req.session.authenticate(db, login, password, env)
834
835         return self.session_info(req)
836
837     @openerpweb.jsonrequest
838     def change_password (self,req,fields):
839         old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
840                 dict(map(operator.itemgetter('name', 'value'), fields)))
841         if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
842             return {'error':_('You cannot leave any password empty.'),'title': _('Change Password')}
843         if new_password != confirm_password:
844             return {'error': _('The new password and its confirmation must be identical.'),'title': _('Change Password')}
845         try:
846             if req.session.model('res.users').change_password(
847                 old_password, new_password):
848                 return {'new_password':new_password}
849         except Exception:
850             return {'error': _('The old password you provided is incorrect, your password was not changed.'), 'title': _('Change Password')}
851         return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
852
853     @openerpweb.jsonrequest
854     def sc_list(self, req):
855         return req.session.model('ir.ui.view_sc').get_sc(
856             req.session._uid, "ir.ui.menu", req.context)
857
858     @openerpweb.jsonrequest
859     def get_lang_list(self, req):
860         try:
861             return req.session.proxy("db").list_lang() or []
862         except Exception, e:
863             return {"error": e, "title": _("Languages")}
864
865     @openerpweb.jsonrequest
866     def modules(self, req):
867         # return all installed modules. Web client is smart enough to not load a module twice
868         return module_installed(req)
869
870     @openerpweb.jsonrequest
871     def save_session_action(self, req, the_action):
872         """
873         This method store an action object in the session object and returns an integer
874         identifying that action. The method get_session_action() can be used to get
875         back the action.
876
877         :param the_action: The action to save in the session.
878         :type the_action: anything
879         :return: A key identifying the saved action.
880         :rtype: integer
881         """
882         saved_actions = req.httpsession.get('saved_actions')
883         if not saved_actions:
884             saved_actions = {"next":0, "actions":{}}
885             req.httpsession['saved_actions'] = saved_actions
886         # we don't allow more than 10 stored actions
887         if len(saved_actions["actions"]) >= 10:
888             del saved_actions["actions"][min(saved_actions["actions"])]
889         key = saved_actions["next"]
890         saved_actions["actions"][key] = the_action
891         saved_actions["next"] = key + 1
892         return key
893
894     @openerpweb.jsonrequest
895     def get_session_action(self, req, key):
896         """
897         Gets back a previously saved action. This method can return None if the action
898         was saved since too much time (this case should be handled in a smart way).
899
900         :param key: The key given by save_session_action()
901         :type key: integer
902         :return: The saved action or None.
903         :rtype: anything
904         """
905         saved_actions = req.httpsession.get('saved_actions')
906         if not saved_actions:
907             return None
908         return saved_actions["actions"].get(key)
909
910     @openerpweb.jsonrequest
911     def check(self, req):
912         req.session.assert_valid()
913         return None
914
915     @openerpweb.jsonrequest
916     def destroy(self, req):
917         req.session._suicide = True
918
919 class Menu(openerpweb.Controller):
920     _cp_path = "/web/menu"
921
922     @openerpweb.jsonrequest
923     def load(self, req):
924         return {'data': self.do_load(req)}
925
926     @openerpweb.jsonrequest
927     def get_user_roots(self, req):
928         return self.do_get_user_roots(req)
929
930     def do_get_user_roots(self, req):
931         """ Return all root menu ids visible for the session user.
932
933         :param req: A request object, with an OpenERP session attribute
934         :type req: < session -> OpenERPSession >
935         :return: the root menu ids
936         :rtype: list(int)
937         """
938         s = req.session
939         Menus = s.model('ir.ui.menu')
940         # If a menu action is defined use its domain to get the root menu items
941         user_menu_id = s.model('res.users').read([s._uid], ['menu_id'],
942                                                  req.context)[0]['menu_id']
943
944         menu_domain = [('parent_id', '=', False)]
945         if user_menu_id:
946             domain_string = s.model('ir.actions.act_window').read(
947                 [user_menu_id[0]], ['domain'],req.context)[0]['domain']
948             if domain_string:
949                 menu_domain = ast.literal_eval(domain_string)
950
951         return Menus.search(menu_domain, 0, False, False, req.context)
952
953     def do_load(self, req):
954         """ Loads all menu items (all applications and their sub-menus).
955
956         :param req: A request object, with an OpenERP session attribute
957         :type req: < session -> OpenERPSession >
958         :return: the menu root
959         :rtype: dict('children': menu_nodes)
960         """
961         Menus = req.session.model('ir.ui.menu')
962
963         fields = ['name', 'sequence', 'parent_id', 'action',
964                   'needaction_enabled', 'needaction_counter']
965         menu_roots = Menus.read(self.do_get_user_roots(req), fields, req.context)
966         menu_root = {
967             'id': False,
968             'name': 'root',
969             'parent_id': [-1, ''],
970             'children': menu_roots
971         }
972
973         # menus are loaded fully unlike a regular tree view, cause there are a
974         # limited number of items (752 when all 6.1 addons are installed)
975         menu_ids = Menus.search([], 0, False, False, req.context)
976         menu_items = Menus.read(menu_ids, fields, req.context)
977         # adds roots at the end of the sequence, so that they will overwrite
978         # equivalent menu items from full menu read when put into id:item
979         # mapping, resulting in children being correctly set on the roots.
980         menu_items.extend(menu_roots)
981
982         # make a tree using parent_id
983         menu_items_map = dict(
984             (menu_item["id"], menu_item) for menu_item in menu_items)
985         for menu_item in menu_items:
986             if menu_item['parent_id']:
987                 parent = menu_item['parent_id'][0]
988             else:
989                 parent = False
990             if parent in menu_items_map:
991                 menu_items_map[parent].setdefault(
992                     'children', []).append(menu_item)
993
994         # sort by sequence a tree using parent_id
995         for menu_item in menu_items:
996             menu_item.setdefault('children', []).sort(
997                 key=operator.itemgetter('sequence'))
998
999         return menu_root
1000
1001     @openerpweb.jsonrequest
1002     def action(self, req, menu_id):
1003         actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1004                                              [('ir.ui.menu', menu_id)], False)
1005         return {"action": actions}
1006
1007 class DataSet(openerpweb.Controller):
1008     _cp_path = "/web/dataset"
1009
1010     @openerpweb.jsonrequest
1011     def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1012         return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1013     def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1014                        , sort=None):
1015         """ Performs a search() followed by a read() (if needed) using the
1016         provided search criteria
1017
1018         :param req: a JSON-RPC request object
1019         :type req: openerpweb.JsonRequest
1020         :param str model: the name of the model to search on
1021         :param fields: a list of the fields to return in the result records
1022         :type fields: [str]
1023         :param int offset: from which index should the results start being returned
1024         :param int limit: the maximum number of records to return
1025         :param list domain: the search domain for the query
1026         :param list sort: sorting directives
1027         :returns: A structure (dict) with two keys: ids (all the ids matching
1028                   the (domain, context) pair) and records (paginated records
1029                   matching fields selection set)
1030         :rtype: list
1031         """
1032         Model = req.session.model(model)
1033
1034         ids = Model.search(domain, offset or 0, limit or False, sort or False,
1035                            req.context)
1036         if limit and len(ids) == limit:
1037             length = Model.search_count(domain, req.context)
1038         else:
1039             length = len(ids) + (offset or 0)
1040         if fields and fields == ['id']:
1041             # shortcut read if we only want the ids
1042             return {
1043                 'length': length,
1044                 'records': [{'id': id} for id in ids]
1045             }
1046
1047         records = Model.read(ids, fields or False, req.context)
1048         records.sort(key=lambda obj: ids.index(obj['id']))
1049         return {
1050             'length': length,
1051             'records': records
1052         }
1053
1054     @openerpweb.jsonrequest
1055     def load(self, req, model, id, fields):
1056         m = req.session.model(model)
1057         value = {}
1058         r = m.read([id], False, req.context)
1059         if r:
1060             value = r[0]
1061         return {'value': value}
1062
1063     def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1064         return self._call_kw(req, model, method, args, {})
1065
1066     def _call_kw(self, req, model, method, args, kwargs):
1067         # Temporary implements future display_name special field for model#read()
1068         if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1069             if 'display_name' in args[1]:
1070                 names = req.session.model(model).name_get(args[0], **kwargs)
1071                 args[1].remove('display_name')
1072                 r = getattr(req.session.model(model), method)(*args, **kwargs)
1073                 for i in range(len(r)):
1074                     r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1075                 return r
1076
1077         return getattr(req.session.model(model), method)(*args, **kwargs)
1078
1079     @openerpweb.jsonrequest
1080     def call(self, req, model, method, args, domain_id=None, context_id=None):
1081         return self._call_kw(req, model, method, args, {})
1082     
1083     @openerpweb.jsonrequest
1084     def call_kw(self, req, model, method, args, kwargs):
1085         return self._call_kw(req, model, method, args, kwargs)
1086
1087     @openerpweb.jsonrequest
1088     def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1089         action = self._call_kw(req, model, method, args, {})
1090         if isinstance(action, dict) and action.get('type') != '':
1091             return clean_action(req, action)
1092         return False
1093
1094     @openerpweb.jsonrequest
1095     def exec_workflow(self, req, model, id, signal):
1096         return req.session.exec_workflow(model, id, signal)
1097
1098     @openerpweb.jsonrequest
1099     def resequence(self, req, model, ids, field='sequence', offset=0):
1100         """ Re-sequences a number of records in the model, by their ids
1101
1102         The re-sequencing starts at the first model of ``ids``, the sequence
1103         number is incremented by one after each record and starts at ``offset``
1104
1105         :param ids: identifiers of the records to resequence, in the new sequence order
1106         :type ids: list(id)
1107         :param str field: field used for sequence specification, defaults to
1108                           "sequence"
1109         :param int offset: sequence number for first record in ``ids``, allows
1110                            starting the resequencing from an arbitrary number,
1111                            defaults to ``0``
1112         """
1113         m = req.session.model(model)
1114         if not m.fields_get([field]):
1115             return False
1116         # python 2.6 has no start parameter
1117         for i, id in enumerate(ids):
1118             m.write(id, { field: i + offset })
1119         return True
1120
1121 class View(openerpweb.Controller):
1122     _cp_path = "/web/view"
1123
1124     def fields_view_get(self, req, model, view_id, view_type,
1125                         transform=True, toolbar=False, submenu=False):
1126         Model = req.session.model(model)
1127         fvg = Model.fields_view_get(view_id, view_type, req.context, toolbar, submenu)
1128         # todo fme?: check that we should pass the evaluated context here
1129         self.process_view(req.session, fvg, req.context, transform, (view_type == 'kanban'))
1130         return fvg
1131
1132     def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1133         # depending on how it feels, xmlrpclib.ServerProxy can translate
1134         # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1135         # enjoy unicode strings which can not be trivially converted to
1136         # strings, and it blows up during parsing.
1137
1138         # So ensure we fix this retardation by converting view xml back to
1139         # bit strings.
1140         if isinstance(fvg['arch'], unicode):
1141             arch = fvg['arch'].encode('utf-8')
1142         else:
1143             arch = fvg['arch']
1144         fvg['arch_string'] = arch
1145
1146         fvg['arch'] = xml2json_from_elementtree(
1147             ElementTree.fromstring(arch), preserve_whitespaces)
1148
1149         if 'id' in fvg['fields']:
1150             # Special case for id's
1151             id_field = fvg['fields']['id']
1152             id_field['original_type'] = id_field['type']
1153             id_field['type'] = 'id'
1154
1155         for field in fvg['fields'].itervalues():
1156             for view in field.get("views", {}).itervalues():
1157                 self.process_view(session, view, None, transform)
1158
1159     @openerpweb.jsonrequest
1160     def add_custom(self, req, view_id, arch):
1161         CustomView = req.session.model('ir.ui.view.custom')
1162         CustomView.create({
1163             'user_id': req.session._uid,
1164             'ref_id': view_id,
1165             'arch': arch
1166         }, req.context)
1167         return {'result': True}
1168
1169     @openerpweb.jsonrequest
1170     def undo_custom(self, req, view_id, reset=False):
1171         CustomView = req.session.model('ir.ui.view.custom')
1172         vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1173                                     0, False, False, req.context)
1174         if vcustom:
1175             if reset:
1176                 CustomView.unlink(vcustom, req.context)
1177             else:
1178                 CustomView.unlink([vcustom[0]], req.context)
1179             return {'result': True}
1180         return {'result': False}
1181
1182     @openerpweb.jsonrequest
1183     def load(self, req, model, view_id, view_type, toolbar=False):
1184         return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1185
1186 class TreeView(View):
1187     _cp_path = "/web/treeview"
1188
1189     @openerpweb.jsonrequest
1190     def action(self, req, model, id):
1191         return load_actions_from_ir_values(
1192             req,'action', 'tree_but_open',[(model, id)],
1193             False)
1194
1195 class Binary(openerpweb.Controller):
1196     _cp_path = "/web/binary"
1197
1198     @openerpweb.httprequest
1199     def image(self, req, model, id, field, **kw):
1200         last_update = '__last_update'
1201         Model = req.session.model(model)
1202         headers = [('Content-Type', 'image/png')]
1203         etag = req.httprequest.headers.get('If-None-Match')
1204         hashed_session = hashlib.md5(req.session_id).hexdigest()
1205         id = None if not id else simplejson.loads(id)
1206         if type(id) is list:
1207             id = id[0] # m2o
1208         if etag:
1209             if not id and hashed_session == etag:
1210                 return werkzeug.wrappers.Response(status=304)
1211             else:
1212                 date = Model.read([id], [last_update], req.context)[0].get(last_update)
1213                 if hashlib.md5(date).hexdigest() == etag:
1214                     return werkzeug.wrappers.Response(status=304)
1215
1216         retag = hashed_session
1217         try:
1218             if not id:
1219                 res = Model.default_get([field], req.context).get(field)
1220                 image_base64 = res
1221             else:
1222                 res = Model.read([id], [last_update, field], req.context)[0]
1223                 retag = hashlib.md5(res.get(last_update)).hexdigest()
1224                 image_base64 = res.get(field)
1225
1226             if kw.get('resize'):
1227                 resize = kw.get('resize').split(',')
1228                 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1229                     width = int(resize[0])
1230                     height = int(resize[1])
1231                     # resize maximum 500*500
1232                     if width > 500: width = 500
1233                     if height > 500: height = 500
1234                     image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1235             
1236             image_data = base64.b64decode(image_base64)
1237
1238         except (TypeError, xmlrpclib.Fault):
1239             image_data = self.placeholder(req)
1240         headers.append(('ETag', retag))
1241         headers.append(('Content-Length', len(image_data)))
1242         try:
1243             ncache = int(kw.get('cache'))
1244             headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1245         except:
1246             pass
1247         return req.make_response(image_data, headers)
1248     def placeholder(self, req):
1249         addons_path = openerpweb.addons_manifest['web']['addons_path']
1250         return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1251
1252     @openerpweb.httprequest
1253     def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1254         """ Download link for files stored as binary fields.
1255
1256         If the ``id`` parameter is omitted, fetches the default value for the
1257         binary field (via ``default_get``), otherwise fetches the field for
1258         that precise record.
1259
1260         :param req: OpenERP request
1261         :type req: :class:`web.common.http.HttpRequest`
1262         :param str model: name of the model to fetch the binary from
1263         :param str field: binary field
1264         :param str id: id of the record from which to fetch the binary
1265         :param str filename_field: field holding the file's name, if any
1266         :returns: :class:`werkzeug.wrappers.Response`
1267         """
1268         Model = req.session.model(model)
1269         fields = [field]
1270         if filename_field:
1271             fields.append(filename_field)
1272         if id:
1273             res = Model.read([int(id)], fields, req.context)[0]
1274         else:
1275             res = Model.default_get(fields, req.context)
1276         filecontent = base64.b64decode(res.get(field, ''))
1277         if not filecontent:
1278             return req.not_found()
1279         else:
1280             filename = '%s_%s' % (model.replace('.', '_'), id)
1281             if filename_field:
1282                 filename = res.get(filename_field, '') or filename
1283             return req.make_response(filecontent,
1284                 [('Content-Type', 'application/octet-stream'),
1285                  ('Content-Disposition', content_disposition(filename, req))])
1286
1287     @openerpweb.httprequest
1288     def saveas_ajax(self, req, data, token):
1289         jdata = simplejson.loads(data)
1290         model = jdata['model']
1291         field = jdata['field']
1292         id = jdata.get('id', None)
1293         filename_field = jdata.get('filename_field', None)
1294         context = jdata.get('context', {})
1295
1296         Model = req.session.model(model)
1297         fields = [field]
1298         if filename_field:
1299             fields.append(filename_field)
1300         if id:
1301             res = Model.read([int(id)], fields, context)[0]
1302         else:
1303             res = Model.default_get(fields, context)
1304         filecontent = base64.b64decode(res.get(field, ''))
1305         if not filecontent:
1306             raise ValueError(_("No content found for field '%s' on '%s:%s'") %
1307                 (field, model, id))
1308         else:
1309             filename = '%s_%s' % (model.replace('.', '_'), id)
1310             if filename_field:
1311                 filename = res.get(filename_field, '') or filename
1312             return req.make_response(filecontent,
1313                 headers=[('Content-Type', 'application/octet-stream'),
1314                         ('Content-Disposition', content_disposition(filename, req))],
1315                 cookies={'fileToken': int(token)})
1316
1317     @openerpweb.httprequest
1318     def upload(self, req, callback, ufile):
1319         # TODO: might be useful to have a configuration flag for max-length file uploads
1320         try:
1321             out = """<script language="javascript" type="text/javascript">
1322                         var win = window.top.window;
1323                         win.jQuery(win).trigger(%s, %s);
1324                     </script>"""
1325             data = ufile.read()
1326             args = [len(data), ufile.filename,
1327                     ufile.content_type, base64.b64encode(data)]
1328         except Exception, e:
1329             args = [False, e.message]
1330         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1331
1332     @openerpweb.httprequest
1333     def upload_attachment(self, req, callback, model, id, ufile):
1334         Model = req.session.model('ir.attachment')
1335         try:
1336             out = """<script language="javascript" type="text/javascript">
1337                         var win = window.top.window;
1338                         win.jQuery(win).trigger(%s, %s);
1339                     </script>"""
1340             attachment_id = Model.create({
1341                 'name': ufile.filename,
1342                 'datas': base64.encodestring(ufile.read()),
1343                 'datas_fname': ufile.filename,
1344                 'res_model': model,
1345                 'res_id': int(id)
1346             }, req.context)
1347             args = {
1348                 'filename': ufile.filename,
1349                 'id':  attachment_id
1350             }
1351         except Exception,e:
1352             args = {'error':e.faultCode }
1353         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1354
1355 class Action(openerpweb.Controller):
1356     _cp_path = "/web/action"
1357
1358     @openerpweb.jsonrequest
1359     def load(self, req, action_id, do_not_eval=False):
1360         Actions = req.session.model('ir.actions.actions')
1361         value = False
1362         try:
1363             action_id = int(action_id)
1364         except ValueError:
1365             try:
1366                 module, xmlid = action_id.split('.', 1)
1367                 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1368                 assert model.startswith('ir.actions.')
1369             except Exception:
1370                 action_id = 0   # force failed read
1371
1372         base_action = Actions.read([action_id], ['type'], req.context)
1373         if base_action:
1374             ctx = {}
1375             action_type = base_action[0]['type']
1376             if action_type == 'ir.actions.report.xml':
1377                 ctx.update({'bin_size': True})
1378             ctx.update(req.context)
1379             action = req.session.model(action_type).read([action_id], False, ctx)
1380             if action:
1381                 value = clean_action(req, action[0])
1382         return value
1383
1384     @openerpweb.jsonrequest
1385     def run(self, req, action_id):
1386         return_action = req.session.model('ir.actions.server').run(
1387             [action_id], req.context)
1388         if return_action:
1389             return clean_action(req, return_action)
1390         else:
1391             return False
1392
1393 class Export(View):
1394     _cp_path = "/web/export"
1395
1396     @openerpweb.jsonrequest
1397     def formats(self, req):
1398         """ Returns all valid export formats
1399
1400         :returns: for each export format, a pair of identifier and printable name
1401         :rtype: [(str, str)]
1402         """
1403         return sorted([
1404             controller.fmt
1405             for path, controller in openerpweb.controllers_path.iteritems()
1406             if path.startswith(self._cp_path)
1407             if hasattr(controller, 'fmt')
1408         ], key=operator.itemgetter("label"))
1409
1410     def fields_get(self, req, model):
1411         Model = req.session.model(model)
1412         fields = Model.fields_get(False, req.context)
1413         return fields
1414
1415     @openerpweb.jsonrequest
1416     def get_fields(self, req, model, prefix='', parent_name= '',
1417                    import_compat=True, parent_field_type=None,
1418                    exclude=None):
1419
1420         if import_compat and parent_field_type == "many2one":
1421             fields = {}
1422         else:
1423             fields = self.fields_get(req, model)
1424
1425         if import_compat:
1426             fields.pop('id', None)
1427         else:
1428             fields['.id'] = fields.pop('id', {'string': 'ID'})
1429
1430         fields_sequence = sorted(fields.iteritems(),
1431             key=lambda field: field[1].get('string', ''))
1432
1433         records = []
1434         for field_name, field in fields_sequence:
1435             if import_compat:
1436                 if exclude and field_name in exclude:
1437                     continue
1438                 if field.get('readonly'):
1439                     # If none of the field's states unsets readonly, skip the field
1440                     if all(dict(attrs).get('readonly', True)
1441                            for attrs in field.get('states', {}).values()):
1442                         continue
1443
1444             id = prefix + (prefix and '/'or '') + field_name
1445             name = parent_name + (parent_name and '/' or '') + field['string']
1446             record = {'id': id, 'string': name,
1447                       'value': id, 'children': False,
1448                       'field_type': field.get('type'),
1449                       'required': field.get('required'),
1450                       'relation_field': field.get('relation_field')}
1451             records.append(record)
1452
1453             if len(name.split('/')) < 3 and 'relation' in field:
1454                 ref = field.pop('relation')
1455                 record['value'] += '/id'
1456                 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1457
1458                 if not import_compat or field['type'] == 'one2many':
1459                     # m2m field in import_compat is childless
1460                     record['children'] = True
1461
1462         return records
1463
1464     @openerpweb.jsonrequest
1465     def namelist(self,req,  model, export_id):
1466         # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1467         export = req.session.model("ir.exports").read([export_id])[0]
1468         export_fields_list = req.session.model("ir.exports.line").read(
1469             export['export_fields'])
1470
1471         fields_data = self.fields_info(
1472             req, model, map(operator.itemgetter('name'), export_fields_list))
1473
1474         return [
1475             {'name': field['name'], 'label': fields_data[field['name']]}
1476             for field in export_fields_list
1477         ]
1478
1479     def fields_info(self, req, model, export_fields):
1480         info = {}
1481         fields = self.fields_get(req, model)
1482         if ".id" in export_fields:
1483             fields['.id'] = fields.pop('id', {'string': 'ID'})
1484             
1485         # To make fields retrieval more efficient, fetch all sub-fields of a
1486         # given field at the same time. Because the order in the export list is
1487         # arbitrary, this requires ordering all sub-fields of a given field
1488         # together so they can be fetched at the same time
1489         #
1490         # Works the following way:
1491         # * sort the list of fields to export, the default sorting order will
1492         #   put the field itself (if present, for xmlid) and all of its
1493         #   sub-fields right after it
1494         # * then, group on: the first field of the path (which is the same for
1495         #   a field and for its subfields and the length of splitting on the
1496         #   first '/', which basically means grouping the field on one side and
1497         #   all of the subfields on the other. This way, we have the field (for
1498         #   the xmlid) with length 1, and all of the subfields with the same
1499         #   base but a length "flag" of 2
1500         # * if we have a normal field (length 1), just add it to the info
1501         #   mapping (with its string) as-is
1502         # * otherwise, recursively call fields_info via graft_subfields.
1503         #   all graft_subfields does is take the result of fields_info (on the
1504         #   field's model) and prepend the current base (current field), which
1505         #   rebuilds the whole sub-tree for the field
1506         #
1507         # result: because we're not fetching the fields_get for half the
1508         # database models, fetching a namelist with a dozen fields (including
1509         # relational data) falls from ~6s to ~300ms (on the leads model).
1510         # export lists with no sub-fields (e.g. import_compatible lists with
1511         # no o2m) are even more efficient (from the same 6s to ~170ms, as
1512         # there's a single fields_get to execute)
1513         for (base, length), subfields in itertools.groupby(
1514                 sorted(export_fields),
1515                 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1516             subfields = list(subfields)
1517             if length == 2:
1518                 # subfields is a seq of $base/*rest, and not loaded yet
1519                 info.update(self.graft_subfields(
1520                     req, fields[base]['relation'], base, fields[base]['string'],
1521                     subfields
1522                 ))
1523             else:
1524                 info[base] = fields[base]['string']
1525
1526         return info
1527
1528     def graft_subfields(self, req, model, prefix, prefix_string, fields):
1529         export_fields = [field.split('/', 1)[1] for field in fields]
1530         return (
1531             (prefix + '/' + k, prefix_string + '/' + v)
1532             for k, v in self.fields_info(req, model, export_fields).iteritems())
1533
1534     #noinspection PyPropertyDefinition
1535     @property
1536     def content_type(self):
1537         """ Provides the format's content type """
1538         raise NotImplementedError()
1539
1540     def filename(self, base):
1541         """ Creates a valid filename for the format (with extension) from the
1542          provided base name (exension-less)
1543         """
1544         raise NotImplementedError()
1545
1546     def from_data(self, fields, rows):
1547         """ Conversion method from OpenERP's export data to whatever the
1548         current export class outputs
1549
1550         :params list fields: a list of fields to export
1551         :params list rows: a list of records to export
1552         :returns:
1553         :rtype: bytes
1554         """
1555         raise NotImplementedError()
1556
1557     @openerpweb.httprequest
1558     def index(self, req, data, token):
1559         model, fields, ids, domain, import_compat = \
1560             operator.itemgetter('model', 'fields', 'ids', 'domain',
1561                                 'import_compat')(
1562                 simplejson.loads(data))
1563
1564         Model = req.session.model(model)
1565         ids = ids or Model.search(domain, 0, False, False, req.context)
1566
1567         field_names = map(operator.itemgetter('name'), fields)
1568         import_data = Model.export_data(ids, field_names, req.context).get('datas',[])
1569
1570         if import_compat:
1571             columns_headers = field_names
1572         else:
1573             columns_headers = [val['label'].strip() for val in fields]
1574
1575
1576         return req.make_response(self.from_data(columns_headers, import_data),
1577             headers=[('Content-Disposition',
1578                             content_disposition(self.filename(model), req)),
1579                      ('Content-Type', self.content_type)],
1580             cookies={'fileToken': int(token)})
1581
1582 class CSVExport(Export):
1583     _cp_path = '/web/export/csv'
1584     fmt = {'tag': 'csv', 'label': 'CSV'}
1585
1586     @property
1587     def content_type(self):
1588         return 'text/csv;charset=utf8'
1589
1590     def filename(self, base):
1591         return base + '.csv'
1592
1593     def from_data(self, fields, rows):
1594         fp = StringIO()
1595         writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1596
1597         writer.writerow([name.encode('utf-8') for name in fields])
1598
1599         for data in rows:
1600             row = []
1601             for d in data:
1602                 if isinstance(d, basestring):
1603                     d = d.replace('\n',' ').replace('\t',' ')
1604                     try:
1605                         d = d.encode('utf-8')
1606                     except UnicodeError:
1607                         pass
1608                 if d is False: d = None
1609                 row.append(d)
1610             writer.writerow(row)
1611
1612         fp.seek(0)
1613         data = fp.read()
1614         fp.close()
1615         return data
1616
1617 class ExcelExport(Export):
1618     _cp_path = '/web/export/xls'
1619     fmt = {
1620         'tag': 'xls',
1621         'label': 'Excel',
1622         'error': None if xlwt else "XLWT required"
1623     }
1624
1625     @property
1626     def content_type(self):
1627         return 'application/vnd.ms-excel'
1628
1629     def filename(self, base):
1630         return base + '.xls'
1631
1632     def from_data(self, fields, rows):
1633         workbook = xlwt.Workbook()
1634         worksheet = workbook.add_sheet('Sheet 1')
1635
1636         for i, fieldname in enumerate(fields):
1637             worksheet.write(0, i, fieldname)
1638             worksheet.col(i).width = 8000 # around 220 pixels
1639
1640         style = xlwt.easyxf('align: wrap yes')
1641
1642         for row_index, row in enumerate(rows):
1643             for cell_index, cell_value in enumerate(row):
1644                 if isinstance(cell_value, basestring):
1645                     cell_value = re.sub("\r", " ", cell_value)
1646                 if cell_value is False: cell_value = None
1647                 worksheet.write(row_index + 1, cell_index, cell_value, style)
1648
1649         fp = StringIO()
1650         workbook.save(fp)
1651         fp.seek(0)
1652         data = fp.read()
1653         fp.close()
1654         return data
1655
1656 class Reports(View):
1657     _cp_path = "/web/report"
1658     POLLING_DELAY = 0.25
1659     TYPES_MAPPING = {
1660         'doc': 'application/vnd.ms-word',
1661         'html': 'text/html',
1662         'odt': 'application/vnd.oasis.opendocument.text',
1663         'pdf': 'application/pdf',
1664         'sxw': 'application/vnd.sun.xml.writer',
1665         'xls': 'application/vnd.ms-excel',
1666     }
1667
1668     @openerpweb.httprequest
1669     def index(self, req, action, token):
1670         action = simplejson.loads(action)
1671
1672         report_srv = req.session.proxy("report")
1673         context = dict(req.context)
1674         context.update(action["context"])
1675
1676         report_data = {}
1677         report_ids = context["active_ids"]
1678         if 'report_type' in action:
1679             report_data['report_type'] = action['report_type']
1680         if 'datas' in action:
1681             if 'ids' in action['datas']:
1682                 report_ids = action['datas'].pop('ids')
1683             report_data.update(action['datas'])
1684
1685         report_id = report_srv.report(
1686             req.session._db, req.session._uid, req.session._password,
1687             action["report_name"], report_ids,
1688             report_data, context)
1689
1690         report_struct = None
1691         while True:
1692             report_struct = report_srv.report_get(
1693                 req.session._db, req.session._uid, req.session._password, report_id)
1694             if report_struct["state"]:
1695                 break
1696
1697             time.sleep(self.POLLING_DELAY)
1698
1699         report = base64.b64decode(report_struct['result'])
1700         if report_struct.get('code') == 'zlib':
1701             report = zlib.decompress(report)
1702         report_mimetype = self.TYPES_MAPPING.get(
1703             report_struct['format'], 'octet-stream')
1704         file_name = action.get('name', 'report')
1705         if 'name' not in action:
1706             reports = req.session.model('ir.actions.report.xml')
1707             res_id = reports.search([('report_name', '=', action['report_name']),],
1708                                     0, False, False, context)
1709             if len(res_id) > 0:
1710                 file_name = reports.read(res_id[0], ['name'], context)['name']
1711             else:
1712                 file_name = action['report_name']
1713         file_name = '%s.%s' % (file_name, report_struct['format'])
1714
1715         return req.make_response(report,
1716              headers=[
1717                  ('Content-Disposition', content_disposition(file_name, req)),
1718                  ('Content-Type', report_mimetype),
1719                  ('Content-Length', len(report))],
1720              cookies={'fileToken': int(token)})
1721
1722 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: