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