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