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