merge 3.19.4 in 3.20 branch
authorJulien Cristau <julien.cristau@logilab.fr>
Thu, 25 Sep 2014 15:49:13 +0200
changeset 9990 c84ad981fc4a
parent 9974 b240b33c7125 (current diff)
parent 9985 c4e740e50fc7 (diff)
child 10000 4352b7ccde04
merge 3.19.4 in 3.20 branch
__init__.py
__pkginfo__.py
cubicweb.spec
cwconfig.py
cwctl.py
debian/control
doc/book/en/devrepo/datamodel/definition.rst
hooks/email.py
predicates.py
schema.py
server/querier.py
server/sources/datafeed.py
server/sources/native.py
server/test/unittest_querier.py
server/test/unittest_schemaserial.py
server/test/unittest_security.py
test/unittest_schema.py
toolsutils.py
view.py
web/data/cubicweb.js
web/facet.py
web/http_headers.py
web/test/unittest_form.py
web/test/unittest_views_editforms.py
web/views/autoform.py
web/views/forms.py
wsgi/request.py
--- a/.hgtags	Tue Sep 23 17:34:36 2014 +0200
+++ b/.hgtags	Thu Sep 25 15:49:13 2014 +0200
@@ -335,6 +335,9 @@
 a979d1594af6501a774fb32eb67cd32fea626655 cubicweb-version-3.17.16
 a979d1594af6501a774fb32eb67cd32fea626655 cubicweb-debian-version-3.17.16-1
 a979d1594af6501a774fb32eb67cd32fea626655 cubicweb-centos-version-3.17.16-1
+57e9d1c70512d0f4e2c33d33db436a8274e10c1a cubicweb-version-3.17.17
+57e9d1c70512d0f4e2c33d33db436a8274e10c1a cubicweb-debian-version-3.17.17-1
+57e9d1c70512d0f4e2c33d33db436a8274e10c1a cubicweb-centos-version-3.17.17-1
 db37bf35a1474843ded0a537f9cb4838f4a78cda cubicweb-version-3.18.0
 db37bf35a1474843ded0a537f9cb4838f4a78cda cubicweb-debian-version-3.18.0-1
 db37bf35a1474843ded0a537f9cb4838f4a78cda cubicweb-centos-version-3.18.0-1
@@ -353,6 +356,9 @@
 5071b69b6b0b0de937bb231404cbf652a103dbe0 cubicweb-version-3.18.5
 5071b69b6b0b0de937bb231404cbf652a103dbe0 cubicweb-debian-version-3.18.5-1
 5071b69b6b0b0de937bb231404cbf652a103dbe0 cubicweb-centos-version-3.18.5-1
+d915013567429b481cb2c367071e36451c07a226 cubicweb-version-3.18.6
+d915013567429b481cb2c367071e36451c07a226 cubicweb-debian-version-3.18.6-1
+d915013567429b481cb2c367071e36451c07a226 cubicweb-centos-version-3.18.6-1
 1141927b8494aabd16e31b0d0d9a50fe1fed5f2f cubicweb-version-3.19.0
 1141927b8494aabd16e31b0d0d9a50fe1fed5f2f cubicweb-debian-version-3.19.0-1
 1141927b8494aabd16e31b0d0d9a50fe1fed5f2f cubicweb-centos-version-3.19.0-1
--- a/__init__.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/__init__.py	Thu Sep 25 15:49:13 2014 +0200
@@ -123,7 +123,7 @@
     def __eq__(self, other):
         if not isinstance(other, Binary):
             return False
-        return self.getvalue(), other.getvalue()
+        return self.getvalue() == other.getvalue()
 
 
     # Binary helpers to store/fetch python objects
--- a/__pkginfo__.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/__pkginfo__.py	Thu Sep 25 15:49:13 2014 +0200
@@ -22,7 +22,7 @@
 
 modname = distname = "cubicweb"
 
-numversion = (3, 19, 3)
+numversion = (3, 19, 4)
 version = '.'.join(str(num) for num in numversion)
 
 description = "a repository of entities / relations for knowledge management"
@@ -39,7 +39,7 @@
 ]
 
 __depends__ = {
-    'logilab-common': '>= 0.60.0',
+    'logilab-common': '>= 0.62.0',
     'logilab-mtconverter': '>= 0.8.0',
     'rql': '>= 0.31.2',
     'yams': '>= 0.40.0',
--- a/cubicweb.spec	Tue Sep 23 17:34:36 2014 +0200
+++ b/cubicweb.spec	Thu Sep 25 15:49:13 2014 +0200
@@ -7,7 +7,7 @@
 %endif
 
 Name:           cubicweb
-Version:        3.19.3
+Version:        3.19.4
 Release:        logilab.1%{?dist}
 Summary:        CubicWeb is a semantic web application framework
 Source0:        http://download.logilab.org/pub/cubicweb/cubicweb-%{version}.tar.gz
@@ -20,7 +20,7 @@
 BuildArch:      noarch
 
 Requires:       %{python}
-Requires:       %{python}-logilab-common >= 0.60.0
+Requires:       %{python}-logilab-common >= 0.62.0
 Requires:       %{python}-logilab-mtconverter >= 0.8.0
 Requires:       %{python}-rql >= 0.31.2
 Requires:       %{python}-yams >= 0.40.0
--- a/cwconfig.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/cwconfig.py	Thu Sep 25 15:49:13 2014 +0200
@@ -124,14 +124,14 @@
 Python
 ``````
 
-If you installed *CubicWeb* by cloning the Mercurial forest or from source
+If you installed *CubicWeb* by cloning the Mercurial shell repository or from source
 distribution, then you will need to update the environment variable PYTHONPATH by
-adding the path to the forest `cubicweb`:
+adding the path to `cubicweb`:
 
 Add the following lines to either :file:`.bashrc` or :file:`.bash_profile` to
 configure your development environment ::
 
-    export PYTHONPATH=/full/path/to/cubicweb-forest
+    export PYTHONPATH=/full/path/to/grshell-cubicweb
 
 If you installed *CubicWeb* with packages, no configuration is required and your
 new cubes will be placed in `/usr/share/cubicweb/cubes` and your instances will
--- a/cwctl.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/cwctl.py	Thu Sep 25 15:49:13 2014 +0200
@@ -396,13 +396,14 @@
         print
         helper.bootstrap(cubes, self.config.automatic, self.config.config_level)
         # input for cubes specific options
-        sections = set(sect.lower() for sect, opt, odict in config.all_options()
-                       if 'type' in odict
-                       and odict.get('level') <= self.config.config_level)
-        for section in sections:
-            if section not in ('main', 'email', 'pyro', 'web'):
-                print '\n' + underline_title('%s options' % section)
-                config.input_config(section, self.config.config_level)
+        if not self.config.automatic:
+            sections = set(sect.lower() for sect, opt, odict in config.all_options()
+                           if 'type' in odict
+                           and odict.get('level') <= self.config.config_level)
+            for section in sections:
+                if section not in ('main', 'email', 'pyro', 'web'):
+                    print '\n' + underline_title('%s options' % section)
+                    config.input_config(section, self.config.config_level)
         # write down configuration
         config.save()
         self._handle_win32(config, appid)
@@ -1050,12 +1051,16 @@
         return ('stdlib',)
     return ('stdlib', 'werkzeug')
 
-class WSGIDebugStartHandler(InstanceCommand):
+class WSGIStartHandler(InstanceCommand):
     """Start an interactive wsgi server """
     name = 'wsgi'
     actionverb = 'started'
     arguments = '<instance>'
     options = (
+        ("debug",
+         {'short': 'D', 'action': 'store_true',
+          'default': False,
+          'help': 'start server in debug mode.'}),
         ('method',
          {'short': 'm',
           'type': 'choice',
@@ -1065,16 +1070,16 @@
           'help': 'wsgi utility/method'}),
         ('loglevel',
          {'short': 'l',
-          'type' : 'choice',
+          'type': 'choice',
           'metavar': '<log level>',
-          'default': 'debug',
+          'default': None,
           'choices': ('debug', 'info', 'warning', 'error'),
           'help': 'debug if -D is set, error otherwise',
           }),
         )
 
     def wsgi_instance(self, appid):
-        config = cwcfg.config_for(appid, debugmode=1)
+        config = cwcfg.config_for(appid, debugmode=self['debug'])
         init_cmdline_log_threshold(config, self['loglevel'])
         assert config.name == 'all-in-one'
         meth = self['method']
@@ -1089,7 +1094,7 @@
 for cmdcls in (ListCommand,
                CreateInstanceCommand, DeleteInstanceCommand,
                StartInstanceCommand, StopInstanceCommand, RestartInstanceCommand,
-               WSGIDebugStartHandler,
+               WSGIStartHandler,
                ReloadConfigurationCommand, StatusCommand,
                UpgradeInstanceCommand,
                ListVersionsInstanceCommand,
--- a/cwvreg.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/cwvreg.py	Thu Sep 25 15:49:13 2014 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2012 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.
@@ -241,7 +241,7 @@
 
 class CWRegistry(Registry):
     def __init__(self, vreg):
-        super(CWRegistry, self).__init__(vreg.config.debugmode)
+        super(CWRegistry, self).__init__(True)
         self.vreg = vreg
 
     @property
@@ -598,11 +598,33 @@
         if self.is_reload_needed(path):
             self.reload(path)
 
+    def _cleanup_sys_modules(self, path):
+        """Remove submodules of `directories` from `sys.modules` and cleanup
+        CW_EVENT_MANAGER accordingly.
+
+        We take care to properly remove obsolete registry callbacks.
+
+        """
+        caches = {}
+        callbackdata = CW_EVENT_MANAGER.callbacks.values()
+        for callbacklist in callbackdata:
+            for callback in callbacklist:
+                func = callback[0]
+                # for non-function callable, we do nothing interesting
+                module = getattr(func, '__module__', None)
+                caches[id(callback)] = module
+        deleted_modules = set(cleanup_sys_modules(path))
+        for callbacklist in callbackdata:
+            for callback in callbacklist[:]:
+                module = caches[id(callback)]
+                if module and module in deleted_modules:
+                    callbacklist.remove(callback)
+
     def reload(self, path, force_reload=True):
         """modification detected, reset and reload the vreg"""
         CW_EVENT_MANAGER.emit('before-registry-reload')
         if force_reload:
-            cleanup_sys_modules(path)
+            self._cleanup_sys_modules(path)
             cubes = self.config.cubes()
             # if the fs code use some cubes not yet registered into the instance
             # we should cleanup sys.modules for those as well to avoid potential
@@ -611,7 +633,7 @@
             for cube in cfg.expand_cubes(cubes, with_recommends=True):
                 if not cube in cubes:
                     cpath = cfg.build_appobjects_cube_path([cfg.cube_dir(cube)])
-                    cleanup_sys_modules(cpath)
+                    self._cleanup_sys_modules(cpath)
         self.register_objects(path)
         CW_EVENT_MANAGER.emit('after-registry-reload')
 
--- a/debian/changelog	Tue Sep 23 17:34:36 2014 +0200
+++ b/debian/changelog	Thu Sep 25 15:49:13 2014 +0200
@@ -1,3 +1,9 @@
+cubicweb (3.19.4-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Julien Cristau <julien.cristau@logilab.fr>  Thu, 25 Sep 2014 14:24:04 +0200
+
 cubicweb (3.19.3-1) unstable; urgency=low
 
   * new upstream release
@@ -22,6 +28,12 @@
 
  -- Julien Cristau <julien.cristau@logilab.fr>  Mon, 28 Apr 2014 18:35:27 +0200
 
+cubicweb (3.18.6-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Julien Cristau <julien.cristau@logilab.fr>  Wed, 24 Sep 2014 15:08:34 +0200
+
 cubicweb (3.18.5-1) unstable; urgency=low
 
   * new upstream release
@@ -58,9 +70,15 @@
 
  -- Julien Cristau <julien.cristau@logilab.fr>  Fri, 10 Jan 2014 17:14:18 +0100
 
+cubicweb (3.17.17-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Aurelien Campeas <aurelien.campeas@logilab.fr>  Tue, 16 Sep 2014 18:38:19 +0200
+
 cubicweb (3.17.16-1) unstable; urgency=low
 
-  * new upstream value
+  * new upstream release
 
  -- Aurelien Campeas <aurelien.campeas@logilab.fr>  Mon, 07 Jul 2014 19:26:12 +0200
 
--- a/debian/control	Tue Sep 23 17:34:36 2014 +0200
+++ b/debian/control	Thu Sep 25 15:49:13 2014 +0200
@@ -151,7 +151,7 @@
  graphviz,
  gettext,
  python-logilab-mtconverter (>= 0.8.0),
- python-logilab-common (>= 0.60.0),
+ python-logilab-common (>= 0.62.0),
  python-yams (>= 0.40.0),
  python-rql (>= 0.31.2),
  python-lxml
--- a/devtools/httptest.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/devtools/httptest.py	Thu Sep 25 15:49:13 2014 +0200
@@ -89,8 +89,6 @@
     * `anonymous_allowed`: flag telling if anonymous browsing should be allowed
     """
     configcls = CubicWebServerConfig
-    # anonymous is logged by default in cubicweb test cases
-    anonymous_allowed = True
 
     def start_server(self):
         # use a semaphore to avoid starting test while the http server isn't
@@ -185,8 +183,3 @@
             # Server could be launched manually
             print err
         super(CubicWebServerTC, self).tearDown()
-
-    @classmethod
-    def init_config(cls, config):
-        config.set_anonymous_allowed(cls.anonymous_allowed)
-        super(CubicWebServerTC, cls).init_config(config)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/unittest_webtest.py	Thu Sep 25 15:49:13 2014 +0200
@@ -0,0 +1,40 @@
+import httplib
+
+from logilab.common.testlib import Tags
+from cubicweb.devtools.webtest import CubicWebTestTC
+
+
+class CWTTC(CubicWebTestTC):
+    def test_response(self):
+        response = self.webapp.get('/')
+        self.assertEqual(200, response.status_int)
+
+    def test_base_url(self):
+        if self.config['base-url'] not in self.webapp.get('/').text:
+            self.fail('no mention of base url in retrieved page')
+
+
+class CWTIdentTC(CubicWebTestTC):
+    anonymous_allowed = False
+    tags = CubicWebTestTC.tags | Tags(('auth',))
+
+    def test_reponse_denied(self):
+        res = self.webapp.get('/', expect_errors=True)
+        self.assertEqual(httplib.FORBIDDEN, res.status_int)
+
+    def test_login(self):
+        res = self.webapp.get('/', expect_errors=True)
+        self.assertEqual(httplib.FORBIDDEN, res.status_int)
+
+        self.login(self.admlogin, self.admpassword)
+        res = self.webapp.get('/')
+        self.assertEqual(httplib.OK, res.status_int)
+
+        self.logout()
+        res = self.webapp.get('/', expect_errors=True)
+        self.assertEqual(httplib.FORBIDDEN, res.status_int)
+
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
--- a/devtools/testlib.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/devtools/testlib.py	Thu Sep 25 15:49:13 2014 +0200
@@ -297,6 +297,9 @@
     _cnxs = set() # establised connection
                   # stay on connection for leak detection purpose
 
+    # anonymous is logged by default in cubicweb test cases
+    anonymous_allowed = True
+
     def __init__(self, *args, **kwargs):
         self._admin_session = None
         self._admin_clt_cnx = None
@@ -532,6 +535,7 @@
             config.global_set_option('embed-allowed', re.compile('.*'))
         except Exception: # not in server only configuration
             pass
+        config.set_anonymous_allowed(cls.anonymous_allowed)
 
     @property
     def vreg(self):
@@ -610,7 +614,7 @@
         """add your database setup code by overriding this method"""
 
     @classmethod
-    def pre_setup_database(cls, session, config):
+    def pre_setup_database(cls, cnx, config):
         """add your pre database setup code by overriding this method
 
         Do not forget to set the cls.test_db_id value to enable caching of the
@@ -879,6 +883,7 @@
             raise
         return result
 
+    @deprecated('[3.19] use .admin_request_from_url instead')
     def req_from_url(self, url):
         """parses `url` and builds the corresponding CW-web request
 
@@ -892,6 +897,20 @@
         req.setup_params(params)
         return req
 
+    @contextmanager
+    def admin_request_from_url(self, url):
+        """parses `url` and builds the corresponding CW-web request
+
+        req.form will be setup using the url's query string
+        """
+        with self.admin_access.web_request(url=url) as req:
+            if isinstance(url, unicode):
+                url = url.encode(req.encoding) # req.setup_params() expects encoded strings
+            querystring = urlparse.urlparse(url)[-2]
+            params = urlparse.parse_qs(querystring)
+            req.setup_params(params)
+            yield req
+
     def url_publish(self, url, data=None):
         """takes `url`, uses application's app_resolver to find the appropriate
         controller and result set, then publishes the result.
@@ -902,22 +921,22 @@
         This should pretty much correspond to what occurs in a real CW server
         except the apache-rewriter component is not called.
         """
-        req = self.req_from_url(url)
-        if data is not None:
-            req.form.update(data)
-        ctrlid, rset = self.app.url_resolver.process(req, req.relative_path(False))
-        return self.ctrl_publish(req, ctrlid, rset)
+        with self.admin_request_from_url(url) as req:
+            if data is not None:
+                req.form.update(data)
+            ctrlid, rset = self.app.url_resolver.process(req, req.relative_path(False))
+            return self.ctrl_publish(req, ctrlid, rset)
 
     def http_publish(self, url, data=None):
         """like `url_publish`, except this returns a http response, even in case
         of errors. You may give form parameters using the `data` argument.
         """
-        req = self.req_from_url(url)
-        if data is not None:
-            req.form.update(data)
-        with real_error_handling(self.app):
-            result = self.app_handle_request(req, req.relative_path(False))
-        return result, req
+        with self.admin_request_from_url(url) as req:
+            if data is not None:
+                req.form.update(data)
+            with real_error_handling(self.app):
+                result = self.app_handle_request(req, req.relative_path(False))
+            return result, req
 
     @staticmethod
     def _parse_location(req, location):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/webtest.py	Thu Sep 25 15:49:13 2014 +0200
@@ -0,0 +1,36 @@
+from __future__ import absolute_import
+
+import webtest
+
+from cubicweb.wsgi import handler
+from cubicweb.devtools.testlib import CubicWebTC
+
+
+class CubicWebTestTC(CubicWebTC):
+    @classmethod
+    def init_config(cls, config):
+        super(CubicWebTestTC, cls).init_config(config)
+        config.global_set_option('base-url', 'http://localhost.local/')
+
+    def setUp(self):
+        super(CubicWebTestTC, self).setUp()
+        webapp = handler.CubicWebWSGIApplication(self.config)
+        self.webapp = webtest.TestApp(webapp)
+
+    def tearDown(self):
+        del self.webapp
+        super(CubicWebTestTC, self).tearDown()
+
+    def login(self, user=None, password=None, **args):
+        if user is None:
+            user = self.admlogin
+        if password is None:
+            password = self.admpassword if user == self.admlogin else user
+        args.update({
+            '__login': user,
+            '__password': password
+        })
+        return self.webapp.get('/login', args)
+
+    def logout(self):
+        return self.webapp.get('/logout')
--- a/doc/book/en/admin/cubicweb-ctl.rst	Tue Sep 23 17:34:36 2014 +0200
+++ b/doc/book/en/admin/cubicweb-ctl.rst	Thu Sep 25 15:49:13 2014 +0200
@@ -37,7 +37,7 @@
    cubicweb-ctl newcube
 
 This will create a new cube in
-``/path/to/forest/cubicweb/cubes/<mycube>`` for a Mercurial forest
+``/path/to/grshell-cubicweb/cubes/<mycube>`` for a Mercurial
 installation, or in ``/usr/share/cubicweb/cubes`` for a debian
 packages installation.
 
--- a/doc/book/en/admin/setup-windows.rst	Tue Sep 23 17:34:36 2014 +0200
+++ b/doc/book/en/admin/setup-windows.rst	Thu Sep 25 15:49:13 2014 +0200
@@ -23,12 +23,12 @@
 |cubicweb| requires some base elements that must be installed to run
 correctly. So, first of all, you must install them :
 
-* python >= 2.5 and < 3
+* python >= 2.6 and < 3
   (`Download Python <http://www.python.org/download/>`_).
   You can also consider the Python(x,y) distribution
   (`Download Python(x,y) <http://code.google.com/p/pythonxy/wiki/Downloads>`_)
   as it makes things easier for Windows user by wrapping in a single installer
-  python 2.5 plus numerous useful third-party modules and
+  python 2.7 plus numerous useful third-party modules and
   applications (including Eclipse + pydev, which is an arguably good
   IDE for Python under Windows).
 
@@ -40,7 +40,7 @@
   (version >=2.2.1) allows working with XML and HTML
   (`Download lxml <http://pypi.python.org/pypi/lxml/2.2.1>`_)
 
-* `Postgresql 8.4 <http://www.postgresql.org/>`_,
+* `Postgresql <http://www.postgresql.org/>`_,
   an object-relational database system
   (`Download Postgresql <http://www.enterprisedb.com/products/pgdownload.do#windows>`_)
   and its python drivers
@@ -50,8 +50,7 @@
   (`Download gettext <http://download.logilab.org/pub/gettext/gettext-0.17-win32-setup.exe>`_).
 
 * `rql <http://www.logilab.org/project/rql>`_,
-  the recent version of the Relationship Query Language parser
-  (`Download rql <http://download.logilab.org/pub/rql/rql-0.26.3.win32-py2.5.exe>`_).
+  the recent version of the Relationship Query Language parser.
 
 Install optional elements
 -------------------------
@@ -59,16 +58,6 @@
 We recommend you to install the following elements. They are not
 mandatory but they activate very interesting features in |cubicweb|:
 
-* `Simplejson <http://pypi.python.org/pypi/simplejson/>`_
-  must be installed if you have python <= 2.5
-  (`Download simplejson <http://www.osuch.org/python-simplejson%3Awin32>`_).
-  It is included in the Standard library from Python >= 2.6.
-
-* `Pyro <http://www.xs4all.nl/~irmen/pyro3/>`_
-  enables remote access to cubicweb repository instances.
-  It also allows the client and the server not running on the same machine
-  (`Download Pyro <http://www.xs4all.nl/~irmen/pyro3/download/>`_).
-
 * `python-ldap <http://pypi.python.org/pypi/python-ldap>`_
   provides access to LDAP/Active directory directories
   (`Download python-ldap <http://www.osuch.org/python-ldap>`_).
--- a/doc/book/en/annexes/depends.rst	Tue Sep 23 17:34:36 2014 +0200
+++ b/doc/book/en/annexes/depends.rst	Thu Sep 25 15:49:13 2014 +0200
@@ -6,7 +6,7 @@
 =========================
 
 When you run CubicWeb from source, either by downloading the tarball or
-cloning the mercurial forest, here is the list of tools and libraries you need
+cloning the mercurial tree, here is the list of tools and libraries you need
 to have installed in order for CubicWeb to work:
 
 * yapps - http://theory.stanford.edu/~amitp/yapps/ -
@@ -15,61 +15,43 @@
 * pygraphviz - http://networkx.lanl.gov/pygraphviz/ -
   http://pypi.python.org/pypi/pygraphviz
 
-* simplejson - http://code.google.com/p/simplejson/ -
-  http://pypi.python.org/pypi/simplejson
-
-* docsutils - http://docutils.sourceforge.net/ - http://pypi.python.org/pypi/docutils
+* docutils - http://docutils.sourceforge.net/ - http://pypi.python.org/pypi/docutils
 
 * lxml - http://codespeak.net/lxml - http://pypi.python.org/pypi/lxml
 
 * twisted - http://twistedmatrix.com/ - http://pypi.python.org/pypi/Twisted
 
 * logilab-common - http://www.logilab.org/project/logilab-common -
-  http://pypi.python.org/pypi/logilab-common/ - included in the forest
+  http://pypi.python.org/pypi/logilab-common/
 
 * logilab-database - http://www.logilab.org/project/logilab-database -
-  http://pypi.python.org/pypi/logilab-database/ - included in the forest
+  http://pypi.python.org/pypi/logilab-database/
 
 * logilab-constraint - http://www.logilab.org/project/logilab-constraint -
-  http://pypi.python.org/pypi/constraint/ - included in the forest
+  http://pypi.python.org/pypi/constraint/
 
 * logilab-mtconverter - http://www.logilab.org/project/logilab-mtconverter -
-  http://pypi.python.org/pypi/logilab-mtconverter - included in the forest
+  http://pypi.python.org/pypi/logilab-mtconverter
 
-* rql - http://www.logilab.org/project/rql - http://pypi.python.org/pypi/rql -
-  included in the forest
+* rql - http://www.logilab.org/project/rql - http://pypi.python.org/pypi/rql
 
 * yams - http://www.logilab.org/project/yams - http://pypi.python.org/pypi/yams
-  - included in the forest
 
 * indexer - http://www.logilab.org/project/indexer -
-  http://pypi.python.org/pypi/indexer - included in the forest
+  http://pypi.python.org/pypi/indexer
 
 * passlib - https://code.google.com/p/passlib/ -
   http://pypi.python.org/pypi/passlib
 
-To use network communication between cubicweb instances / clients:
-
-* Pyro - http://www.xs4all.nl/~irmen/pyro3/ - http://pypi.python.org/pypi/Pyro
-
-If you're using a Postgres database (recommended):
+If you're using a Postgresql database (recommended):
 
 * psycopg2 - http://initd.org/projects/psycopg2 - http://pypi.python.org/pypi/psycopg2
 * plpythonu extension
-* tsearch2 extension (for postgres < 8.3, in postgres-contrib)
 
 Other optional packages:
 
 * fyzz - http://www.logilab.org/project/fyzz -
-  http://pypi.python.org/pypi/fyzz - included in the forest, *to activate Sparql querying*
-
-For the google-appengine extension to be available, you also need:
-
-* vobject - http://vobject.skyhouseconsulting.com/ -
-  http://pypi.python.org/pypi/vobject, *for the icalendar view*. For those not
-  benefiting from a packaging system, note that vobject itself depends on
-  dateutil - http://labix.org/python-dateutil -
-  http://pypi.python.org/pypi/python-dateutil/.
+  http://pypi.python.org/pypi/fyzz *to activate Sparql querying*
 
 
 Any help with the packaging of CubicWeb for more than Debian/Ubuntu (including
--- a/doc/book/en/annexes/mercurial.rst	Tue Sep 23 17:34:36 2014 +0200
+++ b/doc/book/en/annexes/mercurial.rst	Thu Sep 25 15:49:13 2014 +0200
@@ -18,13 +18,13 @@
 
 .. _Mercurial: http://www.selenic.com/mercurial/
 
-In contrast to CVS/Subversion, we usually create a repository by
+In contrast to CVS/Subversion, we usually create a repository per
 project to manage.
 
 In a collaborative development, we usually create a central repository
 accessible to all developers of the project. These central repository is used
-as a reference. According to its needs, then everyone can have a local repository,
-that you will have to synchronize with the central repository from time to time.
+as a reference. According to their needs, everyone can have a local repository,
+that they will have to synchronize with the central repository from time to time.
 
 
 Major commands
@@ -33,7 +33,7 @@
 
      hg clone ssh://myhost//home/src/repo
 
-* See the contents of the local repository (graphical tool in Tk)::
+* See the contents of the local repository (graphical tool in Qt)::
 
      hgview
 
@@ -111,17 +111,15 @@
    3. `hg ci`
    4. `hg push`
 
-Installation of the forest extension
-````````````````````````````````````
+Installation of the guestrepo extension
+```````````````````````````````````````
 
-Set up the forest extension by getting a copy of the sources
-from http://hg.akoha.org/hgforest/ and adding the following
+Set up the guestrepo extension by getting a copy of the sources
+from https://bitbucket.org/selinc/guestrepo and adding the following
 lines to your ``~/.hgrc``: ::
 
    [extensions]
-   hgext.forest=
-   # or, if forest.py is not in the hgext dir:
-   # forest=/path/to/forest.py
+   guestrepo=/path/to/guestrepo/guestrepo
 
 
 More information
--- a/doc/book/en/devrepo/datamodel/definition.rst	Tue Sep 23 17:34:36 2014 +0200
+++ b/doc/book/en/devrepo/datamodel/definition.rst	Thu Sep 25 15:49:13 2014 +0200
@@ -300,7 +300,7 @@
 
 * users and groups of users
 * a user belongs to at least one group of user
-* permissions (read, update, create, delete)
+* permissions (`read`, `update`, `create`, `delete`)
 * permissions are assigned to groups (and not to users)
 
 For *CubicWeb* in particular:
@@ -320,10 +320,10 @@
   * the permissions of this group are only checked on `update`/`delete` actions
     if all the other groups the user belongs to do not provide those permissions
 
-Setting permissions is done with the attribute `__permissions__` of entities and
-relation definition. The value of this attribute is a dictionary where the keys
-are the access types (action), and the values are the authorized groups or
-expressions.
+Setting permissions is done with the class attribute `__permissions__`
+of entity types and relation definitions. The value of this attribute
+is a dictionary where the keys are the access types (action), and the
+values are the authorized groups or rql expressions.
 
 For an entity type, the possible actions are `read`, `add`, `update` and
 `delete`.
@@ -333,6 +333,19 @@
 For an attribute, the possible actions are `read`, `add` and `update`,
 and they are a refinement of an entity type permission.
 
+.. note::
+
+   By default, the permissions of an entity type attributes are
+   equivalent to the permissions of the entity type itself.
+
+   It is possible to provide custom attribute permissions which are
+   stronger than, or are more lenient than the entity type
+   permissions.
+
+   In a situation where all attributes were given custom permissions,
+   the entity type permissions would not be checked, except for the
+   `delete` action.
+
 For each access type, a tuple indicates the name of the authorized groups and/or
 one or multiple RQL expressions to satisfy to grant access. The access is
 provided if the user is in one of the listed groups or if one of the RQL condition
@@ -368,6 +381,13 @@
                       'add': ('managers', ERQLExpression('U has_add_permission X'),
                       'update': ('managers', ERQLExpression('U has_update_permission X')),}
 
+.. note::
+
+   The default permissions for attributes are not syntactically
+   equivalent to the default permissions of the entity types, but the
+   rql expressions work by delegating to the entity type permissions.
+
+
 The standard user groups
 ````````````````````````
 
--- a/doc/book/en/tutorials/base/customizing-the-application.rst	Tue Sep 23 17:34:36 2014 +0200
+++ b/doc/book/en/tutorials/base/customizing-the-application.rst	Thu Sep 25 15:49:13 2014 +0200
@@ -26,7 +26,7 @@
 
   cubicweb-ctl newcube myblog
 
-This will create in the cubes directory (:file:`/path/to/forest/cubes` for source
+This will create in the cubes directory (:file:`/path/to/grshell/cubes` for source
 installation, :file:`/usr/share/cubicweb/cubes` for Debian packages installation)
 a directory named :file:`blog` reflecting the structure described in
 :ref:`cubelayout`.
--- a/entities/adapters.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/entities/adapters.py	Thu Sep 25 15:49:13 2014 +0200
@@ -367,15 +367,3 @@
         globalmsg = _('some relations violate a unicity constraint')
         rtypes_msg['unicity constraint'] = globalmsg
         raise ValidationError(self.entity.eid, rtypes_msg)
-
-# deprecated ###################################################################
-
-
-class adapter_deprecated(view.auto_unwrap_bw_compat):
-    """metaclass to print a warning on instantiation of a deprecated class"""
-
-    def __call__(cls, *args, **kwargs):
-        msg = getattr(cls, "__deprecation_warning__",
-                      "%(cls)s is deprecated") % {'cls': cls.__name__}
-        warn(msg, DeprecationWarning, stacklevel=2)
-        return type.__call__(cls, *args, **kwargs)
--- a/entity.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/entity.py	Thu Sep 25 15:49:13 2014 +0200
@@ -425,8 +425,10 @@
             needcheck = not cls.e_schema.has_unique_values(mainattr)
         else:
             for rschema in cls.e_schema.subject_relations():
-                if rschema.final and rschema != 'eid' \
-                        and cls.e_schema.has_unique_values(rschema):
+                if (rschema.final
+                    and rschema != 'eid'
+                    and cls.e_schema.has_unique_values(rschema)
+                    and cls.e_schema.rdef(rschema.type).cardinality[0] == '1'):
                     mainattr = str(rschema)
                     needcheck = False
                     break
--- a/etwist/server.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/etwist/server.py	Thu Sep 25 15:49:13 2014 +0200
@@ -178,7 +178,7 @@
         path = self.channel._path.split('?', 1)[0].rstrip('/').rsplit('/', 1)[-1]
         self.clientproto = 'HTTP/1.1' # not yet initialized
         self.channel.persistent = 0   # force connection close on cleanup
-        self.setResponseCode(http.BAD_REQUEST)
+        self.setResponseCode(http.REQUEST_ENTITY_TOO_LARGE)
         if path in JSON_PATHS: # XXX better json path detection
             self.setHeader('content-type',"application/json")
             body = json_dumps({'reason': 'request max size exceeded'})
--- a/hooks/security.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/hooks/security.py	Thu Sep 25 15:49:13 2014 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2013 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.
@@ -34,11 +34,15 @@
 def check_entity_attributes(cnx, entity, action, editedattrs=None):
     eid = entity.eid
     eschema = entity.e_schema
+    if action == 'delete':
+        eschema.check_perm(session, action, eid=eid)
+        return
     # ._cw_skip_security_attributes is there to bypass security for attributes
     # set by hooks by modifying the entity's dictionary
     if editedattrs is None:
         editedattrs = entity.cw_edited
     dontcheck = editedattrs.skip_security
+    etypechecked = False
     for attr in editedattrs:
         if attr in dontcheck:
             continue
@@ -54,10 +58,10 @@
             # implements comparison by rql expression.
             if perms == buildobjs.DEFAULT_ATTRPERMS[action]:
                 # The default rule is to delegate to the entity
-                # rule. This is an historical artefact. Hence we take
-                # this object as a marker saying "no specific"
-                # permission rule for this attribute. Thus we just do
-                # nothing.
+                # rule. This needs to be checked only once.
+                if not etypechecked:
+                    entity.cw_check_perm(action)
+                    etypechecked = True
                 continue
             if perms == ():
                 # That means an immutable attribute; as an optimization, avoid
@@ -71,7 +75,6 @@
         cnx = self.cnx
         for eid, action, edited in self.get_data():
             entity = cnx.entity_from_eid(eid)
-            entity.cw_check_perm(action)
             check_entity_attributes(cnx, entity, action, edited)
 
 
--- a/hooks/syncsession.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/hooks/syncsession.py	Thu Sep 25 15:49:13 2014 +0200
@@ -42,15 +42,15 @@
     """base class for group operation"""
     cnxuser = None # make pylint happy
 
-    def __init__(self, session, *args, **kwargs):
+    def __init__(self, cnx, *args, **kwargs):
         """override to get the group name before actual groups manipulation:
 
         we may temporarily loose right access during a commit event, so
         no query should be emitted while comitting
         """
         rql = 'Any N WHERE G eid %(x)s, G name N'
-        result = session.execute(rql, {'x': kwargs['geid']}, build_descr=False)
-        hook.Operation.__init__(self, session, *args, **kwargs)
+        result = cnx.execute(rql, {'x': kwargs['geid']}, build_descr=False)
+        hook.Operation.__init__(self, cnx, *args, **kwargs)
         self.group = result[0][0]
 
 
@@ -94,14 +94,14 @@
 
 class _DelUserOp(hook.Operation):
     """close associated user's session when it is deleted"""
-    def __init__(self, session, cnxid):
-        self.cnxid = cnxid
-        hook.Operation.__init__(self, session)
+    def __init__(self, cnx, sessionid):
+        self.sessionid = sessionid
+        hook.Operation.__init__(self, cnx)
 
     def postcommit_event(self):
         """the observed connections set has been commited"""
         try:
-            self.session.repo.close(self.cnxid)
+            self.cnx.repo.close(self.sessionid)
         except BadConnectionId:
             pass # already closed
 
@@ -148,7 +148,7 @@
         """the observed connections set has been commited"""
         cwprop = self.cwprop
         if not cwprop.for_user:
-            self.session.vreg['propertyvalues'][cwprop.pkey] = cwprop.value
+            self.cnx.vreg['propertyvalues'][cwprop.pkey] = cwprop.value
         # if for_user is set, update is handled by a ChangeCWPropertyOp operation
 
 
@@ -161,19 +161,19 @@
         key, value = self.entity.pkey, self.entity.value
         if key.startswith('sources.'):
             return
-        session = self._cw
+        cnx = self._cw
         try:
-            value = session.vreg.typed_value(key, value)
+            value = cnx.vreg.typed_value(key, value)
         except UnknownProperty:
             msg = _('unknown property key %s')
             raise validation_error(self.entity, {('pkey', 'subject'): msg}, (key,))
         except ValueError as ex:
             raise validation_error(self.entity,
                                   {('value', 'subject'): str(ex)})
-        if not session.user.matching_groups('managers'):
-            session.add_relation(self.entity.eid, 'for_user', session.user.eid)
+        if not cnx.user.matching_groups('managers'):
+            cnx.add_relation(self.entity.eid, 'for_user', cnx.user.eid)
         else:
-            _AddCWPropertyOp(session, cwprop=self.entity)
+            _AddCWPropertyOp(cnx, cwprop=self.entity)
 
 
 class UpdateCWPropertyHook(AddCWPropertyHook):
@@ -188,20 +188,20 @@
         key, value = entity.pkey, entity.value
         if key.startswith('sources.'):
             return
-        session = self._cw
+        cnx = self._cw
         try:
-            value = session.vreg.typed_value(key, value)
+            value = cnx.vreg.typed_value(key, value)
         except UnknownProperty:
             return
         except ValueError as ex:
             raise validation_error(entity, {('value', 'subject'): str(ex)})
         if entity.for_user:
-            for session_ in get_user_sessions(session.repo, entity.for_user[0].eid):
-                _ChangeCWPropertyOp(session, cwpropdict=session_.user.properties,
+            for session in get_user_sessions(cnx.repo, entity.for_user[0].eid):
+                _ChangeCWPropertyOp(cnx, cwpropdict=session.user.properties,
                                     key=key, value=value)
         else:
             # site wide properties
-            _ChangeCWPropertyOp(session, cwpropdict=session.vreg['propertyvalues'],
+            _ChangeCWPropertyOp(cnx, cwpropdict=cnx.vreg['propertyvalues'],
                               key=key, value=value)
 
 
@@ -211,13 +211,13 @@
 
     def __call__(self):
         eid = self.entity.eid
-        session = self._cw
-        for eidfrom, rtype, eidto in session.transaction_data.get('pendingrelations', ()):
+        cnx = self._cw
+        for eidfrom, rtype, eidto in cnx.transaction_data.get('pendingrelations', ()):
             if rtype == 'for_user' and eidfrom == self.entity.eid:
                 # if for_user was set, delete has already been handled
                 break
         else:
-            _DelCWPropertyOp(session, cwpropdict=session.vreg['propertyvalues'],
+            _DelCWPropertyOp(cnx, cwpropdict=cnx.vreg['propertyvalues'],
                              key=self.entity.pkey)
 
 
@@ -227,17 +227,17 @@
     events = ('after_add_relation',)
 
     def __call__(self):
-        session = self._cw
+        cnx = self._cw
         eidfrom = self.eidfrom
-        if not session.entity_metas(eidfrom)['type'] == 'CWProperty':
+        if not cnx.entity_metas(eidfrom)['type'] == 'CWProperty':
             return
-        key, value = session.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V',
+        key, value = cnx.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V',
                                      {'x': eidfrom})[0]
-        if session.vreg.property_info(key)['sitewide']:
+        if cnx.vreg.property_info(key)['sitewide']:
             msg = _("site-wide property can't be set for user")
             raise validation_error(eidfrom, {('for_user', 'subject'): msg})
-        for session_ in get_user_sessions(session.repo, self.eidto):
-            _ChangeCWPropertyOp(session, cwpropdict=session_.user.properties,
+        for session in get_user_sessions(cnx.repo, self.eidto):
+            _ChangeCWPropertyOp(cnx, cwpropdict=session.user.properties,
                               key=key, value=value)
 
 
@@ -246,10 +246,10 @@
     events = ('after_delete_relation',)
 
     def __call__(self):
-        session = self._cw
-        key = session.execute('Any K WHERE P eid %(x)s, P pkey K',
+        cnx = self._cw
+        key = cnx.execute('Any K WHERE P eid %(x)s, P pkey K',
                               {'x': self.eidfrom})[0][0]
-        session.transaction_data.setdefault('pendingrelations', []).append(
+        cnx.transaction_data.setdefault('pendingrelations', []).append(
             (self.eidfrom, self.rtype, self.eidto))
-        for session_ in get_user_sessions(session.repo, self.eidto):
-            _DelCWPropertyOp(session, cwpropdict=session_.user.properties, key=key)
+        for session in get_user_sessions(cnx.repo, self.eidto):
+            _DelCWPropertyOp(cnx, cwpropdict=session.user.properties, key=key)
--- a/hooks/test/unittest_hooks.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/hooks/test/unittest_hooks.py	Thu Sep 25 15:49:13 2014 +0200
@@ -122,14 +122,14 @@
             entity = cnx.create_entity('Bookmark', title=u'wf1', path=u'/view')
             cnx.commit() # fire operations
             self.assertEqual(len(entity.created_by), 1) # make sure we have only one creator
-            self.assertEqual(entity.created_by[0].eid, self.session.user.eid)
+            self.assertEqual(entity.created_by[0].eid, cnx.user.eid)
 
     def test_metadata_owned_by(self):
         with self.admin_access.repo_cnx() as cnx:
             entity = cnx.create_entity('Bookmark', title=u'wf1', path=u'/view')
             cnx.commit() # fire operations
             self.assertEqual(len(entity.owned_by), 1) # make sure we have only one owner
-            self.assertEqual(entity.owned_by[0].eid, self.session.user.eid)
+            self.assertEqual(entity.owned_by[0].eid, cnx.user.eid)
 
     def test_user_login_stripped(self):
         with self.admin_access.repo_cnx() as cnx:
@@ -153,7 +153,7 @@
                               self.repo.connect, u'toto', password='hop')
             cnx.commit()
             cnxid = self.repo.connect(u'toto', password='hop')
-            self.assertNotEqual(cnxid, self.session.id)
+            self.assertNotEqual(cnxid, cnx.sessionid)
             cnx.execute('DELETE CWUser X WHERE X login "toto"')
             self.repo.execute(cnxid, 'State X')
             cnx.commit()
--- a/multipart.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/multipart.py	Thu Sep 25 15:49:13 2014 +0200
@@ -398,13 +398,13 @@
             mem_limit = kw.get('mem_limit', 2**20)
             if content_length > mem_limit:
                 raise MultipartError("Request to big. Increase MAXMEM.")
-            data = stream.read(mem_limit).decode(charset)
+            data = stream.read(mem_limit)
             if stream.read(1): # These is more that does not fit mem_limit
                 raise MultipartError("Request to big. Increase MAXMEM.")
             data = parse_qs(data, keep_blank_values=True)
             for key, values in data.iteritems():
                 for value in values:
-                    forms[key] = value
+                    forms[key] = value.decode(charset)
         else:
             raise MultipartError("Unsupported content type.")
     except MultipartError:
--- a/server/serverctl.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/server/serverctl.py	Thu Sep 25 15:49:13 2014 +0200
@@ -423,7 +423,7 @@
           'question.',
           }),
         ('config-level',
-         {'short': 'l', 'type': 'int', 'default': 1,
+         {'short': 'l', 'type': 'int', 'default': 0,
           'help': 'level threshold for questions asked when configuring '
           'another source'
           }),
--- a/server/session.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/server/session.py	Thu Sep 25 15:49:13 2014 +0200
@@ -532,8 +532,7 @@
     def __exit__(self, exctype=None, excvalue=None, tb=None):
         assert self._open # actually already open
         assert self._cnxset_count == 0
-        self._free_cnxset(ignoremode=True)
-        self.clear()
+        self.rollback()
         self._open = False
 
 
@@ -1712,10 +1711,13 @@
 
     @property
     def anonymous_session(self):
-        # XXX for now, anonymous-user is a web side option.
+        # XXX for now, anonymous_user only exists in webconfig (and testconfig).
         # It will only be present inside all-in-one instance.
         # there is plan to move it down to global config.
-        return self.user.login == self.repo.config.get('anonymous-user')
+        if not hasattr(self.repo.config, 'anonymous_user'):
+            # not a web or test config, no anonymous user
+            return False
+        return self.user.login == self.repo.config.anonymous_user()[0]
 
     @deprecated('[3.13] use getattr(session.rtype_eids_rdef(rtype, eidfrom, eidto), prop)')
     def schema_rproperty(self, rtype, eidfrom, eidto, rprop):
--- a/server/sources/datafeed.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/server/sources/datafeed.py	Thu Sep 25 15:49:13 2014 +0200
@@ -421,7 +421,7 @@
                 self.warning('delete %s %s entities', len(eids), etype)
                 cnx.execute('DELETE %s X WHERE X eid IN (%s)'
                             % (etype, ','.join(eids)))
-                cnx.commit()
+            cnx.commit()
 
     def update_if_necessary(self, entity, attrs):
         entity.complete(tuple(attrs))
--- a/server/sources/native.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/server/sources/native.py	Thu Sep 25 15:49:13 2014 +0200
@@ -25,12 +25,8 @@
 """
 __docformat__ = "restructuredtext en"
 
-try:
-    from cPickle import loads, dumps
-    import cPickle as pickle
-except ImportError:
-    from pickle import loads, dumps
-    import pickle
+from cPickle import loads, dumps
+import cPickle as pickle
 from threading import Lock
 from datetime import datetime
 from base64 import b64decode, b64encode
--- a/server/test/data/migratedapp/schema.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/server/test/data/migratedapp/schema.py	Thu Sep 25 15:49:13 2014 +0200
@@ -91,6 +91,22 @@
     attachment = SubjectRelation('File')
 
 
+class Frozable(EntityType):
+    __permissions__ = {
+        'read':   ('managers', 'users'),
+        'add':    ('managers', 'users'),
+        'update': ('managers', ERQLExpression('X frozen False'),),
+        'delete': ('managers', ERQLExpression('X frozen False'),)
+    }
+    name = String()
+    frozen = Boolean(default=False,
+                     __permissions__ = {
+                         'read':   ('managers', 'users'),
+                         'add':    ('managers', 'users'),
+                         'update': ('managers', 'owners')
+                         })
+
+
 class Personne(EntityType):
     __unique_together__ = [('nom', 'prenom', 'datenaiss')]
     nom    = String(fulltextindexed=True, required=True, maxsize=64)
--- a/server/test/data/schema.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/server/test/data/schema.py	Thu Sep 25 15:49:13 2014 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2013 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.
@@ -92,6 +92,7 @@
     type = String(maxsize=6)
     para = String(maxsize=512,
                   __permissions__ = {
+                      'add': ('managers', ERQLExpression('X in_state S, S name "todo"')),
                       'read':   ('managers', 'users', 'guests'),
                       'update': ('managers', ERQLExpression('X in_state S, S name "todo"')),
                       })
@@ -109,6 +110,23 @@
                                                                'S,Y')])
     todo_by = SubjectRelation('CWUser')
 
+
+class Frozable(EntityType):
+    __permissions__ = {
+        'read':   ('managers', 'users'),
+        'add':    ('managers', 'users'),
+        'update': ('managers', ERQLExpression('X frozen False'),),
+        'delete': ('managers', ERQLExpression('X frozen False'),)
+    }
+    name = String()
+    frozen = Boolean(default=False,
+                     __permissions__ = {
+                         'read':   ('managers', 'users'),
+                         'add':    ('managers', 'users'),
+                         'update': ('managers', 'owners')
+                         })
+
+
 class Personne(EntityType):
     __unique_together__ = [('nom', 'prenom', 'inline2')]
     nom    = String(fulltextindexed=True, required=True, maxsize=64)
--- a/server/test/unittest_querier.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/server/test/unittest_querier.py	Thu Sep 25 15:49:13 2014 +0200
@@ -176,8 +176,8 @@
                                 'X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWComputedRType, '
                                 '        CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, '
                                 '        CWRType, CWRelation, CWSource, CWUniqueTogetherConstraint, CWUser, Card, '
-                                '        Comment, Division, Email, EmailPart, EmailThread, ExternalUri, File, '
-                                '        Folder, Note, Old, Personne, RQLExpression, Societe, State, SubDivision, '
+                                '        Comment, Division, Email, EmailPart, EmailThread, ExternalUri, File, Folder, '
+                                '        Frozable, Note, Old, Personne, RQLExpression, Societe, State, SubDivision, '
                                 '        SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)')
             self.assertListEqual(sorted(solutions),
                                   sorted([{'X': 'BaseTransition', 'ETN': 'String', 'ET': 'CWEType'},
@@ -205,6 +205,7 @@
                                           {'X': 'ExternalUri', 'ETN': 'String', 'ET': 'CWEType'},
                                           {'X': 'File', 'ETN': 'String', 'ET': 'CWEType'},
                                           {'X': 'Folder', 'ETN': 'String', 'ET': 'CWEType'},
+                                          {'X': 'Frozable', 'ETN': 'String', 'ET': 'CWEType'},
                                           {'X': 'Note', 'ETN': 'String', 'ET': 'CWEType'},
                                           {'X': 'Old', 'ETN': 'String', 'ET': 'CWEType'},
                                           {'X': 'Personne', 'ETN': 'String', 'ET': 'CWEType'},
@@ -605,16 +606,16 @@
         self.assertListEqual(rset.rows,
                               [[u'description_format', 13],
                                [u'description', 14],
-                               [u'name', 18],
-                               [u'created_by', 44],
-                               [u'creation_date', 44],
-                               [u'cw_source', 44],
-                               [u'cwuri', 44],
-                               [u'in_basket', 44],
-                               [u'is', 44],
-                               [u'is_instance_of', 44],
-                               [u'modification_date', 44],
-                               [u'owned_by', 44]])
+                               [u'name', 19],
+                               [u'created_by', 45],
+                               [u'creation_date', 45],
+                               [u'cw_source', 45],
+                               [u'cwuri', 45],
+                               [u'in_basket', 45],
+                               [u'is', 45],
+                               [u'is_instance_of', 45],
+                               [u'modification_date', 45],
+                               [u'owned_by', 45]])
 
     def test_select_aggregat_having_dumb(self):
         # dumb but should not raise an error
--- a/server/test/unittest_schemaserial.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/server/test/unittest_schemaserial.py	Thu Sep 25 15:49:13 2014 +0200
@@ -327,7 +327,7 @@
               'internationalizable': True,
               'fulltextindexed': False,
               'ordernum': 3,
-              'defaultval': Binary('text/plain'),
+              'defaultval': Binary.zpickle(u'text/plain'),
               'indexed': False,
               'formula': None,
               'cardinality': u'?1'}),
@@ -344,7 +344,6 @@
                              list(rdef2rql(schema['description_format'].rdefs[('CWRType', 'String')],
                                            cstrtypemap)))
 
-
     def test_updateeschema2rql1(self):
         self.assertListEqual([('SET X description %(description)s,X final %(final)s,'
                                'X name %(name)s WHERE X eid %(x)s',
--- a/server/test/unittest_security.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/server/test/unittest_security.py	Thu Sep 25 15:49:13 2014 +0200
@@ -423,6 +423,21 @@
             self.assertRaises(Unauthorized, cnx.commit)
             cnx.execute('SET X web "http://www.logilab.org" WHERE X eid %(x)s', {'x': eid})
             cnx.commit()
+        with self.new_access('iaminusersgrouponly').repo_cnx() as cnx:
+            cnx.execute('INSERT Frozable F: F name "Foo"')
+            cnx.commit()
+            cnx.execute('SET F name "Bar" WHERE F is Frozable')
+            cnx.commit()
+            cnx.execute('SET F name "BaBar" WHERE F is Frozable')
+            cnx.execute('SET F frozen True WHERE F is Frozable')
+            with self.assertRaises(Unauthorized):
+                cnx.commit()
+            cnx.rollback()
+            cnx.execute('SET F frozen True WHERE F is Frozable')
+            cnx.commit()
+            cnx.execute('SET F name "Bar" WHERE F is Frozable')
+            with self.assertRaises(Unauthorized):
+                cnx.commit()
 
     def test_attribute_security_rqlexpr(self):
         with self.admin_access.repo_cnx() as cnx:
--- a/server/test/unittest_session.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/server/test/unittest_session.py	Thu Sep 25 15:49:13 2014 +0200
@@ -18,6 +18,8 @@
 
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.server.session import HOOKS_ALLOW_ALL, HOOKS_DENY_ALL
+from cubicweb.server import hook
+from cubicweb.predicates import is_instance
 
 class InternalSessionTC(CubicWebTC):
     def test_dbapi_query(self):
@@ -76,7 +78,7 @@
         self.assertEqual(set(), session.disabled_hook_categories)
         self.assertEqual(set(), session.enabled_hook_categories)
 
-    def test_explicite_connection(self):
+    def test_explicit_connection(self):
         with self.session.new_cnx() as cnx:
             rset = cnx.execute('Any X LIMIT 1 WHERE X is CWUser')
             self.assertEqual(1, len(rset))
@@ -98,7 +100,24 @@
             self.assertIsNotNone(new_user.login)
         self.assertFalse(cnx._open)
 
-
+    def test_connection_exit(self):
+        """exiting a connection should roll back the transaction, including any
+        pending operations"""
+        self.rollbacked = False
+        class RollbackOp(hook.Operation):
+            _test = self
+            def rollback_event(self):
+                self._test.rollbacked = True
+        class RollbackHook(hook.Hook):
+            __regid__ = 'rollback'
+            events = ('after_update_entity',)
+            __select__ = hook.Hook.__select__ & is_instance('CWGroup')
+            def __call__(self):
+                RollbackOp(self._cw)
+        with self.temporary_appobjects(RollbackHook):
+            with self.admin_access.client_cnx() as cnx:
+                cnx.execute('SET G name "foo" WHERE G is CWGroup, G name "managers"')
+            self.assertTrue(self.rollbacked)
 
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
--- a/test/data/schema.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/test/data/schema.py	Thu Sep 25 15:49:13 2014 +0200
@@ -89,3 +89,8 @@
 
 class StateFull(WorkflowableEntityType):
     name = String()
+
+
+class Reference(EntityType):
+    nom = String(unique=True)
+    ean = String(unique=True, required=True)
--- a/test/unittest_entity.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/test/unittest_entity.py	Thu Sep 25 15:49:13 2014 +0200
@@ -754,6 +754,11 @@
             # unique attr with None value (nom in this case)
             friend = req.create_entity('Ami', prenom=u'bob')
             self.assertEqual(friend.rest_path(), unicode(friend.eid))
+            # 'ref' below is created without the unique but not required
+            # attribute, make sur that the unique _and_ required 'ean' is used
+            # as the rest attribute
+            ref = req.create_entity('Reference', ean=u'42-1337-42')
+            self.assertEqual(ref.rest_path(), 'reference/42-1337-42')
 
     def test_can_use_rest_path(self):
         self.assertTrue(can_use_rest_path(u'zobi'))
--- a/test/unittest_schema.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/test/unittest_schema.py	Thu Sep 25 15:49:13 2014 +0200
@@ -171,7 +171,7 @@
                              'CWUniqueTogetherConstraint', 'CWUser',
                              'ExternalUri', 'File', 'Float', 'Int', 'Interval', 'Note',
                              'Password', 'Personne', 'Produit',
-                             'RQLExpression',
+                             'RQLExpression', 'Reference',
                              'Service', 'Societe', 'State', 'StateFull', 'String', 'SubNote', 'SubWorkflowExitPoint',
                              'Tag', 'TZDatetime', 'TZTime', 'Time', 'Transition', 'TrInfo',
                              'Usine',
@@ -191,7 +191,7 @@
                               'data', 'data_encoding', 'data_format', 'data_name', 'default_workflow', 'defaultval', 'delete_permission',
                               'description', 'description_format', 'destination_state', 'dirige',
 
-                              'ecrit_par', 'eid', 'end_timestamp', 'evaluee', 'expression', 'exprtype', 'extra_props',
+                              'ean', 'ecrit_par', 'eid', 'end_timestamp', 'evaluee', 'expression', 'exprtype', 'extra_props',
 
                               'fabrique_par', 'final', 'firstname', 'for_user', 'formula', 'fournit',
                               'from_entity', 'from_state', 'fulltext_container', 'fulltextindexed',
@@ -503,6 +503,7 @@
                      ('cw_source', 'Personne', 'CWSource', 'object'),
                      ('cw_source', 'Produit', 'CWSource', 'object'),
                      ('cw_source', 'RQLExpression', 'CWSource', 'object'),
+                     ('cw_source', 'Reference', 'CWSource', 'object'),
                      ('cw_source', 'Service', 'CWSource', 'object'),
                      ('cw_source', 'Societe', 'CWSource', 'object'),
                      ('cw_source', 'State', 'CWSource', 'object'),
--- a/uilib.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/uilib.py	Thu Sep 25 15:49:13 2014 +0200
@@ -444,12 +444,14 @@
 
 def exc_message(ex, encoding):
     try:
-        return unicode(ex)
+        excmsg = unicode(ex)
     except Exception:
         try:
-            return unicode(str(ex), encoding, 'replace')
+            excmsg = unicode(str(ex), encoding, 'replace')
         except Exception:
-            return unicode(repr(ex), encoding, 'replace')
+            excmsg = unicode(repr(ex), encoding, 'replace')
+    exctype = unicode(ex.__class__.__name__)
+    return u'%s: %s' % (exctype, excmsg)
 
 
 def rest_traceback(info, exception):
--- a/view.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/view.py	Thu Sep 25 15:49:13 2014 +0200
@@ -536,14 +536,6 @@
     __registry__ = 'adapters'
 
 
-class auto_unwrap_bw_compat(type):
-    def __new__(mcs, name, bases, classdict):
-        cls = type.__new__(mcs, name, bases, classdict)
-        if not classdict.get('__needs_bw_compat__'):
-            unwrap_adapter_compat(cls)
-        return cls
-
-
 class EntityAdapter(Adapter):
     """base class for entity adapters (eg adapt an entity to an interface)"""
     def __init__(self, _cw, **kwargs):
--- a/web/_exceptions.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/web/_exceptions.py	Thu Sep 25 15:49:13 2014 +0200
@@ -101,7 +101,7 @@
     """raised when a json remote call fails
     """
     def __init__(self, reason='', status=httplib.INTERNAL_SERVER_ERROR):
-        super(RemoteCallFailed, self).__init__(status=status)
+        super(RemoteCallFailed, self).__init__(reason, status=status)
         self.reason = reason
 
     def dumps(self):
--- a/web/component.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/web/component.py	Thu Sep 25 15:49:13 2014 +0200
@@ -513,12 +513,12 @@
         """builds HTML link to edit relation between `entity` and `etarget`"""
         args = {role(self) : entity.eid, target(self): etarget.eid}
         # for each target, provide a link to edit the relation
-        jscall = unicode(js.cw.utils.callAddOrDeleteThenReload(fname,
-                                                               self.rtype,
-                                                               args['subject'],
-                                                               args['object']))
+        jscall = js.cw.utils.callAjaxFuncThenReload(fname,
+                                                    self.rtype,
+                                                    args['subject'],
+                                                    args['object'])
         return u'[<a href="javascript: %s" class="action">%s</a>] %s' % (
-            xml_escape(jscall), label, etarget.view('incontext'))
+            xml_escape(unicode(jscall)), label, etarget.view('incontext'))
 
     def related_boxitems(self, entity):
         return [self.box_item(entity, etarget, 'delete_relation', u'-')
--- a/web/data/cubicweb.form.css	Tue Sep 23 17:34:36 2014 +0200
+++ b/web/data/cubicweb.form.css	Thu Sep 25 15:49:13 2014 +0200
@@ -164,7 +164,7 @@
   font-weight: bold;
 }
  
-div.pendingDelete {
+.pendingDelete {
   text-decoration: line-through;
 }
 
--- a/web/data/cubicweb.js	Tue Sep 23 17:34:36 2014 +0200
+++ b/web/data/cubicweb.js	Thu Sep 25 15:49:13 2014 +0200
@@ -339,14 +339,14 @@
 		    );
     },
 
-    callAddOrDeleteThenReload: function (add_or_delete, rtype, subjeid, objeid) {
-        var d = asyncRemoteExec(add_or_delete, rtype, subjeid, objeid);
-        d.addCallback(function() {
+    callAjaxFuncThenReload: function callAjaxFuncThenReload (/*...*/) {
+        var d = asyncRemoteExec.apply(null, arguments);
+        d.addCallback(function(msg) {
             window.location.reload();
+            if (msg)
+                updateMessage(msg);
         });
     }
-
-
 });
 
 /** DOM factories ************************************************************/
--- a/web/facet.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/web/facet.py	Thu Sep 25 15:49:13 2014 +0200
@@ -1620,9 +1620,11 @@
             cssclass += ' hideFacetBody'
         w(u'<div class="%s" cubicweb:facetName="%s">%s</div>\n' %
                (cssclass, xml_escape(self.facet.__regid__), title))
+        w(u'<div class="facetBody">\n')
         w(u'<input name="%s" type="text" value="%s" />\n' % (
                 xml_escape(self.facet.__regid__), self.value or u''))
         w(u'</div>\n')
+        w(u'</div>\n')
 
 
 class FacetRangeWidget(htmlwidgets.HTMLWidget):
--- a/web/http_headers.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/web/http_headers.py	Thu Sep 25 15:49:13 2014 +0200
@@ -1567,7 +1567,7 @@
     'Server': (unique, str, singleHeader),
     'Set-Cookie': (generateSetCookie,),
     'Set-Cookie2': (generateSetCookie2,),
-    'Vary': (generateList, singleHeader),
+    'Vary': (set, generateList, singleHeader),
     'WWW-Authenticate': (generateWWWAuthenticate,)
 }
 
--- a/web/test/data/schema.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/web/test/data/schema.py	Thu Sep 25 15:49:13 2014 +0200
@@ -43,7 +43,11 @@
 class Personne(EntityType):
     nom    = String(fulltextindexed=True, required=True, maxsize=64)
     prenom = String(fulltextindexed=True, maxsize=64)
-    sexe   = String(maxsize=1, default='M')
+    sexe   = String(maxsize=1, default='M',
+                    __permissions__={
+                        'read': ('managers', 'users', 'guests',),
+                        'add': ('managers', 'users'),
+                        'update': ('managers', )})
     promo  = String(vocabulary=('bon','pasbon'))
     titre  = String(fulltextindexed=True, maxsize=128)
     ass    = String(maxsize=128)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_views_csv.py	Thu Sep 25 15:49:13 2014 +0200
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+# 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 CSVExportViewsTC(CubicWebTC):
+
+    def test_csvexport(self):
+        with self.admin_access.web_request() as req:
+            rset = req.execute('Any GN,COUNT(X) GROUPBY GN ORDERBY GN '
+                               'WHERE X in_group G, G name GN')
+            data = self.view('csvexport', rset, req=req)
+            self.assertEqual(req.headers_out.getRawHeaders('content-type'),
+                             ['text/comma-separated-values;charset=UTF-8'])
+            expected_data = "String;COUNT(CWUser)\nguests;1\nmanagers;1"
+            self.assertMultiLineEqual(expected_data, data)
+
+    def test_csvexport_on_empty_rset(self):
+        """Should return the CSV header.
+        """
+        with self.admin_access.web_request() as req:
+            rset = req.execute('Any GN,COUNT(X) GROUPBY GN ORDERBY GN '
+                               'WHERE X in_group G, G name GN, X login "Miles"')
+            data = self.view('csvexport', rset, req=req)
+            self.assertEqual(req.headers_out.getRawHeaders('content-type'),
+                             ['text/comma-separated-values;charset=UTF-8'])
+            expected_data = "String;COUNT(CWUser)"
+            self.assertMultiLineEqual(expected_data, data)
+
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
--- a/web/test/unittest_views_editforms.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/web/test/unittest_views_editforms.py	Thu Sep 25 15:49:13 2014 +0200
@@ -148,6 +148,26 @@
             self.vreg['forms'].select('edition', req, entity=rset.get_entity(0, 0))
             self.assertFalse(any(f for f in form.fields if f is None))
 
+    def test_attribute_add_permissions(self):
+        # https://www.cubicweb.org/ticket/4342844
+        with self.admin_access.repo_cnx() as cnx:
+            self.create_user(cnx, 'toto')
+            cnx.commit()
+        with self.new_access('toto').web_request() as req:
+            e = self.vreg['etypes'].etype_class('Personne')(req)
+            cform = self.vreg['forms'].select('edition', req, entity=e)
+            self.assertIn('sexe',
+                          [rschema.type
+                           for rschema, _ in cform.editable_attributes()])
+            with self.new_access('toto').repo_cnx() as cnx:
+                person_eid = cnx.create_entity('Personne', nom=u'Robert').eid
+                cnx.commit()
+            person = req.entity_from_eid(person_eid)
+            mform = self.vreg['forms'].select('edition', req, entity=person)
+            self.assertNotIn('sexe',
+                             [rschema.type
+                              for rschema, _ in mform.editable_attributes()])
+
 
 class FormViewsTC(CubicWebTC):
 
--- a/web/test/unittest_viewselector.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/web/test/unittest_viewselector.py	Thu Sep 25 15:49:13 2014 +0200
@@ -107,8 +107,11 @@
     def test_possible_views_noresult(self):
         with self.admin_access.web_request() as req:
             rset = req.execute('Any X WHERE X eid 999999')
-            self.assertListEqual([('jsonexport', json.JsonRsetView)],
-                                 self.pviews(req, rset))
+            self.assertListEqual(self.pviews(req, rset),
+                    [('csvexport', csvexport.CSVRsetView),
+                     ('ecsvexport', csvexport.CSVEntityView),
+                     ('jsonexport', json.JsonRsetView),
+                     ])
 
     def test_possible_views_one_egroup(self):
         with self.admin_access.web_request() as req:
@@ -214,6 +217,7 @@
             rset = req.execute('Any N, X WHERE X in_group Y, Y name N')
             self.assertListEqual(self.pviews(req, rset),
                                  [('csvexport', csvexport.CSVRsetView),
+                                  ('ecsvexport', csvexport.CSVEntityView),
                                   ('jsonexport', json.JsonRsetView),
                                   ('rsetxml', xmlrss.XMLRsetView),
                                   ('table', tableview.RsetTableView),
--- a/web/views/autoform.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/web/views/autoform.py	Thu Sep 25 15:49:13 2014 +0200
@@ -844,9 +844,9 @@
             return [(schema[rtype], role) for rtype, role in self.display_fields]
         if self.edited_entity.has_eid() and not self.edited_entity.cw_has_perm('update'):
             return []
-        # XXX we should simply put eid in the generated section, no?
+        action = 'update' if self.edited_entity.has_eid() else 'add'
         return [(rtype, role) for rtype, _, role in self._relations_by_section(
-            'attributes', 'update', strict)]
+            'attributes', action, strict)]
 
     def editable_relations(self):
         """return a sorted list of (relation's label, relation'schema, role) for
@@ -988,7 +988,7 @@
 
 _AFS = uicfg.autoform_section
 # use primary and not generated for eid since it has to be an hidden
-_AFS.tag_attribute(('*', 'eid'), 'main', 'attributes')
+_AFS.tag_attribute(('*', 'eid'), 'main', 'hidden')
 _AFS.tag_attribute(('*', 'eid'), 'muledit', 'attributes')
 _AFS.tag_attribute(('*', 'description'), 'main', 'attributes')
 _AFS.tag_attribute(('*', 'has_text'), 'main', 'hidden')
--- a/web/views/csvexport.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/web/views/csvexport.py	Thu Sep 25 15:49:13 2014 +0200
@@ -21,6 +21,7 @@
 _ = unicode
 
 from cubicweb.schema import display_name
+from cubicweb.predicates import any_rset
 from cubicweb.uilib import UnicodeCSVWriter
 from cubicweb.view import EntityView, AnyRsetView
 
@@ -47,6 +48,7 @@
 class CSVRsetView(CSVMixIn, AnyRsetView):
     """dumps raw result set in CSV"""
     __regid__ = 'csvexport'
+    __select__ = any_rset()
     title = _('csv export')
 
     def call(self):
@@ -78,6 +80,7 @@
     contents)
     """
     __regid__ = 'ecsvexport'
+    __select__ = any_rset()
     title = _('csv export (entities)')
 
     def call(self):
--- a/web/views/uicfg.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/web/views/uicfg.py	Thu Sep 25 15:49:13 2014 +0200
@@ -330,7 +330,7 @@
             assert section in ('attributes', 'metadata', 'hidden')
             relpermission = 'add'
         else:
-            assert section not in ('attributes', 'metadata', 'hidden')
+            assert section not in ('metadata', 'hidden')
             relpermission = permission
         for rschema, targetschemas, role in eschema.relation_definitions(True):
             _targetschemas = []
--- a/wsgi/request.py	Tue Sep 23 17:34:36 2014 +0200
+++ b/wsgi/request.py	Thu Sep 25 15:49:13 2014 +0200
@@ -25,9 +25,12 @@
 
 __docformat__ = "restructuredtext en"
 
+import tempfile
+
 from StringIO import StringIO
 from urllib import quote
 from urlparse import parse_qs
+from warnings import warn
 
 from cubicweb.multipart import copy_file, parse_form_data
 from cubicweb.web.request import CubicWebRequestBase
@@ -39,6 +42,10 @@
     """
 
     def __init__(self, environ, vreg):
+        # self.vreg is used in get_posted_data, which is called before the
+        # parent constructor.
+        self.vreg = vreg
+
         self.environ = environ
         self.path = environ['PATH_INFO']
         self.method = environ['REQUEST_METHOD'].upper()
@@ -59,11 +66,19 @@
 
         headers_in = dict((normalize_header(k[5:]), v) for k, v in self.environ.items()
                           if k.startswith('HTTP_'))
+        if 'CONTENT_TYPE' in environ:
+            headers_in['Content-Type'] = environ['CONTENT_TYPE']
         https = self.is_secure()
+        if self.path.startswith('/https/'):
+            self.path = self.path[6:]
+            self.environ['PATH_INFO'] = self.path
+            https = True
+
         post, files = self.get_posted_data()
 
         super(CubicWebWsgiRequest, self).__init__(vreg, https, post,
                                                   headers= headers_in)
+        self.content = environ['wsgi.input']
         if files is not None:
             for key, part in files.iteritems():
                 name = None
@@ -114,6 +129,38 @@
         if self.method == 'POST':
             forms, files = parse_form_data(self.environ, strict=True,
                                            mem_limit=self.vreg.config['max-post-length'])
-            post.update(forms)
+            post.update(forms.dict)
         self.content.seek(0, 0)
         return post, files
+
+    def setup_params(self, params):
+        # This is a copy of CubicWebRequestBase.setup_params, but without
+        # converting unicode strings because it is partially done by
+        # get_posted_data
+        self.form = {}
+        if params is None:
+            return
+        encoding = self.encoding
+        for param, val in params.iteritems():
+            if isinstance(val, (tuple, list)):
+                val = [
+                    unicode(x, encoding) if isinstance(x, str) else x
+                    for x in val]
+                if len(val) == 1:
+                    val = val[0]
+            elif isinstance(val, str):
+                val = unicode(val, encoding)
+            if param in self.no_script_form_params and val:
+                val = self.no_script_form_param(param, val)
+            if param == '_cwmsgid':
+                self.set_message_id(val)
+            elif param == '__message':
+                warn('[3.13] __message in request parameter is deprecated (may '
+                     'only be given to .build_url). Seeing this message usualy '
+                     'means your application hold some <form> where you should '
+                     'replace use of __message hidden input by form.set_message, '
+                     'so new _cwmsgid mechanism is properly used',
+                     DeprecationWarning)
+                self.set_message(val)
+            else:
+                self.form[param] = val
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wsgi/test/unittest_wsgi.py	Thu Sep 25 15:49:13 2014 +0200
@@ -0,0 +1,92 @@
+# encoding=utf-8
+
+import webtest.app
+from StringIO import StringIO
+
+from cubicweb.devtools.webtest import CubicWebTestTC
+
+from cubicweb.wsgi.request import CubicWebWsgiRequest
+
+
+class WSGIAppTC(CubicWebTestTC):
+    def test_content_type(self):
+        r = webtest.app.TestRequest.blank('/', {'CONTENT_TYPE': 'text/plain'})
+
+        req = CubicWebWsgiRequest(r.environ, self.vreg)
+
+        self.assertEqual('text/plain', req.get_header('Content-Type'))
+
+    def test_content_body(self):
+        r = webtest.app.TestRequest.blank('/', {
+            'CONTENT_LENGTH': 12,
+            'CONTENT_TYPE': 'text/plain',
+            'wsgi.input': StringIO('some content')})
+
+        req = CubicWebWsgiRequest(r.environ, self.vreg)
+
+        self.assertEqual('some content', req.content.read())
+
+    def test_http_scheme(self):
+        r = webtest.app.TestRequest.blank('/', {
+            'wsgi.url_scheme': 'http'})
+
+        req = CubicWebWsgiRequest(r.environ, self.vreg)
+
+        self.assertFalse(req.https)
+
+    def test_https_scheme(self):
+        r = webtest.app.TestRequest.blank('/', {
+            'wsgi.url_scheme': 'https'})
+
+        req = CubicWebWsgiRequest(r.environ, self.vreg)
+
+        self.assertTrue(req.https)
+
+    def test_https_prefix(self):
+        r = webtest.app.TestRequest.blank('/https/', {
+            'wsgi.url_scheme': 'http'})
+
+        req = CubicWebWsgiRequest(r.environ, self.vreg)
+
+        self.assertTrue(req.https)
+
+    def test_big_content(self):
+        content = 'x'*100001
+        r = webtest.app.TestRequest.blank('/', {
+            'CONTENT_LENGTH': len(content),
+            'CONTENT_TYPE': 'text/plain',
+            'wsgi.input': StringIO(content)})
+
+        req = CubicWebWsgiRequest(r.environ, self.vreg)
+
+        self.assertEqual(content, req.content.read())
+
+    def test_post(self):
+        self.webapp.post(
+            '/',
+            params={'__login': self.admlogin, '__password': self.admpassword})
+
+    def test_get_multiple_variables(self):
+        r = webtest.app.TestRequest.blank('/?arg=1&arg=2')
+        req = CubicWebWsgiRequest(r.environ, self.vreg)
+
+        self.assertEqual([u'1', u'2'], req.form['arg'])
+
+    def test_post_multiple_variables(self):
+        r = webtest.app.TestRequest.blank('/', POST='arg=1&arg=2')
+        req = CubicWebWsgiRequest(r.environ, self.vreg)
+
+        self.assertEqual([u'1', u'2'], req.form['arg'])
+
+    def test_post_unicode_urlencoded(self):
+        params = 'arg=%C3%A9'
+        r = webtest.app.TestRequest.blank(
+            '/', POST=params, content_type='application/x-www-form-urlencoded')
+        req = CubicWebWsgiRequest(r.environ, self.vreg)
+        self.assertEqual(u"é", req.form['arg'])
+
+    @classmethod
+    def init_config(cls, config):
+        super(WSGIAppTC, cls).init_config(config)
+        config.https_uiprops = None
+        config.https_datadir_url = None