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     wsgienv = req.httprequest.environ
349     env = dict(
350         base_location=req.httprequest.url_root.rstrip('/'),
351         HTTP_HOST=wsgienv['HTTP_HOST'],
352         REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
353     )
354     req.session.authenticate(db, login, key, env)
355     return set_cookie_and_redirect(req, redirect_url)
356
357 def set_cookie_and_redirect(req, redirect_url):
358     redirect = werkzeug.utils.redirect(redirect_url, 303)
359     redirect.autocorrect_location_header = False
360     cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
361     redirect.set_cookie('instance0|session_id', cookie_val)
362     return redirect
363
364 def eval_context_and_domain(session, context, domain=None):
365     e_context = session.eval_context(context)
366     # should we give the evaluated context as an evaluation context to the domain?
367     e_domain = session.eval_domain(domain or [])
368
369     return e_context, e_domain
370
371 def load_actions_from_ir_values(req, key, key2, models, meta):
372     context = req.session.eval_context(req.context)
373     Values = req.session.model('ir.values')
374     actions = Values.get(key, key2, models, meta, context)
375
376     return [(id, name, clean_action(req, action, context))
377             for id, name, action in actions]
378
379 def clean_action(req, action, context, do_not_eval=False):
380     action.setdefault('flags', {})
381
382     context = context or {}
383     eval_ctx = req.session.evaluation_context(context)
384
385     if not do_not_eval:
386         # values come from the server, we can just eval them
387         if action.get('context') and isinstance(action.get('context'), basestring):
388             action['context'] = eval( action['context'], eval_ctx ) or {}
389
390         if action.get('domain') and isinstance(action.get('domain'), basestring):
391             action['domain'] = eval( action['domain'], eval_ctx ) or []
392     else:
393         if 'context' in action:
394             action['context'] = parse_context(action['context'], req.session)
395         if 'domain' in action:
396             action['domain'] = parse_domain(action['domain'], req.session)
397
398     action_type = action.setdefault('type', 'ir.actions.act_window_close')
399     if action_type == 'ir.actions.act_window':
400         return fix_view_modes(action)
401     return action
402
403 # I think generate_views,fix_view_modes should go into js ActionManager
404 def generate_views(action):
405     """
406     While the server generates a sequence called "views" computing dependencies
407     between a bunch of stuff for views coming directly from the database
408     (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
409     to return custom view dictionaries generated on the fly.
410
411     In that case, there is no ``views`` key available on the action.
412
413     Since the web client relies on ``action['views']``, generate it here from
414     ``view_mode`` and ``view_id``.
415
416     Currently handles two different cases:
417
418     * no view_id, multiple view_mode
419     * single view_id, single view_mode
420
421     :param dict action: action descriptor dictionary to generate a views key for
422     """
423     view_id = action.get('view_id') or False
424     if isinstance(view_id, (list, tuple)):
425         view_id = view_id[0]
426
427     # providing at least one view mode is a requirement, not an option
428     view_modes = action['view_mode'].split(',')
429
430     if len(view_modes) > 1:
431         if view_id:
432             raise ValueError('Non-db action dictionaries should provide '
433                              'either multiple view modes or a single view '
434                              'mode and an optional view id.\n\n Got view '
435                              'modes %r and view id %r for action %r' % (
436                 view_modes, view_id, action))
437         action['views'] = [(False, mode) for mode in view_modes]
438         return
439     action['views'] = [(view_id, view_modes[0])]
440
441 def fix_view_modes(action):
442     """ For historical reasons, OpenERP has weird dealings in relation to
443     view_mode and the view_type attribute (on window actions):
444
445     * one of the view modes is ``tree``, which stands for both list views
446       and tree views
447     * the choice is made by checking ``view_type``, which is either
448       ``form`` for a list view or ``tree`` for an actual tree view
449
450     This methods simply folds the view_type into view_mode by adding a
451     new view mode ``list`` which is the result of the ``tree`` view_mode
452     in conjunction with the ``form`` view_type.
453
454     TODO: this should go into the doc, some kind of "peculiarities" section
455
456     :param dict action: an action descriptor
457     :returns: nothing, the action is modified in place
458     """
459     if not action.get('views'):
460         generate_views(action)
461
462     if action.pop('view_type', 'form') != 'form':
463         return action
464
465     if 'view_mode' in action:
466         action['view_mode'] = ','.join(
467             mode if mode != 'tree' else 'list'
468             for mode in action['view_mode'].split(','))
469     action['views'] = [
470         [id, mode if mode != 'tree' else 'list']
471         for id, mode in action['views']
472     ]
473
474     return action
475
476 def parse_domain(domain, session):
477     """ Parses an arbitrary string containing a domain, transforms it
478     to either a literal domain or a :class:`nonliterals.Domain`
479
480     :param domain: the domain to parse, if the domain is not a string it
481                    is assumed to be a literal domain and is returned as-is
482     :param session: Current OpenERP session
483     :type session: openerpweb.OpenERPSession
484     """
485     if not isinstance(domain, basestring):
486         return domain
487     try:
488         return ast.literal_eval(domain)
489     except ValueError:
490         # not a literal
491         return nonliterals.Domain(session, domain)
492
493 def parse_context(context, session):
494     """ Parses an arbitrary string containing a context, transforms it
495     to either a literal context or a :class:`nonliterals.Context`
496
497     :param context: the context to parse, if the context is not a string it
498            is assumed to be a literal domain and is returned as-is
499     :param session: Current OpenERP session
500     :type session: openerpweb.OpenERPSession
501     """
502     if not isinstance(context, basestring):
503         return context
504     try:
505         return ast.literal_eval(context)
506     except ValueError:
507         return nonliterals.Context(session, context)
508
509 def _local_web_translations(trans_file):
510     messages = []
511     try:
512         with open(trans_file) as t_file:
513             po = babel.messages.pofile.read_po(t_file)
514     except Exception:
515         return
516     for x in po:
517         if x.id and x.string and "openerp-web" in x.auto_comments:
518             messages.append({'id': x.id, 'string': x.string})
519     return messages
520
521 def xml2json_from_elementtree(el, preserve_whitespaces=False):
522     """ xml2json-direct
523     Simple and straightforward XML-to-JSON converter in Python
524     New BSD Licensed
525     http://code.google.com/p/xml2json-direct/
526     """
527     res = {}
528     if el.tag[0] == "{":
529         ns, name = el.tag.rsplit("}", 1)
530         res["tag"] = name
531         res["namespace"] = ns[1:]
532     else:
533         res["tag"] = el.tag
534     res["attrs"] = {}
535     for k, v in el.items():
536         res["attrs"][k] = v
537     kids = []
538     if el.text and (preserve_whitespaces or el.text.strip() != ''):
539         kids.append(el.text)
540     for kid in el:
541         kids.append(xml2json_from_elementtree(kid, preserve_whitespaces))
542         if kid.tail and (preserve_whitespaces or kid.tail.strip() != ''):
543             kids.append(kid.tail)
544     res["children"] = kids
545     return res
546
547 def content_disposition(filename, req):
548     filename = filename.encode('utf8')
549     escaped = urllib2.quote(filename)
550     browser = req.httprequest.user_agent.browser
551     version = int((req.httprequest.user_agent.version or '0').split('.')[0])
552     if browser == 'msie' and version < 9:
553         return "attachment; filename=%s" % escaped
554     elif browser == 'safari':
555         return "attachment; filename=%s" % filename
556     else:
557         return "attachment; filename*=UTF-8''%s" % escaped
558
559
560 #----------------------------------------------------------
561 # OpenERP Web web Controllers
562 #----------------------------------------------------------
563
564 html_template = """<!DOCTYPE html>
565 <html style="height: 100%%">
566     <head>
567         <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
568         <meta http-equiv="content-type" content="text/html; charset=utf-8" />
569         <title>OpenERP</title>
570         <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
571         <link rel="stylesheet" href="/web/static/src/css/full.css" />
572         %(css)s
573         %(js)s
574         <script type="text/javascript">
575             $(function() {
576                 var s = new openerp.init(%(modules)s);
577                 %(init)s
578             });
579         </script>
580     </head>
581     <body>
582         <!--[if lte IE 8]>
583         <script src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
584         <script>
585             var test = function() {
586                 CFInstall.check({
587                     mode: "overlay"
588                 });
589             };
590             if (window.localStorage && false) {
591                 if (! localStorage.getItem("hasShownGFramePopup")) {
592                     test();
593                     localStorage.setItem("hasShownGFramePopup", true);
594                 }
595             } else {
596                 test();
597             }
598         </script>
599         <![endif]-->
600     </body>
601 </html>
602 """
603
604 class Home(openerpweb.Controller):
605     _cp_path = '/'
606
607     @openerpweb.httprequest
608     def index(self, req, s_action=None, db=None, **kw):
609         js = "\n        ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, 'js', db=db))
610         css = "\n        ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, 'css', db=db))
611
612         r = html_template % {
613             'js': js,
614             'css': css,
615             'modules': simplejson.dumps(module_boot(req, db=db)),
616             'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
617         }
618         return r
619
620     @openerpweb.httprequest
621     def login(self, req, db, login, key):
622         return login_and_redirect(req, db, login, key)
623
624 class WebClient(openerpweb.Controller):
625     _cp_path = "/web/webclient"
626
627     @openerpweb.jsonrequest
628     def csslist(self, req, mods=None):
629         return manifest_list(req, 'css', mods=mods)
630
631     @openerpweb.jsonrequest
632     def jslist(self, req, mods=None):
633         return manifest_list(req, 'js', mods=mods)
634
635     @openerpweb.jsonrequest
636     def qweblist(self, req, mods=None):
637         return manifest_list(req, 'qweb', mods=mods)
638
639     @openerpweb.httprequest
640     def css(self, req, mods=None, db=None):
641         files = list(manifest_glob(req, 'css', addons=mods, db=db))
642         last_modified = get_last_modified(f[0] for f in files)
643         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
644             return werkzeug.wrappers.Response(status=304)
645
646         file_map = dict(files)
647
648         rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
649         rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
650
651         def reader(f):
652             """read the a css file and absolutify all relative uris"""
653             with open(f, 'rb') as fp:
654                 data = fp.read().decode('utf-8')
655
656             path = file_map[f]
657             web_dir = os.path.dirname(path)
658
659             data = re.sub(
660                 rx_import,
661                 r"""@import \1%s/""" % (web_dir,),
662                 data,
663             )
664
665             data = re.sub(
666                 rx_url,
667                 r"""url(\1%s/""" % (web_dir,),
668                 data,
669             )
670             return data.encode('utf-8')
671
672         content, checksum = concat_files((f[0] for f in files), reader)
673
674         return make_conditional(
675             req, req.make_response(content, [('Content-Type', 'text/css')]),
676             last_modified, checksum)
677
678     @openerpweb.httprequest
679     def js(self, req, mods=None, db=None):
680         files = [f[0] for f in manifest_glob(req, 'js', addons=mods, db=db)]
681         last_modified = get_last_modified(files)
682         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
683             return werkzeug.wrappers.Response(status=304)
684
685         content, checksum = concat_js(files)
686
687         return make_conditional(
688             req, req.make_response(content, [('Content-Type', 'application/javascript')]),
689             last_modified, checksum)
690
691     @openerpweb.httprequest
692     def qweb(self, req, mods=None, db=None):
693         files = [f[0] for f in manifest_glob(req, 'qweb', addons=mods, db=db)]
694         last_modified = get_last_modified(files)
695         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
696             return werkzeug.wrappers.Response(status=304)
697
698         content, checksum = concat_xml(files)
699
700         return make_conditional(
701             req, req.make_response(content, [('Content-Type', 'text/xml')]),
702             last_modified, checksum)
703
704     @openerpweb.jsonrequest
705     def bootstrap_translations(self, req, mods):
706         """ Load local translations from *.po files, as a temporary solution
707             until we have established a valid session. This is meant only
708             for translating the login page and db management chrome, using
709             the browser's language. """
710         # For performance reasons we only load a single translation, so for
711         # sub-languages (that should only be partially translated) we load the
712         # main language PO instead - that should be enough for the login screen.
713         lang = req.lang.split('_')[0]
714
715         translations_per_module = {}
716         for addon_name in mods:
717             if openerpweb.addons_manifest[addon_name].get('bootstrap'):
718                 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
719                 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
720                 if not os.path.exists(f_name):
721                     continue
722                 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
723
724         return {"modules": translations_per_module,
725                 "lang_parameters": None}
726
727     @openerpweb.jsonrequest
728     def translations(self, req, mods, lang):
729         res_lang = req.session.model('res.lang')
730         ids = res_lang.search([("code", "=", lang)])
731         lang_params = None
732         if ids:
733             lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
734                                                 "grouping", "decimal_point", "thousands_sep"])
735
736         # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
737         # done server-side when the language is loaded, so we only need to load the user's lang.
738         ir_translation = req.session.model('ir.translation')
739         translations_per_module = {}
740         messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
741                                                ('comments','like','openerp-web'),('value','!=',False),
742                                                ('value','!=','')],
743                                               ['module','src','value','lang'], order='module') 
744         for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
745             translations_per_module.setdefault(mod,{'messages':[]})
746             translations_per_module[mod]['messages'].extend({'id': m['src'],
747                                                              'string': m['value']} \
748                                                                 for m in msg_group)
749         return {"modules": translations_per_module,
750                 "lang_parameters": lang_params}
751
752     @openerpweb.jsonrequest
753     def version_info(self, req):
754         return req.session.proxy('common').version()['openerp']
755
756 class Proxy(openerpweb.Controller):
757     _cp_path = '/web/proxy'
758
759     @openerpweb.jsonrequest
760     def load(self, req, path):
761         """ Proxies an HTTP request through a JSON request.
762
763         It is strongly recommended to not request binary files through this,
764         as the result will be a binary data blob as well.
765
766         :param req: OpenERP request
767         :param path: actual request path
768         :return: file content
769         """
770         from werkzeug.test import Client
771         from werkzeug.wrappers import BaseResponse
772
773         return Client(req.httprequest.app, BaseResponse).get(path).data
774
775 class Database(openerpweb.Controller):
776     _cp_path = "/web/database"
777
778     @openerpweb.jsonrequest
779     def get_list(self, req):
780         return db_list(req)
781
782     @openerpweb.jsonrequest
783     def create(self, req, fields):
784         params = dict(map(operator.itemgetter('name', 'value'), fields))
785         return req.session.proxy("db").create_database(
786             params['super_admin_pwd'],
787             params['db_name'],
788             bool(params.get('demo_data')),
789             params['db_lang'],
790             params['create_admin_pwd'])
791
792     @openerpweb.jsonrequest
793     def duplicate(self, req, fields):
794         params = dict(map(operator.itemgetter('name', 'value'), fields))
795         return req.session.proxy("db").duplicate_database(
796             params['super_admin_pwd'],
797             params['db_original_name'],
798             params['db_name'])
799
800     @openerpweb.jsonrequest
801     def duplicate(self, req, fields):
802         params = dict(map(operator.itemgetter('name', 'value'), fields))
803         duplicate_attrs = (
804             params['super_admin_pwd'],
805             params['db_original_name'],
806             params['db_name'],
807         )
808
809         return req.session.proxy("db").duplicate_database(*duplicate_attrs)
810
811     @openerpweb.jsonrequest
812     def drop(self, req, fields):
813         password, db = operator.itemgetter(
814             'drop_pwd', 'drop_db')(
815                 dict(map(operator.itemgetter('name', 'value'), fields)))
816
817         try:
818             return req.session.proxy("db").drop(password, db)
819         except xmlrpclib.Fault, e:
820             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
821                 return {'error': e.faultCode, 'title': 'Drop Database'}
822         return {'error': 'Could not drop database !', 'title': 'Drop Database'}
823
824     @openerpweb.httprequest
825     def backup(self, req, backup_db, backup_pwd, token):
826         try:
827             db_dump = base64.b64decode(
828                 req.session.proxy("db").dump(backup_pwd, backup_db))
829             filename = "%(db)s_%(timestamp)s.dump" % {
830                 'db': backup_db,
831                 'timestamp': datetime.datetime.utcnow().strftime(
832                     "%Y-%m-%d_%H-%M-%SZ")
833             }
834             return req.make_response(db_dump,
835                [('Content-Type', 'application/octet-stream; charset=binary'),
836                ('Content-Disposition', content_disposition(filename, req))],
837                {'fileToken': int(token)}
838             )
839         except xmlrpclib.Fault, e:
840             return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
841
842     @openerpweb.httprequest
843     def restore(self, req, db_file, restore_pwd, new_db):
844         try:
845             data = base64.b64encode(db_file.read())
846             req.session.proxy("db").restore(restore_pwd, new_db, data)
847             return ''
848         except xmlrpclib.Fault, e:
849             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
850                 raise Exception("AccessDenied")
851
852     @openerpweb.jsonrequest
853     def change_password(self, req, fields):
854         old_password, new_password = operator.itemgetter(
855             'old_pwd', 'new_pwd')(
856                 dict(map(operator.itemgetter('name', 'value'), fields)))
857         try:
858             return req.session.proxy("db").change_admin_password(old_password, new_password)
859         except xmlrpclib.Fault, e:
860             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
861                 return {'error': e.faultCode, 'title': 'Change Password'}
862         return {'error': 'Error, password not changed !', 'title': 'Change Password'}
863
864 class Session(openerpweb.Controller):
865     _cp_path = "/web/session"
866
867     def session_info(self, req):
868         req.session.ensure_valid()
869         return {
870             "session_id": req.session_id,
871             "uid": req.session._uid,
872             "context": req.session.get_context() if req.session._uid else {},
873             "db": req.session._db,
874             "login": req.session._login,
875         }
876
877     @openerpweb.jsonrequest
878     def get_session_info(self, req):
879         return self.session_info(req)
880
881     @openerpweb.jsonrequest
882     def authenticate(self, req, db, login, password, base_location=None):
883         wsgienv = req.httprequest.environ
884         env = dict(
885             base_location=base_location,
886             HTTP_HOST=wsgienv['HTTP_HOST'],
887             REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
888         )
889         req.session.authenticate(db, login, password, env)
890
891         return self.session_info(req)
892
893     @openerpweb.jsonrequest
894     def change_password (self,req,fields):
895         old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
896                 dict(map(operator.itemgetter('name', 'value'), fields)))
897         if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
898             return {'error':'You cannot leave any password empty.','title': 'Change Password'}
899         if new_password != confirm_password:
900             return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
901         try:
902             if req.session.model('res.users').change_password(
903                 old_password, new_password):
904                 return {'new_password':new_password}
905         except Exception:
906             return {'error': 'The old password you provided is incorrect, your password was not changed.', 'title': 'Change Password'}
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 = {'erorr':e.faultCode.split('--')[1],'title':e.faultCode.split('--')[0]}
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         if ".id" in export_fields:
1754             fields['.id'] = fields.pop('id', {'string': 'ID'})
1755             
1756         # To make fields retrieval more efficient, fetch all sub-fields of a
1757         # given field at the same time. Because the order in the export list is
1758         # arbitrary, this requires ordering all sub-fields of a given field
1759         # together so they can be fetched at the same time
1760         #
1761         # Works the following way:
1762         # * sort the list of fields to export, the default sorting order will
1763         #   put the field itself (if present, for xmlid) and all of its
1764         #   sub-fields right after it
1765         # * then, group on: the first field of the path (which is the same for
1766         #   a field and for its subfields and the length of splitting on the
1767         #   first '/', which basically means grouping the field on one side and
1768         #   all of the subfields on the other. This way, we have the field (for
1769         #   the xmlid) with length 1, and all of the subfields with the same
1770         #   base but a length "flag" of 2
1771         # * if we have a normal field (length 1), just add it to the info
1772         #   mapping (with its string) as-is
1773         # * otherwise, recursively call fields_info via graft_subfields.
1774         #   all graft_subfields does is take the result of fields_info (on the
1775         #   field's model) and prepend the current base (current field), which
1776         #   rebuilds the whole sub-tree for the field
1777         #
1778         # result: because we're not fetching the fields_get for half the
1779         # database models, fetching a namelist with a dozen fields (including
1780         # relational data) falls from ~6s to ~300ms (on the leads model).
1781         # export lists with no sub-fields (e.g. import_compatible lists with
1782         # no o2m) are even more efficient (from the same 6s to ~170ms, as
1783         # there's a single fields_get to execute)
1784         for (base, length), subfields in itertools.groupby(
1785                 sorted(export_fields),
1786                 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1787             subfields = list(subfields)
1788             if length == 2:
1789                 # subfields is a seq of $base/*rest, and not loaded yet
1790                 info.update(self.graft_subfields(
1791                     req, fields[base]['relation'], base, fields[base]['string'],
1792                     subfields
1793                 ))
1794             else:
1795                 info[base] = fields[base]['string']
1796
1797         return info
1798
1799     def graft_subfields(self, req, model, prefix, prefix_string, fields):
1800         export_fields = [field.split('/', 1)[1] for field in fields]
1801         return (
1802             (prefix + '/' + k, prefix_string + '/' + v)
1803             for k, v in self.fields_info(req, model, export_fields).iteritems())
1804
1805     #noinspection PyPropertyDefinition
1806     @property
1807     def content_type(self):
1808         """ Provides the format's content type """
1809         raise NotImplementedError()
1810
1811     def filename(self, base):
1812         """ Creates a valid filename for the format (with extension) from the
1813          provided base name (exension-less)
1814         """
1815         raise NotImplementedError()
1816
1817     def from_data(self, fields, rows):
1818         """ Conversion method from OpenERP's export data to whatever the
1819         current export class outputs
1820
1821         :params list fields: a list of fields to export
1822         :params list rows: a list of records to export
1823         :returns:
1824         :rtype: bytes
1825         """
1826         raise NotImplementedError()
1827
1828     @openerpweb.httprequest
1829     def index(self, req, data, token):
1830         model, fields, ids, domain, import_compat = \
1831             operator.itemgetter('model', 'fields', 'ids', 'domain',
1832                                 'import_compat')(
1833                 simplejson.loads(data))
1834
1835         context = req.session.eval_context(req.context)
1836         Model = req.session.model(model)
1837         ids = ids or Model.search(domain, 0, False, False, context)
1838
1839         field_names = map(operator.itemgetter('name'), fields)
1840         import_data = Model.export_data(ids, field_names, context).get('datas',[])
1841
1842         if import_compat:
1843             columns_headers = field_names
1844         else:
1845             columns_headers = [val['label'].strip() for val in fields]
1846
1847
1848         return req.make_response(self.from_data(columns_headers, import_data),
1849             headers=[('Content-Disposition',
1850                             content_disposition(self.filename(model), req)),
1851                      ('Content-Type', self.content_type)],
1852             cookies={'fileToken': int(token)})
1853
1854 class CSVExport(Export):
1855     _cp_path = '/web/export/csv'
1856     fmt = {'tag': 'csv', 'label': 'CSV'}
1857
1858     @property
1859     def content_type(self):
1860         return 'text/csv;charset=utf8'
1861
1862     def filename(self, base):
1863         return base + '.csv'
1864
1865     def from_data(self, fields, rows):
1866         fp = StringIO()
1867         writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1868
1869         writer.writerow([name.encode('utf-8') for name in fields])
1870
1871         for data in rows:
1872             row = []
1873             for d in data:
1874                 if isinstance(d, basestring):
1875                     d = d.replace('\n',' ').replace('\t',' ')
1876                     try:
1877                         d = d.encode('utf-8')
1878                     except UnicodeError:
1879                         pass
1880                 if d is False: d = None
1881                 row.append(d)
1882             writer.writerow(row)
1883
1884         fp.seek(0)
1885         data = fp.read()
1886         fp.close()
1887         return data
1888
1889 class ExcelExport(Export):
1890     _cp_path = '/web/export/xls'
1891     fmt = {
1892         'tag': 'xls',
1893         'label': 'Excel',
1894         'error': None if xlwt else "XLWT required"
1895     }
1896
1897     @property
1898     def content_type(self):
1899         return 'application/vnd.ms-excel'
1900
1901     def filename(self, base):
1902         return base + '.xls'
1903
1904     def from_data(self, fields, rows):
1905         workbook = xlwt.Workbook()
1906         worksheet = workbook.add_sheet('Sheet 1')
1907
1908         for i, fieldname in enumerate(fields):
1909             worksheet.write(0, i, fieldname)
1910             worksheet.col(i).width = 8000 # around 220 pixels
1911
1912         style = xlwt.easyxf('align: wrap yes')
1913
1914         for row_index, row in enumerate(rows):
1915             for cell_index, cell_value in enumerate(row):
1916                 if isinstance(cell_value, basestring):
1917                     cell_value = re.sub("\r", " ", cell_value)
1918                 if cell_value is False: cell_value = None
1919                 worksheet.write(row_index + 1, cell_index, cell_value, style)
1920
1921         fp = StringIO()
1922         workbook.save(fp)
1923         fp.seek(0)
1924         data = fp.read()
1925         fp.close()
1926         return data
1927
1928 class Reports(View):
1929     _cp_path = "/web/report"
1930     POLLING_DELAY = 0.25
1931     TYPES_MAPPING = {
1932         'doc': 'application/vnd.ms-word',
1933         'html': 'text/html',
1934         'odt': 'application/vnd.oasis.opendocument.text',
1935         'pdf': 'application/pdf',
1936         'sxw': 'application/vnd.sun.xml.writer',
1937         'xls': 'application/vnd.ms-excel',
1938     }
1939
1940     @openerpweb.httprequest
1941     def index(self, req, action, token):
1942         action = simplejson.loads(action)
1943
1944         report_srv = req.session.proxy("report")
1945         context = req.session.eval_context(
1946             nonliterals.CompoundContext(
1947                 req.context or {}, action[ "context"]))
1948
1949         report_data = {}
1950         report_ids = context["active_ids"]
1951         if 'report_type' in action:
1952             report_data['report_type'] = action['report_type']
1953         if 'datas' in action:
1954             if 'ids' in action['datas']:
1955                 report_ids = action['datas'].pop('ids')
1956             report_data.update(action['datas'])
1957
1958         report_id = report_srv.report(
1959             req.session._db, req.session._uid, req.session._password,
1960             action["report_name"], report_ids,
1961             report_data, context)
1962
1963         report_struct = None
1964         while True:
1965             report_struct = report_srv.report_get(
1966                 req.session._db, req.session._uid, req.session._password, report_id)
1967             if report_struct["state"]:
1968                 break
1969
1970             time.sleep(self.POLLING_DELAY)
1971
1972         report = base64.b64decode(report_struct['result'])
1973         if report_struct.get('code') == 'zlib':
1974             report = zlib.decompress(report)
1975         report_mimetype = self.TYPES_MAPPING.get(
1976             report_struct['format'], 'octet-stream')
1977         file_name = action.get('name', 'report')
1978         if 'name' not in action:
1979             reports = req.session.model('ir.actions.report.xml')
1980             res_id = reports.search([('report_name', '=', action['report_name']),],
1981                                     0, False, False, context)
1982             if len(res_id) > 0:
1983                 file_name = reports.read(res_id[0], ['name'], context)['name']
1984             else:
1985                 file_name = action['report_name']
1986         file_name = '%s.%s' % (file_name, report_struct['format'])
1987
1988         return req.make_response(report,
1989              headers=[
1990                  ('Content-Disposition', content_disposition(file_name, req)),
1991                  ('Content-Type', report_mimetype),
1992                  ('Content-Length', len(report))],
1993              cookies={'fileToken': int(token)})
1994
1995 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: