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