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