[FIX] regenerate action's views key if it is empty, not just if it's absent
[odoo/odoo.git] / addons / web / controllers / main.py
index 225e55c..397a6a0 100644 (file)
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 
+import ast
 import base64
 import csv
 import glob
@@ -8,151 +9,193 @@ import operator
 import os
 import re
 import simplejson
-import textwrap
-import xmlrpclib
 import time
+import xmlrpclib
 import zlib
-import webrelease
 from xml.etree import ElementTree
 from cStringIO import StringIO
 
-import web.common.dispatch as openerpweb
-import web.common.ast
-import web.common.nonliterals
-openerpweb.ast = web.common.ast
-openerpweb.nonliterals = web.common.nonliterals
-
-from babel.messages.pofile import read_po
-
-# Should move to openerpweb.Xml2Json
-class Xml2Json:
-    # xml2json-direct
-    # Simple and straightforward XML-to-JSON converter in Python
-    # New BSD Licensed
-    #
-    # URL: http://code.google.com/p/xml2json-direct/
-    @staticmethod
-    def convert_to_json(s):
-        return simplejson.dumps(
-            Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
-
-    @staticmethod
-    def convert_to_structure(s):
-        root = ElementTree.fromstring(s)
-        return Xml2Json.convert_element(root)
-
-    @staticmethod
-    def convert_element(el, skip_whitespaces=True):
-        res = {}
-        if el.tag[0] == "{":
-            ns, name = el.tag.rsplit("}", 1)
-            res["tag"] = name
-            res["namespace"] = ns[1:]
-        else:
-            res["tag"] = el.tag
-        res["attrs"] = {}
-        for k, v in el.items():
-            res["attrs"][k] = v
-        kids = []
-        if el.text and (not skip_whitespaces or el.text.strip() != ''):
-            kids.append(el.text)
-        for kid in el:
-            kids.append(Xml2Json.convert_element(kid))
-            if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
-                kids.append(kid.tail)
-        res["children"] = kids
-        return res
+import babel.messages.pofile
+
+import web.common
+openerpweb = web.common.http
 
 #----------------------------------------------------------
 # OpenERP Web web Controllers
 #----------------------------------------------------------
 
-def manifest_glob(addons_path, addons, key):
-    files = []
-    for addon in addons:
-        globlist = openerpweb.addons_manifest.get(addon, {}).get(key, [])
-        for pattern in globlist:
-            for path in glob.glob(os.path.join(addons_path, addon, pattern)):
-                files.append(path[len(addons_path):])
-    return files
 
-def concat_files(addons_path, file_list):
-    """ Concatenate file content
+def concat_xml(file_list):
+    """Concatenate xml files
     return (concat,timestamp)
     concat: concatenation of file content
     timestamp: max(os.path.getmtime of file_list)
     """
+    root = None
+    files_timestamp = 0
+    for fname in file_list:
+        ftime = os.path.getmtime(fname)
+        if ftime > files_timestamp:
+            files_timestamp = ftime
+
+        xml = ElementTree.parse(fname).getroot()
+
+        if root is None:
+            root = ElementTree.Element(xml.tag)
+        #elif root.tag != xml.tag:
+        #    raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
+
+        for child in xml.getchildren():
+            root.append(child)
+    return ElementTree.tostring(root, 'utf-8'), files_timestamp
+
+
+def concat_files(file_list, reader=None):
+    """ Concatenate file content
+    return (concat,timestamp)
+    concat: concatenation of file content, read by `reader`
+    timestamp: max(os.path.getmtime of file_list)
+    """
+    if reader is None:
+        def reader(f):
+            with open(f) as fp:
+                return fp.read()
+
     files_content = []
     files_timestamp = 0
-    for i in file_list:
-        fname = os.path.join(addons_path, i[1:])
+    for fname in file_list:
         ftime = os.path.getmtime(fname)
         if ftime > files_timestamp:
             files_timestamp = ftime
-        files_content.append(open(fname).read())
+
+        files_content.append(reader(fname))
     files_concat = "".join(files_content)
     return files_concat,files_timestamp
 
-home_template = textwrap.dedent("""<!DOCTYPE html>
+html_template = """<!DOCTYPE html>
 <html style="height: 100%%">
     <head>
         <meta http-equiv="content-type" content="text/html; charset=utf-8" />
         <title>OpenERP</title>
         <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
         %(css)s
-        %(javascript)s
+        %(js)s
         <script type="text/javascript">
             $(function() {
-                var c = new openerp.init();
-                var wc = new c.web.WebClient("oe");
-                wc.start();
+                var s = new openerp.init(%(modules)s);
+                %(init)s
             });
         </script>
     </head>
     <body id="oe" class="openerp"></body>
 </html>
-""")
+"""
+
 class WebClient(openerpweb.Controller):
     _cp_path = "/web/webclient"
 
+    def server_wide_modules(self, req):
+        addons = [i for i in req.config.server_wide_modules if i in openerpweb.addons_manifest]
+        return addons
+
+    def manifest_glob(self, req, addons, key):
+        if addons is None:
+            addons = self.server_wide_modules(req)
+        else:
+            addons = addons.split(',')
+        r = []
+        for addon in addons:
+            manifest = openerpweb.addons_manifest.get(addon, None)
+            if not manifest:
+                continue
+            # ensure does not ends with /
+            addons_path = os.path.join(manifest['addons_path'], '')[:-1]
+            globlist = manifest.get(key, [])
+            for pattern in globlist:
+                for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
+                    r.append( (path, path[len(addons_path):]))
+        return r
+
+    def manifest_list(self, req, mods, extension):
+        if not req.debug:
+            path = '/web/webclient/' + extension
+            if mods is not None:
+                path += '?mods=' + mods
+            return [path]
+        return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in self.manifest_glob(req, mods, extension)]
+
     @openerpweb.jsonrequest
-    def csslist(self, req, mods='web'):
-        return manifest_glob(req.config.addons_path, mods.split(','), 'css')
+    def csslist(self, req, mods=None):
+        return self.manifest_list(req, mods, 'css')
 
     @openerpweb.jsonrequest
-    def jslist(self, req, mods='web'):
-        return manifest_glob(req.config.addons_path, mods.split(','), 'js')
+    def jslist(self, req, mods=None):
+        return self.manifest_list(req, mods, 'js')
+
+    @openerpweb.jsonrequest
+    def qweblist(self, req, mods=None):
+        return self.manifest_list(req, mods, 'qweb')
 
     @openerpweb.httprequest
-    def css(self, req, mods='web'):
-        files = manifest_glob(req.config.addons_path, mods.split(','), 'css')
-        content,timestamp = concat_files(req.config.addons_path, files)
-        # TODO request set the Date of last modif and Etag
+    def css(self, req, mods=None):
+
+        files = list(self.manifest_glob(req, mods, 'css'))
+        file_map = dict(files)
+
+        rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
+        rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://)""", re.U)
+
+
+        def reader(f):
+            """read the a css file and absolutify all relative uris"""
+            with open(f) as fp:
+                data = fp.read()
+
+            web_path = file_map[f]
+            web_dir = os.path.dirname(web_path)
+
+            data = re.sub(
+                rx_import,
+                r"""@import \1%s/""" % (web_dir,),
+                data,
+            )
+
+            data = re.sub(
+                rx_url,
+                r"""url(\1%s/""" % (web_dir,),
+                data,
+            )
+            return data
+
+        content,timestamp = concat_files((f[0] for f in files), reader)
+        # TODO use timestamp to set Last mofified date and E-tag
         return req.make_response(content, [('Content-Type', 'text/css')])
 
     @openerpweb.httprequest
