[merge] backport stable fixes
authorAurelien Campeas <aurelien.campeas@logilab.fr>
Thu, 10 Oct 2013 13:26:11 +0200
changeset 9283 5f2c5eb1a820
parent 9265 614762cdc357 (current diff)
parent 9282 1709dd30387c (diff)
child 9284 4c37808fd284
[merge] backport stable fixes
debian/control
doc/book/en/admin/setup.rst
entity.py
schema.py
server/hook.py
server/session.py
server/test/unittest_ldapsource.py
test/unittest_entity.py
web/formfields.py
--- a/.hgtags	Wed Oct 09 11:13:56 2013 +0200
+++ b/.hgtags	Thu Oct 10 13:26:11 2013 +0200
@@ -311,3 +311,5 @@
 909eb8b584c437b3d2580beff1325c3d5b5dcfb5 cubicweb-centos-version-3.17.8-1
 909eb8b584c437b3d2580beff1325c3d5b5dcfb5 cubicweb-version-3.17.8
 909eb8b584c437b3d2580beff1325c3d5b5dcfb5 cubicweb-debian-version-3.17.8-1
+5668d210e49c910180ff27712b6ae9ce8286e06c cubicweb-version-3.17.9
+5668d210e49c910180ff27712b6ae9ce8286e06c cubicweb-debian-version-3.17.9-1
--- a/__pkginfo__.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/__pkginfo__.py	Thu Oct 10 13:26:11 2013 +0200
@@ -22,7 +22,7 @@
 
 modname = distname = "cubicweb"
 
-numversion = (3, 17, 8)
+numversion = (3, 17, 9)
 version = '.'.join(str(num) for num in numversion)
 
 description = "a repository of entities / relations for knowledge management"
