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