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