[merge] start 3.18 development
authorDavid Douard <david.douard@logilab.fr>
Tue, 09 Jul 2013 15:58:26 +0200
changeset 9146 9b58a6406a64
parent 9015 65b8236e1bb4 (current diff)
parent 9145 32b4d5314fd9 (diff)
child 9147 01124cfd4b1f
[merge] start 3.18 development
debian.hardy/compat
debian.hardy/rules
server/test/unittest_ldapsource.py
--- a/__pkginfo__.py	Thu Jun 13 17:27:43 2013 +0200
+++ b/__pkginfo__.py	Tue Jul 09 15:58:26 2013 +0200
@@ -22,7 +22,7 @@
 
 modname = distname = "cubicweb"
 
-numversion = (3, 17, 2)
+numversion = (3, 17, 3)
 version = '.'.join(str(num) for num in numversion)
 
 description = "a repository of entities / relations for knowledge management"
--- a/cubicweb.spec	Thu Jun 13 17:27:43 2013 +0200
+++ b/cubicweb.spec	Tue Jul 09 15:58:26 2013 +0200
@@ -7,7 +7,7 @@
 %endif
 
 Name:           cubicweb
-Version:        3.17.2
+Version:        3.17.3
 Release:        logilab.1%{?dist}
 Summary:        CubicWeb is a semantic web application framework
 Source0:        http://download.logilab.org/pub/cubicweb/cubicweb-%{version}.tar.gz
--- a/debian.hardy/compat	Thu Jun 13 17:27:43 2013 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-5
--- a/debian.hardy/rules	Thu Jun 13 17:27:43 2013 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,82 +0,0 @@
-#!/usr/bin/make -f
-# Sample debian/rules that uses debhelper.
-# GNU copyright 1997 to 1999 by Joey Hess.
-
-# Uncomment this to turn on verbose mode.
-#export DH_VERBOSE=1
-
-PY_VERSION:=$(shell pyversions -d)
-
-build: build-stamp
-build-stamp: 
-	dh_testdir
-	# XXX doesn't work if logilab-doctools, logilab-xml are not in build depends
-	# and I can't get pbuilder find them in its chroot :(
-	# cd doc && make
-	# FIXME cleanup and use sphinx-build as build-depends ?
-	NO_SETUPTOOLS=1 python setup.py build
-	touch build-stamp
-
-clean: 
-	dh_testdir
-	dh_testroot
-	rm -f build-stamp configure-stamp
-	rm -rf build
-	#rm -rf debian/cubicweb-*/
-	find . -name "*.pyc" -delete
-	rm -f $(basename $(wildcard debian/*.in))
-	dh_clean
-
-install: build $(basename $(wildcard debian/*.in))
-	dh_testdir
-	dh_testroot
-	dh_clean
-	dh_installdirs
-
-	NO_SETUPTOOLS=1 python setup.py -q install --no-compile --prefix=debian/tmp/usr
-
-	# Put all the python library and data in cubicweb-common
-	# and scripts in cubicweb-server
-	dh_install -vi
-	# cwctl in the cubicweb-ctl package
-	rm -f debian/cubicweb-common/usr/share/pyshared/cubicweb/cwctl.py
-
-
-	# Remove unittests directory (should be available in cubicweb-dev only)
-	rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/site-packages/cubicweb/server/test
-	rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/site-packages/cubicweb/hooks/test
-	rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/site-packages/cubicweb/sobjects/test
-	rm -rf debian/cubicweb-web/usr/lib/${PY_VERSION}/site-packages/cubicweb/web/test
-	rm -rf debian/cubicweb-twisted/usr/lib/${PY_VERSION}/site-packages/cubicweb/etwist/test
-	rm -rf debian/cubicweb-common/usr/lib/${PY_VERSION}/site-packages/cubicweb/ext/test
-	rm -rf debian/cubicweb-common/usr/lib/${PY_VERSION}/site-packages/cubicweb/entities/test
-
-	# cubes directory must be managed as a valid python module
-	touch debian/cubicweb-common/usr/share/cubicweb/cubes/__init__.py
-
-%: %.in
-	sed "s/PY_VERSION/${PY_VERSION}/g" < $< > $@
-
-# Build architecture-independent files here.
-binary-indep: build install
-	dh_testdir
-	dh_testroot -i
-	dh_pycentral -i
-	dh_installinit -i -n --name cubicweb -u"defaults 99"
-	dh_installlogrotate -i
-	dh_installdocs -i -A README
-	dh_installman -i
-	dh_installchangelogs -i
-	dh_link -i
-	dh_compress -i -X.py -X.ini -X.xml
-	dh_fixperms -i
-	dh_installdeb -i
-	dh_gencontrol  -i
-	dh_md5sums -i
-	dh_builddeb -i
-
-binary-arch:
-
-binary: binary-indep 
-.PHONY: build clean binary binary-indep binary-arch
-
--- a/debian/changelog	Thu Jun 13 17:27:43 2013 +0200
+++ b/debian/changelog	Tue Jul 09 15:58:26 2013 +0200
@@ -1,3 +1,9 @@
+cubicweb (3.17.3-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- David Douard <david.douard@logilab.fr>  Tue, 09 Jul 2013 15:10:16 +0200
+
 cubicweb (3.17.2-1) unstable; urgency=low
 
   * new upstream release
--- a/devtools/httptest.py	Thu Jun 13 17:27:43 2013 +0200
+++ b/devtools/httptest.py	Tue Jul 09 15:58:26 2013 +0200
@@ -20,6 +20,7 @@
 """
 __docformat__ = "restructuredtext en"
 
+import random
 import threading
 import socket
 import httplib
@@ -46,6 +47,8 @@
 
     .. see:: :func:`test.test_support.bind_port`
     """
+    ports_scan = list(ports_scan)
+    random.shuffle(ports_scan)  # lower the chance of race condition
     for port in ports_scan:
         try:
             s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
--- a/entities/adapters.py	Thu Jun 13 17:27:43 2013 +0200
+++ b/entities/adapters.py	Tue Jul 09 15:58:26 2013 +0200
@@ -379,6 +379,7 @@
 class IUserFriendlyError(view.EntityAdapter):
     __regid__ = 'IUserFriendlyError'
     __abstract__ = True
+
     def __init__(self, *args, **kwargs):
         self.exc = kwargs.pop('exc')
         super(IUserFriendlyError, self).__init__(*args, **kwargs)
@@ -386,11 +387,27 @@
 
 class IUserFriendlyUniqueTogether(IUserFriendlyError):
     __select__ = match_exception(UniqueTogetherError)
+
     def raise_user_exception(self):
         etype, rtypes = self.exc.args
-        msg = self._cw._('violates unique_together constraints (%s)') % (
-            ', '.join([self._cw._(rtype) for rtype in rtypes]))
-        raise ValidationError(self.entity.eid, dict((col, msg) for col in rtypes))
+        # Because of index name size limits (e.g: postgres around 64,
+        # sqlserver around 128), we cannot be sure of what we got,
+        # especially for the rtypes part.
+        # Hence we will try to validate them, and handle invalid ones
+        # in the most user-friendly manner ...
+        _ = self._cw._
+        schema = self.entity._cw.vreg.schema
+        rtypes_msg = {}
+        for rtype in rtypes:
+            if rtype in schema:
+                rtypes_msg[rtype] = _('%s is part of violated unicity constraint') % rtype
+        globalmsg = _('some relations %sviolate a unicity constraint')
+        if len(rtypes) != len(rtypes_msg): # we got mangled/missing rtypes
+            globalmsg = globalmsg % _('(not all shown here) ')
+        else:
+            globalmsg = globalmsg % ''
+        rtypes_msg['unicity constraint'] = globalmsg
+        raise ValidationError(self.entity.eid, rtypes_msg)
 
 # deprecated ###################################################################
 
--- a/hooks/security.py	Thu Jun 13 17:27:43 2013 +0200
+++ b/hooks/security.py	Tue Jul 09 15:58:26 2013 +0200
@@ -23,10 +23,13 @@
 
 from logilab.common.registry import objectify_predicate
 
+from yams import buildobjs
+
 from cubicweb import Unauthorized
 from cubicweb.server import BEFORE_ADD_RELATIONS, ON_COMMIT_ADD_RELATIONS, hook
 
 
+_DEFAULT_UPDATE_ATTRPERM = buildobjs.DEFAULT_ATTRPERMS['update']
 def check_entity_attributes(session, entity, editedattrs=None, creation=False):
     eid = entity.eid
     eschema = entity.e_schema
@@ -39,9 +42,26 @@
         if attr in dontcheck:
             continue
         rdef = eschema.rdef(attr)
-        if rdef.final: # non final relation are checked by other hooks
-            # add/delete should be equivalent (XXX: unify them into 'update' ?)
-            if creation and not rdef.permissions.get('update'):
+        if rdef.final: # non final relation are checked by standard hooks
+            # attributes only have a specific 'update' permission
+            updateperm = rdef.permissions.get('update')
+            # comparison below works because the default update perm is:
+            #
+            #  ('managers', ERQLExpression(Any X WHERE U has_update_permission X, X eid %(x)s, U eid %(u)s))
+            #
+            # is deserialized in this order (groups first), and ERQLExpression
+            # implements comparison by expression.
+            if updateperm == _DEFAULT_UPDATE_ATTRPERM:
+                # The default update permission is to delegate to the entity
+                # update permission. This is an historical artefact but it is
+                # costly (in general). Hence we take this permission object as a
+                # marker saying "no specific" update permissions for this
+                # attribute. Thus we just do nothing.
+                continue
+            if creation and updateperm == ():
+                # That actually means an immutable attribute.  We make an
+                # _exception_ to the `check attr update perms at entity create &
+                # update time` rule for this case.
                 continue
             rdef.check_perm(session, 'update', eid=eid)
 
--- a/i18n/de.po	Thu Jun 13 17:27:43 2013 +0200
+++ b/i18n/de.po	Tue Jul 09 15:58:26 2013 +0200
@@ -114,6 +114,10 @@
 msgstr "%s Fehlerbericht"
 
 #, python-format
+msgid "%s is part of violated unicity constraint"
+msgstr ""
+
+#, python-format
 msgid "%s not estimated"
 msgstr "%s unbekannt(e)"
 
@@ -145,6 +149,9 @@
 msgid "(UNEXISTANT EID)"
 msgstr "(EID nicht gefunden)"
 
+msgid "(not all shown here) "
+msgstr ""
+
 #, python-format
 msgid "(suppressed) entity #%d"
 msgstr ""
@@ -3861,6 +3868,10 @@
 "Eine oder mehrere frühere Transaktion(en) betreffen die Tntität. Machen Sie "
 "sie zuerst rückgängig."
 
+#, python-format
+msgid "some relations %sviolate a unicity constraint"
+msgstr ""
+
 msgid "sorry, the server is unable to handle this query"
 msgstr "Der Server kann diese Anfrage leider nicht bearbeiten."
 
--- a/i18n/en.po	Thu Jun 13 17:27:43 2013 +0200
+++ b/i18n/en.po	Tue Jul 09 15:58:26 2013 +0200
@@ -106,6 +106,10 @@
 msgstr ""
 
 #, python-format
+msgid "%s is part of violated unicity constraint"
+msgstr ""
+
+#, python-format
 msgid "%s not estimated"
 msgstr ""
 
@@ -137,6 +141,9 @@
 msgid "(UNEXISTANT EID)"
 msgstr ""
 
+msgid "(not all shown here) "
+msgstr ""
+
 #, python-format
 msgid "(suppressed) entity #%d"
 msgstr ""
@@ -3766,6 +3773,10 @@
 msgid "some later transaction(s) touch entity, undo them first"
 msgstr ""
 
+#, python-format
+msgid "some relations %sviolate a unicity constraint"
+msgstr ""
+
 msgid "sorry, the server is unable to handle this query"
 msgstr ""
 
--- a/i18n/es.po	Thu Jun 13 17:27:43 2013 +0200
+++ b/i18n/es.po	Tue Jul 09 15:58:26 2013 +0200
@@ -115,6 +115,10 @@
 msgstr "%s reporte de errores"
 
 #, python-format
+msgid "%s is part of violated unicity constraint"
+msgstr ""
+
+#, python-format
 msgid "%s not estimated"
 msgstr "%s no estimado(s)"
 
@@ -146,6 +150,9 @@
 msgid "(UNEXISTANT EID)"
 msgstr "(EID INEXISTENTE"
 
+msgid "(not all shown here) "
+msgstr ""
+
 #, python-format
 msgid "(suppressed) entity #%d"
 msgstr ""
@@ -3909,6 +3916,10 @@
 msgstr ""
 "Las transacciones más recientes modificaron esta entidad, anúlelas primero"
 
+#, python-format
+msgid "some relations %sviolate a unicity constraint"
+msgstr ""
+
 msgid "sorry, the server is unable to handle this query"
 msgstr "Lo sentimos, el servidor no puede manejar esta consulta"
 
--- a/i18n/fr.po	Thu Jun 13 17:27:43 2013 +0200
+++ b/i18n/fr.po	Tue Jul 09 15:58:26 2013 +0200
@@ -115,6 +115,10 @@
 msgstr "%s rapport d'erreur"
 
 #, python-format
+msgid "%s is part of violated unicity constraint"
+msgstr "%s appartient à une contrainte d'unicité transgressée"
+
+#, python-format
 msgid "%s not estimated"
 msgstr "%s non estimé(s)"
 
@@ -148,6 +152,9 @@
 msgid "(UNEXISTANT EID)"
 msgstr "(EID INTROUVABLE)"
 
+msgid "(not all shown here) "
+msgstr "(toutes ne sont pas montrées)"
+
 #, python-format
 msgid "(suppressed) entity #%d"
 msgstr "entité #%d (supprimée)"
@@ -3922,6 +3929,10 @@
 msgstr ""
 "des transactions plus récentes modifient cette entité, annulez les d'abord"
 
+#, python-format
+msgid "some relations %sviolate a unicity constraint"
+msgstr "certaines relations %stransgressent une contrainte d'unicité"
+
 msgid "sorry, the server is unable to handle this query"
 msgstr "désolé, le serveur ne peut traiter cette requête"
 
--- a/server/repository.py	Thu Jun 13 17:27:43 2013 +0200
+++ b/server/repository.py	Tue Jul 09 15:58:26 2013 +0200
@@ -1661,7 +1661,7 @@
                 # client was not yet connected to the repo
                 return
             if not session.closed:
-                session.close()
+                self.close(session.id)
         daemon.removeConnection = removeConnection
         return daemon
 
--- a/server/session.py	Thu Jun 13 17:27:43 2013 +0200
+++ b/server/session.py	Tue Jul 09 15:58:26 2013 +0200
@@ -720,14 +720,13 @@
 class Session(RequestSessionBase):
     """Repository user session
 
-    This tie all together:
+    This ties all together:
      * session id,
      * user,
      * connections set,
      * other session data.
 
-    About session storage / transactions
-    ------------------------------------
+    **About session storage / transactions**
 
     Here is a description of internal session attributes. Besides :attr:`data`
     and :attr:`transaction_data`, you should not have to use attributes
--- a/server/sources/native.py	Thu Jun 13 17:27:43 2013 +0200
+++ b/server/sources/native.py	Tue Jul 09 15:58:26 2013 +0200
@@ -410,14 +410,14 @@
 
 
     def init(self, activated, source_entity):
-        super(NativeSQLSource, self).init(activated, source_entity)
-        self.init_creating(source_entity._cw.cnxset)
         try:
             # test if 'asource' column exists
             query = self.dbhelper.sql_add_limit_offset('SELECT asource FROM entities', 1)
             source_entity._cw.system_sql(query)
         except Exception as ex:
             self.eid_type_source = self.eid_type_source_pre_131
+        super(NativeSQLSource, self).init(activated, source_entity)
+        self.init_creating(source_entity._cw.cnxset)
 
     def shutdown(self):
         if self._eid_creation_cnx:
@@ -757,15 +757,15 @@
             if ex.__class__.__name__ == 'IntegrityError':
                 # need string comparison because of various backends
                 for arg in ex.args:
-                    mo = re.search('unique_cw_[^ ]+_idx', arg)
+                    # postgres and sqlserver
+                    mo = re.search('"unique_cw_[^ ]+"', arg)
                     if mo is not None:
-                        index_name = mo.group(0)
-                        # right-chop '_idx' postfix
-                        # (garanteed to be there, see regexp above)
-                        elements = index_name[:-4].split('_cw_')[1:]
+                        index_name = mo.group(0)[1:-1] # eat the surrounding " pair
+                        elements = index_name.split('_cw_')[1:]
                         etype = elements[0]
                         rtypes = elements[1:]
                         raise UniqueTogetherError(etype, rtypes)
+                    # sqlite
                     mo = re.search('columns (.*) are not unique', arg)
                     if mo is not None: # sqlite in use
                         # we left chop the 'cw_' prefix of attribute names
--- a/server/test/unittest_ldapsource.py	Thu Jun 13 17:27:43 2013 +0200
+++ b/server/test/unittest_ldapsource.py	Tue Jul 09 15:58:26 2013 +0200
@@ -142,16 +142,26 @@
         return self._pull(self.session)
 
     def setup_database(self):
-        if self.test_db_id == 'ldap-feed':
-            with self.session.repo.internal_session(safe=True) as session:
-                session.execute('DELETE Any E WHERE E cw_source S, S name "ldap"')
-                session.commit()
-        if self.test_db_id == 'ldap-feed':
-            src = self.sexecute('CWSource S WHERE S name "ldap"').get_entity(0,0)
-            src.cw_set(config=CONFIG_LDAPFEED)
-        self.session.commit()
+        with self.session.repo.internal_session(safe=True) as session:
+            session.execute('DELETE Any E WHERE E cw_source S, S name "ldap"')
+            session.execute('SET S config %(conf)s, S url %(url)s '
+                            'WHERE S is CWSource, S name "ldap"',
+                            {"conf": CONFIG_LDAPFEED, 'url': URL} )
+            session.commit()
         self.pull()
 
+    def add_ldap_entry(self, dn, mods):
+        """
+        add an LDAP entity
+        """
+        modcmd = ['dn: %s'%dn, 'changetype: add']
+        for key, values in mods.iteritems():
+            if isinstance(values, basestring):
+                values = [values]
+            for value in values:
+                modcmd.append('%s: %s'%(key, value))
+        self._ldapmodify(modcmd)
+
     def delete_ldap_entry(self, dn):
         """
         delete an LDAP entity
@@ -328,9 +338,23 @@
                          'deactivated')
         # check that it doesn't choke
         self.pull()
-        # reset the ldap database
-        self.tearDownClass()
-        self.setUpClass()
+        # reinsert syt
+        self.add_ldap_entry('uid=syt,ou=People,dc=cubicweb,dc=test',
+                            { 'objectClass': ['OpenLDAPperson','posixAccount','top','shadowAccount'],
+                              'cn': 'Sylvain Thenault',
+                              'sn': 'Thenault',
+                              'gidNumber': '1004',
+                              'uid': 'syt',
+                              'homeDirectory': '/home/syt',
+                              'shadowFlag': '134538764',
+                              'uidNumber': '1004',
+                              'givenName': 'Sylvain',
+                              'telephoneNumber': '106',
+                              'displayName': 'sthenault',
+                              'gecos': 'Sylvain Thenault',
+                              'mail': ['sylvain.thenault@logilab.fr','syt@logilab.fr'],
+                              'userPassword': 'syt',
+                             })
         self.pull()
         self.assertEqual(self.execute('Any N WHERE U login "syt", '
                                       'U in_state S, S name N').rows[0][0],
@@ -429,8 +453,5 @@
 
 
 
-
-
-
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_repository.py	Thu Jun 13 17:27:43 2013 +0200
+++ b/server/test/unittest_repository.py	Tue Jul 09 15:58:26 2013 +0200
@@ -52,16 +52,18 @@
     and relation
     """
 
-    def test_uniquetogether(self):
+    def test_unique_together_constraint(self):
         self.execute('INSERT Societe S: S nom "Logilab", S type "SSLL", S cp "75013"')
         with self.assertRaises(ValidationError) as wraperr:
             self.execute('INSERT Societe S: S nom "Logilab", S type "SSLL", S cp "75013"')
-        self.assertEqual({'nom': u'violates unique_together constraints (cp, nom, type)',
-                          'cp': u'violates unique_together constraints (cp, nom, type)',
-                          'type': u'violates unique_together constraints (cp, nom, type)'},
-                     wraperr.exception.args[1])
+        self.assertEqual(
+            {'cp': u'cp is part of violated unicity constraint',
+             'nom': u'nom is part of violated unicity constraint',
+             'type': u'type is part of violated unicity constraint',
+             'unicity constraint': u'some relations violate a unicity constraint'},
+            wraperr.exception.args[1])
 
-    def test_unique_together(self):
+    def test_unique_together_schema(self):
         person = self.repo.schema.eschema('Personne')
         self.assertEqual(len(person._unique_together), 1)
         self.assertItemsEqual(person._unique_together[0],
@@ -272,19 +274,21 @@
     def test_initial_schema(self):
         schema = self.repo.schema
         # check order of attributes is respected
-        self.assertListEqual([r.type for r in schema.eschema('CWAttribute').ordered_relations()
-                               if not r.type in ('eid', 'is', 'is_instance_of', 'identity',
-                                                 'creation_date', 'modification_date', 'cwuri',
-                                                 'owned_by', 'created_by', 'cw_source',
-                                                 'update_permission', 'read_permission',
-                                                 'in_basket')],
-                              ['relation_type',
-                               'from_entity', 'to_entity',
-                               'constrained_by',
-                               'cardinality', 'ordernum',
-                               'indexed', 'fulltextindexed', 'internationalizable',
-                               'defaultval', 'extra_props',
-                               'description', 'description_format'])
+        notin = set(('eid', 'is', 'is_instance_of', 'identity',
+                     'creation_date', 'modification_date', 'cwuri',
+                     'owned_by', 'created_by', 'cw_source',
+                     'update_permission', 'read_permission',
+                     'in_basket'))
+        self.assertListEqual(['relation_type',
+                              'from_entity', 'to_entity',
+                              'constrained_by',
+                              'cardinality', 'ordernum',
+                              'indexed', 'fulltextindexed', 'internationalizable',
+                              'defaultval', 'extra_props',
+                              'description', 'description_format'],
+                             [r.type
+                              for r in schema.eschema('CWAttribute').ordered_relations()
+                              if r.type not in notin])
 
         self.assertEqual(schema.eschema('CWEType').main_attribute(), 'name')
         self.assertEqual(schema.eschema('State').main_attribute(), 'name')
--- a/server/test/unittest_schemaserial.py	Thu Jun 13 17:27:43 2013 +0200
+++ b/server/test/unittest_schemaserial.py	Tue Jul 09 15:58:26 2013 +0200
@@ -28,6 +28,7 @@
 from logilab.database import get_db_helper
 from yams import register_base_type, unregister_base_type
 
+schema = config = None
 def setUpModule(*args):
     register_base_type('BabarTestType', ('jungle_speed',))
     helper = get_db_helper('sqlite')
@@ -44,7 +45,7 @@
 
 def tearDownModule(*args):
     global schema, config
-    del schema, config
+    schema = config = None
 
     unregister_base_type('BabarTestType')
     helper = get_db_helper('sqlite')
@@ -63,29 +64,29 @@
 class Schema2RQLTC(TestCase):
 
     def test_eschema2rql1(self):
-        self.assertListEqual(list(eschema2rql(schema.eschema('CWAttribute'))),
-                              [
+        self.assertListEqual([
             ('INSERT CWEType X: X description %(description)s,X final %(final)s,X name %(name)s',
              {'description': u'define a final relation: link a final relation type from a non final entity to a final entity type. used to build the instance schema',
-              'name': u'CWAttribute', 'final': False})
-            ])
+              'name': u'CWAttribute', 'final': False})],
+                             list(eschema2rql(schema.eschema('CWAttribute'))))
 
     def test_eschema2rql2(self):
-        self.assertListEqual(list(eschema2rql(schema.eschema('String'))), [
+        self.assertListEqual([
                 ('INSERT CWEType X: X description %(description)s,X final %(final)s,X name %(name)s',
-                 {'description': u'', 'final': True, 'name': u'String'})])
+                 {'description': u'', 'final': True, 'name': u'String'})],
+                             list(eschema2rql(schema.eschema('String'))))
 
     def test_eschema2rql_specialization(self):
         # x: None since eschema.eid are None
-        self.assertListEqual(sorted(specialize2rql(schema)),
-                              [('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
-                                {'et': None, 'x': None}),
-                               ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
-                                {'et': None, 'x': None}),
-                               ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
-                                {'et': None, 'x': None}),
-                               ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
-                                {'et': None, 'x': None})])
+        self.assertListEqual([('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
+                               {'et': None, 'x': None}),
+                              ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
+                               {'et': None, 'x': None}),
+                              ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
+                               {'et': None, 'x': None}),
+                              ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
+                               {'et': None, 'x': None})],
+                             sorted(specialize2rql(schema)))
 
     def test_esche2rql_custom_type(self):
         expected = [('INSERT CWEType X: X description %(description)s,X final %(final)s,X name %(name)s',
@@ -95,8 +96,7 @@
         self.assertListEqual(expected, got)
 
     def test_rschema2rql1(self):
-        self.assertListEqual(list(rschema2rql(schema.rschema('relation_type'), cstrtypemap)),
-                             [
+        self.assertListEqual([
             ('INSERT CWRType X: X description %(description)s,X final %(final)s,X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,X name %(name)s,X symmetric %(symmetric)s',
              {'description': u'link a relation definition to its relation type', 'symmetric': False, 'name': u'relation_type', 'final' : False, 'fulltext_container': None, 'inlined': True}),
 
@@ -113,11 +113,11 @@
               'ordernum': 1, 'cardinality': u'1*'}),
             ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s',
              {'x': None, 'ct': u'RQLConstraint_eid', 'value': u';O;O final FALSE\n'}),
-            ])
+        ],
+                             list(rschema2rql(schema.rschema('relation_type'), cstrtypemap)))
 
     def test_rschema2rql2(self):
-        self.assertListEqual(list(rschema2rql(schema.rschema('add_permission'), cstrtypemap)),
-                              [
+        self.assertListEqual([
             ('INSERT CWRType X: X description %(description)s,X final %(final)s,X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,X name %(name)s,X symmetric %(symmetric)s', {'description': u'', 'symmetric': False, 'name': u'add_permission', 'final': False, 'fulltext_container': None, 'inlined': False}),
 
             ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
@@ -132,12 +132,11 @@
               'description': u'groups allowed to add entities/relations of this type', 'composite': None, 'ordernum': 9999, 'cardinality': u'**'}),
             ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
              {'se': None, 'rt': None, 'oe': None,
-              'description': u'rql expression allowing to add entities/relations of this type', 'composite': 'subject', 'ordernum': 9999, 'cardinality': u'*?'}),
-            ])
+              'description': u'rql expression allowing to add entities/relations of this type', 'composite': 'subject', 'ordernum': 9999, 'cardinality': u'*?'})],
+                             list(rschema2rql(schema.rschema('add_permission'), cstrtypemap)))
 
     def test_rschema2rql3(self):
-        self.assertListEqual(list(rschema2rql(schema.rschema('cardinality'), cstrtypemap)),
-                             [
+        self.assertListEqual([
             ('INSERT CWRType X: X description %(description)s,X final %(final)s,X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,X name %(name)s,X symmetric %(symmetric)s',
              {'description': u'', 'symmetric': False, 'name': u'cardinality', 'final': True, 'fulltext_container': None, 'inlined': False}),
 
@@ -155,8 +154,8 @@
             ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s',
              {'x': None, 'ct': u'SizeConstraint_eid', 'value': u'max=2'}),
             ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s',
-             {'x': None, 'ct': u'StaticVocabularyConstraint_eid', 'value': u"u'?*', u'1*', u'+*', u'**', u'?+', u'1+', u'++', u'*+', u'?1', u'11', u'+1', u'*1', u'??', u'1?', u'+?', u'*?'"}),
-            ])
+             {'x': None, 'ct': u'StaticVocabularyConstraint_eid', 'value': u"u'?*', u'1*', u'+*', u'**', u'?+', u'1+', u'++', u'*+', u'?1', u'11', u'+1', u'*1', u'??', u'1?', u'+?', u'*?'"})],
+                             list(rschema2rql(schema.rschema('cardinality'), cstrtypemap)))
 
     def test_rschema2rql_custom_type(self):
         expected = [('INSERT CWRType X: X description %(description)s,X final %(final)s,'
@@ -195,37 +194,34 @@
         self.assertListEqual(expected, got)
 
     def test_rdef2rql(self):
-        self.assertListEqual(list(rdef2rql(schema['description_format'].rdefs[('CWRType', 'String')], cstrtypemap)),
-                              [
+        self.assertListEqual([
             ('INSERT CWAttribute X: X cardinality %(cardinality)s,X defaultval %(defaultval)s,X description %(description)s,X fulltextindexed %(fulltextindexed)s,X indexed %(indexed)s,X internationalizable %(internationalizable)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s',
              {'se': None, 'rt': None, 'oe': None,
               'description': u'', 'internationalizable': True, 'fulltextindexed': False, 'ordernum': 3, 'defaultval': u'text/plain', 'indexed': False, 'cardinality': u'?1'}),
             ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s',
              {'x': None, 'value': u'None', 'ct': 'FormatConstraint_eid'}),
             ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s',
-             {'x': None, 'value': u'max=50', 'ct': 'SizeConstraint_eid'})])
+             {'x': None, 'value': u'max=50', 'ct': 'SizeConstraint_eid'})],
+                             list(rdef2rql(schema['description_format'].rdefs[('CWRType', 'String')], cstrtypemap)))
 
 
     def test_updateeschema2rql1(self):
-        self.assertListEqual(list(updateeschema2rql(schema.eschema('CWAttribute'), 1)),
-                              [('SET X description %(description)s,X final %(final)s,X name %(name)s WHERE X eid %(x)s',
-                                {'description': u'define a final relation: link a final relation type from a non final entity to a final entity type. used to build the instance schema', 'x': 1, 'final': False, 'name': u'CWAttribute'}),
-                               ])
+        self.assertListEqual([('SET X description %(description)s,X final %(final)s,X name %(name)s WHERE X eid %(x)s',
+                               {'description': u'define a final relation: link a final relation type from a non final entity to a final entity type. used to build the instance schema', 'x': 1, 'final': False, 'name': u'CWAttribute'})],
+                             list(updateeschema2rql(schema.eschema('CWAttribute'), 1)))
 
     def test_updateeschema2rql2(self):
-        self.assertListEqual(list(updateeschema2rql(schema.eschema('String'), 1)),
-                              [('SET X description %(description)s,X final %(final)s,X name %(name)s WHERE X eid %(x)s',
-                                {'description': u'', 'x': 1, 'final': True, 'name': u'String'})
-                               ])
+        self.assertListEqual([('SET X description %(description)s,X final %(final)s,X name %(name)s WHERE X eid %(x)s',
+                               {'description': u'', 'x': 1, 'final': True, 'name': u'String'})],
+                             list(updateeschema2rql(schema.eschema('String'), 1)))
 
     def test_updaterschema2rql1(self):
-        self.assertListEqual(list(updaterschema2rql(schema.rschema('relation_type'), 1)),
-                             [
+        self.assertListEqual([
             ('SET X description %(description)s,X final %(final)s,X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,X name %(name)s,X symmetric %(symmetric)s WHERE X eid %(x)s',
              {'x': 1, 'symmetric': False,
               'description': u'link a relation definition to its relation type',
-              'final': False, 'fulltext_container': None, 'inlined': True, 'name': u'relation_type'})
-            ])
+              'final': False, 'fulltext_container': None, 'inlined': True, 'name': u'relation_type'})],
+                             list(updaterschema2rql(schema.rschema('relation_type'), 1)))
 
     def test_updaterschema2rql2(self):
         expected = [
@@ -235,7 +231,7 @@
               'inlined': False, 'name': u'add_permission'})
             ]
         for i, (rql, args) in enumerate(updaterschema2rql(schema.rschema('add_permission'), 1)):
-            yield self.assertEqual, (rql, args), expected[i]
+            yield self.assertEqual, expected[i], (rql, args)
 
 class Perms2RQLTC(TestCase):
     GROUP_MAPPING = {
@@ -246,31 +242,33 @@
         }
 
     def test_eperms2rql1(self):
-        self.assertListEqual([(rql, kwargs) for rql, kwargs in erperms2rql(schema.eschema('CWEType'), self.GROUP_MAPPING)],
-                              [('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
-                               ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 1}),
-                               ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 2}),
-                               ('SET X add_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
-                               ('SET X update_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
-                               ('SET X delete_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
-                               ])
+        self.assertListEqual([('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
+                              ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 1}),
+                              ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 2}),
+                              ('SET X add_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
+                              ('SET X update_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
+                              ('SET X delete_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0})],
+                             [(rql, kwargs)
+                              for rql, kwargs in erperms2rql(schema.eschema('CWEType'), self.GROUP_MAPPING)])
 
     def test_rperms2rql2(self):
-        self.assertListEqual([(rql, kwargs) for rql, kwargs in erperms2rql(schema.rschema('read_permission').rdef('CWEType', 'CWGroup'), self.GROUP_MAPPING)],
-                              [('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
-                               ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 1}),
-                               ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 2}),
-                               ('SET X add_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
-                               ('SET X delete_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
-                               ])
+        self.assertListEqual([('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
+                              ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 1}),
+                              ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 2}),
+                              ('SET X add_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
+                              ('SET X delete_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0})],
+                             [(rql, kwargs)
+                              for rql, kwargs in erperms2rql(schema.rschema('read_permission').rdef('CWEType', 'CWGroup'),
+                                                             self.GROUP_MAPPING)])
 
     def test_rperms2rql3(self):
-        self.assertListEqual([(rql, kwargs) for rql, kwargs in erperms2rql(schema.rschema('name').rdef('CWEType', 'String'), self.GROUP_MAPPING)],
-                              [('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
-                               ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 1}),
-                               ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 2}),
-                               ('SET X update_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
-                               ])
+        self.assertListEqual([('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}),
+                              ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 1}),
+                              ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 2}),
+                              ('SET X update_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0})],
+                             [(rql, kwargs)
+                              for rql, kwargs in erperms2rql(schema.rschema('name').rdef('CWEType', 'String'),
+                                                             self.GROUP_MAPPING)])
 
     #def test_perms2rql(self):
     #    self.assertListEqual(perms2rql(schema, self.GROUP_MAPPING),
--- a/web/data/cubicweb.facets.js	Thu Jun 13 17:27:43 2013 +0200
+++ b/web/data/cubicweb.facets.js	Tue Jul 09 15:58:26 2013 +0200
@@ -30,7 +30,7 @@
         });
         // FacetStringWidget (e.g. has-text)
         $(this).find('input:text').each(function(){
-            names.push(facetName);
+            names.push(this.name);
             values.push(this.value);
         });
     });
--- a/web/facet.py	Thu Jun 13 17:27:43 2013 +0200
+++ b/web/facet.py	Tue Jul 09 15:58:26 2013 +0200
@@ -74,10 +74,11 @@
 
 
 def rtype_facet_title(facet):
-    ptypes = facet.cw_rset.column_types(0)
-    if len(ptypes) == 1:
-        return display_name(facet._cw, facet.rtype, form=facet.role,
-                            context=iter(ptypes).next())
+    if facet.cw_rset:
+        ptypes = facet.cw_rset.column_types(0)
+        if len(ptypes) == 1:
+            return display_name(facet._cw, facet.rtype, form=facet.role,
+                                context=iter(ptypes).next())
     return display_name(facet._cw, facet.rtype, form=facet.role)
 
 def get_facet(req, facetid, select, filtered_variable):
@@ -1516,7 +1517,8 @@
             cssclass += ' hideFacetBody'
         w(u'<div class="%s" cubicweb:facetName="%s">%s</div>\n' %
                (cssclass, xml_escape(self.facet.__regid__), title))
-        w(u'<input name="%s" type="text" value="%s" />\n' % (facetid, self.value or u''))
+        w(u'<input name="%s" type="text" value="%s" />\n' % (
+                xml_escape(self.facet.__regid__), self.value or u''))
         w(u'</div>\n')
 
 
--- a/web/formwidgets.py	Thu Jun 13 17:27:43 2013 +0200
+++ b/web/formwidgets.py	Tue Jul 09 15:58:26 2013 +0200
@@ -1016,6 +1016,8 @@
     time, you should not give an already translated string.
     """
     type = 'button'
+    css_class = 'validateButton'
+
     def __init__(self, label=stdmsgs.BUTTON_OK, attrs=None,
                  setdomid=None, settabindex=None,
                  name='', value='', onclick=None, cwaction=None):
@@ -1030,7 +1032,7 @@
         self.value = ''
         self.onclick = onclick
         self.cwaction = cwaction
-        self.attrs.setdefault('class', 'validateButton')
+        self.attrs.setdefault('class', self.css_class)
 
     def render(self, form, field=None, renderer=None):
         label = form._cw._(self.label)
--- a/web/request.py	Thu Jun 13 17:27:43 2013 +0200
+++ b/web/request.py	Tue Jul 09 15:58:26 2013 +0200
@@ -922,7 +922,7 @@
         if reset_xmldecl is not None:
             warn('[3.17] reset_xmldecl is deprecated as we only serve html',
                  DeprecationWarning, stacklevel=2)
-        self.main_stream.set_doctype(doctype, reset_xmldecl)
+        self.main_stream.set_doctype(doctype)
 
     # page data management ####################################################
 
--- a/web/test/unittest_views_baseviews.py	Thu Jun 13 17:27:43 2013 +0200
+++ b/web/test/unittest_views_baseviews.py	Tue Jul 09 15:58:26 2013 +0200
@@ -144,7 +144,7 @@
         class MyView(StartupView):
             __regid__ = 'my-view'
             def call(self):
-                self._cw.set_doctype(html_doctype, reset_xmldecl=False)
+                self._cw.set_doctype(html_doctype)
                 self._cw.main_stream.set_htmlattrs([('lang', 'cz')])
 
         with self.temporary_appobjects(MyView):
--- a/web/views/navigation.py	Thu Jun 13 17:27:43 2013 +0200
+++ b/web/views/navigation.py	Tue Jul 09 15:58:26 2013 +0200
@@ -398,7 +398,7 @@
             title = self._cw._('i18nprevnext_previous')
             icon = self.prev_icon
             cssclass = u'previousEntity left'
-            content = icon + content
+            content = icon + '&#160;&#160;' + content
         else:
             title = self._cw._('i18nprevnext_next')
             icon = self.next_icon