-    def js(self, req, mods='web'):
-        files = manifest_glob(req.config.addons_path, mods.split(','), 'js')
-        content,timestamp = concat_files(req.config.addons_path, files)
-        # TODO request set the Date of last modif and Etag
+    def js(self, req, mods=None):
+        files = [f[0] for f in self.manifest_glob(req, mods, 'js')]
+        content,timestamp = concat_files(files)
+        # TODO use timestamp to set Last mofified date and E-tag
         return req.make_response(content, [('Content-Type', 'application/javascript')])
 
     @openerpweb.httprequest
+    def qweb(self, req, mods=None):
+        files = [f[0] for f in self.manifest_glob(req, mods, 'qweb')]
+        content,timestamp = concat_xml(files)
+        # TODO use timestamp to set Last mofified date and E-tag
+        return req.make_response(content, [('Content-Type', 'text/xml')])
+
+
+    @openerpweb.httprequest
     def home(self, req, s_action=None, **kw):
-        # script tags
-        jslist = ['/web/webclient/js']
-        if req.debug:
-            jslist = [i + '?debug=' + str(time.time()) for i in manifest_glob(req.config.addons_path, ['web'], 'js')]
-        js = "\n        ".join(['<script type="text/javascript" src="%s"></script>'%i for i in jslist])
-
-        # css tags
-        csslist = ['/web/webclient/css']
-        if req.debug:
-            csslist = [i + '?debug=' + str(time.time()) for i in manifest_glob(req.config.addons_path, ['web'], 'css')]
-        css = "\n        ".join(['<link rel="stylesheet" href="%s">'%i for i in csslist])
-        r = home_template % {
-            'javascript': js,
-            'css': css
+        js = "\n        ".join('<script type="text/javascript" src="%s"></script>'%i for i in self.manifest_list(req, None, 'js'))
+        css = "\n        ".join('<link rel="stylesheet" href="%s">'%i for i in self.manifest_list(req, None, 'css'))
+
+        r = html_template % {
+            'js': js,
+            'css': css,
+            'modules': simplejson.dumps(self.server_wide_modules(req)),
+            'init': 'new s.web.WebClient("oe").start();',
         }
         return r
 
@@ -165,25 +208,26 @@ class WebClient(openerpweb.Controller):
                                                 "grouping", "decimal_point", "thousands_sep"])
         else:
             lang_obj = None
