backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 07 Sep 2012 14:01:59 +0200
changeset 8535 268b6349baf3
parent 8525 c09feae04094 (current diff)
parent 8534 6ed331fd4347 (diff)
child 8537 e30d0a7f0087
backport stable
__pkginfo__.py
debian/control
doc/book/en/devweb/views/index.rst
doc/book/en/devweb/views/views.rst
entity.py
i18n/de.po
i18n/en.po
i18n/es.po
i18n/fr.po
misc/scripts/ldapuser2ldapfeed.py
predicates.py
server/hook.py
server/migractions.py
server/session.py
server/sources/datafeed.py
server/test/unittest_repository.py
sobjects/ldapparser.py
test/unittest_entity.py
web/request.py
web/views/ajaxcontroller.py
web/views/tableview.py
--- a/.hgtags	Fri Aug 03 13:29:37 2012 +0200
+++ b/.hgtags	Fri Sep 07 14:01:59 2012 +0200
@@ -256,5 +256,11 @@
 20ee573bd2379a00f29ff27bb88a8a3344d4cdfe cubicweb-debian-version-3.14.7-1
 15fe07ff687238f8cc09d8e563a72981484085b3 cubicweb-version-3.14.8
 81394043ad226942ac0019b8e1d4f7058d67a49f cubicweb-debian-version-3.14.8-1
+9337812cef6b949eee89161190e0c3d68d7f32ea cubicweb-version-3.14.9
+68c762adf2d5a2c338910ef1091df554370586f0 cubicweb-debian-version-3.14.9-1
 783a5df54dc742e63c8a720b1582ff08366733bd cubicweb-version-3.15.1
 fe5e60862b64f1beed2ccdf3a9c96502dfcd811b cubicweb-debian-version-3.15.1-1
+2afc157ea9b2b92eccb0f2d704094e22ce8b5a05 cubicweb-version-3.15.2
+9aa5553b26520ceb68539e7a32721b5cd5393e16 cubicweb-debian-version-3.15.2-1
+0e012eb80990ca6f91aa9a8ad3324fbcf51435b1 cubicweb-version-3.15.3
+7ad423a5b6a883dbdf00e6c87a5f8ab121041640 cubicweb-debian-version-3.15.3-1
--- a/__init__.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/__init__.py	Fri Sep 07 14:01:59 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
--- a/__pkginfo__.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/__pkginfo__.py	Fri Sep 07 14:01:59 2012 +0200
@@ -22,7 +22,7 @@
 
 modname = distname = "cubicweb"
 
-numversion = (3, 15, 1)
+numversion = (3, 15, 4)
 version = '.'.join(str(num) for num in numversion)
 
 description = "a repository of entities / relations for knowledge management"
--- a/cwconfig.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/cwconfig.py	Fri Sep 07 14:01:59 2012 +0200
@@ -171,6 +171,7 @@
 
 import sys
 import os
+import stat
 import logging
 import logging.config
 from smtplib import SMTP
@@ -306,7 +307,10 @@
 _forced_mode = os.environ.get('CW_MODE')
 assert _forced_mode in (None, 'system', 'user')
 
-CWDEV = exists(join(CW_SOFTWARE_ROOT, '.hg'))
+# CWDEV tells whether directories such as i18n/, web/data/, etc. (ie containing
+# some other resources than python libraries) are located with the python code
+# or as a 'shared' cube
+CWDEV = exists(join(CW_SOFTWARE_ROOT, 'i18n'))
 
 try:
     _INSTALL_PREFIX = os.environ['CW_INSTALL_PREFIX']
@@ -1073,7 +1077,12 @@
         If not, try to fix this, letting exception propagate when not possible.
         """
         if not exists(path):
-            os.makedirs(path)
+            self.info('creating %s directory', path)
+            try:
+                os.makedirs(path)
+            except OSError, ex:
+                self.warning('error while creating %s directory: %s', path, ex)
+                return
         if self['uid']:
             try:
                 uid = int(self['uid'])
@@ -1087,10 +1096,20 @@
                 return
         fstat = os.stat(path)
         if fstat.st_uid != uid:
-            os.chown(path, uid, os.getgid())
-        import stat
+            self.info('giving ownership of %s directory to %s', path, self['uid'])
+            try:
+                os.chown(path, uid, os.getgid())
+            except OSError, ex:
+                self.warning('error while giving ownership of %s directory to %s: %s',
+                             path, self['uid'], ex)
         if not (fstat.st_mode & stat.S_IWUSR):
-            os.chmod(path, fstat.st_mode | stat.S_IWUSR)
+            self.info('forcing write permission on directory %s', path)
+            try:
+                os.chmod(path, fstat.st_mode | stat.S_IWUSR)
+            except OSError, ex:
+                self.warning('error while forcing write permission on directory %s: %s',
+                             path, ex)
+                return
 
     @cached
     def instance_md5_version(self):
--- a/debian/changelog	Fri Aug 03 13:29:37 2012 +0200
+++ b/debian/changelog	Fri Sep 07 14:01:59 2012 +0200
@@ -1,6 +1,24 @@
+cubicweb (3.15.4-1) unstable; urgency=low
+
+  * New upstream release
+
+ -- Julien Cristau <jcristau@debian.org>  Fri, 31 Aug 2012 16:43:11 +0200
+
+cubicweb (3.15.3-1) unstable; urgency=low
+
+  * New upstream release
+
+ -- Pierre-Yves David <pierre-yves.david@logilab.fr>  Tue, 21 Aug 2012 14:19:31 +0200
+
+cubicweb (3.15.2-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 20 Jul 2012 15:17:17 +0200
+
 cubicweb (3.15.1-1) quantal; urgency=low
 
-  * new upstream release 
+  * new upstream release
 
  -- David Douard <david.douard@logilab.fr>  Mon, 11 Jun 2012 09:45:24 +0200
 
@@ -10,6 +28,12 @@
 
  -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Thu, 12 Apr 2012 13:52:05 +0200
 
+cubicweb (3.14.9-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Pierre-Yves David <pierre-yves.david@logilab.fr>  Tue, 31 Jul 2012 16:16:28 +0200
+
 cubicweb (3.14.8-1) unstable; urgency=low
 
   * new upstream release
--- a/debian/control	Fri Aug 03 13:29:37 2012 +0200
+++ b/debian/control	Fri Sep 07 14:01:59 2012 +0200
@@ -7,18 +7,25 @@
            Adrien Di Mascio <Adrien.DiMascio@logilab.fr>,
            Aurélien Campéas <aurelien.campeas@logilab.fr>,
            Nicolas Chauvat <nicolas.chauvat@logilab.fr>
-Build-Depends: debhelper (>= 7), python (>= 2.5), python-central (>= 0.5), python-sphinx, python-logilab-common, python-unittest2
-# for the documentation:
-# python-sphinx, python-logilab-common, python-unittest2, logilab-doctools, logilab-xml
+Build-Depends:
+ debhelper (>= 7),
+ python (>= 2.5),
+ python-central (>= 0.5),
+ python-sphinx,
+ python-logilab-common,
+ python-unittest2,
+ python-logilab-mtconverter,
+ python-rql,
+ python-yams,
+ python-lxml,
 Standards-Version: 3.9.1
 Homepage: http://www.cubicweb.org
-XS-Python-Version: >= 2.5, << 3.0
+XS-Python-Version: >= 2.5
 
 Package: cubicweb
 Architecture: all
 XB-Python-Version: ${python:Versions}
 Depends: ${misc:Depends}, ${python:Depends}, cubicweb-server (= ${source:Version}), cubicweb-twisted (= ${source:Version})
-XB-Recommends: (postgresql, postgresql-plpython) | mysql | sqlite3
 Recommends: postgresql | mysql | sqlite3
 Description: the complete CubicWeb framework
  CubicWeb is a semantic web application framework.
--- a/debian/rules	Fri Aug 03 13:29:37 2012 +0200
+++ b/debian/rules	Fri Sep 07 14:01:59 2012 +0200
@@ -11,10 +11,14 @@
 build-stamp:
 	dh_testdir
 	NO_SETUPTOOLS=1 python setup.py build
+	# cubicweb.foo needs to be importable by sphinx, so create a cubicweb symlink to the source dir
+	mkdir -p debian/pythonpath
+	ln -sf $(CURDIR) debian/pythonpath/cubicweb
 	# documentation build is now made optional since it can break for old
 	# distributions and we don't want to block a new release of Cubicweb
 	# because of documentation issues.
-	-PYTHONPATH=$(CURDIR)/.. $(MAKE) -C doc/book/en all
+	-PYTHONPATH=$${PYTHONPATH:+$${PYTHONPATH}:}$(CURDIR)/debian/pythonpath $(MAKE) -C doc/book/en all
+	rm -rf debian/pythonpath
 	touch build-stamp
 
 clean:
--- a/devtools/testlib.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/devtools/testlib.py	Fri Sep 07 14:01:59 2012 +0200
@@ -817,9 +817,8 @@
         """
         req = req or rset and rset.req or self.request()
         req.form['vid'] = vid
-        kwargs['rset'] = rset
         viewsreg = self.vreg['views']
-        view = viewsreg.select(vid, req, **kwargs)
+        view = viewsreg.select(vid, req, rset=rset, **kwargs)
         # set explicit test description
         if rset is not None:
             self.set_description("testing vid=%s defined in %s with (%s)" % (
@@ -831,10 +830,8 @@
             viewfunc = view.render
         else:
             kwargs['view'] = view
-            templateview = viewsreg.select(template, req, **kwargs)
             viewfunc = lambda **k: viewsreg.main_template(req, template,
-                                                          **kwargs)
-        kwargs.pop('rset')
+                                                          rset=rset, **kwargs)
         return self._test_view(viewfunc, view, template, kwargs)
 
 
--- a/doc/book/en/admin/instance-config.rst	Fri Aug 03 13:29:37 2012 +0200
+++ b/doc/book/en/admin/instance-config.rst	Fri Sep 07 14:01:59 2012 +0200
@@ -17,7 +17,7 @@
 each option name is prefixed with its own section and followed by its
 default value if necessary, e.g. "`<section>.<option>` [value]."
 
-.. _`Configuring the Web server`:
+.. _`WebServerConfig`:
 
 Configuring the Web server
 --------------------------
--- a/doc/book/en/admin/ldap.rst	Fri Aug 03 13:29:37 2012 +0200
+++ b/doc/book/en/admin/ldap.rst	Fri Sep 07 14:01:59 2012 +0200
@@ -29,6 +29,15 @@
 The base functionality for this is in
 :file:`cubicweb/server/sources/ldapuser.py`.
 
+External dependencies
+---------------------
+
+You'll need the following packages to make CubicWeb interact with your LDAP /
+Active Directory server:
+
+* python-ldap
+* ldaputils if using `ldapfeed` source
+
 Configurations options
 ----------------------
 
--- a/doc/book/en/annexes/depends.rst	Fri Aug 03 13:29:37 2012 +0200
+++ b/doc/book/en/annexes/depends.rst	Fri Sep 07 14:01:59 2012 +0200
@@ -45,6 +45,9 @@
 * indexer - http://www.logilab.org/project/indexer -
   http://pypi.python.org/pypi/indexer - included in the forest
 
+* 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/devrepo/fti.rst	Fri Sep 07 14:01:59 2012 +0200
@@ -0,0 +1,159 @@
+.. _fti:
+
+Full Text Indexing in CubicWeb
+------------------------------
+
+When an attribute is tagged as *fulltext-indexable* in the datamodel,
+CubicWeb will automatically trigger hooks to update the internal
+fulltext index (i.e the ``appears`` SQL table) each time this attribute
+is modified.
+
+CubicWeb also provides a ``db-rebuild-fti`` command to rebuild the whole
+fulltext on demand:
+
+.. sourcecode:: bash
+
+   cubicweb@esope~$ cubicweb db-rebuild-fti my_tracker_instance
+
+You can also rebuild the fulltext index for a given set of entity types:
+
+.. sourcecode:: bash
+
+   cubicweb@esope~$ cubicweb db-rebuild-fti my_tracker_instance Ticket Version
+
+In the above example, only fulltext index of entity types ``Ticket`` and ``Version``
+will be rebuilt.
+
+
+Standard FTI process
+~~~~~~~~~~~~~~~~~~~~
+
+Considering an entity type ``ET``, the default *fti* process is to :
+
+1. fetch all entities of type ``ET``
+
+2. for each entity, adapt it to ``IFTIndexable`` (see
+   :class:`~cubicweb.entities.adapters.IFTIndexableAdapter`)
+
+3. call
+   :meth:`~cubicweb.entities.adapters.IFTIndexableAdapter.get_words` on
+   the adapter which is supposed to return a dictionary *weight* ->
+   *list of words* as expected by
+   :meth:`~logilab.database.fti.FTIndexerMixIn.index_object`. The
+   tokenization of each attribute value is done by
+   :meth:`~logilab.database.fti.tokenize`.
+
+
+See :class:`~cubicweb.entities.adapters.IFTIndexableAdapter` for more documentation.
+
+
+Yams and ``fultext_container``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+It is possible in the datamodel to indicate that fulltext-indexed
+attributes defined for an entity type will be used to index not the
+entity itself but a related entity. This is especially useful for
+composite entities. Let's take a look at (a simplified version of)
+the base schema defined in CubicWeb (see :mod:`cubicweb.schemas.base`):
+
+.. sourcecode:: python
+
+  class CWUser(WorkflowableEntityType):
+      login     = String(required=True, unique=True, maxsize=64)
+      upassword = Password(required=True)
+
+  class EmailAddress(EntityType):
+      address = String(required=True,  fulltextindexed=True,
+                       indexed=True, unique=True, maxsize=128)
+
+
+  class use_email_relation(RelationDefinition):
+      name = 'use_email'
+      subject = 'CWUser'
+      object = 'EmailAddress'
+      cardinality = '*?'
+      composite = 'subject'
+
+
+The schema above states that there is a relation between ``CWUser`` and ``EmailAddress``
+and that the ``address`` field of ``EmailAddress`` is fulltext indexed. Therefore,
+in your application, if you use fulltext search to look for an email address, CubicWeb
+will return the ``EmailAddress`` itself. But the objects we'd like to index
+are more likely to be the associated ``CWUser`` than the ``EmailAddress`` itself.
+
+The simplest way to achieve that is to tag the ``use_email`` relation in
+the datamodel:
+
+.. sourcecode:: python
+
+  class use_email(RelationType):
+      fulltext_container = 'subject'
+
+
+Customizing how entities are fetched during ``db-rebuild-fti``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``db-rebuild-fti`` will call the
+:meth:`~cubicweb.entities.AnyEntity.cw_fti_index_rql_queries` class
+method on your entity type.
+
+.. automethod:: cubicweb.entities.AnyEntity.cw_fti_index_rql_queries
+
+Now, suppose you've got a _huge_ table to index, you probably don't want to
+get all entities at once. So here's a simple customized example that will
+process block of 10000 entities:
+
+.. sourcecode:: python
+
+
+    class MyEntityClass(AnyEntity):
+        __regid__ = 'MyEntityClass'
+
+    @classmethod
+    def cw_fti_index_rql_queries(cls, req):
+        # get the default RQL method and insert LIMIT / OFFSET instructions
+        base_rql = super(SearchIndex, cls).cw_fti_index_rql_queries(req)[0]
+        selected, restrictions = base_rql.split(' WHERE ')
+        rql_template = '%s ORDERBY X LIMIT %%(limit)s OFFSET %%(offset)s WHERE %s' % (
+            selected, restrictions)
+        # count how many entities you'll have to index
+        count = req.execute('Any COUNT(X) WHERE X is MyEntityClass')[0][0]
+        # iterate by blocks of 10000 entities
+        chunksize = 10000
+        for offset in xrange(0, count, chunksize):
+            print 'SENDING', rql_template % {'limit': chunksize, 'offset': offset}
+            yield rql_template % {'limit': chunksize, 'offset': offset}
+
+Since you have access to ``req``, you can more or less fetch whatever you want.
+
+
+Customizing :meth:`~cubicweb.entities.adapters.IFTIndexableAdapter.get_words`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can also customize the FTI process by providing your own ``get_words()``
+implementation:
+
+.. sourcecode:: python
+
+    from cubicweb.entities.adapters import IFTIndexableAdapter
+
+    class SearchIndexAdapter(IFTIndexableAdapter):
+        __regid__ = 'IFTIndexable'
+        __select__ = is_instance('MyEntityClass')
+
+        def fti_containers(self, _done=None):
+            """this should yield any entity that must be considered to
+            fulltext-index self.entity
+
+            CubicWeb's default implementation will look for yams'
+            ``fulltex_container`` property.
+            """
+            yield self.entity
+            yield self.entity.some_related_entity
+
+
+        def get_words(self):
+            # implement any logic here
+            # see http://www.postgresql.org/docs/9.1/static/textsearch-controls.html
+            # for the actual signification of 'C'
+            return {'C': ['any', 'word', 'I', 'want']}
--- a/doc/book/en/devrepo/index.rst	Fri Aug 03 13:29:37 2012 +0200
+++ b/doc/book/en/devrepo/index.rst	Fri Sep 07 14:01:59 2012 +0200
@@ -21,3 +21,5 @@
    testing.rst
    migration.rst
    profiling.rst
+   fti.rst
+
--- a/doc/book/en/devweb/ajax.rst	Fri Aug 03 13:29:37 2012 +0200
+++ b/doc/book/en/devweb/ajax.rst	Fri Sep 07 14:01:59 2012 +0200
@@ -7,6 +7,6 @@
 
 You can, for instance, register some python functions that will become
 callable from javascript through ajax calls. All the ajax URLs are handled
-by the ``AjaxController`` controller.
+by the :class:`cubicweb.web.views.ajaxcontroller.AjaxController` controller.
 
 .. automodule:: cubicweb.web.views.ajaxcontroller
--- a/doc/book/en/devweb/views/index.rst	Fri Aug 03 13:29:37 2012 +0200
+++ b/doc/book/en/devweb/views/index.rst	Fri Sep 07 14:01:59 2012 +0200
@@ -22,6 +22,7 @@
    breadcrumbs
    idownloadable
    wdoc
+
 ..   editforms
 ..   embedding
 
--- a/doc/book/en/devweb/views/views.rst	Fri Aug 03 13:29:37 2012 +0200
+++ b/doc/book/en/devweb/views/views.rst	Fri Sep 07 14:01:59 2012 +0200
@@ -32,8 +32,8 @@
 Basic class for views
 ~~~~~~~~~~~~~~~~~~~~~
 
-Class `View` (`cubicweb.view`)
-```````````````````````````````
+Class :class:`~cubicweb.view.View`
+``````````````````````````````````
 
 .. autoclass:: cubicweb.view.View
 
--- a/entities/adapters.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/entities/adapters.py	Fri Sep 07 14:01:59 2012 +0200
@@ -87,10 +87,20 @@
 
 
 class IFTIndexableAdapter(view.EntityAdapter):
+    """standard adapter to handle fulltext indexing
+
+    .. automethod:: cubicweb.entities.adapters.IFTIndexableAdapter.fti_containers
+    .. automethod:: cubicweb.entities.adapters.IFTIndexableAdapter.get_words
+    """
     __regid__ = 'IFTIndexable'
     __select__ = is_instance('Any')
 
     def fti_containers(self, _done=None):
+        """return the list of entities to index when handling ``self.entity``
+
+        The actual list of entities depends on ``fulltext_container`` usage
+        in the datamodel definition
+        """
         if _done is None:
             _done = set()
         entity = self.entity
--- a/entity.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/entity.py	Fri Sep 07 14:01:59 2012 +0200
@@ -1153,6 +1153,9 @@
         # insert security RQL expressions granting the permission to 'add' the
         # relation into the rql syntax tree, if necessary
         rqlexprs = rdef.get_rqlexprs('add')
+        if not self.has_eid():
+            rqlexprs = [rqlexpr for rqlexpr in rqlexprs
+                        if searchedvar.name in rqlexpr.mainvars]
         if rqlexprs and not rdef.has_perm(self._cw, 'add', **sec_check_args):
             # compute a varmap suitable to RQLRewriter.rewrite argument
             varmap = dict((v, v) for v in (searchedvar.name, evar.name)
--- a/hooks/integrity.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/hooks/integrity.py	Fri Sep 07 14:01:59 2012 +0200
@@ -301,11 +301,10 @@
     def precommit_event(self):
         session = self.session
         pendingeids = session.transaction_data.get('pendingeids', ())
-        neweids = session.transaction_data.get('neweids', ())
         eids_by_etype_rtype = {}
         for eid, rtype in self.get_data():
-            # don't do anything if the entity is being created or deleted
-            if not (eid in pendingeids or eid in neweids):
+            # don't do anything if the entity is being deleted
+            if eid not in pendingeids:
                 etype = session.describe(eid)[0]
                 key = (etype, rtype)
                 if key not in eids_by_etype_rtype:
--- a/hooks/syncschema.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/hooks/syncschema.py	Fri Sep 07 14:01:59 2012 +0200
@@ -755,7 +755,13 @@
         cols = ['%s%s' % (prefix, c) for c in self.cols]
         sqls = dbhelper.sqls_drop_multicol_unique_index(table, cols)
         for sql in sqls:
-            session.system_sql(sql)
+            try:
+                session.system_sql(sql)
+            except Exception, exc: # should be ProgrammingError
+                if sql.startswith('DROP'):
+                    self.error('execute of `%s` failed (cause: %s)', sql, exc)
+                    continue
+                raise
 
     # XXX revertprecommit_event
 
--- a/i18n/de.po	Fri Aug 03 13:29:37 2012 +0200
+++ b/i18n/de.po	Fri Sep 07 14:01:59 2012 +0200
@@ -3280,6 +3280,9 @@
 msgid "new"
 msgstr "neu"
 
+msgid "next page"
+msgstr ""
+
 msgid "next_results"
 msgstr "weitere Ergebnisse"
 
@@ -3481,6 +3484,9 @@
 msgid "preferences"
 msgstr "Einstellungen"
 
+msgid "previous page"
+msgstr ""
+
 msgid "previous_results"
 msgstr "vorige Ergebnisse"
 
@@ -4618,3 +4624,10 @@
 #, python-format
 msgid "you should un-inline relation %s which is supported and may be crossed "
 msgstr ""
+
+#~ msgid ""
+#~ "Can't restore relation %(rtype)s of entity %(eid)s, this relation does "
+#~ "not exists anymore in the schema."
+#~ msgstr ""
+#~ "Kann die Relation %(rtype)s der Entität %(eid)s nicht wieder herstellen, "
+#~ "diese Relation existiert nicht mehr in dem Schema."
--- a/i18n/en.po	Fri Aug 03 13:29:37 2012 +0200
+++ b/i18n/en.po	Fri Sep 07 14:01:59 2012 +0200
@@ -3196,6 +3196,9 @@
 msgid "new"
 msgstr ""
 
+msgid "next page"
+msgstr ""
+
 msgid "next_results"
 msgstr "next results"
 
@@ -3396,6 +3399,9 @@
 msgid "preferences"
 msgstr ""
 
+msgid "previous page"
+msgstr ""
+
 msgid "previous_results"
 msgstr "previous results"
 
--- a/i18n/es.po	Fri Aug 03 13:29:37 2012 +0200
+++ b/i18n/es.po	Fri Sep 07 14:01:59 2012 +0200
@@ -3321,6 +3321,9 @@
 msgid "new"
 msgstr "Nuevo"
 
+msgid "next page"
+msgstr ""
+
 msgid "next_results"
 msgstr "Siguientes resultados"
 
@@ -3522,6 +3525,9 @@
 msgid "preferences"
 msgstr "Preferencias"
 
+msgid "previous page"
+msgstr ""
+
 msgid "previous_results"
 msgstr "Resultados Anteriores"
 
@@ -4669,3 +4675,10 @@
 msgstr ""
 "usted debe  quitar la puesta en línea de la relación %s que es aceptada y "
 "puede ser cruzada"
+
+#~ msgid ""
+#~ "Can't restore relation %(rtype)s of entity %(eid)s, this relation does "
+#~ "not exists anymore in the schema."
+#~ msgstr ""
+#~ "No puede restaurar la relación %(rtype)s de la entidad %(eid)s, esta "
+#~ "relación ya no existe en el esquema."
--- a/i18n/fr.po	Fri Aug 03 13:29:37 2012 +0200
+++ b/i18n/fr.po	Fri Sep 07 14:01:59 2012 +0200
@@ -2765,7 +2765,7 @@
 msgstr "contient le texte"
 
 msgid "header-center"
-msgstr ""
+msgstr "en-tête (centre)"
 
 msgid "header-left"
 msgstr "en-tête (gauche)"
@@ -3328,6 +3328,9 @@
 msgid "new"
 msgstr "nouveau"
 
+msgid "next page"
+msgstr "page suivante"
+
 msgid "next_results"
 msgstr "résultats suivants"
 
@@ -3531,6 +3534,9 @@
 msgid "preferences"
 msgstr "préférences"
 
+msgid "previous page"
+msgstr "page précédente"
+
 msgid "previous_results"
 msgstr "résultats précédents"
 
@@ -4122,10 +4128,14 @@
 msgstr "la valeur \"%s\" est déjà utilisée, veuillez utiliser une autre valeur"
 
 msgid "there is no next page"
-msgstr "il n'y a pas de page suivante"
+msgstr "Il n'y a pas de page suivante"
 
 msgid "there is no previous page"
-msgstr "il n'y a pas de page précédente"
+msgstr "Il n'y a pas de page précédente"
+
+#, python-format
+msgid "there is no transaction #%s"
+msgstr "Il n'y a pas de transaction #%s"
 
 #, python-format
 msgid "there is no transaction #%s"
@@ -4343,7 +4353,7 @@
 msgstr "valeur non autorisée"
 
 msgid "undefined user"
-msgstr ""
+msgstr "utilisateur inconnu"
 
 msgid "undo"
 msgstr "annuler"
@@ -4679,3 +4689,24 @@
 msgstr ""
 "vous devriez enlevé la mise en ligne de la relation %s qui est supportée et "
 "peut-être croisée"
+
+#~ msgid "Action"
+#~ msgstr "Action"
+
+#~ msgid "day"
+#~ msgstr "jour"
+
+#~ msgid "log out first"
+#~ msgstr "déconnecter vous d'abord"
+
+#~ msgid "month"
+#~ msgstr "mois"
+
+#~ msgid "today"
+#~ msgstr "aujourd'hui"
+
+#~ msgid "undo last change"
+#~ msgstr "annuler dernier changement"
+
+#~ msgid "week"
+#~ msgstr "semaine"
--- a/migration.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/migration.py	Fri Sep 07 14:01:59 2012 +0200
@@ -514,7 +514,9 @@
                     elif op == None:
                         continue
                     else:
-                        print 'unable to handle this case', oper, version, op, ver
+                        print ('unable to handle %s in %s, set to `%s %s` '
+                               'but currently up to `%s %s`' %
+                               (cube, source, oper, version, op, ver))
             # "solve" constraint satisfaction problem
             if cube not in self.cubes:
                 self.errors.append( ('add', cube, version, source) )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.15.4_Any.py	Fri Sep 07 14:01:59 2012 +0200
@@ -0,0 +1,11 @@
+from logilab.common.shellutils import generate_password
+from cubicweb.server.utils import crypt_password
+
+for user in rql('CWUser U WHERE U cw_source S, S name "system", U upassword P, U login L').entities():
+    salt = user.upassword.getvalue()
+    if crypt_password('', salt) == salt:
+        passwd = generate_password()
+        print 'setting random password for user %s' % user.login
+        user.set_attributes(upassword=passwd)
+
+commit()
--- a/misc/scripts/ldapuser2ldapfeed.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/misc/scripts/ldapuser2ldapfeed.py	Fri Sep 07 14:01:59 2012 +0200
@@ -3,6 +3,8 @@
 Once this script is run, execute c-c db-check to cleanup relation tables.
 """
 import sys
+from collections import defaultdict
+from logilab.common.shellutils import generate_password
 
 try:
     source_name, = __args__
@@ -33,44 +35,65 @@
 
 print '******************** backport entity content ***************************'
 
-todelete = {}
+todelete = defaultdict(list)
+extids = set()
+duplicates = []
 for entity in rql('Any X WHERE X cw_source S, S eid %(s)s', {'s': source.eid}).entities():
-        etype = entity.__regid__
-        if not source.support_entity(etype):
-            print "source doesn't support %s, delete %s" % (etype, entity.eid)
-        else:
-            try:
-                entity.complete()
-            except Exception:
-                print '%s %s much probably deleted, delete it (extid %s)' % (
-                    etype, entity.eid, entity.cw_metainformation()['extid'])
-            else:
-                print 'get back', etype, entity.eid
-                entity.cw_edited = EditedEntity(entity, **entity.cw_attr_cache)
-                if not entity.creation_date:
-                    entity.cw_edited['creation_date'] = datetime.now()
-                if not entity.modification_date:
-                    entity.cw_edited['modification_date'] = datetime.now()
-                if not entity.upassword:
-                    entity.cw_edited['upassword'] = u''
-                if not entity.cwuri:
-                    entity.cw_edited['cwuri'] = '%s/?dn=%s' % (
-                        source.urls[0], entity.cw_metainformation()['extid'])
-                print entity.cw_edited
-                system_source.add_entity(session, entity)
-                sql("UPDATE entities SET source='system' "
-                    "WHERE eid=%(eid)s", {'eid': entity.eid})
-                continue
-        todelete.setdefault(etype, []).append(entity)
+    etype = entity.__regid__
+    if not source.support_entity(etype):
+        print "source doesn't support %s, delete %s" % (etype, entity.eid)
+        todelete[etype].append(entity)
+        continue
+    try:
+        entity.complete()
+    except Exception:
+        print '%s %s much probably deleted, delete it (extid %s)' % (
+            etype, entity.eid, entity.cw_metainformation()['extid'])
+        todelete[etype].append(entity)
+        continue
+    print 'get back', etype, entity.eid
+    entity.cw_edited = EditedEntity(entity, **entity.cw_attr_cache)
+    if not entity.creation_date:
+        entity.cw_edited['creation_date'] = datetime.now()
+    if not entity.modification_date:
+        entity.cw_edited['modification_date'] = datetime.now()
+    if not entity.upassword:
+        entity.cw_edited['upassword'] = generate_password()
+    extid = entity.cw_metainformation()['extid']
+    if not entity.cwuri:
+        entity.cw_edited['cwuri'] = '%s/?dn=%s' % (
+            source.urls[0], extid.decode('utf-8', 'ignore'))
+    print entity.cw_edited
+    if extid in extids:
+        duplicates.append(extid)
+        continue
+    extids.add(extid)
+    system_source.add_entity(session, entity)
+    sql("UPDATE entities SET source='system' "
+        "WHERE eid=%(eid)s", {'eid': entity.eid})
 
 # only cleanup entities table, remaining stuff should be cleaned by a c-c
 # db-check to be run after this script
-for entities in todelete.values():
+if duplicates:
+    print 'found %s duplicate entries' % len(duplicates)
+    from pprint import pprint
+    pprint(duplicates)
+
+print len(todelete), 'entities will be deleted'
+for etype, entities in todelete.values():
+    print 'deleting', etype, [e.login for e in entities]
     system_source.delete_info_multi(session, entities, source_name)
 
 
+
 source_ent = rql('CWSource S WHERE S eid %(s)s', {'s': source.eid}).get_entity(0, 0)
 source_ent.cw_set(type=u"ldapfeed", parser=u"ldapfeed")
 
 
-commit()
+if raw_input('Commit ?') in 'yY':
+    print 'committing'
+    commit()
+else:
+    rollback()
+    print 'rollbacked'
+
--- a/predicates.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/predicates.py	Fri Sep 07 14:01:59 2012 +0200
@@ -352,12 +352,12 @@
     """
 
     def __call__(self, cls, req, rset=None, row=None, col=0, accept_none=None,
-                 **kwargs):
-        if not rset and not kwargs.get('entity'):
+                 entity=None, **kwargs):
+        if not rset and entity is None:
             return 0
         score = 0
-        if kwargs.get('entity'):
-            score = self.score_entity(kwargs['entity'])
+        if entity is not None:
+            score = self.score_entity(entity)
         elif row is None:
             col = col or 0
             if accept_none is None:
@@ -558,7 +558,7 @@
 @objectify_predicate
 def nonempty_rset(cls, req, rset=None, **kwargs):
     """Return 1 for result set containing one ore more rows."""
-    if rset is not None and rset.rowcount:
+    if rset:
         return 1
     return 0
 
@@ -567,7 +567,7 @@
 @objectify_predicate
 def empty_rset(cls, req, rset=None, **kwargs):
     """Return 1 for result set which doesn't contain any row."""
-    if rset is not None and rset.rowcount == 0:
+    if rset is not None and len(rset) == 0:
         return 1
     return 0
 
@@ -580,7 +580,7 @@
     """
     if rset is None and 'entity' in kwargs:
         return 1
-    if rset is not None and (row is not None or rset.rowcount == 1):
+    if rset is not None and (row is not None or len(rset) == 1):
         return 1
     return 0
 
@@ -608,7 +608,7 @@
         return self.operator(num, self.expected)
 
     def __call__(self, cls, req, rset=None, **kwargs):
-        return int(rset is not None and self.match_expected(rset.rowcount))
+        return int(rset is not None and self.match_expected(len(rset)))
 
 
 class multi_columns_rset(multi_lines_rset):
@@ -648,7 +648,7 @@
                 page_size = req.property_value('navigation.page-size')
             else:
                 page_size = int(page_size)
-        if rset.rowcount <= (page_size*self.nbpages):
+        if len(rset) <= (page_size*self.nbpages):
             return 0
         return self.nbpages
 
@@ -1080,9 +1080,9 @@
 
     # don't use EntityPredicate.__call__ but this optimized implementation to
     # avoid considering each entity when it's not necessary
-    def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
-        if kwargs.get('entity'):
-            return self.score_entity(kwargs['entity'])
+    def __call__(self, cls, req, rset=None, row=None, col=0, entity=None, **kwargs):
+        if entity is not None:
+            return self.score_entity(entity)
         if rset is None:
             return 0
         if row is None:
--- a/server/checkintegrity.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/server/checkintegrity.py	Fri Sep 07 14:01:59 2012 +0200
@@ -317,7 +317,7 @@
     print 'Checking mandatory relations'
     msg = '%s #%s is missing mandatory %s relation %s (autofix will delete the entity)'
     for rschema in schema.relations():
-        if rschema.final or rschema.type in PURE_VIRTUAL_RTYPES:
+        if rschema.final or rschema in PURE_VIRTUAL_RTYPES or rschema in ('is', 'is_instance_of'):
             continue
         smandatory = set()
         omandatory = set()
--- a/server/hook.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/server/hook.py	Fri Sep 07 14:01:59 2012 +0200
@@ -236,8 +236,8 @@
 or rollback() will restore the hooks.
 
 
-Hooks specific predicate
-~~~~~~~~~~~~~~~~~~~~~~~~
+Hooks specific predicates
+~~~~~~~~~~~~~~~~~~~~~~~~~
 .. autoclass:: cubicweb.server.hook.match_rtype
 .. autoclass:: cubicweb.server.hook.match_rtype_sets
 
@@ -473,16 +473,18 @@
     argument. The goal of this predicate is that it keeps reference to original sets,
     so modification to thoses sets are considered by the predicate. For instance
 
-    MYSET = set()
+    .. sourcecode:: python
+
+      MYSET = set()
 
-    class Hook1(Hook):
-        __regid__ = 'hook1'
-        __select__ = Hook.__select__ & match_rtype_sets(MYSET)
-        ...
+      class Hook1(Hook):
+          __regid__ = 'hook1'
+          __select__ = Hook.__select__ & match_rtype_sets(MYSET)
+          ...
 
-    class Hook2(Hook):
-        __regid__ = 'hook2'
-        __select__ = Hook.__select__ & match_rtype_sets(MYSET)
+      class Hook2(Hook):
+          __regid__ = 'hook2'
+          __select__ = Hook.__select__ & match_rtype_sets(MYSET)
 
     Client code can now change `MYSET`, this will changes the selection criteria
     of :class:`Hook1` and :class:`Hook1`.
--- a/server/ldaputils.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/server/ldaputils.py	Fri Sep 07 14:01:59 2012 +0200
@@ -250,10 +250,11 @@
         except ldap.LDAPError: # Invalid protocol version, fall back safely
             conn.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION2)
         # Deny auto-chasing of referrals to be safe, we handle them instead
-        #try:
-        #    connection.set_option(ldap.OPT_REFERRALS, 0)
-        #except ldap.LDAPError: # Cannot set referrals, so do nothing
-        #    pass
+        # Required for AD
+        try:
+           conn.set_option(ldap.OPT_REFERRALS, 0)
+        except ldap.LDAPError: # Cannot set referrals, so do nothing
+           pass
         #conn.set_option(ldap.OPT_NETWORK_TIMEOUT, conn_timeout)
         #conn.timeout = op_timeout
         # Now bind with the credentials given. Let exceptions propagate out.
--- a/server/migractions.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/server/migractions.py	Fri Sep 07 14:01:59 2012 +0200
@@ -1526,9 +1526,9 @@
     def cmd_reactivate_verification_hooks(self):
         self.session.enable_hook_categories('integrity')
 
-    @deprecated("[3.15] use session.rename_relation_type(oldname, newname)")
+    @deprecated("[3.15] use rename_relation_type(oldname, newname)")
     def cmd_rename_relation(self, oldname, newname, commit=True):
-        self.session.rename_relation_type(oldname, newname, commit)
+        self.cmd_rename_relation_type(oldname, newname, commit)
 
 
 class ForRqlIterator:
--- a/server/session.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/server/session.py	Fri Sep 07 14:01:59 2012 +0200
@@ -106,7 +106,8 @@
         self.free_cnxset = free_cnxset
 
     def __enter__(self):
-        pass
+        # ensure session has a cnxset
+        self.session.set_cnxset()
 
     def __exit__(self, exctype, exc, traceback):
         if exctype:
--- a/server/sources/datafeed.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/server/sources/datafeed.py	Fri Sep 07 14:01:59 2012 +0200
@@ -152,21 +152,24 @@
 
     def update_latest_retrieval(self, session):
         self.latest_retrieval = datetime.utcnow()
+        session.set_cnxset()
         session.execute('SET X latest_retrieval %(date)s WHERE X eid %(x)s',
                         {'x': self.eid, 'date': self.latest_retrieval})
+        session.commit()
 
     def acquire_synchronization_lock(self, session):
         # XXX race condition until WHERE of SET queries is executed using
         # 'SELECT FOR UPDATE'
         now = datetime.utcnow()
+        session.set_cnxset()
         if not session.execute(
             'SET X in_synchronization %(now)s WHERE X eid %(x)s, '
             'X in_synchronization NULL OR X in_synchronization < %(maxdt)s',
             {'x': self.eid, 'now': now, 'maxdt': now - self.max_lock_lifetime}):
             self.error('concurrent synchronization detected, skip pull')
-            session.commit(free_cnxset=False)
+            session.commit()
             return False
-        session.commit(free_cnxset=False)
+        session.commit()
         return True
 
     def release_synchronization_lock(self, session):
@@ -205,7 +208,9 @@
             importlog.record_info('added %s entities' % len(stats['created']))
         if stats.get('updated'):
             importlog.record_info('updated %s entities' % len(stats['updated']))
+        session.set_cnxset()
         importlog.write_log(session, end_timestamp=self.latest_retrieval)
+        session.commit()
         return stats
 
     def process_urls(self, parser, urls, raise_on_error=False):
@@ -376,8 +381,10 @@
                     byetype.setdefault(etype, []).append(str(eid))
             for etype, eids in byetype.iteritems():
                 self.warning('delete %s %s entities', len(eids), etype)
+                session.set_cnxset()
                 session.execute('DELETE %s X WHERE X eid IN (%s)'
                                 % (etype, ','.join(eids)))
+                session.commit()
 
     def update_if_necessary(self, entity, attrs):
         entity.complete(tuple(attrs))
--- a/server/sources/native.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/server/sources/native.py	Fri Sep 07 14:01:59 2012 +0200
@@ -613,12 +613,10 @@
         etype = entities[0].__regid__
         for attr, storage in self._storages.get(etype, {}).items():
             for entity in entities:
-                try:
+                if event == 'deleted':
+                    storage.entity_deleted(entity, attr)
+                else:
                     edited = entity.cw_edited
-                except AttributeError:
-                    assert event == 'deleted'
-                    getattr(storage, 'entity_deleted')(entity, attr)
-                else:
                     if attr in edited:
                         handler = getattr(storage, 'entity_%s' % event)
                         to_restore = handler(entity, attr)
@@ -1597,9 +1595,10 @@
         pass
 
 class LoginPasswordAuthentifier(BaseAuthentifier):
-    passwd_rql = "Any P WHERE X is CWUser, X login %(login)s, X upassword P"
-    auth_rql = "Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s"
-    _sols = ({'X': 'CWUser', 'P': 'Password'},)
+    passwd_rql = 'Any P WHERE X is CWUser, X login %(login)s, X upassword P'
+    auth_rql = ('Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s, '
+                'X cw_source S, S name "system"')
+    _sols = ({'X': 'CWUser', 'P': 'Password', 'S': 'CWSource'},)
 
     def set_schema(self, schema):
         """set the instance'schema"""
--- a/server/test/unittest_ldapuser.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/server/test/unittest_ldapuser.py	Fri Sep 07 14:01:59 2012 +0200
@@ -32,7 +32,6 @@
 from cubicweb.devtools.httptest import get_available_port
 from cubicweb.devtools import get_test_db_handler
 
-from cubicweb.server.session import security_enabled
 from cubicweb.server.sources.ldapuser import GlobTrFunc, UnknownEid, RQL2LDAPFilter
 
 CONFIG = u'user-base-dn=ou=People,dc=cubicweb,dc=test'
@@ -110,10 +109,9 @@
 
     def _pull(self):
         with self.session.repo.internal_session() as isession:
-            with security_enabled(isession, read=False, write=False):
-                lfsource = isession.repo.sources_by_uri['ldapuser']
-                stats = lfsource.pull_data(isession, force=True, raise_on_error=True)
-                isession.commit()
+            lfsource = isession.repo.sources_by_uri['ldapuser']
+            stats = lfsource.pull_data(isession, force=True, raise_on_error=True)
+            isession.commit()
 
     def test_delete(self):
         """ delete syt, pull, check deactivation, repull,
@@ -138,6 +136,14 @@
         self.assertEqual(self.execute('Any N WHERE U login "syt", '
                                       'U in_state S, S name N').rows[0][0],
                          'deactivated')
+        # test reactivating the user isn't enough to authenticate, as the native source
+        # refuse to authenticate user from other sources
+        os.system(deletecmd)
+        self._pull()
+        user = self.execute('CWUser U WHERE U login "syt"').get_entity(0, 0)
+        user.cw_adapt_to('IWorkflowable').fire_transition('activate')
+        self.commit()
+        self.assertRaises(AuthenticationError, self.repo.connect, 'syt', password='syt')
 
 class LDAPFeedSourceTC(LDAPTestBase):
     test_db_id = 'ldap-feed'
--- a/server/test/unittest_repository.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/server/test/unittest_repository.py	Fri Sep 07 14:01:59 2012 +0200
@@ -113,6 +113,8 @@
         self.assertRaises(AuthenticationError,
                           self.repo.connect, self.admlogin, password='nimportnawak')
         self.assertRaises(AuthenticationError,
+                          self.repo.connect, self.admlogin, password='')
+        self.assertRaises(AuthenticationError,
                           self.repo.connect, self.admlogin, password=None)
         self.assertRaises(AuthenticationError,
                           self.repo.connect, None, password=None)
--- a/server/test/unittest_security.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/server/test/unittest_security.py	Fri Sep 07 14:01:59 2012 +0200
@@ -16,6 +16,7 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """functional tests for server'security"""
+from __future__ import with_statement
 
 import sys
 
--- a/sobjects/ldapparser.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/sobjects/ldapparser.py	Fri Sep 07 14:01:59 2012 +0200
@@ -20,6 +20,7 @@
 unlike ldapuser source, this source is copy based and will import ldap content
 (beside passwords for authentication) into the system source.
 """
+from __future__ import with_statement
 
 from logilab.common.decorators import cached
 from logilab.common.shellutils import generate_password
@@ -71,7 +72,9 @@
         session.commit(free_cnxset=False)
 
     def update_if_necessary(self, entity, attrs):
-        entity.complete(tuple(attrs))
+        # disable read security to allow password selection
+        with entity._cw.security_enabled(read=False):
+            entity.complete(tuple(attrs))
         if entity.__regid__ == 'CWUser':
             wf = entity.cw_adapt_to('IWorkflowable')
             if wf.state == 'deactivated':
--- a/test/unittest_entity.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/test/unittest_entity.py	Fri Sep 07 14:01:59 2012 +0200
@@ -18,6 +18,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """unit tests for cubicweb.web.views.entities module"""
 
+from __future__ import with_statement
+
 from datetime import datetime
 
 from logilab.common import tempattr
@@ -28,7 +30,7 @@
 from cubicweb.mttransforms import HAS_TAL
 from cubicweb.entities import fetch_config
 from cubicweb.uilib import soup2xhtml
-from cubicweb.schema import RQLVocabularyConstraint
+from cubicweb.schema import RQLVocabularyConstraint, RRQLExpression
 
 class EntityTC(CubicWebTC):
 
@@ -361,6 +363,18 @@
             'NOT (S connait AD, AD nom "toto"), AD is Personne, '
             'EXISTS(S travaille AE, AE nom "tutu")')
 
+    def test_unrelated_rql_security_rel_perms(self):
+        '''check `connait` add permission has no effect for a new entity on the
+        unrelated rql'''
+        rdef = self.schema['Personne'].rdef('connait')
+        perm_rrqle = RRQLExpression('U has_update_permission S')
+        with self.temporary_permissions((rdef, {'add': (perm_rrqle,)})):
+            person = self.vreg['etypes'].etype_class('Personne')(self.request())
+            rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0]
+        self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE '
+                         'O is Personne, O nom AA, O prenom AB, '
+                         'O modification_date AC')
+
     def test_unrelated_rql_constraints_edition_subject(self):
         person = self.request().create_entity('Personne', nom=u'sylvain')
         rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0]
--- a/web/application.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/web/application.py	Fri Sep 07 14:01:59 2012 +0200
@@ -381,6 +381,9 @@
                     content = self.loggedout_content(req)
                     # let the explicitly reset http credential
                     raise AuthenticationError()
+        except Redirect, ex:
+            # authentication needs redirection (eg openid)
+            content = self.redirect_handler(req, ex)
         # Wrong, absent or Reseted credential
         except AuthenticationError:
             # If there is an https url configured and
@@ -445,17 +448,9 @@
                 result = ex.content
                 req.status_out = ex.status
             except Redirect, ex:
-                # handle redirect
-                # - comply to ex status
-                # - set header field
-                #
-                # Redirect Maybe be is raised by edit controller when
-                # everything went fine, so try to commit
-                self.debug('redirecting to %s', str(ex.location))
-                req.headers_out.setHeader('location', str(ex.location))
-                assert 300<= ex.status < 400
-                req.status_out = ex.status
-                result = ''
+                # Redirect may be raised by edit controller when everything went
+                # fine, so attempt to commit
+                result = self.redirect_handler(req, ex)
             if req.cnx:
                 txuuid = req.cnx.commit()
                 commited = True
@@ -501,7 +496,20 @@
         self.debug('query %s executed in %s sec', req.relative_path(), clock() - tstart)
         return result
 
-    ### Error handler
+    # Error handlers
+
+    def redirect_handler(self, req, ex):
+        """handle redirect
+        - comply to ex status
+        - set header field
+        - return empty content
+        """
+        self.debug('redirecting to %s', str(ex.location))
+        req.headers_out.setHeader('location', str(ex.location))
+        assert 300 <= ex.status < 400
+        req.status_out = ex.status
+        return ''
+
     def validation_error_handler(self, req, ex):
         ex.errors = dict((k, v) for k, v in ex.errors.items())
         if '__errorurl' in req.form:
@@ -551,8 +559,6 @@
             req.build_url('undo', txuuid=txuuid), req._('undo'))
             req.append_to_redirect_message(msg)
 
-
-
     def ajax_error_handler(self, req, ex):
         req.set_header('content-type', 'application/json')
         status = ex.status
--- a/web/form.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/web/form.py	Fri Sep 07 14:01:59 2012 +0200
@@ -88,23 +88,19 @@
 
     def __init__(self, req, rset=None, row=None, col=None,
                  submitmsg=None, mainform=True, **kwargs):
-        super(Form, self).__init__(req, rset=rset, row=row, col=col)
+        # process kwargs first so we can properly pass them to Form and match
+        # order expectation (ie cw_extra_kwargs populated almost first)
+        hiddens, extrakw = self._process_kwargs(kwargs)
+        # now call ancestor init
+        super(Form, self).__init__(req, rset=rset, row=row, col=col, **extrakw)
+        # then continue with further specific initialization
         self.fields = list(self.__class__._fields_)
+        for key, val in hiddens:
+            self.add_hidden(key, val)
         if mainform:
             formid = kwargs.pop('formvid', self.__regid__)
             self.add_hidden(u'__form_id', formid)
             self._posting = self._cw.form.get('__form_id') == formid
-        for key, val in kwargs.iteritems():
-            if key in controller.NAV_FORM_PARAMETERS:
-                self.add_hidden(key, val)
-            elif key == 'redirect_path':
-                self.add_hidden(u'__redirectpath', val)
-            elif hasattr(self.__class__, key) and not key[0] == '_':
-                setattr(self, key, val)
-            else:
-                self.cw_extra_kwargs[key] = val
-            # skip other parameters, usually given for selection
-            # (else write a custom class to handle them)
         if mainform:
             self.add_hidden(u'__errorurl', self.session_key())
             self.add_hidden(u'__domid', self.domid)
@@ -119,6 +115,22 @@
         if submitmsg is not None:
             self.set_message(submitmsg)
 
+    def _process_kwargs(self, kwargs):
+        hiddens = []
+        extrakw = {}
+        # search for navigation parameters and customization of existing
+        # attributes; remaining stuff goes in extrakwargs
+        for key, val in kwargs.iteritems():
+            if key in controller.NAV_FORM_PARAMETERS:
+                hiddens.append( (key, val) )
+            elif key == 'redirect_path':
+                hiddens.append( (u'__redirectpath', val) )
+            elif hasattr(self.__class__, key) and not key[0] == '_':
+                setattr(self, key, val)
+            else:
+                extrakw[key] = val
+        return hiddens, extrakw
+
     def set_message(self, submitmsg):
         """sets a submitmsg if exists, using _cwmsgid mechanism """
         cwmsgid = self._cw.set_redirect_message(submitmsg)
--- a/web/formfields.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/web/formfields.py	Fri Sep 07 14:01:59 2012 +0200
@@ -313,6 +313,7 @@
 
     def role_name(self):
         """return <field.name>-<field.role> if role is specified, else field.name"""
+        assert self.name, 'field without a name (give it to constructor for explicitly built fields)'
         if self.role is not None:
             return role_name(self.name, self.role)
         return self.name
--- a/web/formwidgets.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/web/formwidgets.py	Fri Sep 07 14:01:59 2012 +0200
@@ -672,10 +672,11 @@
     """
     needs_js = ('jquery.ui.js', )
     needs_css = ('jquery.ui.css',)
+    default_size = 10
 
     def __init__(self, datestr=None, **kwargs):
         super(JQueryDatePicker, self).__init__(**kwargs)
-        self.datestr = datestr
+        self.value = datestr
 
     def _render(self, form, field, renderer):
         req = form._cw
@@ -689,44 +690,36 @@
                        '{buttonImage: "%s", dateFormat: "%s", firstDay: 1,'
                        ' showOn: "button", buttonImageOnly: true})' % (
                            domid, req.uiprops['CALENDAR_ICON'], fmt))
-        if self.datestr is None:
+        return self._render_input(form, field, domid)
+
+    def _render_input(self, form, field, domid):
+        if self.value is None:
             value = self.values(form, field)[0]
         else:
-            value = self.datestr
-        attrs = {}
-        if self.settabindex:
-            attrs['tabindex'] = req.next_tabindex()
-        return tags.input(id=domid, name=domid, value=value,
-                          type='text', size='10', **attrs)
+            value = self.value
+        attrs = self.attributes(form, field)
+        attrs.setdefault('size', unicode(self.default_size))
+        return tags.input(name=domid, value=value, type='text', **attrs)
 
 
-class JQueryTimePicker(FieldWidget):
+class JQueryTimePicker(JQueryDatePicker):
     """Use jquery.timePicker to define a time picker. Will return the time as an
     unicode string.
     """
     needs_js = ('jquery.timePicker.js',)
     needs_css = ('jquery.timepicker.css',)
+    default_size = 5
 
     def __init__(self, timestr=None, timesteps=30, separator=u':', **kwargs):
-        super(JQueryTimePicker, self).__init__(**kwargs)
-        self.timestr = timestr
+        super(JQueryTimePicker, self).__init__(timestr, **kwargs)
         self.timesteps = timesteps
         self.separator = separator
 
     def _render(self, form, field, renderer):
-        req = form._cw
         domid = field.dom_id(form, self.suffix)
-        req.add_onload(u'cw.jqNode("%s").timePicker({selectedTime: "%s", step: %s, separator: "%s"})' % (
-            domid, self.timestr, self.timesteps, self.separator))
-        if self.timestr is None:
-            value = self.values(form, field)[0]
-        else:
-            value = self.timestr
-        attrs = {}
-        if self.settabindex:
-            attrs['tabindex'] = req.next_tabindex()
-        return tags.input(id=domid, name=domid, value=value,
-                          type='text', size='5')
+        form._cw.add_onload(u'cw.jqNode("%s").timePicker({step: %s, separator: "%s"})' % (
+                domid, self.timesteps, self.separator))
+        return self._render_input(form, field, domid)
 
 
 class JQueryDateTimePicker(FieldWidget):
--- a/web/request.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/web/request.py	Fri Sep 07 14:01:59 2012 +0200
@@ -170,8 +170,7 @@
     @property
     def authmode(self):
         """Authentification mode of the instance
-
-        (see :ref:`Configuring the Web server`)"""
+        (see :ref:`WebServerConfig`)"""
         return self.vreg.config['auth-mode']
 
     # Various variable generator.
--- a/web/test/unittest_formfields.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/web/test/unittest_formfields.py	Fri Sep 07 14:01:59 2012 +0200
@@ -147,7 +147,7 @@
     def test_property_key_field(self):
         from cubicweb.web.views.cwproperties import PropertyKeyField
         req = self.request()
-        field = PropertyKeyField()
+        field = PropertyKeyField(name='test')
         e = self.vreg['etypes'].etype_class('CWProperty')(req)
         renderer = self.vreg['formrenderers'].select('base', req)
         form = EntityFieldsForm(req, entity=e)
--- a/web/test/unittest_views_basecontrollers.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/web/test/unittest_views_basecontrollers.py	Fri Sep 07 14:01:59 2012 +0200
@@ -781,6 +781,14 @@
         res, req = self.remote_call('foo')
         self.assertEqual(res, '12')
 
+    def test_monkeypatch_jsoncontroller_stdfunc(self):
+        @monkeypatch(JSonController)
+        @jsonize
+        def js_reledit_form(self):
+            return 12
+        res, req = self.remote_call('reledit_form')
+        self.assertEqual(res, '12')
+
 
 class UndoControllerTC(CubicWebTC):
 
@@ -843,16 +851,16 @@
 
     def test_login_with_dest(self):
         req = self.request()
-        req.form = {'postlogin_path': '/elephants/babar'}
+        req.form = {'postlogin_path': 'elephants/babar'}
         with self.assertRaises(Redirect) as cm:
             self.ctrl_publish(req, ctrl='login')
-        self.assertEqual('/elephants/babar', cm.exception.location)
+        self.assertEqual(req.build_url('elephants/babar'), cm.exception.location)
 
     def test_login_no_dest(self):
         req = self.request()
         with self.assertRaises(Redirect) as cm:
             self.ctrl_publish(req, ctrl='login')
-        self.assertEqual('.', cm.exception.location)
+        self.assertEqual(req.base_url(), cm.exception.location)
 
 if __name__ == '__main__':
     unittest_main()
--- a/web/views/ajaxcontroller.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/web/views/ajaxcontroller.py	Fri Sep 07 14:01:59 2012 +0200
@@ -63,6 +63,7 @@
 
 __docformat__ = "restructuredtext en"
 
+from warnings import warn
 from functools import partial
 
 from logilab.common.date import strptime
@@ -114,22 +115,20 @@
             fname = self._cw.form['fname']
         except KeyError:
             raise RemoteCallFailed('no method specified')
+        # 1/ check first for old-style (JSonController) ajax func for bw compat
         try:
-            func = self._cw.vreg['ajax-func'].select(fname, self._cw)
-        except ObjectNotFound:
-            # function not found in the registry, inspect JSonController for
-            # backward compatibility
+            func = getattr(basecontrollers.JSonController, 'js_%s' % fname).im_func
+            func = partial(func, self)
+        except AttributeError:
+            # 2/ check for new-style (AjaxController) ajax func
             try:
-                func = getattr(basecontrollers.JSonController, 'js_%s' % fname).im_func
-                func = partial(func, self)
-            except AttributeError:
+                func = self._cw.vreg['ajax-func'].select(fname, self._cw)
+            except ObjectNotFound:
                 raise RemoteCallFailed('no %s method' % fname)
-            else:
-                self.warning('remote function %s found on JSonController, '
-                             'use AjaxFunction / @ajaxfunc instead', fname)
-        except NoSelectableObject:
-            raise RemoteCallFailed('method %s not available in this context'
-                                   % fname)
+        else:
+            warn('[3.15] remote function %s found on JSonController, '
+                 'use AjaxFunction / @ajaxfunc instead' % fname,
+                 DeprecationWarning, stacklevel=2)
         # no <arg> attribute means the callback takes no argument
         args = self._cw.form.get('arg', ())
         if not isinstance(args, (list, tuple)):
--- a/web/views/basecontrollers.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/web/views/basecontrollers.py	Fri Sep 07 14:01:59 2012 +0200
@@ -90,8 +90,10 @@
 
     def publish(self, rset=None):
         """log in the instance"""
-        path = self._cw.form.get('postlogin_path', '.')
-        raise Redirect(path)
+        path = self._cw.form.get('postlogin_path', '')
+        # redirect expect an url, not a path. Also path may contains a query
+        # string, hence should not be given to _cw.build_url()
+        raise Redirect(self._cw.base_url() + path)
 
 
 class LogoutController(Controller):
--- a/web/views/basetemplates.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/web/views/basetemplates.py	Fri Sep 07 14:01:59 2012 +0200
@@ -449,7 +449,7 @@
 
     def form_action(self):
         if self.action is None:
-            # reuse existing redirection if it exist
+            # reuse existing redirection if it exists
             target = self._cw.form.get('postlogin_path',
                                        self._cw.relative_path())
             url_args = {}
--- a/web/views/navigation.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/web/views/navigation.py	Fri Sep 07 14:01:59 2012 +0200
@@ -364,11 +364,13 @@
 
     @property
     def prev_icon(self):
-        return '<img src="%s"/>' % xml_escape(self._cw.data_url('go_prev.png'))
+        return '<img src="%s" alt="%s" />' % (
+            xml_escape(self._cw.data_url('go_prev.png')), self._cw._('previous page'))
 
     @property
     def next_icon(self):
-        return '<img src="%s"/>' % xml_escape(self._cw.data_url('go_next.png'))
+        return '<img src="%s" alt="%s" />' % (
+            xml_escape(self._cw.data_url('go_next.png')), self._cw._('next page'))
 
     def init_rendering(self):
         adapter = self.entity.cw_adapt_to('IPrevNext')
--- a/web/views/tableview.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/web/views/tableview.py	Fri Sep 07 14:01:59 2012 +0200
@@ -174,8 +174,8 @@
 
     @cachedproperty
     def initial_load(self):
-        """We detect a bit heuristically if we are built for the first time.
-        or from subsequent calls by the form filter or by the pagination hooks.
+        """We detect a bit heuristically if we are built for the first time or
+        from subsequent calls by the form filter or by the pagination hooks.
         """
         form = self._cw.form
         return 'fromformfilter' not in form and '__fromnavigation' not in form
--- a/web/views/tabs.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/web/views/tabs.py	Fri Sep 07 14:01:59 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -90,6 +90,7 @@
 
 class TabsMixin(LazyViewMixin):
     """a tab mixin to easily get jQuery based, lazy, ajax tabs"""
+    lazy = True
 
     @property
     def cookie_name(self):
@@ -122,7 +123,7 @@
             vid = tabkwargs.get('vid', tabid)
             domid = uilib.domid(tabid)
             try:
-                viewsvreg.select(vid, self._cw, **tabkwargs)
+                viewsvreg.select(vid, self._cw, tabid=domid, **tabkwargs)
             except NoSelectableObject:
                 continue
             selected_tabs.append((tabid, domid, tabkwargs))
@@ -157,17 +158,20 @@
         w(u'</ul>')
         for tabid, domid, tabkwargs in tabs:
             w(u'<div id="%s">' % domid)
-            tabkwargs.setdefault('tabid', domid)
-            tabkwargs.setdefault('vid', tabid)
-            tabkwargs.setdefault('rset', self.cw_rset)
-            self.lazyview(**tabkwargs)
+            if self.lazy:
+                tabkwargs.setdefault('tabid', domid)
+                tabkwargs.setdefault('vid', tabid)
+                self.lazyview(**tabkwargs)
+            else:
+                self._cw.view(tabid, w=self.w, **tabkwargs)
             w(u'</div>')
         w(u'</div>')
         # call the setTab() JS function *after* each tab is generated
         # because the callback binding needs to be done before
         # XXX make work history: true
-        self._cw.add_onload(u"""
-  jQuery('#entity-tabs-%(eeid)s').tabs(
+        if self.lazy:
+            self._cw.add_onload(u"""
+  jQuery('#entity-tabs-%(uid)s').tabs(
     { selected: %(tabindex)s,
       select: function(event, ui) {
         setTab(ui.panel.id, '%(cookiename)s');
@@ -175,9 +179,13 @@
     });
   setTab('%(domid)s', '%(cookiename)s');
 """ % {'tabindex'   : active_tab_idx,
-       'domid'        : active_tab,
-       'eeid'       : (entity and entity.eid or uid),
+       'domid'      : active_tab,
+       'uid'        : uid,
        'cookiename' : self.cookie_name})
+        else:
+            self._cw.add_onload(
+                u"jQuery('#entity-tabs-%(uid)s').tabs({selected: %(tabindex)s});"
+                % {'tabindex': active_tab_idx, 'uid': uid})
 
 
 class EntityRelationView(EntityView):
@@ -218,8 +226,7 @@
     tabs = [_('main_tab')]
     default_tab = 'main_tab'
 
-    def cell_call(self, row, col):
-        entity = self.cw_rset.complete_entity(row, col)
+    def render_entity(self, entity):
         self.render_entity_toolbox(entity)
         self.w(u'<div class="tabbedprimary"></div>')
         self.render_entity_title(entity)
--- a/web/views/workflow.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/web/views/workflow.py	Fri Sep 07 14:01:59 2012 +0200
@@ -315,7 +315,7 @@
     wf = req.entity_from_eid(wfeid)
     rschema = req.vreg.schema[field.name]
     param = 'toeid' if field.role == 'subject' else 'fromeid'
-    return sorted((e.view('combobox'), e.eid)
+    return sorted((e.view('combobox'), unicode(e.eid))
                   for e in getattr(wf, 'reverse_%s' % wfrelation)
                   if rschema.has_perm(req, 'add', **{param: e.eid}))
 
@@ -330,12 +330,14 @@
 
 def transition_states_vocabulary(form, field):
     entity = form.edited_entity
-    if not entity.has_eid():
+    if entity.has_eid():
+        wfeid = entity.transition_of[0].eid
+    else:
         eids = form.linked_to.get(('transition_of', 'subject'))
         if not eids:
             return []
-        return _wf_items_for_relation(form._cw, eids[0], 'state_of', field)
-    return field.relvoc_unrelated(form)
+        wfeid = eids[0]
+    return _wf_items_for_relation(form._cw, wfeid, 'state_of', field)
 
 _afs.tag_subject_of(('*', 'destination_state', '*'), 'main', 'attributes')
 _affk.tag_subject_of(('*', 'destination_state', '*'),
@@ -348,12 +350,14 @@
 
 def state_transitions_vocabulary(form, field):
     entity = form.edited_entity
-    if not entity.has_eid():
+    if entity.has_eid():
+        wfeid = entity.state_of[0].eid
+    else :
         eids = form.linked_to.get(('state_of', 'subject'))
-        if eids:
-            return _wf_items_for_relation(form._cw, eids[0], 'transition_of', field)
-        return []
-    return field.relvoc_unrelated(form)
+        if not eids:
+            return []
+        wfeid = eids[0]
+    return _wf_items_for_relation(form._cw, wfeid, 'transition_of', field)
 
 _afs.tag_subject_of(('State', 'allowed_transition', '*'), 'main', 'attributes')
 _affk.tag_subject_of(('State', 'allowed_transition', '*'),
--- a/web/webconfig.py	Fri Aug 03 13:29:37 2012 +0200
+++ b/web/webconfig.py	Fri Sep 07 14:01:59 2012 +0200
@@ -331,10 +331,9 @@
         self.datadir_url = baseurl + data_relpath
 
     def data_relpath(self):
-        if (self.debugmode or self.mode == 'test'):
+        if self.mode == 'test':
             return 'data/'
-        else:
-            return 'data/%s/' % self.instance_md5_version()
+        return 'data/%s/' % self.instance_md5_version()
 
     def _build_ui_properties(self):
         # self.datadir_url[:-1] to remove trailing /