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