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