backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Thu, 01 Jul 2010 17:06:37 +0200
changeset 5849 9db65b381028
parent 5840 60880c81e32e (current diff)
parent 5848 b5640328ffad (diff)
child 5852 5cb23d24df26
backport stable
__pkginfo__.py
debian/control
hooks/security.py
hooks/syncschema.py
server/session.py
server/sources/__init__.py
server/sources/native.py
server/sources/rql2sql.py
server/test/unittest_rql2sql.py
web/uicfg.py
web/views/autoform.py
web/views/basetemplates.py
web/views/formrenderers.py
web/views/startup.py
--- a/hooks/security.py	Wed Jun 30 15:43:36 2010 +0200
+++ b/hooks/security.py	Thu Jul 01 17:06:37 2010 +0200
@@ -69,10 +69,12 @@
 
 class _CheckRelationPermissionOp(hook.LateOperation):
     def precommit_event(self):
-        rdef = self.rschema.rdef(self.session.describe(self.eidfrom)[0],
-                                 self.session.describe(self.eidto)[0])
-        rdef.check_perm(self.session, self.action,
-                        fromeid=self.eidfrom, toeid=self.eidto)
+        session = self.session
+        for args in session.transaction_data.pop('check_relation_perm_op'):
+            action, rschema, eidfrom, eidto = args
+            rdef = rschema.rdef(session.describe(eidfrom)[0],
+                                session.describe(eidto)[0])
+            rdef.check_perm(session, action, fromeid=eidfrom, toeid=eidto)
 
     def commit_event(self):
         pass
@@ -154,10 +156,9 @@
                 return
             rschema = self._cw.repo.schema[self.rtype]
             if self.rtype in ON_COMMIT_ADD_RELATIONS:
-                _CheckRelationPermissionOp(self._cw, action='add',
-                                           rschema=rschema,
-                                           eidfrom=self.eidfrom,
-                                           eidto=self.eidto)
+                hook.set_operation(self._cw, 'check_relation_perm_op',
+                                   ('add', rschema, self.eidfrom, self.eidto),
+                                   _CheckRelationPermissionOp)
             else:
                 rdef = rschema.rdef(self._cw.describe(self.eidfrom)[0],
                                     self._cw.describe(self.eidto)[0])
--- a/hooks/syncschema.py	Wed Jun 30 15:43:36 2010 +0200
+++ b/hooks/syncschema.py	Thu Jul 01 17:06:37 2010 +0200
@@ -252,12 +252,11 @@
             return
         session = self.session
         if 'fulltext_container' in self.values:
-            ftiupdates = session.transaction_data.setdefault(
-                'fti_update_etypes', set())
             for subjtype, objtype in rschema.rdefs:
-                ftiupdates.add(subjtype)
-                ftiupdates.add(objtype)
-            UpdateFTIndexOp(session)
+                hook.set_operation(session, 'fti_update_etypes', subjtype,
+                                   UpdateFTIndexOp)
+                hook.set_operation(session, 'fti_update_etypes', objtype,
+                                   UpdateFTIndexOp)
         if not 'inlined' in self.values:
             return # nothing to do
         inlined = self.values['inlined']
@@ -367,7 +366,7 @@
         sysource = session.pool.source('system')
         attrtype = y2sql.type_from_constraints(
             sysource.dbhelper, rdef.object, rdef.constraints)
-        # XXX should be moved somehow into lgc.adbh: sqlite doesn't support to
+        # XXX should be moved somehow into lgdb: sqlite doesn't support to
         # add a new column with UNIQUE, it should be added after the ALTER TABLE
         # using ADD INDEX
         if sysource.dbdriver == 'sqlite' and 'UNIQUE' in attrtype:
@@ -506,23 +505,21 @@
             else:
                 sysource.drop_index(session, table, column)
         if 'cardinality' in self.values and self.rschema.final:
-            adbh = session.pool.source('system').dbhelper
-            if not adbh.alter_column_support:
+            syssource = session.pool.source('system')
+            if not syssource.dbhelper.alter_column_support:
                 # not supported (and NOT NULL not set by yams in that case, so
-                # no worry)
+                # no worry) XXX (syt) then should we set NOT NULL below ??
                 return
             atype = self.rschema.objects(etype)[0]
             constraints = self.rschema.rdef(etype, atype).constraints
-            coltype = y2sql.type_from_constraints(adbh, atype, constraints,
+            coltype = y2sql.type_from_constraints(syssource.dbhelper, atype, constraints,
                                                   creating=False)
             # XXX check self.values['cardinality'][0] actually changed?
-            notnull = self.values['cardinality'][0] != '1'
-            sql = adbh.sql_set_null_allowed(table, column, coltype, notnull)
-            session.system_sql(sql)
+            syssource.set_null_allowed(self.session, table, column, coltype,
+                                       self.values['cardinality'][0] != '1')
         if 'fulltextindexed' in self.values:
-            UpdateFTIndexOp(session)
-            session.transaction_data.setdefault(
-                'fti_update_etypes', set()).add(etype)
+            hook.set_operation(session, 'fti_update_etypes', etype,
+                               UpdateFTIndexOp)
 
 
 class SourceDbCWConstraintAdd(hook.Operation):
@@ -548,13 +545,12 @@
         # alter the physical schema on size constraint changes
         if newcstr.type() == 'SizeConstraint' and (
             oldcstr is None or oldcstr.max != newcstr.max):
-            adbh = self.session.pool.source('system').dbhelper
+            syssource = self.session.pool.source('system')
             card = rtype.rdef(subjtype, objtype).cardinality
-            coltype = y2sql.type_from_constraints(adbh, objtype, [newcstr],
-                                                  creating=False)
-            sql = adbh.sql_change_col_type(table, column, coltype, card != '1')
+            coltype = y2sql.type_from_constraints(syssource.dbhelper, objtype,
+                                                  [newcstr], creating=False)
             try:
-                session.system_sql(sql, rollback_on_failure=False)
+                syssource.change_col_type(session, table, column, coltype, card[0] != '1')
                 self.info('altered column %s of table %s: now %s',
                           column, table, coltype)
             except Exception, ex:
@@ -575,13 +571,13 @@
         column = SQL_PREFIX + str(self.rdef.rtype)
         # alter the physical schema on size/unique constraint changes
         if cstrtype == 'SizeConstraint':
+            syssource = self.session.pool.source('system')
+            coltype = y2sql.type_from_constraints(syssource.dbhelper,
+                                                  self.rdef.object, [],
+                                                  creating=False)
             try:
-                adbh = self.session.pool.source('system').dbhelper
-                coltype = y2sql.type_from_constraints(adbh, rdef.object, [],
-                                                      creating=False)
-                sql = adbh.sql_change_col_type(table, column, coltype,
-                                               rdef.cardinality != '1')
-                self.session.system_sql(sql, rollback_on_failure=False)
+                syssource.change_col_type(session, table, column, coltype,
+                                          self.rdef.cardinality[0] != '1')
                 self.info('altered column %s of table %s: now %s',
                           column, table, coltype)
             except Exception, ex:
@@ -1174,7 +1170,7 @@
     def postcommit_event(self):
         session = self.session
         source = session.repo.system_source
-        to_reindex = session.transaction_data.get('fti_update_etypes', ())
+        to_reindex = session.transaction_data.pop('fti_update_etypes', ())
         self.info('%i etypes need full text indexed reindexation',
                   len(to_reindex))
         schema = self.session.repo.vreg.schema
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.8.5_Any.py	Thu Jul 01 17:06:37 2010 +0200
@@ -0,0 +1,59 @@
+def migrate_varchar_to_nvarchar():
+    dbdriver  = config.sources()['system']['db-driver']
+    if dbdriver != "sqlserver2005":
+        return
+
+    introspection_sql = """\
+SELECT table_schema, table_name, column_name, is_nullable, character_maximum_length
+FROM information_schema.columns
+WHERE data_type = 'VARCHAR' and table_name <> 'SYSDIAGRAMS'
+"""
+    has_index_sql = """\
+SELECT i.name AS index_name,
+       i.type_desc,
+       i.is_unique,
+       i.is_unique_constraint
+FROM sys.indexes AS i, sys.index_columns as j, sys.columns as k
+WHERE is_hypothetical = 0 AND i.index_id <> 0
+AND i.object_id = j.object_id
+AND i.index_id = j.index_id
+AND i.object_id = OBJECT_ID('%(table)s')
+AND k.name = '%(col)s'
+AND k.object_id=i.object_id
+AND j.column_id = k.column_id;"""
+
+    generated_statements = []
+    for schema, table, column, is_nullable, length in sql(introspection_sql, ask_confirm=False):
+        qualified_table = '[%s].[%s]' % (schema, table)
+        rset = sql(has_index_sql % {'table': qualified_table, 'col':column},
+                   ask_confirm = False)
+        drops = []
+        creates = []
+        for idx_name, idx_type, idx_unique, is_unique_constraint in rset:
+            if is_unique_constraint:
+                drops.append('ALTER TABLE %s DROP CONSTRAINT %s' % (qualified_table, idx_name))
+                creates.append('ALTER TABLE %s ADD CONSTRAINT %s UNIQUE (%s)' % (qualified_table, idx_name, column))
+            else:
+                drops.append('DROP INDEX %s ON %s' % (idx_name, qualified_table))
+                if idx_unique:
+                    unique = 'UNIQUE'
+                else:
+                    unique = ''
+                creates.append('CREATE %s %s INDEX %s ON %s(%s)' % (unique, idx_type, idx_name, qualified_table, column))
+
+        if length == -1:
+            length = 'max'
+        if is_nullable == 'YES':
+            not_null = 'NULL'
+        else:
+            not_null = 'NOT NULL'
+        alter_sql = 'ALTER TABLE %s ALTER COLUMN %s NVARCHAR(%s) %s' % (qualified_table, column, length, not_null)
+        generated_statements+= drops + [alter_sql] + creates
+
+
+    for statement in generated_statements:
+        print statement
+        sql(statement, ask_confirm=False)
+    commit()
+
+migrate_varchar_to_nvarchar()
--- a/rtags.py	Wed Jun 30 15:43:36 2010 +0200
+++ b/rtags.py	Thu Jul 01 17:06:37 2010 +0200
@@ -82,16 +82,14 @@
         self._tagdefs.clear()
 
     def _get_keys(self, stype, rtype, otype, tagged):
-        keys = [('*', rtype, '*', tagged),
-                ('*', rtype, otype, tagged),
-                (stype, rtype, '*', tagged),
-                (stype, rtype, otype, tagged)]
-        if stype == '*' or otype == '*':
-            keys.remove( ('*', rtype, '*', tagged) )
-            if stype == '*':
-                keys.remove( ('*', rtype, otype, tagged) )
-            if otype == '*':
-                keys.remove( (stype, rtype, '*', tagged) )
+        keys = []
+        if '*' not in (stype, otype):
+            keys.append(('*', rtype, '*', tagged))
+        if '*' != stype:
+            keys.append(('*', rtype, otype, tagged))
+        if '*' != otype:
+            keys.append((stype, rtype, '*', tagged))
+        keys.append((stype, rtype, otype, tagged))
         return keys
 
     def init(self, schema, check=True):
--- a/server/hook.py	Wed Jun 30 15:43:36 2010 +0200
+++ b/server/hook.py	Thu Jul 01 17:06:37 2010 +0200
@@ -46,8 +46,8 @@
 `timestamp` attributes, but *their `_cw` attribute is None*.
 
 Session hooks (eg session_open, session_close) have no special attribute.
+"""
 
-"""
 from __future__ import with_statement
 
 __docformat__ = "restructuredtext en"
@@ -369,10 +369,10 @@
     commit / rollback transations. Possible events are:
 
     precommit:
-      the pool is preparing to commit. You shouldn't do anything things which
-      has to be reverted if the commit fail at this point, but you can freely
+      the pool is preparing to commit. You shouldn't do anything which
+      has to be reverted if the commit fails at this point, but you can freely
       do any heavy computation or raise an exception if the commit can't go.
-      You can add some new operation during this phase but their precommit
+      You can add some new operations during this phase but their precommit
       event won't be triggered
 
     commit:
@@ -391,6 +391,12 @@
        * a commit event failed, all operations which are not been triggered for
          commit are rollbacked
 
+    postcommit:
+      The transaction is over. All the ORM entities are
+      invalid. If you need to work on the database, you need to stard
+      a new transaction, for instance using a new internal_session,
+      which you will need to commit (and close!).
+
     order of operations may be important, and is controlled according to
     the insert_index's method output
     """
--- a/server/pool.py	Wed Jun 30 15:43:36 2010 +0200
+++ b/server/pool.py	Thu Jul 01 17:06:37 2010 +0200
@@ -19,9 +19,8 @@
 connections pools, each of them dealing with a set of connections on each source
 used by the repository. A connections pools (`ConnectionsPool`) is an
 abstraction for a group of connection to each source.
-
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 import sys
--- a/server/session.py	Wed Jun 30 15:43:36 2010 +0200
+++ b/server/session.py	Thu Jul 01 17:06:37 2010 +0200
@@ -563,11 +563,15 @@
     @property
     def pool(self):
         """connections pool, set according to transaction mode for each query"""
+        if self._closed:
+            self.reset_pool(True)
+            raise Exception('try to access pool on a closed session')
         return getattr(self._threaddata, 'pool', None)
 
-    def set_pool(self, checkclosed=True):
+    def set_pool(self):
         """the session need a pool to execute some queries"""
-        if checkclosed and self._closed:
+        if self._closed:
+            self.reset_pool(True)
             raise Exception('try to set pool on a closed session')
         if self.pool is None:
             # get pool first to avoid race-condition
@@ -578,24 +582,34 @@
                 self._threaddata.pool = None
                 self.repo._free_pool(pool)
                 raise
-            self._threads_in_transaction.add(threading.currentThread())
+            self._threads_in_transaction.add(
+                (threading.currentThread(), pool) )
         return self._threaddata.pool
 
+    def _free_thread_pool(self, thread, pool, force_close=False):
+        try:
+            self._threads_in_transaction.remove( (thread, pool) )
+        except KeyError:
+            # race condition on pool freeing (freed by commit or rollback vs
+            # close)
+            pass
+        else:
+            if force_close:
+                pool.reconnect()
+            else:
+                pool.pool_reset()
+            # free pool once everything is done to avoid race-condition
+            self.repo._free_pool(pool)
+
     def reset_pool(self, ignoremode=False):
         """the session is no longer using its pool, at least for some time"""
         # pool may be none if no operation has been done since last commit
         # or rollback
-        if self.pool is not None and (ignoremode or self.mode == 'read'):
+        pool = getattr(self._threaddata, 'pool', None)
+        if pool is not None and (ignoremode or self.mode == 'read'):
             # even in read mode, we must release the current transaction
-            pool = self.pool
-            try:
-                self._threads_in_transaction.remove(threading.currentThread())
-            except KeyError:
-                pass
-            pool.pool_reset()
+            self._free_thread_pool(threading.currentThread(), pool)
             del self._threaddata.pool
-            # free pool once everything is done to avoid race-condition
-            self.repo._free_pool(pool)
 
     def _touch(self):
         """update latest session usage timestamp and reset mode to read"""
@@ -772,7 +786,9 @@
 
     def rollback(self, reset_pool=True):
         """rollback the current session's transaction"""
-        if self.pool is None:
+        # don't use self.pool, rollback may be called with _closed == True
+        pool = getattr(self._threaddata, 'pool', None)
+        if pool is None:
             self._clear_thread_data()
             self._touch()
             self.debug('rollback session %s done (no db activity)', self.id)
@@ -787,7 +803,7 @@
                     except:
                         self.critical('rollback error', exc_info=sys.exc_info())
                         continue
-                self.pool.rollback()
+                pool.rollback()
                 self.debug('rollback for session %s done', self.id)
         finally:
             self._touch()
@@ -799,7 +815,7 @@
         """do not close pool on session close, since they are shared now"""
         self._closed = True
         # copy since _threads_in_transaction maybe modified while waiting
-        for thread in self._threads_in_transaction.copy():
+        for thread, pool in self._threads_in_transaction.copy():
             if thread is threading.currentThread():
                 continue
             self.info('waiting for thread %s', thread)
@@ -809,11 +825,12 @@
             for i in xrange(10):
                 thread.join(1)
                 if not (thread.isAlive() and
-                        thread in self._threads_in_transaction):
+                        (thread, pool) in self._threads_in_transaction):
                     break
             else:
                 self.error('thread %s still alive after 10 seconds, will close '
                            'session anyway', thread)
+                self._free_thread_pool(thread, pool, force_close=True)
         self.rollback()
         del self.__threaddata
         del self._tx_data
--- a/server/sources/extlite.py	Wed Jun 30 15:43:36 2010 +0200
+++ b/server/sources/extlite.py	Thu Jul 01 17:06:37 2010 +0200
@@ -16,8 +16,8 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """provide an abstract class for external sources using a sqlite database helper
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 
--- a/server/sources/ldapuser.py	Wed Jun 30 15:43:36 2010 +0200
+++ b/server/sources/ldapuser.py	Thu Jul 01 17:06:37 2010 +0200
@@ -293,7 +293,13 @@
             raise AuthenticationError()
         # check password by establishing a (unused) connection
         try:
-            self._connect(user, password)
+            if password:
+                self._connect(user, password)
+            else:
+                # On Windows + ADAM this would have succeeded (!!!)
+                # You get Authenticated as: 'NT AUTHORITY\ANONYMOUS LOGON'.
+                # we really really don't want that
+                raise Exception('No password provided')
         except Exception, ex:
             self.info('while trying to authenticate %s: %s', user, ex)
             # Something went wrong, most likely bad credentials
@@ -553,7 +559,7 @@
             self._cache[rec_dn] = rec_dict
             result.append(rec_dict)
         #print '--->', result
-        self.info('ldap built results %s', len(result))
+        self.debug('ldap built results %s', len(result))
         return result
 
     def before_entity_insertion(self, session, lid, etype, eid):
--- a/server/sources/native.py	Wed Jun 30 15:43:36 2010 +0200
+++ b/server/sources/native.py	Thu Jul 01 17:06:37 2010 +0200
@@ -275,6 +275,8 @@
         if self.dbdriver == 'sqlite':
             self._create_eid = None
             self.create_eid = self._create_eid_sqlite
+        self.binary_to_str = self.dbhelper.dbapi_module.binary_to_str
+
 
     @property
     def _sqlcnx(self):
@@ -672,9 +674,6 @@
 
     # short cut to method requiring advanced db helper usage ##################
 
-    def binary_to_str(self, value):
-        return self.dbhelper.dbapi_module.binary_to_str(value)
-
     def create_index(self, session, table, column, unique=False):
         cursor = LogCursor(session.pool[self.uri])
         self.dbhelper.create_index(cursor, table, column, unique)
@@ -683,6 +682,14 @@
         cursor = LogCursor(session.pool[self.uri])
         self.dbhelper.drop_index(cursor, table, column, unique)
 
+    def change_col_type(self, session, table, column, coltype, null_allowed):
+        cursor = LogCursor(session.pool[self.uri])
+        self.dbhelper.change_col_type(cursor, table, column, coltype, null_allowed)
+
+    def set_null_allowed(self, session, table, column, coltype, null_allowed):
+        cursor = LogCursor(session.pool[self.uri])
+        self.dbhelper.set_null_allowed(cursor, table, column, coltype, null_allowed)
+
     # system source interface #################################################
 
     def eid_type_source(self, session, eid):
--- a/server/sources/rql2sql.py	Wed Jun 30 15:43:36 2010 +0200
+++ b/server/sources/rql2sql.py	Thu Jul 01 17:06:37 2010 +0200
@@ -833,10 +833,11 @@
             # if the rhs variable is only linked to this relation, this mean we
             # only want the relation to exists, eg NOT NULL in case of inlined
             # relation
-            if len(rhsvar.stinfo['relations']) == 1 and rhsvar._q_invariant:
+            if rhsvar._q_invariant:
+                sql = self._extra_join_sql(relation, lhssql, rhsvar)
+                if sql:
+                    return sql
                 return '%s IS NOT NULL' % lhssql
-            if rhsvar._q_invariant:
-                return self._extra_join_sql(relation, lhssql, rhsvar)
         return '%s=%s' % (lhssql, rhsvar.accept(self))
 
     def _process_relation_term(self, relation, rid, termvar, termconst, relfield):
--- a/server/test/unittest_rql2sql.py	Wed Jun 30 15:43:36 2010 +0200
+++ b/server/test/unittest_rql2sql.py	Thu Jul 01 17:06:37 2010 +0200
@@ -272,7 +272,7 @@
     ('Any O WHERE NOT S ecrit_par O, S eid 1, S inline1 P, O inline2 P',
      '''SELECT _O.cw_eid
 FROM cw_Note AS _S, cw_Personne AS _O
-WHERE NOT (_S.cw_ecrit_par=_O.cw_eid) AND _S.cw_eid=1 AND _O.cw_inline2=_S.cw_inline1'''),
+WHERE NOT (_S.cw_ecrit_par=_O.cw_eid) AND _S.cw_eid=1 AND _S.cw_inline1 IS NOT NULL AND _O.cw_inline2=_S.cw_inline1'''),
 
     ('DISTINCT Any S ORDERBY stockproc(SI) WHERE NOT S ecrit_par O, S para SI',
      '''SELECT T1.C0 FROM (SELECT DISTINCT _S.cw_eid AS C0, STOCKPROC(_S.cw_para) AS C1
@@ -966,6 +966,12 @@
     ]
 
 INLINE = [
+
+    ('Any P WHERE N eid 1, N ecrit_par P, NOT P owned_by P2',
+     '''SELECT _N.cw_ecrit_par
+FROM cw_Note AS _N
+WHERE _N.cw_eid=1 AND _N.cw_ecrit_par IS NOT NULL AND NOT (EXISTS(SELECT 1 FROM owned_by_relation AS rel_owned_by0 WHERE _N.cw_ecrit_par=rel_owned_by0.eid_from))'''),
+
     ('Any P, L WHERE N ecrit_par P, P nom L, N eid 0',
      '''SELECT _P.cw_eid, _P.cw_nom
 FROM cw_Note AS _N, cw_Personne AS _P
@@ -997,9 +1003,10 @@
 WHERE NOT (_N.cw_ecrit_par=_P.cw_eid) AND _N.cw_eid=512'''),
 
     ('Any S,ES,T WHERE S state_of ET, ET name "CWUser", ES allowed_transition T, T destination_state S',
+     # XXX "_T.cw_destination_state IS NOT NULL" could be avoided here but it's not worth it
      '''SELECT _T.cw_destination_state, rel_allowed_transition1.eid_from, _T.cw_eid
 FROM allowed_transition_relation AS rel_allowed_transition1, cw_Transition AS _T, cw_Workflow AS _ET, state_of_relation AS rel_state_of0
-WHERE _T.cw_destination_state=rel_state_of0.eid_from AND rel_state_of0.eid_to=_ET.cw_eid AND _ET.cw_name=CWUser AND rel_allowed_transition1.eid_to=_T.cw_eid'''),
+WHERE _T.cw_destination_state=rel_state_of0.eid_from AND rel_state_of0.eid_to=_ET.cw_eid AND _ET.cw_name=CWUser AND rel_allowed_transition1.eid_to=_T.cw_eid AND _T.cw_destination_state IS NOT NULL'''),
 
     ('Any O WHERE S eid 0, S in_state O',
      '''SELECT _S.cw_in_state
--- a/web/test/unittest_uicfg.py	Wed Jun 30 15:43:36 2010 +0200
+++ b/web/test/unittest_uicfg.py	Thu Jul 01 17:06:37 2010 +0200
@@ -15,6 +15,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/>.
+from logilab.common.testlib import tag
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.web import uicfg
 
@@ -25,6 +26,57 @@
     def test_default_actionbox_appearsin_addmenu_config(self):
         self.failIf(abaa.etype_get('TrInfo', 'wf_info_for', 'object', 'CWUser'))
 
+
+
+class DefinitionOrderTC(CubicWebTC):
+    """This test check that when multiple definition could match a key, only
+    the more accurate apply"""
+
+    def setUp(self):
+
+        new_def = (
+                    (('*', 'login', '*'),
+                         {'formtype':'main', 'section':'hidden'}),
+                    (('*', 'login', '*'),
+                         {'formtype':'muledit', 'section':'hidden'}),
+                    (('CWUser', 'login', '*'),
+                         {'formtype':'main', 'section':'attributes'}),
+                    (('CWUser', 'login', '*'),
+                         {'formtype':'muledit', 'section':'attributes'}),
+                    (('CWUser', 'login', 'String'),
+                         {'formtype':'main', 'section':'inlined'}),
+                    (('CWUser', 'login', 'String'),
+                         {'formtype':'inlined', 'section':'attributes'}),
+                    )
+        self._old_def = []
+
+        for key, kwargs in new_def:
+            nkey = key[0], key[1], key[2], 'subject'
+            self._old_def.append((nkey, uicfg.autoform_section._tagdefs.get(nkey)))
+            uicfg.autoform_section.tag_subject_of(key, **kwargs)
+
+        super(DefinitionOrderTC, self).setUp()
+
+
+    @tag('uicfg')
+    def test_definition_order_hidden(self):
+        result = uicfg.autoform_section.get('CWUser', 'login', 'String', 'subject')
+        expected = set(['main_inlined', 'muledit_attributes', 'inlined_attributes'])
+        self.assertSetEquals(result, expected)
+
+    def tearDown(self):
+        super(DefinitionOrderTC, self).tearDown()
+        for key, tags in self._old_def:
+                if tags is None:
+                    uicfg.autoform_section.del_rtag(*key)
+                else:
+                    for tag in tags:
+                        formtype, section = tag.split('_')
+                        uicfg.autoform_section.tag_subject_of(key[:3], formtype=formtype, section=section)
+
+        uicfg.autoform_section.clear()
+        uicfg.autoform_section.init(self.repo.vreg.schema)
+
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
     unittest_main()
--- a/web/uicfg.py	Wed Jun 30 15:43:36 2010 +0200
+++ b/web/uicfg.py	Thu Jul 01 17:06:37 2010 +0200
@@ -284,8 +284,19 @@
         rtags.add('%s_%s' % (formtype, section))
         return rtags
 
-    def init_get(self, *key):
-        return super(AutoformSectionRelationTags, self).get(*key)
+    def init_get(self, stype, rtype, otype, tagged):
+        key = (stype, rtype, otype, tagged)
+        rtags = {}
+        for key in self._get_keys(stype, rtype, otype, tagged):
+            tags = self._tagdefs.get(key, ())
+            for tag in tags:
+                assert '_' in tag, (tag, tags)
+                section, value = tag.split('_', 1)
+                rtags[section] = value
+        cls = self.tag_container_cls
+        rtags = cls('_'.join([section,value]) for section,value in rtags.iteritems())
+        return rtags
+
 
     def get(self, *key):
         # overriden to avoid recomputing done in parent classes
--- a/web/views/autoform.py	Wed Jun 30 15:43:36 2010 +0200
+++ b/web/views/autoform.py	Thu Jul 01 17:06:37 2010 +0200
@@ -238,13 +238,29 @@
             self.peid, self.rtype, entity.eid)
         self.render_form(i18nctx, divonclick=divonclick, **kwargs)
 
+    def _get_removejs(self):
+        """
+        Don't display the remove link in edition form if the
+        cardinality is 1. Handled in InlineEntityCreationFormView for
+        creation form.
+        """
+        entity = self._entity()
+        if isinstance(self.peid, int):
+            pentity = self._cw.entity_from_eid(self.peid)
+            petype = pentity.e_schema.type
+            rdef = entity.e_schema.rdef(self.rtype, neg_role(self.role), petype)
+            card= rdef.role_cardinality(self.role)
+            if card == '1': # don't display remove link
+                return None
+        return self.removejs and self.removejs % (
+            self.peid, self.rtype, entity.eid)
+
     def render_form(self, i18nctx, **kwargs):
         """fetch and render the form"""
         entity = self._entity()
         divid = '%s-%s-%s' % (self.peid, self.rtype, entity.eid)
         title = self.form_title(entity, i18nctx)
-        removejs = self.removejs and self.removejs % (
-            self.peid, self.rtype, entity.eid)
+        removejs = self._get_removejs()
         countkey = '%s_count' % self.rtype
         try:
             self._cw.data[countkey] += 1
--- a/web/views/basetemplates.py	Wed Jun 30 15:43:36 2010 +0200
+++ b/web/views/basetemplates.py	Thu Jul 01 17:06:37 2010 +0200
@@ -483,7 +483,7 @@
         if cw.vreg.config['allow-email-login']:
             label = cw._('login or email')
         else:
-            label = cw._('login')
+            label = cw.pgettext('CWUser', 'login')
         form.field_by_name('__login').label = label
         self.w(form.render(table_class='', display_progress_div=False))
         cw.html_headers.add_onload('jQuery("#__login:visible").focus()')
--- a/web/views/boxes.py	Wed Jun 30 15:43:36 2010 +0200
+++ b/web/views/boxes.py	Thu Jul 01 17:06:37 2010 +0200
@@ -15,8 +15,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/>.
-"""
-generic boxes for CubicWeb web client:
+"""Generic boxes for CubicWeb web client:
 
 * actions box
 * possible views box
@@ -24,8 +23,8 @@
 additional (disabled by default) boxes
 * schema box
 * startup views box
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
@@ -185,7 +184,6 @@
         for view in self._cw.vreg['views'].possible_views(self._cw, None):
             if view.category == 'startupview':
                 box.append(self.box_action(view))
-
         if not box.is_empty():
             box.render(self.w)
 
--- a/web/views/debug.py	Wed Jun 30 15:43:36 2010 +0200
+++ b/web/views/debug.py	Thu Jul 01 17:06:37 2010 +0200
@@ -15,10 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""management and error screens
+"""management and error screens"""
 
-
-"""
 __docformat__ = "restructuredtext en"
 
 from time import strftime, localtime
@@ -45,6 +43,7 @@
     __select__ = none_rset() & match_user_groups('managers')
 
     title = _('server information')
+    cache_max_age = 0
 
     def call(self, **kwargs):
         req = self._cw
@@ -128,6 +127,7 @@
     __regid__ = 'registry'
     __select__ = StartupView.__select__ & match_user_groups('managers')
     title = _('registry')
+    cache_max_age = 0
 
     def call(self, **kwargs):
         self.w(u'<h1>%s</h1>' % _("Registry's content"))
@@ -150,6 +150,7 @@
     __regid__ = 'gc'
     __select__ = StartupView.__select__ & match_user_groups('managers')
     title = _('memory leak debugging')
+    cache_max_age = 0
 
     def call(self, **kwargs):
         from cubicweb._gcdebug import gc_info
--- a/web/views/formrenderers.py	Wed Jun 30 15:43:36 2010 +0200
+++ b/web/views/formrenderers.py	Thu Jul 01 17:06:37 2010 +0200
@@ -397,10 +397,6 @@
     _options = FormRenderer._options + ('main_form_title',)
     main_form_title = _('main informations')
 
-    def render(self, form, values):
-        rendered = super(EntityFormRenderer, self).render(form, values)
-        return rendered + u'</div>' # close extra div introducted by open_form
-
     def open_form(self, form, values):
         attrs_fs_label = ''
         if self.main_form_title:
@@ -409,6 +405,13 @@
         attrs_fs_label += '<div class="formBody">'
         return attrs_fs_label + super(EntityFormRenderer, self).open_form(form, values)
 
+    def close_form(self, form, values):
+        """seems dumb but important for consistency w/ close form, and necessary
+        for form renderers overriding open_form to use something else or more than
+        and <form>
+        """
+        return super(EntityFormRenderer, self).close_form(form, values) + '</div>'
+
     def render_buttons(self, w, form):
         if len(form.form_buttons) == 3:
             w("""<table width="100%%">
--- a/web/views/startup.py	Wed Jun 30 15:43:36 2010 +0200
+++ b/web/views/startup.py	Thu Jul 01 17:06:37 2010 +0200
@@ -17,8 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Set of HTML startup views. A startup view is global, e.g. doesn't
 apply to a result set.
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode