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