[IMP] minify cache
[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':'All passwords have to be filled.','title': 'Change Password'}
894         if new_password != confirm_password:
895             return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
896         try:
897             if req.session.model('res.users').change_password(
898                 old_password, new_password):
899                 return {'new_password':new_password}
900         except Exception:
901             return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
902         return {'error': 'Error, password not changed !', 'title': 'Change Password'}
903
904     @openerpweb.jsonrequest
905     def sc_list(self, req):
906         return req.session.model('ir.ui.view_sc').get_sc(
907             req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
908
909     @openerpweb.jsonrequest
910     def get_lang_list(self, req):
911         try:
912             return req.session.proxy("db").list_lang() or []
913         except Exception, e:
914             return {"error": e, "title": "Languages"}
915
916     @openerpweb.jsonrequest
917     def modules(self, req):
918         # return all installed modules. Web client is smart enough to not load a module twice
919         return module_installed(req)
920
921     @openerpweb.jsonrequest
922     def eval_domain_and_context(self, req, contexts, domains,
923                                 group_by_seq=None):
924         """ Evaluates sequences of domains and contexts, composing them into
925         a single context, domain or group_by sequence.
926
927         :param list contexts: list of contexts to merge together. Contexts are
928                               evaluated in sequence, all previous contexts
929                               are part of their own evaluation context
930                               (starting at the session context).
931         :param list domains: list of domains to merge together. Domains are
932                              evaluated in sequence and appended to one another
933                              (implicit AND), their evaluation domain is the
934                              result of merging all contexts.
935         :param list group_by_seq: list of domains (which may be in a different
936                                   order than the ``contexts`` parameter),
937                                   evaluated in sequence, their ``'group_by'``
938                                   key is extracted if they have one.
939         :returns:
940             a 3-dict of:
941
942             context (``dict``)
943                 the global context created by merging all of
944                 ``contexts``
945
946             domain (``list``)
947                 the concatenation of all domains
948
949             group_by (``list``)
950                 a list of fields to group by, potentially empty (in which case
951                 no group by should be performed)
952         """
953         context, domain = eval_context_and_domain(req.session,
954                                                   nonliterals.CompoundContext(*(contexts or [])),
955                                                   nonliterals.CompoundDomain(*(domains or [])))
956
957         group_by_sequence = []
958         for candidate in (group_by_seq or []):
959             ctx = req.session.eval_context(candidate, context)
960             group_by = ctx.get('group_by')
961             if not group_by:
962                 continue
963             elif isinstance(group_by, basestring):
964                 group_by_sequence.append(group_by)
965             else:
966                 group_by_sequence.extend(group_by)
967
968         return {
969             'context': context,
970             'domain': domain,
971             'group_by': group_by_sequence
972         }
973
974     @openerpweb.jsonrequest
975     def save_session_action(self, req, the_action):
976         """
977         This method store an action object in the session object and returns an integer
978         identifying that action. The method get_session_action() can be used to get
979         back the action.
980
981         :param the_action: The action to save in the session.
982         :type the_action: anything
983         :return: A key identifying the saved action.
984         :rtype: integer
985         """
986         saved_actions = req.httpsession.get('saved_actions')
987         if not saved_actions:
988             saved_actions = {"next":0, "actions":{}}
989             req.httpsession['saved_actions'] = saved_actions
990         # we don't allow more than 10 stored actions
991         if len(saved_actions["actions"]) >= 10:
992             del saved_actions["actions"][min(saved_actions["actions"])]
993         key = saved_actions["next"]
994         saved_actions["actions"][key] = the_action
995         saved_actions["next"] = key + 1
996         return key
997
998     @openerpweb.jsonrequest
999     def get_session_action(self, req, key):
1000         """
1001         Gets back a previously saved action. This method can return None if the action
1002         was saved since too much time (this case should be handled in a smart way).
1003
1004         :param key: The key given by save_session_action()
1005         :type key: integer
1006         :return: The saved action or None.
1007         :rtype: anything
1008         """
1009         saved_actions = req.httpsession.get('saved_actions')
1010         if not saved_actions:
1011             return None
1012         return saved_actions["actions"].get(key)
1013
1014     @openerpweb.jsonrequest
1015     def check(self, req):
1016         req.session.assert_valid()
1017         return None
1018
1019     @openerpweb.jsonrequest
1020     def destroy(self, req):
1021         req.session._suicide = True
1022
1023 class Menu(openerpweb.Controller):
1024     _cp_path = "/web/menu"
1025
1026     @openerpweb.jsonrequest
1027     def load(self, req):
1028         return {'data': self.do_load(req)}
1029
1030     @openerpweb.jsonrequest
1031     def get_user_roots(self, req):
1032         return self.do_get_user_roots(req)
1033
1034     def do_get_user_roots(self, req):
1035         """ Return all root menu ids visible for the session user.
1036
1037         :param req: A request object, with an OpenERP session attribute
1038         :type req: < session -> OpenERPSession >
1039         :return: the root menu ids
1040         :rtype: list(int)
1041         """
1042         s = req.session
1043         context = s.eval_context(req.context)
1044         Menus = s.model('ir.ui.menu')
1045         # If a menu action is defined use its domain to get the root menu items
1046         user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
1047
1048         menu_domain = [('parent_id', '=', False)]
1049         if user_menu_id:
1050             domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
1051             if domain_string:
1052                 menu_domain = ast.literal_eval(domain_string)
1053
1054         return Menus.search(menu_domain, 0, False, False, context)
1055
1056     def do_load(self, req):
1057         """ Loads all menu items (all applications and their sub-menus).
1058
1059         :param req: A request object, with an OpenERP session attribute
1060         :type req: < session -> OpenERPSession >
1061         :return: the menu root
1062         :rtype: dict('children': menu_nodes)
1063         """
1064         context = req.session.eval_context(req.context)
1065         Menus = req.session.model('ir.ui.menu')
1066
1067         menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1068         menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
1069
1070         # menus are loaded fully unlike a regular tree view, cause there are a
1071         # limited number of items (752 when all 6.1 addons are installed)
1072         menu_ids = Menus.search([], 0, False, False, context)
1073         menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1074         # adds roots at the end of the sequence, so that they will overwrite
1075         # equivalent menu items from full menu read when put into id:item
1076         # mapping, resulting in children being correctly set on the roots.
1077         menu_items.extend(menu_roots)
1078
1079         # make a tree using parent_id
1080         menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1081         for menu_item in menu_items:
1082             if menu_item['parent_id']:
1083                 parent = menu_item['parent_id'][0]
1084             else:
1085                 parent = False
1086             if parent in menu_items_map:
1087                 menu_items_map[parent].setdefault(
1088                     'children', []).append(menu_item)
1089
1090         # sort by sequence a tree using parent_id
1091         for menu_item in menu_items:
1092             menu_item.setdefault('children', []).sort(
1093                 key=operator.itemgetter('sequence'))
1094
1095         return menu_root
1096
1097     @openerpweb.jsonrequest
1098     def action(self, req, menu_id):
1099         actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1100                                              [('ir.ui.menu', menu_id)], False)
1101         return {"action": actions}
1102
1103 class DataSet(openerpweb.Controller):
1104     _cp_path = "/web/dataset"
1105
1106     @openerpweb.jsonrequest
1107     def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1108         return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1109     def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1110                        , sort=None):
1111         """ Performs a search() followed by a read() (if needed) using the
1112         provided search criteria
1113
1114         :param req: a JSON-RPC request object
1115         :type req: openerpweb.JsonRequest
1116         :param str model: the name of the model to search on
1117         :param fields: a list of the fields to return in the result records
1118         :type fields: [str]
1119         :param int offset: from which index should the results start being returned
1120         :param int limit: the maximum number of records to return
1121         :param list domain: the search domain for the query
1122         :param list sort: sorting directives
1123         :returns: A structure (dict) with two keys: ids (all the ids matching
1124                   the (domain, context) pair) and records (paginated records
1125                   matching fields selection set)
1126         :rtype: list
1127         """
1128         Model = req.session.model(model)
1129
1130         context, domain = eval_context_and_domain(
1131             req.session, req.context, domain)
1132
1133         ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1134         if limit and len(ids) == limit:
1135             length = Model.search_count(domain, context)
1136         else:
1137             length = len(ids) + (offset or 0)
1138         if fields and fields == ['id']:
1139             # shortcut read if we only want the ids
1140             return {
1141                 'length': length,
1142                 'records': [{'id': id} for id in ids]
1143             }
1144
1145         records = Model.read(ids, fields or False, context)
1146         records.sort(key=lambda obj: ids.index(obj['id']))
1147         return {
1148             'length': length,
1149             'records': records
1150         }
1151
1152     @openerpweb.jsonrequest
1153     def load(self, req, model, id, fields):
1154         m = req.session.model(model)
1155         value = {}
1156         r = m.read([id], False, req.session.eval_context(req.context))
1157         if r:
1158             value = r[0]
1159         return {'value': value}
1160
1161     def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1162         has_domain = domain_id is not None and domain_id < len(args)
1163         has_context = context_id is not None and context_id < len(args)
1164
1165         domain = args[domain_id] if has_domain else []
1166         context = args[context_id] if has_context else {}
1167         c, d = eval_context_and_domain(req.session, context, domain)
1168         if has_domain:
1169             args[domain_id] = d
1170         if has_context:
1171             args[context_id] = c
1172
1173         return self._call_kw(req, model, method, args, {})
1174     
1175     def _call_kw(self, req, model, method, args, kwargs):
1176         for i in xrange(len(args)):
1177             if isinstance(args[i], nonliterals.BaseContext):
1178                 args[i] = req.session.eval_context(args[i])
1179             elif isinstance(args[i], nonliterals.BaseDomain):
1180                 args[i] = req.session.eval_domain(args[i])
1181         for k in kwargs.keys():
1182             if isinstance(kwargs[k], nonliterals.BaseContext):
1183                 kwargs[k] = req.session.eval_context(kwargs[k])
1184             elif isinstance(kwargs[k], nonliterals.BaseDomain):
1185                 kwargs[k] = req.session.eval_domain(kwargs[k])
1186
1187         # Temporary implements future display_name special field for model#read()
1188         if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1189             if 'display_name' in args[1]:
1190                 names = req.session.model(model).name_get(args[0], **kwargs)
1191                 args[1].remove('display_name')
1192                 r = getattr(req.session.model(model), method)(*args, **kwargs)
1193                 for i in range(len(r)):
1194                     r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1195                 return r
1196
1197         return getattr(req.session.model(model), method)(*args, **kwargs)
1198
1199     @openerpweb.jsonrequest
1200     def onchange(self, req, model, method, args, context_id=None):
1201         """ Support method for handling onchange calls: behaves much like call
1202         with the following differences:
1203
1204         * Does not take a domain_id
1205         * Is aware of the return value's structure, and will parse the domains
1206           if needed in order to return either parsed literal domains (in JSON)
1207           or non-literal domain instances, allowing those domains to be used
1208           from JS
1209
1210         :param req:
1211         :type req: web.common.http.JsonRequest
1212         :param str model: object type on which to call the method
1213         :param str method: name of the onchange handler method
1214         :param list args: arguments to call the onchange handler with
1215         :param int context_id: index of the context object in the list of
1216                                arguments
1217         :return: result of the onchange call with all domains parsed
1218         """
1219         result = self.call_common(req, model, method, args, context_id=context_id)
1220         if not result or 'domain' not in result:
1221             return result
1222
1223         result['domain'] = dict(
1224             (k, parse_domain(v, req.session))
1225             for k, v in result['domain'].iteritems())
1226
1227         return result
1228
1229     @openerpweb.jsonrequest
1230     def call(self, req, model, method, args, domain_id=None, context_id=None):
1231         return self.call_common(req, model, method, args, domain_id, context_id)
1232     
1233     @openerpweb.jsonrequest
1234     def call_kw(self, req, model, method, args, kwargs):
1235         return self._call_kw(req, model, method, args, kwargs)
1236
1237     @openerpweb.jsonrequest
1238     def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1239         action = self.call_common(req, model, method, args, domain_id, context_id)
1240         if isinstance(action, dict) and action.get('type') != '':
1241             return clean_action(req, action)
1242         return False
1243
1244     @openerpweb.jsonrequest
1245     def exec_workflow(self, req, model, id, signal):
1246         return req.session.exec_workflow(model, id, signal)
1247
1248     @openerpweb.jsonrequest
1249     def resequence(self, req, model, ids, field='sequence', offset=0):
1250         """ Re-sequences a number of records in the model, by their ids
1251
1252         The re-sequencing starts at the first model of ``ids``, the sequence
1253         number is incremented by one after each record and starts at ``offset``
1254
1255         :param ids: identifiers of the records to resequence, in the new sequence order
1256         :type ids: list(id)
1257         :param str field: field used for sequence specification, defaults to
1258                           "sequence"
1259         :param int offset: sequence number for first record in ``ids``, allows
1260                            starting the resequencing from an arbitrary number,
1261                            defaults to ``0``
1262         """
1263         m = req.session.model(model)
1264         if not m.fields_get([field]):
1265             return False
1266         # python 2.6 has no start parameter
1267         for i, id in enumerate(ids):
1268             m.write(id, { field: i + offset })
1269         return True
1270
1271 class View(openerpweb.Controller):
1272     _cp_path = "/web/view"
1273
1274     def fields_view_get(self, req, model, view_id, view_type,
1275                         transform=True, toolbar=False, submenu=False):
1276         Model = req.session.model(model)
1277         context = req.session.eval_context(req.context)
1278         fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1279         # todo fme?: check that we should pass the evaluated context here
1280         self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1281         if toolbar and transform:
1282             self.process_toolbar(req, fvg['toolbar'])
1283         return fvg
1284
1285     def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1286         # depending on how it feels, xmlrpclib.ServerProxy can translate
1287         # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1288         # enjoy unicode strings which can not be trivially converted to
1289         # strings, and it blows up during parsing.
1290
1291         # So ensure we fix this retardation by converting view xml back to
1292         # bit strings.
1293         if isinstance(fvg['arch'], unicode):
1294             arch = fvg['arch'].encode('utf-8')
1295         else:
1296             arch = fvg['arch']
1297         fvg['arch_string'] = arch
1298
1299         if transform:
1300             evaluation_context = session.evaluation_context(context or {})
1301             xml = self.transform_view(arch, session, evaluation_context)
1302         else:
1303             xml = ElementTree.fromstring(arch)
1304         fvg['arch'] = xml2json_from_elementtree(xml, preserve_whitespaces)
1305
1306         if 'id' in fvg['fields']:
1307             # Special case for id's
1308             id_field = fvg['fields']['id']
1309             id_field['original_type'] = id_field['type']
1310             id_field['type'] = 'id'
1311
1312         for field in fvg['fields'].itervalues():
1313             if field.get('views'):
1314                 for view in field["views"].itervalues():
1315                     self.process_view(session, view, None, transform)
1316             if field.get('domain'):
1317                 field["domain"] = parse_domain(field["domain"], session)
1318             if field.get('context'):
1319                 field["context"] = parse_context(field["context"], session)
1320
1321     def process_toolbar(self, req, toolbar):
1322         """
1323         The toolbar is a mapping of section_key: [action_descriptor]
1324
1325         We need to clean all those actions in order to ensure correct
1326         round-tripping
1327         """
1328         for actions in toolbar.itervalues():
1329             for action in actions:
1330                 if 'context' in action:
1331                     action['context'] = parse_context(
1332                         action['context'], req.session)
1333                 if 'domain' in action:
1334                     action['domain'] = parse_domain(
1335                         action['domain'], req.session)
1336
1337     @openerpweb.jsonrequest
1338     def add_custom(self, req, view_id, arch):
1339         CustomView = req.session.model('ir.ui.view.custom')
1340         CustomView.create({
1341             'user_id': req.session._uid,
1342             'ref_id': view_id,
1343             'arch': arch
1344         }, req.session.eval_context(req.context))
1345         return {'result': True}
1346
1347     @openerpweb.jsonrequest
1348     def undo_custom(self, req, view_id, reset=False):
1349         CustomView = req.session.model('ir.ui.view.custom')
1350         context = req.session.eval_context(req.context)
1351         vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1352                                     0, False, False, context)
1353         if vcustom:
1354             if reset:
1355                 CustomView.unlink(vcustom, context)
1356             else:
1357                 CustomView.unlink([vcustom[0]], context)
1358             return {'result': True}
1359         return {'result': False}
1360
1361     def transform_view(self, view_string, session, context=None):
1362         # transform nodes on the fly via iterparse, instead of
1363         # doing it statically on the parsing result
1364         parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1365         root = None
1366         for event, elem in parser:
1367             if event == "start":
1368                 if root is None:
1369                     root = elem
1370                 self.parse_domains_and_contexts(elem, session)
1371         return root
1372
1373     def parse_domains_and_contexts(self, elem, session):
1374         """ Converts domains and contexts from the view into Python objects,
1375         either literals if they can be parsed by literal_eval or a special
1376         placeholder object if the domain or context refers to free variables.
1377
1378         :param elem: the current node being parsed
1379         :type param: xml.etree.ElementTree.Element
1380         :param session: OpenERP session object, used to store and retrieve
1381                         non-literal objects
1382         :type session: openerpweb.openerpweb.OpenERPSession
1383         """
1384         for el in ['domain', 'filter_domain']:
1385             domain = elem.get(el, '').strip()
1386             if domain:
1387                 elem.set(el, parse_domain(domain, session))
1388                 elem.set(el + '_string', domain)
1389         for el in ['context', 'default_get']:
1390             context_string = elem.get(el, '').strip()
1391             if context_string:
1392                 elem.set(el, parse_context(context_string, session))
1393                 elem.set(el + '_string', context_string)
1394
1395     @openerpweb.jsonrequest
1396     def load(self, req, model, view_id, view_type, toolbar=False):
1397         return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1398
1399 class TreeView(View):
1400     _cp_path = "/web/treeview"
1401
1402     @openerpweb.jsonrequest
1403     def action(self, req, model, id):
1404         return load_actions_from_ir_values(
1405             req,'action', 'tree_but_open',[(model, id)],
1406             False)
1407
1408 class SearchView(View):
1409     _cp_path = "/web/searchview"
1410
1411     @openerpweb.jsonrequest
1412     def load(self, req, model, view_id):
1413         fields_view = self.fields_view_get(req, model, view_id, 'search')
1414         return {'fields_view': fields_view}
1415
1416     @openerpweb.jsonrequest
1417     def fields_get(self, req, model):
1418         Model = req.session.model(model)
1419         fields = Model.fields_get(False, req.session.eval_context(req.context))
1420         for field in fields.values():
1421             # shouldn't convert the views too?
1422             if field.get('domain'):
1423                 field["domain"] = parse_domain(field["domain"], req.session)
1424             if field.get('context'):
1425                 field["context"] = parse_context(field["context"], req.session)
1426         return {'fields': fields}
1427
1428     @openerpweb.jsonrequest
1429     def get_filters(self, req, model):
1430         logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1431         Model = req.session.model("ir.filters")
1432         filters = Model.get_filters(model)
1433         for filter in filters:
1434             try:
1435                 parsed_context = parse_context(filter["context"], req.session)
1436                 filter["context"] = (parsed_context
1437                         if not isinstance(parsed_context, nonliterals.BaseContext)
1438                         else req.session.eval_context(parsed_context))
1439
1440                 parsed_domain = parse_domain(filter["domain"], req.session)
1441                 filter["domain"] = (parsed_domain
1442                         if not isinstance(parsed_domain, nonliterals.BaseDomain)
1443                         else req.session.eval_domain(parsed_domain))
1444             except Exception:
1445                 logger.exception("Failed to parse custom filter %s in %s",
1446                                  filter['name'], model)
1447                 filter['disabled'] = True
1448                 del filter['context']
1449                 del filter['domain']
1450         return filters
1451
1452 class Binary(openerpweb.Controller):
1453     _cp_path = "/web/binary"
1454
1455     @openerpweb.httprequest
1456     def image(self, req, model, id, field, **kw):
1457         last_update = '__last_update'
1458         Model = req.session.model(model)
1459         context = req.session.eval_context(req.context)
1460         headers = [('Content-Type', 'image/png')]
1461         etag = req.httprequest.headers.get('If-None-Match')
1462         hashed_session = hashlib.md5(req.session_id).hexdigest()
1463         id = None if not id else simplejson.loads(id)
1464         if type(id) is list:
1465             id = id[0] # m2o
1466         if etag:
1467             if not id and hashed_session == etag:
1468                 return werkzeug.wrappers.Response(status=304)
1469             else:
1470                 date = Model.read([id], [last_update], context)[0].get(last_update)
1471                 if hashlib.md5(date).hexdigest() == etag:
1472                     return werkzeug.wrappers.Response(status=304)
1473
1474         retag = hashed_session
1475         try:
1476             if not id:
1477                 res = Model.default_get([field], context).get(field)
1478                 image_base64 = res
1479             else:
1480                 res = Model.read([id], [last_update, field], context)[0]
1481                 retag = hashlib.md5(res.get(last_update)).hexdigest()
1482                 image_base64 = res.get(field)
1483
1484             if kw.get('resize'):
1485                 resize = kw.get('resize').split(',');
1486                 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1487                     width = int(resize[0])
1488                     height = int(resize[1])
1489                     # resize maximum 500*500
1490                     if width > 500: width = 500
1491                     if height > 500: height = 500
1492                     image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1493             
1494             image_data = base64.b64decode(image_base64)
1495
1496         except (TypeError, xmlrpclib.Fault):
1497             image_data = self.placeholder(req)
1498         headers.append(('ETag', retag))
1499         headers.append(('Content-Length', len(image_data)))
1500         try:
1501             ncache = int(kw.get('cache'))
1502             headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1503         except:
1504             pass
1505         return req.make_response(image_data, headers)
1506     def placeholder(self, req):
1507         addons_path = openerpweb.addons_manifest['web']['addons_path']
1508         return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1509
1510     @openerpweb.httprequest
1511     def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1512         """ Download link for files stored as binary fields.
1513
1514         If the ``id`` parameter is omitted, fetches the default value for the
1515         binary field (via ``default_get``), otherwise fetches the field for
1516         that precise record.
1517
1518         :param req: OpenERP request
1519         :type req: :class:`web.common.http.HttpRequest`
1520         :param str model: name of the model to fetch the binary from
1521         :param str field: binary field
1522         :param str id: id of the record from which to fetch the binary
1523         :param str filename_field: field holding the file's name, if any
1524         :returns: :class:`werkzeug.wrappers.Response`
1525         """
1526         Model = req.session.model(model)
1527         context = req.session.eval_context(req.context)
1528         fields = [field]
1529         if filename_field:
1530             fields.append(filename_field)
1531         if id:
1532             res = Model.read([int(id)], fields, context)[0]
1533         else:
1534             res = Model.default_get(fields, context)
1535         filecontent = base64.b64decode(res.get(field, ''))
1536         if not filecontent:
1537             return req.not_found()
1538         else:
1539             filename = '%s_%s' % (model.replace('.', '_'), id)
1540             if filename_field:
1541                 filename = res.get(filename_field, '') or filename
1542             return req.make_response(filecontent,
1543                 [('Content-Type', 'application/octet-stream'),
1544                  ('Content-Disposition', content_disposition(filename, req))])
1545
1546     @openerpweb.httprequest
1547     def saveas_ajax(self, req, data, token):
1548         jdata = simplejson.loads(data)
1549         model = jdata['model']
1550         field = jdata['field']
1551         id = jdata.get('id', None)
1552         filename_field = jdata.get('filename_field', None)
1553         context = jdata.get('context', dict())
1554
1555         context = req.session.eval_context(context)
1556         Model = req.session.model(model)
1557         fields = [field]
1558         if filename_field:
1559             fields.append(filename_field)
1560         if id:
1561             res = Model.read([int(id)], fields, context)[0]
1562         else:
1563             res = Model.default_get(fields, context)
1564         filecontent = base64.b64decode(res.get(field, ''))
1565         if not filecontent:
1566             raise ValueError("No content found for field '%s' on '%s:%s'" %
1567                 (field, model, id))
1568         else:
1569             filename = '%s_%s' % (model.replace('.', '_'), id)
1570             if filename_field:
1571                 filename = res.get(filename_field, '') or filename
1572             return req.make_response(filecontent,
1573                 headers=[('Content-Type', 'application/octet-stream'),
1574                         ('Content-Disposition', content_disposition(filename, req))],
1575                 cookies={'fileToken': int(token)})
1576
1577     @openerpweb.httprequest
1578     def upload(self, req, callback, ufile):
1579         # TODO: might be useful to have a configuration flag for max-length file uploads
1580         try:
1581             out = """<script language="javascript" type="text/javascript">
1582                         var win = window.top.window;
1583                         win.jQuery(win).trigger(%s, %s);
1584                     </script>"""
1585             data = ufile.read()
1586             args = [len(data), ufile.filename,
1587                     ufile.content_type, base64.b64encode(data)]
1588         except Exception, e:
1589             args = [False, e.message]
1590         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1591
1592     @openerpweb.httprequest
1593     def upload_attachment(self, req, callback, model, id, ufile):
1594         context = req.session.eval_context(req.context)
1595         Model = req.session.model('ir.attachment')
1596         try:
1597             out = """<script language="javascript" type="text/javascript">
1598                         var win = window.top.window;
1599                         win.jQuery(win).trigger(%s, %s);
1600                     </script>"""
1601             attachment_id = Model.create({
1602                 'name': ufile.filename,
1603                 'datas': base64.encodestring(ufile.read()),
1604                 'datas_fname': ufile.filename,
1605                 'res_model': model,
1606                 'res_id': int(id)
1607             }, context)
1608             args = {
1609                 'filename': ufile.filename,
1610                 'id':  attachment_id
1611             }
1612         except Exception, e:
1613             args = { 'error': e.message }
1614         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1615
1616 class Action(openerpweb.Controller):
1617     _cp_path = "/web/action"
1618
1619     @openerpweb.jsonrequest
1620     def load(self, req, action_id, do_not_eval=False):
1621         Actions = req.session.model('ir.actions.actions')
1622         value = False
1623         context = req.session.eval_context(req.context)
1624
1625         try:
1626             action_id = int(action_id)
1627         except ValueError:
1628             try:
1629                 module, xmlid = action_id.split('.', 1)
1630                 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1631                 assert model.startswith('ir.actions.')
1632             except Exception:
1633                 action_id = 0   # force failed read
1634
1635         base_action = Actions.read([action_id], ['type'], context)
1636         if base_action:
1637             ctx = {}
1638             action_type = base_action[0]['type']
1639             if action_type == 'ir.actions.report.xml':
1640                 ctx.update({'bin_size': True})
1641             ctx.update(context)
1642             action = req.session.model(action_type).read([action_id], False, ctx)
1643             if action:
1644                 value = clean_action(req, action[0], do_not_eval)
1645         return value
1646
1647     @openerpweb.jsonrequest
1648     def run(self, req, action_id):
1649         return_action = req.session.model('ir.actions.server').run(
1650             [action_id], req.session.eval_context(req.context))
1651         if return_action:
1652             return clean_action(req, return_action)
1653         else:
1654             return False
1655
1656 class Export(View):
1657     _cp_path = "/web/export"
1658
1659     @openerpweb.jsonrequest
1660     def formats(self, req):
1661         """ Returns all valid export formats
1662
1663         :returns: for each export format, a pair of identifier and printable name
1664         :rtype: [(str, str)]
1665         """
1666         return sorted([
1667             controller.fmt
1668             for path, controller in openerpweb.controllers_path.iteritems()
1669             if path.startswith(self._cp_path)
1670             if hasattr(controller, 'fmt')
1671         ], key=operator.itemgetter("label"))
1672
1673     def fields_get(self, req, model):
1674         Model = req.session.model(model)
1675         fields = Model.fields_get(False, req.session.eval_context(req.context))
1676         return fields
1677
1678     @openerpweb.jsonrequest
1679     def get_fields(self, req, model, prefix='', parent_name= '',
1680                    import_compat=True, parent_field_type=None,
1681                    exclude=None):
1682
1683         if import_compat and parent_field_type == "many2one":
1684             fields = {}
1685         else:
1686             fields = self.fields_get(req, model)
1687
1688         if import_compat:
1689             fields.pop('id', None)
1690         else:
1691             fields['.id'] = fields.pop('id', {'string': 'ID'})
1692
1693         fields_sequence = sorted(fields.iteritems(),
1694             key=lambda field: field[1].get('string', ''))
1695
1696         records = []
1697         for field_name, field in fields_sequence:
1698             if import_compat:
1699                 if exclude and field_name in exclude:
1700                     continue
1701                 if field.get('readonly'):
1702                     # If none of the field's states unsets readonly, skip the field
1703                     if all(dict(attrs).get('readonly', True)
1704                            for attrs in field.get('states', {}).values()):
1705                         continue
1706
1707             id = prefix + (prefix and '/'or '') + field_name
1708             name = parent_name + (parent_name and '/' or '') + field['string']
1709             record = {'id': id, 'string': name,
1710                       'value': id, 'children': False,
1711                       'field_type': field.get('type'),
1712                       'required': field.get('required'),
1713                       'relation_field': field.get('relation_field')}
1714             records.append(record)
1715
1716             if len(name.split('/')) < 3 and 'relation' in field:
1717                 ref = field.pop('relation')
1718                 record['value'] += '/id'
1719                 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1720
1721                 if not import_compat or field['type'] == 'one2many':
1722                     # m2m field in import_compat is childless
1723                     record['children'] = True
1724
1725         return records
1726
1727     @openerpweb.jsonrequest
1728     def namelist(self,req,  model, export_id):
1729         # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1730         export = req.session.model("ir.exports").read([export_id])[0]
1731         export_fields_list = req.session.model("ir.exports.line").read(
1732             export['export_fields'])
1733
1734         fields_data = self.fields_info(
1735             req, model, map(operator.itemgetter('name'), export_fields_list))
1736
1737         return [
1738             {'name': field['name'], 'label': fields_data[field['name']]}
1739             for field in export_fields_list
1740         ]
1741
1742     def fields_info(self, req, model, export_fields):
1743         info = {}
1744         fields = self.fields_get(req, model)
1745
1746         # To make fields retrieval more efficient, fetch all sub-fields of a
1747         # given field at the same time. Because the order in the export list is
1748         # arbitrary, this requires ordering all sub-fields of a given field
1749         # together so they can be fetched at the same time
1750         #
1751         # Works the following way:
1752         # * sort the list of fields to export, the default sorting order will
1753         #   put the field itself (if present, for xmlid) and all of its
1754         #   sub-fields right after it
1755         # * then, group on: the first field of the path (which is the same for
1756         #   a field and for its subfields and the length of splitting on the
1757         #   first '/', which basically means grouping the field on one side and
1758         #   all of the subfields on the other. This way, we have the field (for
1759         #   the xmlid) with length 1, and all of the subfields with the same
1760         #   base but a length "flag" of 2
1761         # * if we have a normal field (length 1), just add it to the info
1762         #   mapping (with its string) as-is
1763         # * otherwise, recursively call fields_info via graft_subfields.
1764         #   all graft_subfields does is take the result of fields_info (on the
1765         #   field's model) and prepend the current base (current field), which
1766         #   rebuilds the whole sub-tree for the field
1767         #
1768         # result: because we're not fetching the fields_get for half the
1769         # database models, fetching a namelist with a dozen fields (including
1770         # relational data) falls from ~6s to ~300ms (on the leads model).
1771         # export lists with no sub-fields (e.g. import_compatible lists with
1772         # no o2m) are even more efficient (from the same 6s to ~170ms, as
1773         # there's a single fields_get to execute)
1774         for (base, length), subfields in itertools.groupby(
1775                 sorted(export_fields),
1776                 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1777             subfields = list(subfields)
1778             if length == 2:
1779                 # subfields is a seq of $base/*rest, and not loaded yet
1780                 info.update(self.graft_subfields(
1781                     req, fields[base]['relation'], base, fields[base]['string'],
1782                     subfields
1783                 ))
1784             else:
1785                 info[base] = fields[base]['string']
1786
1787         return info
1788
1789     def graft_subfields(self, req, model, prefix, prefix_string, fields):
1790         export_fields = [field.split('/', 1)[1] for field in fields]
1791         return (
1792             (prefix + '/' + k, prefix_string + '/' + v)
1793             for k, v in self.fields_info(req, model, export_fields).iteritems())
1794
1795     #noinspection PyPropertyDefinition
1796     @property
1797     def content_type(self):
1798         """ Provides the format's content type """
1799         raise NotImplementedError()
1800
1801     def filename(self, base):
1802         """ Creates a valid filename for the format (with extension) from the
1803          provided base name (exension-less)
1804         """
1805         raise NotImplementedError()
1806
1807     def from_data(self, fields, rows):
1808         """ Conversion method from OpenERP's export data to whatever the
1809         current export class outputs
1810
1811         :params list fields: a list of fields to export
1812         :params list rows: a list of records to export
1813         :returns:
1814         :rtype: bytes
1815         """
1816         raise NotImplementedError()
1817
1818     @openerpweb.httprequest
1819     def index(self, req, data, token):
1820         model, fields, ids, domain, import_compat = \
1821             operator.itemgetter('model', 'fields', 'ids', 'domain',
1822                                 'import_compat')(
1823                 simplejson.loads(data))
1824
1825         context = req.session.eval_context(req.context)
1826         Model = req.session.model(model)
1827         ids = ids or Model.search(domain, 0, False, False, context)
1828
1829         field_names = map(operator.itemgetter('name'), fields)
1830         import_data = Model.export_data(ids, field_names, context).get('datas',[])
1831
1832         if import_compat:
1833             columns_headers = field_names
1834         else:
1835             columns_headers = [val['label'].strip() for val in fields]
1836
1837
1838         return req.make_response(self.from_data(columns_headers, import_data),
1839             headers=[('Content-Disposition',
1840                             content_disposition(self.filename(model), req)),
1841                      ('Content-Type', self.content_type)],
1842             cookies={'fileToken': int(token)})
1843
1844 class CSVExport(Export):
1845     _cp_path = '/web/export/csv'
1846     fmt = {'tag': 'csv', 'label': 'CSV'}
1847
1848     @property
1849     def content_type(self):
1850         return 'text/csv;charset=utf8'
1851
1852     def filename(self, base):
1853         return base + '.csv'
1854
1855     def from_data(self, fields, rows):
1856         fp = StringIO()
1857         writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1858
1859         writer.writerow([name.encode('utf-8') for name in fields])
1860
1861         for data in rows:
1862             row = []
1863             for d in data:
1864                 if isinstance(d, basestring):
1865                     d = d.replace('\n',' ').replace('\t',' ')
1866                     try:
1867                         d = d.encode('utf-8')
1868                     except UnicodeError:
1869                         pass
1870                 if d is False: d = None
1871                 row.append(d)
1872             writer.writerow(row)
1873
1874         fp.seek(0)
1875         data = fp.read()
1876         fp.close()
1877         return data
1878
1879 class ExcelExport(Export):
1880     _cp_path = '/web/export/xls'
1881     fmt = {
1882         'tag': 'xls',
1883         'label': 'Excel',
1884         'error': None if xlwt else "XLWT required"
1885     }
1886
1887     @property
1888     def content_type(self):
1889         return 'application/vnd.ms-excel'
1890
1891     def filename(self, base):
1892         return base + '.xls'
1893
1894     def from_data(self, fields, rows):
1895         workbook = xlwt.Workbook()
1896         worksheet = workbook.add_sheet('Sheet 1')
1897
1898         for i, fieldname in enumerate(fields):
1899             worksheet.write(0, i, fieldname)
1900             worksheet.col(i).width = 8000 # around 220 pixels
1901
1902         style = xlwt.easyxf('align: wrap yes')
1903
1904         for row_index, row in enumerate(rows):
1905             for cell_index, cell_value in enumerate(row):
1906                 if isinstance(cell_value, basestring):
1907                     cell_value = re.sub("\r", " ", cell_value)
1908                 if cell_value is False: cell_value = None
1909                 worksheet.write(row_index + 1, cell_index, cell_value, style)
1910
1911         fp = StringIO()
1912         workbook.save(fp)
1913         fp.seek(0)
1914         data = fp.read()
1915         fp.close()
1916         return data
1917
1918 class Reports(View):
1919     _cp_path = "/web/report"
1920     POLLING_DELAY = 0.25
1921     TYPES_MAPPING = {
1922         'doc': 'application/vnd.ms-word',
1923         'html': 'text/html',
1924         'odt': 'application/vnd.oasis.opendocument.text',
1925         'pdf': 'application/pdf',
1926         'sxw': 'application/vnd.sun.xml.writer',
1927         'xls': 'application/vnd.ms-excel',
1928     }
1929
1930     @openerpweb.httprequest
1931     def index(self, req, action, token):
1932         action = simplejson.loads(action)
1933
1934         report_srv = req.session.proxy("report")
1935         context = req.session.eval_context(
1936             nonliterals.CompoundContext(
1937                 req.context or {}, action[ "context"]))
1938
1939         report_data = {}
1940         report_ids = context["active_ids"]
1941         if 'report_type' in action:
1942             report_data['report_type'] = action['report_type']
1943         if 'datas' in action:
1944             if 'ids' in action['datas']:
1945                 report_ids = action['datas'].pop('ids')
1946             report_data.update(action['datas'])
1947
1948         report_id = report_srv.report(
1949             req.session._db, req.session._uid, req.session._password,
1950             action["report_name"], report_ids,
1951             report_data, context)
1952
1953         report_struct = None
1954         while True:
1955             report_struct = report_srv.report_get(
1956                 req.session._db, req.session._uid, req.session._password, report_id)
1957             if report_struct["state"]:
1958                 break
1959
1960             time.sleep(self.POLLING_DELAY)
1961
1962         report = base64.b64decode(report_struct['result'])
1963         if report_struct.get('code') == 'zlib':
1964             report = zlib.decompress(report)
1965         report_mimetype = self.TYPES_MAPPING.get(
1966             report_struct['format'], 'octet-stream')
1967         file_name = action.get('name', 'report')
1968         if 'name' not in action:
1969             reports = req.session.model('ir.actions.report.xml')
1970             res_id = reports.search([('report_name', '=', action['report_name']),],
1971                                     0, False, False, context)
1972             if len(res_id) > 0:
1973                 file_name = reports.read(res_id[0], ['name'], context)['name']
1974             else:
1975                 file_name = action['report_name']
1976         file_name = '%s.%s' % (file_name, report_struct['format'])
1977
1978         return req.make_response(report,
1979              headers=[
1980                  ('Content-Disposition', content_disposition(file_name, req)),
1981                  ('Content-Type', report_mimetype),
1982                  ('Content-Length', len(report))],
1983              cookies={'fileToken': int(token)})
1984
1985 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: