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