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