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