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