[IMP]Set Priority to display warning messages for change password dialog
[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
32 from .. import http
33 from .. import nonliterals
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     dbs = []
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 is listed returns 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         import openerp.modules.registry
174         registry = openerp.modules.registry.RegistryManager.get(dbname)
175         with registry.cursor() as cr:
176             m = registry.get('ir.module.module')
177             # TODO The following code should move to ir.module.module.list_installed_modules()
178             domain = [('state','=','installed'), ('name','in', loadable)]
179             ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
180             for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
181                 modules[module['name']] = []
182                 deps = module.get('dependencies_id')
183                 if deps:
184                     deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
185                     dependencies = [i['name'] for i in deps_read]
186                     modules[module['name']] = dependencies
187     except Exception,e:
188         pass
189     sorted_modules = module_topological_sort(modules)
190     return sorted_modules
191
192 def module_boot(req):
193     server_wide_modules = openerp.conf.server_wide_modules or ['web']
194     serverside = []
195     dbside = []
196     for i in server_wide_modules:
197         if i in openerpweb.addons_manifest:
198             serverside.append(i)
199     monodb = db_monodb(req)
200     if monodb:
201         dbside = module_installed_bypass_session(monodb)
202         dbside = [i for i in dbside if i not in serverside]
203     addons = serverside + dbside
204     return addons
205
206 def concat_xml(file_list):
207     """Concatenate xml files
208
209     :param list(str) file_list: list of files to check
210     :returns: (concatenation_result, checksum)
211     :rtype: (str, str)
212     """
213     checksum = hashlib.new('sha1')
214     if not file_list:
215         return '', checksum.hexdigest()
216
217     root = None
218     for fname in file_list:
219         with open(fname, 'rb') as fp:
220             contents = fp.read()
221             checksum.update(contents)
222             fp.seek(0)
223             xml = ElementTree.parse(fp).getroot()
224
225         if root is None:
226             root = ElementTree.Element(xml.tag)
227         #elif root.tag != xml.tag:
228         #    raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
229
230         for child in xml.getchildren():
231             root.append(child)
232     return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
233
234 def concat_files(file_list, reader=None, intersperse=""):
235     """ Concatenates contents of all provided files
236
237     :param list(str) file_list: list of files to check
238     :param function reader: reading procedure for each file
239     :param str intersperse: string to intersperse between file contents
240     :returns: (concatenation_result, checksum)
241     :rtype: (str, str)
242     """
243     checksum = hashlib.new('sha1')
244     if not file_list:
245         return '', checksum.hexdigest()
246
247     if reader is None:
248         def reader(f):
249             with open(f, 'rb') as fp:
250                 return fp.read()
251
252     files_content = []
253     for fname in file_list:
254         contents = reader(fname)
255         checksum.update(contents)
256         files_content.append(contents)
257
258     files_concat = intersperse.join(files_content)
259     return files_concat, checksum.hexdigest()
260
261 concat_js_cache = {}
262
263 def concat_js(file_list):
264     content, checksum = concat_files(file_list, intersperse=';')
265     if checksum in concat_js_cache:
266         content = concat_js_cache[checksum]
267     else:
268         content = rjsmin(content)
269         concat_js_cache[checksum] = content
270     return content, checksum
271
272 def fs2web(path):
273     """convert FS path into web path"""
274     return '/'.join(path.split(os.path.sep))
275
276 def manifest_glob(req, addons, key):
277     if addons is None:
278         addons = module_boot(req)
279     else:
280         addons = addons.split(',')
281     r = []
282     for addon in addons:
283         manifest = openerpweb.addons_manifest.get(addon, None)
284         if not manifest:
285             continue
286         # ensure does not ends with /
287         addons_path = os.path.join(manifest['addons_path'], '')[:-1]
288         globlist = manifest.get(key, [])
289         for pattern in globlist:
290             for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
291                 r.append((path, fs2web(path[len(addons_path):])))
292     return r
293
294 def manifest_list(req, mods, extension):
295     if not req.debug:
296         path = '/web/webclient/' + extension
297         if mods is not None:
298             path += '?mods=' + mods
299         return [path]
300     files = manifest_glob(req, mods, extension)
301     i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
302                     req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
303     if i_am_diabetic:
304         return [wp for _fp, wp in files]
305     else:
306         return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
307
308 def get_last_modified(files):
309     """ Returns the modification time of the most recently modified
310     file provided
311
312     :param list(str) files: names of files to check
313     :return: most recent modification time amongst the fileset
314     :rtype: datetime.datetime
315     """
316     files = list(files)
317     if files:
318         return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
319                    for f in files)
320     return datetime.datetime(1970, 1, 1)
321
322 def make_conditional(req, response, last_modified=None, etag=None):
323     """ Makes the provided response conditional based upon the request,
324     and mandates revalidation from clients
325
326     Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
327     setting ``last_modified`` and ``etag`` correctly on the response object
328
329     :param req: OpenERP request
330     :type req: web.common.http.WebRequest
331     :param response: Werkzeug response
332     :type response: werkzeug.wrappers.Response
333     :param datetime.datetime last_modified: last modification date of the response content
334     :param str etag: some sort of checksum of the content (deep etag)
335     :return: the response object provided
336     :rtype: werkzeug.wrappers.Response
337     """
338     response.cache_control.must_revalidate = True
339     response.cache_control.max_age = 0
340     if last_modified:
341         response.last_modified = last_modified
342     if etag:
343         response.set_etag(etag)
344     return response.make_conditional(req.httprequest)
345
346 def login_and_redirect(req, db, login, key, redirect_url='/'):
347     req.session.authenticate(db, login, key, {})
348     return set_cookie_and_redirect(req, redirect_url)
349
350 def set_cookie_and_redirect(req, redirect_url):
351     redirect = werkzeug.utils.redirect(redirect_url, 303)
352     redirect.autocorrect_location_header = False
353     cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
354     redirect.set_cookie('instance0|session_id', cookie_val)
355     return redirect
356
357 def eval_context_and_domain(session, context, domain=None):
358     e_context = session.eval_context(context)
359     # should we give the evaluated context as an evaluation context to the domain?
360     e_domain = session.eval_domain(domain or [])
361
362     return e_context, e_domain
363
364 def load_actions_from_ir_values(req, key, key2, models, meta):
365     context = req.session.eval_context(req.context)
366     Values = req.session.model('ir.values')
367     actions = Values.get(key, key2, models, meta, context)
368
369     return [(id, name, clean_action(req, action))
370             for id, name, action in actions]
371
372 def clean_action(req, action, do_not_eval=False):
373     action.setdefault('flags', {})
374
375     context = req.session.eval_context(req.context)
376     eval_ctx = req.session.evaluation_context(context)
377
378     if not do_not_eval:
379         # values come from the server, we can just eval them
380         if action.get('context') and isinstance(action.get('context'), basestring):
381             action['context'] = eval( action['context'], eval_ctx ) or {}
382
383         if action.get('domain') and isinstance(action.get('domain'), basestring):
384             action['domain'] = eval( action['domain'], eval_ctx ) or []
385     else:
386         if 'context' in action:
387             action['context'] = parse_context(action['context'], req.session)
388         if 'domain' in action:
389             action['domain'] = parse_domain(action['domain'], req.session)
390
391     action_type = action.setdefault('type', 'ir.actions.act_window_close')
392     if action_type == 'ir.actions.act_window':
393         return fix_view_modes(action)
394     return action
395
396 # I think generate_views,fix_view_modes should go into js ActionManager
397 def generate_views(action):
398     """
399     While the server generates a sequence called "views" computing dependencies
400     between a bunch of stuff for views coming directly from the database
401     (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
402     to return custom view dictionaries generated on the fly.
403
404     In that case, there is no ``views`` key available on the action.
405
406     Since the web client relies on ``action['views']``, generate it here from
407     ``view_mode`` and ``view_id``.
408
409     Currently handles two different cases:
410
411     * no view_id, multiple view_mode
412     * single view_id, single view_mode
413
414     :param dict action: action descriptor dictionary to generate a views key for
415     """
416     view_id = action.get('view_id') or False
417     if isinstance(view_id, (list, tuple)):
418         view_id = view_id[0]
419
420     # providing at least one view mode is a requirement, not an option
421     view_modes = action['view_mode'].split(',')
422
423     if len(view_modes) > 1:
424         if view_id:
425             raise ValueError('Non-db action dictionaries should provide '
426                              'either multiple view modes or a single view '
427                              'mode and an optional view id.\n\n Got view '
428                              'modes %r and view id %r for action %r' % (
429                 view_modes, view_id, action))
430         action['views'] = [(False, mode) for mode in view_modes]
431         return
432     action['views'] = [(view_id, view_modes[0])]
433
434 def fix_view_modes(action):
435     """ For historical reasons, OpenERP has weird dealings in relation to
436     view_mode and the view_type attribute (on window actions):
437
438     * one of the view modes is ``tree``, which stands for both list views
439       and tree views
440     * the choice is made by checking ``view_type``, which is either
441       ``form`` for a list view or ``tree`` for an actual tree view
442
443     This methods simply folds the view_type into view_mode by adding a
444     new view mode ``list`` which is the result of the ``tree`` view_mode
445     in conjunction with the ``form`` view_type.
446
447     TODO: this should go into the doc, some kind of "peculiarities" section
448
449     :param dict action: an action descriptor
450     :returns: nothing, the action is modified in place
451     """
452     if not action.get('views'):
453         generate_views(action)
454
455     if action.pop('view_type', 'form') != 'form':
456         return action
457
458     if 'view_mode' in action:
459         action['view_mode'] = ','.join(
460             mode if mode != 'tree' else 'list'
461             for mode in action['view_mode'].split(','))
462     action['views'] = [
463         [id, mode if mode != 'tree' else 'list']
464         for id, mode in action['views']
465     ]
466
467     return action
468
469 def parse_domain(domain, session):
470     """ Parses an arbitrary string containing a domain, transforms it
471     to either a literal domain or a :class:`nonliterals.Domain`
472
473     :param domain: the domain to parse, if the domain is not a string it
474                    is assumed to be a literal domain and is returned as-is
475     :param session: Current OpenERP session
476     :type session: openerpweb.OpenERPSession
477     """
478     if not isinstance(domain, basestring):
479         return domain
480     try:
481         return ast.literal_eval(domain)
482     except ValueError:
483         # not a literal
484         return nonliterals.Domain(session, domain)
485
486 def parse_context(context, session):
487     """ Parses an arbitrary string containing a context, transforms it
488     to either a literal context or a :class:`nonliterals.Context`
489
490     :param context: the context to parse, if the context is not a string it
491            is assumed to be a literal domain and is returned as-is
492     :param session: Current OpenERP session
493     :type session: openerpweb.OpenERPSession
494     """
495     if not isinstance(context, basestring):
496         return context
497     try:
498         return ast.literal_eval(context)
499     except ValueError:
500         return nonliterals.Context(session, context)
501
502 def _local_web_translations(trans_file):
503     messages = []
504     try:
505         with open(trans_file) as t_file:
506             po = babel.messages.pofile.read_po(t_file)
507     except Exception:
508         return
509     for x in po:
510         if x.id and x.string and "openerp-web" in x.auto_comments:
511             messages.append({'id': x.id, 'string': x.string})
512     return messages
513
514 def xml2json_from_elementtree(el, preserve_whitespaces=False):
515     """ xml2json-direct
516     Simple and straightforward XML-to-JSON converter in Python
517     New BSD Licensed
518     http://code.google.com/p/xml2json-direct/
519     """
520     res = {}
521     if el.tag[0] == "{":
522         ns, name = el.tag.rsplit("}", 1)
523         res["tag"] = name
524         res["namespace"] = ns[1:]
525     else:
526         res["tag"] = el.tag
527     res["attrs"] = {}
528     for k, v in el.items():
529         res["attrs"][k] = v
530     kids = []
531     if el.text and (preserve_whitespaces or el.text.strip() != ''):
532         kids.append(el.text)
533     for kid in el:
534         kids.append(xml2json_from_elementtree(kid, preserve_whitespaces))
535         if kid.tail and (preserve_whitespaces or kid.tail.strip() != ''):
536             kids.append(kid.tail)
537     res["children"] = kids
538     return res
539
540 def content_disposition(filename, req):
541     filename = filename.encode('utf8')
542     escaped = urllib2.quote(filename)
543     browser = req.httprequest.user_agent.browser
544     version = int((req.httprequest.user_agent.version or '0').split('.')[0])
545     if browser == 'msie' and version < 9:
546         return "attachment; filename=%s" % escaped
547     elif browser == 'safari':
548         return "attachment; filename=%s" % filename
549     else:
550         return "attachment; filename*=UTF-8''%s" % escaped
551
552
553 #----------------------------------------------------------
554 # OpenERP Web web Controllers
555 #----------------------------------------------------------
556
557 html_template = """<!DOCTYPE html>
558 <html style="height: 100%%">
559     <head>
560         <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
561         <meta http-equiv="content-type" content="text/html; charset=utf-8" />
562         <title>OpenERP</title>
563         <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
564         <link rel="stylesheet" href="/web/static/src/css/full.css" />
565         %(css)s
566         %(js)s
567         <script type="text/javascript">
568             $(function() {
569                 var s = new openerp.init(%(modules)s);
570                 %(init)s
571             });
572         </script>
573     </head>
574     <body>
575         <!--[if lte IE 8]>
576         <script src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
577         <script>
578             var test = function() {
579                 CFInstall.check({
580                     mode: "overlay"
581                 });
582             };
583             if (window.localStorage && false) {
584                 if (! localStorage.getItem("hasShownGFramePopup")) {
585                     test();
586                     localStorage.setItem("hasShownGFramePopup", true);
587                 }
588             } else {
589                 test();
590             }
591         </script>
592         <![endif]-->
593     </body>
594 </html>
595 """
596
597 class Home(openerpweb.Controller):
598     _cp_path = '/'
599
600     @openerpweb.httprequest
601     def index(self, req, s_action=None, **kw):
602         js = "\n        ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, None, 'js'))
603         css = "\n        ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, None, 'css'))
604
605         r = html_template % {
606             'js': js,
607             'css': css,
608             'modules': simplejson.dumps(module_boot(req)),
609             'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
610         }
611         return r
612
613     @openerpweb.httprequest
614     def login(self, req, db, login, key):
615         return login_and_redirect(req, db, login, key)
616
617 class WebClient(openerpweb.Controller):
618     _cp_path = "/web/webclient"
619
620     @openerpweb.jsonrequest
621     def csslist(self, req, mods=None):
622         return manifest_list(req, mods, 'css')
623
624     @openerpweb.jsonrequest
625     def jslist(self, req, mods=None):
626         return manifest_list(req, mods, 'js')
627
628     @openerpweb.jsonrequest
629     def qweblist(self, req, mods=None):
630         return manifest_list(req, mods, 'qweb')
631
632     @openerpweb.httprequest
633     def css(self, req, mods=None):
634         files = list(manifest_glob(req, mods, 'css'))
635         last_modified = get_last_modified(f[0] for f in files)
636         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
637             return werkzeug.wrappers.Response(status=304)
638
639         file_map = dict(files)
640
641         rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
642         rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
643
644         def reader(f):
645             """read the a css file and absolutify all relative uris"""
646             with open(f, 'rb') as fp:
647                 data = fp.read().decode('utf-8')
648
649             path = file_map[f]
650             web_dir = os.path.dirname(path)
651
652             data = re.sub(
653                 rx_import,
654                 r"""@import \1%s/""" % (web_dir,),
655                 data,
656             )
657
658             data = re.sub(
659                 rx_url,
660                 r"""url(\1%s/""" % (web_dir,),
661                 data,
662             )
663             return data.encode('utf-8')
664
665         content, checksum = concat_files((f[0] for f in files), reader)
666
667         return make_conditional(
668             req, req.make_response(content, [('Content-Type', 'text/css')]),
669             last_modified, checksum)
670
671     @openerpweb.httprequest
672     def js(self, req, mods=None):
673         files = [f[0] for f in manifest_glob(req, mods, 'js')]
674         last_modified = get_last_modified(files)
675         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
676             return werkzeug.wrappers.Response(status=304)
677
678         content, checksum = concat_js(files)
679
680         return make_conditional(
681             req, req.make_response(content, [('Content-Type', 'application/javascript')]),
682             last_modified, checksum)
683
684     @openerpweb.httprequest
685     def qweb(self, req, mods=None):
686         files = [f[0] for f in manifest_glob(req, mods, 'qweb')]
687         last_modified = get_last_modified(files)
688         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
689             return werkzeug.wrappers.Response(status=304)
690
691         content, checksum = concat_xml(files)
692
693         return make_conditional(
694             req, req.make_response(content, [('Content-Type', 'text/xml')]),
695             last_modified, checksum)
696
697     @openerpweb.jsonrequest
698     def bootstrap_translations(self, req, mods):
699         """ Load local translations from *.po files, as a temporary solution
700             until we have established a valid session. This is meant only
701             for translating the login page and db management chrome, using
702             the browser's language. """
703         # For performance reasons we only load a single translation, so for
704         # sub-languages (that should only be partially translated) we load the
705         # main language PO instead - that should be enough for the login screen.
706         lang = req.lang.split('_')[0]
707
708         translations_per_module = {}
709         for addon_name in mods:
710             if openerpweb.addons_manifest[addon_name].get('bootstrap'):
711                 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
712                 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
713                 if not os.path.exists(f_name):
714                     continue
715                 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
716
717         return {"modules": translations_per_module,
718                 "lang_parameters": None}
719
720     @openerpweb.jsonrequest
721     def translations(self, req, mods, lang):
722         res_lang = req.session.model('res.lang')
723         ids = res_lang.search([("code", "=", lang)])
724         lang_params = None
725         if ids:
726             lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
727                                                 "grouping", "decimal_point", "thousands_sep"])
728
729         # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
730         # done server-side when the language is loaded, so we only need to load the user's lang.
731         ir_translation = req.session.model('ir.translation')
732         translations_per_module = {}
733         messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
734                                                ('comments','like','openerp-web'),('value','!=',False),
735                                                ('value','!=','')],
736                                               ['module','src','value','lang'], order='module') 
737         for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
738             translations_per_module.setdefault(mod,{'messages':[]})
739             translations_per_module[mod]['messages'].extend({'id': m['src'],
740                                                              'string': m['value']} \
741                                                                 for m in msg_group)
742         return {"modules": translations_per_module,
743                 "lang_parameters": lang_params}
744
745     @openerpweb.jsonrequest
746     def version_info(self, req):
747         return {
748             "version": openerp.release.version
749         }
750
751 class Proxy(openerpweb.Controller):
752     _cp_path = '/web/proxy'
753
754     @openerpweb.jsonrequest
755     def load(self, req, path):
756         """ Proxies an HTTP request through a JSON request.
757
758         It is strongly recommended to not request binary files through this,
759         as the result will be a binary data blob as well.
760
761         :param req: OpenERP request
762         :param path: actual request path
763         :return: file content
764         """
765         from werkzeug.test import Client
766         from werkzeug.wrappers import BaseResponse
767
768         return Client(req.httprequest.app, BaseResponse).get(path).data
769
770 class Database(openerpweb.Controller):
771     _cp_path = "/web/database"
772
773     @openerpweb.jsonrequest
774     def get_list(self, req):
775         return db_list(req)
776
777     @openerpweb.jsonrequest
778     def create(self, req, fields):
779         params = dict(map(operator.itemgetter('name', 'value'), fields))
780         return req.session.proxy("db").create_database(
781             params['super_admin_pwd'],
782             params['db_name'],
783             bool(params.get('demo_data')),
784             params['db_lang'],
785             params['create_admin_pwd'])
786
787     @openerpweb.jsonrequest
788     def duplicate(self, req, fields):
789         params = dict(map(operator.itemgetter('name', 'value'), fields))
790         return req.session.proxy("db").duplicate_database(
791             params['super_admin_pwd'],
792             params['db_original_name'],
793             params['db_name'])
794
795     @openerpweb.jsonrequest
796     def duplicate(self, req, fields):
797         params = dict(map(operator.itemgetter('name', 'value'), fields))
798         duplicate_attrs = (
799             params['super_admin_pwd'],
800             params['db_original_name'],
801             params['db_name'],
802         )
803
804         return req.session.proxy("db").duplicate_database(*duplicate_attrs)
805
806     @openerpweb.jsonrequest
807     def drop(self, req, fields):
808         password, db = operator.itemgetter(
809             'drop_pwd', 'drop_db')(
810                 dict(map(operator.itemgetter('name', 'value'), fields)))
811
812         try:
813             return req.session.proxy("db").drop(password, db)
814         except xmlrpclib.Fault, e:
815             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
816                 return {'error': e.faultCode, 'title': 'Drop Database'}
817         return {'error': 'Could not drop database !', 'title': 'Drop Database'}
818
819     @openerpweb.httprequest
820     def backup(self, req, backup_db, backup_pwd, token):
821         try:
822             db_dump = base64.b64decode(
823                 req.session.proxy("db").dump(backup_pwd, backup_db))
824             filename = "%(db)s_%(timestamp)s.dump" % {
825                 'db': backup_db,
826                 'timestamp': datetime.datetime.utcnow().strftime(
827                     "%Y-%m-%d_%H-%M-%SZ")
828             }
829             return req.make_response(db_dump,
830                [('Content-Type', 'application/octet-stream; charset=binary'),
831                ('Content-Disposition', content_disposition(filename, req))],
832                {'fileToken': int(token)}
833             )
834         except xmlrpclib.Fault, e:
835             return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
836
837     @openerpweb.httprequest
838     def restore(self, req, db_file, restore_pwd, new_db):
839         try:
840             data = base64.b64encode(db_file.read())
841             req.session.proxy("db").restore(restore_pwd, new_db, data)
842             return ''
843         except xmlrpclib.Fault, e:
844             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
845                 raise Exception("AccessDenied")
846
847     @openerpweb.jsonrequest
848     def change_password(self, req, fields):
849         old_password, new_password = operator.itemgetter(
850             'old_pwd', 'new_pwd')(
851                 dict(map(operator.itemgetter('name', 'value'), fields)))
852         try:
853             return req.session.proxy("db").change_admin_password(old_password, new_password)
854         except xmlrpclib.Fault, e:
855             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
856                 return {'error': e.faultCode, 'title': 'Change Password'}
857         return {'error': 'Error, password not changed !', 'title': 'Change Password'}
858
859 class Session(openerpweb.Controller):
860     _cp_path = "/web/session"
861
862     def session_info(self, req):
863         req.session.ensure_valid()
864         return {
865             "session_id": req.session_id,
866             "uid": req.session._uid,
867             "context": req.session.get_context() if req.session._uid else {},
868             "db": req.session._db,
869             "login": req.session._login,
870         }
871
872     @openerpweb.jsonrequest
873     def get_session_info(self, req):
874         return self.session_info(req)
875
876     @openerpweb.jsonrequest
877     def authenticate(self, req, db, login, password, base_location=None):
878         wsgienv = req.httprequest.environ
879         env = dict(
880             base_location=base_location,
881             HTTP_HOST=wsgienv['HTTP_HOST'],
882             REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
883         )
884         req.session.authenticate(db, login, password, env)
885
886         return self.session_info(req)
887
888     @openerpweb.jsonrequest
889     def change_password (self,req,fields):
890         old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
891                 dict(map(operator.itemgetter('name', 'value'), fields)))
892         if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
893             return {'error':'You cannot leave any password empty.','title': 'Change Password'}
894         user_pwd = req.session.model('res.users').read([req.session._uid], ['password'], None)[0]['password']
895         try:
896             if old_password == user_pwd:
897                 if old_password == new_password:
898                     return {'error':'New password must be different from the old password. Please try again.','title': 'Change Password'}
899                 elif new_password != confirm_password:
900                     return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
901                 elif req.session.model('res.users').change_password(old_password, new_password):
902                     return {'new_password':new_password}
903             else:
904                 return {'error': 'The Old Password you provided is incorrect. Please try again.', 'title': 'Change Password'}
905         except Exception:
906             return {'error': 'Error, password not changed !', 'title': 'Change Password'}
907
908     @openerpweb.jsonrequest
909     def sc_list(self, req):
910         return req.session.model('ir.ui.view_sc').get_sc(
911             req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
912
913     @openerpweb.jsonrequest
914     def get_lang_list(self, req):
915         try:
916             return req.session.proxy("db").list_lang() or []
917         except Exception, e:
918             return {"error": e, "title": "Languages"}
919
920     @openerpweb.jsonrequest
921     def modules(self, req):
922         # return all installed modules. Web client is smart enough to not load a module twice
923         return module_installed(req)
924
925     @openerpweb.jsonrequest
926     def eval_domain_and_context(self, req, contexts, domains,
927                                 group_by_seq=None):
928         """ Evaluates sequences of domains and contexts, composing them into
929         a single context, domain or group_by sequence.
930
931         :param list contexts: list of contexts to merge together. Contexts are
932                               evaluated in sequence, all previous contexts
933                               are part of their own evaluation context
934                               (starting at the session context).
935         :param list domains: list of domains to merge together. Domains are
936                              evaluated in sequence and appended to one another
937                              (implicit AND), their evaluation domain is the
938                              result of merging all contexts.
939         :param list group_by_seq: list of domains (which may be in a different
940                                   order than the ``contexts`` parameter),
941                                   evaluated in sequence, their ``'group_by'``
942                                   key is extracted if they have one.
943         :returns:
944             a 3-dict of:
945
946             context (``dict``)
947                 the global context created by merging all of
948                 ``contexts``
949
950             domain (``list``)
951                 the concatenation of all domains
952
953             group_by (``list``)
954                 a list of fields to group by, potentially empty (in which case
955                 no group by should be performed)
956         """
957         context, domain = eval_context_and_domain(req.session,
958                                                   nonliterals.CompoundContext(*(contexts or [])),
959                                                   nonliterals.CompoundDomain(*(domains or [])))
960
961         group_by_sequence = []
962         for candidate in (group_by_seq or []):
963             ctx = req.session.eval_context(candidate, context)
964             group_by = ctx.get('group_by')
965             if not group_by:
966                 continue
967             elif isinstance(group_by, basestring):
968                 group_by_sequence.append(group_by)
969             else:
970                 group_by_sequence.extend(group_by)
971
972         return {
973             'context': context,
974             'domain': domain,
975             'group_by': group_by_sequence
976         }
977
978     @openerpweb.jsonrequest
979     def save_session_action(self, req, the_action):
980         """
981         This method store an action object in the session object and returns an integer
982         identifying that action. The method get_session_action() can be used to get
983         back the action.
984
985         :param the_action: The action to save in the session.
986         :type the_action: anything
987         :return: A key identifying the saved action.
988         :rtype: integer
989         """
990         saved_actions = req.httpsession.get('saved_actions')
991         if not saved_actions:
992             saved_actions = {"next":0, "actions":{}}
993             req.httpsession['saved_actions'] = saved_actions
994         # we don't allow more than 10 stored actions
995         if len(saved_actions["actions"]) >= 10:
996             del saved_actions["actions"][min(saved_actions["actions"])]
997         key = saved_actions["next"]
998         saved_actions["actions"][key] = the_action
999         saved_actions["next"] = key + 1
1000         return key
1001
1002     @openerpweb.jsonrequest
1003     def get_session_action(self, req, key):
1004         """
1005         Gets back a previously saved action. This method can return None if the action
1006         was saved since too much time (this case should be handled in a smart way).
1007
1008         :param key: The key given by save_session_action()
1009         :type key: integer
1010         :return: The saved action or None.
1011         :rtype: anything
1012         """
1013         saved_actions = req.httpsession.get('saved_actions')
1014         if not saved_actions:
1015             return None
1016         return saved_actions["actions"].get(key)
1017
1018     @openerpweb.jsonrequest
1019     def check(self, req):
1020         req.session.assert_valid()
1021         return None
1022
1023     @openerpweb.jsonrequest
1024     def destroy(self, req):
1025         req.session._suicide = True
1026
1027 class Menu(openerpweb.Controller):
1028     _cp_path = "/web/menu"
1029
1030     @openerpweb.jsonrequest
1031     def load(self, req):
1032         return {'data': self.do_load(req)}
1033
1034     @openerpweb.jsonrequest
1035     def get_user_roots(self, req):
1036         return self.do_get_user_roots(req)
1037
1038     def do_get_user_roots(self, req):
1039         """ Return all root menu ids visible for the session user.
1040
1041         :param req: A request object, with an OpenERP session attribute
1042         :type req: < session -> OpenERPSession >
1043         :return: the root menu ids
1044         :rtype: list(int)
1045         """
1046         s = req.session
1047         context = s.eval_context(req.context)
1048         Menus = s.model('ir.ui.menu')
1049         # If a menu action is defined use its domain to get the root menu items
1050         user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
1051
1052         menu_domain = [('parent_id', '=', False)]
1053         if user_menu_id:
1054             domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
1055             if domain_string:
1056                 menu_domain = ast.literal_eval(domain_string)
1057
1058         return Menus.search(menu_domain, 0, False, False, context)
1059
1060     def do_load(self, req):
1061         """ Loads all menu items (all applications and their sub-menus).
1062
1063         :param req: A request object, with an OpenERP session attribute
1064         :type req: < session -> OpenERPSession >
1065         :return: the menu root
1066         :rtype: dict('children': menu_nodes)
1067         """
1068         context = req.session.eval_context(req.context)
1069         Menus = req.session.model('ir.ui.menu')
1070
1071         menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1072         menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
1073
1074         # menus are loaded fully unlike a regular tree view, cause there are a
1075         # limited number of items (752 when all 6.1 addons are installed)
1076         menu_ids = Menus.search([], 0, False, False, context)
1077         menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1078         # adds roots at the end of the sequence, so that they will overwrite
1079         # equivalent menu items from full menu read when put into id:item
1080         # mapping, resulting in children being correctly set on the roots.
1081         menu_items.extend(menu_roots)
1082
1083         # make a tree using parent_id
1084         menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1085         for menu_item in menu_items:
1086             if menu_item['parent_id']:
1087                 parent = menu_item['parent_id'][0]
1088             else:
1089                 parent = False
1090             if parent in menu_items_map:
1091                 menu_items_map[parent].setdefault(
1092                     'children', []).append(menu_item)
1093
1094         # sort by sequence a tree using parent_id
1095         for menu_item in menu_items:
1096             menu_item.setdefault('children', []).sort(
1097                 key=operator.itemgetter('sequence'))
1098
1099         return menu_root
1100
1101     @openerpweb.jsonrequest
1102     def action(self, req, menu_id):
1103         actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1104                                              [('ir.ui.menu', menu_id)], False)
1105         return {"action": actions}
1106
1107 class DataSet(openerpweb.Controller):
1108     _cp_path = "/web/dataset"
1109
1110     @openerpweb.jsonrequest
1111     def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1112         return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1113     def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1114                        , sort=None):
1115         """ Performs a search() followed by a read() (if needed) using the
1116         provided search criteria
1117
1118         :param req: a JSON-RPC request object
1119         :type req: openerpweb.JsonRequest
1120         :param str model: the name of the model to search on
1121         :param fields: a list of the fields to return in the result records
1122         :type fields: [str]
1123         :param int offset: from which index should the results start being returned
1124         :param int limit: the maximum number of records to return
1125         :param list domain: the search domain for the query
1126         :param list sort: sorting directives
1127         :returns: A structure (dict) with two keys: ids (all the ids matching
1128                   the (domain, context) pair) and records (paginated records
1129                   matching fields selection set)
1130         :rtype: list
1131         """
1132         Model = req.session.model(model)
1133
1134         context, domain = eval_context_and_domain(
1135             req.session, req.context, domain)
1136
1137         ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1138         if limit and len(ids) == limit:
1139             length = Model.search_count(domain, context)
1140         else:
1141             length = len(ids) + (offset or 0)
1142         if fields and fields == ['id']:
1143             # shortcut read if we only want the ids
1144             return {
1145                 'length': length,
1146                 'records': [{'id': id} for id in ids]
1147             }
1148
1149         records = Model.read(ids, fields or False, context)
1150         records.sort(key=lambda obj: ids.index(obj['id']))
1151         return {
1152             'length': length,
1153             'records': records
1154         }
1155
1156     @openerpweb.jsonrequest
1157     def load(self, req, model, id, fields):
1158         m = req.session.model(model)
1159         value = {}
1160         r = m.read([id], False, req.session.eval_context(req.context))
1161         if r:
1162             value = r[0]
1163         return {'value': value}
1164
1165     def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1166         has_domain = domain_id is not None and domain_id < len(args)
1167         has_context = context_id is not None and context_id < len(args)
1168
1169         domain = args[domain_id] if has_domain else []
1170         context = args[context_id] if has_context else {}
1171         c, d = eval_context_and_domain(req.session, context, domain)
1172         if has_domain:
1173             args[domain_id] = d
1174         if has_context:
1175             args[context_id] = c
1176
1177         return self._call_kw(req, model, method, args, {})
1178     
1179     def _call_kw(self, req, model, method, args, kwargs):
1180         for i in xrange(len(args)):
1181             if isinstance(args[i], nonliterals.BaseContext):
1182                 args[i] = req.session.eval_context(args[i])
1183             elif isinstance(args[i], nonliterals.BaseDomain):
1184                 args[i] = req.session.eval_domain(args[i])
1185         for k in kwargs.keys():
1186             if isinstance(kwargs[k], nonliterals.BaseContext):
1187                 kwargs[k] = req.session.eval_context(kwargs[k])
1188             elif isinstance(kwargs[k], nonliterals.BaseDomain):
1189                 kwargs[k] = req.session.eval_domain(kwargs[k])
1190
1191         # Temporary implements future display_name special field for model#read()
1192         if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1193             if 'display_name' in args[1]:
1194                 names = req.session.model(model).name_get(args[0], **kwargs)
1195                 args[1].remove('display_name')
1196                 r = getattr(req.session.model(model), method)(*args, **kwargs)
1197                 for i in range(len(r)):
1198                     r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1199                 return r
1200
1201         return getattr(req.session.model(model), method)(*args, **kwargs)
1202
1203     @openerpweb.jsonrequest
1204     def onchange(self, req, model, method, args, context_id=None):
1205         """ Support method for handling onchange calls: behaves much like call
1206         with the following differences:
1207
1208         * Does not take a domain_id
1209         * Is aware of the return value's structure, and will parse the domains
1210           if needed in order to return either parsed literal domains (in JSON)
1211           or non-literal domain instances, allowing those domains to be used
1212           from JS
1213
1214         :param req:
1215         :type req: web.common.http.JsonRequest
1216         :param str model: object type on which to call the method
1217         :param str method: name of the onchange handler method
1218         :param list args: arguments to call the onchange handler with
1219         :param int context_id: index of the context object in the list of
1220                                arguments
1221         :return: result of the onchange call with all domains parsed
1222         """
1223         result = self.call_common(req, model, method, args, context_id=context_id)
1224         if not result or 'domain' not in result:
1225             return result
1226
1227         result['domain'] = dict(
1228             (k, parse_domain(v, req.session))
1229             for k, v in result['domain'].iteritems())
1230
1231         return result
1232
1233     @openerpweb.jsonrequest
1234     def call(self, req, model, method, args, domain_id=None, context_id=None):
1235         return self.call_common(req, model, method, args, domain_id, context_id)
1236     
1237     @openerpweb.jsonrequest
1238     def call_kw(self, req, model, method, args, kwargs):
1239         return self._call_kw(req, model, method, args, kwargs)
1240
1241     @openerpweb.jsonrequest
1242     def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1243         action = self.call_common(req, model, method, args, domain_id, context_id)
1244         if isinstance(action, dict) and action.get('type') != '':
1245             return clean_action(req, action)
1246         return False
1247
1248     @openerpweb.jsonrequest
1249     def exec_workflow(self, req, model, id, signal):
1250         return req.session.exec_workflow(model, id, signal)
1251
1252     @openerpweb.jsonrequest
1253     def resequence(self, req, model, ids, field='sequence', offset=0):
1254         """ Re-sequences a number of records in the model, by their ids
1255
1256         The re-sequencing starts at the first model of ``ids``, the sequence
1257         number is incremented by one after each record and starts at ``offset``
1258
1259         :param ids: identifiers of the records to resequence, in the new sequence order
1260         :type ids: list(id)
1261         :param str field: field used for sequence specification, defaults to
1262                           "sequence"
1263         :param int offset: sequence number for first record in ``ids``, allows
1264                            starting the resequencing from an arbitrary number,
1265                            defaults to ``0``
1266         """
1267         m = req.session.model(model)
1268         if not m.fields_get([field]):
1269             return False
1270         # python 2.6 has no start parameter
1271         for i, id in enumerate(ids):
1272             m.write(id, { field: i + offset })
1273         return True
1274
1275 class View(openerpweb.Controller):
1276     _cp_path = "/web/view"
1277
1278     def fields_view_get(self, req, model, view_id, view_type,
1279                         transform=True, toolbar=False, submenu=False):
1280         Model = req.session.model(model)
1281         context = req.session.eval_context(req.context)
1282         fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1283         # todo fme?: check that we should pass the evaluated context here
1284         self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1285         if toolbar and transform:
1286             self.process_toolbar(req, fvg['toolbar'])
1287         return fvg
1288
1289     def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1290         # depending on how it feels, xmlrpclib.ServerProxy can translate
1291         # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1292         # enjoy unicode strings which can not be trivially converted to
1293         # strings, and it blows up during parsing.
1294
1295         # So ensure we fix this retardation by converting view xml back to
1296         # bit strings.
1297         if isinstance(fvg['arch'], unicode):
1298             arch = fvg['arch'].encode('utf-8')
1299         else:
1300             arch = fvg['arch']
1301         fvg['arch_string'] = arch
1302
1303         if transform:
1304             evaluation_context = session.evaluation_context(context or {})
1305             xml = self.transform_view(arch, session, evaluation_context)
1306         else:
1307             xml = ElementTree.fromstring(arch)
1308         fvg['arch'] = xml2json_from_elementtree(xml, preserve_whitespaces)
1309
1310         if 'id' in fvg['fields']:
1311             # Special case for id's
1312             id_field = fvg['fields']['id']
1313             id_field['original_type'] = id_field['type']
1314             id_field['type'] = 'id'
1315
1316         for field in fvg['fields'].itervalues():
1317             if field.get('views'):
1318                 for view in field["views"].itervalues():
1319                     self.process_view(session, view, None, transform)
1320             if field.get('domain'):
1321                 field["domain"] = parse_domain(field["domain"], session)
1322             if field.get('context'):
1323                 field["context"] = parse_context(field["context"], session)
1324
1325     def process_toolbar(self, req, toolbar):
1326         """
1327         The toolbar is a mapping of section_key: [action_descriptor]
1328
1329         We need to clean all those actions in order to ensure correct
1330         round-tripping
1331         """
1332         for actions in toolbar.itervalues():
1333             for action in actions:
1334                 if 'context' in action:
1335                     action['context'] = parse_context(
1336                         action['context'], req.session)
1337                 if 'domain' in action:
1338                     action['domain'] = parse_domain(
1339                         action['domain'], req.session)
1340
1341     @openerpweb.jsonrequest
1342     def add_custom(self, req, view_id, arch):
1343         CustomView = req.session.model('ir.ui.view.custom')
1344         CustomView.create({
1345             'user_id': req.session._uid,
1346             'ref_id': view_id,
1347             'arch': arch
1348         }, req.session.eval_context(req.context))
1349         return {'result': True}
1350
1351     @openerpweb.jsonrequest
1352     def undo_custom(self, req, view_id, reset=False):
1353         CustomView = req.session.model('ir.ui.view.custom')
1354         context = req.session.eval_context(req.context)
1355         vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1356                                     0, False, False, context)
1357         if vcustom:
1358             if reset:
1359                 CustomView.unlink(vcustom, context)
1360             else:
1361                 CustomView.unlink([vcustom[0]], context)
1362             return {'result': True}
1363         return {'result': False}
1364
1365     def transform_view(self, view_string, session, context=None):
1366         # transform nodes on the fly via iterparse, instead of
1367         # doing it statically on the parsing result
1368         parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1369         root = None
1370         for event, elem in parser:
1371             if event == "start":
1372                 if root is None:
1373                     root = elem
1374                 self.parse_domains_and_contexts(elem, session)
1375         return root
1376
1377     def parse_domains_and_contexts(self, elem, session):
1378         """ Converts domains and contexts from the view into Python objects,
1379         either literals if they can be parsed by literal_eval or a special
1380         placeholder object if the domain or context refers to free variables.
1381
1382         :param elem: the current node being parsed
1383         :type param: xml.etree.ElementTree.Element
1384         :param session: OpenERP session object, used to store and retrieve
1385                         non-literal objects
1386         :type session: openerpweb.openerpweb.OpenERPSession
1387         """
1388         for el in ['domain', 'filter_domain']:
1389             domain = elem.get(el, '').strip()
1390             if domain:
1391                 elem.set(el, parse_domain(domain, session))
1392                 elem.set(el + '_string', domain)
1393         for el in ['context', 'default_get']:
1394             context_string = elem.get(el, '').strip()
1395             if context_string:
1396                 elem.set(el, parse_context(context_string, session))
1397                 elem.set(el + '_string', context_string)
1398
1399     @openerpweb.jsonrequest
1400     def load(self, req, model, view_id, view_type, toolbar=False):
1401         return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1402
1403 class TreeView(View):
1404     _cp_path = "/web/treeview"
1405
1406     @openerpweb.jsonrequest
1407     def action(self, req, model, id):
1408         return load_actions_from_ir_values(
1409             req,'action', 'tree_but_open',[(model, id)],
1410             False)
1411
1412 class SearchView(View):
1413     _cp_path = "/web/searchview"
1414
1415     @openerpweb.jsonrequest
1416     def load(self, req, model, view_id):
1417         fields_view = self.fields_view_get(req, model, view_id, 'search')
1418         return {'fields_view': fields_view}
1419
1420     @openerpweb.jsonrequest
1421     def fields_get(self, req, model):
1422         Model = req.session.model(model)
1423         fields = Model.fields_get(False, req.session.eval_context(req.context))
1424         for field in fields.values():
1425             # shouldn't convert the views too?
1426             if field.get('domain'):
1427                 field["domain"] = parse_domain(field["domain"], req.session)
1428             if field.get('context'):
1429                 field["context"] = parse_context(field["context"], req.session)
1430         return {'fields': fields}
1431
1432     @openerpweb.jsonrequest
1433     def get_filters(self, req, model):
1434         logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1435         Model = req.session.model("ir.filters")
1436         filters = Model.get_filters(model)
1437         for filter in filters:
1438             try:
1439                 parsed_context = parse_context(filter["context"], req.session)
1440                 filter["context"] = (parsed_context
1441                         if not isinstance(parsed_context, nonliterals.BaseContext)
1442                         else req.session.eval_context(parsed_context))
1443
1444                 parsed_domain = parse_domain(filter["domain"], req.session)
1445                 filter["domain"] = (parsed_domain
1446                         if not isinstance(parsed_domain, nonliterals.BaseDomain)
1447                         else req.session.eval_domain(parsed_domain))
1448             except Exception:
1449                 logger.exception("Failed to parse custom filter %s in %s",
1450                                  filter['name'], model)
1451                 filter['disabled'] = True
1452                 del filter['context']
1453                 del filter['domain']
1454         return filters
1455
1456 class Binary(openerpweb.Controller):
1457     _cp_path = "/web/binary"
1458
1459     @openerpweb.httprequest
1460     def image(self, req, model, id, field, **kw):
1461         last_update = '__last_update'
1462         Model = req.session.model(model)
1463         context = req.session.eval_context(req.context)
1464         headers = [('Content-Type', 'image/png')]
1465         etag = req.httprequest.headers.get('If-None-Match')
1466         hashed_session = hashlib.md5(req.session_id).hexdigest()
1467         id = None if not id else simplejson.loads(id)
1468         if type(id) is list:
1469             id = id[0] # m2o
1470         if etag:
1471             if not id and hashed_session == etag:
1472                 return werkzeug.wrappers.Response(status=304)
1473             else:
1474                 date = Model.read([id], [last_update], context)[0].get(last_update)
1475                 if hashlib.md5(date).hexdigest() == etag:
1476                     return werkzeug.wrappers.Response(status=304)
1477
1478         retag = hashed_session
1479         try:
1480             if not id:
1481                 res = Model.default_get([field], context).get(field)
1482                 image_base64 = res
1483             else:
1484                 res = Model.read([id], [last_update, field], context)[0]
1485                 retag = hashlib.md5(res.get(last_update)).hexdigest()
1486                 image_base64 = res.get(field)
1487
1488             if kw.get('resize'):
1489                 resize = kw.get('resize').split(',');
1490                 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1491                     width = int(resize[0])
1492                     height = int(resize[1])
1493                     # resize maximum 500*500
1494                     if width > 500: width = 500
1495                     if height > 500: height = 500
1496                     image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1497             
1498             image_data = base64.b64decode(image_base64)
1499
1500         except (TypeError, xmlrpclib.Fault):
1501             image_data = self.placeholder(req)
1502         headers.append(('ETag', retag))
1503         headers.append(('Content-Length', len(image_data)))
1504         try:
1505             ncache = int(kw.get('cache'))
1506             headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1507         except:
1508             pass
1509         return req.make_response(image_data, headers)
1510     def placeholder(self, req):
1511         addons_path = openerpweb.addons_manifest['web']['addons_path']
1512         return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1513
1514     @openerpweb.httprequest
1515     def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1516         """ Download link for files stored as binary fields.
1517
1518         If the ``id`` parameter is omitted, fetches the default value for the
1519         binary field (via ``default_get``), otherwise fetches the field for
1520         that precise record.
1521
1522         :param req: OpenERP request
1523         :type req: :class:`web.common.http.HttpRequest`
1524         :param str model: name of the model to fetch the binary from
1525         :param str field: binary field
1526         :param str id: id of the record from which to fetch the binary
1527         :param str filename_field: field holding the file's name, if any
1528         :returns: :class:`werkzeug.wrappers.Response`
1529         """
1530         Model = req.session.model(model)
1531         context = req.session.eval_context(req.context)
1532         fields = [field]
1533         if filename_field:
1534             fields.append(filename_field)
1535         if id:
1536             res = Model.read([int(id)], fields, context)[0]
1537         else:
1538             res = Model.default_get(fields, context)
1539         filecontent = base64.b64decode(res.get(field, ''))
1540         if not filecontent:
1541             return req.not_found()
1542         else:
1543             filename = '%s_%s' % (model.replace('.', '_'), id)
1544             if filename_field:
1545                 filename = res.get(filename_field, '') or filename
1546             return req.make_response(filecontent,
1547                 [('Content-Type', 'application/octet-stream'),
1548                  ('Content-Disposition', content_disposition(filename, req))])
1549
1550     @openerpweb.httprequest
1551     def saveas_ajax(self, req, data, token):
1552         jdata = simplejson.loads(data)
1553         model = jdata['model']
1554         field = jdata['field']
1555         id = jdata.get('id', None)
1556         filename_field = jdata.get('filename_field', None)
1557         context = jdata.get('context', dict())
1558
1559         context = req.session.eval_context(context)
1560         Model = req.session.model(model)
1561         fields = [field]
1562         if filename_field:
1563             fields.append(filename_field)
1564         if id:
1565             res = Model.read([int(id)], fields, context)[0]
1566         else:
1567             res = Model.default_get(fields, context)
1568         filecontent = base64.b64decode(res.get(field, ''))
1569         if not filecontent:
1570             raise ValueError("No content found for field '%s' on '%s:%s'" %
1571                 (field, model, id))
1572         else:
1573             filename = '%s_%s' % (model.replace('.', '_'), id)
1574             if filename_field:
1575                 filename = res.get(filename_field, '') or filename
1576             return req.make_response(filecontent,
1577                 headers=[('Content-Type', 'application/octet-stream'),
1578                         ('Content-Disposition', content_disposition(filename, req))],
1579                 cookies={'fileToken': int(token)})
1580
1581     @openerpweb.httprequest
1582     def upload(self, req, callback, ufile):
1583         # TODO: might be useful to have a configuration flag for max-length file uploads
1584         try:
1585             out = """<script language="javascript" type="text/javascript">
1586                         var win = window.top.window;
1587                         win.jQuery(win).trigger(%s, %s);
1588                     </script>"""
1589             data = ufile.read()
1590             args = [len(data), ufile.filename,
1591                     ufile.content_type, base64.b64encode(data)]
1592         except Exception, e:
1593             args = [False, e.message]
1594         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1595
1596     @openerpweb.httprequest
1597     def upload_attachment(self, req, callback, model, id, ufile):
1598         context = req.session.eval_context(req.context)
1599         Model = req.session.model('ir.attachment')
1600         try:
1601             out = """<script language="javascript" type="text/javascript">
1602                         var win = window.top.window;
1603                         win.jQuery(win).trigger(%s, %s);
1604                     </script>"""
1605             attachment_id = Model.create({
1606                 'name': ufile.filename,
1607                 'datas': base64.encodestring(ufile.read()),
1608                 'datas_fname': ufile.filename,
1609                 'res_model': model,
1610                 'res_id': int(id)
1611             }, context)
1612             args = {
1613                 'filename': ufile.filename,
1614                 'id':  attachment_id
1615             }
1616         except Exception, e:
1617             args = { 'error': e.message }
1618         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1619
1620 class Action(openerpweb.Controller):
1621     _cp_path = "/web/action"
1622
1623     @openerpweb.jsonrequest
1624     def load(self, req, action_id, do_not_eval=False):
1625         Actions = req.session.model('ir.actions.actions')
1626         value = False
1627         context = req.session.eval_context(req.context)
1628
1629         try:
1630             action_id = int(action_id)
1631         except ValueError:
1632             try:
1633                 module, xmlid = action_id.split('.', 1)
1634                 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1635                 assert model.startswith('ir.actions.')
1636             except Exception:
1637                 action_id = 0   # force failed read
1638
1639         base_action = Actions.read([action_id], ['type'], context)
1640         if base_action:
1641             ctx = {}
1642             action_type = base_action[0]['type']
1643             if action_type == 'ir.actions.report.xml':
1644                 ctx.update({'bin_size': True})
1645             ctx.update(context)
1646             action = req.session.model(action_type).read([action_id], False, ctx)
1647             if action:
1648                 value = clean_action(req, action[0], do_not_eval)
1649         return value
1650
1651     @openerpweb.jsonrequest
1652     def run(self, req, action_id):
1653         return_action = req.session.model('ir.actions.server').run(
1654             [action_id], req.session.eval_context(req.context))
1655         if return_action:
1656             return clean_action(req, return_action)
1657         else:
1658             return False
1659
1660 class Export(View):
1661     _cp_path = "/web/export"
1662
1663     @openerpweb.jsonrequest
1664     def formats(self, req):
1665         """ Returns all valid export formats
1666
1667         :returns: for each export format, a pair of identifier and printable name
1668         :rtype: [(str, str)]
1669         """
1670         return sorted([
1671             controller.fmt
1672             for path, controller in openerpweb.controllers_path.iteritems()
1673             if path.startswith(self._cp_path)
1674             if hasattr(controller, 'fmt')
1675         ], key=operator.itemgetter("label"))
1676
1677     def fields_get(self, req, model):
1678         Model = req.session.model(model)
1679         fields = Model.fields_get(False, req.session.eval_context(req.context))
1680         return fields
1681
1682     @openerpweb.jsonrequest
1683     def get_fields(self, req, model, prefix='', parent_name= '',
1684                    import_compat=True, parent_field_type=None,
1685                    exclude=None):
1686
1687         if import_compat and parent_field_type == "many2one":
1688             fields = {}
1689         else:
1690             fields = self.fields_get(req, model)
1691
1692         if import_compat:
1693             fields.pop('id', None)
1694         else:
1695             fields['.id'] = fields.pop('id', {'string': 'ID'})
1696
1697         fields_sequence = sorted(fields.iteritems(),
1698             key=lambda field: field[1].get('string', ''))
1699
1700         records = []
1701         for field_name, field in fields_sequence:
1702             if import_compat:
1703                 if exclude and field_name in exclude:
1704                     continue
1705                 if field.get('readonly'):
1706                     # If none of the field's states unsets readonly, skip the field
1707                     if all(dict(attrs).get('readonly', True)
1708                            for attrs in field.get('states', {}).values()):
1709                         continue
1710
1711             id = prefix + (prefix and '/'or '') + field_name
1712             name = parent_name + (parent_name and '/' or '') + field['string']
1713             record = {'id': id, 'string': name,
1714                       'value': id, 'children': False,
1715                       'field_type': field.get('type'),
1716                       'required': field.get('required'),
1717                       'relation_field': field.get('relation_field')}
1718             records.append(record)
1719
1720             if len(name.split('/')) < 3 and 'relation' in field:
1721                 ref = field.pop('relation')
1722                 record['value'] += '/id'
1723                 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1724
1725                 if not import_compat or field['type'] == 'one2many':
1726                     # m2m field in import_compat is childless
1727                     record['children'] = True
1728
1729         return records
1730
1731     @openerpweb.jsonrequest
1732     def namelist(self,req,  model, export_id):
1733         # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1734         export = req.session.model("ir.exports").read([export_id])[0]
1735         export_fields_list = req.session.model("ir.exports.line").read(
1736             export['export_fields'])
1737
1738         fields_data = self.fields_info(
1739             req, model, map(operator.itemgetter('name'), export_fields_list))
1740
1741         return [
1742             {'name': field['name'], 'label': fields_data[field['name']]}
1743             for field in export_fields_list
1744         ]
1745
1746     def fields_info(self, req, model, export_fields):
1747         info = {}
1748         fields = self.fields_get(req, model)
1749
1750         # To make fields retrieval more efficient, fetch all sub-fields of a
1751         # given field at the same time. Because the order in the export list is
1752         # arbitrary, this requires ordering all sub-fields of a given field
1753         # together so they can be fetched at the same time
1754         #
1755         # Works the following way:
1756         # * sort the list of fields to export, the default sorting order will
1757         #   put the field itself (if present, for xmlid) and all of its
1758         #   sub-fields right after it
1759         # * then, group on: the first field of the path (which is the same for
1760         #   a field and for its subfields and the length of splitting on the
1761         #   first '/', which basically means grouping the field on one side and
1762         #   all of the subfields on the other. This way, we have the field (for
1763         #   the xmlid) with length 1, and all of the subfields with the same
1764         #   base but a length "flag" of 2
1765         # * if we have a normal field (length 1), just add it to the info
1766         #   mapping (with its string) as-is
1767         # * otherwise, recursively call fields_info via graft_subfields.
1768         #   all graft_subfields does is take the result of fields_info (on the
1769         #   field's model) and prepend the current base (current field), which
1770         #   rebuilds the whole sub-tree for the field
1771         #
1772         # result: because we're not fetching the fields_get for half the
1773         # database models, fetching a namelist with a dozen fields (including
1774         # relational data) falls from ~6s to ~300ms (on the leads model).
1775         # export lists with no sub-fields (e.g. import_compatible lists with
1776         # no o2m) are even more efficient (from the same 6s to ~170ms, as
1777         # there's a single fields_get to execute)
1778         for (base, length), subfields in itertools.groupby(
1779                 sorted(export_fields),
1780                 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1781             subfields = list(subfields)
1782             if length == 2:
1783                 # subfields is a seq of $base/*rest, and not loaded yet
1784                 info.update(self.graft_subfields(
1785                     req, fields[base]['relation'], base, fields[base]['string'],
1786                     subfields
1787                 ))
1788             else:
1789                 info[base] = fields[base]['string']
1790
1791         return info
1792
1793     def graft_subfields(self, req, model, prefix, prefix_string, fields):
1794         export_fields = [field.split('/', 1)[1] for field in fields]
1795         return (
1796             (prefix + '/' + k, prefix_string + '/' + v)
1797             for k, v in self.fields_info(req, model, export_fields).iteritems())
1798
1799     #noinspection PyPropertyDefinition
1800     @property
1801     def content_type(self):
1802         """ Provides the format's content type """
1803         raise NotImplementedError()
1804
1805     def filename(self, base):
1806         """ Creates a valid filename for the format (with extension) from the
1807          provided base name (exension-less)
1808         """
1809         raise NotImplementedError()
1810
1811     def from_data(self, fields, rows):
1812         """ Conversion method from OpenERP's export data to whatever the
1813         current export class outputs
1814
1815         :params list fields: a list of fields to export
1816         :params list rows: a list of records to export
1817         :returns:
1818         :rtype: bytes
1819         """
1820         raise NotImplementedError()
1821
1822     @openerpweb.httprequest
1823     def index(self, req, data, token):
1824         model, fields, ids, domain, import_compat = \
1825             operator.itemgetter('model', 'fields', 'ids', 'domain',
1826                                 'import_compat')(
1827                 simplejson.loads(data))
1828
1829         context = req.session.eval_context(req.context)
1830         Model = req.session.model(model)
1831         ids = ids or Model.search(domain, 0, False, False, context)
1832
1833         field_names = map(operator.itemgetter('name'), fields)
1834         import_data = Model.export_data(ids, field_names, context).get('datas',[])
1835
1836         if import_compat:
1837             columns_headers = field_names
1838         else:
1839             columns_headers = [val['label'].strip() for val in fields]
1840
1841
1842         return req.make_response(self.from_data(columns_headers, import_data),
1843             headers=[('Content-Disposition',
1844                             content_disposition(self.filename(model), req)),
1845                      ('Content-Type', self.content_type)],
1846             cookies={'fileToken': int(token)})
1847
1848 class CSVExport(Export):
1849     _cp_path = '/web/export/csv'
1850     fmt = {'tag': 'csv', 'label': 'CSV'}
1851
1852     @property
1853     def content_type(self):
1854         return 'text/csv;charset=utf8'
1855
1856     def filename(self, base):
1857         return base + '.csv'
1858
1859     def from_data(self, fields, rows):
1860         fp = StringIO()
1861         writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1862
1863         writer.writerow([name.encode('utf-8') for name in fields])
1864
1865         for data in rows:
1866             row = []
1867             for d in data:
1868                 if isinstance(d, basestring):
1869                     d = d.replace('\n',' ').replace('\t',' ')
1870                     try:
1871                         d = d.encode('utf-8')
1872                     except UnicodeError:
1873                         pass
1874                 if d is False: d = None
1875                 row.append(d)
1876             writer.writerow(row)
1877
1878         fp.seek(0)
1879         data = fp.read()
1880         fp.close()
1881         return data
1882
1883 class ExcelExport(Export):
1884     _cp_path = '/web/export/xls'
1885     fmt = {
1886         'tag': 'xls',
1887         'label': 'Excel',
1888         'error': None if xlwt else "XLWT required"
1889     }
1890
1891     @property
1892     def content_type(self):
1893         return 'application/vnd.ms-excel'
1894
1895     def filename(self, base):
1896         return base + '.xls'
1897
1898     def from_data(self, fields, rows):
1899         workbook = xlwt.Workbook()
1900         worksheet = workbook.add_sheet('Sheet 1')
1901
1902         for i, fieldname in enumerate(fields):
1903             worksheet.write(0, i, fieldname)
1904             worksheet.col(i).width = 8000 # around 220 pixels
1905
1906         style = xlwt.easyxf('align: wrap yes')
1907
1908         for row_index, row in enumerate(rows):
1909             for cell_index, cell_value in enumerate(row):
1910                 if isinstance(cell_value, basestring):
1911                     cell_value = re.sub("\r", " ", cell_value)
1912                 if cell_value is False: cell_value = None
1913                 worksheet.write(row_index + 1, cell_index, cell_value, style)
1914
1915         fp = StringIO()
1916         workbook.save(fp)
1917         fp.seek(0)
1918         data = fp.read()
1919         fp.close()
1920         return data
1921
1922 class Reports(View):
1923     _cp_path = "/web/report"
1924     POLLING_DELAY = 0.25
1925     TYPES_MAPPING = {
1926         'doc': 'application/vnd.ms-word',
1927         'html': 'text/html',
1928         'odt': 'application/vnd.oasis.opendocument.text',
1929         'pdf': 'application/pdf',
1930         'sxw': 'application/vnd.sun.xml.writer',
1931         'xls': 'application/vnd.ms-excel',
1932     }
1933
1934     @openerpweb.httprequest
1935     def index(self, req, action, token):
1936         action = simplejson.loads(action)
1937
1938         report_srv = req.session.proxy("report")
1939         context = req.session.eval_context(
1940             nonliterals.CompoundContext(
1941                 req.context or {}, action[ "context"]))
1942
1943         report_data = {}
1944         report_ids = context["active_ids"]
1945         if 'report_type' in action:
1946             report_data['report_type'] = action['report_type']
1947         if 'datas' in action:
1948             if 'ids' in action['datas']:
1949                 report_ids = action['datas'].pop('ids')
1950             report_data.update(action['datas'])
1951
1952         report_id = report_srv.report(
1953             req.session._db, req.session._uid, req.session._password,
1954             action["report_name"], report_ids,
1955             report_data, context)
1956
1957         report_struct = None
1958         while True:
1959             report_struct = report_srv.report_get(
1960                 req.session._db, req.session._uid, req.session._password, report_id)
1961             if report_struct["state"]:
1962                 break
1963
1964             time.sleep(self.POLLING_DELAY)
1965
1966         report = base64.b64decode(report_struct['result'])
1967         if report_struct.get('code') == 'zlib':
1968             report = zlib.decompress(report)
1969         report_mimetype = self.TYPES_MAPPING.get(
1970             report_struct['format'], 'octet-stream')
1971         file_name = action.get('name', 'report')
1972         if 'name' not in action:
1973             reports = req.session.model('ir.actions.report.xml')
1974             res_id = reports.search([('report_name', '=', action['report_name']),],
1975                                     0, False, False, context)
1976             if len(res_id) > 0:
1977                 file_name = reports.read(res_id[0], ['name'], context)['name']
1978             else:
1979                 file_name = action['report_name']
1980         file_name = '%s.%s' % (file_name, report_struct['format'])
1981
1982         return req.make_response(report,
1983              headers=[
1984                  ('Content-Disposition', content_disposition(file_name, req)),
1985                  ('Content-Type', report_mimetype),
1986                  ('Content-Length', len(report))],
1987              cookies={'fileToken': int(token)})
1988
1989 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: