merge 3.19.3 into 3.20 branch
authorJulien Cristau <julien.cristau@logilab.fr>
Fri, 18 Jul 2014 17:35:25 +0200
changeset 9897 fa44db7da2dc
parent 9892 928732ec00dd (diff)
parent 9896 4900a937838b (current diff)
child 9898 70056633085c
merge 3.19.3 into 3.20 branch
migration.py
server/sources/datafeed.py
server/test/unittest_datafeed.py
web/application.py
web/data/cubicweb.css
web/data/cubicweb.edition.js
web/data/cubicweb.old.css
web/request.py
web/test/unittest_views_editforms.py
web/test/unittest_web.py
web/views/ajaxedit.py
web/views/autoform.py
--- a/__init__.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/__init__.py	Fri Jul 18 17:35:25 2014 +0200
@@ -44,10 +44,8 @@
 from logilab.common.logging_ext import set_log_methods
 from yams.constraints import BASE_CONVERTERS
 
-if os.environ.get('APYCOT_ROOT'):
-    logging.basicConfig(level=logging.CRITICAL)
-else:
-    logging.basicConfig()
+# pre python 2.7.2 safety
+logging.basicConfig()
 
 from cubicweb.__pkginfo__ import version as __version__
 
--- a/cwconfig.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/cwconfig.py	Fri Jul 18 17:35:25 2014 +0200
@@ -827,13 +827,6 @@
     else:
         _INSTANCES_DIR = join(_INSTALL_PREFIX, 'etc', 'cubicweb.d')
 
-    if os.environ.get('APYCOT_ROOT'):
-        _cubes_init = join(CubicWebNoAppConfiguration.CUBES_DIR, '__init__.py')
-        if not exists(_cubes_init):
-            file(join(_cubes_init), 'w').close()
-        if not exists(_INSTANCES_DIR):
-            os.makedirs(_INSTANCES_DIR)
-
     # set to true during repair (shell, migration) to allow some things which
     # wouldn't be possible otherwise
     repairing = False
--- a/cwctl.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/cwctl.py	Fri Jul 18 17:35:25 2014 +0200
@@ -835,6 +835,8 @@
         config = cwcfg.config_for(appid)
         # should not raise error if db versions don't match fs versions
         config.repairing = True
+        # no need to load all appobjects and schema
+        config.quick_start = True
         if hasattr(config, 'set_sources_mode'):
             config.set_sources_mode(('migration',))
         repo = config.migration_handler().repo_connect()
--- a/entities/wfobjs.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/entities/wfobjs.py	Fri Jul 18 17:35:25 2014 +0200
@@ -27,7 +27,6 @@
 
 from logilab.common.decorators import cached, clear_cache
 from logilab.common.deprecation import deprecated
-from logilab.common.compat import any
 
 from cubicweb.entities import AnyEntity, fetch_config
 from cubicweb.view import EntityAdapter
--- a/hooks/email.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/hooks/email.py	Fri Jul 18 17:35:25 2014 +0200
@@ -21,8 +21,6 @@
 
 from cubicweb.server import hook
 
-from logilab.common.compat import any
-
 
 class SetUseEmailRelationOp(hook.Operation):
     """delay this operation to commit to avoid conflict with a late rql query
--- a/migration.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/migration.py	Fri Jul 18 17:35:25 2014 +0200
@@ -247,12 +247,13 @@
         local_ctx = self._create_context()
         try:
             import readline
-            from rlcompleter import Completer
+            from cubicweb.toolsutils import CWShellCompleter
         except ImportError:
             # readline not available
             pass
         else:
-            readline.set_completer(Completer(local_ctx).complete)
+            rql_completer = CWShellCompleter(local_ctx)
+            readline.set_completer(rql_completer.complete)
             readline.parse_and_bind('tab: complete')
             home_key = 'HOME'
             if sys.platform == 'win32':
--- a/predicates.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/predicates.py	Fri Jul 18 17:35:25 2014 +0200
@@ -188,7 +188,6 @@
 from warnings import warn
 from operator import eq
 
-from logilab.common.compat import all, any
 from logilab.common.interface import implements as implements_iface
 from logilab.common.registry import Predicate, objectify_predicate, yes
 
--- a/req.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/req.py	Fri Jul 18 17:35:25 2014 +0200
@@ -485,12 +485,16 @@
             raise ValueError(self._('can\'t parse %(value)r (expected %(format)s)')
                              % {'value': value, 'format': format})
 
+    def _base_url(self, secure=None):
+        if secure:
+            return self.vreg.config.get('https-url') or self.vreg.config['base-url']
+        return self.vreg.config['base-url']
+
     def base_url(self, secure=None):
         """return the root url of the instance
         """
-        if secure:
-            return self.vreg.config.get('https-url') or self.vreg.config['base-url']
-        return self.vreg.config['base-url']
+        url = self._base_url(secure=secure)
+        return url if url is None else url.rstrip('/') + '/'
 
     # abstract methods to override according to the web front-end #############
 
--- a/schema.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/schema.py	Fri Jul 18 17:35:25 2014 +0200
@@ -31,7 +31,6 @@
 from logilab.common.deprecation import deprecated, class_moved, moved
 from logilab.common.textutils import splitstrip
 from logilab.common.graph import get_cycles
-from logilab.common.compat import any
 
 from yams import BadSchemaDefinition, buildobjs as ybo
 from yams.schema import Schema, ERSchema, EntitySchema, RelationSchema, \
--- a/server/querier.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/server/querier.py	Fri Jul 18 17:35:25 2014 +0200
@@ -22,7 +22,6 @@
 
 from itertools import repeat
 
-from logilab.common.compat import any
 from rql import RQLSyntaxError, CoercionError
 from rql.stmts import Union
 from rql.nodes import ETYPE_PYOBJ_MAP, etype_from_pyobj, Relation, Exists, Not
--- a/server/rqlannotation.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/server/rqlannotation.py	Fri Jul 18 17:35:25 2014 +0200
@@ -21,8 +21,6 @@
 
 __docformat__ = "restructuredtext en"
 
-from logilab.common.compat import any
-
 from rql import BadRQLQuery
 from rql.nodes import Relation, VariableRef, Constant, Variable, Or, Exists
 from rql.utils import common_parent
--- a/server/schemaserial.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/server/schemaserial.py	Fri Jul 18 17:35:25 2014 +0200
@@ -309,19 +309,14 @@
     """synchronize schema and permissions in the database according to
     current schema
     """
-    quiet = os.environ.get('APYCOT_ROOT')
-    if not quiet:
-        _title = '-> storing the schema in the database '
-        print _title,
+    _title = '-> storing the schema in the database '
+    print _title,
     execute = cnx.execute
     eschemas = schema.entities()
-    if not quiet:
-        pb_size = (len(eschemas + schema.relations())
-                   + len(CONSTRAINTS)
-                   + len([x for x in eschemas if x.specializes()]))
-        pb = ProgressBar(pb_size, title=_title)
-    else:
-        pb = None
+    pb_size = (len(eschemas + schema.relations())
+               + len(CONSTRAINTS)
+               + len([x for x in eschemas if x.specializes()]))
+    pb = ProgressBar(pb_size, title=_title)
     groupmap = group_mapping(cnx, interactive=False)
     # serialize all entity types, assuring CWEType is serialized first for proper
     # is / is_instance_of insertion
@@ -366,8 +361,7 @@
         execute(rql, kwargs, build_descr=False)
         if pb is not None:
             pb.update()
-    if not quiet:
-        print
+    print
 
 
 # high level serialization functions
--- a/server/sources/__init__.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/server/sources/__init__.py	Fri Jul 18 17:35:25 2014 +0200
@@ -105,7 +105,7 @@
         self.support_relations['identity'] = False
         self.eid = eid
         self.public_config = source_config.copy()
-        self.public_config.setdefault('use-cwuri-as-url', self.use_cwuri_as_url)
+        self.public_config['use-cwuri-as-url'] = self.use_cwuri_as_url
         self.remove_sensitive_information(self.public_config)
         self.uri = source_config.pop('uri')
         set_log_methods(self, getLogger('cubicweb.sources.'+self.uri))
--- a/server/sources/datafeed.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/server/sources/datafeed.py	Fri Jul 18 17:35:25 2014 +0200
@@ -83,6 +83,13 @@
           'help': ('Timeout of HTTP GET requests, when synchronizing a source.'),
           'group': 'datafeed-source', 'level': 2,
           }),
+        ('use-cwuri-as-url',
+         {'type': 'yn',
+          'default': None, # explicitly unset
+          'help': ('Use cwuri (i.e. external URL) for link to the entity '
+                   'instead of its local URL.'),
+          'group': 'datafeed-source', 'level': 1,
+          }),
         )
 
     def check_config(self, source_entity):
@@ -107,6 +114,12 @@
         self.synchro_interval = timedelta(seconds=typed_config['synchronization-interval'])
         self.max_lock_lifetime = timedelta(seconds=typed_config['max-lock-lifetime'])
         self.http_timeout = typed_config['http-timeout']
+        # if typed_config['use-cwuri-as-url'] is set, we have to update
+        # use_cwuri_as_url attribute and public configuration dictionary
+        # accordingly
+        if typed_config['use-cwuri-as-url'] is not None:
+            self.use_cwuri_as_url = typed_config['use-cwuri-as-url']
+            self.public_config['use-cwuri-as-url'] = self.use_cwuri_as_url
 
     def init(self, activated, source_entity):
         super(DataFeedSource, self).init(activated, source_entity)
@@ -285,12 +298,39 @@
         self.stats = {'created': set(), 'updated': set(), 'checked': set()}
 
     def normalize_url(self, url):
-        from cubicweb.sobjects import URL_MAPPING # available after registration
+        """Normalize an url by looking if there is a replacement for it in
+        `cubicweb.sobjects.URL_MAPPING`.
+
+        This dictionary allow to redirect from one host to another, which may be
+        useful for example in case of test instance using production data, while
+        you don't want to load the external source nor to hack your `/etc/hosts`
+        file.
+        """
+        # local import mandatory, it's available after registration
+        from cubicweb.sobjects import URL_MAPPING
         for mappedurl in URL_MAPPING:
             if url.startswith(mappedurl):
                 return url.replace(mappedurl, URL_MAPPING[mappedurl], 1)
         return url
 
+    def retrieve_url(self, url, data=None, headers=None):
+        """Return stream linked by the given url:
+        * HTTP urls will be normalized (see :meth:`normalize_url`)
+        * handle file:// URL
+        * other will be considered as plain content, useful for testing purpose
+        """
+        if url.startswith('http'):
+            url = self.normalize_url(url)
+            if data:
+                self.source.info('POST %s %s', url, data)
+            else:
+                self.source.info('GET %s', url)
+            req = urllib2.Request(url, data, headers)
+            return _OPENER.open(req, timeout=self.source.http_timeout)
+        if url.startswith('file://'):
+            return URLLibResponseAdapter(open(url[7:]), url)
+        return URLLibResponseAdapter(StringIO.StringIO(url), url)
+
     def add_schema_config(self, schemacfg, checkonly=False):
         """added CWSourceSchemaConfig, modify mapping accordingly"""
         msg = schemacfg._cw._("this parser doesn't use a mapping")
@@ -427,14 +467,7 @@
         return error
 
     def parse(self, url):
-        if url.startswith('http'):
-            url = self.normalize_url(url)
-            self.source.info('GET %s', url)
-            stream = _OPENER.open(url, timeout=self.source.http_timeout)
-        elif url.startswith('file://'):
-            stream = open(url[7:])
-        else:
-            stream = StringIO.StringIO(url)
+        stream = self.retrieve_url(url)
         return self.parse_etree(etree.parse(stream).getroot())
 
     def parse_etree(self, document):
@@ -455,6 +488,27 @@
             return exists(extid[7:])
         return False
 
+
+class URLLibResponseAdapter(object):
+    """Thin wrapper to be used to fake a value returned by urllib2.urlopen"""
+    def __init__(self, stream, url, code=200):
+        self._stream = stream
+        self._url = url
+        self.code = code
+
+    def read(self, *args):
+        return self._stream.read(*args)
+
+    def geturl(self):
+        return self._url
+
+    def getcode(self):
+        return self.code
+
+    def info(self):
+        from mimetools import Message
+        return Message(StringIO.StringIO())
+
 # use a cookie enabled opener to use session cookie if any
 _OPENER = urllib2.build_opener()
 try:
--- a/server/sources/native.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/server/sources/native.py	Fri Jul 18 17:35:25 2014 +0200
@@ -42,7 +42,6 @@
 import logging
 import sys
 
-from logilab.common.compat import any
 from logilab.common.decorators import cached, clear_cache
 from logilab.common.configuration import Method
 from logilab.common.shellutils import getlogin
@@ -323,10 +322,16 @@
                   'want trusted authentication for the database connection',
           'group': 'native-source', 'level': 2,
           }),
+        ('db-statement-timeout',
+         {'type': 'int',
+          'default': 0,
+          'help': 'sql statement timeout, in milliseconds (postgres only)',
+          'group': 'native-source', 'level': 2,
+          }),
     )
 
     def __init__(self, repo, source_config, *args, **kwargs):
-        SQLAdapterMixIn.__init__(self, source_config)
+        SQLAdapterMixIn.__init__(self, source_config, repairing=repo.config.repairing)
         self.authentifiers = [LoginPasswordAuthentifier(self)]
         if repo.config['allow-email-login']:
             self.authentifiers.insert(0, EmailPasswordAuthentifier(self))
--- a/server/sqlutils.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/server/sqlutils.py	Fri Jul 18 17:35:25 2014 +0200
@@ -47,7 +47,7 @@
     return subprocess.call(cmd)
 
 
-def sqlexec(sqlstmts, cursor_or_execute, withpb=not os.environ.get('APYCOT_ROOT'),
+def sqlexec(sqlstmts, cursor_or_execute, withpb=True,
             pbtitle='', delimiter=';', cnx=None):
     """execute sql statements ignoring DROP/ CREATE GROUP or USER statements
     error.
@@ -299,7 +299,7 @@
     """
     cnx_wrap = ConnectionWrapper
 
-    def __init__(self, source_config):
+    def __init__(self, source_config, repairing=False):
         try:
             self.dbdriver = source_config['db-driver'].lower()
             dbname = source_config['db-name']
@@ -328,6 +328,14 @@
         if self.dbdriver == 'sqlite':
             self.cnx_wrap = SqliteConnectionWrapper
             self.dbhelper.dbname = abspath(self.dbhelper.dbname)
+        if not repairing:
+            statement_timeout = int(source_config.get('db-statement-timeout', 0))
+            if statement_timeout > 0:
+                def set_postgres_timeout(cnx):
+                    cnx.cursor().execute('SET statement_timeout to %d' % statement_timeout)
+                    cnx.commit()
+                postgres_hooks = SQL_CONNECT_HOOKS['postgres']
+                postgres_hooks.append(set_postgres_timeout)
 
     def wrapped_connection(self):
         """open and return a connection to the database, wrapped into a class
--- a/server/test/unittest_datafeed.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/server/test/unittest_datafeed.py	Fri Jul 18 17:35:25 2014 +0200
@@ -16,7 +16,9 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 
+import mimetools
 from datetime import timedelta
+from contextlib import contextmanager
 
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.server.sources import datafeed
@@ -25,19 +27,14 @@
 class DataFeedTC(CubicWebTC):
     def setup_database(self):
         with self.admin_access.repo_cnx() as cnx:
-            cnx.create_entity('CWSource', name=u'myfeed', type=u'datafeed',
-                              parser=u'testparser', url=u'ignored',
-                              config=u'synchronization-interval=1min')
-            cnx.commit()
+            with self.base_parser(cnx):
+                cnx.create_entity('CWSource', name=u'myfeed', type=u'datafeed',
+                                  parser=u'testparser', url=u'ignored',
+                                  config=u'synchronization-interval=1min')
+                cnx.commit()
 
-    def test(self):
-        self.assertIn('myfeed', self.repo.sources_by_uri)
-        dfsource = self.repo.sources_by_uri['myfeed']
-        self.assertEqual(dfsource.latest_retrieval, None)
-        self.assertEqual(dfsource.synchro_interval, timedelta(seconds=60))
-        self.assertFalse(dfsource.fresh())
-
-
+    @contextmanager
+    def base_parser(self, session):
         class AParser(datafeed.DataFeedParser):
             __regid__ = 'testparser'
             def process(self, url, raise_on_error=False):
@@ -50,7 +47,24 @@
                 entity.cw_edited.update(sourceparams['item'])
 
         with self.temporary_appobjects(AParser):
-            with self.repo.internal_cnx() as cnx:
+            if 'myfeed' in self.repo.sources_by_uri:
+                yield self.repo.sources_by_uri['myfeed']._get_parser(session)
+            else:
+                yield
+
+    def test(self):
+        self.assertIn('myfeed', self.repo.sources_by_uri)
+        dfsource = self.repo.sources_by_uri['myfeed']
+        self.assertNotIn('use_cwuri_as_url', dfsource.__dict__)
+        self.assertEqual({'type': u'datafeed', 'uri': u'myfeed', 'use-cwuri-as-url': True},
+                         dfsource.public_config)
+        self.assertEqual(dfsource.use_cwuri_as_url, True)
+        self.assertEqual(dfsource.latest_retrieval, None)
+        self.assertEqual(dfsource.synchro_interval, timedelta(seconds=60))
+        self.assertFalse(dfsource.fresh())
+
+        with self.repo.internal_cnx() as cnx:
+            with self.base_parser(cnx):
                 stats = dfsource.pull_data(cnx, force=True)
                 cnx.commit()
                 # test import stats
@@ -119,6 +133,28 @@
             self.assertFalse(cnx.execute('Card X WHERE X title "cubicweb.org"'))
             self.assertFalse(cnx.execute('Any X WHERE X has_text "cubicweb.org"'))
 
+    def test_parser_retrieve_url_local(self):
+        with self.admin_access.repo_cnx() as cnx:
+            with self.base_parser(cnx) as parser:
+                value = parser.retrieve_url('a string')
+                self.assertEqual(200, value.getcode())
+                self.assertEqual('a string', value.geturl())
+                self.assertIsInstance(value.info(), mimetools.Message)
+
+
+class DataFeedConfigTC(CubicWebTC):
+
+    def test_use_cwuri_as_url_override(self):
+        with self.admin_access.client_cnx() as cnx:
+            cnx.create_entity('CWSource', name=u'myfeed', type=u'datafeed',
+                              parser=u'testparser', url=u'ignored',
+                              config=u'use-cwuri-as-url=no')
+            cnx.commit()
+        dfsource = self.repo.sources_by_uri['myfeed']
+        self.assertEqual(dfsource.use_cwuri_as_url, False)
+        self.assertEqual({'type': u'datafeed', 'uri': u'myfeed', 'use-cwuri-as-url': False},
+                         dfsource.public_config)
+
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
     unittest_main()
--- a/sobjects/notification.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/sobjects/notification.py	Fri Jul 18 17:35:25 2014 +0200
@@ -80,15 +80,8 @@
 
     # this is usually the method to call
     def render_and_send(self, **kwargs):