--- a/debian/changelog	Wed Oct 09 11:13:56 2013 +0200
+++ b/debian/changelog	Thu Oct 10 13:26:11 2013 +0200
@@ -1,3 +1,9 @@
+cubicweb (3.17.9-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Julien Cristau <julien.cristau@logilab.fr>  Tue, 08 Oct 2013 17:57:04 +0200
+
 cubicweb (3.17.8-1) unstable; urgency=low
 
   * new upstream release
--- a/debian/control	Wed Oct 09 11:13:56 2013 +0200
+++ b/debian/control	Thu Oct 10 13:26:11 2013 +0200
@@ -12,7 +12,7 @@
  python (>= 2.6),
  python-sphinx,
  python-logilab-common,
- python-unittest2,
+ python-unittest2 | python (>= 2.7),
  python-logilab-mtconverter,
  python-rql,
  python-yams (>= 0.37),
--- a/doc/book/en/admin/setup.rst	Wed Oct 09 11:13:56 2013 +0200
+++ b/doc/book/en/admin/setup.rst	Thu Oct 10 13:26:11 2013 +0200
@@ -131,25 +131,40 @@
 `pip` install
 -------------
 
-pip_ is a python utility that helps downloading, building, installing, and
-managing python packages and their dependencies. It is fully compatible with
-`virtualenv`_ and installs the packages from sources published on the
-`The Python Package Index`_.
+`pip <http://pip.openplans.org/>`_ is a python tool that helps downloading,
+building, installing, and managing Python packages and their dependencies. It
+is fully compatible with `virtualenv`_ and installs the packages from sources
+published on the `The Python Package Index`_.
 
-.. _`pip`: http://pip.openplans.org/
 .. _`virtualenv`: http://virtualenv.openplans.org/
 
 A working compilation chain is needed to build the modules that include C
-extensions. If you definitively wont, installing `Lxml <http://lxml.de/>`_,
+extensions. If you really do not want to compile anything, installing `Lxml <http://lxml.de/>`_,
 `Twisted Web <http://twistedmatrix.com/trac/wiki/Downloads/>`_ and `libgecode
 <http://www.gecode.org/>`_ will help.
 
-To install |cubicweb| and its dependencies, just run::
+For Debian, these minimal dependencies can be obtained by doing::
+
+  apt-get install gcc python-pip python-dev python-lxml
+
+or, if you prefer to get as much as possible from pip::
+
+  apt-get install gcc python-pip python-dev libxslt1-dev libxml2-dev
+
+For Windows, you can install pre-built packages (possible `source
+<http://www.lfd.uci.edu/~gohlke/pythonlibs/>`_). For a minimal setup, install
+`pip <http://www.lfd.uci.edu/~gohlke/pythonlibs/#pip>`_, `setuptools
+<http://www.lfd.uci.edu/~gohlke/pythonlibs/#setuptools>`_, `libxml-python
+<http://www.lfd.uci.edu/~gohlke/pythonlibs/#libxml-python>`_, `lxml
+<http://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml>`_ and `twisted
+<http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted>`_ from this source making
+sure to choose the correct architecture and version of Python.
+
+Finally, install |cubicweb| and its dependencies, by running::
 
   pip install cubicweb
 
-There is also a wide variety of :ref:`cubes <AvailableCubes>`. You can access a
-list of available cubes on
+Many other :ref:`cubes <AvailableCubes>` are available. A list is available at
 `PyPI <http://pypi.python.org/pypi?%3Aaction=search&term=cubicweb&submit=search>`_
 or at the `CubicWeb.org forge`_.
 
--- a/entity.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/entity.py	Thu Oct 10 13:26:11 2013 +0200
@@ -1307,7 +1307,8 @@
 
     @deprecated('[3.16] use cw_set() instead of set_attributes()')
     def set_attributes(self, **kwargs): # XXX cw_set_attributes
-        self.cw_set(**kwargs)
+        if kwargs:
+            self.cw_set(**kwargs)
 
     @deprecated('[3.16] use cw_set() instead of set_relations()')
     def set_relations(self, **kwargs): # XXX cw_set_relations
@@ -1318,7 +1319,8 @@
         (meaning that all relations of the given type from or to this object
         should be deleted).
         """
-        self.cw_set(**kwargs)
+        if kwargs:
+            self.cw_set(**kwargs)
 
     @deprecated('[3.13] use entity.cw_clear_all_caches()')
     def clear_all_caches(self):
--- a/hooks/__init__.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/hooks/__init__.py	Thu Oct 10 13:26:11 2013 +0200
@@ -59,7 +59,9 @@
         def update_feeds(repo):
             # don't iter on repo.sources which doesn't include copy based
             # sources (the one we're looking for)
-            for source in repo.sources_by_eid.itervalues():
+            # take a list to avoid iterating on a dictionary which size may
+            # change
+            for source in list(repo.sources_by_eid.values()):
                 if (not source.copy_based_source
                     or not repo.config.source_enabled(source)
                     or not source.config['synchronize']):
--- a/hooks/notification.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/hooks/notification.py	Thu Oct 10 13:26:11 2013 +0200
@@ -52,7 +52,7 @@
 
     All others Operations end up adding data to this Operation.
     The notification are done on ``postcommit_event`` to make sure to prevent
-    sending notification about rollbacked data.
+    sending notification about rolled back data.
     """
 
     containercls = list
--- a/hooks/test/unittest_integrity.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/hooks/test/unittest_integrity.py	Thu Oct 10 13:26:11 2013 +0200
@@ -41,9 +41,6 @@
         self.execute('SET X in_group Y WHERE X login "toto", Y name "guests"')
         self.commit()
 
-    def test_delete_required_relations_object(self):
-        self.skipTest('no sample in the schema ! YAGNI ? Kermaat ?')
-
     def test_static_vocabulary_check(self):
         self.assertRaises(ValidationError,
                           self.execute,
--- a/misc/scripts/ldapuser2ldapfeed.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/misc/scripts/ldapuser2ldapfeed.py	Thu Oct 10 13:26:11 2013 +0200
@@ -95,5 +95,5 @@
     commit()
 else:
     rollback()
-    print 'rollbacked'
+    print 'rolled back'
 
--- a/rset.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/rset.py	Thu Oct 10 13:26:11 2013 +0200
@@ -45,7 +45,7 @@
     :param rql: the original RQL query string
     """
 
-    def __init__(self, results, rql, args=None, description=(), rqlst=None):
+    def __init__(self, results, rql, args=None, description=None, rqlst=None):
         self.rows = results
         self.rowcount = results and len(results) or 0
         # original query and arguments
@@ -53,7 +53,7 @@
         self.args = args
         # entity types for each cell (same shape as rows)
         # maybe discarded if specified when the query has been executed
-        self.description = description
+        self.description = description or []
         # parsed syntax tree
         if rqlst is not None:
             rqlst.schema = None # reset schema in case of pyro transfert
--- a/schema.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/schema.py	Thu Oct 10 13:26:11 2013 +0200
@@ -25,7 +25,7 @@
 from logging import getLogger
 from warnings import warn
 
-from logilab.common.decorators import cached, clear_cache, monkeypatch
+from logilab.common.decorators import cached, clear_cache, monkeypatch, cachedproperty
 from logilab.common.logging_ext import set_log_methods
 from logilab.common.deprecation import deprecated, class_moved, moved
 from logilab.common.textutils import splitstrip
@@ -696,7 +696,7 @@
     # only defining here to prevent pylint from complaining
     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
     # to be defined in concrete classes
-    full_rql = None
+    rqlst = None
     predefined_variables = None
 
     def __init__(self, expression, mainvars, eid):
@@ -715,7 +715,7 @@
         self.mainvars = mainvars
         self.expression = normalize_expression(expression)
         try:
-            self.rqlst = parse(self.full_rql, print_errors=False).children[0]
+            self.full_rql = self.rqlst.as_string()
         except RQLSyntaxError:
             raise RQLSyntaxError(expression)
         for mainvar in mainvars:
@@ -730,6 +730,8 @@
                              'expression %s', mainvar, self)
         # syntax tree used by read security (inserted in queries when necessary)
         self.snippet_rqlst = parse(self.minimal_rql, print_errors=False).children[0]
+        # graph of links between variables, used by rql rewriter
+        self.vargraph = vargraph(self.rqlst)
 
     def __str__(self):
         return self.full_rql
@@ -756,6 +758,15 @@
     def __setstate__(self, state):
         self.__init__(*state)
 
+    @cachedproperty
+    def rqlst(self):
+        select = parse(self.minimal_rql, print_errors=False).children[0]
+        defined = set(split_expression(self.expression))
+        for varname in self.predefined_variables:
+            if varname in defined:
+                select.add_eid_restriction(select.get_variable(varname), varname.lower(), 'Substitute')
+        return select
+
     # permission rql expression specific stuff #################################
 
     @cached
@@ -874,25 +885,11 @@
 # rql expressions for use in permission definition #############################
 
 class ERQLExpression(RQLExpression):
-    predefined_variables = 'UX'
+    predefined_variables = 'XU'
 
     def __init__(self, expression, mainvars=None, eid=None):
         RQLExpression.__init__(self, expression, mainvars or 'X', eid)
 
-    @property
-    def full_rql(self):
-        rql = self.minimal_rql
-        rqlst = getattr(self, 'rqlst', None) # may be not set yet
-        if rqlst is not None:
-            defined = rqlst.defined_vars
-        else:
-            defined = set(split_expression(self.expression))
-        if 'X' in defined:
-            rql += ', X eid %(x)s'
-        if 'U' in defined:
-            rql += ', U eid %(u)s'
-        return rql
-
     def check(self, _cw, eid=None, creating=False, **kwargs):
         if 'X' in self.rqlst.defined_vars:
             if eid is None:
@@ -931,30 +928,12 @@
 
 
 class RRQLExpression(RQLExpression):
-    predefined_variables = 'USO'
+    predefined_variables = 'SOU'
 
     def __init__(self, expression, mainvars=None, eid=None):
         if mainvars is None:
             mainvars = guess_rrqlexpr_mainvars(expression)
         RQLExpression.__init__(self, expression, mainvars, eid)
-        # graph of links between variable, used by rql rewriter
-        self.vargraph = vargraph(self.rqlst)
-
-    @property
-    def full_rql(self):
-        rql = self.minimal_rql
-        rqlst = getattr(self, 'rqlst', None) # may be not set yet
-        if rqlst is not None:
-            defined = rqlst.defined_vars
-        else:
-            defined = set(split_expression(self.expression))
-        if 'S' in defined:
-            rql += ', S eid %(s)s'
-        if 'O' in defined:
-            rql += ', O eid %(o)s'
-        if 'U' in defined:
-            rql += ', U eid %(u)s'
-        return rql
 
     def check(self, _cw, fromeid=None, toeid=None):
         kwargs = {}
--- a/server/hook.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/server/hook.py	Thu Oct 10 13:26:11 2013 +0200
@@ -41,7 +41,7 @@
 defined over data events.
 
 Also, some :class:`~cubicweb.server.hook.Operation` may be registered by hooks,
-which will be fired when the transaction is commited or rollbacked.
+which will be fired when the transaction is commited or rolled back.
 
 The purpose of data event hooks is usually to complement the data model as
 defined in the schema, which is static by nature and only provide a restricted
@@ -705,10 +705,10 @@
 
     * `rollback`:
 
-      the transaction has been either rollbacked either:
+      the transaction has been either rolled back either:
 
        * intentionaly
-       * a 'precommit' event failed, in which case all operations are rollbacked
+       * a 'precommit' event failed, in which case all operations are rolled back
          once 'revertprecommit'' has been called
 
     * `postcommit`:
@@ -770,7 +770,7 @@
         """
 
     def rollback_event(self):
-        """the observed connections set has been rollbacked
+        """the observed connections set has been rolled back
 
         do nothing by default
         """
@@ -1034,7 +1034,7 @@
     type/source cache eids of entities added in that transaction.
 
     NOTE: querier's rqlst/solutions cache may have been polluted too with
-    queries such as Any X WHERE X eid 32 if 32 has been rollbacked however
+    queries such as Any X WHERE X eid 32 if 32 has been rolled back however
     generated queries are unpredictable and analysing all the cache probably
     too expensive. Notice that there is no pb when using args to specify eids
     instead of giving them into the rql string.
@@ -1042,7 +1042,7 @@
     data_key = 'neweids'
 
     def rollback_event(self):
-        """the observed connections set has been rollbacked,
+        """the observed connections set has been rolled back,
         remove inserted eid from repository type/source cache
         """
         try:
@@ -1056,7 +1056,7 @@
     """
     data_key = 'pendingeids'
     def postcommit_event(self):
-        """the observed connections set has been rollbacked,
+        """the observed connections set has been rolled back,
         remove inserted eid from repository type/source cache
         """
         try:
--- a/server/querier.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/server/querier.py	Thu Oct 10 13:26:11 2013 +0200
@@ -632,7 +632,7 @@
             results = plan.execute()
         except (Unauthorized, ValidationError):
             # getting an Unauthorized/ValidationError exception means the
-            # transaction must been rollbacked
+            # transaction must be rolled back
             #
             # notes:
             # * we should not reset the connections set here, since we don't want the
--- a/server/repository.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/server/repository.py	Thu Oct 10 13:26:11 2013 +0200
@@ -407,7 +407,7 @@
             return self._cnxsets_pool.get(True, timeout=5)
         except Queue.Empty:
             raise Exception('no connections set available after 5 secs, probably either a '
-                            'bug in code (too many uncommited/rollbacked '
+                            'bug in code (too many uncommited/rolled back '
                             'connections) or too much load on the server (in '
                             'which case you can try to set a bigger '
                             'connections pool size)')
@@ -868,7 +868,7 @@
         """close the session with the given id"""
         session = self._get_session(sessionid, setcnxset=True, txid=txid,
                                     checkshuttingdown=checkshuttingdown)
-        # operation uncommited before close are rollbacked before hook is called
+        # operation uncommited before close are rolled back before hook is called
         session.rollback(free_cnxset=False)
         self.hm.call_hooks('session_close', session)
         # commit session at this point in case write operation has been done
@@ -1009,7 +1009,7 @@
 
     def _get_session(self, sessionid, setcnxset=False, txid=None,
                      checkshuttingdown=True):
-        """return the user associated to the given session identifier"""
+        """return the session associated with the given session identifier"""
         if checkshuttingdown and self.shutting_down:
             raise ShuttingDown('Repository is shutting down')
         try:
--- a/server/session.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/server/session.py	Thu Oct 10 13:26:11 2013 +0200
@@ -67,7 +67,7 @@
 
 
 class transaction(object):
-    """Ensure that the transaction is either commited or rollbacked at exit
+    """Ensure that the transaction is either commited or rolled back at exit
 
     Context manager to enter a transaction for a session: when exiting the
     `with` block on exception, call `session.rollback()`, else call
@@ -201,18 +201,17 @@
 class CnxSetTracker(object):
     """Keep track of which transaction use which cnxset.
 
-    There should be one of this object per session plus one another for
-    internal session.
+    There should be one of these object per session (including internal sessions).
 
-    Session object are responsible of creating their CnxSetTracker object.
+    Session objects are responsible of creating their CnxSetTracker object.
 
-    Transaction should use the :meth:`record` and :meth:`forget` to inform the
-    tracker of cnxset they have acquired.
+    Transactions should use the :meth:`record` and :meth:`forget` to inform the
+    tracker of cnxsets they have acquired.
 
     .. automethod:: cubicweb.server.session.CnxSetTracker.record
     .. automethod:: cubicweb.server.session.CnxSetTracker.forget
 
-    Session use the :meth:`close` and :meth:`wait` method when closing.
+    Sessions use the :meth:`close` and :meth:`wait` methods when closing.
 
     .. automethod:: cubicweb.server.session.CnxSetTracker.close
     .. automethod:: cubicweb.server.session.CnxSetTracker.wait
@@ -233,12 +232,12 @@
         return self._condition.__exit__(*args)
 
     def record(self, txid, cnxset):
-        """Inform the tracker that a txid have acquired a cnxset
+        """Inform the tracker that a txid has acquired a cnxset
 
-        This methode is to be used by Transaction object.
+        This method is to be used by Transaction objects.
 
         This method fails when:
-        - The txid already have a recorded cnxset.
+        - The txid already has a recorded cnxset.
         - The tracker is not active anymore.
 
         Notes about the caller:
@@ -246,7 +245,7 @@
         (2) It must be prepared to release the cnxset if the
             `cnxsettracker.forget` call fails.
         (3) It should acquire the tracker lock until the very end of the operation.
-        (4) However It take care to lock the CnxSetTracker object after having
+        (4) However it must only lock the CnxSetTracker object after having
             retrieved the cnxset to prevent deadlock.
 
         A typical usage look like::
@@ -261,13 +260,13 @@
             repo._free_cnxset(cnxset) # (2)
             raise
         """
-        # dubious since the caller is suppose to have acquired it anyway.
+        # dubious since the caller is supposed to have acquired it anyway.
         with self._condition:
             if not self._active:
                 raise SessionClosedError('Closed')
             old = self._record.get(txid)
             if old is not None:
-                raise ValueError('"%s" already have a cnx_set (%r)'
+                raise ValueError('transaction "%s" already has a cnx_set (%r)'
                                  % (txid, old))
             self._record[txid] = cnxset
 
@@ -307,19 +306,19 @@
     def close(self):
         """Marks the tracker as inactive.
 
-        This methode is to be used by Session object.
+        This method is to be used by Session objects.
 
-        Inactive tracker does not accept new record anymore.
+        An inactive tracker does not accept new records anymore.
         """
         with self._condition:
             self._active = False
 
     def wait(self, timeout=10):
-        """Wait for all recorded cnxset to be released
+        """Wait for all recorded cnxsets to be released
 
-        This methode is to be used by Session object.
+        This method is to be used by Session objects.
 
-        returns a tuple of transaction id that remains open.
+        Returns a tuple of transaction ids that remain open.
         """
         with self._condition:
             if  self._active:
@@ -336,15 +335,15 @@
 
     Holds all transaction related data
 
-    Database connections resource:
+    Database connection resources:
 
       :attr:`running_dbapi_query`, boolean flag telling if the executing query
       is coming from a dbapi connection or is a query from within the repository
 
       :attr:`cnxset`, the connections set to use to execute queries on sources.
       If the transaction is read only, the connection set may be freed between
-      actual query. This allows multiple transaction with a reasonable low
-      connection set pool size. control mechanism is detailed below
+      actual queries. This allows multiple transactions with a reasonably low
+      connection set pool size.  Control mechanism is detailed below.
 
     .. automethod:: cubicweb.server.session.Transaction.set_cnxset
     .. automethod:: cubicweb.server.session.Transaction.free_cnxset
@@ -357,7 +356,7 @@
 
     Internal transaction data:
 
-      :attr:`data`,is a dictionary containing some shared data
+      :attr:`data` is a dictionary containing some shared data
       cleared at the end of the transaction. Hooks and operations may put
       arbitrary data in there, and this may also be used as a communication
       channel between the client and the repository.
@@ -369,7 +368,7 @@
       of None (not yet committing), 'precommit' (calling precommit event on
       operations), 'postcommit' (calling postcommit event on operations),
       'uncommitable' (some :exc:`ValidationError` or :exc:`Unauthorized` error
-      has been raised during the transaction and so it must be rollbacked).
+      has been raised during the transaction and so it must be rolled back).
 
     Hooks controls:
 
@@ -439,7 +438,6 @@
     def transaction_data(self):
         return self.data
 
-
     def clear(self):
         """reset internal data"""
         self.data = {}
@@ -448,6 +446,7 @@
         #: (None, 'precommit', 'postcommit', 'uncommitable')
         self.commit_state = None
         self.pruned_hooks_cache = {}
+
     # Connection Set Management ###############################################
     @property
     def cnxset(self):
@@ -499,7 +498,7 @@
 
     # Entity cache management #################################################
     #
-    # The transaction entity cache as held in tx.data it is removed at end the
+    # The transaction entity cache as held in tx.data is removed at the
     # end of the transaction (commit and rollback)
     #
     # XXX transaction level caching may be a pb with multiple repository
@@ -529,9 +528,7 @@
         else:
             del self.data['ecache'][eid]
 
-    # Tracking of entity added of removed in the transaction ##################
-    #
-    # Those are function to  allows cheap call from client in other process.
+    # Tracking of entities added of removed in the transaction ##################
 
     def deleted_in_transaction(self, eid):
         """return True if the entity of the given eid is being deleted in the
@@ -652,6 +649,7 @@
         num = self.data.setdefault('tx_action_count', 0) + 1
         self.data['tx_action_count'] = num
         return num
+
     # db-api like interface ###################################################
 
     def source_defs(self):
@@ -662,10 +660,9 @@
         metas = self.repo.type_and_source_from_eid(eid, self)
         if asdict:
             return dict(zip(('type', 'source', 'extid', 'asource'), metas))
-       # XXX :-1 for cw compat, use asdict=True for full information
+        # XXX :-1 for cw compat, use asdict=True for full information
         return metas[:-1]
 
-
     def source_from_eid(self, eid):
         """return the source where the entity with id <eid> is located"""
         return self.repo.source_from_eid(eid, self)
@@ -786,7 +783,7 @@
       of None (not yet committing), 'precommit' (calling precommit event on
       operations), 'postcommit' (calling postcommit event on operations),
       'uncommitable' (some :exc:`ValidationError` or :exc:`Unauthorized` error
-      has been raised during the transaction and so it must be rollbacked).
+      has been raised during the transaction and so it must be rolled back).
 
     .. automethod:: cubicweb.server.session.Session.commit
     .. automethod:: cubicweb.server.session.Session.rollback
@@ -899,7 +896,7 @@
         call `session.commit()` on normal exit.
 
         The `free_cnxset` will be given to rollback/commit methods to indicate
-        wether the connections set should be freed or not.
+        whether the connections set should be freed or not.
         """
         return transaction(self, free_cnxset)
 
@@ -1230,7 +1227,7 @@
             return
         cstate = self.commit_state
         if cstate == 'uncommitable':
-            raise QueryError('transaction must be rollbacked')
+            raise QueryError('transaction must be rolled back')
         if cstate is not None:
             return
         # on rollback, an operation should have the following state
@@ -1345,7 +1342,7 @@
             self._closed = True
         tracker.close()
         self.rollback()
-        self.info('waiting for open transaction of session: %s', self)
+        self.debug('waiting for open transaction of session: %s', self)
         timeout = 10
         pendings = tracker.wait(timeout)
         if pendings:
--- a/server/sources/extlite.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/server/sources/extlite.py	Thu Oct 10 13:26:11 2013 +0200
@@ -295,7 +295,7 @@
                           query, args, ex.args[0])
             try:
                 session.cnxset.connection(self.uri).rollback()
-                self.critical('transaction has been rollbacked')
+                self.critical('transaction has been rolled back')
             except Exception:
                 pass
             raise
--- a/server/sources/native.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/server/sources/native.py	Thu Oct 10 13:26:11 2013 +0200
@@ -751,7 +751,7 @@
                 try:
                     session.cnxset.connection(self.uri).rollback()
                     if self.repo.config.mode != 'test':
-                        self.critical('transaction has been rollbacked')
+                        self.critical('transaction has been rolled back')
                 except Exception as ex:
                     pass
             if ex.__class__.__name__ == 'IntegrityError':
@@ -795,7 +795,7 @@
             try:
                 session.cnxset.connection(self.uri).rollback()
                 if self.repo.config.mode != 'test':
-                    self.critical('transaction has been rollbacked')
+                    self.critical('transaction has been rolled back')
             except Exception:
                 pass
             raise
--- a/server/sqlutils.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/server/sqlutils.py	Thu Oct 10 13:26:11 2013 +0200
@@ -378,7 +378,7 @@
 def init_postgres_connexion(cnx):
     cnx.cursor().execute('SET TIME ZONE UTC')
     # commit is needed, else setting are lost if the connection is first
-    # rollbacked
+    # rolled back
     cnx.commit()
 
 postgres_hooks = SQL_CONNECT_HOOKS.setdefault('postgres', [])
--- a/server/test/unittest_migractions.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/server/test/unittest_migractions.py	Thu Oct 10 13:26:11 2013 +0200
@@ -434,12 +434,12 @@
         self.mh.commit()
         # unique_together test
         self.assertEqual(len(self.schema.eschema('Personne')._unique_together), 1)
-        self.assertItemsEqual(self.schema.eschema('Personne')._unique_together[0],
+        self.assertCountEqual(self.schema.eschema('Personne')._unique_together[0],
                                            ('nom', 'prenom', 'datenaiss'))
         rset = cursor.execute('Any C WHERE C is CWUniqueTogetherConstraint, C constraint_of ET, ET name "Personne"')
         self.assertEqual(len(rset), 1)
         relations = [r.name for r in rset.get_entity(0, 0).relations]
-        self.assertItemsEqual(relations, ('nom', 'prenom', 'datenaiss'))
+        self.assertCountEqual(relations, ('nom', 'prenom', 'datenaiss'))
 
     def _erqlexpr_rset(self, action, ertype):
         rql = 'RQLExpression X WHERE ET is CWEType, ET %s_permission X, ET name %%(name)s' % action
--- a/server/test/unittest_querier.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/server/test/unittest_querier.py	Thu Oct 10 13:26:11 2013 +0200
@@ -702,7 +702,7 @@
         rset = self.execute('Any X WHERE X is CWGroup', build_descr=0)
         rset.rows.sort()
         self.assertEqual(tuplify(rset.rows), [(2,), (3,), (4,), (5,)])
-        self.assertEqual(rset.description, ())
+        self.assertEqual(rset.description, [])
 
     def test_select_limit_offset(self):
         rset = self.execute('CWGroup X ORDERBY N LIMIT 2 WHERE X name N')
--- a/server/test/unittest_repository.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/server/test/unittest_repository.py	Thu Oct 10 13:26:11 2013 +0200
@@ -137,7 +137,7 @@
             self.assertTrue(self.execute('Any X WHERE X is CWGroup, X name "toto"'))
             with self.assertRaises(QueryError) as cm:
                 self.commit()
-            self.assertEqual(str(cm.exception), 'transaction must be rollbacked')
+            self.assertEqual(str(cm.exception), 'transaction must be rolled back')
             self.rollback()
             self.assertFalse(self.execute('Any X WHERE X is CWGroup, X name "toto"'))
 
@@ -154,7 +154,7 @@
             self.assertTrue(self.execute('Any X WHERE X is CWGroup, X name "toto"'))
             with self.assertRaises(QueryError) as cm:
                 self.commit()
-            self.assertEqual(str(cm.exception), 'transaction must be rollbacked')
+            self.assertEqual(str(cm.exception), 'transaction must be rolled back')
             self.rollback()
             self.assertFalse(self.execute('Any X WHERE X is CWGroup, X name "toto"'))
 
--- a/server/test/unittest_storage.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/server/test/unittest_storage.py	Thu Oct 10 13:26:11 2013 +0200
@@ -237,7 +237,7 @@
         self.assertEqual(osp.splitext(new_path)[1], '.jpg')
 
     @tag('update', 'extension', 'rollback')
-    def test_bfss_update_with_different_extension_rollbacked(self):
+    def test_bfss_update_with_different_extension_rolled_back(self):
         # use self.session to use server-side cache
         f1 = self.session.create_entity('File', data=Binary('some data'),
                                         data_format=u'text/plain', data_name=u'foo.txt')
--- a/setup.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/setup.py	Thu Oct 10 13:26:11 2013 +0200
@@ -63,7 +63,7 @@
 ext_modules = getattr(__pkginfo__, 'ext_modules', None)
 package_data = getattr(__pkginfo__, 'package_data', {})
 
-BASE_BLACKLIST = ('CVS', 'debian', 'dist', 'build', '__buildlog')
+BASE_BLACKLIST = ('CVS', 'dist', 'build', '__buildlog')
 IGNORED_EXTENSIONS = ('.pyc', '.pyo', '.elc')
 
 
--- a/skeleton/debian/rules.tmpl	Wed Oct 09 11:13:56 2013 +0200
+++ b/skeleton/debian/rules.tmpl	Thu Oct 10 13:26:11 2013 +0200
@@ -37,7 +37,7 @@
 	dh_install -i
 	dh_installchangelogs -i
 	dh_installexamples -i
-	dh_installdocs -i
+	dh_installdocs -i README
 	dh_installman -i
 	dh_pysupport -i /usr/share/cubicweb
 	dh_link -i
--- a/test/unittest_entity.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/test/unittest_entity.py	Thu Oct 10 13:26:11 2013 +0200
@@ -150,24 +150,24 @@
         p1 = req.create_entity('Personne', nom=u'di')
         p2 = req.create_entity('Personne', nom=u'mascio')
         t = req.create_entity('Tag', name=u't0', tags=[])
-        self.assertItemsEqual(t.tags, [])
+        self.assertCountEqual(t.tags, [])
         t = req.create_entity('Tag', name=u't1', tags=p1)
-        self.assertItemsEqual(t.tags, [p1])
+        self.assertCountEqual(t.tags, [p1])
         t = req.create_entity('Tag', name=u't2', tags=p1.eid)
-        self.assertItemsEqual(t.tags, [p1])
+        self.assertCountEqual(t.tags, [p1])
         t = req.create_entity('Tag', name=u't3', tags=[p1, p2.eid])
-        self.assertItemsEqual(t.tags, [p1, p2])
+        self.assertCountEqual(t.tags, [p1, p2])
 
     def test_cw_instantiate_reverse_relation(self):
         req = self.request()
         t1 = req.create_entity('Tag', name=u't1')
         t2 = req.create_entity('Tag', name=u't2')
         p = req.create_entity('Personne', nom=u'di mascio', reverse_tags=t1)
-        self.assertItemsEqual(p.reverse_tags, [t1])
+        self.assertCountEqual(p.reverse_tags, [t1])
         p = req.create_entity('Personne', nom=u'di mascio', reverse_tags=t1.eid)
-        self.assertItemsEqual(p.reverse_tags, [t1])
+        self.assertCountEqual(p.reverse_tags, [t1])
         p = req.create_entity('Personne', nom=u'di mascio', reverse_tags=[t1, t2.eid])
-        self.assertItemsEqual(p.reverse_tags, [t1, t2])
+        self.assertCountEqual(p.reverse_tags, [t1, t2])
 
     def test_fetch_rql(self):
         user = self.user()
--- a/test/unittest_rqlrewrite.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/test/unittest_rqlrewrite.py	Thu Oct 10 13:26:11 2013 +0200
@@ -438,30 +438,30 @@
                          u"Any C WHERE C is Card, EXISTS(C owned_by A, A is CWUser)")
 
     def test_rqlexpr_not_relation_1_1(self):
-        constraint = RRQLExpression('X owned_by Z, Z login "hop"', 'X')
+        constraint = ERQLExpression('X owned_by Z, Z login "hop"', 'X')
         rqlst = parse('Affaire A WHERE NOT EXISTS(A documented_by C)')
         rewrite(rqlst, {('C', 'X'): (constraint,)}, {}, 'X')
         self.assertEqual(rqlst.as_string(),
                          u'Any A WHERE NOT EXISTS(A documented_by C, EXISTS(C owned_by B, B login "hop", B is CWUser), C is Card), A is Affaire')
 
     def test_rqlexpr_not_relation_1_2(self):
-        constraint = RRQLExpression('X owned_by Z, Z login "hop"', 'X')
+        constraint = ERQLExpression('X owned_by Z, Z login "hop"', 'X')
         rqlst = parse('Affaire A WHERE NOT EXISTS(A documented_by C)')
         rewrite(rqlst, {('A', 'X'): (constraint,)}, {}, 'X')
         self.assertEqual(rqlst.as_string(),
                          u'Any A WHERE NOT EXISTS(A documented_by C, C is Card), A is Affaire, EXISTS(A owned_by B, B login "hop", B is CWUser)')
 
     def test_rqlexpr_not_relation_2(self):
-        constraint = RRQLExpression('X owned_by Z, Z login "hop"', 'X')
+        constraint = ERQLExpression('X owned_by Z, Z login "hop"', 'X')
         rqlst = rqlhelper.parse('Affaire A WHERE NOT A documented_by C', annotate=False)
         rewrite(rqlst, {('C', 'X'): (constraint,)}, {}, 'X')
         self.assertEqual(rqlst.as_string(),
                          u'Any A WHERE NOT EXISTS(A documented_by C, EXISTS(C owned_by B, B login "hop", B is CWUser), C is Card), A is Affaire')
 
     def test_rqlexpr_multiexpr_outerjoin(self):
-        c1 = RRQLExpression('X owned_by Z, Z login "hop"', 'X')
-        c2 = RRQLExpression('X owned_by Z, Z login "hip"', 'X')
-        c3 = RRQLExpression('X owned_by Z, Z login "momo"', 'X')
+        c1 = ERQLExpression('X owned_by Z, Z login "hop"', 'X')
+        c2 = ERQLExpression('X owned_by Z, Z login "hip"', 'X')
+        c3 = ERQLExpression('X owned_by Z, Z login "momo"', 'X')
         rqlst = rqlhelper.parse('Any A WHERE A documented_by C?', annotate=False)
         rewrite(rqlst, {('C', 'X'): (c1, c2, c3)}, {}, 'X')
         self.assertEqual(rqlst.as_string(),
--- a/test/unittest_schema.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/test/unittest_schema.py	Thu Oct 10 13:26:11 2013 +0200
@@ -132,6 +132,8 @@
         self.assertRaises(RQLSyntaxError, ERQLExpression, '1')
         expr = ERQLExpression('X travaille S, S owned_by U')
         self.assertEqual(str(expr), 'Any X WHERE X travaille S, S owned_by U, X eid %(x)s, U eid %(u)s')
+        expr = ERQLExpression('X foo S, S bar U, X baz XE, S quux SE HAVING XE > SE')
+        self.assertEqual(str(expr), 'Any X WHERE X foo S, S bar U, X baz XE, S quux SE, X eid %(x)s, U eid %(u)s HAVING XE > SE')
 
     def test_rrqlexpression(self):
         self.assertRaises(Exception, RRQLExpression, '1')
--- a/utils.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/utils.py	Thu Oct 10 13:26:11 2013 +0200
@@ -195,6 +195,8 @@
         if isinstance(other, RepeatList):
             return other._size == self._size and other._item == self._item
         return self[:] == other
+    # py3k future warning "Overriding __eq__ blocks inheritance of __hash__ in 3.x"
+    # is annoying but won't go away because we don't want to hash() the repeatlist
     def pop(self, i):
         self._size -= 1
 
--- a/web/formfields.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/web/formfields.py	Thu Oct 10 13:26:11 2013 +0200
@@ -553,7 +553,7 @@
             widget.attrs.setdefault('maxlength', self.max_length)
 
     def init_text_area(self, widget):
-        if self.max_length < 513:
+        if self.max_length and self.max_length < 513:
             widget.attrs.setdefault('cols', 60)
             widget.attrs.setdefault('rows', 5)
 
--- a/web/test/unittest_uicfg.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/web/test/unittest_uicfg.py	Thu Oct 10 13:26:11 2013 +0200
@@ -85,15 +85,15 @@
     def test_uihelper_hide_fields(self):
         # original conf : in_group is edited in 'attributes' section everywhere
         section_conf = uicfg.autoform_section.get('CWUser', 'in_group', '*', 'subject')
-        self.assertItemsEqual(section_conf, ['main_attributes', 'muledit_attributes'])
+        self.assertCountEqual(section_conf, ['main_attributes', 'muledit_attributes'])
         # hide field in main form
         uihelper.hide_fields('CWUser', ('login', 'in_group'))
         section_conf = uicfg.autoform_section.get('CWUser', 'in_group', '*', 'subject')
-        self.assertItemsEqual(section_conf, ['main_hidden', 'muledit_attributes'])
+        self.assertCountEqual(section_conf, ['main_hidden', 'muledit_attributes'])
         # hide field in muledit form
         uihelper.hide_fields('CWUser', ('login', 'in_group'), formtype='muledit')
         section_conf = uicfg.autoform_section.get('CWUser', 'in_group', '*', 'subject')
-        self.assertItemsEqual(section_conf, ['main_hidden', 'muledit_hidden'])
+        self.assertCountEqual(section_conf, ['main_hidden', 'muledit_hidden'])
 
     @tag('uihelper', 'hidden', 'formconfig')
     def test_uihelper_formconfig(self):
@@ -103,7 +103,7 @@
             hidden = ('in_group',)
             fields_order = ('login', 'firstname')
         section_conf = uicfg.autoform_section.get('CWUser', 'in_group', '*', 'subject')
-        self.assertItemsEqual(section_conf, ['main_hidden', 'muledit_attributes'])
+        self.assertCountEqual(section_conf, ['main_hidden', 'muledit_attributes'])
         self.assertEqual(afk_get('CWUser', 'firstname', 'String', 'subject'), {'order': 1})
 
 
--- a/web/test/unittest_views_basecontrollers.py	Wed Oct 09 11:13:56 2013 +0200
+++ b/web/test/unittest_views_basecontrollers.py	Thu Oct 10 13:26:11 2013 +0200
@@ -397,7 +397,7 @@
         path, params = self.expect_redirect_handle_request(req, 'edit')
         usergroups = [gname for gname, in
                       self.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
-        self.assertItemsEqual(usergroups, ['managers', 'test'])
+        self.assertCountEqual(usergroups, ['managers', 'test'])
         self.assertEqual(get_pending_inserts(req), [])
 
     def test_req_pending_delete(self):
@@ -408,14 +408,14 @@
         usergroups = [gname for gname, in
                       self.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
         # just make sure everything was set correctly
-        self.assertItemsEqual(usergroups, ['managers', 'test'])
+        self.assertCountEqual(usergroups, ['managers', 'test'])
         # now try to delete the relation
         req = self.request(**req_form(user))
         req.session.data['pending_delete'] = set([(user.eid, 'in_group', groupeid)])
         path, params = self.expect_redirect_handle_request(req, 'edit')
         usergroups = [gname for gname, in
                       self.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
-        self.assertItemsEqual(usergroups, ['managers'])
+        self.assertCountEqual(usergroups, ['managers'])
         self.assertEqual(get_pending_deletes(req), [])
 
     def test_redirect_apply_button(self):
@@ -715,7 +715,7 @@
 
     def test_remote_add_existing_tag(self):
         self.remote_call('tag_entity', self.john.eid, ['python'])
-        self.assertItemsEqual(
+        self.assertCountEqual(
             [tname for tname, in self.execute('Any N WHERE T is Tag, T name N')],
             ['python', 'cubicweb'])
         self.assertEqual(
@@ -724,7 +724,7 @@
 
     def test_remote_add_new_tag(self):
         self.remote_call('tag_entity', self.john.eid, ['javascript'])
-        self.assertItemsEqual(
+        self.assertCountEqual(
             [tname for tname, in self.execute('Any N WHERE T is Tag, T name N')],
             ['python', 'cubicweb', 'javascript'])
         self.assertEqual(