-            
+
         if lang.count("_") > 0:
             separator = "_"
         else:
             separator = "@"
         langs = lang.split(separator)
         langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
-        
+
         transs = {}
         for addon_name in mods:
             transl = {"messages":[]}
             transs[addon_name] = transl
             for l in langs:
-                f_name = os.path.join(req.config.addons_path, addon_name, "po", l + ".po")
+                addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
+                f_name = os.path.join(addons_path, addon_name, "po", l + ".po")
                 if not os.path.exists(f_name):
                     continue
                 try:
                     with open(f_name) as t_file:
-                        po = read_po(t_file)
+                        po = babel.messages.pofile.read_po(t_file)
                 except:
                     continue
                 for x in po:
@@ -195,7 +239,7 @@ class WebClient(openerpweb.Controller):
     @openerpweb.jsonrequest
     def version_info(self, req):
         return {
-            "version": webrelease.version
+            "version": web.common.release.version
         }
 
 class Database(openerpweb.Controller):
@@ -226,20 +270,25 @@ class Database(openerpweb.Controller):
             params['db_lang'],
             params['create_admin_pwd']
         )
-
+        
         try:
             return req.session.proxy("db").create(*create_attrs)
         except xmlrpclib.Fault, e:
-            if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
-                return {'error': e.faultCode, 'title': 'Create Database'}
-        return {'error': 'Could not create database !', 'title': 'Create Database'}
+            if e.faultCode and isinstance(e.faultCode, str)\
+                and e.faultCode.split(':')[0] == 'AccessDenied':
+                    return {'error': e.faultCode, 'title': 'Database creation error'}
+            return {
+                'error': "Could not create database '%s': %s" % (
+                    params['db_name'], e.faultString),
+                'title': 'Database creation error'
+            }
 
     @openerpweb.jsonrequest
     def drop(self, req, fields):
         password, db = operator.itemgetter(
             'drop_pwd', 'drop_db')(
                 dict(map(operator.itemgetter('name', 'value'), fields)))
-        
+
         try:
             return req.session.proxy("db").drop(password, db)
         except xmlrpclib.Fault, e:
@@ -265,7 +314,7 @@ class Database(openerpweb.Controller):
     @openerpweb.httprequest
     def restore(self, req, db_file, restore_pwd, new_db):
         try:
-            data = base64.b64encode(db_file.file.read())
+            data = base64.b64encode(db_file.read())
             req.session.proxy("db").restore(restore_pwd, new_db, data)
             return ''
         except xmlrpclib.Fault, e:
@@ -290,13 +339,26 @@ class Session(openerpweb.Controller):
     @openerpweb.jsonrequest
     def login(self, req, db, login, password):
         req.session.login(db, login, password)
-        ctx = req.session.get_context()
+        ctx = req.session.get_context() if req.session._uid else {}
 
         return {
             "session_id": req.session_id,
             "uid": req.session._uid,
-            "context": ctx
+            "context": ctx,
+            "db": req.session._db,
+            "login": req.session._login
         }
+
+    @openerpweb.jsonrequest
+    def get_session_info(self, req):
+        req.session.assert_valid(force=True)
+        return {
+            "uid": req.session._uid,
+            "context": req.session.get_context() if req.session._uid else False,
+            "db": req.session._db,
+            "login": req.session._login
+        }
+
     @openerpweb.jsonrequest
     def change_password (self,req,fields):
         old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
@@ -312,6 +374,7 @@ class Session(openerpweb.Controller):
         except:
             return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
         return {'error': 'Error, password not changed !', 'title': 'Change Password'}
+
     @openerpweb.jsonrequest
     def sc_list(self, req):
         return req.session.model('ir.ui.view_sc').get_sc(
@@ -326,15 +389,25 @@ class Session(openerpweb.Controller):
             }
         except Exception, e:
             return {"error": e, "title": "Languages"}
-            
+
     @openerpweb.jsonrequest
     def modules(self, req):
-        # TODO query server for installed web modules
-        mods = []
-        for name, manifest in openerpweb.addons_manifest.items():
-            if name != 'web' and manifest.get('active', True):
-                mods.append(name)
-        return mods
+        # Compute available candidates module
+        loadable = openerpweb.addons_manifest.iterkeys()
+        loaded = req.config.server_wide_modules
+        candidates = [mod for mod in loadable if mod not in loaded]
+
+        # Compute active true modules that might be on the web side only
+        active = set(name for name in candidates
+                     if openerpweb.addons_manifest[name].get('active'))
+
+        # Retrieve database installed modules
+        Modules = req.session.model('ir.module.module')
+        installed = set(module['name'] for module in Modules.search_read(
+            [('state','=','installed'), ('name','in', candidates)], ['name']))
+
+        # Merge both
+        return list(active | installed)
 
     @openerpweb.jsonrequest
     def eval_domain_and_context(self, req, contexts, domains,
@@ -369,8 +442,8 @@ class Session(openerpweb.Controller):
                 no group by should be performed)
         """
         context, domain = eval_context_and_domain(req.session,
-                                                  openerpweb.nonliterals.CompoundContext(*(contexts or [])),
-                                                  openerpweb.nonliterals.CompoundDomain(*(domains or [])))
+                                                  web.common.nonliterals.CompoundContext(*(contexts or [])),
+                                                  web.common.nonliterals.CompoundDomain(*(domains or [])))
 
         group_by_sequence = []
         for candidate in (group_by_seq or []):
@@ -449,18 +522,27 @@ def load_actions_from_ir_values(req, key, key2, models, meta):
     return [(id, name, clean_action(req, action))
             for id, name, action in actions]
 
-def clean_action(req, action):
+def clean_action(req, action, do_not_eval=False):
     action.setdefault('flags', {})
 
     context = req.session.eval_context(req.context)
     eval_ctx = req.session.evaluation_context(context)
 
-    # values come from the server, we can just eval them
-    if isinstance(action.get('context'), basestring):
-        action['context'] = eval( action['context'], eval_ctx ) or {}
+    if not do_not_eval:
+        # values come from the server, we can just eval them
+        if isinstance(action.get('context'), basestring):
+            action['context'] = eval( action['context'], eval_ctx ) or {}
+
+        if isinstance(action.get('domain'), basestring):
+            action['domain'] = eval( action['domain'], eval_ctx ) or []
+    else:
+        if 'context' in action:
+            action['context'] = parse_context(action['context'], req.session)
+        if 'domain' in action:
+            action['domain'] = parse_domain(action['domain'], req.session)
 
-    if isinstance(action.get('domain'), basestring):
-        action['domain'] = eval( action['domain'], eval_ctx ) or []
+    if 'type' not in action:
+       action['type'] = 'ir.actions.act_window_close'
 
     if action['type'] == 'ir.actions.act_window':
         return fix_view_modes(action)
@@ -522,10 +604,18 @@ def fix_view_modes(action):
     :param dict action: an action descriptor
     :returns: nothing, the action is modified in place
     """
-    if 'views' not in action:
+    if not action.get('views'):
         generate_views(action)
 
-    if action.pop('view_type') != 'form':
+    id_form = None
+    for index, (id, mode) in enumerate(action['views']):
+        if mode == 'form':
+            id_form = id
+            break
+    if id_form is not None:
+        action['views'].insert(index + 1, (id_form, 'page'))
+
+    if action.pop('view_type', 'form') != 'form':
         return action
 
     action['views'] = [
@@ -698,14 +788,17 @@ class DataSet(openerpweb.Controller):
         return Model.unlink(ids, req.session.eval_context(req.context))
 
     def call_common(self, req, model, method, args, domain_id=None, context_id=None):
-        domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id  else []
-        context = args[context_id] if context_id and len(args) - 1 >= context_id  else {}
+        has_domain = domain_id is not None and domain_id < len(args)
+        has_context = context_id is not None and context_id < len(args)
+
+        domain = args[domain_id] if has_domain else []
+        context = args[context_id] if has_context else {}
         c, d = eval_context_and_domain(req.session, context, domain)
-        if domain_id and len(args) - 1 >= domain_id:
+        if has_domain:
             args[domain_id] = d
-        if context_id and len(args) - 1 >= context_id:
+        if has_context:
             args[context_id] = c
-        
+
         for i in xrange(len(args)):
             if isinstance(args[i], web.common.nonliterals.BaseContext):
                 args[i] = req.session.eval_context(args[i])
@@ -761,10 +854,12 @@ class View(openerpweb.Controller):
         context = req.session.eval_context(req.context)
         fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
         # todo fme?: check that we should pass the evaluated context here
-        self.process_view(req.session, fvg, context, transform)
+        self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
+        if toolbar and transform:
+            self.process_toolbar(req, fvg['toolbar'])
         return fvg
 
-    def process_view(self, session, fvg, context, transform):
+    def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
         # depending on how it feels, xmlrpclib.ServerProxy can translate
         # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
         # enjoy unicode strings which can not be trivially converted to
@@ -782,16 +877,32 @@ class View(openerpweb.Controller):
             xml = self.transform_view(arch, session, evaluation_context)
         else:
             xml = ElementTree.fromstring(arch)
-        fvg['arch'] = Xml2Json.convert_element(xml)
+        fvg['arch'] = web.common.xml2json.Xml2Json.convert_element(xml, preserve_whitespaces)
 
         for field in fvg['fields'].itervalues():
             if field.get('views'):
                 for view in field["views"].itervalues():
                     self.process_view(session, view, None, transform)
             if field.get('domain'):
-                field["domain"] = self.parse_domain(field["domain"], session)
+                field["domain"] = parse_domain(field["domain"], session)
             if field.get('context'):
-                field["context"] = self.parse_context(field["context"], session)
+                field["context"] = parse_context(field["context"], session)
+
+    def process_toolbar(self, req, toolbar):
+        """
+        The toolbar is a mapping of section_key: [action_descriptor]
+
+        We need to clean all those actions in order to ensure correct
+        round-tripping
+        """
+        for actions in toolbar.itervalues():
+            for action in actions:
+                if 'context' in action:
+                    action['context'] = parse_context(
+                        action['context'], req.session)
+                if 'domain' in action:
+                    action['domain'] = parse_domain(
+                        action['domain'], req.session)
 
     @openerpweb.jsonrequest
     def add_custom(self, req, view_id, arch):
@@ -829,39 +940,6 @@ class View(openerpweb.Controller):
                 self.parse_domains_and_contexts(elem, session)
         return root
 
-    def parse_domain(self, domain, session):
-        """ Parses an arbitrary string containing a domain, transforms it
-        to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
-
-        :param domain: the domain to parse, if the domain is not a string it
-                       is assumed to be a literal domain and is returned as-is
-        :param session: Current OpenERP session
-        :type session: openerpweb.openerpweb.OpenERPSession
-        """
-        if not isinstance(domain, (str, unicode)):
-            return domain
-        try:
-            return openerpweb.ast.literal_eval(domain)
-        except ValueError:
-            # not a literal
-            return openerpweb.nonliterals.Domain(session, domain)
-
-    def parse_context(self, context, session):
-        """ Parses an arbitrary string containing a context, transforms it
-        to either a literal context or a :class:`openerpweb.nonliterals.Context`
-
-        :param context: the context to parse, if the context is not a string it
-               is assumed to be a literal domain and is returned as-is
-        :param session: Current OpenERP session
-        :type session: openerpweb.openerpweb.OpenERPSession
-        """
-        if not isinstance(context, (str, unicode)):
-            return context
-        try:
-            return openerpweb.ast.literal_eval(context)
-        except ValueError:
-            return openerpweb.nonliterals.Context(session, context)
-
     def parse_domains_and_contexts(self, elem, session):
         """ Converts domains and contexts from the view into Python objects,
         either literals if they can be parsed by literal_eval or a special
@@ -876,16 +954,51 @@ class View(openerpweb.Controller):
         for el in ['domain', 'filter_domain']:
             domain = elem.get(el, '').strip()
             if domain:
-                elem.set(el, self.parse_domain(domain, session))
+                elem.set(el, parse_domain(domain, session))
+                elem.set(el + '_string', domain)
         for el in ['context', 'default_get']:
             context_string = elem.get(el, '').strip()
             if context_string:
-                elem.set(el, self.parse_context(context_string, session))
+                elem.set(el, parse_context(context_string, session))
+                elem.set(el + '_string', context_string)
 
     @openerpweb.jsonrequest
     def load(self, req, model, view_id, view_type, toolbar=False):
         return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
 
+def parse_domain(domain, session):
+    """ Parses an arbitrary string containing a domain, transforms it
+    to either a literal domain or a :class:`web.common.nonliterals.Domain`
+
+    :param domain: the domain to parse, if the domain is not a string it
+                   is assumed to be a literal domain and is returned as-is
+    :param session: Current OpenERP session
+    :type session: openerpweb.openerpweb.OpenERPSession
+    """
+    if not isinstance(domain, (str, unicode)):
+        return domain
+    try:
+        return ast.literal_eval(domain)
+    except ValueError:
+        # not a literal
+        return web.common.nonliterals.Domain(session, domain)
+
+def parse_context(context, session):
+    """ Parses an arbitrary string containing a context, transforms it
+    to either a literal context or a :class:`web.common.nonliterals.Context`
+
+    :param context: the context to parse, if the context is not a string it
+           is assumed to be a literal domain and is returned as-is
+    :param session: Current OpenERP session
+    :type session: openerpweb.openerpweb.OpenERPSession
+    """
+    if not isinstance(context, (str, unicode)):
+        return context
+    try:
+        return ast.literal_eval(context)
+    except ValueError:
+        return web.common.nonliterals.Context(session, context)
+
 class ListView(View):
     _cp_path = "/web/listview"
 
@@ -931,27 +1044,27 @@ class SearchView(View):
         for field in fields.values():
             # shouldn't convert the views too?
             if field.get('domain'):
-                field["domain"] = self.parse_domain(field["domain"], req.session)
+                field["domain"] = parse_domain(field["domain"], req.session)
             if field.get('context'):
-                field["context"] = self.parse_domain(field["context"], req.session)
+                field["context"] = parse_context(field["context"], req.session)
         return {'fields': fields}
-    
+
     @openerpweb.jsonrequest
     def get_filters(self, req, model):
         Model = req.session.model("ir.filters")
         filters = Model.get_filters(model)
         for filter in filters:
-            filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
-            filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
+            filter["context"] = req.session.eval_context(parse_context(filter["context"], req.session))
+            filter["domain"] = req.session.eval_domain(parse_domain(filter["domain"], req.session))
         return filters
-    
+
     @openerpweb.jsonrequest
     def save_filter(self, req, model, name, context_to_save, domain):
         Model = req.session.model("ir.filters")
-        ctx = openerpweb.nonliterals.CompoundContext(context_to_save)
+        ctx = web.common.nonliterals.CompoundContext(context_to_save)
         ctx.session = req.session
         ctx = ctx.evaluate()
-        domain = openerpweb.nonliterals.CompoundDomain(domain)
+        domain = web.common.nonliterals.CompoundDomain(domain)
         domain.session = req.session
         domain = domain.evaluate()
         uid = req.session._uid
@@ -964,6 +1077,44 @@ class SearchView(View):
                                              }, context)
         return to_return
 
+    @openerpweb.jsonrequest
+    def add_to_dashboard(self, req, menu_id, action_id, context_to_save, domain, view_mode, name=''):
+        ctx = web.common.nonliterals.CompoundContext(context_to_save)
+        ctx.session = req.session
+        ctx = ctx.evaluate()
+        domain = web.common.nonliterals.CompoundDomain(domain)
+        domain.session = req.session
+        domain = domain.evaluate()
+
+        dashboard_action = load_actions_from_ir_values(req, 'action', 'tree_but_open',
+                                             [('ir.ui.menu', menu_id)], False)
+        if dashboard_action:
+            action = dashboard_action[0][2]
+            if action['res_model'] == 'board.board' and action['views'][0][1] == 'form':
+                # Maybe should check the content instead of model board.board ?
+                view_id = action['views'][0][0]
+                board = req.session.model(action['res_model']).fields_view_get(view_id, 'form')
+                if board and 'arch' in board:
+                    xml = ElementTree.fromstring(board['arch'])
+                    column = xml.find('./board/column')
+                    if column:
+                        new_action = ElementTree.Element('action', {
+                                'name' : str(action_id),
+                                'string' : name,
+                                'view_mode' : view_mode,
+                                'context' : str(ctx),
+                                'domain' : str(domain)
+                            })
+                        column.insert(0, new_action)
+                        arch = ElementTree.tostring(xml, 'utf-8')
+                        return req.session.model('ir.ui.view.custom').create({
+                                'user_id': req.session._uid,
+                                'ref_id': view_id,
+                                'arch': arch
+                            }, req.session.eval_context(req.context))
+
+        return False
+
 class Binary(openerpweb.Controller):
     _cp_path = "/web/binary"
 
@@ -974,23 +1125,27 @@ class Binary(openerpweb.Controller):
 
         try:
             if not id:
-                res = Model.default_get([field], context).get(field, '')
+                res = Model.default_get([field], context).get(field)
             else:
-                res = Model.read([int(id)], [field], context)[0].get(field, '')
+                res = Model.read([int(id)], [field], context)[0].get(field)
             image_data = base64.b64decode(res)
         except (TypeError, xmlrpclib.Fault):
             image_data = self.placeholder(req)
         return req.make_response(image_data, [
             ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
     def placeholder(self, req):
-        return open(os.path.join(req.addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
+        addons_path = openerpweb.addons_manifest['web']['addons_path']
+        return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
 
     @openerpweb.httprequest
     def saveas(self, req, model, id, field, fieldname, **kw):
         Model = req.session.model(model)
         context = req.session.eval_context(req.context)
-        res = Model.read([int(id)], [field, fieldname], context)[0]
-        filecontent = res.get(field, '')
+        if id:
+            res = Model.read([int(id)], [field, fieldname], context)[0]
+        else:
+            res = Model.default_get([field, fieldname], context)
+        filecontent = base64.b64decode(res.get(field, ''))
         if not filecontent:
             return req.not_found()
         else:
@@ -1054,7 +1209,7 @@ class Action(openerpweb.Controller):
     _cp_path = "/web/action"
 
     @openerpweb.jsonrequest
-    def load(self, req, action_id):
+    def load(self, req, action_id, do_not_eval=False):
         Actions = req.session.model('ir.actions.actions')
         value = False
         context = req.session.eval_context(req.context)
@@ -1066,7 +1221,7 @@ class Action(openerpweb.Controller):
             ctx.update(context)
             action = req.session.model(action_type[0]['type']).read([action_id], False, ctx)
             if action:
-                value = clean_action(req, action[0])
+                value = clean_action(req, action[0], do_not_eval)
         return {'result': value}
 
     @openerpweb.jsonrequest
@@ -1098,19 +1253,26 @@ class Export(View):
 
     @openerpweb.jsonrequest
     def get_fields(self, req, model, prefix='', parent_name= '',
-                   import_compat=True, parent_field_type=None):
+                   import_compat=True, parent_field_type=None,
+                   exclude=None):
 
         if import_compat and parent_field_type == "many2one":
             fields = {}
         else:
             fields = self.fields_get(req, model)
-        fields['.id'] = fields.pop('id') if 'id' in fields else {'string': 'ID'}
+
+        if import_compat:
+            fields.pop('id', None)
+        else:
+            fields['.id'] = fields.pop('id', {'string': 'ID'})
 
         fields_sequence = sorted(fields.iteritems(),
             key=lambda field: field[1].get('string', ''))
 
         records = []
         for field_name, field in fields_sequence:
+            if import_compat and (exclude and field_name in exclude):
+                continue
             if import_compat and field.get('readonly'):
                 # If none of the field's states unsets readonly, skip the field
                 if all(dict(attrs).get('readonly', True)
@@ -1122,7 +1284,8 @@ class Export(View):
             record = {'id': id, 'string': name,
                       'value': id, 'children': False,
                       'field_type': field.get('type'),
-                      'required': field.get('required')}
+                      'required': field.get('required'),
+                      'relation_field': field.get('relation_field')}
             records.append(record)
 
             if len(name.split('/')) < 3 and 'relation' in field:
@@ -1154,7 +1317,6 @@ class Export(View):
     def fields_info(self, req, model, export_fields):
         info = {}
         fields = self.fields_get(req, model)
-        fields['.id'] = fields.pop('id') if 'id' in fields else {'string': 'ID'}
 
         # To make fields retrieval more efficient, fetch all sub-fields of a
         # given field at the same time. Because the order in the export list is
@@ -1237,7 +1399,7 @@ class Export(View):
 
         context = req.session.eval_context(req.context)
         Model = req.session.model(model)
-        ids = ids or Model.search(domain, context=context)
+        ids = ids or Model.search(domain, 0, False, False, context)
 
         field_names = map(operator.itemgetter('name'), fields)
         import_data = Model.export_data(ids, field_names, context).get('datas',[])
@@ -1315,6 +1477,7 @@ class ExcelExport(Export):
             for cell_index, cell_value in enumerate(row):
                 if isinstance(cell_value, basestring):
                     cell_value = re.sub("\r", " ", cell_value)
+                if cell_value is False: cell_value = None
                 worksheet.write(row_index + 1, cell_index, cell_value, style)
 
         fp = StringIO()
@@ -1342,21 +1505,30 @@ class Reports(View):
 
         report_srv = req.session.proxy("report")
         context = req.session.eval_context(
-            openerpweb.nonliterals.CompoundContext(
+            web.common.nonliterals.CompoundContext(
                 req.context or {}, action[ "context"]))
+
+        report_data = {}
+        report_ids = context["active_ids"]
+        if 'report_type' in action:
+            report_data['report_type'] = action['report_type']
+        if 'datas' in action:
+            if 'ids' in action['datas']:
+                report_ids = action['datas'].pop('ids')
+            report_data.update(action['datas'])
+
         report_id = report_srv.report(
             req.session._db, req.session._uid, req.session._password,
-            action["report_name"], context["active_ids"],
-            {"id": context["active_id"],
-             "model": context["active_model"],
-             "report_type": action["report_type"]},
-            context)
+            action["report_name"], report_ids,
+            report_data, context)
+
         report_struct = None
         while True:
             report_struct = report_srv.report_get(
                 req.session._db, req.session._uid, req.session._password, report_id)
             if report_struct["state"]:
                 break
+
             time.sleep(self.POLLING_DELAY)
 
         report = base64.b64decode(report_struct['result'])
@@ -1366,7 +1538,111 @@ class Reports(View):
             report_struct['format'], 'octet-stream')
         return req.make_response(report,
              headers=[
-                 ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], action['report_type'])),
+                 ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], report_struct['format'])),
                  ('Content-Type', report_mimetype),
                  ('Content-Length', len(report))],
              cookies={'fileToken': int(token)})
+
+class Import(View):
+    _cp_path = "/web/import"
+
+    def fields_get(self, req, model):
+        Model = req.session.model(model)
+        fields = Model.fields_get(False, req.session.eval_context(req.context))
+        return fields
+
+    @openerpweb.httprequest
+    def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
+        try:
+            data = list(csv.reader(
+                csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
+        except csv.Error, e:
+            csvfile.seek(0)
+            return '<script>window.top.%s(%s);</script>' % (
+                jsonp, simplejson.dumps({'error': {
+                    'message': 'Error parsing CSV file: %s' % e,
+                    # decodes each byte to a unicode character, which may or
+                    # may not be printable, but decoding will succeed.
+                    # Otherwise simplejson will try to decode the `str` using
+                    # utf-8, which is very likely to blow up on characters out
+                    # of the ascii range (in range [128, 256))
+                    'preview': csvfile.read(200).decode('iso-8859-1')}}))
+
+        try:
+            return '<script>window.top.%s(%s);</script>' % (
+                jsonp, simplejson.dumps(
+                    {'records': data[:10]}, encoding=csvcode))
+        except UnicodeDecodeError:
+            return '<script>window.top.%s(%s);</script>' % (
+                jsonp, simplejson.dumps({
+                    'message': u"Failed to decode CSV file using encoding %s, "
+                               u"try switching to a different encoding" % csvcode
+                }))
+
+    @openerpweb.httprequest
+    def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
+                    meta):
+        modle_obj = req.session.model(model)
+        skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
+            simplejson.loads(meta))
+
+        error = None
+        if not (csvdel and len(csvdel) == 1):
+            error = u"The CSV delimiter must be a single character"
+
+        if not indices and fields:
+            error = u"You must select at least one field to import"
+
+        if error:
+            return '<script>window.top.%s(%s);</script>' % (
+                jsonp, simplejson.dumps({'error': {'message': error}}))
+
+        # skip ignored records
+        data_record = itertools.islice(
+            csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
+            skip, None)
+
+        # if only one index, itemgetter will return an atom rather than a tuple
+        if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
+        else: mapper = operator.itemgetter(*indices)
+
+        data = None
+        error = None
+        try:
+            # decode each data row
+            data = [
+                [record.decode(csvcode) for record in row]
+                for row in itertools.imap(mapper, data_record)
+                # don't insert completely empty rows (can happen due to fields
+                # filtering in case of e.g. o2m content rows)
+                if any(row)
+            ]
+        except UnicodeDecodeError:
+            error = u"Failed to decode CSV file using encoding %s" % csvcode
+        except csv.Error, e:
+            error = u"Could not process CSV file: %s" % e
+
+        # If the file contains nothing,
+        if not data:
+            error = u"File to import is empty"
+        if error:
+            return '<script>window.top.%s(%s);</script>' % (
+                jsonp, simplejson.dumps({'error': {'message': error}}))
+
+        try:
+            (code, record, message, _nope) = modle_obj.import_data(
+                fields, data, 'init', '', False,
+                req.session.eval_context(req.context))
+        except xmlrpclib.Fault, e:
+            error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
+            return '<script>window.top.%s(%s);</script>' % (
+                jsonp, simplejson.dumps({'error':error}))
+
+        if code != -1:
+            return '<script>window.top.%s(%s);</script>' % (
+                jsonp, simplejson.dumps({'success':True}))
+
+        msg = u"Error during import: %s\n\nTrying to import record %r" % (
+            message, record)
+        return '<script>window.top.%s(%s);</script>' % (
+            jsonp, simplejson.dumps({'error': {'message':msg}}))