-        """generate and send an email message for this view"""
-        delayed = kwargs.pop('delay_to_commit', None)
-        for recipients, msg in self.render_emails(**kwargs):
-            if delayed is None:
-                self.send(recipients, msg)
-            elif delayed:
-                self.send_on_commit(recipients, msg)
-            else:
-                self.send_now(recipients, msg)
+        """generate and send email messages for this view"""
+        self._cw.vreg.config.sendmails(self.render_emails(**kwargs))
 
     def cell_call(self, row, col=0, **kwargs):
         self.w(self._cw._(self.content) % self.context(**kwargs))
@@ -146,16 +139,11 @@
                         continue
                     msg = format_mail(self.user_data, [emailaddr], content, subject,
                                       config=self._cw.vreg.config, msgid=msgid, references=refs)
-                    yield [emailaddr], msg
+                    yield msg, [emailaddr]
                 finally:
-                    # ensure we have a cnxset since commit will fail if there is
-                    # some operation but no cnxset. This may occurs in this very
-                    # specific case (eg SendMailOp)
-                    with cnx.ensure_cnx_set:
-                        cnx.commit()
                     self._cw = req
 
-    # recipients / email sending ###############################################
+    # recipients handling ######################################################
 
     def recipients(self):
         """return a list of either 2-uple (email, language) or user entity to
@@ -166,13 +154,6 @@
             row=self.cw_row or 0, col=self.cw_col or 0)
         return finder.recipients()
 
-    def send_now(self, recipients, msg):
-        self._cw.vreg.config.sendmails([(msg, recipients)])
-
-    def send_on_commit(self, recipients, msg):
-        SendMailOp(self._cw, recipients=recipients, msg=msg)
-    send = send_on_commit
-
     # email generation helpers #################################################
 
     def construct_message_id(self, eid):
--- a/test/unittest_dbapi.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/test/unittest_dbapi.py	Fri Jul 18 17:35:25 2014 +0200
@@ -78,7 +78,7 @@
         with tempattr(cnx.vreg, 'config', config):
             cnx.use_web_compatible_requests('http://perdu.com')
             req = cnx.request()
-            self.assertEqual(req.base_url(), 'http://perdu.com')
+            self.assertEqual(req.base_url(), 'http://perdu.com/')
             self.assertEqual(req.from_controller(), 'view')
             self.assertEqual(req.relative_path(), '')
             req.ajax_replace_url('domid') # don't crash
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/unittest_toolsutils.py	Fri Jul 18 17:35:25 2014 +0200
@@ -0,0 +1,57 @@
+# copyright 2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+
+
+from logilab.common.testlib import TestCase, unittest_main
+
+from cubicweb.toolsutils import RQLExecuteMatcher
+
+
+class RQLExecuteMatcherTests(TestCase):
+    def matched_query(self, text):
+        match = RQLExecuteMatcher.match(text)
+        if match is None:
+            return None
+        return match['rql_query']
+
+    def test_unknown_function_dont_match(self):
+        self.assertIsNone(self.matched_query('foo'))
+        self.assertIsNone(self.matched_query('rql('))
+        self.assertIsNone(self.matched_query('hell("")'))
+        self.assertIsNone(self.matched_query('eval("rql(\'bla\''))
+
+    def test_rql_other_parameters_dont_match(self):
+        self.assertIsNone(self.matched_query('rql("Any X WHERE X eid %(x)s")'))
+        self.assertIsNone(self.matched_query('rql("Any X WHERE X eid %(x)s", {'))
+        self.assertIsNone(self.matched_query('session.execute("Any X WHERE X eid %(x)s")'))
+        self.assertIsNone(self.matched_query('session.execute("Any X WHERE X eid %(x)s", {'))
+
+    def test_rql_function_match(self):
+        for func_expr in ('rql', 'session.execute'):
+            query = self.matched_query('%s("Any X WHERE X is ' % func_expr)
+            self.assertEqual(query, 'Any X WHERE X is ')
+
+    def test_offseted_rql_function_match(self):
+        """check indentation is allowed"""
+        for func_expr in ('  rql', '  session.execute'):
+            query = self.matched_query('%s("Any X WHERE X is ' % func_expr)
+            self.assertEqual(query, 'Any X WHERE X is ')
+
+
+if __name__ == '__main__':
+    unittest_main()
--- a/toolsutils.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/toolsutils.py	Fri Jul 18 17:35:25 2014 +0200
@@ -25,7 +25,12 @@
 import subprocess
 from os import listdir, makedirs, environ, chmod, walk, remove
 from os.path import exists, join, abspath, normpath
-
+import re
+from rlcompleter import Completer
+try:
+    import readline
+except ImportError: # readline not available, no completion
+    pass
 try:
     from os import symlink
 except ImportError:
@@ -33,7 +38,6 @@
         raise NotImplementedError
 
 from logilab.common.clcommands import Command as BaseCommand
-from logilab.common.compat import any
 from logilab.common.shellutils import ASK
 
 from cubicweb import warning # pylint: disable=E0611
@@ -264,3 +268,155 @@
         password = getpass('password: ')
     return connect(login=user, password=password, host=optconfig.host, database=appid)
 
+
+## cwshell helpers #############################################################
+
+class AbstractMatcher(object):
+    """Abstract class for CWShellCompleter's matchers.
+
+    A matcher should implement a ``possible_matches`` method. This
+    method has to return the list of possible completions for user's input.
+    Because of the python / readline interaction, each completion should
+    be a superset of the user's input.
+
+    NOTE: readline tokenizes user's input and only passes last token to
+    completers.
+    """
+
+    def possible_matches(self, text):
+        """return possible completions for user's input.
+
+        Parameters:
+            text: the user's input
+
+        Return:
+            a list of completions. Each completion includes the original input.
+        """
+        raise NotImplementedError()
+
+
+class RQLExecuteMatcher(AbstractMatcher):
+    """Custom matcher for rql queries.
+
+    If user's input starts with ``rql(`` or ``session.execute(`` and
+    the corresponding rql query is incomplete, suggest some valid completions.
+    """
+    query_match_rgx = re.compile(
+        r'(?P<func_prefix>\s*(?:rql)'  # match rql, possibly indented
+        r'|'                           # or
+        r'\s*(?:\w+\.execute))'        # match .execute, possibly indented
+        # end of <func_prefix>
+        r'\('                          # followed by a parenthesis
+        r'(?P<quote_delim>["\'])'      # a quote or double quote
+        r'(?P<parameters>.*)')         # and some content
+
+    def __init__(self, local_ctx, req):
+        self.local_ctx = local_ctx
+        self.req = req
+        self.schema = req.vreg.schema
+        self.rsb = req.vreg['components'].select('rql.suggestions', req)
+
+    @staticmethod
+    def match(text):
+        """check if ``text`` looks like a call to ``rql`` or ``session.execute``
+
+        Parameters:
+            text: the user's input
+
+        Returns:
+            None if it doesn't match, the query structure otherwise.
+        """
+        query_match = RQLExecuteMatcher.query_match_rgx.match(text)
+        if query_match is None:
+            return None
+        parameters_text = query_match.group('parameters')
+        quote_delim = query_match.group('quote_delim')
+        # first parameter is fully specified, no completion needed
+        if re.match(r"(.*?)%s" % quote_delim, parameters_text) is not None:
+            return None
+        func_prefix = query_match.group('func_prefix')
+        return {
+            # user's input
+            'text': text,
+            # rql( or session.execute(
+            'func_prefix': func_prefix,
+            # offset of rql query
+            'rql_offset': len(func_prefix) + 2,
+            # incomplete rql query
+            'rql_query': parameters_text,
+            }
+
+    def possible_matches(self, text):
+        """call ``rql.suggestions`` component to complete user's input.
+        """
+        # readline will only send last token, but we need the entire user's input
+        user_input = readline.get_line_buffer()
+        query_struct = self.match(user_input)
+        if query_struct is None:
+            return []
+        else:
+            # we must only send completions of the last token => compute where it
+            # starts relatively to the rql query itself.
+            completion_offset = readline.get_begidx() - query_struct['rql_offset']
+            rql_query = query_struct['rql_query']
+            return [suggestion[completion_offset:]
+                    for suggestion in self.rsb.build_suggestions(rql_query)]
+
+
+class DefaultMatcher(AbstractMatcher):
+    """Default matcher: delegate to standard's `rlcompleter.Completer`` class
+    """
+    def __init__(self, local_ctx):
+        self.completer = Completer(local_ctx)
+
+    def possible_matches(self, text):
+        if "." in text:
+            return self.completer.attr_matches(text)
+        else:
+            return self.completer.global_matches(text)
+
+
+class CWShellCompleter(object):
+    """Custom auto-completion helper for cubicweb-ctl shell.
+
+    ``CWShellCompleter`` provides a ``complete`` method suitable for
+    ``readline.set_completer``.
+
+    Attributes:
+        matchers: the list of ``AbstractMatcher`` instances that will suggest
+                  possible completions
+
+    The completion process is the following:
+
+    - readline calls the ``complete`` method with user's input,
+    - the ``complete`` method asks for each known matchers if
+      it can suggest completions for user's input.
+    """
+
+    def __init__(self, local_ctx):
+        # list of matchers to ask for possible matches on completion
+        self.matchers = [DefaultMatcher(local_ctx)]
+        self.matchers.insert(0, RQLExecuteMatcher(local_ctx, local_ctx['session']))
+
+    def complete(self, text, state):
+        """readline's completer method
+
+        cf http://docs.python.org/2/library/readline.html#readline.set_completer
+        for more details.
+
+        Implementation inspired by `rlcompleter.Completer`
+        """
+        if state == 0:
+            # reset self.matches
+            self.matches = []
+            for matcher in self.matchers:
+                matches = matcher.possible_matches(text)
+                if matches:
+                    self.matches = matches
+                    break
+            else:
+                return None # no matcher able to handle `text`
+        try:
+            return self.matches[state]
+        except IndexError:
+            return None
--- a/view.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/view.py	Fri Jul 18 17:35:25 2014 +0200
@@ -501,28 +501,6 @@
 class ReloadableMixIn(object):
     """simple mixin for reloadable parts of UI"""
 
-    def user_callback(self, cb, args, msg=None, nonify=False):
-        """register the given user callback and return a URL to call it ready to be
-        inserted in html
-        """
-        self._cw.add_js('cubicweb.ajax.js')
-        if nonify:
-            _cb = cb
-            def cb(*args):
-                _cb(*args)
-        cbname = self._cw.register_onetime_callback(cb, *args)
-        return self.build_js(cbname, xml_escape(msg or ''))
-
-    def build_update_js_call(self, cbname, msg):
-        rql = self.cw_rset.printable_rql()
-        return "javascript: %s" % js.userCallbackThenUpdateUI(
-            cbname, self.__regid__, rql, msg, self.__registry__, self.domid)
-
-    def build_reload_js_call(self, cbname, msg):
-        return "javascript: %s" % js.userCallbackThenReloadPage(cbname, msg)
-
-    build_js = build_update_js_call # expect updatable component by default
-
     @property
     def domid(self):
         return domid(self.__regid__)
--- a/web/application.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/web/application.py	Fri Jul 18 17:35:25 2014 +0200
@@ -23,6 +23,7 @@
 from time import clock, time
 from contextlib import contextmanager
 from warnings import warn
+import json
 
 import httplib
 
@@ -589,8 +590,10 @@
         status = httplib.INTERNAL_SERVER_ERROR
         if isinstance(ex, PublishException) and ex.status is not None:
             status = ex.status
-        req.status_out = status
-        json_dumper = getattr(ex, 'dumps', lambda : unicode(ex))
+        if req.status_out < 400:
+            # don't overwrite it if it's already set
+            req.status_out = status
+        json_dumper = getattr(ex, 'dumps', lambda : json.dumps({'reason': unicode(ex)}))
         return json_dumper()
 
     # special case handling
--- a/web/data/cubicweb.ajax.js	Fri Jul 18 16:44:44 2014 +0200
+++ b/web/data/cubicweb.ajax.js	Fri Jul 18 17:35:25 2014 +0200
@@ -88,8 +88,8 @@
 });
 
 var AJAX_PREFIX_URL = 'ajax';
-var JSON_BASE_URL = baseuri() + 'json?';
-var AJAX_BASE_URL = baseuri() + AJAX_PREFIX_URL + '?';
+var JSON_BASE_URL = BASE_URL + 'json?';
+var AJAX_BASE_URL = BASE_URL + AJAX_PREFIX_URL + '?';
 
 
 jQuery.extend(cw.ajax, {
@@ -122,9 +122,7 @@
      * (e.g. http://..../data??resource1.js,resource2.js)
      */
     _modconcatLikeUrl: function(url) {
-        var base = baseuri();
-        if (!base.endswith('/')) { base += '/'; }
-        var modconcat_rgx = new RegExp('(' + base + 'data/([a-z0-9]+/)?)\\?\\?(.+)');
+        var modconcat_rgx = new RegExp('(' + BASE_URL + 'data/([a-z0-9]+/)?)\\?\\?(.+)');
         return modconcat_rgx.exec(url);
     },
 
@@ -379,8 +377,8 @@
  * dictionary, `reqtype` the HTTP request type (get 'GET' or 'POST').
  */
 function loadRemote(url, form, reqtype, sync) {
-    if (!url.toLowerCase().startswith(baseuri().toLowerCase())) {
-        url = baseuri() + url;
+    if (!url.toLowerCase().startswith(BASE_URL.toLowerCase())) {
+        url = BASE_URL + url;
     }
     if (!sync) {
         var deferred = new Deferred();
@@ -601,7 +599,7 @@
                 var fck = new FCKeditor(this.id);
                 fck.Config['CustomConfigurationsPath'] = fckconfigpath;
                 fck.Config['DefaultLanguage'] = fcklang;
-                fck.BasePath = baseuri() + "fckeditor/";
+                fck.BasePath = BASE_URL + "fckeditor/";
                 fck.ReplaceTextarea();
             } else {
                 cw.log('fckeditor could not be found.');
--- a/web/data/cubicweb.edition.js	Fri Jul 18 16:44:44 2014 +0200
+++ b/web/data/cubicweb.edition.js	Fri Jul 18 17:35:25 2014 +0200
@@ -67,7 +67,7 @@
                 rql: rql_for_eid(eid),
                 '__notemplate': 1
             };
-            var d = jQuery('#unrelatedDivs_' + eid).loadxhtml(baseuri() + 'view', args, 'post', 'append');
+            var d = jQuery('#unrelatedDivs_' + eid).loadxhtml(BASE_URL + 'view', args, 'post', 'append');
             d.addCallback(function() {
                 _showMatchingSelect(eid, jQuery('#' + divId));
             });
--- a/web/data/cubicweb.facets.js	Fri Jul 18 16:44:44 2014 +0200
+++ b/web/data/cubicweb.facets.js	Fri Jul 18 17:35:25 2014 +0200
@@ -69,7 +69,7 @@
         }
         var $focusLink = $('#focusLink');
         if ($focusLink.length) {
-            var url = baseuri()+ 'view?rql=' + encodeURIComponent(rql);
+            var url = BASE_URL + 'view?rql=' + encodeURIComponent(rql);
             if (vid) {
                 url += '&vid=' + encodeURIComponent(vid);
             }
--- a/web/data/cubicweb.htmlhelpers.js	Fri Jul 18 16:44:44 2014 +0200
+++ b/web/data/cubicweb.htmlhelpers.js	Fri Jul 18 17:35:25 2014 +0200
@@ -12,20 +12,13 @@
 /**
  * .. function:: baseuri()
  *
- * returns the document's baseURI. (baseuri() uses document.baseURI if
- * available and inspects the <base> tag manually otherwise.)
+ * returns the document's baseURI.
  */
-function baseuri() {
-    if (typeof BASE_URL === 'undefined') {
-        // backward compatibility, BASE_URL might be undefined
-        var uri = document.baseURI;
-        if (uri) { // some browsers don't define baseURI
-            return uri.toLowerCase();
-        }
-        return jQuery('base').attr('href').toLowerCase();
-    }
-    return BASE_URL;
-}
+baseuri = cw.utils.deprecatedFunction(
+    "[3.20] baseuri() is deprecated, use BASE_URL instead",
+    function () {
+        return BASE_URL;
+    });
 
 /**
  * .. function:: setProgressCursor()
@@ -107,18 +100,6 @@
 }
 
 /**
- * .. function:: popupLoginBox()
- *
- * toggles visibility of login popup div
- */
-// XXX used exactly ONCE in basecomponents
-popupLoginBox = cw.utils.deprecatedFunction(
-    function() {
-        $('#popupLoginBox').toggleClass('hidden');
-        jQuery('#__login:visible').focus();
-});
-
-/**
  * .. function getElementsMatching(tagName, properties, \/* optional \*\/ parent)
  *
  * returns the list of elements in the document matching the tag name
--- a/web/data/cubicweb.js	Fri Jul 18 16:44:44 2014 +0200
+++ b/web/data/cubicweb.js	Fri Jul 18 17:35:25 2014 +0200
@@ -208,91 +208,40 @@
     },
 
     /**
-     * .. function:: formContents(elem \/* = document.body *\/)
+     * .. function:: formContents(elem)
      *
-     * this implementation comes from MochiKit
+     * cannot use jQuery.serializeArray() directly because of FCKeditor
      */
-    formContents: function (elem /* = document.body */ ) {
-        var names = [];
-        var values = [];
-        if (typeof(elem) == "undefined" || elem === null) {
-            elem = document.body;
-        } else {
-            elem = cw.getNode(elem);
-        }
-        cw.utils.nodeWalkDepthFirst(elem, function (elem) {
-            var name = elem.name;
-            if (name && name.length) {
-                if (elem.disabled) {
-                    return null;
-                }
-                var tagName = elem.tagName.toUpperCase();
-                if (tagName === "INPUT" && (elem.type == "radio" || elem.type == "checkbox") && !elem.checked) {
-                    return null;
-                }
-                if (tagName === "SELECT") {
-                    if (elem.type == "select-one") {
-                        if (elem.selectedIndex >= 0) {
-                            var opt = elem.options[elem.selectedIndex];
-                            var v = opt.value;
-                            if (!v) {
-                                var h = opt.outerHTML;
-                                // internet explorer sure does suck.
-                                if (h && !h.match(/^[^>]+\svalue\s*=/i)) {
-                                    v = opt.text;
-                                }
-                            }
-                            names.push(name);
-                            values.push(v);
+    formContents: function (elem) {
+        var $elem, array, names, values;
+        $elem = cw.jqNode(elem);
+        array = $elem.serializeArray();
+
+        if (typeof FCKeditor !== 'undefined') {
+            $elem.find('textarea').each(function (idx, textarea) {
+                var fck = FCKeditorAPI.GetInstance(textarea.id);
+                if (fck) {
+                    array = jQuery.map(array, function (dict) {
+                        if (dict.name === textarea.name) {
+                            // filter out the textarea's - likely empty - value ...
                             return null;
                         }
-                        // no form elements?
-                        names.push(name);
-                        values.push("");
-                        return null;
-                    } else {
-                        var opts = elem.options;
-                        if (!opts.length) {
-                            names.push(name);
-                            values.push("");
-                            return null;
-                        }
-                        for (var i = 0; i < opts.length; i++) {
-                            var opt = opts[i];
-                            if (!opt.selected) {
-                                continue;
-                            }
-                            var v = opt.value;
-                            if (!v) {
-                                var h = opt.outerHTML;
-                                // internet explorer sure does suck.
-                                if (h && !h.match(/^[^>]+\svalue\s*=/i)) {
-                                    v = opt.text;
-                                }
-                            }
-                            names.push(name);
-                            values.push(v);
-                        }
-                        return null;
-                    }
+                        return dict;
+                    });
+                    // ... so we can put the HTML coming from FCKeditor instead.
+                    array.push({
+                        name: textarea.name,
+                        value: fck.GetHTML()
+                    });
                 }
-                if (tagName === "FORM" || tagName === "P" || tagName === "SPAN" || tagName === "DIV") {
-                    return elem.childNodes;
-                }
-		var value = elem.value;
-		if (tagName === "TEXTAREA") {
-		    if (typeof(FCKeditor) != 'undefined') {
-			var fck = FCKeditorAPI.GetInstance(elem.id);
-			if (fck) {
-			    value = fck.GetHTML();
-			}
-		    }
-		}
-                names.push(name);
-                values.push(value || '');
-                return null;
-            }
-            return elem.childNodes;
+            });
+        }
+
+        names = [];
+        values = [];
+        jQuery.each(array, function (idx, dict) {
+            names.push(dict.name);
+            values.push(dict.value);
         });
         return [names, values];
     },
--- a/web/data/cubicweb.timeline-bundle.js	Fri Jul 18 16:44:44 2014 +0200
+++ b/web/data/cubicweb.timeline-bundle.js	Fri Jul 18 17:35:25 2014 +0200
@@ -3,8 +3,8 @@
  *  :organization: Logilab
  */
 
-var SimileAjax_urlPrefix = baseuri() + 'data/';
-var Timeline_urlPrefix = baseuri() + 'data/';
+var SimileAjax_urlPrefix = BASE_URL + 'data/';
+var Timeline_urlPrefix = BASE_URL + 'data/';
 
 /*
  *  Simile Ajax API
--- a/web/facet.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/web/facet.py	Fri Jul 18 17:35:25 2014 +0200
@@ -61,7 +61,6 @@
 from logilab.common.graph import has_path
 from logilab.common.decorators import cached, cachedproperty
 from logilab.common.date import datetime2ticks, ustrftime, ticks2datetime
-from logilab.common.compat import all
 from logilab.common.deprecation import deprecated
 from logilab.common.registry import yes
 
--- a/web/formfields.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/web/formfields.py	Fri Jul 18 17:35:25 2014 +0200
@@ -529,6 +529,7 @@
     """
     widget = fw.TextArea
     size = 45
