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