[imp] made ids in report overridable by report action
[odoo/odoo.git] / addons / web / controllers / main.py
1 # -*- coding: utf-8 -*-
2
3 import base64
4 import csv
5 import glob
6 import itertools
7 import operator
8 import os
9 import re
10 import simplejson
11 import textwrap
12 import xmlrpclib
13 import time
14 import zlib
15 import webrelease
16 from xml.etree import ElementTree
17 from cStringIO import StringIO
18
19 import web.common.dispatch as openerpweb
20 import web.common.ast
21 import web.common.nonliterals
22 openerpweb.ast = web.common.ast
23 openerpweb.nonliterals = web.common.nonliterals
24
25 from babel.messages.pofile import read_po
26
27 # Should move to openerpweb.Xml2Json
28 class Xml2Json:
29     # xml2json-direct
30     # Simple and straightforward XML-to-JSON converter in Python
31     # New BSD Licensed
32     #
33     # URL: http://code.google.com/p/xml2json-direct/
34     @staticmethod
35     def convert_to_json(s):
36         return simplejson.dumps(
37             Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
38
39     @staticmethod
40     def convert_to_structure(s):
41         root = ElementTree.fromstring(s)
42         return Xml2Json.convert_element(root)
43
44     @staticmethod
45     def convert_element(el, skip_whitespaces=True):
46         res = {}
47         if el.tag[0] == "{":
48             ns, name = el.tag.rsplit("}", 1)
49             res["tag"] = name
50             res["namespace"] = ns[1:]
51         else:
52             res["tag"] = el.tag
53         res["attrs"] = {}
54         for k, v in el.items():
55             res["attrs"][k] = v
56         kids = []
57         if el.text and (not skip_whitespaces or el.text.strip() != ''):
58             kids.append(el.text)
59         for kid in el:
60             kids.append(Xml2Json.convert_element(kid))
61             if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
62                 kids.append(kid.tail)
63         res["children"] = kids
64         return res
65
66 #----------------------------------------------------------
67 # OpenERP Web web Controllers
68 #----------------------------------------------------------
69
70 def manifest_glob(addons_path, addons, key):
71     files = []
72     for addon in addons:
73         globlist = openerpweb.addons_manifest.get(addon, {}).get(key, [])
74         for pattern in globlist:
75             for path in glob.glob(os.path.join(addons_path, addon, pattern)):
76                 files.append(path[len(addons_path):])
77     return files
78
79 def concat_files(addons_path, file_list):
80     """ Concatenate file content
81     return (concat,timestamp)
82     concat: concatenation of file content
83     timestamp: max(os.path.getmtime of file_list)
84     """
85     files_content = []
86     files_timestamp = 0
87     for i in file_list:
88         fname = os.path.join(addons_path, i[1:])
89         ftime = os.path.getmtime(fname)
90         if ftime > files_timestamp:
91             files_timestamp = ftime
92         files_content.append(open(fname).read())
93     files_concat = "".join(files_content)
94     return files_concat,files_timestamp
95
96 home_template = textwrap.dedent("""<!DOCTYPE html>
97 <html style="height: 100%%">
98     <head>
99         <meta http-equiv="content-type" content="text/html; charset=utf-8" />
100         <title>OpenERP</title>
101         <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
102         %(css)s
103         %(javascript)s
104         <script type="text/javascript">
105             $(function() {
106                 var c = new openerp.init();
107                 var wc = new c.web.WebClient("oe");
108                 wc.start();
109             });
110         </script>
111     </head>
112     <body id="oe" class="openerp"></body>
113 </html>
114 """)
115 class WebClient(openerpweb.Controller):
116     _cp_path = "/web/webclient"
117
118     @openerpweb.jsonrequest
119     def csslist(self, req, mods='web'):
120         return manifest_glob(req.config.addons_path, mods.split(','), 'css')
121
122     @openerpweb.jsonrequest
123     def jslist(self, req, mods='web'):
124         return manifest_glob(req.config.addons_path, mods.split(','), 'js')
125
126     @openerpweb.httprequest
127     def css(self, req, mods='web'):
128         files = manifest_glob(req.config.addons_path, mods.split(','), 'css')
129         content,timestamp = concat_files(req.config.addons_path, files)
130         # TODO request set the Date of last modif and Etag
131         return req.make_response(content, [('Content-Type', 'text/css')])
132
133     @openerpweb.httprequest
134     def js(self, req, mods='web'):
135         files = manifest_glob(req.config.addons_path, mods.split(','), 'js')
136         content,timestamp = concat_files(req.config.addons_path, files)
137         # TODO request set the Date of last modif and Etag
138         return req.make_response(content, [('Content-Type', 'application/javascript')])
139
140     @openerpweb.httprequest
141     def home(self, req, s_action=None, **kw):
142         # script tags
143         jslist = ['/web/webclient/js']
144         if req.debug:
145             jslist = [i + '?debug=' + str(time.time()) for i in manifest_glob(req.config.addons_path, ['web'], 'js')]
146         js = "\n        ".join(['<script type="text/javascript" src="%s"></script>'%i for i in jslist])
147
148         # css tags
149         csslist = ['/web/webclient/css']
150         if req.debug:
151             csslist = [i + '?debug=' + str(time.time()) for i in manifest_glob(req.config.addons_path, ['web'], 'css')]
152         css = "\n        ".join(['<link rel="stylesheet" href="%s">'%i for i in csslist])
153         r = home_template % {
154             'javascript': js,
155             'css': css
156         }
157         return r
158
159     @openerpweb.jsonrequest
160     def translations(self, req, mods, lang):
161         lang_model = req.session.model('res.lang')
162         ids = lang_model.search([("code", "=", lang)])
163         if ids:
164             lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
165                                                 "grouping", "decimal_point", "thousands_sep"])
166         else:
167             lang_obj = None
168             
169         if lang.count("_") > 0:
170             separator = "_"
171         else:
172             separator = "@"
173         langs = lang.split(separator)
174         langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
175         
176         transs = {}
177         for addon_name in mods:
178             transl = {"messages":[]}
179             transs[addon_name] = transl
180             for l in langs:
181                 f_name = os.path.join(req.config.addons_path, addon_name, "po", l + ".po")
182                 if not os.path.exists(f_name):
183                     continue
184                 try:
185                     with open(f_name) as t_file:
186                         po = read_po(t_file)
187                 except:
188                     continue
189                 for x in po:
190                     if x.id and x.string:
191                         transl["messages"].append({'id': x.id, 'string': x.string})
192         return {"modules": transs,
193                 "lang_parameters": lang_obj}
194
195     @openerpweb.jsonrequest
196     def version_info(self, req):
197         return {
198             "version": webrelease.version
199         }
200
201 class Database(openerpweb.Controller):
202     _cp_path = "/web/database"
203
204     @openerpweb.jsonrequest
205     def get_list(self, req):
206         proxy = req.session.proxy("db")
207         dbs = proxy.list()
208         h = req.httprequest.headers['Host'].split(':')[0]
209         d = h.split('.')[0]
210         r = req.config.dbfilter.replace('%h', h).replace('%d', d)
211         dbs = [i for i in dbs if re.match(r, i)]
212         return {"db_list": dbs}
213
214     @openerpweb.jsonrequest
215     def progress(self, req, password, id):
216         return req.session.proxy('db').get_progress(password, id)
217
218     @openerpweb.jsonrequest
219     def create(self, req, fields):
220
221         params = dict(map(operator.itemgetter('name', 'value'), fields))
222         create_attrs = (
223             params['super_admin_pwd'],
224             params['db_name'],
225             bool(params.get('demo_data')),
226             params['db_lang'],
227             params['create_admin_pwd']
228         )
229
230         try:
231             return req.session.proxy("db").create(*create_attrs)
232         except xmlrpclib.Fault, e:
233             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
234                 return {'error': e.faultCode, 'title': 'Create Database'}
235         return {'error': 'Could not create database !', 'title': 'Create Database'}
236
237     @openerpweb.jsonrequest
238     def drop(self, req, fields):
239         password, db = operator.itemgetter(
240             'drop_pwd', 'drop_db')(
241                 dict(map(operator.itemgetter('name', 'value'), fields)))
242         
243         try:
244             return req.session.proxy("db").drop(password, db)
245         except xmlrpclib.Fault, e:
246             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
247                 return {'error': e.faultCode, 'title': 'Drop Database'}
248         return {'error': 'Could not drop database !', 'title': 'Drop Database'}
249
250     @openerpweb.httprequest
251     def backup(self, req, backup_db, backup_pwd, token):
252         try:
253             db_dump = base64.b64decode(
254                 req.session.proxy("db").dump(backup_pwd, backup_db))
255             return req.make_response(db_dump,
256                 [('Content-Type', 'application/octet-stream; charset=binary'),
257                  ('Content-Disposition', 'attachment; filename="' + backup_db + '.dump"')],
258                 {'fileToken': int(token)}
259             )
260         except xmlrpclib.Fault, e:
261             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
262                 return 'Backup Database|' + e.faultCode
263         return 'Backup Database|Could not generate database backup'
264
265     @openerpweb.httprequest
266     def restore(self, req, db_file, restore_pwd, new_db):
267         try:
268             data = base64.b64encode(db_file.file.read())
269             req.session.proxy("db").restore(restore_pwd, new_db, data)
270             return ''
271         except xmlrpclib.Fault, e:
272             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
273                 raise Exception("AccessDenied")
274
275     @openerpweb.jsonrequest
276     def change_password(self, req, fields):
277         old_password, new_password = operator.itemgetter(
278             'old_pwd', 'new_pwd')(
279                 dict(map(operator.itemgetter('name', 'value'), fields)))
280         try:
281             return req.session.proxy("db").change_admin_password(old_password, new_password)
282         except xmlrpclib.Fault, e:
283             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
284                 return {'error': e.faultCode, 'title': 'Change Password'}
285         return {'error': 'Error, password not changed !', 'title': 'Change Password'}
286
287 class Session(openerpweb.Controller):
288     _cp_path = "/web/session"
289
290     @openerpweb.jsonrequest
291     def login(self, req, db, login, password):
292         req.session.login(db, login, password)
293         ctx = req.session.get_context()
294
295         return {
296             "session_id": req.session_id,
297             "uid": req.session._uid,
298             "context": ctx
299         }
300     @openerpweb.jsonrequest
301     def change_password (self,req,fields):
302         old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
303                 dict(map(operator.itemgetter('name', 'value'), fields)))
304         if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
305             return {'error':'All passwords have to be filled.','title': 'Change Password'}
306         if new_password != confirm_password:
307             return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
308         try:
309             if req.session.model('res.users').change_password(
310                 old_password, new_password):
311                 return {'new_password':new_password}
312         except:
313             return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
314         return {'error': 'Error, password not changed !', 'title': 'Change Password'}
315     @openerpweb.jsonrequest
316     def sc_list(self, req):
317         return req.session.model('ir.ui.view_sc').get_sc(
318             req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
319
320     @openerpweb.jsonrequest
321     def get_lang_list(self, req):
322         try:
323             return {
324                 'lang_list': (req.session.proxy("db").list_lang() or []),
325                 'error': ""
326             }
327         except Exception, e:
328             return {"error": e, "title": "Languages"}
329             
330     @openerpweb.jsonrequest
331     def modules(self, req):
332         # TODO query server for installed web modules
333         mods = []
334         for name, manifest in openerpweb.addons_manifest.items():
335             if name != 'web' and manifest.get('active', True):
336                 mods.append(name)
337         return mods
338
339     @openerpweb.jsonrequest
340     def eval_domain_and_context(self, req, contexts, domains,
341                                 group_by_seq=None):
342         """ Evaluates sequences of domains and contexts, composing them into
343         a single context, domain or group_by sequence.
344
345         :param list contexts: list of contexts to merge together. Contexts are
346                               evaluated in sequence, all previous contexts
347                               are part of their own evaluation context
348                               (starting at the session context).
349         :param list domains: list of domains to merge together. Domains are
350                              evaluated in sequence and appended to one another
351                              (implicit AND), their evaluation domain is the
352                              result of merging all contexts.
353         :param list group_by_seq: list of domains (which may be in a different
354                                   order than the ``contexts`` parameter),
355                                   evaluated in sequence, their ``'group_by'``
356                                   key is extracted if they have one.
357         :returns:
358             a 3-dict of:
359
360             context (``dict``)
361                 the global context created by merging all of
362                 ``contexts``
363
364             domain (``list``)
365                 the concatenation of all domains
366
367             group_by (``list``)
368                 a list of fields to group by, potentially empty (in which case
369                 no group by should be performed)
370         """
371         context, domain = eval_context_and_domain(req.session,
372                                                   openerpweb.nonliterals.CompoundContext(*(contexts or [])),
373                                                   openerpweb.nonliterals.CompoundDomain(*(domains or [])))
374
375         group_by_sequence = []
376         for candidate in (group_by_seq or []):
377             ctx = req.session.eval_context(candidate, context)
378             group_by = ctx.get('group_by')
379             if not group_by:
380                 continue
381             elif isinstance(group_by, basestring):
382                 group_by_sequence.append(group_by)
383             else:
384                 group_by_sequence.extend(group_by)
385
386         return {
387             'context': context,
388             'domain': domain,
389             'group_by': group_by_sequence
390         }
391
392     @openerpweb.jsonrequest
393     def save_session_action(self, req, the_action):
394         """
395         This method store an action object in the session object and returns an integer
396         identifying that action. The method get_session_action() can be used to get
397         back the action.
398
399         :param the_action: The action to save in the session.
400         :type the_action: anything
401         :return: A key identifying the saved action.
402         :rtype: integer
403         """
404         saved_actions = req.httpsession.get('saved_actions')
405         if not saved_actions:
406             saved_actions = {"next":0, "actions":{}}
407             req.httpsession['saved_actions'] = saved_actions
408         # we don't allow more than 10 stored actions
409         if len(saved_actions["actions"]) >= 10:
410             del saved_actions["actions"][min(saved_actions["actions"].keys())]
411         key = saved_actions["next"]
412         saved_actions["actions"][key] = the_action
413         saved_actions["next"] = key + 1
414         return key
415
416     @openerpweb.jsonrequest
417     def get_session_action(self, req, key):
418         """
419         Gets back a previously saved action. This method can return None if the action
420         was saved since too much time (this case should be handled in a smart way).
421
422         :param key: The key given by save_session_action()
423         :type key: integer
424         :return: The saved action or None.
425         :rtype: anything
426         """
427         saved_actions = req.httpsession.get('saved_actions')
428         if not saved_actions:
429             return None
430         return saved_actions["actions"].get(key)
431
432     @openerpweb.jsonrequest
433     def check(self, req):
434         req.session.assert_valid()
435         return None
436
437 def eval_context_and_domain(session, context, domain=None):
438     e_context = session.eval_context(context)
439     # should we give the evaluated context as an evaluation context to the domain?
440     e_domain = session.eval_domain(domain or [])
441
442     return e_context, e_domain
443
444 def load_actions_from_ir_values(req, key, key2, models, meta):
445     context = req.session.eval_context(req.context)
446     Values = req.session.model('ir.values')
447     actions = Values.get(key, key2, models, meta, context)
448
449     return [(id, name, clean_action(req, action))
450             for id, name, action in actions]
451
452 def clean_action(req, action):
453     action.setdefault('flags', {})
454
455     context = req.session.eval_context(req.context)
456     eval_ctx = req.session.evaluation_context(context)
457
458     # values come from the server, we can just eval them
459     if isinstance(action.get('context'), basestring):
460         action['context'] = eval( action['context'], eval_ctx ) or {}
461
462     if isinstance(action.get('domain'), basestring):
463         action['domain'] = eval( action['domain'], eval_ctx ) or []
464
465     if action['type'] == 'ir.actions.act_window':
466         return fix_view_modes(action)
467     return action
468
469 # I think generate_views,fix_view_modes should go into js ActionManager
470 def generate_views(action):
471     """
472     While the server generates a sequence called "views" computing dependencies
473     between a bunch of stuff for views coming directly from the database
474     (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
475     to return custom view dictionaries generated on the fly.
476
477     In that case, there is no ``views`` key available on the action.
478
479     Since the web client relies on ``action['views']``, generate it here from
480     ``view_mode`` and ``view_id``.
481
482     Currently handles two different cases:
483
484     * no view_id, multiple view_mode
485     * single view_id, single view_mode
486
487     :param dict action: action descriptor dictionary to generate a views key for
488     """
489     view_id = action.get('view_id', False)
490     if isinstance(view_id, (list, tuple)):
491         view_id = view_id[0]
492
493     # providing at least one view mode is a requirement, not an option
494     view_modes = action['view_mode'].split(',')
495
496     if len(view_modes) > 1:
497         if view_id:
498             raise ValueError('Non-db action dictionaries should provide '
499                              'either multiple view modes or a single view '
500                              'mode and an optional view id.\n\n Got view '
501                              'modes %r and view id %r for action %r' % (
502                 view_modes, view_id, action))
503         action['views'] = [(False, mode) for mode in view_modes]
504         return
505     action['views'] = [(view_id, view_modes[0])]
506
507 def fix_view_modes(action):
508     """ For historical reasons, OpenERP has weird dealings in relation to
509     view_mode and the view_type attribute (on window actions):
510
511     * one of the view modes is ``tree``, which stands for both list views
512       and tree views
513     * the choice is made by checking ``view_type``, which is either
514       ``form`` for a list view or ``tree`` for an actual tree view
515
516     This methods simply folds the view_type into view_mode by adding a
517     new view mode ``list`` which is the result of the ``tree`` view_mode
518     in conjunction with the ``form`` view_type.
519
520     TODO: this should go into the doc, some kind of "peculiarities" section
521
522     :param dict action: an action descriptor
523     :returns: nothing, the action is modified in place
524     """
525     if 'views' not in action:
526         generate_views(action)
527
528     if action.pop('view_type') != 'form':
529         return action
530
531     action['views'] = [
532         [id, mode if mode != 'tree' else 'list']
533         for id, mode in action['views']
534     ]
535
536     return action
537
538 class Menu(openerpweb.Controller):
539     _cp_path = "/web/menu"
540
541     @openerpweb.jsonrequest
542     def load(self, req):
543         return {'data': self.do_load(req)}
544
545     def do_load(self, req):
546         """ Loads all menu items (all applications and their sub-menus).
547
548         :param req: A request object, with an OpenERP session attribute
549         :type req: < session -> OpenERPSession >
550         :return: the menu root
551         :rtype: dict('children': menu_nodes)
552         """
553         Menus = req.session.model('ir.ui.menu')
554         # menus are loaded fully unlike a regular tree view, cause there are
555         # less than 512 items
556         context = req.session.eval_context(req.context)
557         menu_ids = Menus.search([], 0, False, False, context)
558         menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
559         menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
560         menu_items.append(menu_root)
561
562         # make a tree using parent_id
563         menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
564         for menu_item in menu_items:
565             if menu_item['parent_id']:
566                 parent = menu_item['parent_id'][0]
567             else:
568                 parent = False
569             if parent in menu_items_map:
570                 menu_items_map[parent].setdefault(
571                     'children', []).append(menu_item)
572
573         # sort by sequence a tree using parent_id
574         for menu_item in menu_items:
575             menu_item.setdefault('children', []).sort(
576                 key=lambda x:x["sequence"])
577
578         return menu_root
579
580     @openerpweb.jsonrequest
581     def action(self, req, menu_id):
582         actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
583                                              [('ir.ui.menu', menu_id)], False)
584         return {"action": actions}
585
586 class DataSet(openerpweb.Controller):
587     _cp_path = "/web/dataset"
588
589     @openerpweb.jsonrequest
590     def fields(self, req, model):
591         return {'fields': req.session.model(model).fields_get(False,
592                                                               req.session.eval_context(req.context))}
593
594     @openerpweb.jsonrequest
595     def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
596         return self.do_search_read(req, model, fields, offset, limit, domain, sort)
597     def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
598                        , sort=None):
599         """ Performs a search() followed by a read() (if needed) using the
600         provided search criteria
601
602         :param req: a JSON-RPC request object
603         :type req: openerpweb.JsonRequest
604         :param str model: the name of the model to search on
605         :param fields: a list of the fields to return in the result records
606         :type fields: [str]
607         :param int offset: from which index should the results start being returned
608         :param int limit: the maximum number of records to return
609         :param list domain: the search domain for the query
610         :param list sort: sorting directives
611         :returns: A structure (dict) with two keys: ids (all the ids matching
612                   the (domain, context) pair) and records (paginated records
613                   matching fields selection set)
614         :rtype: list
615         """
616         Model = req.session.model(model)
617
618         context, domain = eval_context_and_domain(
619             req.session, req.context, domain)
620
621         ids = Model.search(domain, 0, False, sort or False, context)
622         # need to fill the dataset with all ids for the (domain, context) pair,
623         # so search un-paginated and paginate manually before reading
624         paginated_ids = ids[offset:(offset + limit if limit else None)]
625         if fields and fields == ['id']:
626             # shortcut read if we only want the ids
627             return {
628                 'ids': ids,
629                 'records': map(lambda id: {'id': id}, paginated_ids)
630             }
631
632         records = Model.read(paginated_ids, fields or False, context)
633         records.sort(key=lambda obj: ids.index(obj['id']))
634         return {
635             'ids': ids,
636             'records': records
637         }
638
639
640     @openerpweb.jsonrequest
641     def read(self, req, model, ids, fields=False):
642         return self.do_search_read(req, model, ids, fields)
643
644     @openerpweb.jsonrequest
645     def get(self, req, model, ids, fields=False):
646         return self.do_get(req, model, ids, fields)
647
648     def do_get(self, req, model, ids, fields=False):
649         """ Fetches and returns the records of the model ``model`` whose ids
650         are in ``ids``.
651
652         The results are in the same order as the inputs, but elements may be
653         missing (if there is no record left for the id)
654
655         :param req: the JSON-RPC2 request object
656         :type req: openerpweb.JsonRequest
657         :param model: the model to read from
658         :type model: str
659         :param ids: a list of identifiers
660         :type ids: list
661         :param fields: a list of fields to fetch, ``False`` or empty to fetch
662                        all fields in the model
663         :type fields: list | False
664         :returns: a list of records, in the same order as the list of ids
665         :rtype: list
666         """
667         Model = req.session.model(model)
668         records = Model.read(ids, fields, req.session.eval_context(req.context))
669
670         record_map = dict((record['id'], record) for record in records)
671
672         return [record_map[id] for id in ids if record_map.get(id)]
673
674     @openerpweb.jsonrequest
675     def load(self, req, model, id, fields):
676         m = req.session.model(model)
677         value = {}
678         r = m.read([id], False, req.session.eval_context(req.context))
679         if r:
680             value = r[0]
681         return {'value': value}
682
683     @openerpweb.jsonrequest
684     def create(self, req, model, data):
685         m = req.session.model(model)
686         r = m.create(data, req.session.eval_context(req.context))
687         return {'result': r}
688
689     @openerpweb.jsonrequest
690     def save(self, req, model, id, data):
691         m = req.session.model(model)
692         r = m.write([id], data, req.session.eval_context(req.context))
693         return {'result': r}
694
695     @openerpweb.jsonrequest
696     def unlink(self, req, model, ids=()):
697         Model = req.session.model(model)
698         return Model.unlink(ids, req.session.eval_context(req.context))
699
700     def call_common(self, req, model, method, args, domain_id=None, context_id=None):
701         domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id  else []
702         context = args[context_id] if context_id and len(args) - 1 >= context_id  else {}
703         c, d = eval_context_and_domain(req.session, context, domain)
704         if domain_id and len(args) - 1 >= domain_id:
705             args[domain_id] = d
706         if context_id and len(args) - 1 >= context_id:
707             args[context_id] = c
708         
709         for i in xrange(len(args)):
710             if isinstance(args[i], web.common.nonliterals.BaseContext):
711                 args[i] = req.session.eval_context(args[i])
712             if isinstance(args[i], web.common.nonliterals.BaseDomain):
713                 args[i] = req.session.eval_domain(args[i])
714
715         return getattr(req.session.model(model), method)(*args)
716
717     @openerpweb.jsonrequest
718     def call(self, req, model, method, args, domain_id=None, context_id=None):
719         return self.call_common(req, model, method, args, domain_id, context_id)
720
721     @openerpweb.jsonrequest
722     def call_button(self, req, model, method, args, domain_id=None, context_id=None):
723         action = self.call_common(req, model, method, args, domain_id, context_id)
724         if isinstance(action, dict) and action.get('type') != '':
725             return {'result': clean_action(req, action)}
726         return {'result': False}
727
728     @openerpweb.jsonrequest
729     def exec_workflow(self, req, model, id, signal):
730         r = req.session.exec_workflow(model, id, signal)
731         return {'result': r}
732
733     @openerpweb.jsonrequest
734     def default_get(self, req, model, fields):
735         Model = req.session.model(model)
736         return Model.default_get(fields, req.session.eval_context(req.context))
737
738     @openerpweb.jsonrequest
739     def name_search(self, req, model, search_str, domain=[], context={}):
740         m = req.session.model(model)
741         r = m.name_search(search_str+'%', domain, '=ilike', context)
742         return {'result': r}
743
744 class DataGroup(openerpweb.Controller):
745     _cp_path = "/web/group"
746     @openerpweb.jsonrequest
747     def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
748         Model = req.session.model(model)
749         context, domain = eval_context_and_domain(req.session, req.context, domain)
750
751         return Model.read_group(
752             domain or [], fields, group_by_fields, 0, False,
753             dict(context, group_by=group_by_fields), sort or False)
754
755 class View(openerpweb.Controller):
756     _cp_path = "/web/view"
757
758     def fields_view_get(self, req, model, view_id, view_type,
759                         transform=True, toolbar=False, submenu=False):
760         Model = req.session.model(model)
761         context = req.session.eval_context(req.context)
762         fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
763         # todo fme?: check that we should pass the evaluated context here
764         self.process_view(req.session, fvg, context, transform)
765         if toolbar and transform:
766             self.process_toolbar(req, fvg['toolbar'])
767         return fvg
768
769     def process_view(self, session, fvg, context, transform):
770         # depending on how it feels, xmlrpclib.ServerProxy can translate
771         # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
772         # enjoy unicode strings which can not be trivially converted to
773         # strings, and it blows up during parsing.
774
775         # So ensure we fix this retardation by converting view xml back to
776         # bit strings.
777         if isinstance(fvg['arch'], unicode):
778             arch = fvg['arch'].encode('utf-8')
779         else:
780             arch = fvg['arch']
781
782         if transform:
783             evaluation_context = session.evaluation_context(context or {})
784             xml = self.transform_view(arch, session, evaluation_context)
785         else:
786             xml = ElementTree.fromstring(arch)
787         fvg['arch'] = Xml2Json.convert_element(xml)
788
789         for field in fvg['fields'].itervalues():
790             if field.get('views'):
791                 for view in field["views"].itervalues():
792                     self.process_view(session, view, None, transform)
793             if field.get('domain'):
794                 field["domain"] = self.parse_domain(field["domain"], session)
795             if field.get('context'):
796                 field["context"] = self.parse_context(field["context"], session)
797
798     def process_toolbar(self, req, toolbar):
799         """
800         The toolbar is a mapping of section_key: [action_descriptor]
801
802         We need to clean all those actions in order to ensure correct
803         round-tripping
804         """
805         for actions in toolbar.itervalues():
806             for action in actions:
807                 if 'context' in action:
808                     action['context'] = self.parse_context(
809                         action['context'], req.session)
810                 if 'domain' in action:
811                     action['domain'] = self.parse_domain(
812                         action['domain'], req.session)
813
814     @openerpweb.jsonrequest
815     def add_custom(self, req, view_id, arch):
816         CustomView = req.session.model('ir.ui.view.custom')
817         CustomView.create({
818             'user_id': req.session._uid,
819             'ref_id': view_id,
820             'arch': arch
821         }, req.session.eval_context(req.context))
822         return {'result': True}
823
824     @openerpweb.jsonrequest
825     def undo_custom(self, req, view_id, reset=False):
826         CustomView = req.session.model('ir.ui.view.custom')
827         context = req.session.eval_context(req.context)
828         vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
829                                     0, False, False, context)
830         if vcustom:
831             if reset:
832                 CustomView.unlink(vcustom, context)
833             else:
834                 CustomView.unlink([vcustom[0]], context)
835             return {'result': True}
836         return {'result': False}
837
838     def transform_view(self, view_string, session, context=None):
839         # transform nodes on the fly via iterparse, instead of
840         # doing it statically on the parsing result
841         parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
842         root = None
843         for event, elem in parser:
844             if event == "start":
845                 if root is None:
846                     root = elem
847                 self.parse_domains_and_contexts(elem, session)
848         return root
849
850     def parse_domain(self, domain, session):
851         """ Parses an arbitrary string containing a domain, transforms it
852         to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
853
854         :param domain: the domain to parse, if the domain is not a string it
855                        is assumed to be a literal domain and is returned as-is
856         :param session: Current OpenERP session
857         :type session: openerpweb.openerpweb.OpenERPSession
858         """
859         if not isinstance(domain, (str, unicode)):
860             return domain
861         try:
862             return openerpweb.ast.literal_eval(domain)
863         except ValueError:
864             # not a literal
865             return openerpweb.nonliterals.Domain(session, domain)
866
867     def parse_context(self, context, session):
868         """ Parses an arbitrary string containing a context, transforms it
869         to either a literal context or a :class:`openerpweb.nonliterals.Context`
870
871         :param context: the context to parse, if the context is not a string it
872                is assumed to be a literal domain and is returned as-is
873         :param session: Current OpenERP session
874         :type session: openerpweb.openerpweb.OpenERPSession
875         """
876         if not isinstance(context, (str, unicode)):
877             return context
878         try:
879             return openerpweb.ast.literal_eval(context)
880         except ValueError:
881             return openerpweb.nonliterals.Context(session, context)
882
883     def parse_domains_and_contexts(self, elem, session):
884         """ Converts domains and contexts from the view into Python objects,
885         either literals if they can be parsed by literal_eval or a special
886         placeholder object if the domain or context refers to free variables.
887
888         :param elem: the current node being parsed
889         :type param: xml.etree.ElementTree.Element
890         :param session: OpenERP session object, used to store and retrieve
891                         non-literal objects
892         :type session: openerpweb.openerpweb.OpenERPSession
893         """
894         for el in ['domain', 'filter_domain']:
895             domain = elem.get(el, '').strip()
896             if domain:
897                 elem.set(el, self.parse_domain(domain, session))
898         for el in ['context', 'default_get']:
899             context_string = elem.get(el, '').strip()
900             if context_string:
901                 elem.set(el, self.parse_context(context_string, session))
902
903     @openerpweb.jsonrequest
904     def load(self, req, model, view_id, view_type, toolbar=False):
905         return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
906
907 class ListView(View):
908     _cp_path = "/web/listview"
909
910     def process_colors(self, view, row, context):
911         colors = view['arch']['attrs'].get('colors')
912
913         if not colors:
914             return None
915
916         color = [
917             pair.split(':')[0]
918             for pair in colors.split(';')
919             if eval(pair.split(':')[1], dict(context, **row))
920         ]
921
922         if not color:
923             return None
924         elif len(color) == 1:
925             return color[0]
926         return 'maroon'
927
928 class TreeView(View):
929     _cp_path = "/web/treeview"
930
931     @openerpweb.jsonrequest
932     def action(self, req, model, id):
933         return load_actions_from_ir_values(
934             req,'action', 'tree_but_open',[(model, id)],
935             False)
936
937 class SearchView(View):
938     _cp_path = "/web/searchview"
939
940     @openerpweb.jsonrequest
941     def load(self, req, model, view_id):
942         fields_view = self.fields_view_get(req, model, view_id, 'search')
943         return {'fields_view': fields_view}
944
945     @openerpweb.jsonrequest
946     def fields_get(self, req, model):
947         Model = req.session.model(model)
948         fields = Model.fields_get(False, req.session.eval_context(req.context))
949         for field in fields.values():
950             # shouldn't convert the views too?
951             if field.get('domain'):
952                 field["domain"] = self.parse_domain(field["domain"], req.session)
953             if field.get('context'):
954                 field["context"] = self.parse_domain(field["context"], req.session)
955         return {'fields': fields}
956     
957     @openerpweb.jsonrequest
958     def get_filters(self, req, model):
959         Model = req.session.model("ir.filters")
960         filters = Model.get_filters(model)
961         for filter in filters:
962             filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
963             filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
964         return filters
965     
966     @openerpweb.jsonrequest
967     def save_filter(self, req, model, name, context_to_save, domain):
968         Model = req.session.model("ir.filters")
969         ctx = openerpweb.nonliterals.CompoundContext(context_to_save)
970         ctx.session = req.session
971         ctx = ctx.evaluate()
972         domain = openerpweb.nonliterals.CompoundDomain(domain)
973         domain.session = req.session
974         domain = domain.evaluate()
975         uid = req.session._uid
976         context = req.session.eval_context(req.context)
977         to_return = Model.create_or_replace({"context": ctx,
978                                              "domain": domain,
979                                              "model_id": model,
980                                              "name": name,
981                                              "user_id": uid
982                                              }, context)
983         return to_return
984
985 class Binary(openerpweb.Controller):
986     _cp_path = "/web/binary"
987
988     @openerpweb.httprequest
989     def image(self, req, model, id, field, **kw):
990         Model = req.session.model(model)
991         context = req.session.eval_context(req.context)
992
993         try:
994             if not id:
995                 res = Model.default_get([field], context).get(field, '')
996             else:
997                 res = Model.read([int(id)], [field], context)[0].get(field, '')
998             image_data = base64.b64decode(res)
999         except (TypeError, xmlrpclib.Fault):
1000             image_data = self.placeholder(req)
1001         return req.make_response(image_data, [
1002             ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
1003     def placeholder(self, req):
1004         return open(os.path.join(req.addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1005
1006     @openerpweb.httprequest
1007     def saveas(self, req, model, id, field, fieldname, **kw):
1008         Model = req.session.model(model)
1009         context = req.session.eval_context(req.context)
1010         res = Model.read([int(id)], [field, fieldname], context)[0]
1011         filecontent = res.get(field, '')
1012         if not filecontent:
1013             return req.not_found()
1014         else:
1015             filename = '%s_%s' % (model.replace('.', '_'), id)
1016             if fieldname:
1017                 filename = res.get(fieldname, '') or filename
1018             return req.make_response(filecontent,
1019                 [('Content-Type', 'application/octet-stream'),
1020                  ('Content-Disposition', 'attachment; filename=' +  filename)])
1021
1022     @openerpweb.httprequest
1023     def upload(self, req, callback, ufile):
1024         # TODO: might be useful to have a configuration flag for max-length file uploads
1025         try:
1026             out = """<script language="javascript" type="text/javascript">
1027                         var win = window.top.window,
1028                             callback = win[%s];
1029                         if (typeof(callback) === 'function') {
1030                             callback.apply(this, %s);
1031                         } else {
1032                             win.jQuery('#oe_notification', win.document).notify('create', {
1033                                 title: "Ajax File Upload",
1034                                 text: "Could not find callback"
1035                             });
1036                         }
1037                     </script>"""
1038             data = ufile.read()
1039             args = [ufile.content_length, ufile.filename,
1040                     ufile.content_type, base64.b64encode(data)]
1041         except Exception, e:
1042             args = [False, e.message]
1043         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1044
1045     @openerpweb.httprequest
1046     def upload_attachment(self, req, callback, model, id, ufile):
1047         context = req.session.eval_context(req.context)
1048         Model = req.session.model('ir.attachment')
1049         try:
1050             out = """<script language="javascript" type="text/javascript">
1051                         var win = window.top.window,
1052                             callback = win[%s];
1053                         if (typeof(callback) === 'function') {
1054                             callback.call(this, %s);
1055                         }
1056                     </script>"""
1057             attachment_id = Model.create({
1058                 'name': ufile.filename,
1059                 'datas': base64.encodestring(ufile.read()),
1060                 'res_model': model,
1061                 'res_id': int(id)
1062             }, context)
1063             args = {
1064                 'filename': ufile.filename,
1065                 'id':  attachment_id
1066             }
1067         except Exception, e:
1068             args = { 'error': e.message }
1069         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1070
1071 class Action(openerpweb.Controller):
1072     _cp_path = "/web/action"
1073
1074     @openerpweb.jsonrequest
1075     def load(self, req, action_id):
1076         Actions = req.session.model('ir.actions.actions')
1077         value = False
1078         context = req.session.eval_context(req.context)
1079         action_type = Actions.read([action_id], ['type'], context)
1080         if action_type:
1081             ctx = {}
1082             if action_type[0]['type'] == 'ir.actions.report.xml':
1083                 ctx.update({'bin_size': True})
1084             ctx.update(context)
1085             action = req.session.model(action_type[0]['type']).read([action_id], False, ctx)
1086             if action:
1087                 value = clean_action(req, action[0])
1088         return {'result': value}
1089
1090     @openerpweb.jsonrequest
1091     def run(self, req, action_id):
1092         return clean_action(req, req.session.model('ir.actions.server').run(
1093             [action_id], req.session.eval_context(req.context)))
1094
1095 class Export(View):
1096     _cp_path = "/web/export"
1097
1098     @openerpweb.jsonrequest
1099     def formats(self, req):
1100         """ Returns all valid export formats
1101
1102         :returns: for each export format, a pair of identifier and printable name
1103         :rtype: [(str, str)]
1104         """
1105         return sorted([
1106             controller.fmt
1107             for path, controller in openerpweb.controllers_path.iteritems()
1108             if path.startswith(self._cp_path)
1109             if hasattr(controller, 'fmt')
1110         ], key=operator.itemgetter(1))
1111
1112     def fields_get(self, req, model):
1113         Model = req.session.model(model)
1114         fields = Model.fields_get(False, req.session.eval_context(req.context))
1115         return fields
1116
1117     @openerpweb.jsonrequest
1118     def get_fields(self, req, model, prefix='', parent_name= '',
1119                    import_compat=True, parent_field_type=None):
1120
1121         if import_compat and parent_field_type == "many2one":
1122             fields = {}
1123         else:
1124             fields = self.fields_get(req, model)
1125         fields['.id'] = fields.pop('id') if 'id' in fields else {'string': 'ID'}
1126
1127         fields_sequence = sorted(fields.iteritems(),
1128             key=lambda field: field[1].get('string', ''))
1129
1130         records = []
1131         for field_name, field in fields_sequence:
1132             if import_compat and field.get('readonly'):
1133                 # If none of the field's states unsets readonly, skip the field
1134                 if all(dict(attrs).get('readonly', True)
1135                        for attrs in field.get('states', {}).values()):
1136                     continue
1137
1138             id = prefix + (prefix and '/'or '') + field_name
1139             name = parent_name + (parent_name and '/' or '') + field['string']
1140             record = {'id': id, 'string': name,
1141                       'value': id, 'children': False,
1142                       'field_type': field.get('type'),
1143                       'required': field.get('required')}
1144             records.append(record)
1145
1146             if len(name.split('/')) < 3 and 'relation' in field:
1147                 ref = field.pop('relation')
1148                 record['value'] += '/id'
1149                 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1150
1151                 if not import_compat or field['type'] == 'one2many':
1152                     # m2m field in import_compat is childless
1153                     record['children'] = True
1154
1155         return records
1156
1157     @openerpweb.jsonrequest
1158     def namelist(self,req,  model, export_id):
1159         # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1160         export = req.session.model("ir.exports").read([export_id])[0]
1161         export_fields_list = req.session.model("ir.exports.line").read(
1162             export['export_fields'])
1163
1164         fields_data = self.fields_info(
1165             req, model, map(operator.itemgetter('name'), export_fields_list))
1166
1167         return [
1168             {'name': field['name'], 'label': fields_data[field['name']]}
1169             for field in export_fields_list
1170         ]
1171
1172     def fields_info(self, req, model, export_fields):
1173         info = {}
1174         fields = self.fields_get(req, model)
1175         fields['.id'] = fields.pop('id') if 'id' in fields else {'string': 'ID'}
1176
1177         # To make fields retrieval more efficient, fetch all sub-fields of a
1178         # given field at the same time. Because the order in the export list is
1179         # arbitrary, this requires ordering all sub-fields of a given field
1180         # together so they can be fetched at the same time
1181         #
1182         # Works the following way:
1183         # * sort the list of fields to export, the default sorting order will
1184         #   put the field itself (if present, for xmlid) and all of its
1185         #   sub-fields right after it
1186         # * then, group on: the first field of the path (which is the same for
1187         #   a field and for its subfields and the length of splitting on the
1188         #   first '/', which basically means grouping the field on one side and
1189         #   all of the subfields on the other. This way, we have the field (for
1190         #   the xmlid) with length 1, and all of the subfields with the same
1191         #   base but a length "flag" of 2
1192         # * if we have a normal field (length 1), just add it to the info
1193         #   mapping (with its string) as-is
1194         # * otherwise, recursively call fields_info via graft_subfields.
1195         #   all graft_subfields does is take the result of fields_info (on the
1196         #   field's model) and prepend the current base (current field), which
1197         #   rebuilds the whole sub-tree for the field
1198         #
1199         # result: because we're not fetching the fields_get for half the
1200         # database models, fetching a namelist with a dozen fields (including
1201         # relational data) falls from ~6s to ~300ms (on the leads model).
1202         # export lists with no sub-fields (e.g. import_compatible lists with
1203         # no o2m) are even more efficient (from the same 6s to ~170ms, as
1204         # there's a single fields_get to execute)
1205         for (base, length), subfields in itertools.groupby(
1206                 sorted(export_fields),
1207                 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1208             subfields = list(subfields)
1209             if length == 2:
1210                 # subfields is a seq of $base/*rest, and not loaded yet
1211                 info.update(self.graft_subfields(
1212                     req, fields[base]['relation'], base, fields[base]['string'],
1213                     subfields
1214                 ))
1215             else:
1216                 info[base] = fields[base]['string']
1217
1218         return info
1219
1220     def graft_subfields(self, req, model, prefix, prefix_string, fields):
1221         export_fields = [field.split('/', 1)[1] for field in fields]
1222         return (
1223             (prefix + '/' + k, prefix_string + '/' + v)
1224             for k, v in self.fields_info(req, model, export_fields).iteritems())
1225
1226     #noinspection PyPropertyDefinition
1227     @property
1228     def content_type(self):
1229         """ Provides the format's content type """
1230         raise NotImplementedError()
1231
1232     def filename(self, base):
1233         """ Creates a valid filename for the format (with extension) from the
1234          provided base name (exension-less)
1235         """
1236         raise NotImplementedError()
1237
1238     def from_data(self, fields, rows):
1239         """ Conversion method from OpenERP's export data to whatever the
1240         current export class outputs
1241
1242         :params list fields: a list of fields to export
1243         :params list rows: a list of records to export
1244         :returns:
1245         :rtype: bytes
1246         """
1247         raise NotImplementedError()
1248
1249     @openerpweb.httprequest
1250     def index(self, req, data, token):
1251         model, fields, ids, domain, import_compat = \
1252             operator.itemgetter('model', 'fields', 'ids', 'domain',
1253                                 'import_compat')(
1254                 simplejson.loads(data))
1255
1256         context = req.session.eval_context(req.context)
1257         Model = req.session.model(model)
1258         ids = ids or Model.search(domain, context=context)
1259
1260         field_names = map(operator.itemgetter('name'), fields)
1261         import_data = Model.export_data(ids, field_names, context).get('datas',[])
1262
1263         if import_compat:
1264             columns_headers = field_names
1265         else:
1266             columns_headers = [val['label'].strip() for val in fields]
1267
1268
1269         return req.make_response(self.from_data(columns_headers, import_data),
1270             headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1271                      ('Content-Type', self.content_type)],
1272             cookies={'fileToken': int(token)})
1273
1274 class CSVExport(Export):
1275     _cp_path = '/web/export/csv'
1276     fmt = ('csv', 'CSV')
1277
1278     @property
1279     def content_type(self):
1280         return 'text/csv;charset=utf8'
1281
1282     def filename(self, base):
1283         return base + '.csv'
1284
1285     def from_data(self, fields, rows):
1286         fp = StringIO()
1287         writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1288
1289         writer.writerow(fields)
1290
1291         for data in rows:
1292             row = []
1293             for d in data:
1294                 if isinstance(d, basestring):
1295                     d = d.replace('\n',' ').replace('\t',' ')
1296                     try:
1297                         d = d.encode('utf-8')
1298                     except:
1299                         pass
1300                 if d is False: d = None
1301                 row.append(d)
1302             writer.writerow(row)
1303
1304         fp.seek(0)
1305         data = fp.read()
1306         fp.close()
1307         return data
1308
1309 class ExcelExport(Export):
1310     _cp_path = '/web/export/xls'
1311     fmt = ('xls', 'Excel')
1312
1313     @property
1314     def content_type(self):
1315         return 'application/vnd.ms-excel'
1316
1317     def filename(self, base):
1318         return base + '.xls'
1319
1320     def from_data(self, fields, rows):
1321         import xlwt
1322
1323         workbook = xlwt.Workbook()
1324         worksheet = workbook.add_sheet('Sheet 1')
1325
1326         for i, fieldname in enumerate(fields):
1327             worksheet.write(0, i, str(fieldname))
1328             worksheet.col(i).width = 8000 # around 220 pixels
1329
1330         style = xlwt.easyxf('align: wrap yes')
1331
1332         for row_index, row in enumerate(rows):
1333             for cell_index, cell_value in enumerate(row):
1334                 if isinstance(cell_value, basestring):
1335                     cell_value = re.sub("\r", " ", cell_value)
1336                 worksheet.write(row_index + 1, cell_index, cell_value, style)
1337
1338         fp = StringIO()
1339         workbook.save(fp)
1340         fp.seek(0)
1341         data = fp.read()
1342         fp.close()
1343         return data
1344
1345 class Reports(View):
1346     _cp_path = "/web/report"
1347     POLLING_DELAY = 0.25
1348     TYPES_MAPPING = {
1349         'doc': 'application/vnd.ms-word',
1350         'html': 'text/html',
1351         'odt': 'application/vnd.oasis.opendocument.text',
1352         'pdf': 'application/pdf',
1353         'sxw': 'application/vnd.sun.xml.writer',
1354         'xls': 'application/vnd.ms-excel',
1355     }
1356
1357     @openerpweb.httprequest
1358     def index(self, req, action, token):
1359         action = simplejson.loads(action)
1360
1361         report_srv = req.session.proxy("report")
1362         context = req.session.eval_context(
1363             openerpweb.nonliterals.CompoundContext(
1364                 req.context or {}, action[ "context"]))
1365
1366         report_data = {}
1367         report_ids = context["active_ids"]
1368         if 'report_type' in action:
1369             report_data['report_type'] = action['report_type']
1370         if 'datas' in action:
1371             if 'form' in action['datas']:
1372                 report_data['form'] = action['datas']['form']
1373             if 'ids' in action['datas']:
1374                 report_ids = action['datas']['ids']
1375         report_id = report_srv.report(
1376             req.session._db, req.session._uid, req.session._password,
1377             action["report_name"], report_ids,
1378             report_data, context)
1379
1380         report_struct = None
1381         while True:
1382             report_struct = report_srv.report_get(
1383                 req.session._db, req.session._uid, req.session._password, report_id)
1384             if report_struct["state"]:
1385                 break
1386             time.sleep(self.POLLING_DELAY)
1387
1388         report = base64.b64decode(report_struct['result'])
1389         if report_struct.get('code') == 'zlib':
1390             report = zlib.decompress(report)
1391         report_mimetype = self.TYPES_MAPPING.get(
1392             report_struct['format'], 'octet-stream')
1393         return req.make_response(report,
1394              headers=[
1395                  ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], report_struct['format'])),
1396                  ('Content-Type', report_mimetype),
1397                  ('Content-Length', len(report))],
1398              cookies={'fileToken': int(token)})