+    placeholder = None
 
     def __init__(self, name=None, max_length=None, **kwargs):
         self.max_length = max_length # must be set before super call
@@ -547,6 +548,9 @@
         elif isinstance(self.widget, fw.TextInput):
             self.init_text_input(self.widget)
 
+        if self.placeholder:
+            self.widget.attrs.setdefault('placeholder', self.placeholder)
+
     def init_text_input(self, widget):
         if self.max_length:
             widget.attrs.setdefault('size', min(self.size, self.max_length))
@@ -557,6 +561,11 @@
             widget.attrs.setdefault('cols', 60)
             widget.attrs.setdefault('rows', 5)
 
+    def set_placeholder(self, placeholder):
+        self.placeholder = placeholder
+        if self.widget and self.placeholder:
+            self.widget.attrs.setdefault('placeholder', self.placeholder)
+
 
 class PasswordField(StringField):
     """Use this field to edit password (`Password` yams type, encoded python
--- a/web/formwidgets.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/web/formwidgets.py	Fri Jul 18 17:35:25 2014 +0200
@@ -210,6 +210,8 @@
             attrs['id'] = field.dom_id(form, self.suffix)
         if self.settabindex and not 'tabindex' in attrs:
             attrs['tabindex'] = form._cw.next_tabindex()
+        if 'placeholder' in attrs:
+            attrs['placeholder'] = form._cw._(attrs['placeholder'])
         return attrs
 
     def values(self, form, field):
--- a/web/http_headers.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/web/http_headers.py	Fri Jul 18 17:35:25 2014 +0200
@@ -1324,6 +1324,9 @@
         h = self._headers.get(name, None)
         r = self.handler.generate(name, h)
         if r is not None:
+            assert isinstance(r, list)
+            for v in r:
+                assert isinstance(v, str)
             self._raw_headers[name] = r
         return r
 
@@ -1362,6 +1365,9 @@
         Value should be a list of strings, each being one header of the
         given name.
         """
+        assert isinstance(value, list)
+        for v in value:
+            assert isinstance(v, str)
         name = name.lower()
         self._raw_headers[name] = value
         self._headers[name] = _RecalcNeeded
--- a/web/request.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/web/request.py	Fri Jul 18 17:35:25 2014 +0200
@@ -179,7 +179,7 @@
         self.ajax_request = value
     json_request = property(_get_json_request, _set_json_request)
 
-    def base_url(self, secure=None):
+    def _base_url(self, secure=None):
         """return the root url of the instance
 
         secure = False -> base-url
@@ -192,7 +192,7 @@
         if secure:
             base_url = self.vreg.config.get('https-url')
         if base_url is None:
-            base_url = super(_CubicWebRequestBase, self).base_url()
+            base_url = super(_CubicWebRequestBase, self)._base_url()
         return base_url
 
     @property
@@ -786,10 +786,6 @@
             if 'Expires' not in self.headers_out:
                 # Expires header seems to be required by IE7 -- Are you sure ?
                 self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
-            if self.http_method() == 'HEAD':
-                self.status_out = 200
-                # XXX replace by True once validate_cache bw compat method is dropped
-                return 200
             # /!\ no raise, the function returns and we keep processing the request
         else:
             # overwrite headers_out to forge a brand new not-modified response
--- a/web/test/unittest_form.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/web/test/unittest_form.py	Fri Jul 18 17:35:25 2014 +0200
@@ -19,7 +19,6 @@
 from xml.etree.ElementTree import fromstring
 
 from logilab.common.testlib import unittest_main, mock_object
-from logilab.common.compat import any
 
 from cubicweb import Binary, ValidationError
 from cubicweb.devtools.testlib import CubicWebTC
--- a/web/test/unittest_http.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/web/test/unittest_http.py	Fri Jul 18 17:35:25 2014 +0200
@@ -227,7 +227,7 @@
         hout = [('etag', 'rhino/really-not-babar'),
                ]
         req = _test_cache(hin, hout, method='HEAD')
-        self.assertCache(200, req.status_out, 'modifier HEAD verb')
+        self.assertCache(None, req.status_out, 'modifier HEAD verb')
         # not modified
         hin  = [('if-none-match', 'babar'),
                ]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_views_forms.py	Fri Jul 18 17:35:25 2014 +0200
@@ -0,0 +1,36 @@
+# copyright 2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+
+from cubicweb.devtools.testlib import CubicWebTC
+
+class InlinedFormTC(CubicWebTC):
+
+    def test_linked_to(self):
+        req = self.request()
+        formview = req.vreg['views'].select(
+            'inline-creation', req,
+            etype='File', rtype='described_by_test', role='subject',
+            peid=123,
+            petype='Salesterm')
+        self.assertEqual({('described_by_test', 'object'): [123]},
+                         formview.form.linked_to)
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
+
--- a/web/test/unittest_web.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/web/test/unittest_web.py	Fri Jul 18 17:35:25 2014 +0200
@@ -94,6 +94,7 @@
         self.assertEqual(webreq.status_code, 200)
         self.assertDictEqual(expect, loads(webreq.content))
 
+
 class LanguageTC(CubicWebServerTC):
 
     def test_language_neg(self):
@@ -104,6 +105,19 @@
         webreq = self.web_request(headers=headers)
         self.assertIn('lang="en"', webreq.read())
 
+    def test_response_codes(self):
+        with self.admin_access.client_cnx() as cnx:
+            admin_eid = cnx.user.eid
+        # guest can't see admin
+        webreq = self.web_request('/%d' % admin_eid)
+        self.assertEqual(webreq.status, 403)
+
+        # but admin can
+        self.web_login()
+        webreq = self.web_request('/%d' % admin_eid)
+        self.assertEqual(webreq.status, 200)
+
+
 class LogQueriesTC(CubicWebServerTC):
     @classmethod
     def init_config(cls, config):
--- a/web/views/ajaxedit.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/web/views/ajaxedit.py	Fri Jul 18 17:35:25 2014 +0200
@@ -36,8 +36,6 @@
     cw_property_defs = {} # don't want to inherit this from Box
     expected_kwargs = form_params = ('rtype', 'target')
 
-    build_js = component.EditRelationMixIn.build_reload_js_call
-
     def cell_call(self, row, col, rtype=None, target=None, etype=None):
         self.rtype = rtype or self._cw.form['rtype']
         self.target = target or self._cw.form['target']
--- a/web/views/forms.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/web/views/forms.py	Fri Jul 18 17:35:25 2014 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -41,17 +41,17 @@
 
 but you'll use this one rarely.
 """
+
 __docformat__ = "restructuredtext en"
 
 from warnings import warn
 
 from logilab.common import dictattr, tempattr
 from logilab.common.decorators import iclassmethod, cached
-from logilab.common.compat import any
 from logilab.common.textutils import splitstrip
 from logilab.common.deprecation import deprecated
 
-from cubicweb import ValidationError
+from cubicweb import ValidationError, neg_role
 from cubicweb.utils import support_args
 from cubicweb.predicates import non_final_entity, match_kwargs, one_line_rset
 from cubicweb.web import RequestError, ProcessFormError
@@ -400,12 +400,21 @@
     @property
     @cached
     def linked_to(self):
-        # if current form is not the main form, exit immediately
+        linked_to = {}
+        # case where this is an embeded creation form
+        try:
+            eid = int(self.cw_extra_kwargs['peid'])
+        except KeyError:
+            pass
+        else:
+            ltrtype = self.cw_extra_kwargs['rtype']
+            ltrole = neg_role(self.cw_extra_kwargs['role'])
+            linked_to[(ltrtype, ltrole)] = [eid]
+        # now consider __linkto if the current form is the main form
         try:
             self.field_by_name('__maineid')
         except form.FieldNotFound:
-            return {}
-        linked_to = {}
+            return linked_to
         for linkto in self._cw.list_form_param('__linkto'):
             ltrtype, eid, ltrole = linkto.split(':')
             linked_to.setdefault((ltrtype, ltrole), []).append(int(eid))
--- a/web/views/uicfg.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/web/views/uicfg.py	Fri Jul 18 17:35:25 2014 +0200
@@ -57,8 +57,6 @@
 
 from warnings import warn
 
-from logilab.common.compat import any
-
 from cubicweb import neg_role
 from cubicweb.rtags import (RelationTags, RelationTagsBool, RelationTagsSet,
                             RelationTagsDict, NoTargetRelationTagsDict,
--- a/wsgi/request.py	Fri Jul 18 16:44:44 2014 +0200
+++ b/wsgi/request.py	Fri Jul 18 17:35:25 2014 +0200
@@ -59,7 +59,7 @@
 
         headers_in = dict((normalize_header(k[5:]), v) for k, v in self.environ.items()
                           if k.startswith('HTTP_'))
-        https = environ.get("HTTPS") in ('yes', 'on', '1')
+        https = self.is_secure()
         post, files = self.get_posted_data()
 
         super(CubicWebWsgiRequest, self).__init__(vreg, https, post,
@@ -104,32 +104,8 @@
 
     ## wsgi request helpers ###################################################
 
-    def instance_uri(self):
-        """Return the instance's base URI (no PATH_INFO or QUERY_STRING)
-
-        see python2.5's wsgiref.util.instance_uri code
-        """
-        environ = self.environ
-        url = environ['wsgi.url_scheme'] + '://'
-        if environ.get('HTTP_HOST'):
-            url += environ['HTTP_HOST']
-        else:
-            url += environ['SERVER_NAME']
-            if environ['wsgi.url_scheme'] == 'https':
-                if environ['SERVER_PORT'] != '443':
-                    url += ':' + environ['SERVER_PORT']
-            else:
-                if environ['SERVER_PORT'] != '80':
-                    url += ':' + environ['SERVER_PORT']
-        url += quote(environ.get('SCRIPT_NAME') or '/')
-        return url
-
-    def get_full_path(self):
-        return '%s%s' % (self.path, self.environ.get('QUERY_STRING', '') and ('?' + self.environ.get('QUERY_STRING', '')) or '')
-
     def is_secure(self):
-        return 'wsgi.url_scheme' in self.environ \
-            and self.environ['wsgi.url_scheme'] == 'https'
+        return self.environ['wsgi.url_scheme'] == 'https'
 
     def get_posted_data(self):
         # The WSGI spec says 'QUERY_STRING' may be absent.