backport stable branch (one more time painfully)
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 20 Nov 2009 19:35:54 +0100
changeset 3890 d7a270f50f54
parent 3810 5b75fd66c80e (current diff)
parent 3889 8902b8745918 (diff)
child 3892 c53df9a310a6
child 3894 27cbf98ad863
backport stable branch (one more time painfully)
__pkginfo__.py
common/migration.py
common/mixins.py
dbapi.py
devtools/devctl.py
entities/authobjs.py
entities/test/unittest_base.py
entities/test/unittest_wfobjs.py
entity.py
etwist/server.py
etwist/twctl.py
hooks/integrity.py
hooks/security.py
hooks/syncschema.py
hooks/workflow.py
misc/migration/bootstrapmigration_repository.py
rset.py
schema.py
schemas/base.py
schemas/bootstrap.py
schemas/workflow.py
selectors.py
server/__init__.py
server/hookhelper.py
server/migractions.py
server/repository.py
server/schemaserial.py
server/serverconfig.py
server/serverctl.py
server/sources/native.py
server/test/unittest_ldapuser.py
server/test/unittest_msplanner.py
server/test/unittest_multisources.py
server/test/unittest_rql2sql.py
server/test/unittest_security.py
sobjects/notification.py
test/unittest_entity.py
utils.py
view.py
web/__init__.py
web/component.py
web/formfields.py
web/formwidgets.py
web/request.py
web/test/unittest_application.py
web/uicfg.py
web/views/actions.py
web/views/autoform.py
web/views/basecomponents.py
web/views/basecontrollers.py
web/views/basetemplates.py
web/views/baseviews.py
web/views/cwuser.py
web/views/editforms.py
web/views/forms.py
web/views/ibreadcrumbs.py
web/views/idownloadable.py
web/views/owl.py
web/views/plots.py
web/views/primary.py
web/views/schema.py
web/views/startup.py
web/views/tableview.py
web/views/tabs.py
web/views/timetable.py
web/views/treeview.py
web/views/urlrewrite.py
web/views/wdoc.py
web/views/workflow.py
--- a/__pkginfo__.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/__pkginfo__.py	Fri Nov 20 19:35:54 2009 +0100
@@ -10,7 +10,7 @@
 numversion = (3, 5, 5)
 version = '.'.join(str(num) for num in numversion)
 
-license = 'LGPL v2'
+license = 'LGPL'
 copyright = '''Copyright (c) 2003-2009 LOGILAB S.A. (Paris, FRANCE).
 http://www.logilab.fr/ -- mailto:contact@logilab.fr'''
 
--- a/common/mixins.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/common/mixins.py	Fri Nov 20 19:35:54 2009 +0100
@@ -180,7 +180,9 @@
         For instance, the Person class might want to return a `companyname`
         key.
         """
-        return set(rs.type for rs, _ in cls.e_schema.attribute_definitions())
+        return set(rschema.type
+                   for rschema, attrtype in cls.e_schema.attribute_definitions()
+                   if attrtype.type not in ('Password', 'Bytes'))
 
     def as_email_context(self):
         """returns the dictionary as used by the sendmail controller to
--- a/common/tags.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/common/tags.py	Fri Nov 20 19:35:54 2009 +0100
@@ -18,6 +18,7 @@
         attrs.setdefault('escapecontent', self.escapecontent)
         return simple_sgml_tag(self.name, __content, **attrs)
 
+button = tag('button')
 input = tag('input')
 textarea = tag('textarea')
 a = tag('a')
@@ -31,6 +32,9 @@
 h3 = tag('h3')
 h4 = tag('h4')
 h5 = tag('h5')
+tr = tag('tr')
+th = tag('th')
+td = tag('td')
 
 def select(name, id=None, multiple=False, options=[], **attrs):
     if multiple:
--- a/dbapi.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/dbapi.py	Fri Nov 20 19:35:54 2009 +0100
@@ -499,6 +499,8 @@
         DBAPIRequest.relative_path = fake
         DBAPIRequest.url = fake
         DBAPIRequest.next_tabindex = fake
+        DBAPIRequest.get_page_data = fake
+        DBAPIRequest.set_page_data = fake
         DBAPIRequest.add_js = fake #cwrb.add_js.im_func
         DBAPIRequest.add_css = fake #cwrb.add_css.im_func
         # XXX could ask the repo for it's base-url configuration
--- a/devtools/devctl.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/devtools/devctl.py	Fri Nov 20 19:35:54 2009 +0100
@@ -180,6 +180,8 @@
                             tschema, tschema, rschema, eschema)
                     add_msg(w, label)
                     add_msg(w, label2)
+            # XXX also generate "creating ...' messages for actions in the
+            # addrelated submenu
     w('# subject and object forms for each relation type\n')
     w('# (no object form for final or symetric relation types)\n')
     w('\n')
--- a/doc/book/en/annexes/depends.rst	Sun Nov 08 21:53:18 2009 +0100
+++ b/doc/book/en/annexes/depends.rst	Fri Nov 20 19:35:54 2009 +0100
@@ -9,10 +9,6 @@
 cloning the mercurial forest, here is the list of tools and libraries you need
 to have installed in order for CubicWeb to work:
 
-* mxDateTime - http://www.egenix.com/products/python/mxBase/mxDateTime/ - http://pypi.python.org/pypi/egenix-mx-base
-
-* pyro - http://pyro.sourceforge.net/ - http://pypi.python.org/pypi/Pyro
-
 * yapps - http://theory.stanford.edu/~amitp/yapps/ -
   http://pypi.python.org/pypi/Yapps2
 
@@ -22,6 +18,8 @@
 * simplejson - http://code.google.com/p/simplejson/ -
   http://pypi.python.org/pypi/simplejson
 
+* docsutils - http://docutils.sourceforge.net/ - http://pypi.python.org/pypi/docutils
+
 * lxml - http://codespeak.net/lxml - http://pypi.python.org/pypi/lxml
 
 * twisted - http://twistedmatrix.com/ - http://pypi.python.org/pypi/Twisted
@@ -44,12 +42,17 @@
 * indexer - http://www.logilab.org/project/indexer -
   http://pypi.python.org/pypi/indexer - included in the forest
 
+To activate Sparql querying:
+
 * fyzz - http://www.logilab.org/project/fyzz - http://pypi.python.org/pypi/fyzz
   - included in the forest
 
-* psycopg2 - http://initd.org/projects/psycopg2 - http://pypi.python.org/pypi/psycopg2
+To use network communication between cubicweb instances / clients:
 
-* docsutils - http://docutils.sourceforge.net/ - http://pypi.python.org/pypi/docutils
+* Pyro - http://pyro.sourceforge.net/ - http://pypi.python.org/pypi/Pyro
+
+If you're using a Postgres database (recommended):
+* psycopg2 - http://initd.org/projects/psycopg2 - http://pypi.python.org/pypi/psycopg2
 
 For the google-appengine extension to be available, you also need:
 
@@ -59,5 +62,6 @@
 * vobject - http://vobject.skyhouseconsulting.com/ -
   http://pypi.python.org/pypi/vobject
 
+  
 Any help with the packaging of CubicWeb for more than Debian/Ubuntu (including
 eggs, buildouts, etc) will be greatly appreciated.
--- a/doc/book/en/development/datamodel/definition.rst	Sun Nov 08 21:53:18 2009 +0100
+++ b/doc/book/en/development/datamodel/definition.rst	Fri Nov 20 19:35:54 2009 +0100
@@ -223,7 +223,7 @@
 
 RQL expression for entity type permission :
 
-* you have to use the class `RQLExpression`
+* you have to use the class `ERQLExpression`
 
 * the used expression corresponds to the WHERE statement of an RQL query
 
@@ -240,16 +240,16 @@
 For RQL expressions on a relation type, the principles are the same except
 for the following :
 
-* you have to use the class `RQLExpression` in the case of a non-final relation
+* you have to use the class `RRQLExpression` in the case of a non-final relation
 
 * in the expression, the variables S, O and U are pre-defined references
   to respectively the subject and the object of the current relation (on
   which the action is being verified) and the user who executed the query
 
-* we can also defined rights on attributes of an entity (non-final relation),
+* we can also define rights over attributes of an entity (non-final relation),
   knowing that :
 
-  - to define RQL expression, we have to use the class `RQLExpression`
+  - to define RQL expression, we have to use the class `ERQLExpression`
     in which X represents the entity the attribute belongs to
 
   - the permissions `add` and `delete` are equivalent. Only `add`/`read`
--- a/entities/test/unittest_base.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/entities/test/unittest_base.py	Fri Nov 20 19:35:54 2009 +0100
@@ -44,38 +44,24 @@
                           {'description_format': ('format', 'description')})
 
 
-class CWUserTC(BaseEntityTC):
-    def test_dc_title_and_name(self):
-        e = self.entity('CWUser U WHERE U login "member"')
-        self.assertEquals(e.dc_title(), 'member')
-        self.assertEquals(e.name(), 'member')
-        self.execute(u'SET X firstname "bouah" WHERE X is CWUser, X login "member"')
-        self.assertEquals(e.dc_title(), 'member')
-        self.assertEquals(e.name(), u'bouah')
-        self.execute(u'SET X surname "lôt" WHERE X is CWUser, X login "member"')
-        self.assertEquals(e.dc_title(), 'member')
-        self.assertEquals(e.name(), u'bouah lôt')
-
 class EmailAddressTC(BaseEntityTC):
     def test_canonical_form(self):
         email1 = self.execute('INSERT EmailAddress X: X address "maarten.ter.huurne@philips.com"').get_entity(0, 0)
         email2 = self.execute('INSERT EmailAddress X: X address "maarten@philips.com"').get_entity(0, 0)
         email3 = self.execute('INSERT EmailAddress X: X address "toto@logilab.fr"').get_entity(0, 0)
-        self.execute('SET X prefered_form Y WHERE X eid %s, Y eid %s' % (email1.eid, email2.eid))
+        email1.set_relations(prefered_form=email2)
         self.assertEquals(email1.prefered.eid, email2.eid)
         self.assertEquals(email2.prefered.eid, email2.eid)
         self.assertEquals(email3.prefered.eid, email3.eid)
 
     def test_mangling(self):
-        eid = self.execute('INSERT EmailAddress X: X address "maarten.ter.huurne@philips.com"')[0][0]
-        email = self.entity('Any X WHERE X eid %(x)s', {'x':eid}, 'x')
+        email = self.execute('INSERT EmailAddress X: X address "maarten.ter.huurne@philips.com"').get_entity(0, 0)
         self.assertEquals(email.display_address(), 'maarten.ter.huurne@philips.com')
         self.assertEquals(email.printable_value('address'), 'maarten.ter.huurne@philips.com')
         self.vreg.config.global_set_option('mangle-emails', True)
         self.assertEquals(email.display_address(), 'maarten.ter.huurne at philips dot com')
         self.assertEquals(email.printable_value('address'), 'maarten.ter.huurne at philips dot com')
-        eid = self.execute('INSERT EmailAddress X: X address "syt"')[0][0]
-        email = self.entity('Any X WHERE X eid %(x)s', {'x':eid}, 'x')
+        email = self.execute('INSERT EmailAddress X: X address "syt"').get_entity(0, 0)
         self.assertEquals(email.display_address(), 'syt')
         self.assertEquals(email.printable_value('address'), 'syt')
 
@@ -93,6 +79,25 @@
         self.failUnless(e.matching_groups(('xyz', 'managers')))
         self.failIf(e.matching_groups(('xyz', 'abcd')))
 
+    def test_dc_title_and_name(self):
+        e = self.entity('CWUser U WHERE U login "member"')
+        self.assertEquals(e.dc_title(), 'member')
+        self.assertEquals(e.name(), 'member')
+        e.set_attributes(firstname=u'bouah')
+        self.assertEquals(e.dc_title(), 'member')
+        self.assertEquals(e.name(), u'bouah')
+        e.set_attributes(surname=u'lôt')
+        self.assertEquals(e.dc_title(), 'member')
+        self.assertEquals(e.name(), u'bouah lôt')
+
+    def test_allowed_massmail_keys(self):
+        e = self.entity('CWUser U WHERE U login "member"')
+        # Bytes/Password attributes should be omited
+        self.assertEquals(e.allowed_massmail_keys(),
+                          set(('surname', 'firstname', 'login', 'last_login_time',
+                               'creation_date', 'modification_date', 'cwuri', 'eid'))
+                          )
+
 
 class InterfaceTC(CubicWebTC):
 
--- a/entities/test/unittest_wfobjs.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/entities/test/unittest_wfobjs.py	Fri Nov 20 19:35:54 2009 +0100
@@ -482,12 +482,21 @@
     #     self.commit()
 
     # test that the workflow is correctly enforced
+
+    def _cleanup_msg(self, msg):
+        """remove the variable part of one specific error message"""
+        lmsg = msg.split()
+        lmsg.pop(1)
+        lmsg.pop()
+        return ' '.join(lmsg)
+
     def test_transition_checking1(self):
         cnx = self.login('stduser')
         user = cnx.user(self.session)
         ex = self.assertRaises(ValidationError,
                                user.fire_transition, 'activate')
-        self.assertEquals(ex.errors, {'by_transition': u"transition isn't allowed"})
+        self.assertEquals(self._cleanup_msg(ex.errors['by_transition']),
+                          u"transition isn't allowed from")
         cnx.close()
 
     def test_transition_checking2(self):
@@ -495,7 +504,8 @@
         user = cnx.user(self.session)
         ex = self.assertRaises(ValidationError,
                                user.fire_transition, 'dummy')
-        self.assertEquals(ex.errors, {'by_transition': u"transition isn't allowed"})
+        self.assertEquals(self._cleanup_msg(ex.errors['by_transition']),
+                          u"transition isn't allowed from")
         cnx.close()
 
     def test_transition_checking3(self):
@@ -507,7 +517,8 @@
         session.set_pool()
         ex = self.assertRaises(ValidationError,
                                user.fire_transition, 'deactivate')
-        self.assertEquals(ex.errors, {'by_transition': u"transition isn't allowed"})
+        self.assertEquals(self._cleanup_msg(ex.errors['by_transition']),
+                                            u"transition isn't allowed from")
         # get back now
         user.fire_transition('activate')
         cnx.commit()
--- a/entity.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/entity.py	Fri Nov 20 19:35:54 2009 +0100
@@ -34,7 +34,7 @@
 def greater_card(rschema, subjtypes, objtypes, index):
     for subjtype in subjtypes:
         for objtype in objtypes:
-            card = rschema.rproperty(subjtype, objtype, 'cardinality')[index]
+            card = rschema.rdef(subjtype, objtype).cardinality[index]
             if card in '+*':
                 return card
     return '1'
@@ -144,7 +144,8 @@
                 cls.warning('skipping fetch_attr %s defined in %s (not found in schema)',
                             attr, cls.__regid__)
                 continue
-            if not user.matching_groups(rschema.get_groups('read')):
+            rdef = eschema.rdef(attr)
+            if not user.matching_groups(rdef.get_groups('read')):
                 continue
             var = varmaker.next()
             selection.append(var)
@@ -153,7 +154,7 @@
             if not rschema.final:
                 # XXX this does not handle several destination types
                 desttype = rschema.objects(eschema.type)[0]
-                card = rschema.rproperty(eschema, desttype, 'cardinality')[0]
+                card = rdef.cardinality[0]
                 if card not in '?1':
                     cls.warning('bad relation %s specified in fetch attrs for %s',
                                  attr, cls)
@@ -256,10 +257,10 @@
             self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
 
     def check_perm(self, action):
-        self.e_schema.check_perm(self._cw, action, self.eid)
+        self.e_schema.check_perm(self._cw, action, eid=self.eid)
 
     def has_perm(self, action):
-        return self.e_schema.has_perm(self._cw, action, self.eid)
+        return self.e_schema.has_perm(self._cw, action, eid=self.eid)
 
     def view(self, vid, __registry='views', **kwargs):
         """shortcut to apply a view on this entity"""
@@ -339,11 +340,11 @@
             return u''
         if attrtype is None:
             attrtype = self.e_schema.destination(attr)
-        props = self.e_schema.rproperties(attr)
+        props = self.e_schema.rdef(attr)
         if attrtype == 'String':
             # internalinalized *and* formatted string such as schema
             # description...
-            if props.get('internationalizable'):
+            if props.internationalizable:
                 value = self._cw._(value)
             attrformat = self.attr_metadata(attr, 'format')
             if attrformat:
@@ -391,11 +392,12 @@
             if rschema.type in self.skip_copy_for:
                 continue
             # skip composite relation
-            if self.e_schema.subjrproperty(rschema, 'composite'):
+            rdef = self.e_schema.rdef(rschema)
+            if rdef.composite:
                 continue
             # skip relation with card in ?1 else we either change the copied
             # object (inlined relation) or inserting some inconsistency
-            if self.e_schema.subjrproperty(rschema, 'cardinality')[1] in '?1':
+            if rdef.cardinality[1] in '?1':
                 continue
             rql = 'SET X %s V WHERE X eid %%(x)s, Y eid %%(y)s, Y %s V' % (
                 rschema.type, rschema.type)
@@ -405,14 +407,15 @@
             if rschema.meta:
                 continue
             # skip already defined relations
-            if getattr(self, 'reverse_%s' % rschema.type):
+            if self.related(rschema.type, 'object'):
                 continue
+            rdef = self.e_schema.rdef(rschema, 'object')
             # skip composite relation
-            if self.e_schema.objrproperty(rschema, 'composite'):
+            if rdef.composite:
                 continue
             # skip relation with card in ?1 else we either change the copied
             # object (inlined relation) or inserting some inconsistency
-            if self.e_schema.objrproperty(rschema, 'cardinality')[0] in '?1':
+            if rdef.cardinality[0] in '?1':
                 continue
             rql = 'SET V %s X WHERE X eid %%(x)s, Y eid %%(y)s, V %s Y' % (
                 rschema.type, rschema.type)
@@ -433,15 +436,16 @@
         for rschema in self.e_schema.subject_relations():
             if rschema.final:
                 continue
-            if len(rschema.objects(self.e_schema)) > 1:
+            targets = rschema.objects(self.e_schema)
+            if len(targets) > 1:
                 # ambigous relations, the querier doesn't handle
                 # outer join correctly in this case
                 continue
             if rschema.inlined:
                 matching_groups = self._cw.user.matching_groups
-                if matching_groups(rschema.get_groups('read')) and \
-                   all(matching_groups(es.get_groups('read'))
-                       for es in rschema.objects(self.e_schema)):
+                rdef = rschema.rdef(self.e_schema, targets[0])
+                if matching_groups(rdef.get_groups('read')) and \
+                   all(matching_groups(e.get_groups('read')) for e in targets):
                     yield rschema, 'subject'
 
     def to_complete_attributes(self, skip_bytes=True):
@@ -453,7 +457,8 @@
             if attr == 'eid':
                 continue
             # password retreival is blocked at the repository server level
-            if not self._cw.user.matching_groups(rschema.get_groups('read')) \
+            rdef = rschema.rdef(self.e_schema, attrschema)
+            if not self._cw.user.matching_groups(rdef.get_groups('read')) \
                    or attrschema.type == 'Password':
                 self[attr] = None
                 continue
@@ -489,24 +494,21 @@
                 if self.relation_cached(rtype, role):
                     continue
                 var = varmaker.next()
+                targettype = rschema.targets(self.e_schema, role)[0]
+                rdef = rschema.role_rdef(self.e_schema, targettype, role)
+                card = rdef.role_cardinality(role)
+                assert card in '1?', '%s %s %s %s' % (self.e_schema, rtype,
+                                                      role, card)
                 if role == 'subject':
-                    targettype = rschema.objects(self.e_schema)[0]
-                    card = rschema.rproperty(self.e_schema, targettype,
-                                             'cardinality')[0]
                     if card == '1':
                         rql.append('%s %s %s' % (V, rtype, var))
-                    else: # '?"
+                    else:
                         rql.append('%s %s %s?' % (V, rtype, var))
                 else:
-                    targettype = rschema.subjects(self.e_schema)[1]
-                    card = rschema.rproperty(self.e_schema, targettype,
-                                             'cardinality')[1]
                     if card == '1':
                         rql.append('%s %s %s' % (var, rtype, V))
-                    else: # '?"
+                    else:
                         rql.append('%s? %s %s' % (var, rtype, V))
-                assert card in '1?', '%s %s %s %s' % (self.e_schema, rtype,
-                                                      role, card)
                 selected.append(((rtype, role), var))
         if selected:
             # select V, we need it as the left most selected variable
@@ -652,16 +654,16 @@
             restriction = []
             args = {}
             securitycheck_args = {}
-        insertsecurity = (rtype.has_local_role('add') and not
-                          rtype.has_perm(self._cw, 'add', **securitycheck_args))
-        constraints = rtype.rproperty(subjtype, objtype, 'constraints')
+        rdef = rtype.role_rdef(self.e_schema, targettype, role)
+        insertsecurity = (rdef.has_local_role('add') and not
+                          rdef.has_perm(self._cw, 'add', **securitycheck_args))
         if vocabconstraints:
             # RQLConstraint is a subclass for RQLVocabularyConstraint, so they
             # will be included as well
-            restriction += [cstr.restriction for cstr in constraints
+            restriction += [cstr.restriction for cstr in rdef.constraints
                             if isinstance(cstr, RQLVocabularyConstraint)]
         else:
-            restriction += [cstr.restriction for cstr in constraints
+            restriction += [cstr.restriction for cstr in rdef.constraints
                             if isinstance(cstr, RQLConstraint)]
         etypecls = self._cw.vreg['etypes'].etype_class(targettype)
         rql = etypecls.fetch_rql(self._cw.user, restriction,
@@ -671,12 +673,16 @@
             before, after = rql.split(' WHERE ', 1)
             rql = '%s ORDERBY %s WHERE %s' % (before, searchedvar, after)
         if insertsecurity:
-            rqlexprs = rtype.get_rqlexprs('add')
+            rqlexprs = rdef.get_rqlexprs('add')
             rewriter = RQLRewriter(self._cw)
             rqlst = self._cw.vreg.parse(self._cw, rql, args)
+            if not self.has_eid():
+                existant = searchedvar
+            else:
+                existant = None # instead of 'SO', improve perfs
             for select in rqlst.children:
                 rewriter.rewrite(select, [((searchedvar, searchedvar), rqlexprs)],
-                                 select.solutions, args)
+                                 select.solutions, args, existant)
             rql = rqlst.as_string()
         return rql, args
 
@@ -719,12 +725,10 @@
             related = list(rset.entities(col))
             rschema = self._cw.vreg.schema.rschema(rtype)
             if role == 'subject':
-                rcard = rschema.rproperty(self.e_schema, related[0].e_schema,
-                                          'cardinality')[1]
+                rcard = rschema.rdef(self.e_schema, related[0].e_schema).cardinality[1]
                 target = 'object'
             else:
-                rcard = rschema.rproperty(related[0].e_schema, self.e_schema,
-                                          'cardinality')[0]
+                rcard = rschema.rdef(related[0].e_schema, self.e_schema).cardinality[0]
                 target = 'subject'
             if rcard in '?1':
                 for rentity in related:
--- a/etwist/server.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/etwist/server.py	Fri Nov 20 19:35:54 2009 +0100
@@ -103,11 +103,13 @@
         assert self.base_url[-1] == '/'
         self.https_url = config['https-url']
         assert not self.https_url or self.https_url[-1] == '/'
+        # instantiate publisher here and not in init_publisher to get some
+        # checks done before daemonization (eg versions consistency)
+        self.appli = CubicWebPublisher(config, debug=self.debugmode)
+        self.versioned_datadir = 'data%s' % config.instance_md5_version()
 
     def init_publisher(self):
         config = self.config
-        self.appli = CubicWebPublisher(config, debug=self.debugmode)
-        self.versioned_datadir = 'data%s' % config.instance_md5_version()
         # when we have an in-memory repository, clean unused sessions every XX
         # seconds and properly shutdown the server
         if config.repo_method == 'inmemory':
@@ -382,14 +384,13 @@
     port = config['port'] or 8080
     reactor.listenTCP(port, channel.HTTPFactory(website))
     logger = getLogger('cubicweb.twisted')
-    logger.info('instance started on %s', root_resource.base_url)
     if not debug:
         print 'instance starting in the background'
         if daemonize():
             return # child process
         if config['pid-file']:
             # ensure the directory where the pid-file should be set exists (for
-            # instance /var/run/cubicweb may be deleted on computer restart) 
+            # instance /var/run/cubicweb may be deleted on computer restart)
             piddir = os.path.dirname(config['pid-file'])
             if not os.path.exists(piddir):
                 os.makedirs(piddir)
@@ -403,6 +404,7 @@
             uid = getpwnam(config['uid']).pw_uid
         os.setuid(uid)
     root_resource.start_service()
+    logger.info('instance started on %s', root_resource.base_url)
     if config['profile']:
         prof = hotshot.Profile(config['profile'])
         prof.runcall(reactor.run)
--- a/etwist/twctl.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/etwist/twctl.py	Fri Nov 20 19:35:54 2009 +0100
@@ -9,10 +9,14 @@
 import sys
 
 from cubicweb.toolsutils import CommandHandler
+from cubicweb.web.webctl import WebCreateHandler
 
 # trigger configuration registration
 import cubicweb.etwist.twconfig # pylint: disable-msg=W0611
 
+class TWCreateHandler(WebCreateHandler):
+    cfgname = 'twisted'
+
 class TWStartHandler(CommandHandler):
     cmdname = 'start'
     cfgname = 'twisted'
@@ -28,8 +32,8 @@
 
 try:
     from cubicweb.server import serverctl
-
-    class AllInOneCreateHandler(serverctl.RepositoryCreateHandler):
+    class AllInOneCreateHandler(serverctl.RepositoryCreateHandler,
+                                TWCreateHandler):
         """configuration to get an instance running in a twisted web server
         integrating a repository server in the same process
         """
@@ -38,6 +42,7 @@
         def bootstrap(self, cubes, inputlevel=0):
             """bootstrap this configuration"""
             serverctl.RepositoryCreateHandler.bootstrap(self, cubes, inputlevel)
+            TWCreateHandler.bootstrap(self, cubes, inputlevel)
 
     class AllInOneStartHandler(TWStartHandler):
         cmdname = 'start'
--- a/hooks/integrity.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/hooks/integrity.py	Fri Nov 20 19:35:54 2009 +0100
@@ -33,6 +33,8 @@
         # recheck pending eids
         if self.session.deleted_in_transaction(self.eid):
             return
+        if self.rtype in self.session.transaction_data.get('pendingrtypes', ()):
+            return
         if self.session.unsafe_execute(*self._rql()).rowcount < 1:
             etype = self.session.describe(self.eid)[0]
             _ = self.session._
@@ -87,22 +89,16 @@
     def after_add_entity(self):
         eid = self.entity.eid
         eschema = self.entity.e_schema
-        for rschema, targetschemas, x in eschema.relation_definitions():
+        for rschema, targetschemas, role in eschema.relation_definitions():
             # skip automatically handled relations
             if rschema.type in DONT_CHECK_RTYPES_ON_ADD:
                 continue
-            if x == 'subject':
-                subjtype = eschema
-                objtype = targetschemas[0].type
-                cardindex = 0
+            if role == 'subject':
                 opcls = _CheckSRelationOp
             else:
-                subjtype = targetschemas[0].type
-                objtype = eschema
-                cardindex = 1
                 opcls = _CheckORelationOp
-            card = rschema.rproperty(subjtype, objtype, 'cardinality')
-            if card[cardindex] in '1+':
+            rdef = rschema.role_rdef(eschema, targetschemas[0], role)
+            if rdef.role_cardinality(role) in '1+':
                 self.checkrel_if_necessary(opcls, rschema.type, eid)
 
     def before_delete_relation(self):
@@ -173,7 +169,7 @@
         entity = self.entity
         for attr in entity.edited_attributes:
             if schema.rschema(attr).final:
-                constraints = [c for c in entity.e_schema.constraints(attr)
+                constraints = [c for c in entity.rdef(attr).constraints
                                if isinstance(c, RQLVocabularyConstraint)]
                 if constraints:
                     _CheckConstraintsOp(self._cw, constraints=constraints,
--- a/hooks/security.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/hooks/security.py	Fri Nov 20 19:35:54 2009 +0100
@@ -25,10 +25,10 @@
     for attr in editedattrs:
         if attr in defaults:
             continue
-        rschema = eschema.subjrels[attr]
-        if rschema.final: # non final relation are checked by other hooks
+        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' ?)
-            rschema.check_perm(session, 'add', eid)
+            rdef.check_perm(session, 'add', eid=eid)
 
 
 class _CheckEntityPermissionOp(hook.LateOperation):
@@ -43,7 +43,10 @@
 
 class _CheckRelationPermissionOp(hook.LateOperation):
     def precommit_event(self):
-        self.rschema.check_perm(self.session, self.action, self.eidfrom, self.eidto)
+        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)
 
     def commit_event(self):
         pass
@@ -95,7 +98,9 @@
             if (self.eidfrom, self.rtype, self.eidto) in nocheck:
                 return
             rschema = self._cw.repo.schema[self.rtype]
-            rschema.check_perm(self._cw, 'add', self.eidfrom, self.eidto)
+            rdef = rschema.rdef(self._cw.describe(self.eidfrom)[0],
+                                self._cw.describe(self.eidto)[0])
+            rdef.check_perm(session, 'add', fromeid=self.eidfrom, toeid=self.eidto)
 
 
 class AfterAddRelationSecurityHook(SecurityHook):
@@ -114,17 +119,7 @@
                                            eidfrom=self.eidfrom,
                                            eidto=self.eidto)
             else:
-                rschema.check_perm(self._cw, 'add', self.eidfrom, self.eidto)
-
-
-class BeforeDelRelationSecurityHook(SecurityHook):
-    __regid__ = 'securitybeforedelrelation'
-    events = ('before_delete_relation',)
+                rdef = rschema.rdef(session.describe(self.eidfrom)[0],
+                                    session.describe(self.eidto)[0])
+                rdef.check_perm(session, 'add', fromeid=self.eidfrom, toeid=self.eidto)
 
-    def __call__(self):
-        nocheck = self._cw.transaction_data.get('skip-security', ())
-        if (self.eidfrom, self.rtype, self.eidto) in nocheck:
-            return
-        self._cw.repo.schema[self.rtype].check_perm(self._cw, 'delete',
-                                                       self.eidfrom, self.eidto)
-
--- a/hooks/syncschema.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/hooks/syncschema.py	Fri Nov 20 19:35:54 2009 +0100
@@ -153,8 +153,11 @@
 
     def commit_event(self):
         rebuildinfered = self.session.data.get('rebuild-infered', True)
-        repo = self.session.repo
-        repo.set_schema(repo.schema, rebuildinfered=rebuildinfered)
+        self.repo.set_schema(self.repo.schema, rebuildinfered=rebuildinfered)
+        # CWUser class might have changed, update current session users
+        cwuser_cls = self.session.vreg['etypes'].etype_class('CWUser')
+        for session in self.repo._sessions.values():
+            session.user.__class__ = cwuser_cls
 
     def rollback_event(self):
         self.precommit_event()
@@ -188,19 +191,6 @@
         return i + 1
 
 
-class MemSchemaPermOperation(MemSchemaOperation):
-    """base class to synchronize schema permission definitions"""
-    def __init__(self, session, perm, etype_eid):
-        self.perm = perm
-        try:
-            self.name = session.entity_from_eid(etype_eid).name
-        except IndexError:
-            self.error('changing permission of a no more existant type #%s',
-                etype_eid)
-        else:
-            hook.Operation.__init__(self, session)
-
-
 # operations for high-level source database alteration  ########################
 
 class SourceDbCWETypeRename(hook.Operation):
@@ -495,7 +485,7 @@
             return
         subjtype, rtype, objtype = session.vreg.schema.schema_by_eid(rdef.eid)
         cstrtype = self.entity.type
-        oldcstr = rtype.constraint_by_type(subjtype, objtype, cstrtype)
+        oldcstr = rtype.rdef(subjtype, objtype).constraint_by_type(cstrtype)
         newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
         table = SQL_PREFIX + str(subjtype)
         column = SQL_PREFIX + str(rtype)
@@ -575,8 +565,7 @@
     """actually add the relation type to the instance's schema"""
     eid = None # make pylint happy
     def commit_event(self):
-        rschema = self.session.vreg.schema.add_relation_type(self.kobj)
-        rschema.set_default_groups()
+        self.session.vreg.schema.add_relation_type(self.kobj)
 
 
 class MemSchemaCWRTypeUpdate(MemSchemaOperation):
@@ -614,7 +603,7 @@
     def commit_event(self):
         # structure should be clean, not need to remove entity's relations
         # at this point
-        self.rschema._rproperties[self.kobj].update(self.values)
+        self.rschema.rdef[self.kobj].update(self.values)
 
 
 class MemSchemaRDefDel(MemSchemaOperation):
@@ -646,7 +635,7 @@
         subjtype, rtype, objtype = self.session.vreg.schema.schema_by_eid(rdef.eid)
         self.prepare_constraints(subjtype, rtype, objtype)
         cstrtype = self.entity.type
-        self.cstr = rtype.constraint_by_type(subjtype, objtype, cstrtype)
+        self.cstr = rtype.rdef(subjtype, objtype).constraint_by_type(cstrtype)
         self.newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
         self.newcstr.eid = self.entity.eid
 
@@ -672,13 +661,9 @@
         self.constraints.remove(self.cstr)
 
 
-class MemSchemaPermCWGroupAdd(MemSchemaPermOperation):
+class MemSchemaPermissionAdd(MemSchemaOperation):
     """synchronize schema when a *_permission relation has been added on a group
     """
-    def __init__(self, session, perm, etype_eid, group_eid):
-        self.group = session.entity_from_eid(group_eid).name
-        super(MemSchemaPermCWGroupAdd, self).__init__(
-            session, perm, etype_eid)
 
     def commit_event(self):
         """the observed connections pool has been commited"""
@@ -688,17 +673,21 @@
             # duh, schema not found, log error and skip operation
             self.error('no schema for %s', self.name)
             return
-        groups = list(erschema.get_groups(self.perm))
+        perms = list(erschema.action_permissions(self.action))
+        if hasattr(self, group_eid):
+            perm = self.session.entity_from_eid(self.group_eid).name
+        else:
+            perm = erschema.rql_expression(self.expr)
         try:
-            groups.index(self.group)
-            self.warning('group %s already have permission %s on %s',
-                         self.group, self.perm, erschema.type)
+            perms.index(perm)
+            self.warning('%s already in permissions for %s on %s',
+                         perm, self.action, erschema)
         except ValueError:
-            groups.append(self.group)
-            erschema.set_groups(self.perm, groups)
+            perms.append(perm)
+            erschema.set_action_permissions(self.action, perms)
 
 
-class MemSchemaPermCWGroupDel(MemSchemaPermCWGroupAdd):
+class MemSchemaPermissionDel(MemSchemaPermissionAdd):
     """synchronize schema when a *_permission relation has been deleted from a
     group
     """
@@ -711,60 +700,17 @@
             # duh, schema not found, log error and skip operation
             self.error('no schema for %s', self.name)
             return
-        groups = list(erschema.get_groups(self.perm))
-        try:
-            groups.remove(self.group)
-            erschema.set_groups(self.perm, groups)
-        except ValueError:
-            self.error('can\'t remove permission %s on %s to group %s',
-                self.perm, erschema.type, self.group)
-
-
-class MemSchemaPermRQLExpressionAdd(MemSchemaPermOperation):
-    """synchronize schema when a *_permission relation has been added on a rql
-    expression
-    """
-    def __init__(self, session, perm, etype_eid, expression):
-        self.expr = expression
-        super(MemSchemaPermRQLExpressionAdd, self).__init__(
-            session, perm, etype_eid)
-
-    def commit_event(self):
-        """the observed connections pool has been commited"""
+        perms = list(erschema.action_permissions(self.action))
+        if hasattr(self, group_eid):
+            perm = self.session.entity_from_eid(self.group_eid).name
+        else:
+            perm = erschema.rql_expression(self.expr)
         try:
-            erschema = self.session.vreg.schema[self.name]
-        except KeyError:
-            # duh, schema not found, log error and skip operation
-            self.error('no schema for %s', self.name)
-            return
-        exprs = list(erschema.get_rqlexprs(self.perm))
-        exprs.append(erschema.rql_expression(self.expr))
-        erschema.set_rqlexprs(self.perm, exprs)
-
-
-class MemSchemaPermRQLExpressionDel(MemSchemaPermRQLExpressionAdd):
-    """synchronize schema when a *_permission relation has been deleted from an
-    rql expression
-    """
-
-    def commit_event(self):
-        """the observed connections pool has been commited"""
-        try:
-            erschema = self.session.vreg.schema[self.name]
-        except KeyError:
-            # duh, schema not found, log error and skip operation
-            self.error('no schema for %s', self.name)
-            return
-        rqlexprs = list(erschema.get_rqlexprs(self.perm))
-        for i, rqlexpr in enumerate(rqlexprs):
-            if rqlexpr.expression == self.expr:
-                rqlexprs.pop(i)
-                break
-        else:
-            self.error('can\'t remove permission %s on %s for expression %s',
-                self.perm, erschema.type, self.expr)
-            return
-        erschema.set_rqlexprs(self.perm, rqlexprs)
+            perms.remove(self.group)
+            erschema.set_action_permissions(self.action, perms)
+        except ValueError:
+            self.error('can\'t remove permission %s for %s on %s',
+                       perm, self.action, erschema)
 
 
 class MemSchemaSpecializesAdd(MemSchemaOperation):
@@ -835,6 +781,7 @@
     * register an operation to add the entity type to the instance's
       schema on commit
     """
+<<<<<<< /home/syt/src/fcubicweb/cubicweb/hooks/syncschema.py
     __regid__ = 'syncaddcwetype'
     events = ('after_add_entity',)
 
@@ -859,7 +806,7 @@
             rschema = schema[rtype]
             sampletype = rschema.subjects()[0]
             desttype = rschema.objects()[0]
-            props = rschema.rproperties(sampletype, desttype)
+            props = rschema.rdef(sampletype, desttype)
             relrqls += list(ss.rdef2rql(rschema, name, desttype, props))
         # now remove it !
         schema.del_entity_type(name)
@@ -965,6 +912,17 @@
             SourceDbCWRTypeUpdate(self._cw, rschema=rschema, values=newvalues,
                                   entity=entity)
 
+def check_valid_changes(session, entity, ro_attrs=('name', 'final')):
+    errors = {}
+    # don't use getattr(entity, attr), we would get the modified value if any
+    for attr in ro_attrs:
+        if attr in entity.edited_attributes:
+            origval, newval = entity_oldnewvalue(entity, attr)
+            if newval != origval:
+                errors[attr] = session._("can't change the %s attribute") % \
+                               display_name(session, attr)
+    if errors:
+        raise ValidationError(entity.eid, errors)
 
 # relation_type hooks ##########################################################
 
@@ -1102,8 +1060,8 @@
         entity = self._cw.entity_from_eid(self.eidto)
         subjtype, rtype, objtype = schema.schema_by_eid(self.eidfrom)
         try:
-            cstr = rtype.constraint_by_type(subjtype, objtype,
-                                            entity.cstrtype[0].name)
+            cstr = rtype.rdef(subjtype, objtype).constraint_by_type(
+                entity.cstrtype[0].name)
         except IndexError:
             self._cw.critical('constraint type no more accessible')
         else:
@@ -1124,12 +1082,14 @@
     events = ('after_add_relation',)
 
     def __call__(self):
-        perm = self.rtype.split('_', 1)[0]
+        action = self.rtype.split('_', 1)[0]
         if self._cw.describe(self.eidto)[0] == 'CWGroup':
-            MemSchemaPermCWGroupAdd(self._cw, perm, self.eidfrom, self.eidto)
+            MemSchemaPermissionAdd(self._cw, action=action, eid=self.eidfrom,
+                                   group_eid=self.eidto)
         else: # RQLExpression
             expr = self._cw.entity_from_eid(self.eidto).expression
-            MemSchemaPermRQLExpressionAdd(self._cw, perm, self.eidfrom, expr)
+            MemSchemaPermissionAdd(session, action=action, eid=self.eidfrom,
+                                   expr=expr)
 
 
 class BeforeDelPermissionHook(AfterAddPermissionHook):
@@ -1143,12 +1103,14 @@
     def __call__(self):
         if self._cw.deleted_in_transaction(self.eidfrom):
             return
-        perm = self.rtype.split('_', 1)[0]
+        action = self.rtype.split('_', 1)[0]
         if self._cw.describe(self.eidto)[0] == 'CWGroup':
-            MemSchemaPermCWGroupDel(self._cw, perm, self.eidfrom, self.eidto)
+            MemSchemaPermissionDel(self._cw, action=action, eid=self.eidfrom,
+                                   group_eid=self.eidto)
         else: # RQLExpression
             expr = self._cw.entity_from_eid(self.eidto).expression
-            MemSchemaPermRQLExpressionDel(self._cw, perm, self.eidfrom, expr)
+            MemSchemaPermissionDel(self._cw, action=action, eid=self.eidfrom,
+                                   expr=expr)
 
 
 # specializes synchronization hooks ############################################
--- a/hooks/workflow.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/hooks/workflow.py	Fri Nov 20 19:35:54 2009 +0100
@@ -224,9 +224,10 @@
                     msg = session._("transition doesn't belong to entity's workflow")
                     raise ValidationError(entity.eid, {'by_transition': msg})
                 if not tr.has_input_state(fromstate):
-                    msg = session._("transition isn't allowed")
+                    msg = session._("transition %s isn't allowed from %s") % (
+                        _(tr.name), _(fromstate.name))
                     raise ValidationError(entity.eid, {'by_transition': msg})
-                if not tr.may_be_fired(foreid):
+                 if not tr.may_be_fired(foreid):
                     msg = session._("transition may not be fired")
                     raise ValidationError(entity.eid, {'by_transition': msg})
             if entity.get('to_state'):
--- a/i18n/en.po	Sun Nov 08 21:53:18 2009 +0100
+++ b/i18n/en.po	Fri Nov 20 19:35:54 2009 +0100
@@ -356,6 +356,9 @@
 msgid "From:"
 msgstr ""
 
+msgid "Help"
+msgstr ""
+
 msgid "Int"
 msgstr "Integer"
 
@@ -787,6 +790,12 @@
 msgid "actions_follow_description"
 msgstr ""
 
+msgid "actions_help"
+msgstr ""
+
+msgid "actions_help_description"
+msgstr ""
+
 msgid "actions_logout"
 msgstr "logout"
 
--- a/i18n/es.po	Sun Nov 08 21:53:18 2009 +0100
+++ b/i18n/es.po	Fri Nov 20 19:35:54 2009 +0100
@@ -364,6 +364,9 @@
 msgid "From:"
 msgstr "De: "
 
+msgid "Help"
+msgstr ""
+
 msgid "Int"
 msgstr "Número entero"
 
@@ -810,6 +813,12 @@
 msgid "actions_follow_description"
 msgstr ""
 
+msgid "actions_help"
+msgstr ""
+
+msgid "actions_help_description"
+msgstr ""
+
 msgid "actions_logout"
 msgstr "Desconectarse"
 
--- a/i18n/fr.po	Sun Nov 08 21:53:18 2009 +0100
+++ b/i18n/fr.po	Fri Nov 20 19:35:54 2009 +0100
@@ -363,6 +363,9 @@
 msgid "From:"
 msgstr "De :"
 
+msgid "Help"
+msgstr "Aide"
+
 msgid "Int"
 msgstr "Nombre entier"
 
@@ -815,6 +818,12 @@
 msgid "actions_follow_description"
 msgstr ""
 
+msgid "actions_help"
+msgstr ""
+
+msgid "actions_help_description"
+msgstr ""
+
 msgid "actions_logout"
 msgstr "se déconnecter"
 
@@ -2037,8 +2046,9 @@
 "destination state. No destination state means that transition should go back "
 "to the state from which we've entered the subworkflow."
 msgstr ""
-"état de destination de la transition. Si aucun état de destination n'est spécifié, la transition "
-"ira vers l'état depuis lequel l'entité est entrée dans le sous-workflow."
+"état de destination de la transition. Si aucun état de destination n'est "
+"spécifié, la transition ira vers l'état depuis lequel l'entité est entrée "
+"dans le sous-workflow."
 
 msgid "destination_state"
 msgstr "état de destination"
@@ -3076,7 +3086,7 @@
 msgstr "vues possibles"
 
 msgid "powered by CubicWeb"
-msgstr "utilise la technologie CubicWeb"
+msgstr "construit avec CubicWeb"
 
 msgid "prefered_form"
 msgstr "forme préférée"
@@ -3229,10 +3239,10 @@
 
 msgctxt "CWGroup"
 msgid "require_group_object"
-msgstr "dé"
+msgstr "de"
 
 msgid "require_group_object"
-msgstr "à les droits"
+msgstr "a les droits"
 
 msgid "require_permission"
 msgstr "require permission"
@@ -3815,7 +3825,7 @@
 msgstr "peut modifier"
 
 msgid "update_permission_object"
-msgstr "à la permission de modifier"
+msgstr "a la permission de modifier"
 
 msgid "updated"
 msgstr ""
--- a/misc/migration/bootstrapmigration_repository.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/misc/migration/bootstrapmigration_repository.py	Fri Nov 20 19:35:54 2009 +0100
@@ -28,6 +28,17 @@
     session.set_shared_data('do-not-insert-cwuri', False)
 
 if applcubicwebversion < (3, 5, 0) and cubicwebversion >= (3, 5, 0):
+    # check that migration is not doomed
+    rset = rql('Any X,Y WHERE X transition_of E, Y transition_of E, '
+               'X name N, Y name N, NOT X identity Y',
+               ask_confirm=False)
+    if rset:
+        from logilab.common.shellutils import ASK
+        if not ASK.confirm('Migration will fail because of transitions with the same name. '
+                           'Continue anyway ?'):
+            import sys
+            sys.exit(1)
+    # proceed with migration
     add_entity_type('Workflow')
     add_entity_type('BaseTransition')
     add_entity_type('WorkflowTransition')
--- a/misc/migration/postcreate.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/misc/migration/postcreate.py	Fri Nov 20 19:35:54 2009 +0100
@@ -34,7 +34,10 @@
 # create anonymous user if all-in-one config and anonymous user has been specified
 if hasattr(config, 'anonymous_user'):
     anonlogin, anonpwd = config.anonymous_user()
-    if anonlogin:
+    if anonlogin == session.user.login:
+        print 'you are using a manager account as anonymous user.'
+        print 'Hopefully this is not a production instance...'
+    elif anonlogin:
         rql('INSERT CWUser X: X login %(login)s, X upassword %(pwd)s,'
             'X in_group G WHERE G name "guests"',
             {'login': unicode(anonlogin), 'pwd': anonpwd})
--- a/rqlrewrite.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/rqlrewrite.py	Fri Nov 20 19:35:54 2009 +0100
@@ -13,6 +13,7 @@
 from rql import nodes as n, stmts, TypeResolverException
 
 from logilab.common.compat import any
+from logilab.common.graph import has_path
 
 from cubicweb import Unauthorized, typed_eid
 
@@ -134,7 +135,7 @@
         if len(self.select.solutions) < len(self.solutions):
             raise Unsupported()
 
-    def rewrite(self, select, snippets, solutions, kwargs):
+    def rewrite(self, select, snippets, solutions, kwargs, existingvars=None):
         """
         snippets: (varmap, list of rql expression)
                   with varmap a *tuple* (select var, snippet var)
@@ -146,6 +147,7 @@
         self.removing_ambiguity = False
         self.exists_snippet = {}
         self.pending_keys = []
+        self.existingvars = existingvars
         # we have to annotate the rqlst before inserting snippets, even though
         # we'll have to redo it latter
         self.annotate(select)
@@ -213,6 +215,14 @@
 
     def insert_snippet(self, varmap, snippetrqlst, parent=None):
         new = snippetrqlst.where.accept(self)
+        existing = self.existingvars
+        self.existingvars = None
+        try:
+            return self._insert_snippet(varmap, parent, new)
+        finally:
+            self.existingvars = existing
+
+    def _insert_snippet(self, varmap, parent, new):
         if new is not None:
             if self.varinfo.get('stinfo', {}).get('optrelations'):
                 assert parent is None
@@ -392,12 +402,12 @@
                 orel = self.varinfo['lhs_rels'][sniprel.r_type]
                 cardindex = 0
                 ttypes_func = rschema.objects
-                rprop = rschema.rproperty
+                rdef = rschema.rdef
             else: # target == 'subject':
                 orel = self.varinfo['rhs_rels'][sniprel.r_type]
                 cardindex = 1
                 ttypes_func = rschema.subjects
-                rprop = lambda x, y, z: rschema.rproperty(y, x, z)
+                rdef = lambda x, y: rschema.rdef(y, x)
         except KeyError, ex:
             # may be raised by self.varinfo['xhs_rels'][sniprel.r_type]
             return None
@@ -409,7 +419,7 @@
         # variable from the original query
         for etype in self.varinfo['stinfo']['possibletypes']:
             for ttype in ttypes_func(etype):
-                if rprop(etype, ttype, 'cardinality')[cardindex] in '+*':
+                if rdef(etype, ttype).cardinality[cardindex] in '+*':
                     return None
         return orel
 
@@ -478,8 +488,26 @@
     def visit_exists(self, node):
         return self._visit_unary(node, n.Exists)
 
+    def keep_var(self, varname):
+        if varname in 'SO':
+            return varname in self.existingvars
+        if varname == 'U':
+            return True
+        vargraph = self.current_expr.vargraph
+        for existingvar in self.existingvars:
+            #path = has_path(vargraph, varname, existingvar)
+            if has_path(vargraph, varname, existingvar):
+                return True
+        # no path from this variable to an existing variable
+        return False
+
     def visit_relation(self, node):
         lhs, rhs = node.get_variable_parts()
+        # remove relations where an unexistant variable and or a variable linked
+        # to an unexistant variable is used.
+        if self.existingvars:
+            if not self.keep_var(lhs.name):
+                return
         if node.r_type in ('has_add_permission', 'has_update_permission',
                            'has_delete_permission', 'has_read_permission'):
             assert lhs.name == 'U'
@@ -488,6 +516,8 @@
             self.pending_keys.append( (key, action) )
             return
         if isinstance(rhs, n.VariableRef):
+            if self.existingvars and not self.keep_var(rhs.name):
+                return
             if lhs.name in self.revvarmap and rhs.name != 'U':
                 orel = self._may_be_shared_with(node, 'object', lhs.name)
                 if orel is not None:
--- a/rset.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/rset.py	Fri Nov 20 19:35:54 2009 +0100
@@ -455,25 +455,22 @@
                 select = rqlst
             # take care, due to outer join support, we may find None
             # values for non final relation
-            for i, attr, x in attr_desc_iterator(select, col):
+            for i, attr, role in attr_desc_iterator(select, col):
                 outerselidx = rqlst.subquery_selection_index(select, i)
                 if outerselidx is None:
                     continue
-                if x == 'subject':
+                if role == 'subject':
                     rschema = eschema.subjrels[attr]
                     if rschema.final:
                         entity[attr] = rowvalues[outerselidx]
                         continue
-                    tetype = rschema.objects(etype)[0]
-                    card = rschema.rproperty(etype, tetype, 'cardinality')[0]
                 else:
                     rschema = eschema.objrels[attr]
-                    tetype = rschema.subjects(etype)[0]
-                    card = rschema.rproperty(tetype, etype, 'cardinality')[1]
+                rdef = eschema.rdef(attr, role)
                 # only keep value if it can't be multivalued
-                if card in '1?':
+                if rdef.role_cardinality(role) in '1?':
                     if rowvalues[outerselidx] is None:
-                        if x == 'subject':
+                        if role == 'subject':
                             rql = 'Any Y WHERE X %s Y, X eid %s'
                         else:
                             rql = 'Any Y WHERE Y %s X, X eid %s'
@@ -481,7 +478,7 @@
                         req.decorate_rset(rrset)
                     else:
                         rrset = self._build_entity(row, outerselidx).as_rset()
-                    entity.set_related_cache(attr, x, rrset)
+                    entity.set_related_cache(attr, role, rrset)
         return entity
 
     @cached
--- a/schema.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/schema.py	Fri Nov 20 19:35:54 2009 +0100
@@ -20,7 +20,8 @@
 from logilab.common.compat import any
 
 from yams import BadSchemaDefinition, buildobjs as ybo
-from yams.schema import Schema, ERSchema, EntitySchema, RelationSchema
+from yams.schema import Schema, ERSchema, EntitySchema, RelationSchema, \
+     RelationDefinitionSchema, PermissionMixIn
 from yams.constraints import (BaseConstraint, StaticVocabularyConstraint,
                               FormatConstraint)
 from yams.reader import (CONSTRAINTS, PyFileReader, SchemaLoader,
@@ -127,7 +128,7 @@
 ERSchema.display_name = ERSchema_display_name
 
 @cached
-def ERSchema_get_groups(self, action):
+def get_groups(self, action):
     """return the groups authorized to perform <action> on entities of
     this type
 
@@ -140,28 +141,13 @@
     assert action in self.ACTIONS, action
     #assert action in self._groups, '%s %s' % (self, action)
     try:
-        return frozenset(g for g in self._groups[action] if isinstance(g, basestring))
+        return frozenset(g for g in self.permissions[action] if isinstance(g, basestring))
     except KeyError:
         return ()
-ERSchema.get_groups = ERSchema_get_groups
-
-def ERSchema_set_groups(self, action, groups):
-    """set the groups allowed to perform <action> on entities of this type. Don't
-    change rql expressions for the same action.
-
-    :type action: str
-    :param action: the name of a permission
-
-    :type groups: list or tuple
-    :param groups: names of the groups granted to do the given action
-    """
-    assert action in self.ACTIONS, action
-    clear_cache(self, 'ERSchema_get_groups')
-    self._groups[action] = tuple(groups) + self.get_rqlexprs(action)
-ERSchema.set_groups = ERSchema_set_groups
+PermissionMixIn.get_groups = get_groups
 
 @cached
-def ERSchema_get_rqlexprs(self, action):
+def get_rqlexprs(self, action):
     """return the rql expressions representing queries to check the user is allowed
     to perform <action> on entities of this type
 
@@ -174,27 +160,13 @@
     assert action in self.ACTIONS, action
     #assert action in self._rqlexprs, '%s %s' % (self, action)
     try:
-        return tuple(g for g in self._groups[action] if not isinstance(g, basestring))
+        return tuple(g for g in self.permissions[action] if not isinstance(g, basestring))
     except KeyError:
         return ()
-ERSchema.get_rqlexprs = ERSchema_get_rqlexprs
-
-def ERSchema_set_rqlexprs(self, action, rqlexprs):
-    """set the rql expression allowing to perform <action> on entities of this type. Don't
-    change groups for the same action.
-
-    :type action: str
-    :param action: the name of a permission
+PermissionMixIn.get_rqlexprs = get_rqlexprs
 
-    :type rqlexprs: list or tuple
-    :param rqlexprs: the rql expressions allowing the given action
-    """
-    assert action in self.ACTIONS, action
-    clear_cache(self, 'ERSchema_get_rqlexprs')
-    self._groups[action] = tuple(self.get_groups(action)) + tuple(rqlexprs)
-ERSchema.set_rqlexprs = ERSchema_set_rqlexprs
-
-def ERSchema_set_permissions(self, action, permissions):
+orig_set_action_permissions = PermissionMixIn.set_action_permissions
+def set_action_permissions(self, action, permissions):
     """set the groups and rql expressions allowing to perform <action> on
     entities of this type
 
@@ -204,22 +176,12 @@
     :type permissions: tuple
     :param permissions: the groups and rql expressions allowing the given action
     """
-    assert action in self.ACTIONS, action
-    clear_cache(self, 'ERSchema_get_rqlexprs')
-    clear_cache(self, 'ERSchema_get_groups')
-    self._groups[action] = tuple(permissions)
-ERSchema.set_permissions = ERSchema_set_permissions
+    orig_set_action_permissions(self, action, tuple(permissions))
+    clear_cache(self, 'get_rqlexprs')
+    clear_cache(self, 'get_groups')
+PermissionMixIn.set_action_permissions = set_action_permissions
 
-def ERSchema_has_perm(self, session, action, *args, **kwargs):
-    """return true if the action is granted globaly or localy"""
-    try:
-        self.check_perm(session, action, *args, **kwargs)
-        return True
-    except Unauthorized:
-        return False
-ERSchema.has_perm = ERSchema_has_perm
-
-def ERSchema_has_local_role(self, action):
+def has_local_role(self, action):
     """return true if the action *may* be granted localy (eg either rql
     expressions or the owners group are used in security definition)
 
@@ -230,9 +192,83 @@
     if self.get_rqlexprs(action):
         return True
     if action in ('update', 'delete'):
-        return self.has_group(action, 'owners')
+        return 'owners' in self.get_groups(action)
     return False
-ERSchema.has_local_role = ERSchema_has_local_role
+PermissionMixIn.has_local_role = has_local_role
+
+def may_have_permission(self, action, req):
+    if action != 'read' and not (self.has_local_role('read') or
+                                 self.has_perm(req, 'read')):
+        return False
+    return self.has_local_role(action) or self.has_perm(req, action)
+PermissionMixIn.may_have_permission = may_have_permission
+
+def has_perm(self, session, action, **kwargs):
+    """return true if the action is granted globaly or localy"""
+    try:
+        self.check_perm(session, action, **kwargs)
+        return True
+    except Unauthorized:
+        return False
+PermissionMixIn.has_perm = has_perm
+
+def check_perm(self, session, action, **kwargs):
+    # NB: session may be a server session or a request object check user is
+    # in an allowed group, if so that's enough internal sessions should
+    # always stop there
+    groups = self.get_groups(action)
+    if session.user.matching_groups(groups):
+        return
+    # if 'owners' in allowed groups, check if the user actually owns this
+    # object, if so that's enough
+    if 'owners' in groups and 'eid' in kwargs and session.user.owns(kwargs['eid']):
+        return
+    # else if there is some rql expressions, check them
+    if any(rqlexpr.check(session, **kwargs)
+           for rqlexpr in self.get_rqlexprs(action)):
+        return
+    raise Unauthorized(action, str(self))
+PermissionMixIn.check_perm = check_perm
+
+
+RelationDefinitionSchema._RPROPERTIES['eid'] = None
+
+def rql_expression(self, expression, mainvars=None, eid=None):
+    """rql expression factory"""
+    if self.rtype.final:
+        return ERQLExpression(expression, mainvars, eid)
+    return RRQLExpression(expression, mainvars, eid)
+RelationDefinitionSchema.rql_expression = rql_expression
+
+orig_check_permission_definitions = RelationDefinitionSchema.check_permission_definitions
+def check_permission_definitions(self):
+    orig_check_permission_definitions(self)
+    schema = self.subject.schema
+    for action, groups in self.permissions.iteritems():
+        for group_or_rqlexpr in groups:
+            if action == 'read' and \
+                   isinstance(group_or_rqlexpr, RQLExpression):
+                msg = "can't use rql expression for read permission of %s"
+                raise BadSchemaDefinition(msg % self)
+            elif self.final and isinstance(group_or_rqlexpr, RRQLExpression):
+                if schema.reading_from_database:
+                    # we didn't have final relation earlier, so turn
+                    # RRQLExpression into ERQLExpression now
+                    rqlexpr = group_or_rqlexpr
+                    newrqlexprs = [x for x in self.get_rqlexprs(action)
+                                   if not x is rqlexpr]
+                    newrqlexprs.append(ERQLExpression(rqlexpr.expression,
+                                                      rqlexpr.mainvars,
+                                                      rqlexpr.eid))
+                    self.set_rqlexprs(action, newrqlexprs)
+                else:
+                    msg = "can't use RRQLExpression on %s, use an ERQLExpression"
+                    raise BadSchemaDefinition(msg % self)
+            elif not self.final and \
+                     isinstance(group_or_rqlexpr, ERQLExpression):
+                msg = "can't use ERQLExpression on %s, use a RRQLExpression"
+                raise BadSchemaDefinition(msg % self)
+RelationDefinitionSchema.check_permission_definitions = check_permission_definitions
 
 
 def system_etypes(schema):
@@ -256,8 +292,8 @@
             eid = getattr(edef, 'eid', None)
         self.eid = eid
         # take care: no _groups attribute when deep-copying
-        if getattr(self, '_groups', None):
-            for groups in self._groups.itervalues():
+        if getattr(self, 'permissions', None):
+            for groups in self.permissions.itervalues():
                 for group_or_rqlexpr in groups:
                     if isinstance(group_or_rqlexpr, RRQLExpression):
                         msg = "can't use RRQLExpression on an entity type, use an ERQLExpression (%s)"
@@ -304,7 +340,7 @@
             if rschema.final:
                 if rschema == 'has_text':
                     has_has_text = True
-                elif self.rproperty(rschema, 'fulltextindexed'):
+                elif self.rdef(rschema).get('fulltextindexed'):
                     may_need_has_text = True
             elif rschema.fulltext_container:
                 if rschema.fulltext_container == 'subject':
@@ -329,32 +365,12 @@
         """return True if this entity type is used to build the schema"""
         return self.type in SCHEMA_TYPES
 
-    def check_perm(self, session, action, eid=None):
-        # NB: session may be a server session or a request object
-        user = session.user
-        # check user is in an allowed group, if so that's enough
-        # internal sessions should always stop there
-        if user.matching_groups(self.get_groups(action)):
-            return
-        # if 'owners' in allowed groups, check if the user actually owns this
-        # object, if so that's enough
-        if eid is not None and 'owners' in self.get_groups(action) and \
-               user.owns(eid):
-            return
-        # else if there is some rql expressions, check them
-        if any(rqlexpr.check(session, eid)
-               for rqlexpr in self.get_rqlexprs(action)):
-            return
-        raise Unauthorized(action, str(self))
-
     def rql_expression(self, expression, mainvars=None, eid=None):
         """rql expression factory"""
         return ERQLExpression(expression, mainvars, eid)
 
 
 class CubicWebRelationSchema(RelationSchema):
-    RelationSchema._RPROPERTIES['eid'] = None
-    _perms_checked = False
 
     def __init__(self, schema=None, rdef=None, eid=None, **kwargs):
         if rdef is not None:
@@ -369,73 +385,52 @@
     def meta(self):
         return self.type in META_RTYPES
 
-    def update(self, subjschema, objschema, rdef):
-        super(CubicWebRelationSchema, self).update(subjschema, objschema, rdef)
-        if not self._perms_checked and self._groups:
-            for action, groups in self._groups.iteritems():
-                for group_or_rqlexpr in groups:
-                    if action == 'read' and \
-                           isinstance(group_or_rqlexpr, RQLExpression):
-                        msg = "can't use rql expression for read permission of "\
-                              "a relation type (%s)"
-                        raise BadSchemaDefinition(msg % self.type)
-                    elif self.final and isinstance(group_or_rqlexpr, RRQLExpression):
-                        if self.schema.reading_from_database:
-                            # we didn't have final relation earlier, so turn
-                            # RRQLExpression into ERQLExpression now
-                            rqlexpr = group_or_rqlexpr
-                            newrqlexprs = [x for x in self.get_rqlexprs(action) if not x is rqlexpr]
-                            newrqlexprs.append(ERQLExpression(rqlexpr.expression,
-                                                              rqlexpr.mainvars,
-                                                              rqlexpr.eid))
-                            self.set_rqlexprs(action, newrqlexprs)
-                        else:
-                            msg = "can't use RRQLExpression on a final relation "\
-                                  "type (eg attribute relation), use an ERQLExpression (%s)"
-                            raise BadSchemaDefinition(msg % self.type)
-                    elif not self.final and \
-                             isinstance(group_or_rqlexpr, ERQLExpression):
-                        msg = "can't use ERQLExpression on a relation type, use "\
-                              "a RRQLExpression (%s)"
-                        raise BadSchemaDefinition(msg % self.type)
-            self._perms_checked = True
-
-    def cardinality(self, subjtype, objtype, target):
-        card = self.rproperty(subjtype, objtype, 'cardinality')
-        return (target == 'subject' and card[0]) or \
-               (target == 'object' and card[1])
-
     def schema_relation(self):
         """return True if this relation type is used to build the schema"""
         return self.type in SCHEMA_TYPES
 
-    def physical_mode(self):
-        """return an appropriate mode for physical storage of this relation type:
-        * 'subjectinline' if every possible subject cardinalities are 1 or ?
-        * 'objectinline' if 'subjectinline' mode is not possible but every
-          possible object cardinalities are 1 or ?
-        * None if neither 'subjectinline' and 'objectinline'
-        """
-        assert not self.final
-        return self.inlined and 'subjectinline' or None
+    def may_have_permission(self, action, req, eschema=None, role=None):
+        if eschema is not None:
+            for tschema in rschema.targets(eschema, role):
+                rdef = rschema.role_rdef(eschema, tschema, role)
+                if rdef.may_have_permission(action, req):
+                    return True
+        else:
+            for rdef in self.rdefs.itervalues():
+                if rdef.may_have_permission(action, req):
+                    return True
+        return False
 
-    def check_perm(self, session, action, *args, **kwargs):
-        # NB: session may be a server session or a request object check user is
-        # in an allowed group, if so that's enough internal sessions should
-        # always stop there
-        if session.user.matching_groups(self.get_groups(action)):
-            return
-        # else if there is some rql expressions, check them
-        if any(rqlexpr.check(session, *args, **kwargs)
-               for rqlexpr in self.get_rqlexprs(action)):
-            return
-        raise Unauthorized(action, str(self))
+    def has_perm(self, session, action, **kwargs):
+        """return true if the action is granted globaly or localy"""
+        if 'fromeid' in kwargs:
+            subjtype = session.describe(kwargs['fromeid'])
+        else:
+            subjtype = None
+        if 'toeid' in kwargs:
+            objtype = session.describe(kwargs['toeid'])
+        else:
+            objtype = Nono
+        if objtype and subjtype:
+            return self.rdef(subjtype, objtype).has_perm(session, action, **kwargs)
+        elif subjtype:
+            for tschema in rschema.targets(subjtype, 'subject'):
+                rdef = rschema.rdef(subjtype, tschema)
+                if not rdef.has_perm(action, req, **kwargs):
+                    return False
+        elif objtype:
+            for tschema in rschema.targets(objtype, 'object'):
+                rdef = rschema.rdef(tschema, objtype)
+                if not rdef.has_perm(action, req, **kwargs):
+                    return False
+        else:
+            for rdef in self.rdefs.itervalues():
+                if not rdef.has_perm(action, req, **kwargs):
+                    return False
 
-    def rql_expression(self, expression, mainvars=None, eid=None):
-        """rql expression factory"""
-        if self.final:
-            return ERQLExpression(expression, mainvars, eid)
-        return RRQLExpression(expression, mainvars, eid)
+    @deprecated('use .rdef(subjtype, objtype).role_cardinality(role)')
+    def cardinality(self, subjtype, objtype, target):
+        return self.rdef(subjtype, objtype).role_cardinality(target)
 
 
 class CubicWebSchema(Schema):
@@ -460,13 +455,10 @@
         ybo.register_base_types(self)
         rschema = self.add_relation_type(ybo.RelationType('eid'))
         rschema.final = True
-        rschema.set_default_groups()
         rschema = self.add_relation_type(ybo.RelationType('has_text'))
         rschema.final = True
-        rschema.set_default_groups()
         rschema = self.add_relation_type(ybo.RelationType('identity'))
         rschema.final = False
-        rschema.set_default_groups()
 
     def add_entity_type(self, edef):
         edef.name = edef.name.encode()
@@ -508,11 +500,10 @@
         rdef.name = rdef.name.lower()
         rdef.subject = bw_normalize_etype(rdef.subject)
         rdef.object = bw_normalize_etype(rdef.object)
-        if super(CubicWebSchema, self).add_relation_def(rdef):
+        rdefs = super(CubicWebSchema, self).add_relation_def(rdef)
+        if rdefs:
             try:
-                self._eid_index[rdef.eid] = (self.eschema(rdef.subject),
-                                             self.rschema(rdef.name),
-                                             self.eschema(rdef.object))
+                self._eid_index[rdef.eid] = rdefs
             except AttributeError:
                 pass # not a serialized schema
 
@@ -523,7 +514,9 @@
 
     def del_relation_def(self, subjtype, rtype, objtype):
         for k, v in self._eid_index.items():
-            if v == (subjtype, rtype, objtype):
+            if not isinstance(v, RelationDefinitionSchema):
+                continue
+            if v.subject == subjtype and v.rtype == rtype and v.object == objtype:
                 del self._eid_index[k]
                 break
         super(CubicWebSchema, self).del_relation_def(subjtype, rtype, objtype)
@@ -646,7 +639,7 @@
             if len(self.rqlst.defined_vars[mainvar].references()) <= 2:
                 _LOGGER.warn('You did not use the %s variable in your RQL '
                              'expression %s', mainvar, self)
-        # syntax tree used by read security (inserted in queries when necessary
+        # syntax tree used by read security (inserted in queries when necessary)
         self.snippet_rqlst = parse(self.minimal_rql, print_errors=False).children[0]
 
     def __str__(self):
@@ -654,6 +647,11 @@
     def __repr__(self):
         return '%s(%s)' % (self.__class__.__name__, self.full_rql)
 
+    def __cmp__(self, other):
+        if hasattr(other, 'expression'):
+            return cmp(other.expression, self.expression)
+        return False
+
     def __deepcopy__(self, memo):
         return self.__class__(self.expression, self.mainvars)
     def __getstate__(self):
@@ -755,7 +753,7 @@
                 for eaction, var, col in has_perm_defs:
                     for i in xrange(len(rset)):
                         eschema = get_eschema(rset.description[i][col])
-                        eschema.check_perm(session, eaction, rset[i][col])
+                        eschema.check_perm(session, eaction, eid=rset[i][col])
                 if self.eid is not None:
                     session.local_perm_cache[key] = True
                 return True
@@ -806,10 +804,24 @@
                 mainvars.append('S')
             if 'O' in defined:
                 mainvars.append('O')
+            if 'U' in defined:
+                mainvars.append('U')
             if not mainvars:
                 raise Exception('unable to guess selection variables')
             mainvars = ','.join(mainvars)
         RQLExpression.__init__(self, expression, mainvars, eid)
+        # graph of links between variable, used by rql rewriter
+        self.vargraph = {}
+        for relation in self.rqlst.get_nodes(nodes.Relation):
+            try:
+                rhsvarname = relation.children[1].children[0].variable.name
+                lhsvarname = relation.children[0].name
+            except AttributeError:
+                pass
+            else:
+                self.vargraph.setdefault(lhsvarname, []).append(rhsvarname)
+                self.vargraph.setdefault(rhsvarname, []).append(lhsvarname)
+                #self.vargraph[(lhsvarname, rhsvarname)] = relation.r_type
 
     @property
     def full_rql(self):
--- a/schemas/Bookmark.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/schemas/Bookmark.py	Fri Nov 20 19:35:54 2009 +0100
@@ -13,7 +13,7 @@
 
 class Bookmark(EntityType):
     """bookmarks are used to have user's specific internal links"""
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests',),
         'add':    ('managers', 'users',),
         'delete': ('managers', 'owners',),
@@ -29,7 +29,7 @@
 
 
 class bookmarked_by(RelationType):
-    permissions = {'read':   ('managers', 'users', 'guests',),
+    __permissions__ = {'read':   ('managers', 'users', 'guests',),
                    # test user in users group to avoid granting permission to anonymous user
                    'add':    ('managers', RRQLExpression('O identity U, U in_group G, G name "users"')),
                    'delete': ('managers', RRQLExpression('O identity U, U in_group G, G name "users"')),
--- a/schemas/base.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/schemas/base.py	Fri Nov 20 19:35:54 2009 +0100
@@ -16,7 +16,7 @@
 
 class CWUser(WorkflowableEntityType):
     """define a CubicWeb user"""
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', ERQLExpression('X identity U')),
         'add':    ('managers',),
         'delete': ('managers',),
@@ -37,12 +37,12 @@
 
     in_group = SubjectRelation('CWGroup', cardinality='+*',
                                constraints=[RQLConstraint('NOT O name "owners"')],
-                               description=_('groups grant permissions to the user'))
+                               description=_('groups grant __permissions__ to the user'))
 
 
 class EmailAddress(EntityType):
     """an electronic mail address associated to a short alias"""
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests',), # XXX if P use_email X, U has_read_permission P
         'add':    ('managers', 'users',),
         'delete': ('managers', 'owners', ERQLExpression('P use_email X, U has_update_permission P')),
@@ -59,7 +59,7 @@
 
 class use_email(RelationType):
     """ """
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests',),
         'add':    ('managers', RRQLExpression('U has_update_permission S'),),
         'delete': ('managers', RRQLExpression('U has_update_permission S'),),
@@ -68,12 +68,12 @@
 
 class primary_email(RelationType):
     """the prefered email"""
-    permissions = use_email.permissions
+    __permissions__ = use_email.__permissions__
 
 class prefered_form(RelationType):
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests',),
-        # XXX should have update permissions on both subject and object,
+        # XXX should have update __permissions__ on both subject and object,
         #     though by doing this we will probably have no way to add
         #     this relation in the web ui. The easiest way to acheive this
         #     is probably to be able to have "U has_update_permission O" as
@@ -85,13 +85,13 @@
 
 class in_group(RelationType):
     """core relation indicating a user's groups"""
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
 
 class owned_by(RelationType):
     """core relation indicating owners of an entity. This relation
     implicitly put the owner into the owners group for the entity
     """
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests'),
         'add':    ('managers', RRQLExpression('S owned_by U'),),
         'delete': ('managers', RRQLExpression('S owned_by U'),),
@@ -104,7 +104,7 @@
 
 class created_by(RelationType):
     """core relation indicating the original creator of an entity"""
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests'),
         'add':    ('managers',),
         'delete': ('managers',),
@@ -134,12 +134,37 @@
     subject = '*'
     object = 'String'
 
+
+class CWProperty(EntityType):
+    """used for cubicweb configuration. Once a property has been created you
+    can't change the key.
+    """
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests'),
+        'add':    ('managers', 'users',),
+        'update': ('managers', 'owners',),
+        'delete': ('managers', 'owners',),
+        }
+    # key is a reserved word for mysql
+    pkey = String(required=True, internationalizable=True, maxsize=256,
+                  description=_('defines what\'s the property is applied for. '
+                                'You must select this first to be able to set '
+                                'value'))
+    value = String(internationalizable=True, maxsize=256)
+
+    for_user = SubjectRelation('CWUser', cardinality='?*', composite='object',
+                               description=_('user for which this property is '
+                                             'applying. If this relation is not '
+                                             'set, the property is considered as'
+                                             ' a global property'))
+
+
 # XXX find a better relation name
 class for_user(RelationType):
     """link a property to the user which want this property customization. Unless
     you're a site manager, this relation will be handled automatically.
     """
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests'),
         'add':    ('managers',),
         'delete': ('managers',),
@@ -150,10 +175,11 @@
     composite = 'object'
     cardinality = '?*'
 
+
 class CWPermission(EntityType):
     """entity type that may be used to construct some advanced security configuration
     """
-    permissions = META_ETYPE_PERMS
+    __permissions__ = META_ETYPE_PERMS
 
     name = String(required=True, indexed=True, internationalizable=True, maxsize=100,
                   description=_('name or identifier of the permission'))
@@ -168,7 +194,7 @@
     """link a permission to the entity. This permission should be used in the
     security definition of the entity's type to be useful.
     """
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests'),
         'add':    ('managers',),
         'delete': ('managers',),
@@ -176,7 +202,7 @@
 
 class require_group(RelationType):
     """used to grant a permission to a group"""
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests'),
         'add':    ('managers',),
         'delete': ('managers',),
@@ -196,7 +222,7 @@
     NOTE: You'll have to explicitly declare which entity types can have a
     same_as relation
     """
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests',),
         'add':    ('managers', 'users'),
         'delete': ('managers', 'owners'),
@@ -216,7 +242,7 @@
 
     Also, checkout the AppObject.get_cache() method.
     """
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests'),
         'add':    ('managers',),
         'update': ('managers', 'users',), # XXX
@@ -233,9 +259,9 @@
 class identical_to(RelationType):
     """identical to"""
     symetric = True
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests',),
-        # XXX should have update permissions on both subject and object,
+        # XXX should have update __permissions__ on both subject and object,
         #     though by doing this we will probably have no way to add
         #     this relation in the web ui. The easiest way to acheive this
         #     is probably to be able to have "U has_update_permission O" as
@@ -248,7 +274,7 @@
 class see_also(RelationType):
     """generic relation to link one entity to another"""
     symetric = True
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests',),
         'add':    ('managers', RRQLExpression('U has_update_permission S'),),
         'delete': ('managers', RRQLExpression('U has_update_permission S'),),
--- a/schemas/bootstrap.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/schemas/bootstrap.py	Fri Nov 20 19:35:54 2009 +0100
@@ -17,7 +17,7 @@
 # access to this
 class CWEType(EntityType):
     """define an entity type, used to build the instance schema"""
-    permissions = META_ETYPE_PERMS
+    __permissions__ = META_ETYPE_PERMS
     name = String(required=True, indexed=True, internationalizable=True,
                   unique=True, maxsize=64)
     description = RichString(internationalizable=True,
@@ -28,7 +28,7 @@
 
 class CWRType(EntityType):
     """define a relation type, used to build the instance schema"""
-    permissions = META_ETYPE_PERMS
+    __permissions__ = META_ETYPE_PERMS
     name = String(required=True, indexed=True, internationalizable=True,
                   unique=True, maxsize=64)
     description = RichString(internationalizable=True,
@@ -48,7 +48,7 @@
 
     used to build the instance schema
     """
-    permissions = META_ETYPE_PERMS
+    __permissions__ = META_ETYPE_PERMS
     relation_type = SubjectRelation('CWRType', cardinality='1*',
                                     constraints=[RQLConstraint('O final TRUE')],
                                     composite='object')
@@ -85,7 +85,7 @@
 
     used to build the instance schema
     """
-    permissions = META_ETYPE_PERMS
+    __permissions__ = META_ETYPE_PERMS
     relation_type = SubjectRelation('CWRType', cardinality='1*',
                                     constraints=[RQLConstraint('O final FALSE')],
                                     composite='object')
@@ -115,8 +115,8 @@
 
 # not restricted since it has to be read when checking allowed transitions
 class RQLExpression(EntityType):
-    """define a rql expression used to define permissions"""
-    permissions = META_ETYPE_PERMS
+    """define a rql expression used to define __permissions__"""
+    __permissions__ = META_ETYPE_PERMS
     exprtype = String(required=True, vocabulary=['ERQLExpression', 'RRQLExpression'])
     mainvars = String(maxsize=8,
                       description=_('name of the main variables which should be '
@@ -131,11 +131,11 @@
                                       'relation\'subject, object and to '
                                       'the request user. '))
 
-    read_permission = ObjectRelation(('CWEType', 'CWRType'), cardinality='+?', composite='subject',
+    read_permission = ObjectRelation(('CWEType', 'CWAttribute', 'CWRelation'), cardinality='+?', composite='subject',
                                       description=_('rql expression allowing to read entities/relations of this type'))
-    add_permission = ObjectRelation(('CWEType', 'CWRType'), cardinality='*?', composite='subject',
+    add_permission = ObjectRelation(('CWEType', 'CWAttribute', 'CWRelation'), cardinality='*?', composite='subject',
                                      description=_('rql expression allowing to add entities/relations of this type'))
-    delete_permission = ObjectRelation(('CWEType', 'CWRType'), cardinality='*?', composite='subject',
+    delete_permission = ObjectRelation(('CWEType', 'CWAttribute', 'CWRelation'), cardinality='*?', composite='subject',
                                         description=_('rql expression allowing to delete entities/relations of this type'))
     update_permission = ObjectRelation('CWEType', cardinality='*?', composite='subject',
                                         description=_('rql expression allowing to update entities of this type'))
@@ -143,14 +143,14 @@
 
 class CWConstraint(EntityType):
     """define a schema constraint"""
-    permissions = META_ETYPE_PERMS
+    __permissions__ = META_ETYPE_PERMS
     cstrtype = SubjectRelation('CWConstraintType', cardinality='1*')
     value = String(description=_('depends on the constraint type'))
 
 
 class CWConstraintType(EntityType):
     """define a schema constraint type"""
-    permissions = META_ETYPE_PERMS
+    __permissions__ = META_ETYPE_PERMS
     name = String(required=True, indexed=True, internationalizable=True,
                   unique=True, maxsize=64)
 
@@ -158,15 +158,15 @@
 # not restricted since it has to be read when checking allowed transitions
 class CWGroup(EntityType):
     """define a CubicWeb users group"""
-    permissions = META_ETYPE_PERMS
+    __permissions__ = META_ETYPE_PERMS
     name = String(required=True, indexed=True, internationalizable=True,
                   unique=True, maxsize=64)
 
-    read_permission = ObjectRelation(('CWEType', 'CWRType'), cardinality='+*',
+    read_permission = ObjectRelation(('CWEType', 'CWAttribute', 'CWRelation'), cardinality='+*',
                                       description=_('groups allowed to read entities/relations of this type'))
-    add_permission = ObjectRelation(('CWEType', 'CWRType'),
+    add_permission = ObjectRelation(('CWEType', 'CWAttribute', 'CWRelation'),
                                      description=_('groups allowed to add entities/relations of this type'))
-    delete_permission = ObjectRelation(('CWEType', 'CWRType'),
+    delete_permission = ObjectRelation(('CWEType', 'CWAttribute', 'CWRelation'),
                                         description=_('groups allowed to delete entities/relations of this type'))
     update_permission = ObjectRelation('CWEType',
                                         description=_('groups allowed to update entities of this type'))
@@ -192,50 +192,50 @@
 
 class relation_type(RelationType):
     """link a relation definition to its relation type"""
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
     inlined = True
 
 class from_entity(RelationType):
     """link a relation definition to its subject entity type"""
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
     inlined = True
 
 class to_entity(RelationType):
     """link a relation definition to its object entity type"""
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
     inlined = True
 
 class constrained_by(RelationType):
     """constraints applying on this relation"""
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
 
 class cstrtype(RelationType):
     """constraint factory"""
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
     inlined = True
 
 class read_permission(RelationType):
     """core relation giving to a group the permission to read an entity or
     relation type
     """
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
 
 class add_permission(RelationType):
     """core relation giving to a group the permission to add an entity or
     relation type
     """
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
 
 class delete_permission(RelationType):
     """core relation giving to a group the permission to delete an entity or
     relation type
     """
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
 
 class update_permission(RelationType):
     """core relation giving to a group the permission to update an entity type
     """
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
 
 
 class is_(RelationType):
@@ -244,7 +244,7 @@
     name = 'is'
     # don't explicitly set composite here, this is handled anyway
     #composite = 'object'
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests'),
         'add':    (),
         'delete': (),
@@ -259,7 +259,7 @@
     """
     # don't explicitly set composite here, this is handled anyway
     #composite = 'object'
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests'),
         'add':    (),
         'delete': (),
@@ -270,7 +270,7 @@
 
 class specializes(RelationType):
     name = 'specializes'
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests'),
         'add':    ('managers',),
         'delete': ('managers',),
--- a/schemas/workflow.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/schemas/workflow.py	Fri Nov 20 19:35:54 2009 +0100
@@ -15,7 +15,7 @@
                               HOOKS_RTYPE_PERMS)
 
 class Workflow(EntityType):
-    permissions = META_ETYPE_PERMS
+    __permissions__ = META_ETYPE_PERMS
 
     name = String(required=True, indexed=True, internationalizable=True,
                   maxsize=256)
@@ -33,7 +33,7 @@
 
 class default_workflow(RelationType):
     """default workflow for an entity type"""
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
 
     subject = 'CWEType'
     object = 'Workflow'
@@ -45,7 +45,7 @@
     """used to associate simple states to an entity type and/or to define
     workflows
     """
-    permissions = META_ETYPE_PERMS
+    __permissions__ = META_ETYPE_PERMS
 
     name = String(required=True, indexed=True, internationalizable=True,
                   maxsize=256)
@@ -64,7 +64,7 @@
 
 class BaseTransition(EntityType):
     """abstract base class for transitions"""
-    permissions = META_ETYPE_PERMS
+    __permissions__ = META_ETYPE_PERMS
 
     name = String(required=True, indexed=True, internationalizable=True,
                   maxsize=256)
@@ -126,7 +126,7 @@
 class TrInfo(EntityType):
     """workflow history item"""
     # 'add' security actually done by hooks
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests',), # XXX U has_read_permission O ?
         'add':    ('managers', 'users', 'guests',),
         'delete': (), # XXX should we allow managers to delete TrInfo?
@@ -142,11 +142,11 @@
     # get actor and date time using owned_by and creation_date
 
 class from_state(RelationType):
-    permissions = HOOKS_RTYPE_PERMS.copy()
+    __permissions__ = HOOKS_RTYPE_PERMS.copy()
     inlined = True
 
 class to_state(RelationType):
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests',),
         'add':    ('managers',),
         'delete': (),
@@ -155,7 +155,7 @@
 
 class by_transition(RelationType):
     # 'add' security actually done by hooks
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests',),
         'add':    ('managers', 'users', 'guests',),
         'delete': (),
@@ -164,11 +164,11 @@
 
 class workflow_of(RelationType):
     """link a workflow to one or more entity type"""
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
 
 class state_of(RelationType):
     """link a state to one or more workflow"""
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
     inlined = True
 
 class transition_of(RelationType):
@@ -178,40 +178,40 @@
 
 class subworkflow(RelationType):
     """link a transition to one or more workflow"""
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
     inlined = True
 
 class exit_point(RelationType):
     """link a transition to one or more workflow"""
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
 
 class subworkflow_state(RelationType):
     """link a transition to one or more workflow"""
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
     inlined = True
 
 class initial_state(RelationType):
     """indicate which state should be used by default when an entity using
     states is created
     """
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
     inlined = True
 
 class destination_state(RelationType):
     """destination state of a transition"""
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
     inlined = True
 
 class allowed_transition(RelationType):
     """allowed transition from this state"""
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
 
 
 # "abstract" relations, set by WorkflowableEntityType ##########################
 
 class custom_workflow(RelationType):
     """allow to set a specific workflow for an entity"""
-    permissions = META_RTYPE_PERMS
+    __permissions__ = META_RTYPE_PERMS
 
     cardinality = '?*'
     constraints = [RQLConstraint('S is ET, O workflow_of ET')]
@@ -221,7 +221,7 @@
 class wf_info_for(RelationType):
     """link a transition information to its object"""
     # 'add' security actually done by hooks
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests',),
         'add':    ('managers', 'users', 'guests',),
         'delete': (),
@@ -236,7 +236,7 @@
 
 class in_state(RelationType):
     """indicate the current state of an entity"""
-    permissions = HOOKS_RTYPE_PERMS
+    __permissions__ = HOOKS_RTYPE_PERMS
 
     # not inlined intentionnaly since when using ldap sources, user'state
     # has to be stored outside the CWUser table
--- a/schemaviewer.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/schemaviewer.py	Fri Nov 20 19:35:54 2009 +0100
@@ -13,6 +13,7 @@
 
 I18NSTRINGS = [_('read'), _('add'), _('delete'), _('update'), _('order')]
 
+
 class SchemaViewer(object):
     """return an ureport layout for some part of a schema"""
     def __init__(self, req=None, encoding=None):
@@ -68,7 +69,8 @@
         _ = self.req._
         data = [_('attribute'), _('type'), _('default'), _('constraints')]
         for rschema, aschema in eschema.attribute_definitions():
-            if not (rschema.has_local_role('read') or rschema.has_perm(self.req, 'read')):
+            rdef = eschema.rdef(rschema)
+            if not rdef.may_have_permission('read', self.req):
                 continue
             aname = rschema.type
             if aname == 'eid':
@@ -78,7 +80,7 @@
             defaultval = eschema.default(aname)
             if defaultval is not None:
                 default = self.to_string(defaultval)
-            elif eschema.rproperty(rschema, 'cardinality')[0] == '1':
+            elif rdef.cardinality[0] == '1':
                 default = _('required field')
             else:
                 default = ''
@@ -119,20 +121,23 @@
         t_vars = []
         rels = []
         first = True
-        for rschema, targetschemas, x in eschema.relation_definitions():
+        for rschema, targetschemas, role in eschema.relation_definitions():
             if rschema.type in skiptypes:
                 continue
-            if not (rschema.has_local_role('read') or rschema.has_perm(self.req, 'read')):
-                continue
             rschemaurl = self.rschema_link_url(rschema)
             for oeschema in targetschemas:
+                rdef = rschema.role_rdef(eschema, oeschema, role)
+                if not rdef.may_have_permission('read', self.req):
+                    continue
                 label = rschema.type
-                if x == 'subject':
+                if role == 'subject':
                     cards = rschema.rproperty(eschema, oeschema, 'cardinality')
                 else:
                     cards = rschema.rproperty(oeschema, eschema, 'cardinality')
                     cards = cards[::-1]
-                label = '%s %s (%s) %s' % (CARD_MAP[cards[1]], label, display_name(self.req, label, x), CARD_MAP[cards[0]])
+                label = '%s %s (%s) %s' % (CARD_MAP[cards[1]], label,
+                                           display_name(self.req, label, role),
+                                           CARD_MAP[cards[0]])
                 rlink = Link(rschemaurl, label)
                 elink = Link(self.eschema_link_url(oeschema), oeschema.type)
                 if first:
--- a/selectors.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/selectors.py	Fri Nov 20 19:35:54 2009 +0100
@@ -667,19 +667,6 @@
         self.target_etype = target_etype
         self.action = action
 
-    @lltrace
-    def __call__(self, cls, req, *args, **kwargs):
-        rschema = req.vreg.schema.rschema(self.rtype)
-        if not (rschema.has_perm(req, self.action)
-                or rschema.has_local_role(self.action)):
-            return 0
-        if self.action != 'read':
-            if not (rschema.has_perm(req, 'read')
-                    or rschema.has_local_role('read')):
-                return 0
-        score = super(relation_possible, self).__call__(cls, req, *args, **kwargs)
-        return score
-
     def score_class(self, eclass, req):
         eschema = eclass.e_schema
         try:
@@ -691,12 +678,13 @@
             return 0
         if self.target_etype is not None:
             try:
-                if self.role == 'subject':
-                    return int(self.target_etype in rschema.objects(eschema))
-                else:
-                    return int(self.target_etype in rschema.subjects(eschema))
+                rdef = rschema.role_rdef(eschema, self.target_etype, self.role)
+                if not rdef.may_have_permission(self.action, req):
+                    return 0
             except KeyError:
                 return 0
+        else:
+            return rschema.may_have_permission(self.action, req, eschema, self.role)
         return 1
 
 
--- a/server/__init__.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/server/__init__.py	Fri Nov 20 19:35:54 2009 +0100
@@ -29,7 +29,7 @@
 DBG_SQL = 2   # executed sql
 DBG_REPO = 4  # repository events
 DBG_MS = 8    # multi-sources
-DBG_MORE = 16 # repository events
+DBG_MORE = 16 # more verbosity
 
 # current debug mode
 DEBUG = 0
--- a/server/hookhelper.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/server/hookhelper.py	Fri Nov 20 19:35:54 2009 +0100
@@ -11,6 +11,17 @@
 
 from cubicweb import RepositoryError
 
+def entity_oldnewvalue(entity, attr):
+    """returns the couple (old attr value, new attr value)
+    NOTE: will only work in a before_update_entity hook
+    """
+    # get new value and remove from local dict to force a db query to
+    # fetch old value
+    newvalue = entity.pop(attr, None)
+    oldvalue = getattr(entity, attr)
+    if newvalue is not None:
+        entity[attr] = newvalue
+    return oldvalue, newvalue
 
 @deprecated('[3.6] entity_name is deprecated, use entity.name')
 def entity_name(session, eid):
--- a/server/migractions.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/server/migractions.py	Fri Nov 20 19:35:54 2009 +0100
@@ -104,9 +104,14 @@
             if migrscript.endswith('.sql'):
                 if self.execscript_confirm(migrscript):
                     sqlexec(open(migrscript).read(), self.session.system_sql)
-            else:
+            elif migrscript.endswith('.py'):
                 return super(ServerMigrationHelper, self).cmd_process_script(
                     migrscript, funcname, *args, **kwargs)
+            else:
+                print
+                print ('-> ignoring %s, only .py and .sql scripts are considered' %
+                       migrscript)
+                print
             self.commit()
         except:
             self.rollback()
@@ -305,35 +310,33 @@
 
     # schema synchronization internals ########################################
 
-    def _synchronize_permissions(self, ertype):
+    def _synchronize_permissions(self, erschema, teid):
         """permission synchronization for an entity or relation type"""
-        if ertype in VIRTUAL_RTYPES:
+        if erschema in VIRTUAL_RTYPES:
             return
-        newrschema = self.fs_schema[ertype]
-        teid = self.repo.schema[ertype].eid
-        if 'update' in newrschema.ACTIONS or newrschema.final:
+        assert teid, erschema
+        if 'update' in erschema.ACTIONS or erschema.final:
             # entity type
             exprtype = u'ERQLExpression'
         else:
             # relation type
             exprtype = u'RRQLExpression'
-        assert teid, ertype
         gm = self.group_mapping()
         confirm = self.verbosity >= 2
         # * remove possibly deprecated permission (eg in the persistent schema
         #   but not in the new schema)
         # * synchronize existing expressions
         # * add new groups/expressions
-        for action in newrschema.ACTIONS:
+        for action in erschema.ACTIONS:
             perm = '%s_permission' % action
             # handle groups
-            newgroups = list(newrschema.get_groups(action))
+            newgroups = list(erschema.get_groups(action))
             for geid, gname in self.rqlexec('Any G, GN WHERE T %s G, G name GN, '
                                             'T eid %%(x)s' % perm, {'x': teid}, 'x',
                                             ask_confirm=False):
                 if not gname in newgroups:
                     if not confirm or self.confirm('remove %s permission of %s to %s?'
-                                                   % (action, ertype, gname)):
+                                                   % (action, erschema, gname)):
                         self.rqlexec('DELETE T %s G WHERE G eid %%(x)s, T eid %s'
                                      % (perm, teid),
                                      {'x': geid}, 'x', ask_confirm=False)
@@ -341,18 +344,18 @@
                     newgroups.remove(gname)
             for gname in newgroups:
                 if not confirm or self.confirm('grant %s permission of %s to %s?'
-                                               % (action, ertype, gname)):
+                                               % (action, erschema, gname)):
                     self.rqlexec('SET T %s G WHERE G eid %%(x)s, T eid %s'
                                  % (perm, teid),
                                  {'x': gm[gname]}, 'x', ask_confirm=False)
             # handle rql expressions
-            newexprs = dict((expr.expression, expr) for expr in newrschema.get_rqlexprs(action))
+            newexprs = dict((expr.expression, expr) for expr in erschema.get_rqlexprs(action))
             for expreid, expression in self.rqlexec('Any E, EX WHERE T %s E, E expression EX, '
                                                     'T eid %s' % (perm, teid),
                                                     ask_confirm=False):
                 if not expression in newexprs:
                     if not confirm or self.confirm('remove %s expression for %s permission of %s?'
-                                                   % (expression, action, ertype)):
+                                                   % (expression, action, erschema)):
                         # deleting the relation will delete the expression entity
                         self.rqlexec('DELETE T %s E WHERE E eid %%(x)s, T eid %s'
                                      % (perm, teid),
@@ -362,7 +365,7 @@
             for expression in newexprs.values():
                 expr = expression.expression
                 if not confirm or self.confirm('add %s expression for %s permission of %s?'
-                                               % (expr, action, ertype)):
+                                               % (expr, action, erschema)):
                     self.rqlexec('INSERT RQLExpression X: X exprtype %%(exprtype)s, '
                                  'X expression %%(expr)s, X mainvars %%(vars)s, T %s X '
                                  'WHERE T eid %%(x)s' % perm,
@@ -394,9 +397,7 @@
             for subj, obj in rschema.iter_rdefs():
                 if not reporschema.has_rdef(subj, obj):
                     continue
-                self._synchronize_rdef_schema(subj, rschema, obj)
-        if syncperms:
-            self._synchronize_permissions(rtype)
+                self._synchronize_rdef_schema(subj, rschema, obj, syncperms=syncperms)
 
     def _synchronize_eschema(self, etype, syncperms=True):
         """synchronize properties of the persistent entity schema against
@@ -446,9 +447,9 @@
                         continue
                     self._synchronize_rdef_schema(subj, rschema, obj)
         if syncperms:
-            self._synchronize_permissions(etype)
+            self._synchronize_permissions(eschema, repoeschema.eid)
 
-    def _synchronize_rdef_schema(self, subjtype, rtype, objtype):
+    def _synchronize_rdef_schema(self, subjtype, rtype, objtype, syncperms=True):
         """synchronize properties of the persistent relation definition schema
         against its current definition:
         * order and other properties
@@ -467,12 +468,14 @@
         self.rqlexecall(ss.updaterdef2rql(rschema, subjtype, objtype),
                         ask_confirm=confirm)
         # constraints
-        newconstraints = list(rschema.rproperty(subjtype, objtype, 'constraints'))
+        rdef = rschema.rdef(subjtype, objtype)
+        repordef = reporschema.rdef(subjtype, objtype)
+        newconstraints = list(rdef.constraints)
         # 1. remove old constraints and update constraints of the same type
         # NOTE: don't use rschema.constraint_by_type because it may be
         #       out of sync with newconstraints when multiple
         #       constraints of the same type are used
-        for cstr in reporschema.rproperty(subjtype, objtype, 'constraints'):
+        for cstr in repordef.constraints:
             for newcstr in newconstraints:
                 if newcstr.type() == cstr.type():
                     break
@@ -496,6 +499,8 @@
             self.rqlexecall(ss.constraint2rql(rschema, subjtype, objtype,
                                               newcstr),
                             ask_confirm=confirm)
+        if syncperms:
+            self._synchronize_permissions(rdef, repordef.eid)
 
     # base actions ############################################################
 
@@ -526,10 +531,17 @@
                 sourcescfg[cube] = ask_source_config(cube)
                 self.config.write_sources_file(sourcescfg)
                 clear_cache(self.config, 'read_sources_file')
+            # ensure added cube is in config cubes
+            # XXX worth restoring on error?
+            if not cube in self.config._cubes:
+                self.config._cubes += (cube,)
         if not update_database:
             self.commit()
             return
         newcubes_schema = self.config.load_schema(construction_mode='non-strict')
+        # XXX we have to replace fs_schema, used in cmd_add_relation_type
+        # etc. and fsschema of migration script contexts
+        self.fs_schema = self._create_context()['fsschema'] = newcubes_schema
         new = set()
         # execute pre-create files
         for pack in reversed(newcubes):
@@ -881,7 +893,8 @@
             if isinstance(ertype, (tuple, list)):
                 assert len(ertype) == 3, 'not a relation definition'
                 assert syncprops, 'can\'t update permission for a relation definition'
-                self._synchronize_rdef_schema(*ertype)
+                self._synchronize_rdef_schema(ertype[0], ertype[1], ertype[2],
+                                              syncperms=syncperms)
             elif syncprops:
                 erschema = self.repo.schema[ertype]
                 if isinstance(erschema, CubicWebRelationSchema):
@@ -890,13 +903,14 @@
                 else:
                     self._synchronize_eschema(erschema, syncperms=syncperms)
             else:
-                self._synchronize_permissions(ertype)
+                erschema = self.repo.schema[ertype]
+                self._synchronize_permissions(self.fs_schema[ertype], erschema.eid)
         else:
             for etype in self.repo.schema.entities():
                 if syncprops:
                     self._synchronize_eschema(etype, syncperms=syncperms)
                 else:
-                    self._synchronize_permissions(etype)
+                    self._synchronize_permissions(self.fs_schema[etype], erschema.eid)
         if commit:
             self.commit()
 
--- a/server/msplanner.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/server/msplanner.py	Fri Nov 20 19:35:54 2009 +0100
@@ -1470,11 +1470,19 @@
     def visit_constant(self, node, newroot, terms):
         return copy_node(newroot, node), node
 
+    def visit_comparison(self, node, newroot, terms):
+        subparts, node = self._visit_children(node, newroot, terms)
+        copy = copy_node(newroot, node, subparts)
+        # ignore comparison operator when fetching non final query
+        if not self.final and isinstance(node.children[0], VariableRef):
+            copy.operator = '='
+        return copy, node
+
     def visit_default(self, node, newroot, terms):
         subparts, node = self._visit_children(node, newroot, terms)
         return copy_node(newroot, node, subparts), node
 
-    visit_comparison = visit_mathexpression = visit_constant = visit_function = visit_default
+    visit_mathexpression = visit_constant = visit_function = visit_default
     visit_sort = visit_sortterm = visit_default
 
     def _visit_children(self, node, newroot, terms):
--- a/server/querier.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/server/querier.py	Fri Nov 20 19:35:54 2009 +0100
@@ -71,14 +71,23 @@
             # XXX has_text may have specific perm ?
             if rel.r_type in READ_ONLY_RTYPES:
                 continue
-            if not schema.rschema(rel.r_type).has_access(user, 'read'):
+            rschema = schema.rschema(rel.r_type)
+            if rschema.final:
+                eschema = schema.eschema(solution[rel.children[0].name])
+                rdef = eschema.rdef(rschema)
+            else:
+                rdef = rschema.rdef(solution[rel.children[0].name],
+                                    solution[rel.children[1].children[0].name])
+            if not user.matching_groups(rdef.get_groups('read')):
                 raise Unauthorized('read', rel.r_type)
     localchecks = {}
     # iterate on defined_vars and not on solutions to ignore column aliases
     for varname in rqlst.defined_vars:
         etype = solution[varname]
         eschema = schema.eschema(etype)
-        if not eschema.has_access(user, 'read'):
+        if eschema.final:
+            continue
+        if not user.matching_groups(eschema.get_groups('read')):
             erqlexprs = eschema.get_rqlexprs('read')
             if not erqlexprs:
                 ex = Unauthorized('read', etype)
--- a/server/repository.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/server/repository.py	Fri Nov 20 19:35:54 2009 +0100
@@ -1049,7 +1049,7 @@
                 continue
             rschema = eschema.subjrels[attr]
             if rschema.final:
-                if eschema.rproperty(attr, 'fulltextindexed'):
+                if getattr(eschema.rdef(attr), 'fulltextindexed', False):
                     need_fti_update = True
                 only_inline_rels = False
             else:
--- a/server/rqlannotation.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/server/rqlannotation.py	Fri Nov 20 19:35:54 2009 +0100
@@ -10,6 +10,7 @@
 
 from logilab.common.compat import any
 
+from rql import BadRQLQuery
 from rql.nodes import Relation, VariableRef, Constant, Variable, Or
 from rql.utils import common_parent
 
@@ -177,10 +178,20 @@
     """given a list of rqlst relations, select one which will be used as main
     relation for the rhs variable
     """
-    for rel in relations:
+    principal = None
+    # sort for test predictability
+    for rel in sorted(relations, key=lambda x: (x.children[0].name, x.r_type)):
+        # only equality relation with a variable as rhs may be principal
+        if rel.operator() not in ('=', 'IS') \
+               or not isinstance(rel.children[1].children[0], VariableRef):
+            continue
         if rel.sqlscope is rel.stmt:
             return rel
         principal = rel
+    if principal is None:
+        print iter(relations).next().root
+        raise BadRQLQuery('unable to find principal in %s' % ', '.join(
+            r.as_string() for r in relations))
     return principal
 
 
--- a/server/schemaserial.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/server/schemaserial.py	Fri Nov 20 19:35:54 2009 +0100
@@ -7,6 +7,7 @@
 """
 __docformat__ = "restructuredtext en"
 
+import os
 import sys
 import os
 from itertools import chain
@@ -256,7 +257,7 @@
                 actperms.append(erschema.rql_expression(*something))
             else: # group name
                 actperms.append(something)
-        erschema.set_permissions(action, actperms)
+        erschema.set_action_permissions(action, actperms)
 
 
 def deserialize_rdef_constraints(session):
@@ -516,25 +517,51 @@
         yield erperms2rql(schema[rtype], groupmapping)
 
 def erperms2rql(erschema, groupmapping):
+    if hasattr(erschema, 'iter_rdefs'):
+        # relation schema
+        if erschema.final:
+            etype = 'CWAttribute'
+        else:
+            etype = 'CWRelation'
+        for subject, object in erschema.iter_rdefs():
+            permissions = erschema.rproperty(subject, object, 'permissions')
+            for rql, args in _erperms2rql(erschema.rproperties(subject, object),
+                                          groupmapping):
+                args['st'] = str(subject)
+                args['rt'] = str(erschema)
+                args['ot'] = str(object)
+                yield rql + 'X is %s, X from_entity ST, X to_entity OT, '\
+                      'X relation_type RT, RT name %%(rt)s, ST name %%(st)s, '\
+                      'OT name %%(ot)s' % etype, args
+    else:
+        # entity schema
+        for rql, args in _erperms2rql(erschema, groupmapping):
+            args['name'] = str(eschema)
+            yield rql + 'X is CWEType, X name %(name)s', args
+
+def _erperms2rql(erschema, groupmapping):
     """return rql insert statements to enter the entity or relation
     schema's permissions in the database as
     [read|add|delete|update]_permission relations between CWEType/CWRType
     and CWGroup entities
     """
-    etype = isinstance(erschema, schemamod.EntitySchema) and 'CWEType' or 'CWRType'
     for action in erschema.ACTIONS:
-        for group in sorted(erschema.get_groups(action)):
-            try:
-                yield ('SET X %s_permission Y WHERE X is %s, X name "%s", Y eid %s'
-                       % (action, etype, erschema, groupmapping[group]), None)
-            except KeyError:
-                continue
-        for rqlexpr in sorted(erschema.get_rqlexprs(action)):
-            yield ('INSERT RQLExpression E: E expression %%(e)s, E exprtype %%(t)s, '
-                   'E mainvars %%(v)s, X %s_permission E '
-                   'WHERE X is %s, X name "%s"' % (action, etype, erschema),
-                   {'e': unicode(rqlexpr.expression), 'v': unicode(rqlexpr.mainvars),
-                    't': unicode(rqlexpr.__class__.__name__)})
+        for group_or_rqlexpr in erschema.action_permissions(action):
+            if isinstance(group_or_rqlexpr, basestring):
+                # group
+                try:
+                    yield ('SET X %s_permission Y WHERE Y eid %%(g)s' % action,
+                           {'g': groupmapping[group_or_rqlexpr]})
+                except KeyError:
+                    continue
+            else:
+                # rqlexpr
+                rqlexpr = group_or_rqlexpr
+                yield ('INSERT RQLExpression E: E expression %%(e)s, E exprtype %%(t)s, '
+                       'E mainvars %%(v)s, X %s_permission E WHERE ' % action,
+                       {'e': unicode(rqlexpr.expression),
+                        'v': unicode(rqlexpr.mainvars),
+                        't': unicode(rqlexpr.__class__.__name__)})
 
 
 def updateeschema2rql(eschema):
--- a/server/serverconfig.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/server/serverconfig.py	Fri Nov 20 19:35:54 2009 +0100
@@ -10,7 +10,7 @@
 import os
 from os.path import join, exists
 
-from logilab.common.configuration import REQUIRED, Method, Configuration, \
+from logilab.common.configuration import Method, Configuration, \
      ini_format_section
 from logilab.common.decorators import wproperty, cached, clear_cache
 
@@ -22,7 +22,7 @@
 
 USER_OPTIONS =  (
     ('login', {'type' : 'string',
-               'default': REQUIRED,
+               'default': 'admin',
                'help': "cubicweb manager account's login "
                '(this user will be created)',
                'inputlevel': 0,
--- a/server/serverctl.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/server/serverctl.py	Fri Nov 20 19:35:54 2009 +0100
@@ -133,6 +133,9 @@
             config.input_config('pyro', inputlevel)
         print '\n'+underline_title('Configuring the sources')
         sourcesfile = config.sources_file()
+        # XXX hack to make Method('default_instance_id') usable in db option
+        # defs (in native.py)
+        Configuration.default_instance_id = staticmethod(lambda: config.appid)
         sconfig = Configuration(options=SOURCE_TYPES['native'].options)
         sconfig.adapter = 'native'
         sconfig.input_config(inputlevel=inputlevel)
@@ -465,6 +468,12 @@
         from cubicweb.server.server import RepositoryServer
         appid = pop_arg(args, msg='No instance specified !')
         config = ServerConfiguration.config_for(appid)
+        if sys.platform == 'win32':
+            if not self.config.debug:
+                from logging import getLogger
+                logger = getLogger('cubicweb.ctl')
+                logger.info('Forcing debug mode on win32 platform')
+                self.config.debug = True
         debug = self.config.debug
         # create the server
         server = RepositoryServer(config, debug)
--- a/server/sources/native.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/server/sources/native.py	Fri Nov 20 19:35:54 2009 +0100
@@ -18,12 +18,14 @@
 from base64 import b64decode, b64encode
 
 from logilab.common.cache import Cache
-from logilab.common.configuration import REQUIRED
+from logilab.common.configuration import Method
 from logilab.common.adbh import get_adv_func_helper
+from logilab.common.shellutils import getlogin
 
 from indexer import get_indexer
 
 from cubicweb import UnknownEid, AuthenticationError, Binary, server
+from cubicweb.cwconfig import CubicWebNoAppConfiguration
 from cubicweb.server.utils import crypt_password
 from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn
 from cubicweb.server.rqlannotation import set_qdata
@@ -115,13 +117,13 @@
           }),
         ('db-name',
          {'type' : 'string',
-          'default': REQUIRED,
+          'default': Method('default_instance_id'),
           'help': 'database name',
           'group': 'native-source', 'inputlevel': 0,
           }),
         ('db-user',
          {'type' : 'string',
-          'default': 'cubicweb',
+          'default': CubicWebNoAppConfiguration.mode == 'user' and getlogin() or 'cubicweb',
           'help': 'database user',
           'group': 'native-source', 'inputlevel': 0,
           }),
--- a/server/sources/rql2sql.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/server/sources/rql2sql.py	Fri Nov 20 19:35:54 2009 +0100
@@ -131,37 +131,56 @@
     """cleanup solutions: remove solutions where invariant variables are taking
     different types
     """
-    newsolutions = _new_solutions(rqlst, solutions)
+    newsols = _new_solutions(rqlst, solutions)
     existssols = {}
     unstable = set()
+    invariants = {}
     for vname, var in rqlst.defined_vars.iteritems():
-        vtype = newsolutions[0][vname]
+        vtype = newsols[0][vname]
         if var._q_invariant or vname in varmap:
-            for i in xrange(len(newsolutions)-1, 0, -1):
-                if vtype != newsolutions[i][vname]:
-                    newsolutions.pop(i)
-        elif not var.scope is rqlst:
+            # remove invariant variable from solutions to remove duplicates
+            # later, then reinserting a type for the variable even later
+            for sol in newsols:
+                invariants.setdefault(id(sol), {})[vname] = sol.pop(vname)
+        elif var.scope is not rqlst:
             # move appart variables which are in a EXISTS scope and are variating
             try:
                 thisexistssols, thisexistsvars = existssols[var.scope]
             except KeyError:
-                thisexistssols = [newsolutions[0]]
+                thisexistssols = [newsols[0]]
                 thisexistsvars = set()
                 existssols[var.scope] = thisexistssols, thisexistsvars
-            for i in xrange(len(newsolutions)-1, 0, -1):
-                if vtype != newsolutions[i][vname]:
-                    thisexistssols.append(newsolutions.pop(i))
+            for i in xrange(len(newsols)-1, 0, -1):
+                if vtype != newsols[i][vname]:
+                    thisexistssols.append(newsols.pop(i))
                     thisexistsvars.add(vname)
         else:
             # remember unstable variables
-            for i in xrange(1, len(newsolutions)):
-                if vtype != newsolutions[i][vname]:
+            for i in xrange(1, len(newsols)):
+                if vtype != newsols[i][vname]:
                     unstable.add(vname)
-    if len(newsolutions) > 1:
-        if rewrite_unstable_outer_join(rqlst, newsolutions, unstable, schema):
+    if invariants:
+        # filter out duplicates
+        newsols_ = []
+        for sol in newsols:
+            if not sol in newsols_:
+                newsols_.append(sol)
+        newsols = newsols_
+        # reinsert solutions for invariants
+        for sol in newsols:
+            for invvar, vartype in invariants[id(sol)].iteritems():
+                sol[invvar] = vartype
+        for sol in existssols:
+            try:
+                for invvar, vartype in invariants[id(sol)].iteritems():
+                    sol[invvar] = vartype
+            except KeyError:
+                continue
+    if len(newsols) > 1:
+        if rewrite_unstable_outer_join(rqlst, newsols, unstable, schema):
             # remove variables extracted to subqueries from solutions
-            newsolutions = _new_solutions(rqlst, newsolutions)
-    return newsolutions, existssols, unstable
+            newsols = _new_solutions(rqlst, newsols)
+    return newsols, existssols, unstable
 
 def relation_info(relation):
     lhs, rhs = relation.get_variable_parts()
@@ -252,10 +271,10 @@
             self.actual_tables[-1].append(tsql)
         self.outer_tables = {}
         self.duplicate_switches = []
-        self.attr_vars = {}
         self.aliases = {}
         self.restrictions = []
         self._restr_stack = []
+        self.ignore_varmap = False
 
     def add_restriction(self, restr):
         if restr:
@@ -848,23 +867,23 @@
         nothing to do here.
         """
         contextrels = {}
-        attrvars = self._state.attr_vars
         for var in rhs_vars:
-            try:
-                contextrels[var.name] = attrvars[var.name]
-            except KeyError:
-                attrvars[var.name] = relation
             if var.name in self._varmap:
                 # ensure table is added
                 self._var_info(var.variable)
+            principal = var.variable.stinfo.get('principal')
+            if principal is not None and principal is not relation:
+                contextrels[var.name] = relation
         if not contextrels:
-            relation.children[1].accept(self, contextrels)
             return ''
-        # at least one variable is already in attr_vars, this means we have to
-        # generate unification expression
+        # we have to generate unification expression
         lhssql = self._inlined_var_sql(relation.children[0].variable,
                                        relation.r_type)
-        return '%s%s' % (lhssql, relation.children[1].accept(self, contextrels))
+        try:
+            self._state.ignore_varmap = True
+            return '%s%s' % (lhssql, relation.children[1].accept(self))
+        finally:
+            self._state.ignore_varmap = False
 
     def _visit_attribute_relation(self, rel):
         """generate SQL for an attribute relation"""
@@ -932,8 +951,8 @@
         return self.dbms_helper.fti_restriction_sql(alias, const.eval(self._args),
                                                     jointo, not_) + restriction
 
-    def visit_comparison(self, cmp, contextrels=None):
-        """generate SQL for a comparaison"""
+    def visit_comparison(self, cmp):
+        """generate SQL for a comparison"""
         if len(cmp.children) == 2:
             # XXX occurs ?
             lhs, rhs = cmp.children
@@ -950,16 +969,15 @@
               and rhs.eval(self._args) is None):
             if lhs is None:
                 return ' IS NULL'
-            return '%s IS NULL' % lhs.accept(self, contextrels)
+            return '%s IS NULL' % lhs.accept(self)
         elif isinstance(rhs, Function) and rhs.name == 'IN':
             assert operator == '='
             operator = ' '
         if lhs is None:
-            return '%s%s'% (operator, rhs.accept(self, contextrels))
-        return '%s%s%s'% (lhs.accept(self, contextrels), operator,
-                          rhs.accept(self, contextrels))
+            return '%s%s'% (operator, rhs.accept(self))
+        return '%s%s%s'% (lhs.accept(self), operator, rhs.accept(self))
 
-    def visit_mathexpression(self, mexpr, contextrels=None):
+    def visit_mathexpression(self, mexpr):
         """generate SQL for a mathematic expression"""
         lhs, rhs = mexpr.get_parts()
         # check for string concatenation
@@ -969,17 +987,16 @@
                 operator = '||'
         except CoercionError:
             pass
-        return '(%s %s %s)'% (lhs.accept(self, contextrels), operator,
-                              rhs.accept(self, contextrels))
+        return '(%s %s %s)'% (lhs.accept(self), operator, rhs.accept(self))
 
-    def visit_function(self, func, contextrels=None):
+    def visit_function(self, func):
         """generate SQL name for a function"""
         # function_description will check function is supported by the backend
         sqlname = self.dbms_helper.func_sqlname(func.name)
-        return '%s(%s)' % (sqlname, ', '.join(c.accept(self, contextrels)
+        return '%s(%s)' % (sqlname, ', '.join(c.accept(self)
                                               for c in func.children))
 
-    def visit_constant(self, constant, contextrels=None):
+    def visit_constant(self, constant):
         """generate SQL name for a constant"""
         value = constant.value
         if constant.type is None:
@@ -1004,12 +1021,12 @@
             self._query_attrs[_id] = value
         return '%%(%s)s' % _id
 
-    def visit_variableref(self, variableref, contextrels=None):
+    def visit_variableref(self, variableref):
         """get the sql name for a variable reference"""
         # use accept, .variable may be a variable or a columnalias
-        return variableref.variable.accept(self, contextrels)
+        return variableref.variable.accept(self)
 
-    def visit_columnalias(self, colalias, contextrels=None):
+    def visit_columnalias(self, colalias):
         """get the sql name for a subquery column alias"""
         if colalias.name in self._varmap:
             sql = self._varmap[colalias.name]
@@ -1020,20 +1037,21 @@
             return sql
         return colalias._q_sql
 
-    def visit_variable(self, variable, contextrels=None):
+    def visit_variable(self, variable):
         """get the table name and sql string for a variable"""
-        if contextrels is None and variable.name in self._state.done:
+        #if contextrels is None and variable.name in self._state.done:
+        if variable.name in self._state.done:
             if self._in_wrapping_query:
                 return 'T1.%s' % self._state.aliases[variable.name]
             return variable._q_sql
         self._state.done.add(variable.name)
         vtablename = None
-        if contextrels is None and variable.name in self._varmap:
+        if not self._state.ignore_varmap and variable.name in self._varmap:
             sql, vtablename = self._var_info(variable)
         elif variable.stinfo['attrvar']:
             # attribute variable (systematically used in rhs of final
             # relation(s)), get table name and sql from any rhs relation
-            sql = self._linked_var_sql(variable, contextrels)
+            sql = self._linked_var_sql(variable)
         elif variable._q_invariant:
             # since variable is invariant, we know we won't found final relation
             principal = variable.stinfo['principal']
@@ -1056,7 +1074,7 @@
                                  self.dbms_helper.fti_uid_attr)
             elif principal in variable.stinfo['rhsrelations']:
                 if self.schema.rschema(principal.r_type).inlined:
-                    sql = self._linked_var_sql(variable, contextrels)
+                    sql = self._linked_var_sql(variable)
                 else:
                     sql = '%s.eid_to' % self._relation_table(principal)
             else:
@@ -1076,8 +1094,14 @@
         # generate extra join
         try:
             if not var.stinfo['principal'] is relation:
-                # need a predicable result for tests
-                return '%s=%s' % tuple(sorted((sql, var.accept(self))))
+                op = relation.operator()
+                if op == '=':
+                    # need a predicable result for tests
+                    args = sorted( (sql, var.accept(self)) )
+                    args.insert(1, op)
+                else:
+                    args = (sql, op, var.accept(self))
+                return '%s%s%s' % tuple(args)
         except KeyError:
             # no principal defined, relation is necessarily the principal and
             # so nothing to return here
@@ -1123,14 +1147,13 @@
             #self._state.done.add(var.name)
         return sql
 
-    def _linked_var_sql(self, variable, contextrels=None):
-        if contextrels is None:
+    def _linked_var_sql(self, variable):
+        if not self._state.ignore_varmap:
             try:
                 return self._varmap[variable.name]
             except KeyError:
                 pass
-        rel = (contextrels and contextrels.get(variable.name) or
-               variable.stinfo.get('principal') or
+        rel = (variable.stinfo.get('principal') or
                iter(variable.stinfo['rhsrelations']).next())
         linkedvar = rel.children[0].variable
         if rel.r_type == 'eid':
--- a/server/test/data/migratedapp/schema.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/server/test/data/migratedapp/schema.py	Fri Nov 20 19:35:54 2009 +0100
@@ -13,7 +13,7 @@
                              ERQLExpression, RRQLExpression)
 
 class Affaire(EntityType):
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests'),
         'add':    ('managers', ERQLExpression('X concerne S, S owned_by U')),
         'update': ('managers', 'owners', ERQLExpression('X concerne S, S owned_by U')),
@@ -27,7 +27,7 @@
     concerne = SubjectRelation('Societe')
 
 class concerne(RelationType):
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests'),
         'add':    ('managers', RRQLExpression('U has_update_permission S')),
         'delete': ('managers', RRQLExpression('O owned_by U')),
@@ -42,7 +42,7 @@
 class Note(Para):
     __specializes_schema__ = True
 
-    permissions = {'read':   ('managers', 'users', 'guests',),
+    __permissions__ = {'read':   ('managers', 'users', 'guests',),
                    'update': ('managers', 'owners',),
                    'delete': ('managers', ),
                    'add':    ('managers',
@@ -63,7 +63,7 @@
     summary = String(maxsize=512)
 
 class ecrit_par(RelationType):
-    permissions = {'read':   ('managers', 'users', 'guests',),
+    __permissions__ = {'read':   ('managers', 'users', 'guests',),
                    'delete': ('managers', ),
                    'add':    ('managers',
                               RRQLExpression('O require_permission P, P name "add_note", '
@@ -105,7 +105,7 @@
     connait = SubjectRelation('Personne', symetric=True)
 
 class Societe(WorkflowableEntityType):
-    permissions = {
+    __permissions__ = {
         'read': ('managers', 'users', 'guests'),
         'update': ('managers', 'owners'),
         'delete': ('managers', 'owners'),
--- a/server/test/data/schema.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/server/test/data/schema.py	Fri Nov 20 19:35:54 2009 +0100
@@ -13,7 +13,7 @@
                              ERQLExpression, RRQLExpression)
 
 class Affaire(WorkflowableEntityType):
-    permissions = {
+    __permissions__ = {
         'read':   ('managers',
                    ERQLExpression('X owned_by U'), ERQLExpression('X concerne S?, S owned_by U')),
         'add':    ('managers', ERQLExpression('X concerne S, S owned_by U')),
@@ -39,7 +39,7 @@
 
 
 class Societe(EntityType):
-    permissions = {
+    __permissions__ = {
         'read': ('managers', 'users', 'guests'),
         'update': ('managers', 'owners', ERQLExpression('U login L, X nom L')),
         'delete': ('managers', 'owners', ERQLExpression('U login L, X nom L')),
@@ -121,28 +121,28 @@
     symetric = True
 
 class concerne(RelationType):
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests'),
         'add':    ('managers', RRQLExpression('U has_update_permission S')),
         'delete': ('managers', RRQLExpression('O owned_by U')),
         }
 
 class travaille(RelationType):
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests'),
         'add':    ('managers', RRQLExpression('U has_update_permission S')),
         'delete': ('managers', RRQLExpression('O owned_by U')),
         }
 
 class para(RelationType):
-    permissions = {
+    __permissions__ = {
         'read':   ('managers', 'users', 'guests'),
         'add':    ('managers', ERQLExpression('X in_state S, S name "todo"')),
         'delete': ('managers', ERQLExpression('X in_state S, S name "todo"')),
         }
 
 class test(RelationType):
-    permissions = {'read': ('managers', 'users', 'guests'),
+    __permissions__ = {'read': ('managers', 'users', 'guests'),
                    'delete': ('managers',),
                    'add': ('managers',)}
 
--- a/server/test/unittest_ldapuser.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/server/test/unittest_ldapuser.py	Fri Nov 20 19:35:54 2009 +0100
@@ -6,6 +6,8 @@
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
 
+import socket
+
 from logilab.common.testlib import TestCase, unittest_main, mock_object
 from cubicweb.devtools import TestServerConfiguration
 from cubicweb.devtools.testlib import CubicWebTC
@@ -13,6 +15,14 @@
 
 from cubicweb.server.sources.ldapuser import *
 
+if socket.gethostbyname('ldap').startswith('172'):
+    SYT = 'syt'
+    ADIM = 'adim'
+else:
+    SYT = 'sthenault'
+    ADIM = 'adimascio'
+
+
 def nopwd_authenticate(self, session, login, upassword):
     """used to monkey patch the source to get successful authentication without
     upassword checking
@@ -68,15 +78,15 @@
 
     def test_base(self):
         # check a known one
-        e = self.sexecute('CWUser X WHERE X login "syt"').get_entity(0, 0)
-        self.assertEquals(e.login, 'syt')
+        e = self.sexecute('CWUser X WHERE X login %(login)s', {'login': SYT}).get_entity(0, 0)
+        self.assertEquals(e.login, SYT)
         e.complete()
         self.assertEquals(e.creation_date, None)
         self.assertEquals(e.modification_date, None)
         self.assertEquals(e.firstname, None)
         self.assertEquals(e.surname, None)
         self.assertEquals(e.in_group[0].name, 'users')
-        self.assertEquals(e.owned_by[0].login, 'syt')
+        self.assertEquals(e.owned_by[0].login, SYT)
         self.assertEquals(e.created_by, ())
         self.assertEquals(e.primary_email[0].address, 'Sylvain Thenault')
         # email content should be indexed on the user
@@ -84,31 +94,33 @@
         self.assertEquals(rset.rows, [[e.eid]])
 
     def test_not(self):
-        eid = self.sexecute('CWUser X WHERE X login "syt"')[0][0]
+        eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': SYT})[0][0]
         rset = self.sexecute('CWUser X WHERE NOT X eid %s' % eid)
         self.assert_(rset)
         self.assert_(not eid in (r[0] for r in rset))
 
     def test_multiple(self):
-        seid = self.sexecute('CWUser X WHERE X login "syt"')[0][0]
-        aeid = self.sexecute('CWUser X WHERE X login "adim"')[0][0]
-        rset = self.sexecute('CWUser X, Y WHERE X login "syt", Y login "adim"')
+        seid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': SYT})[0][0]
+        aeid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': ADIM})[0][0]
+        rset = self.sexecute('CWUser X, Y WHERE X login %(syt)s, Y login %(adim)s',
+                            {'syt': SYT, 'adim': ADIM})
         self.assertEquals(rset.rows, [[seid, aeid]])
-        rset = self.sexecute('Any X,Y,L WHERE X login L, X login "syt", Y login "adim"')
-        self.assertEquals(rset.rows, [[seid, aeid, 'syt']])
+        rset = self.sexecute('Any X,Y,L WHERE X login L, X login %(syt)s, Y login %(adim)s',
+                            {'syt': SYT, 'adim': ADIM})
+        self.assertEquals(rset.rows, [[seid, aeid, SYT]])
 
     def test_in(self):
-        seid = self.sexecute('CWUser X WHERE X login "syt"')[0][0]
-        aeid = self.sexecute('CWUser X WHERE X login "adim"')[0][0]
-        rset = self.sexecute('Any X,L ORDERBY L WHERE X login IN("syt", "adim"), X login L')
-        self.assertEquals(rset.rows, [[aeid, 'adim'], [seid, 'syt']])
+        seid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': SYT})[0][0]
+        aeid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': ADIM})[0][0]
+        rset = self.sexecute('Any X,L ORDERBY L WHERE X login IN("%s", "%s"), X login L' % (SYT, ADIM))
+        self.assertEquals(rset.rows, [[aeid, ADIM], [seid, SYT]])
 
     def test_relations(self):
-        eid = self.sexecute('CWUser X WHERE X login "syt"')[0][0]
+        eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': SYT})[0][0]
         rset = self.sexecute('Any X,E WHERE X is CWUser, X login L, X primary_email E')
         self.assert_(eid in (r[0] for r in rset))
         rset = self.sexecute('Any X,L,E WHERE X is CWUser, X login L, X primary_email E')
-        self.assert_('syt' in (r[1] for r in rset))
+        self.assert_(SYT in (r[1] for r in rset))
 
     def test_count(self):
         nbusers = self.sexecute('Any COUNT(X) WHERE X is CWUser')[0][0]
@@ -117,15 +129,15 @@
         self.assert_(nbusers < 30, nbusers)
 
     def test_upper(self):
-        eid = self.sexecute('CWUser X WHERE X login "syt"')[0][0]
+        eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': SYT})[0][0]
         rset = self.sexecute('Any UPPER(L) WHERE X eid %s, X login L' % eid)
-        self.assertEquals(rset[0][0], 'SYT')
+        self.assertEquals(rset[0][0], SYT.upper())
 
     def test_unknown_attr(self):
-        eid = self.sexecute('CWUser X WHERE X login "syt"')[0][0]
+        eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': SYT})[0][0]
         rset = self.sexecute('Any L,C,M WHERE X eid %s, X login L, '
                             'X creation_date C, X modification_date M' % eid)
-        self.assertEquals(rset[0][0], 'syt')
+        self.assertEquals(rset[0][0], SYT)
         self.assertEquals(rset[0][1], None)
         self.assertEquals(rset[0][2], None)
 
@@ -138,54 +150,55 @@
         self.assertEquals(logins, sorted(logins))
 
     def test_or(self):
-        rset = self.sexecute('DISTINCT Any X WHERE X login "syt" OR (X in_group G, G name "managers")')
+        rset = self.sexecute('DISTINCT Any X WHERE X login %(login)s OR (X in_group G, G name "managers")',
+                            {'login': SYT})
         self.assertEquals(len(rset), 2, rset.rows) # syt + admin
 
     def test_nonregr_set_owned_by(self):
         # test that when a user coming from ldap is triggering a transition
         # the related TrInfo has correct owner information
-        self.sexecute('SET X in_group G WHERE X login "syt", G name "managers"')
+        self.sexecute('SET X in_group G WHERE X login %(syt)s, G name "managers"', {'syt': SYT})
         self.commit()
-        syt = self.sexecute('CWUser X WHERE X login "syt"').get_entity(0, 0)
+        syt = self.sexecute('CWUser X WHERE X login %(login)s', {'login': SYT}).get_entity(0, 0)
         self.assertEquals([g.name for g in syt.in_group], ['managers', 'users'])
         self.patch_authenticate()
-        cnx = self.login('syt', 'dummypassword')
+        cnx = self.login(SYT, 'dummypassword')
         cu = cnx.cursor()
-        alf = cu.execute('Any X WHERE X login "alf"').get_entity(0, 0)
-        alf.fire_transition('deactivate')
+        adim = cu.execute('CWUser X WHERE X login %(login)s', {'login': ADIM}).get_entity(0, 0)
+        adim.fire_transition('deactivate')
         try:
             cnx.commit()
-            alf = self.sexecute('CWUser X WHERE X login "alf"').get_entity(0, 0)
-            self.assertEquals(alf.in_state[0].name, 'deactivated')
-            trinfo = alf.latest_trinfo()
-            self.assertEquals(trinfo.owned_by[0].login, 'syt')
+            adim.clear_all_caches()
+            self.assertEquals(adim.in_state[0].name, 'deactivated')
+            trinfo = adim.latest_trinfo()
+            self.assertEquals(trinfo.owned_by[0].login, SYT)
             # select from_state to skip the user's creation TrInfo
             rset = self.sexecute('Any U ORDERBY D DESC WHERE WF wf_info_for X,'
                                 'WF creation_date D, WF from_state FS,'
                                 'WF owned_by U?, X eid %(x)s',
-                                {'x': alf.eid}, 'x')
+                                {'x': adim.eid}, 'x')
             self.assertEquals(rset.rows, [[syt.eid]])
         finally:
             # restore db state
             self.restore_connection()
-            alf = self.sexecute('Any X WHERE X login "alf"').get_entity(0, 0)
-            alf.fire_transition('activate')
-            self.sexecute('DELETE X in_group G WHERE X login "syt", G name "managers"')
+            adim = self.sexecute('CWUser X WHERE X login %(login)s', {'login': ADIM}).get_entity(0, 0)
+            adim.fire_transition('activate')
+            self.execute('DELETE X in_group G WHERE X login %(syt)s, G name "managers"', {'syt': SYT})
 
     def test_same_column_names(self):
         self.sexecute('Any X, Y WHERE X copain Y, X login "comme", Y login "cochon"')
 
     def test_multiple_entities_from_different_sources(self):
         self.create_user('cochon', req=self.session)
-        self.failUnless(self.sexecute('Any X,Y WHERE X login "syt", Y login "cochon"'))
+        self.failUnless(self.sexecute('Any X,Y WHERE X login %(syt)s, Y login "cochon"', {'syt': SYT}))
 
     def test_exists1(self):
         self.add_entity('CWGroup', name=u'bougloup1', req=self.session)
         self.add_entity('CWGroup', name=u'bougloup2', req=self.session)
         self.sexecute('SET U in_group G WHERE G name ~= "bougloup%", U login "admin"')
-        self.sexecute('SET U in_group G WHERE G name = "bougloup1", U login "syt"')
+        self.sexecute('SET U in_group G WHERE G name = "bougloup1", U login %(syt)s', {'syt': SYT})
         rset = self.sexecute('Any L,SN ORDERBY L WHERE X in_state S, S name SN, X login L, EXISTS(X in_group G, G name ~= "bougloup%")')
-        self.assertEquals(rset.rows, [['admin', 'activated'], ['syt', 'activated']])
+        self.assertEquals(rset.rows, [['admin', 'activated'], [SYT, 'activated']])
 
     def test_exists2(self):
         self.create_user('comme', req=self.session)
@@ -199,10 +212,10 @@
         self.create_user('cochon', req=self.session)
         self.sexecute('SET X copain Y WHERE X login "comme", Y login "cochon"')
         self.failUnless(self.sexecute('Any X, Y WHERE X copain Y, X login "comme", Y login "cochon"'))
-        self.sexecute('SET X copain Y WHERE X login "syt", Y login "cochon"')
-        self.failUnless(self.sexecute('Any X, Y WHERE X copain Y, X login "syt", Y login "cochon"'))
+        self.sexecute('SET X copain Y WHERE X login %(syt)s, Y login "cochon"', {'syt': SYT})
+        self.failUnless(self.sexecute('Any X, Y WHERE X copain Y, X login %(syt)s, Y login "cochon"', {'syt': SYT}))
         rset = self.sexecute('Any GN,L WHERE X in_group G, X login L, G name GN, G name "managers" OR EXISTS(X copain T, T login in ("comme", "cochon"))')
-        self.assertEquals(sorted(rset.rows), [['managers', 'admin'], ['users', 'comme'], ['users', 'syt']])
+        self.assertEquals(sorted(rset.rows), [['managers', 'admin'], ['users', 'comme'], ['users', SYT]])
 
     def test_exists4(self):
         self.create_user('comme', req=self.session)
@@ -211,7 +224,7 @@
         self.sexecute('SET X copain Y WHERE X login "comme", Y login "cochon"')
         self.sexecute('SET X copain Y WHERE X login "cochon", Y login "cochon"')
         self.sexecute('SET X copain Y WHERE X login "comme", Y login "billy"')
-        self.sexecute('SET X copain Y WHERE X login "syt", Y login "billy"')
+        self.sexecute('SET X copain Y WHERE X login %(syt)s, Y login "billy"', {'syt': SYT})
         # search for group name, login where
         #   CWUser copain with "comme" or "cochon" AND same login as the copain
         # OR
@@ -223,7 +236,7 @@
                            'EXISTS(X in_state S, S name "activated", NOT X copain T2, T2 login "billy")')
         all = self.sexecute('Any GN, L WHERE X in_group G, X login L, G name GN')
         all.rows.remove(['users', 'comme'])
-        all.rows.remove(['users', 'syt'])
+        all.rows.remove(['users', SYT])
         self.assertEquals(sorted(rset.rows), sorted(all.rows))
 
     def test_exists5(self):
@@ -233,17 +246,17 @@
         self.sexecute('SET X copain Y WHERE X login "comme", Y login "cochon"')
         self.sexecute('SET X copain Y WHERE X login "cochon", Y login "cochon"')
         self.sexecute('SET X copain Y WHERE X login "comme", Y login "billy"')
-        self.sexecute('SET X copain Y WHERE X login "syt", Y login "cochon"')
+        self.sexecute('SET X copain Y WHERE X login %(syt)s, Y login "cochon"', {'syt': SYT})
         rset= self.sexecute('Any L WHERE X login L, '
                            'EXISTS(X copain T, T login in ("comme", "cochon")) AND '
                            'NOT EXISTS(X copain T2, T2 login "billy")')
-        self.assertEquals(sorted(rset.rows), [['cochon'], ['syt']])
+        self.assertEquals(sorted(rset.rows), [['cochon'], [SYT]])
         rset= self.sexecute('Any GN,L WHERE X in_group G, X login L, G name GN, '
                            'EXISTS(X copain T, T login in ("comme", "cochon")) AND '
                            'NOT EXISTS(X copain T2, T2 login "billy")')
         self.assertEquals(sorted(rset.rows), [['guests', 'cochon'],
                                               ['users', 'cochon'],
-                                              ['users', 'syt']])
+                                              ['users', SYT]])
 
     def test_cd_restriction(self):
         rset = self.sexecute('CWUser X WHERE X creation_date > "2009-02-01"')
@@ -265,21 +278,21 @@
 
     def test_security1(self):
         cu = self._init_security_test()
-        rset = cu.execute('Any X WHERE X login "syt"')
+        rset = cu.execute('CWUser X WHERE X login %(login)s', {'login': SYT})
         self.assertEquals(rset.rows, [])
         rset = cu.execute('Any X WHERE X login "iaminguestsgrouponly"')
         self.assertEquals(len(rset.rows), 1)
 
     def test_security2(self):
         cu = self._init_security_test()
-        rset = cu.execute('Any X WHERE X has_text "syt"')
+        rset = cu.execute('Any X WHERE X has_text %(syt)s', {'syt': SYT})
         self.assertEquals(rset.rows, [])
         rset = cu.execute('Any X WHERE X has_text "iaminguestsgrouponly"')
         self.assertEquals(len(rset.rows), 1)
 
     def test_security3(self):
         cu = self._init_security_test()
-        rset = cu.execute('Any F WHERE X has_text "syt", X firstname F')
+        rset = cu.execute('Any F WHERE X has_text %(syt)s, X firstname F', {'syt': SYT})
         self.assertEquals(rset.rows, [])
         rset = cu.execute('Any F WHERE X has_text "iaminguestsgrouponly", X firstname F')
         self.assertEquals(rset.rows, [[None]])
--- a/server/test/unittest_msplanner.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/server/test/unittest_msplanner.py	Fri Nov 20 19:35:54 2009 +0100
@@ -706,7 +706,7 @@
         self._test('Any V, MAX(VR) WHERE V is Card, V creation_date VR, '
                    '(V creation_date TODAY OR (V creation_date < TODAY AND NOT EXISTS('
                    'X is Card, X creation_date < TODAY, X creation_date >= VR)))',
-                   [('FetchStep', [('Any VR WHERE X creation_date < TODAY, X creation_date >= VR, X is Card',
+                   [('FetchStep', [('Any VR WHERE X creation_date < TODAY, X creation_date VR, X is Card',
                                     [{'X': 'Card', 'VR': 'Datetime'}])],
                      [self.cards, self.system], None,
                      {'VR': 'table0.C0', 'X.creation_date': 'table0.C0'}, []),
@@ -1349,7 +1349,7 @@
     def test_attr_unification_neq_1(self):
         self._test('Any X,Y WHERE X is Bookmark, Y is Card, X creation_date D, Y creation_date > D',
                    [('FetchStep',
-                     [('Any Y,D WHERE Y creation_date > D, Y is Card',
+                     [('Any Y,D WHERE Y creation_date D, Y is Card',
                        [{'D': 'Datetime', 'Y': 'Card'}])],
                      [self.cards,self.system], None,
                      {'D': 'table0.C1', 'Y': 'table0.C0', 'Y.creation_date': 'table0.C1'}, []),
--- a/server/test/unittest_rql2sql.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/server/test/unittest_rql2sql.py	Fri Nov 20 19:35:54 2009 +0100
@@ -10,13 +10,13 @@
 
 import sys
 
-from logilab.common.testlib import TestCase, unittest_main
+from logilab.common.testlib import TestCase, unittest_main, mock_object
 
 from rql import BadRQLQuery
 from indexer import get_indexer
 
 #from cubicweb.server.sources.native import remove_unused_solutions
-from cubicweb.server.sources.rql2sql import SQLGenerator
+from cubicweb.server.sources.rql2sql import SQLGenerator, remove_unused_solutions
 
 from rql.utils import register_function, FunctionDescr
 # add a dumb registered procedure
@@ -165,6 +165,9 @@
      '''SELECT _X.cw_eid
 FROM cw_Personne AS _X
 WHERE _X.cw_prenom=lulu AND NOT EXISTS(SELECT 1 FROM owned_by_relation AS rel_owned_by0, in_group_relation AS rel_in_group1, cw_CWGroup AS _G WHERE rel_owned_by0.eid_from=_X.cw_eid AND rel_in_group1.eid_from=rel_owned_by0.eid_to AND rel_in_group1.eid_to=_G.cw_eid AND ((_G.cw_name=lulufanclub) OR (_G.cw_name=managers)))'''),
+
+
+
 ]
 
 ADVANCED= [
@@ -868,10 +871,21 @@
     ]
 
 VIRTUAL_VARS = [
-    ("Personne P WHERE P travaille S, S tel T, S fax T, S is Societe;",
+
+    ('Any X WHERE X is CWUser, X creation_date > D1, Y creation_date D1, Y login "SWEB09"',
+     '''SELECT _X.cw_eid
+FROM cw_CWUser AS _X, cw_CWUser AS _Y
+WHERE _X.cw_creation_date>_Y.cw_creation_date AND _Y.cw_login=SWEB09'''),
+
+    ('Any X WHERE X is CWUser, Y creation_date D1, Y login "SWEB09", X creation_date > D1',
+     '''SELECT _X.cw_eid
+FROM cw_CWUser AS _X, cw_CWUser AS _Y
+WHERE _Y.cw_login=SWEB09 AND _X.cw_creation_date>_Y.cw_creation_date'''),
+
+    ('Personne P WHERE P travaille S, S tel T, S fax T, S is Societe',
      '''SELECT rel_travaille0.eid_from
 FROM cw_Societe AS _S, travaille_relation AS rel_travaille0
-WHERE rel_travaille0.eid_to=_S.cw_eid AND _S.cw_fax=_S.cw_tel'''),
+WHERE rel_travaille0.eid_to=_S.cw_eid AND _S.cw_tel=_S.cw_fax'''),
 
     ("Personne P where X eid 0, X creation_date D, P datenaiss < D, X is Affaire",
      '''SELECT _P.cw_eid
@@ -1590,7 +1604,26 @@
 WHERE EXISTS(SELECT 1 FROM owned_by_relation AS rel_owned_by0, cw_Affaire AS _P WHERE rel_owned_by0.eid_from=_P.cw_eid AND rel_owned_by0.eid_to=1 UNION SELECT 1 FROM owned_by_relation AS rel_owned_by1, cw_Note AS _P WHERE rel_owned_by1.eid_from=_P.cw_eid AND rel_owned_by1.eid_to=1)''')
 
 
+class removeUnsusedSolutionsTC(TestCase):
+    def test_invariant_not_varying(self):
+        rqlst = mock_object(defined_vars={})
+        rqlst.defined_vars['A'] = mock_object(scope=rqlst, stinfo={'optrelations':False}, _q_invariant=True)
+        rqlst.defined_vars['B'] = mock_object(scope=rqlst, stinfo={'optrelations':False}, _q_invariant=False)
+        self.assertEquals(remove_unused_solutions(rqlst, [{'A': 'RugbyGroup', 'B': 'RugbyTeam'},
+                                                          {'A': 'FootGroup', 'B': 'FootTeam'}], {}, None),
+                          ([{'A': 'RugbyGroup', 'B': 'RugbyTeam'},
+                            {'A': 'FootGroup', 'B': 'FootTeam'}],
+                           {}, set('B'))
+                          )
 
+    def test_invariant_varying(self):
+        rqlst = mock_object(defined_vars={})
+        rqlst.defined_vars['A'] = mock_object(scope=rqlst, stinfo={'optrelations':False}, _q_invariant=True)
+        rqlst.defined_vars['B'] = mock_object(scope=rqlst, stinfo={'optrelations':False}, _q_invariant=False)
+        self.assertEquals(remove_unused_solutions(rqlst, [{'A': 'RugbyGroup', 'B': 'RugbyTeam'},
+                                                          {'A': 'FootGroup', 'B': 'RugbyTeam'}], {}, None),
+                          ([{'A': 'RugbyGroup', 'B': 'RugbyTeam'}], {}, set())
+                          )
 
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_security.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/server/test/unittest_security.py	Fri Nov 20 19:35:54 2009 +0100
@@ -14,13 +14,13 @@
     def setUp(self):
         CubicWebTC.setUp(self)
         self.create_user('iaminusersgrouponly')
-        self.readoriggroups = self.schema['Personne'].get_groups('read')
-        self.addoriggroups = self.schema['Personne'].get_groups('add')
+        self.readoriggroups = self.schema['Personne'].permissions['read']
+        self.addoriggroups = self.schema['Personne'].permissions['add']
 
     def tearDown(self):
         CubicWebTC.tearDown(self)
-        self.schema['Personne'].set_groups('read', self.readoriggroups)
-        self.schema['Personne'].set_groups('add', self.addoriggroups)
+        self.schema['Personne'].set_action_permissions('read', self.readoriggroups)
+        self.schema['Personne'].set_action_permissions('add', self.addoriggroups)
 
 
 class LowLevelSecurityFunctionTC(BaseSecurityTC):
@@ -29,7 +29,7 @@
         rql = u'Personne U where U nom "managers"'
         rqlst = self.repo.vreg.rqlhelper.parse(rql).children[0]
         origgroups = self.schema['Personne'].get_groups('read')
-        self.schema['Personne'].set_groups('read', ('users', 'managers'))
+        self.schema['Personne'].set_action_permissions('read', ('users', 'managers'))
         self.repo.vreg.rqlhelper.compute_solutions(rqlst)
         solution = rqlst.solutions[0]
         check_read_access(self.schema, self.session.user, rqlst, solution)
@@ -96,8 +96,8 @@
     def test_update_security_2(self):
         cnx = self.login('anon')
         cu = cnx.cursor()
-        self.repo.schema['Personne'].set_groups('read', ('users', 'managers'))
-        self.repo.schema['Personne'].set_groups('add', ('guests', 'users', 'managers'))
+        self.repo.schema['Personne'].set_action_permissions('read', ('users', 'managers'))
+        self.repo.schema['Personne'].set_action_permissions('add', ('guests', 'users', 'managers'))
         self.assertRaises(Unauthorized, cu.execute, "SET X nom 'bidulechouette' WHERE X is Personne")
         #self.assertRaises(Unauthorized, cnx.commit)
         # test nothing has actually been inserted
@@ -177,7 +177,7 @@
         ent = rset.get_entity(0, 0)
         session.set_pool() # necessary
         self.assertRaises(Unauthorized,
-                          ent.e_schema.check_perm, session, 'update', ent.eid)
+                          ent.e_schema.check_perm, session, 'update', eid=ent.eid)
         self.assertRaises(Unauthorized,
                           cu.execute, "SET P travaille S WHERE P is Personne, S is Societe")
         # test nothing has actually been inserted:
@@ -230,7 +230,7 @@
     # read security test
 
     def test_read_base(self):
-        self.schema['Personne'].set_groups('read', ('users', 'managers'))
+        self.schema['Personne'].set_action_permissions('read', ('users', 'managers'))
         cnx = self.login('anon')
         cu = cnx.cursor()
         self.assertRaises(Unauthorized,
@@ -281,7 +281,7 @@
         self.execute("INSERT Personne X: X nom 'bidule'")
         self.execute("INSERT Societe X: X nom 'bidule'")
         self.commit()
-        self.schema['Personne'].set_groups('read', ('managers',))
+        self.schema['Personne'].set_action_permissions('read', ('managers',))
         cnx = self.login('iaminusersgrouponly')
         cu = cnx.cursor()
         rset = cu.execute('Any N WHERE N has_text "bidule"')
@@ -293,7 +293,7 @@
         self.execute("INSERT Personne X: X nom 'bidule'")
         self.execute("INSERT Societe X: X nom 'bidule'")
         self.commit()
-        self.schema['Personne'].set_groups('read', ('managers',))
+        self.schema['Personne'].set_action_permissions('read', ('managers',))
         cnx = self.login('anon')
         cu = cnx.cursor()
         rset = cu.execute('Any N,U WHERE N has_text "bidule", N owned_by U?')
@@ -371,8 +371,8 @@
 
     def test_attribute_read_security(self):
         # anon not allowed to see users'login, but they can see users
-        self.repo.schema['CWUser'].set_groups('read', ('guests', 'users', 'managers'))
-        self.repo.schema['login'].set_groups('read', ('users', 'managers'))
+        self.repo.schema['CWUser'].set_action_permissions('read', ('guests', 'users', 'managers'))
+        self.repo.schema['CWUser'].rdef('login').set_action_permissions('read', ('users', 'managers'))
         cnx = self.login('anon')
         cu = cnx.cursor()
         rset = cu.execute('CWUser X')
@@ -494,11 +494,11 @@
         # needed to avoid check_perm error
         session.set_pool()
         # needed to remove rql expr granting update perm to the user
-        self.schema['Affaire'].set_rqlexprs('update', ())
+        self.schema['Affaire'].set_action_permissions('update', self.schema['Affaire'].get_groups('update'))
         self.assertRaises(Unauthorized,
-                          self.schema['Affaire'].check_perm, session, 'update', eid)
+                          self.schema['Affaire'].check_perm, session, 'update', eid=eid)
         cu = cnx.cursor()
-        self.schema['Affaire'].set_groups('read', ('users',))
+        self.schema['Affaire'].set_action_permissions('read', ('users',))
         try:
             aff = cu.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
             aff.fire_transition('abort')
@@ -510,7 +510,7 @@
             # from the current state but Unauthorized if it exists but user can't pass it
             self.assertRaises(ValidationError, user.fire_transition, 'deactivate')
         finally:
-            self.schema['Affaire'].set_groups('read', ('managers',))
+            self.schema['Affaire'].set_action_permissions('read', ('managers',))
 
     def test_trinfo_security(self):
         aff = self.execute('INSERT Affaire X: X ref "ARCT01"').get_entity(0, 0)
--- a/sobjects/notification.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/sobjects/notification.py	Fri Nov 20 19:35:54 2009 +0100
@@ -149,14 +149,16 @@
         changes = self.req.transaction_data['changes'][self.rset[0][0]]
         _ = self.req._
         formatted_changes = []
+        entity = self.entity(self.row or 0, self.col or 0)
         for attr, oldvalue, newvalue in sorted(changes):
             # check current user has permission to see the attribute
             rschema = self.vreg.schema[attr]
             if rschema.final:
-                if not rschema.has_perm(self.req, 'read', eid=self.rset[0][0]):
+                rdef = entity.e_schema.rdef(rschema)
+                if not rdef.has_perm(self.req, 'read', eid=self.rset[0][0]):
                     continue
             # XXX suppose it's a subject relation...
-            elif not rschema.has_perm(self.req, 'read', fromeid=self.rset[0][0]):
+            elif not rschema.has_perm(self.req, 'read', fromeid=self.rset[0][0]): # XXX toeid
                 continue
             if attr in self.no_detailed_change_attrs:
                 msg = _('%s updated') % _(attr)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sobjects/textparsers.py	Fri Nov 20 19:35:54 2009 +0100
@@ -0,0 +1,73 @@
+"""hooks triggered on email entities creation:
+
+* look for state change instruction (XXX security)
+* set email content as a comment on an entity when comments are supported and
+  linking information are found
+
+:organization: Logilab
+:copyright: 2003-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+import re
+
+from cubicweb import UnknownEid, typed_eid
+from cubicweb.view import Component
+
+        # XXX use user session if gpg signature validated
+
+class TextAnalyzer(Component):
+    """analyze and extract information from plain text by calling registered
+    text parsers
+    """
+    id = 'textanalyzer'
+
+    def parse(self, caller, text):
+        for parsercls in self.req.vreg['components'].get('textparser', ()):
+            parsercls(self.req).parse(caller, text)
+
+
+class TextParser(Component):
+    """base class for text parser, responsible to extract some information
+    from plain text. When something is done, it usually call the
+
+      .fire_event(something, {event args})
+
+    method on the caller.
+    """
+    id = 'textparser'
+    __abstract__ = True
+
+    def parse(self, caller, text):
+        raise NotImplementedError
+
+
+class ChangeStateTextParser(TextParser):
+    """search some text for change state instruction in the form
+
+         :<transition name>: #?<eid>
+    """
+    instr_rgx = re.compile(':(\w+):\s*#?(\d+)', re.U)
+
+    def parse(self, caller, text):
+        for trname, eid in self.instr_rgx.findall(text):
+            try:
+                entity = self.req.entity_from_eid(typed_eid(eid))
+            except UnknownEid:
+                self.error("can't get entity with eid %s", eid)
+                continue
+            if not hasattr(entity, 'in_state'):
+                self.error('bad change state instruction for eid %s', eid)
+                continue
+            tr = entity.current_workflow and entity.current_workflow.transition_by_name(trname)
+            if tr and tr.may_be_fired(entity.eid):
+                try:
+                    trinfo = entity.fire_transition(tr)
+                    caller.fire_event('state-changed', {'trinfo': trinfo,
+                                                        'entity': entity})
+                except:
+                    self.exception('while changing state of %s', entity)
+            else:
+                self.error("can't pass transition %s on entity %s",
+                           trname, entity)
--- a/test/data/erqlexpr_on_ertype.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/test/data/erqlexpr_on_ertype.py	Fri Nov 20 19:35:54 2009 +0100
@@ -9,7 +9,7 @@
 from cubicweb.schema import ERQLExpression
 
 class ToTo(EntityType):
-    permissions = {
+    __permissions__ = {
         'read': ('managers',),
         'add': ('managers',),
         'update': ('managers',),
@@ -18,7 +18,7 @@
     toto = SubjectRelation('TuTu')
 
 class TuTu(EntityType):
-    permissions = {
+    __permissions__ = {
         'read': ('managers',),
         'add': ('managers',),
         'update': ('managers',),
@@ -26,7 +26,7 @@
         }
 
 class toto(RelationType):
-    permissions = {
+    __permissions__ = {
         'read': ('managers', ),
         'add': ('managers', ERQLExpression('S bla Y'),),
         'delete': ('managers',),
--- a/test/data/rewrite/schema.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/test/data/rewrite/schema.py	Fri Nov 20 19:35:54 2009 +0100
@@ -2,7 +2,7 @@
 from cubicweb.schema import ERQLExpression
 
 class Affaire(EntityType):
-    permissions = {
+    __permissions__ = {
         'read':   ('managers',
                    ERQLExpression('X owned_by U'), ERQLExpression('X concerne S?, S owned_by U')),
         'add':    ('managers', ERQLExpression('X concerne S, S owned_by U')),
@@ -15,7 +15,7 @@
 
 
 class Societe(EntityType):
-    permissions = {
+    __permissions__ = {
         'read': ('managers', 'users', 'guests'),
         'update': ('managers', 'owners', ERQLExpression('U login L, X nom L')),
         'delete': ('managers', 'owners', ERQLExpression('U login L, X nom L')),
--- a/test/data/rqlexpr_on_ertype_read.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/test/data/rqlexpr_on_ertype_read.py	Fri Nov 20 19:35:54 2009 +0100
@@ -9,7 +9,7 @@
 from cubicweb.schema import RRQLExpression
 
 class ToTo(EntityType):
-    permissions = {
+    __permissions__ = {
         'read': ('managers',),
         'add': ('managers',),
         'update': ('managers',),
@@ -18,7 +18,7 @@
     toto = SubjectRelation('TuTu')
 
 class TuTu(EntityType):
-    permissions = {
+    __permissions__ = {
         'read': ('managers',),
         'add': ('managers',),
         'update': ('managers',),
@@ -26,7 +26,7 @@
         }
 
 class toto(RelationType):
-    permissions = {
+    __permissions__ = {
         'read': ('managers', RRQLExpression('S bla Y'), ),
         'add': ('managers',),
         'delete': ('managers',),
--- a/test/data/rrqlexpr_on_attr.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/test/data/rrqlexpr_on_attr.py	Fri Nov 20 19:35:54 2009 +0100
@@ -9,7 +9,7 @@
 from cubicweb.schema import RRQLExpression
 
 class ToTo(EntityType):
-    permissions = {
+    __permissions__ = {
         'read': ('managers',),
         'add': ('managers',),
         'update': ('managers',),
@@ -18,7 +18,7 @@
     attr = String()
 
 class attr(RelationType):
-    permissions = {
+    __permissions__ = {
         'read': ('managers', ),
         'add': ('managers', RRQLExpression('S bla Y'),),
         'delete': ('managers',),
--- a/test/data/rrqlexpr_on_eetype.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/test/data/rrqlexpr_on_eetype.py	Fri Nov 20 19:35:54 2009 +0100
@@ -9,7 +9,7 @@
 from cubicweb.schema import RRQLExpression
 
 class ToTo(EntityType):
-    permissions = {
+    __permissions__ = {
         'read': ('managers', RRQLExpression('S bla Y'),),
         'add': ('managers',),
         'update': ('managers',),
--- a/test/unittest_entity.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/test/unittest_entity.py	Fri Nov 20 19:35:54 2009 +0100
@@ -56,7 +56,7 @@
     def test_copy_with_nonmeta_composite_inlined(self):
         p = self.add_entity('Personne', nom=u'toto')
         oe = self.add_entity('Note', type=u'x')
-        self.schema['ecrit_par'].set_rproperty('Note', 'Personne', 'composite', 'subject')
+        self.schema['ecrit_par'].rdef('Note', 'Personne').composite = 'subject'
         self.execute('SET T ecrit_par U WHERE T eid %(t)s, U eid %(u)s',
                      {'t': oe.eid, 'u': p.eid}, ('t','u'))
         e = self.add_entity('Note', type=u'z')
@@ -120,10 +120,10 @@
         Note = self.vreg['etypes'].etype_class('Note')
         peschema = Personne.e_schema
         seschema = Societe.e_schema
-        peschema.subjrels['travaille'].set_rproperty(peschema, seschema, 'cardinality', '1*')
-        peschema.subjrels['connait'].set_rproperty(peschema, peschema, 'cardinality', '11')
-        peschema.subjrels['evaluee'].set_rproperty(peschema, Note.e_schema, 'cardinality', '1*')
-        seschema.subjrels['evaluee'].set_rproperty(seschema, Note.e_schema, 'cardinality', '1*')
+        peschema.subjrels['travaille'].rdef(peschema, seschema).cardinality = '1*'
+        peschema.subjrels['connait'].rdef(peschema, peschema).cardinality = '11'
+        peschema.subjrels['evaluee'].rdef(peschema, Note.e_schema).cardinality = '1*'
+        seschema.subjrels['evaluee'].rdef(seschema, Note.e_schema).cardinality = '1*'
         # testing basic fetch_attrs attribute
         self.assertEquals(Personne.fetch_rql(user),
                           'Any X,AA,AB,AC ORDERBY AA ASC '
@@ -158,13 +158,13 @@
             self.assertEquals(Personne.fetch_rql(user), 'Any X,AA,AB ORDERBY AA ASC '
                               'WHERE X is Personne, X nom AA, X connait AB?')
             # testing optional relation
-            peschema.subjrels['travaille'].set_rproperty(peschema, seschema, 'cardinality', '?*')
+            peschema.subjrels['travaille'].rdef(peschema, seschema).cardinality = '?*'
             Personne.fetch_attrs = ('nom', 'prenom', 'travaille')
             Societe.fetch_attrs = ('nom',)
             self.assertEquals(Personne.fetch_rql(user),
                               'Any X,AA,AB,AC,AD ORDERBY AA ASC WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD')
             # testing relation with cardinality > 1
-            peschema.subjrels['travaille'].set_rproperty(peschema, seschema, 'cardinality', '**')
+            peschema.subjrels['travaille'].rdef(peschema, seschema).cardinality = '**'
             self.assertEquals(Personne.fetch_rql(user),
                               'Any X,AA,AB ORDERBY AA ASC WHERE X is Personne, X nom AA, X prenom AB')
             # XXX test unauthorized attribute
@@ -232,6 +232,14 @@
         #rql = email.unrelated_rql('use_email', 'Person', 'object')[0]
         #self.assertEquals(rql, '')
 
+    def test_unrelated_rql_security_nonexistant(self):
+        self.login('anon')
+        email = self.vreg['etypes'].etype_class('EmailAddress')(self.request())
+        rql = email.unrelated_rql('use_email', 'CWUser', 'object')[0]
+        self.assertEquals(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
+                          'WHERE S is CWUser, S login AA, S firstname AB, S surname AC, S modification_date AD, '
+                          'A eid %(B)s, EXISTS(S identity A, NOT A in_group C, C name "guests", C is CWGroup)')
+
     def test_unrelated_base(self):
         p = self.add_entity('Personne', nom=u'di mascio', prenom=u'adrien')
         e = self.add_entity('Tag', name=u'x')
--- a/test/unittest_rqlrewrite.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/test/unittest_rqlrewrite.py	Fri Nov 20 19:35:54 2009 +0100
@@ -11,13 +11,15 @@
 from rql import parse, nodes, RQLHelper
 
 from cubicweb import Unauthorized
+from cubicweb.schema import RRQLExpression
 from cubicweb.rqlrewrite import RQLRewriter
 from cubicweb.devtools import repotest, TestServerConfiguration
 
 config = TestServerConfiguration('data/rewrite')
 config.bootstrap_cubes()
 schema = config.load_schema()
-schema.add_relation_def(mock_object(subject='Card', name='in_state', object='State', cardinality='1*'))
+from yams.buildobjs import RelationDefinition
+schema.add_relation_def(RelationDefinition(subject='Card', name='in_state', object='State', cardinality='1*'))
 
 rqlhelper = RQLHelper(schema, special_relations={'eid': 'uid',
                                                  'has_text': 'fti'})
@@ -32,7 +34,7 @@
     return {1: 'CWUser',
             2: 'Card'}[eid]
 
-def rewrite(rqlst, snippets_map, kwargs):
+def rewrite(rqlst, snippets_map, kwargs, existingvars=None):
     class FakeVReg:
         schema = schema
         @staticmethod
@@ -47,12 +49,15 @@
                 rqlhelper.simplify(rqlst, needcopy)
     rewriter = RQLRewriter(mock_object(vreg=FakeVReg, user=(mock_object(eid=1))))
     for v, snippets in snippets_map.items():
-        snippets_map[v] = [mock_object(snippet_rqlst=parse('Any X WHERE '+snippet).children[0],
-                                       expression='Any X WHERE '+snippet)
+        snippets_map[v] = [isinstance(snippet, basestring)
+                           and mock_object(snippet_rqlst=parse('Any X WHERE '+snippet).children[0],
+                                           expression='Any X WHERE '+snippet)
+                           or snippet
                            for snippet in snippets]
     rqlhelper.compute_solutions(rqlst.children[0], {'eid': eid_func_map}, kwargs=kwargs)
     solutions = rqlst.children[0].solutions
-    rewriter.rewrite(rqlst.children[0], snippets_map.items(), solutions, kwargs)
+    rewriter.rewrite(rqlst.children[0], snippets_map.items(), solutions, kwargs,
+                     existingvars)
     test_vrefs(rqlst.children[0])
     return rewriter.rewritten
 
@@ -241,6 +246,61 @@
                              u"Any X,C WHERE X? documented_by C, C is Card WITH X BEING (Any X WHERE X concerne A, X is Affaire)")
 
 
+    def test_rrqlexpr_nonexistant_subject_1(self):
+        constraint = RRQLExpression('S owned_by U')
+        rqlst = parse('Card C')
+        rewrite(rqlst, {('C', 'S'): (constraint,)}, {}, 'SU')
+        self.failUnlessEqual(rqlst.as_string(),
+                             u"Any C WHERE C is Card, A eid %(B)s, EXISTS(C owned_by A)")
+        rqlst = parse('Card C')
+        rewrite(rqlst, {('C', 'S'): (constraint,)}, {}, 'OU')
+        self.failUnlessEqual(rqlst.as_string(),
+                             u"Any C WHERE C is Card")
+        rqlst = parse('Card C')
+        rewrite(rqlst, {('C', 'S'): (constraint,)}, {}, 'SOU')
+        self.failUnlessEqual(rqlst.as_string(),
+                             u"Any C WHERE C is Card, A eid %(B)s, EXISTS(C owned_by A)")
+
+    def test_rrqlexpr_nonexistant_subject_2(self):
+        constraint = RRQLExpression('S owned_by U, O owned_by U, O is Card')
+        rqlst = parse('Card C')
+        rewrite(rqlst, {('C', 'S'): (constraint,)}, {}, 'SU')
+        self.failUnlessEqual(rqlst.as_string(),
+                             'Any C WHERE C is Card, A eid %(B)s, EXISTS(C owned_by A)')
+        rqlst = parse('Card C')
+        rewrite(rqlst, {('C', 'S'): (constraint,)}, {}, 'OU')
+        self.failUnlessEqual(rqlst.as_string(),
+                             'Any C WHERE C is Card, B eid %(D)s, EXISTS(A owned_by B, A is Card)')
+        rqlst = parse('Card C')
+        rewrite(rqlst, {('C', 'S'): (constraint,)}, {}, 'SOU')
+        self.failUnlessEqual(rqlst.as_string(),
+                             'Any C WHERE C is Card, A eid %(B)s, EXISTS(C owned_by A, D owned_by A, D is Card)')
+
+    def test_rrqlexpr_nonexistant_subject_3(self):
+        constraint = RRQLExpression('U in_group G, G name "users"')
+        rqlst = parse('Card C')
+        rewrite(rqlst, {('C', 'S'): (constraint,)}, {}, 'SU')
+        self.failUnlessEqual(rqlst.as_string(),
+                             u'Any C WHERE C is Card, A eid %(B)s, EXISTS(A in_group D, D name "users", D is CWGroup)')
+
+    def test_rrqlexpr_nonexistant_subject_4(self):
+        constraint = RRQLExpression('U in_group G, G name "users", S owned_by U')
+        rqlst = parse('Card C')
+        rewrite(rqlst, {('C', 'S'): (constraint,)}, {}, 'SU')
+        self.failUnlessEqual(rqlst.as_string(),
+                             u'Any C WHERE C is Card, A eid %(B)s, EXISTS(A in_group D, D name "users", C owned_by A, D is CWGroup)')
+        rqlst = parse('Card C')
+        rewrite(rqlst, {('C', 'S'): (constraint,)}, {}, 'OU')
+        self.failUnlessEqual(rqlst.as_string(),
+                             u'Any C WHERE C is Card, A eid %(B)s, EXISTS(A in_group D, D name "users", D is CWGroup)')
+
+    def test_rrqlexpr_nonexistant_subject_5(self):
+        constraint = RRQLExpression('S owned_by Z, O owned_by Z, O is Card')
+        rqlst = parse('Card C')
+        rewrite(rqlst, {('C', 'S'): (constraint,)}, {}, 'S')
+        self.failUnlessEqual(rqlst.as_string(),
+                             u"Any C WHERE C is Card, EXISTS(C owned_by A, A is CWUser)")
+
 
 if __name__ == '__main__':
     unittest_main()
--- a/test/unittest_schema.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/test/unittest_schema.py	Fri Nov 20 19:35:54 2009 +0100
@@ -19,7 +19,7 @@
 from yams.reader import PyFileReader
 
 from cubicweb.schema import CubicWebSchema, CubicWebEntitySchema, \
-     RQLConstraint, CubicWebSchemaLoader, ERQLExpression, RRQLExpression, \
+     RQLConstraint, CubicWebSchemaLoader, RQLExpression, ERQLExpression, RRQLExpression, \
      normalize_expression, order_eschemas
 from cubicweb.devtools import TestServerConfiguration as TestConfiguration
 
@@ -44,7 +44,7 @@
 schema = CubicWebSchema('Test Schema')
 enote = schema.add_entity_type(EntityType('Note'))
 eaffaire = schema.add_entity_type(EntityType('Affaire'))
-eperson = schema.add_entity_type(EntityType('Personne', permissions=PERSONNE_PERMISSIONS))
+eperson = schema.add_entity_type(EntityType('Personne', __permissions__=PERSONNE_PERMISSIONS))
 esociete = schema.add_entity_type(EntityType('Societe'))
 
 RELS = (
@@ -73,12 +73,13 @@
 for rel in RELS:
     _from, _type, _to = rel.split()
     if not _type.lower() in done:
-        if _type == 'concerne':
-            schema.add_relation_type(RelationType(_type, permissions=CONCERNE_PERMISSIONS))
-        else:
-            schema.add_relation_type(RelationType(_type))
+        schema.add_relation_type(RelationType(_type))
         done[_type.lower()] = True
-    schema.add_relation_def(RelationDefinition(_from, _type, _to))
+    if _type == 'concerne':
+        schema.add_relation_def(RelationDefinition(_from, _type, _to,
+                                                   __permissions__=CONCERNE_PERMISSIONS))
+    else:
+        schema.add_relation_def(RelationDefinition(_from, _type, _to))
 
 class CubicWebSchemaTC(TestCase):
 
@@ -94,26 +95,24 @@
         self.assertEqual(schema.rschema('concerne').type, 'concerne')
 
     def test_entity_perms(self):
-        eperson.set_default_groups()
         self.assertEqual(eperson.get_groups('read'), set(('managers', 'users', 'guests')))
         self.assertEqual(eperson.get_groups('update'), set(('managers', 'owners',)))
         self.assertEqual(eperson.get_groups('delete'), set(('managers', 'owners')))
         self.assertEqual(eperson.get_groups('add'), set(('managers',)))
         self.assertEqual([str(e) for e in eperson.get_rqlexprs('add')],
                          ['Any X WHERE X travaille S, S owned_by U, X eid %(x)s, U eid %(u)s'])
-        eperson.set_groups('read', ('managers',))
+        eperson.set_action_permissions('read', ('managers',))
         self.assertEqual(eperson.get_groups('read'), set(('managers',)))
 
     def test_relation_perms(self):
-        rconcerne = schema.rschema('concerne')
-        rconcerne.set_default_groups()
+        rconcerne = schema.rschema('concerne').rdef('Personne', 'Societe')
         self.assertEqual(rconcerne.get_groups('read'), set(('managers', 'users', 'guests')))
         self.assertEqual(rconcerne.get_groups('delete'), set(('managers',)))
         self.assertEqual(rconcerne.get_groups('add'), set(('managers', )))
-        rconcerne.set_groups('read', ('managers',))
+        rconcerne.set_action_permissions('read', ('managers',))
         self.assertEqual(rconcerne.get_groups('read'), set(('managers',)))
         self.assertEqual([str(e) for e in rconcerne.get_rqlexprs('add')],
-                         ['Any S WHERE U has_update_permission S, S eid %(s)s, U eid %(u)s'])
+                         ['Any S,U WHERE U has_update_permission S, S eid %(s)s, U eid %(u)s'])
 
     def test_erqlexpression(self):
         self.assertRaises(RQLSyntaxError, ERQLExpression, '1')
@@ -124,7 +123,7 @@
         self.assertRaises(Exception, RRQLExpression, '1')
         self.assertRaises(RQLSyntaxError, RRQLExpression, 'O X Y')
         expr = RRQLExpression('U has_update_permission O')
-        self.assertEquals(str(expr), 'Any O WHERE U has_update_permission O, O eid %(o)s, U eid %(u)s')
+        self.assertEquals(str(expr), 'Any O,U WHERE U has_update_permission O, O eid %(o)s, U eid %(u)s')
 
 loader = CubicWebSchemaLoader()
 config = TestConfiguration('data')
@@ -215,9 +214,9 @@
         self.assertListEquals(rels, ['bookmarked_by', 'created_by', 'for_user',
                                      'identity', 'owned_by', 'wf_info_for'])
         rschema = schema.rschema('relation_type')
-        properties = rschema.rproperties('CWAttribute', 'CWRType')
-        self.assertEquals(properties['cardinality'], '1*')
-        constraints = properties['constraints']
+        properties = rschema.rdef('CWAttribute', 'CWRType')
+        self.assertEquals(properties.cardinality, '1*')
+        constraints = properties.constraints
         self.failUnlessEqual(len(constraints), 1, constraints)
         constraint = constraints[0]
         self.failUnless(isinstance(constraint, RQLConstraint))
@@ -246,13 +245,13 @@
         self._test('rrqlexpr_on_eetype.py', "can't use RRQLExpression on an entity type, use an ERQLExpression (ToTo)")
 
     def test_erqlexpr_on_rtype(self):
-        self._test('erqlexpr_on_ertype.py', "can't use ERQLExpression on a relation type, use a RRQLExpression (toto)")
+        self._test('erqlexpr_on_ertype.py', "can't use ERQLExpression on relation ToTo toto TuTu, use a RRQLExpression")
 
     def test_rqlexpr_on_rtype_read(self):
-        self._test('rqlexpr_on_ertype_read.py', "can't use rql expression for read permission of a relation type (toto)")
+        self._test('rqlexpr_on_ertype_read.py', "can't use rql expression for read permission of relation ToTo toto TuTu")
 
     def test_rrqlexpr_on_attr(self):
-        self._test('rrqlexpr_on_attr.py', "can't use RRQLExpression on a final relation type (eg attribute relation), use an ERQLExpression (attr)")
+        self._test('rrqlexpr_on_attr.py', "can't use RRQLExpression on attribute ToTo.attr[String], use an ERQLExpression")
 
 
 class NormalizeExpressionTC(TestCase):
@@ -261,5 +260,9 @@
         self.assertEquals(normalize_expression('X  bla Y,Y blur Z  ,  Z zigoulou   X '),
                                                'X bla Y, Y blur Z, Z zigoulou X')
 
+class RQLExpressionTC(TestCase):
+    def test_comparison(self):
+        self.assertEquals(ERQLExpression('X is CWUser', 'X', 0), ERQLExpression('X is CWUser', 'X', 0))
+        self.assertNotEquals(ERQLExpression('X is CWUser', 'X', 0), ERQLExpression('X is CWGroup', 'X', 0))
 if __name__ == '__main__':
     unittest_main()
--- a/utils.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/utils.py	Fri Nov 20 19:35:54 2009 +0100
@@ -53,6 +53,13 @@
 def days_in_month(date_):
     return monthrange(date_.year, date_.month)[1]
 
+def days_in_year(date_):
+    feb = pydatetime.date(date_.year, 2, 1)
+    if days_in_month(feb) == 29:
+        return 366
+    else:
+        return 365
+
 def previous_month(date_, nbmonth=1):
     while nbmonth:
         date_ = first_day(date_) - ONEDAY
@@ -142,6 +149,20 @@
     return dict1
 
 
+# use networkX instead ?
+# http://networkx.lanl.gov/reference/algorithms.traversal.html#module-networkx.algorithms.traversal.astar
+def transitive_closure_of(entity, relname, _seen=None):
+    if _seen is None:
+        _seen = set()
+    _seen.add(entity.eid)
+    yield entity
+    for child in getattr(entity, relname):
+        if child.eid in _seen:
+            continue
+        for subchild in transitive_closure_of(child, relname, _seen):
+            yield subchild
+
+
 class SizeConstrainedList(list):
     """simple list that makes sure the list does not get bigger
     than a given size.
@@ -215,8 +236,16 @@
     def add_raw(self, rawheader):
         self.write(rawheader)
 
-    def define_var(self, var, value):
-        self.jsvars.append( (var, value) )
+    def define_var(self, var, value, override=True):
+        """adds a javascript var declaration / assginment in the header
+
+        :param var: the variable name
+        :param value: the variable value (as a raw python value,
+                      it will be jsonized later)
+        :param override: if False, don't set the variable value if the variable
+                         is already defined. Default is True.
+        """
+        self.jsvars.append( (var, value, override) )
 
     def add_post_inline_script(self, content):
         self.post_inlined_scripts.append(content)
@@ -270,8 +299,12 @@
         # 1/ variable declaration if any
         if self.jsvars:
             w(u'<script type="text/javascript"><!--//--><![CDATA[//><!--\n')
-            for var, value in self.jsvars:
-                w(u'%s = %s;\n' % (var, dumps(value)))
+            for var, value, override in self.jsvars:
+                vardecl = u'%s = %s;' % (var, dumps(value))
+                if not override:
+                    vardecl = (u'if (typeof %s == "undefined") {%s}' %
+                               (var, vardecl))
+                w(vardecl + u'\n')
             w(u'//--><!]]></script>\n')
         # 2/ css files
         for cssfile, media in self.cssfiles:
@@ -338,7 +371,7 @@
 
 def can_do_pdf_conversion(__answer=[None]):
     """pdf conversion depends on
-    * pyxmltrf (python package)
+    * pysixt (python package)
     * fop 0.9x
     """
     if __answer[0] is not None:
--- a/view.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/view.py	Fri Nov 20 19:35:54 2009 +0100
@@ -34,35 +34,35 @@
   style       CDATA         #IMPLIED
   title       CDATA         #IMPLIED
 
- cubicweb:sortvalue         CDATA   #IMPLIED
- cubicweb:target            CDATA   #IMPLIED
- cubicweb:limit             CDATA   #IMPLIED
- cubicweb:type              CDATA   #IMPLIED
- cubicweb:loadtype          CDATA   #IMPLIED
- cubicweb:wdgtype           CDATA   #IMPLIED
+ cubicweb:accesskey         CDATA   #IMPLIED
+ cubicweb:actualrql         CDATA   #IMPLIED
+ cubicweb:dataurl           CDATA   #IMPLIED
+ cubicweb:displayactions    CDATA   #IMPLIED
+ cubicweb:facetName         CDATA   #IMPLIED
+ cubicweb:facetargs         CDATA   #IMPLIED
+ cubicweb:fallbackvid       CDATA   #IMPLIED
+ cubicweb:fname             CDATA   #IMPLIED
  cubicweb:initfunc          CDATA   #IMPLIED
  cubicweb:inputid           CDATA   #IMPLIED
- cubicweb:tindex            CDATA   #IMPLIED
  cubicweb:inputname         CDATA   #IMPLIED
- cubicweb:value             CDATA   #IMPLIED
+ cubicweb:limit             CDATA   #IMPLIED
+ cubicweb:loadtype          CDATA   #IMPLIED
+ cubicweb:loadurl           CDATA   #IMPLIED
+ cubicweb:maxlength         CDATA   #IMPLIED
  cubicweb:required          CDATA   #IMPLIED
- cubicweb:accesskey         CDATA   #IMPLIED
- cubicweb:maxlength         CDATA   #IMPLIED
+ cubicweb:rooteid           CDATA   #IMPLIED
+ cubicweb:rql               CDATA   #IMPLIED
+ cubicweb:size              CDATA   #IMPLIED
+ cubicweb:sortvalue         CDATA   #IMPLIED
+ cubicweb:target            CDATA   #IMPLIED
+ cubicweb:tindex            CDATA   #IMPLIED
+ cubicweb:tlunit            CDATA   #IMPLIED
+ cubicweb:type              CDATA   #IMPLIED
+ cubicweb:uselabel          CDATA   #IMPLIED
+ cubicweb:value             CDATA   #IMPLIED
  cubicweb:variables         CDATA   #IMPLIED
- cubicweb:displayactions    CDATA   #IMPLIED
- cubicweb:fallbackvid       CDATA   #IMPLIED
- cubicweb:fname             CDATA   #IMPLIED
  cubicweb:vid               CDATA   #IMPLIED
- cubicweb:rql               CDATA   #IMPLIED
- cubicweb:actualrql         CDATA   #IMPLIED
- cubicweb:rooteid           CDATA   #IMPLIED
- cubicweb:dataurl           CDATA   #IMPLIED
- cubicweb:size              CDATA   #IMPLIED
- cubicweb:tlunit            CDATA   #IMPLIED
- cubicweb:loadurl           CDATA   #IMPLIED
- cubicweb:uselabel          CDATA   #IMPLIED
- cubicweb:facetargs         CDATA   #IMPLIED
- cubicweb:facetName         CDATA   #IMPLIED
+ cubicweb:wdgtype           CDATA   #IMPLIED
   "> ] '''
 
 TRANSITIONAL_DOCTYPE = u'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" %s>\n' % CW_XHTML_EXTENSIONS
@@ -305,21 +305,27 @@
 
     def create_url(self, etype, **kwargs):
         """ return the url of the entity creation form for a given entity type"""
-        return self._cw.build_url('add/%s'%etype, **kwargs)
+        return self._cw.build_url('add/%s' % etype, **kwargs)
 
-    def field(self, label, value, row=True, show_label=True, w=None, tr=True):
-        """ read-only field """
+    def field(self, label, value, row=True, show_label=True, w=None, tr=True, table=False):
+        """read-only field"""
         if w is None:
             w = self.w
-        if row:
-            w(u'<div class="row">')
+        if table:
+            w(u'<tr class="entityfield">')
+        else:
+            w(u'<div class="entityfield">')
         if show_label and label:
             if tr:
                 label = display_name(self._cw, label)
-            w(u'<span class="label">%s</span>' % label)
-        w(u'<div class="field">%s</div>' % value)
-        if row:
-            w(u'</div>')
+            if table:
+                w(u'<th>%s</th>' % label)
+            else:
+                w(u'<span>%s</span> ' % label)
+        if table:
+            w(u'<td>%s</td></tr>' % value)
+        else:
+            w(u'<span>%s</span></div>' % value)
 
 
 
--- a/web/__init__.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/__init__.py	Fri Nov 20 19:35:54 2009 +0100
@@ -23,12 +23,12 @@
 
 class stdmsgs(object):
     """standard ui message (in a class for bw compat)"""
-    BUTTON_OK     = _('button_ok')
-    BUTTON_APPLY  = _('button_apply')
-    BUTTON_CANCEL = _('button_cancel')
-    BUTTON_DELETE = _('button_delete')
-    YES = _('yes')
-    NO  = _('no')
+    BUTTON_OK     = (_('button_ok'), 'OK_ICON')
+    BUTTON_APPLY  = (_('button_apply'), 'APPLY_ICON')
+    BUTTON_CANCEL = (_('button_cancel'), 'CANCEL_ICON')
+    BUTTON_DELETE = (_('button_delete'), 'TRASH_ICON')
+    YES = (_('yes'), None)
+    NO  = (_('no'), None)
 
 
 def eid_param(name, eid):
--- a/web/component.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/component.py	Fri Nov 20 19:35:54 2009 +0100
@@ -40,14 +40,14 @@
                             help=_('display the component or not')),
         _('order'):    dict(type='Int', default=99,
                             help=_('display order of the component')),
-        _('context'):  dict(type='String', default='header',
+        _('context'):  dict(type='String', default='navtop',
                             vocabulary=(_('navtop'), _('navbottom'),
-                                        _('navcontenttop'), _('navcontentbottom')),
-                            #vocabulary=(_('header'), _('incontext'), _('footer')),
+                                        _('navcontenttop'), _('navcontentbottom'),
+                                        _('ctxtoolbar')),
                             help=_('context where this component should be displayed')),
     }
 
-    context = 'navcontentbottom' # 'footer' | 'header' | 'incontext'
+    context = 'navcontentbottom'
 
     def call(self, view=None):
         return self.cell_call(0, 0, view=view)
Binary file web/data/cancel.png has changed
--- a/web/data/cubicweb.ajax.js	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/data/cubicweb.ajax.js	Fri Nov 20 19:35:54 2009 +0100
@@ -62,6 +62,9 @@
     if (typeof roundedCorners != 'undefined') {
 	roundedCorners(node);
     }
+    if (typeof setFormsTarget != 'undefined') {
+       setFormsTarget(node);
+    }
     loadDynamicFragments(node);
     // XXX simulates document.ready, but the former
     // only runs once, this one potentially many times
@@ -139,14 +142,29 @@
     for(var i=0; i<fragments.length; i++) {
 	var fragment = fragments[i];
 	fragment.innerHTML = '<h3>' + LOADING_MSG + ' ... <img src="data/loading.gif" /></h3>';
+	// if cubicweb:loadurl is set, just pick the url et send it to loadxhtml
+	var url = getNodeAttribute(fragment, 'cubicweb:loadurl');
+	if (url) {
+	    jQuery(fragment).loadxhtml(url);
+	    continue;
+	}
+	// else: rebuild full url by fetching cubicweb:rql, cubicweb:vid, etc.
 	var rql = getNodeAttribute(fragment, 'cubicweb:rql');
-	var vid = getNodeAttribute(fragment, 'cubicweb:vid');
+	var items = getNodeAttribute(fragment, 'cubicweb:vid').split('&');
+	var vid = items[0];
         var extraparams = {};
+	// case where vid='myvid&param1=val1&param2=val2': this is a deprecated abuse-case
+	if (items.length > 1) {
+	    console.log("[3.5] you're using extraargs in cubicweb:vid attribute, this is deprecated, consider using loadurl instead");
+	    for (var j=1; j<items.length; j++) {
+		var keyvalue = items[j].split('=');
+		extraparams[keyvalue[0]] = keyvalue[1];
+	    }
+	}
 	var actrql = getNodeAttribute(fragment, 'cubicweb:actualrql');
 	if (actrql) { extraparams['actualrql'] = actrql; }
 	var fbvid = getNodeAttribute(fragment, 'cubicweb:fallbackvid');
 	if (fbvid) { extraparams['fallbackvid'] = fbvid; }
-
 	replacePageChunk(fragment.id, rql, vid, extraparams);
     }
 }
--- a/web/data/cubicweb.css	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/data/cubicweb.css	Fri Nov 20 19:35:54 2009 +0100
@@ -237,19 +237,6 @@
   width: 100%;
 }
 
-a.help{
-  display: block;
-  margin: 0px 5px 0px 8px;
-  height: 17px;
-  width: 17px;
-  background: url('help.png') 0% 0% no-repeat;
-}
-
-a.help:hover {
-  background-position: 0px -16px;
-  text-decoration: none;
-}
-
 /* FIXME appear with 4px width in IE6 */
 div#stateheader{
   min-width: 66%;
@@ -508,6 +495,7 @@
 div#userActionsBox a.popupMenu {
   color: black;
   text-decoration: underline;
+  padding-right: 2em;
 }
 
 /* download box XXX move to its own file? */
@@ -595,20 +583,23 @@
 
 /* basic entity view */
 
-div.row {
- clear: both;
- padding-bottom:0.4px
-}
-
-div.row span.label{
- padding-right:1em
+tr.entityfield th {
+  text-align: left;
+  padding-right: 0.5em;
 }
 
 div.field {
-  margin-left: 0.2em;
   display: inline;
 }
 
+div.ctxtoolbar {
+  float: right;
+  padding-left: 24px;
+  position: relative;
+}
+div.toolbarButton {
+  display: inline;
+}
 
 /***************************************/
 /* messages                            */
--- a/web/data/cubicweb.edition.js	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/data/cubicweb.edition.js	Fri Nov 20 19:35:54 2009 +0100
@@ -363,6 +363,9 @@
 	}
       return true;
     }
+    if (onfailure && !onfailure(result, formid, cbargs)) {
+	return false;
+    }
     unfreezeFormButtons(formid);
     // Failures
     _clearPreviousErrors(formid);
@@ -376,9 +379,6 @@
     _displayValidationerrors(formid, descr[0], descr[1]);
     updateMessage(_('please correct errors below'));
     document.location.hash = '#header';
-    if (onfailure) {
-	onfailure(formid, cbargs);
-    }
     return false;
 }
 
@@ -416,8 +416,9 @@
  * NOTE3: there is a XHTML module allowing iframe elements but there
  *        is still the problem of the form's `target` attribute
  */
-function setFormsTarget() {
-    jQuery('form.entityForm').each(function () {
+function setFormsTarget(node) {
+    var $node = jQuery(node || document.body);
+    $node.find('form.entityForm').each(function () {
 	var form = jQuery(this);
 	var target = form.attr('cubicweb:target');
 	if (target) {
@@ -431,7 +432,7 @@
     });
 }
 
-jQuery(document).ready(setFormsTarget);
+jQuery(document).ready(function() {setFormsTarget();});
 
 
 /*
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.fckcwconfig-full.js	Fri Nov 20 19:35:54 2009 +0100
@@ -0,0 +1,34 @@
+// cf /usr/share/fckeditor/fckconfig.js
+
+FCKConfig.AutoDetectLanguage	= false ;
+
+FCKConfig.ToolbarSets["Default"] = [
+    // removed : 'Save','NewPage','DocProps','-','Templates','-','Preview'
+	['Source'],
+    // removed: 'Print','-','SpellCheck'
+	['Cut','Copy','Paste','PasteText','PasteWord'],
+	['Undo','Redo','-','Find','Replace','-','SelectAll','RemoveFormat'],
+    //['Form','Checkbox','Radio','TextField','Textarea','Select','Button','ImageButton','HiddenField'],
+	'/',
+    // ,'StrikeThrough','-','Subscript','Superscript'
+	['Bold','Italic','Underline'],
+    // ,'-','Outdent','Indent','Blockquote'
+	['OrderedList','UnorderedList'],
+    // ['JustifyLeft','JustifyCenter','JustifyRight','JustifyFull'],
+	['Link','Unlink','Anchor'],
+    // removed : 'Image','Flash','Smiley','PageBreak'
+	['Table','Rule','SpecialChar']
+    // , '/',
+    // ['Style','FontFormat','FontName','FontSize'],
+    // ['TextColor','BGColor'],
+    //,'ShowBlocks'
+    // ['FitWindow','-','About']		// No comma for the last row.
+] ;
+
+// 'Flash','Select','Textarea','Checkbox','Radio','TextField','HiddenField','ImageButton','Button','Form',
+FCKConfig.ContextMenu = ['Generic','Link','Anchor','Image','BulletedList','NumberedList','Table'] ;
+
+FCKConfig.LinkUpload = false ;
+FCKConfig.ImageUpload = false ;
+FCKConfig.FlashUpload = false ;
+
--- a/web/data/cubicweb.fckcwconfig.js	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/data/cubicweb.fckcwconfig.js	Fri Nov 20 19:35:54 2009 +0100
@@ -1,34 +1,15 @@
-// cf /usr/share/fckeditor/fckconfig.js
-
-FCKConfig.AutoDetectLanguage	= false ;
-
-FCKConfig.ToolbarSets["Default"] = [
-    // removed : 'Save','NewPage','DocProps','-','Templates','-','Preview'
-	['Source'],
-    // removed: 'Print','-','SpellCheck'
-	['Cut','Copy','Paste','PasteText','PasteWord'],
-	['Undo','Redo','-','Find','Replace','-','SelectAll','RemoveFormat'],
-    //['Form','Checkbox','Radio','TextField','Textarea','Select','Button','ImageButton','HiddenField'],
-	'/',
-    // ,'StrikeThrough','-','Subscript','Superscript'
-	['Bold','Italic','Underline'],
-    // ,'-','Outdent','Indent','Blockquote'
-	['OrderedList','UnorderedList'],
-	['JustifyLeft','JustifyCenter','JustifyRight','JustifyFull'],
-	['Link','Unlink','Anchor'],
-    // removed : 'Image','Flash','Smiley','PageBreak'
-	['Table','Rule','SpecialChar'],
-	'/',
-	['Style','FontFormat','FontName','FontSize'],
-	['TextColor','BGColor'],
-    //,'ShowBlocks'
-	['FitWindow','-','About']		// No comma for the last row.
-] ;
-
-// 'Flash','Select','Textarea','Checkbox','Radio','TextField','HiddenField','ImageButton','Button','Form',
-FCKConfig.ContextMenu = ['Generic','Link','Anchor','Image','BulletedList','NumberedList','Table'] ;
-
-FCKConfig.LinkUpload = false ;
-FCKConfig.ImageUpload = false ;
-FCKConfig.FlashUpload = false ;
-
+
+FCKConfig.AutoDetectLanguage    = false ;
+
+FCKConfig.ToolbarSets["Default"] = [
+['Bold','Italic','Underline'],
+['OrderedList','UnorderedList'],
+['Link'],
+['Table']
+] ;
+// 'Flash','Select','Textarea','Checkbox','Radio','TextField','HiddenField','ImageButton','Button','Form',
+FCKConfig.ContextMenu = ['Generic','Link','Anchor','Image','BulletedList','NumberedList','Table'] ;
+
+FCKConfig.LinkUpload = false ;
+FCKConfig.ImageUpload = false ;
+FCKConfig.FlashUpload = false ;
--- a/web/data/cubicweb.form.css	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/data/cubicweb.form.css	Fri Nov 20 19:35:54 2009 +0100
@@ -12,7 +12,6 @@
   color: #ff4500;
   padding-bottom : 0.4em;
   text-transform: capitalize;
-  background: url("bg_trame_grise.png") left bottom repeat-x;
   margin-bottom: 0.6em
 }
 
@@ -209,10 +208,6 @@
   text-align: center;
 }
 
-div.trame_grise {
-  background: url("bg_trame_grise.png") left top repeat-x;
-}
-
 div.notice {
   display: none;
   font-style: italic;
@@ -231,7 +226,7 @@
   cursor: default;
 }
 
-input.validateButton {  
+input.validateButton {
   margin: 1em 1em 0px 0px;
   border: 1px solid #edecd2;
   border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
--- a/web/data/cubicweb.htmlhelpers.js	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/data/cubicweb.htmlhelpers.js	Fri Nov 20 19:35:54 2009 +0100
@@ -42,11 +42,11 @@
 }
 
 /* builds an url from an object (used as a dictionnary)
- * Notable difference with MochiKit's queryString: asURL does not
- * *url_quote* each value found in the dictionnary
  *
  * >>> asURL({'rql' : "RQL", 'x': [1, 2], 'itemvid' : "oneline"})
  * rql=RQL&vid=list&itemvid=oneline&x=1&x=2
+ * >>> asURL({'rql' : "a&b", 'x': [1, 2], 'itemvid' : "oneline"})
+ * rql=a%26b&x=1&x=2&itemvid=oneline
  */
 function asURL(props) {
     var chunks = [];
@@ -55,10 +55,10 @@
 	// generate a list of couple key=value if key is multivalued
 	if (isArrayLike(value)) {
 	    for (var i=0; i<value.length;i++) {
-		chunks.push(key + '=' + value[i]);
+		chunks.push(key + '=' + urlEncode(value[i]));
 	    }
 	} else {
-	    chunks.push(key + '=' + value);
+	    chunks.push(key + '=' + urlEncode(value));
 	}
     }
     return chunks.join('&');
--- a/web/data/external_resources	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/data/external_resources	Fri Nov 20 19:35:54 2009 +0100
@@ -38,6 +38,8 @@
 
 #FCKEDITOR_PATH = /usr/share/fckeditor/
 
+PUCE_UP = DATADIR/puce_up.png
+PUCE_DOWN = DATADIR/puce_down.png
 
 # icons for entity types
 BOOKMARK_ICON = DATADIR/icon_bookmark.gif
@@ -53,3 +55,8 @@
 UPLOAD_ICON = DATADIR/upload.gif
 GMARKER_ICON = DATADIR/gmap_blue_marker.png
 UP_ICON = DATADIR/up.gif
+
+OK_ICON = DATADIR/ok.png
+CANCEL_ICON = DATADIR/cancel.png
+APPLY_ICON = DATADIR/plus.png
+TRASH_ICON = DATADIR/trash_can_small.png
Binary file web/data/nomail.gif has changed
Binary file web/data/ok.png has changed
Binary file web/data/pen_icon.png has changed
Binary file web/data/plus.png has changed
Binary file web/data/puce_up.png has changed
Binary file web/data/trash_can.png has changed
Binary file web/data/trash_can_small.png has changed
--- a/web/formfields.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/formfields.py	Fri Nov 20 19:35:54 2009 +0100
@@ -81,6 +81,8 @@
        role of the entity in the relation (eg 'subject' or 'object')
     :fieldset:
        optional fieldset to which this field belongs to
+    :order:
+       key used by automatic forms to sort fields
 
     """
     # default widget associated to this class of fields. May be overriden per
@@ -94,7 +96,7 @@
     def __init__(self, name=None, id=None, label=None, help=None,
                  widget=None, required=False, initial=None,
                  choices=None, sort=True, internationalizable=False,
-                 eidparam=False, role='subject', fieldset=None):
+                 eidparam=False, role='subject', fieldset=None, order=None):
         self.name = name
         self.id = id or name
         self.label = label or name
@@ -108,6 +110,7 @@
         self.role = role
         self.fieldset = fieldset
         self.init_widget(widget)
+        self.order = order
         # ordering number for this field instance
         self.creation_rank = Field.__creation_rank
         Field.__creation_rank += 1
@@ -178,12 +181,7 @@
         renderer
         """
         widget = self.get_widget(form)
-        try:
-            return widget.render(form, self, renderer)
-        except TypeError:
-            warn('[3.3] %s: widget.render now take the renderer as third argument, '
-                 'please update implementation' % widget, DeprecationWarning)
-            return widget.render(form, self)
+        return widget.render(form, self, renderer)
 
     def vocabulary(self, form):
         """return vocabulary for this field. This method will be called by
@@ -203,7 +201,10 @@
         else:
             vocab = form.form_field_vocabulary(self)
         if self.internationalizable:
-            vocab = [(form._cw._(label), value) for label, value in vocab]
+            # the short-cirtcuit 'and' boolean operator is used here to permit
+            # a valid empty string in vocabulary without attempting to translate
+            # it by gettext (which can lead to weird strings display)
+            vocab = [(label and form._cw._(label), value) for label, value in vocab]
         if self.sort:
             vocab = vocab_sort(vocab)
         return vocab
--- a/web/formwidgets.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/formwidgets.py	Fri Nov 20 19:35:54 2009 +0100
@@ -13,6 +13,7 @@
 from cubicweb.common import tags, uilib
 from cubicweb.web import stdmsgs, INTERNAL_FIELD_VALUE, ProcessFormError
 
+from logilab.mtconverter import xml_escape
 
 class FieldWidget(object):
     """abstract widget class"""
@@ -472,7 +473,12 @@
                  setdomid=None, settabindex=None,
                  name='', value='', onclick=None, cwaction=None):
         super(Button, self).__init__(attrs, setdomid, settabindex)
-        self.label = label
+        if isinstance(label, tuple):
+            self.label = label[0]
+            self.icon = label[1]
+        else:
+            self.label = label
+            self.icon = None
         self.name = name
         self.value = ''
         self.onclick = onclick
@@ -494,7 +500,12 @@
                 attrs['id'] = self.name
         if self.settabindex and not 'tabindex' in attrs:
             attrs['tabindex'] = form._cw.next_tabindex()
-        return tags.input(value=label, type=self.type, **attrs)
+        if self.icon:
+            img = tags.img(src=form._cw.external_resource(self.icon), alt=self.icon)
+        else:
+            img = u''
+        return tags.button(img + xml_escape(label), escapecontent=False,
+                           value=label, type=self.type, **attrs)
 
 
 class SubmitButton(Button):
--- a/web/htmlwidgets.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/htmlwidgets.py	Fri Nov 20 19:35:54 2009 +0100
@@ -12,7 +12,8 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb.utils import UStringIO
-from cubicweb.common.uilib import toggle_action
+from cubicweb.common.uilib import toggle_action, limitsize, htmlescape
+from cubicweb.web import jsonize
 
 # XXX HTMLWidgets should have access to req (for datadir / static urls,
 #     i18n strings, etc.)
@@ -250,9 +251,46 @@
     def add_attr(self, attr, value):
         self.cell_attrs[attr] = value
 
+class SimpleTableModel(object):
+    """
+    uses a list of lists as a storage backend
+
+    NB: the model expectes the cellvid passed to
+    TableColumn.append_renderer to be a callable accepting a single
+    argument and returning a unicode object
+    """
+    def __init__(self, rows):
+        self._rows = rows
+
+
+    def get_rows(self):
+        return self._rows
+
+    def render_cell(self, cellvid, rowindex, colindex, w):
+        value = self._rows[rowindex][colindex]
+        w(cellvid(value))
+
+    @htmlescape
+    @jsonize
+    def sortvalue(self, rowindex, colindex):
+        value =  self._rows[rowindex][colindex]
+        if value is None:
+            return u''
+        elif isinstance(value, int):
+            return u'%09d'%value
+        else:
+            return unicode(value)
+
 
 class TableWidget(HTMLWidget):
+    """
+    Display data in a Table with sortable column.
 
+    When using remember to include the required css and js with:
+
+    self.req.add_js('jquery.tablesorter.js')
+    self.req.add_css(('cubicweb.tablesorter.css', 'cubicweb.tableview.css'))
+    """
     highlight = "onmouseover=\"addElementClass(this, 'highlighted');\" " \
                 "onmouseout=\"removeElementClass(this, 'highlighted');\""
 
@@ -304,20 +342,20 @@
 
     def _render(self):
         try:
-            pourcent = self.done*100./self.total
+            percent = self.done*100./self.total
         except ZeroDivisionError:
-            pourcent = 0
-        real_pourcent = pourcent
-        if pourcent > 100 :
+            percent = 0
+        real_percent = percent
+        if percent > 100 :
             color = 'done'
-            pourcent = 100
+            percent = 100
         elif self.todo + self.done > self.total :
             color = 'overpassed'
         else:
             color = 'inprogress'
-        if pourcent < 0:
-            pourcent = 0
-        self.w(u'<div class="progressbarback" title="%i %%">' % real_pourcent)
-        self.w(u'<div class="progressbar %s" style="width: %spx; align: left;" ></div>' % (color, pourcent))
+        if percent < 0:
+            percent = 0
+        self.w(u'<div class="progressbarback" title="%i %%">' % real_percent)
+        self.w(u'<div class="progressbar %s" style="width: %spx; align: left;" ></div>' % (color, percent))
         self.w(u'</div>')
 
--- a/web/request.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/request.py	Fri Nov 20 19:35:54 2009 +0100
@@ -27,7 +27,7 @@
 from cubicweb.dbapi import DBAPIRequest
 from cubicweb.common.mail import header
 from cubicweb.common.uilib import remove_html_tags
-from cubicweb.utils import SizeConstrainedList, HTMLHead
+from cubicweb.utils import SizeConstrainedList, HTMLHead, make_uid
 from cubicweb.view import STRICT_DOCTYPE, TRANSITIONAL_DOCTYPE_NOEXT
 from cubicweb.web import (INTERNAL_FIELD_VALUE, LOGGER, NothingToEdit,
                           RequestError, StatusResponse)
@@ -87,7 +87,20 @@
         self.next_tabindex = self.tabindexgen.next
         # page id, set by htmlheader template
         self.pageid = None
+        self.varmaker = rqlvar_maker()
         self.datadir_url = self._datadir_url()
+        self._set_pageid()
+
+    def _set_pageid(self):
+        """initialize self.pageid
+        if req.form provides a specific pageid, use it, otherwise build a
+        new one.
+        """
+        pid = self.form.get('pageid')
+        if pid is None:
+            pid = make_uid(id(self))
+        self.pageid = pid
+        self.html_headers.define_var('pageid', pid, override=False)
 
     @property
     def varmaker(self):
--- a/web/uicfg.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/uicfg.py	Fri Nov 20 19:35:54 2009 +0100
@@ -113,30 +113,24 @@
 from cubicweb.web import formwidgets
 
 
-def card_from_role(card, role):
-    if role == 'subject':
-        return card[0]
-    assert role in ('object', 'sobject'), repr(role)
-    return card[1]
-
 # primary view configuration ##################################################
 
 def init_primaryview_section(rtag, sschema, rschema, oschema, role):
     if rtag.get(sschema, rschema, oschema, role) is None:
-        card = card_from_role(rschema.rproperty(sschema, oschema, 'cardinality'), role)
-        composed = rschema.rproperty(sschema, oschema, 'composite') == neg_role(role)
+        rdef = rschema.rdef(sschema, oschema)
         if rschema.final:
             if rschema.meta or sschema.is_metadata(rschema) \
                     or oschema.type in ('Password', 'Bytes'):
                 section = 'hidden'
             else:
                 section = 'attributes'
-        elif card in '1+':
-            section = 'attributes'
-        elif composed:
-            section = 'relations'
         else:
-            section = 'sideboxes'
+            if rdef.role_cardinality(role) in '1+':
+                section = 'attributes'
+            elif rdef.composite == neg_role(role):
+                section = 'relations'
+            else:
+                section = 'sideboxes'
         rtag.tag_relation((sschema, rschema, oschema, role), section)
 
 primaryview_section = RelationTags('primaryview_section',
@@ -200,6 +194,8 @@
 
     def init(self, schema, check=True):
         for eschema in schema.entities():
+            if eschema.final:
+                continue
             if eschema.schema_entity():
                 self.setdefault(eschema, 'schema')
             elif eschema.is_subobject(strict=True):
@@ -357,12 +353,18 @@
         for rschema, targetschemas, role in eschema.relation_definitions(True):
             # check category first, potentially lower cost than checking
             # permission which may imply rql queries
-            if tag is not None:
-                targetschemas = [tschema for tschema in targetschemas
-                                 if tag in self.etype_get(eschema, rschema,
-                                                          role, tschema)]
-            if not targetschemas:
+            _targetschemas = []
+            for tschema in targetschemas:
+                if not rtags.etype_get(eschema, rschema, role, tschema) in categories:
+                        continue
+                    rdef = rschema.role_rdef(eschema, tschema, role)
+                    if not ((not strict and rdef.has_local_role(permission)) or
+                            rdef.has_perm(entity.req, permission, fromeid=eid)):
+                        continue
+                    _targetschemas.append(tschema)
+            if not _targetschemas:
                 continue
+            targetschemas = _targetschemas
             if permission is not None:
                 # tag allowing to hijack the permission machinery when
                 # permission is not verifiable until the entity is actually
@@ -371,12 +373,9 @@
                     yield (rschema, targetschemas, role)
                     continue
                 if rschema.final:
-                    if not rschema.has_perm(entity._cw, permission, eid):
+                    if not eschema.rdef(rschema).has_perm(entity._cw, permission, eid):
                         continue
                 elif role == 'subject':
-                    if not ((not strict and rschema.has_local_role(permission)) or
-                            rschema.has_perm(entity._cw, permission, fromeid=eid)):
-                        continue
                     # on relation with cardinality 1 or ?, we need delete perm as well
                     # if the relation is already set
                     if (permission == 'add'
@@ -386,9 +385,6 @@
                                                  toeid=entity.related(rschema.type, role)[0][0])):
                         continue
                 elif role == 'object':
-                    if not ((not strict and rschema.has_local_role(permission)) or
-                            rschema.has_perm(entity._cw, permission, toeid=eid)):
-                        continue
                     # on relation with cardinality 1 or ?, we need delete perm as well
                     # if the relation is already set
                     if (permission == 'add'
@@ -421,9 +417,8 @@
         if rschema in META_RTYPES:
             rtag.tag_relation((sschema, rschema, oschema, role), False)
             return
-        card = rschema.rproperty(sschema, oschema, 'cardinality')[role == 'object']
-        if not card in '?1' and \
-               rschema.rproperty(sschema, oschema, 'composite') == role:
+        rdef = rschema.rdef(sschema, oschema)
+        if not rdef.role_cardinality(role) in '?1' and rdef.composite == role:
             rtag.tag_relation((sschema, rschema, oschema, role), True)
 
 actionbox_appearsin_addmenu = RelationTagsBool('actionbox_appearsin_addmenu',
--- a/web/views/actions.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/actions.py	Fri Nov 20 19:35:54 2009 +0100
@@ -272,20 +272,21 @@
             for rschema in rschemas:
                 if rschema.final:
                     continue
-                # check the relation can be added as well
-                # XXX consider autoform_permissions_overrides?
-                if role == 'subject'and not rschema.has_perm(req, 'add',
-                                                             fromeid=entity.eid):
-                    continue
-                if role == 'object'and not rschema.has_perm(req, 'add',
-                                                            toeid=entity.eid):
-                    continue
-                # check the target types can be added as well
                 for teschema in rschema.targets(eschema, role):
                     if not appearsin_addmenu.etype_get(eschema, rschema,
                                                        role, teschema):
                         continue
-                    if teschema.has_local_role('add') or teschema.has_perm(req, 'add'):
+                    rdef = rschema.role_rdef(eschema, teschema, role)
+                    # check the relation can be added
+                    # XXX consider autoform_permissions_overrides?
+                    if role == 'subject'and not rdef.has_perm(
+                        req, 'add', fromeid=entity.eid):
+                        continue
+                    if role == 'object'and not rdef.has_perm(
+                        req, 'add', toeid=entity.eid):
+                        continue
+                    # check the target types can be added as well
+                    if teschema.may_have_permission('add', req):
                         yield rschema, teschema, role
 
     def linkto_url(self, entity, rtype, etype, target):
--- a/web/views/autoform.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/autoform.py	Fri Nov 20 19:35:54 2009 +0100
@@ -50,6 +50,94 @@
 
     # class methods mapping schema relations to fields in the form ############
 
+<<<<<<< /home/syt/src/fcubicweb/cubicweb/web/views/autoform.py
+=======
+    @classmethod
+    def erelations_by_category(cls, entity, categories=None, permission=None,
+                               rtags=None, strict=False):
+        """return a list of (relation schema, target schemas, role) matching
+        categories and permission
+
+        `strict`:
+          bool telling if having local role is enough (strict = False) or not
+        """
+        if categories is not None:
+            if not isinstance(categories, (list, tuple, set, frozenset)):
+                categories = (categories,)
+            if not isinstance(categories, (set, frozenset)):
+                categories = frozenset(categories)
+        eschema  = entity.e_schema
+        if rtags is None:
+            rtags = cls.rcategories
+        permsoverrides = cls.rpermissions_overrides
+        if entity.has_eid():
+            eid = entity.eid
+        else:
+            eid = None
+            strict = False
+        for rschema, targetschemas, role in eschema.relation_definitions(True):
+            # check category first, potentially lower cost than checking
+            # permission which may imply rql queries
+            if categories is not None:
+                _targetschemas = []
+                for tschema in targetschemas:
+                    if not rtags.etype_get(eschema, rschema, role, tschema) in categories:
+                        continue
+                    rdef = rschema.role_rdef(eschema, tschema, role)
+                    if not ((not strict and rdef.has_local_role(permission)) or
+                            rdef.has_perm(entity.req, permission, fromeid=eid)):
+                        continue
+                    _targetschemas.append(tschema)
+                if not _targetschemas:
+                    continue
+                targetschemas = _targetschemas
+            if permission is not None:
+                # tag allowing to hijack the permission machinery when
+                # permission is not verifiable until the entity is actually
+                # created...
+                if eid is None and '%s_on_new' % permission in permsoverrides.etype_get(eschema, rschema, role):
+                    yield (rschema, targetschemas, role)
+                    continue
+                if rschema.final:
+                    if not eschema.rdef(rschema).has_perm(entity.req, permission, eid=eid):
+                        continue
+                elif role == 'subject':
+                    # on relation with cardinality 1 or ?, we need delete perm as well
+                    # if the relation is already set
+                    if (permission == 'add'
+                        and rschema.rdef(eschema, targetschemas[0]).role_cardinality(role) in '1?'
+                        and eid and entity.related(rschema.type, role)
+                        and not rschema.has_perm(entity.req, 'delete', fromeid=eid,
+                                                 toeid=entity.related(rschema.type, role)[0][0])):
+                        continue
+                elif role == 'object':
+                    # on relation with cardinality 1 or ?, we need delete perm as well
+                    # if the relation is already set
+                    if (permission == 'add'
+                        and rschema.rdef(targetschemas[0], eschema).role_cardinality(role) in '1?'
+                        and eid and entity.related(rschema.type, role)
+                        and not rschema.has_perm(entity.req, 'delete', toeid=eid,
+                                                 fromeid=entity.related(rschema.type, role)[0][0])):
+                        continue
+            yield (rschema, targetschemas, role)
+
+    @classmethod
+    def esrelations_by_category(cls, entity, categories=None, permission=None,
+                                strict=False):
+        """filter out result of relations_by_category(categories, permission) by
+        removing final relations
+
+        return a sorted list of (relation's label, relation'schema, role)
+        """
+        result = []
+        for rschema, ttypes, role in cls.erelations_by_category(
+            entity, categories, permission, strict=strict):
+            if rschema.final:
+                continue
+            result.append((rschema.display_name(entity.req, role), rschema, role))
+        return sorted(result)
+
+>>>>>>> /tmp/autoform.py~other.rHDQ-C
     @iclassmethod
     def field_by_name(cls_or_self, name, role='subject', eschema=None):
         """return field with the given name and role. If field is not explicitly
@@ -101,6 +189,8 @@
                     continue
         self.maxrelitems = self._cw.property_value('navigation.related-limit')
         self.force_display = bool(self._cw.form.get('__force_display'))
+        fnum = len(self.fields)
+        self.fields.sort(key=lambda f: f.order is None and fnum or f.order)
 
     @property
     def related_limit(self):
--- a/web/views/basecomponents.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/basecomponents.py	Fri Nov 20 19:35:54 2009 +0100
@@ -215,23 +215,30 @@
         self.w(u'&#160;|&#160;'.join(html))
         self.w(u'</div>')
 
-class PdfViewComponent(component.Component):
+
+class PdfViewComponent(component.EntityVComponent):
     __regid__ = 'pdfview'
     __select__ = yes()
 
-    context = 'header'
-    cw_property_defs = {
-        _('visible'):  dict(type='Boolean', default=True,
-                            help=_('display the pdf icon or not')),
-    }
+    context = 'ctxtoolbar'
 
-    def call(self, vid):
-        entity = self.entity(0,0)
-        url = entity.absolute_url(vid=vid, __template='pdf-main-template')
-        self.w(u'<a href="%s" class="otherView"><img src="data/pdf_icon.gif" alt="%s"/></a>' %
-               (xml_escape(url), self._cw._('download page as pdf')))
+    def cell_call(self, row, col, view):
+        entity = self.entity(row, col)
+        url = entity.absolute_url(vid=view.id, __template='pdf-main-template')
+        iconurl = self.req.build_url('data/pdf_icon.gif')
+        label = self.req._('Download page as pdf')
+        self.w(u'<a href="%s" title="%s" class="toolbarButton"><img src="%s" alt="%s"/></a>' %
+               (xml_escape(url), label, iconurl, label))
 
 
+class MetaDataComponent(component.EntityVComponent):
+    id = 'metadata'
+    context = 'navbottom'
+    order = 1
+
+    def cell_call(self, row, col, view=None):
+        self.wview('metadata', self.rset, row=row, col=col)
+
 
 def registration_callback(vreg):
     vreg.register_all(globals().values(), __name__, (SeeAlsoVComponent,))
--- a/web/views/basecontrollers.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/basecontrollers.py	Fri Nov 20 19:35:54 2009 +0100
@@ -131,8 +131,8 @@
                 req.set_message(req._("You have no access to this view or it can not "
                                       "be used to display the current data."))
             self.warning("the view %s can not be applied to this query", vid)
-            vid = vid_from_rset(req, rset, self._cw.vreg.schema)
-            view = self._cw.vreg['views'].select(vid, req, rset=rset)
+            vid = req.form.get('fallbackvid') or vid_from_rset(req, rset, req.vreg.schema)
+            view = req.vreg['views'].select(vid, req, rset=rset)
         return view, rset
 
     def add_to_breadcrumbs(self, view):
@@ -249,8 +249,12 @@
         note: it's the responsability of js_* methods to set the correct
         response content type
         """
+<<<<<<< /home/syt/src/fcubicweb/cubicweb/web/views/basecontrollers.py
         self._cw.json_request = True
         self._cw.pageid = self._cw.form.get('pageid')
+=======
+        self.req.json_request = True
+>>>>>>> /tmp/basecontrollers.py~other.K2YVFx
         try:
             fname = self._cw.form['fname']
             func = getattr(self, 'js_%s' % fname)
--- a/web/views/basetemplates.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/basetemplates.py	Fri Nov 20 19:35:54 2009 +0100
@@ -13,7 +13,7 @@
 from cubicweb.appobject import objectify_selector
 from cubicweb.selectors import match_kwargs
 from cubicweb.view import View, MainTemplate, NOINDEX, NOFOLLOW
-from cubicweb.utils import make_uid, UStringIO, can_do_pdf_conversion
+from cubicweb.utils import UStringIO, can_do_pdf_conversion
 from cubicweb.schema import display_name
 
 # main templates ##############################################################
@@ -315,7 +315,6 @@
         self.stylesheets()
         self.javascripts()
         self.alternates()
-        self.pageid()
 
     def favicon(self):
         favicon = self._cw.external_resource('FAVICON', None)
@@ -343,12 +342,6 @@
             self.whead(u'<link rel="alternate" type="application/rss+xml" title="RSS feed" href="%s"/>\n'
                        %  xml_escape(urlgetter.feed_url()))
 
-    def pageid(self):
-        req = self._cw
-        pid = make_uid(id(req))
-        req.pageid = pid
-        req.html_headers.define_var('pageid', pid)
-
 
 class HTMLPageHeader(View):
     """default html page header"""
@@ -387,11 +380,6 @@
             'loggeduserlink', self._cw, rset=self.cw_rset)
         if comp and comp.cw_propval('visible'):
             comp.render(w=self.w)
-        self.w(u'</td><td>')
-        helpcomp = self._cw.vreg['components'].select_or_none(
-            'help', self._cw, rset=self.cw_rset)
-        if helpcomp and helpcomp.cw_propval('visible'):
-            helpcomp.render(w=self.w)
         self.w(u'</td>')
         # lastcolumn
         self.w(u'<td id="lastcolumn">')
@@ -417,7 +405,7 @@
 
 
 class HTMLPageFooter(View):
-    """default html page footer: include logo if any, and close the HTML body
+    """default html page footer: include footer actions
     """
     __regid__ = 'footer'
 
--- a/web/views/baseviews.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/baseviews.py	Fri Nov 20 19:35:54 2009 +0100
@@ -174,7 +174,7 @@
         entity = self.cw_rset.get_entity(row, col)
         self.w(u'<div class="metadata">')
         if self.show_eid:
-            self.w(u'#%s - ' % entity.eid)
+            self.w(u'%s #%s - ' % (entity.dc_type(), entity.eid))
         if entity.modification_date != entity.creation_date:
             self.w(u'<span>%s</span> ' % _('latest update on'))
             self.w(u'<span class="value">%s</span>, '
--- a/web/views/cwuser.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/cwuser.py	Fri Nov 20 19:35:54 2009 +0100
@@ -16,6 +16,14 @@
 
 uicfg.primaryview_section.tag_attribute(('CWUser', 'login'), 'hidden')
 
+uicfg.primaryview_section.tag_attribute(('CWGroup', 'name'), 'hidden')
+uicfg.primaryview_section.tag_subject_of(('CWGroup', 'read_permission', '*'), 'relations')
+uicfg.primaryview_section.tag_subject_of(('CWGroup', 'add_permission', '*'), 'relations')
+uicfg.primaryview_section.tag_subject_of(('CWGroup', 'delete_permission', '*'), 'relations')
+uicfg.primaryview_section.tag_subject_of(('CWGroup', 'update_permission', '*'), 'relations')
+uicfg.primaryview_section.tag_object_of(('*', 'in_group', 'CWGroup'), 'relations')
+uicfg.primaryview_section.tag_object_of(('*', 'require_group', 'CWGroup'), 'relations')
+
 class UserPreferencesEntityAction(action.Action):
     __regid__ = 'prefs'
     __select__ = (one_line_rset() & implements('CWUser') &
--- a/web/views/editforms.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/editforms.py	Fri Nov 20 19:35:54 2009 +0100
@@ -51,8 +51,8 @@
 
     domid = 'deleteconf'
     copy_nav_params = True
-    form_buttons = [Button(stdmsgs.YES, cwaction='delete'),
-                    Button(stdmsgs.NO, cwaction='cancel')]
+    form_buttons = [Button(stdmsgs.BUTTON_DELETE, cwaction='delete'),
+                    Button(stdmsgs.BUTTON_CANCEL, cwaction='cancel')]
     @property
     def action(self):
         return self._cw.build_url('edit')
@@ -113,7 +113,7 @@
     _onsubmit = ("return inlineValidateRelationForm('%(rtype)s', '%(role)s', '%(eid)s', "
                  "'%(divid)s', %(reload)s, '%(vid)s', '%(default)s', '%(lzone)s');")
     _cancelclick = "hideInlineEdit(%s,\'%s\',\'%s\')"
-    _defaultlandingzone = (u'<img title="%(msg)s" src="data/file.gif" '
+    _defaultlandingzone = (u'<img title="%(msg)s" src="data/pen_icon.png" '
                            'alt="%(msg)s"/>')
     _landingzonemsg = _('click to edit this field')
     # default relation vids according to cardinality
@@ -197,14 +197,17 @@
         """
         w = self.w
         divid = form.event_args['divid']
-        w(u'<div id="%s-reledit" class="field">' % form.event_args['divid'])
-        w(u'<div id="%s" class="editableField" onclick="%s" title="%s">' % (
+        w(u'<div id="%s-reledit" class="field" '
+          u'onmouseout="addElementClass(jQuery(\'#%s\'), \'hidden\')" '
+          u'onmouseover="removeElementClass(jQuery(\'#%s\'), \'hidden\')">'
+          % (divid, divid, divid))
+        w(u'<div id="%s-value" class="editableFieldValue">%s</div>' % (divid, value))
+        w(form.form_render(renderer=renderer))
+        w(u'<div id="%s" class="editableField hidden" onclick="%s" title="%s">' % (
                 divid, xml_escape(self._onclick % form.event_args),
                 self.req._(self._landingzonemsg)))
         w(lzone)
         w(u'</div>')
-        w(u'<div id="%s-value" class="editableFieldValue">%s</div>' % (divid, value))
-        w(form.form_render(renderer=renderer))
         w(u'</div>')
 
     def _compute_best_vid(self, eschema, rschema, role):
@@ -269,10 +272,8 @@
         dispctrl = uicfg.primaryview_display_ctrl.etype_get(eschema, rtype, role)
         vid = dispctrl.get('vid', 'reledit')
         if vid != 'reledit': # reledit explicitly disabled
-            self.wview(vid, entity.related(rtype, role), 'null')
             return False
         if eschema.role_rproperty(role, rschema, 'composite') == role:
-            self.wview(rvid, entity.related(rtype, role), 'null')
             return False
         return super(AutoClickAndEditFormView, self).should_edit_relation(
             entity, rschema, role, rvid)
@@ -426,6 +427,7 @@
             form = self._cw.vreg['forms'].select('edition', self._cw,
                                                  rset=self.cw_rset, row=row,
                                                  formtype='muledit',
+                                                 copy_nav_params=False,
                                                  mainform=False)
             # XXX rely on the EntityCompositeFormRenderer to put the eid input
             form.remove_field(form.field_by_name('eid'))
@@ -442,7 +444,9 @@
         should be the eid
         """
         #self.form_title(entity)
-        form = self._cw.vreg['forms'].select(self.__regid__, self._cw, rset=self.cw_rset)
+        form = self._cw.vreg['forms'].select(self.__regid__, self._cw,
+                                             rset=self.cw_rset,
+                                             copy_nav_params=True)
         self.w(form.form_render())
 
 
@@ -577,4 +581,3 @@
         self.w(u'<a class="addEntity" id="add%s:%slink" href="javascript: %s" >+ %s.</a>'
           % (self.rtype, self.peid, js, __(i18nctx, 'add a %s' % self.etype)))
         self.w(u'</div>')
-        self.w(u'<div class="trame_grise">&#160;</div>')
--- a/web/views/forms.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/forms.py	Fri Nov 20 19:35:54 2009 +0100
@@ -89,6 +89,7 @@
         if mainform:
             self.form_add_hidden('__errorurl', self.session_key())
             self.form_add_hidden('__domid', self.domid)
+        # XXX why do we need two different variables (mainform and copy_nav_params ?)
         if self.copy_nav_params:
             for param in NAV_FORM_PARAMETERS:
                 if not param in kwargs:
--- a/web/views/ibreadcrumbs.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/ibreadcrumbs.py	Fri Nov 20 19:35:54 2009 +0100
@@ -30,22 +30,29 @@
     title = _('contentnavigation_breadcrumbs')
     help = _('contentnavigation_breadcrumbs_description')
     separator = u'&#160;&gt;&#160;'
+    link_template = u'<a href="%s">%s</a>'
 
     def call(self, view=None, first_separator=True):
         entity = self.cw_rset.get_entity(0, 0)
         path = entity.breadcrumbs(view)
         if path:
-            self.w(u'<span id="breadcrumbs" class="pathbar">')
+            self.open_breadcrumbs()
             if first_separator:
                 self.w(self.separator)
             self.render_breadcrumbs(entity, path)
-            self.w(u'</span>')
+            self.close_breadcrumbs()
+
+    def open_breadcrumbs(self):
+        self.w(u'<span id="breadcrumbs" class="pathbar">')
+
+    def close_breadcrumbs(self):
+        self.w(u'</span>')
 
     def render_breadcrumbs(self, contextentity, path):
         root = path.pop(0)
         if isinstance(root, Entity):
-            self.w(u'<a href="%s">%s</a>' % (self._cw.build_url(root.__regid__),
-                                             root.dc_type('plural')))
+            self.w(self.link_template % (self._cw.build_url(root.__regid__),
+                                         root.dc_type('plural')))
             self.w(self.separator)
         self.wpath_part(root, contextentity, not path)
         for i, parent in enumerate(path):
@@ -62,7 +69,7 @@
         elif isinstance(part, tuple):
             url, title = part
             textsize = self._cw.property_value('navigation.short-line-size')
-            self.w(u'<a href="%s">%s</a>' % (
+            self.w(self.link_template % (
                 xml_escape(url), xml_escape(uilib.cut(title, textsize))))
         else:
             textsize = self._cw.property_value('navigation.short-line-size')
--- a/web/views/idownloadable.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/idownloadable.py	Fri Nov 20 19:35:54 2009 +0100
@@ -25,7 +25,7 @@
         return 0
     return 1
 
-def download_box(w, entity, title=None, label=None):
+def download_box(w, entity, title=None, label=None, footer=u''):
     req = entity._cw
     w(u'<div class="sideBox">')
     if title is None:
@@ -37,8 +37,8 @@
       % (xml_escape(entity.download_url()),
          req.external_resource('DOWNLOAD_ICON'),
          _('download icon'), xml_escape(label or entity.dc_title())))
-    w(u'</div>')
-    w(u'</div>\n</div>\n')
+    w(u'%s</div>' % footer)
+    w(u'</div></div>\n')
 
 
 class DownloadBox(EntityBoxTemplate):
@@ -113,7 +113,8 @@
             except TransformError:
                 pass
             except Exception, ex:
-                msg = self._cw._("can't display data, unexpected error: %s") % ex
+                msg = self._cw._("can't display data, unexpected error: %s") \
+                      % xml_escape(str(ex))
                 self.w('<div class="error">%s</div>' % msg)
         self.w(u'</div>')
 
--- a/web/views/owl.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/owl.py	Fri Nov 20 19:35:54 2009 +0100
@@ -71,10 +71,15 @@
         if writeprefix:
             self.w(OWL_CLOSING_ROOT)
 
+<<<<<<< /home/syt/src/fcubicweb/cubicweb/web/views/owl.py
     def should_display_rschema(self, rschema):
         return not rschema in self.skiptypes and (
             rschema.has_local_role('read') or
             rschema.has_perm(self._cw, 'read'))
+=======
+    def should_display_rschema(self, eschema, rschema, tschemas, role):
+        return rschema.may_have_permissions('read', self.req, eschema, role)
+>>>>>>> /tmp/owl.py~other.-maWGS
 
     def visit_schema(self, skiptypes):
         """get a layout for a whole schema"""
@@ -94,7 +99,7 @@
         self.w(u'<owl:Class rdf:ID="%s">'% eschema)
         self.w(u'<!-- relations -->')
         for rschema, targetschemas, role in eschema.relation_definitions():
-            if not self.should_display_rschema(rschema):
+            if not self.should_display_rschema(eschema, rschema, targetschemas, role):
                 continue
             for oeschema in targetschemas:
                 if role == 'subject':
@@ -112,7 +117,7 @@
 
         self.w(u'<!-- attributes -->')
         for rschema, aschema in eschema.attribute_definitions():
-            if not self.should_display_rschema(rschema):
+            if not self.should_display_rschema(eschema, rschema, (aschema,), 'subject'):
                 continue
             self.w(u'''<rdfs:subClassOf>
   <owl:Restriction>
@@ -125,7 +130,7 @@
     def visit_property_schema(self, eschema):
         """get a layout for property entity OWL schema"""
         for rschema, targetschemas, role in eschema.relation_definitions():
-            if not self.should_display_rschema(rschema):
+            if not self.should_display_rschema(eschema, rschema, targetschemas, role):
                 continue
             for oeschema in targetschemas:
                 self.w(u'''<owl:ObjectProperty rdf:ID="%s">
@@ -135,7 +140,7 @@
 
     def visit_property_object_schema(self, eschema):
         for rschema, aschema in eschema.attribute_definitions():
-            if not self.should_display_rschema(rschema):
+            if not self.should_display_rschema(eschema, rschema, (aschema,), 'subject'):
                 continue
             self.w(u'''<owl:DatatypeProperty rdf:ID="%s">
   <rdfs:domain rdf:resource="#%s"/>
@@ -174,7 +179,8 @@
         for rschema, aschema in eschema.attribute_definitions():
             if rschema.meta:
                 continue
-            if not (rschema.has_local_role('read') or rschema.has_perm(self._cw, 'read')):
+            rdef = rschema.rdef(eschema, aschema)
+            if not rdef.may_have_permission('read', self._cw):
                 continue
             aname = rschema.type
             if aname == 'eid':
@@ -189,7 +195,12 @@
         for rschema, targetschemas, role in eschema.relation_definitions():
             if rschema.meta:
                 continue
-            if not (rschema.has_local_role('read') or rschema.has_perm(self._cw, 'read')):
+            for tschema in targetschemas:
+                rdef = rschema.role_rdef(eschema, tschema, role)
+                if rdef.may_have_permission('read', self.req):
+                    break
+            else:
+                # no read perms to any relation of this type. Skip.
                 continue
             if role == 'object':
                 attr = 'reverse_%s' % rschema.type
--- a/web/views/plots.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/plots.py	Fri Nov 20 19:35:54 2009 +0100
@@ -118,7 +118,8 @@
                                     {'plotdefs': '\n'.join(plotdefs),
                                      'figid': figid,
                                      'plotdata': ','.join(plotdata),
-                                     'mode': self.timemode and "'time'" or 'null'})
+                                     'mode': self.timemode and "'time'" or 'null'},
+                                    jsoncall=req.form.get('jsoncall', False))
 
 
 class PlotView(baseviews.AnyRsetView):
--- a/web/views/primary.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/primary.py	Fri Nov 20 19:35:54 2009 +0100
@@ -46,7 +46,8 @@
 
     def render_entity(self, entity):
         self.render_entity_title(entity)
-        self.render_entity_metadata(entity)
+        # XXX uncomment this in 3.6
+        #self.render_entity_toolbox(entity)
         # entity's attributes and relations, excluding meta data
         # if the entity isn't meta itself
         boxes = self._prepare_side_boxes(entity)
@@ -87,10 +88,13 @@
         """default implementation return dc_title"""
         title = xml_escape(entity.dc_title())
         if title:
-            self.w(u'<h1><span class="etype">%s</span> %s</h1>'
-                   % (entity.dc_type().capitalize(), title))
+            self.w(u'<h1>%s</h1>' % title)
+
+    def render_entity_toolbox(self, entity):
+        self.content_navigation_components('ctxtoolbar')
 
     def render_entity_metadata(self, entity):
+        # XXX deprecated
         entity.view('metadata', w=self.w)
 
     def render_entity_summary(self, entity):
@@ -103,7 +107,11 @@
         return u''
 
     def render_entity_attributes(self, entity, siderelations=None):
-        for rschema, tschemas, role, dispctrl in self._section_def(entity, 'attributes'):
+        entity_attributes = self._section_def(entity, 'attributes')
+        if not entity_attributes:
+            return
+        self.w(u'<table>')
+        for rschema, tschemas, role, dispctrl in entity_attributes:
             vid = dispctrl.get('vid', 'reledit')
             if rschema.final or vid == 'reledit':
                 value = entity.view(vid, rtype=rschema.type, role=role)
@@ -115,7 +123,8 @@
                     value = None
             if self.skip_none and (value is None or value == ''):
                 continue
-            self._render_attribute(rschema, value)
+            self._render_attribute(rschema, value, role=role, table=True)
+        self.w(u'</table>')
 
     def render_entity_relations(self, entity, siderelations=None):
         for rschema, tschemas, role, dispctrl in self._section_def(entity, 'relations'):
@@ -191,13 +200,13 @@
                    initargs={'dispctrl': dispctrl})
         self.w(u'</div>')
 
-    def _render_attribute(self, rschema, value, role='subject'):
+    def _render_attribute(self, rschema, value, role='subject', table=False):
         if rschema.final:
             show_label = self.show_attr_label
         else:
             show_label = self.show_rel_label
         label = display_name(self._cw, rschema.type, role)
-        self.field(label, value, show_label=show_label, tr=False)
+        self.field(label, value, show_label=show_label, tr=False, table=table)
 
 
 class RelatedView(EntityView):
@@ -223,7 +232,7 @@
         # else show links to display related entities
         else:
             rql = self.cw_rset.printable_rql()
-            self.cw_rset.limit(maxrelated) # remove extra entity
+            self.cw_rset.limit(limit) # remove extra entity
             self.w(u'<div>')
             self.wview('simplelist', self.cw_rset)
             self.w(u'[<a href="%s">%s</a>]' % (self._cw.build_url(rql=rql),
@@ -232,18 +241,20 @@
 
 ## default primary ui configuration ###########################################
 
+_pvs = uicfg.primaryview_section
 for rtype in ('eid', 'creation_date', 'modification_date', 'cwuri',
               'is', 'is_instance_of', 'identity',
               'owned_by', 'created_by', 'in_state',
               'wf_info_for', 'by_transition', 'from_state', 'to_state',
               'require_permission', 'from_entity', 'to_entity',
               'see_also'):
-    uicfg.primaryview_section.tag_subject_of(('*', rtype, '*'), 'hidden')
-    uicfg.primaryview_section.tag_object_of(('*', rtype, '*'), 'hidden')
-uicfg.primaryview_section.tag_subject_of(('*', 'use_email', '*'), 'attributes')
-uicfg.primaryview_section.tag_subject_of(('*', 'primary_email', '*'), 'hidden')
+    _pvs.tag_subject_of(('*', rtype, '*'), 'hidden')
+    _pvs.tag_object_of(('*', rtype, '*'), 'hidden')
+
+_pvs.tag_subject_of(('*', 'use_email', '*'), 'attributes')
+_pvs.tag_subject_of(('*', 'primary_email', '*'), 'hidden')
 
 for attr in ('name', 'final'):
-    uicfg.primaryview_section.tag_attribute(('CWEType', attr), 'hidden')
+    _pvs.tag_attribute(('CWEType', attr), 'hidden')
 for attr in ('name', 'final', 'symetric', 'inlined'):
-    uicfg.primaryview_section.tag_attribute(('CWRType', attr), 'hidden')
+    _pvs.tag_attribute(('CWRType', attr), 'hidden')
--- a/web/views/schema.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/schema.py	Fri Nov 20 19:35:54 2009 +0100
@@ -358,13 +358,11 @@
 
     def should_display_schema(self, rschema):
         return (super(RestrictedSchemaVisitorMixIn, self).should_display_schema(rschema)
-                and (rschema.has_local_role('read')
-                     or rschema.has_perm(self._cw, 'read')))
+                and rschema.may_have_permission('read', self._cw))
 
-    def should_display_attr(self, rschema):
+    def should_display_attr(self, eschema, rschema):
         return (super(RestrictedSchemaVisitorMixIn, self).should_display_attr(rschema)
-                and (rschema.has_local_role('read')
-                     or rschema.has_perm(self._cw, 'read')))
+                and eschema.rdef(rschema).may_have_permission('read', self._cw))
 
 
 class FullSchemaVisitor(RestrictedSchemaVisitorMixIn, s2d.FullSchemaVisitor):
--- a/web/views/startup.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/startup.py	Fri Nov 20 19:35:54 2009 +0100
@@ -130,8 +130,7 @@
         """
         req = self._cw
         for eschema in eschemas:
-            if eschema.final or (not eschema.has_perm(req, 'read') and
-                                      not eschema.has_local_role('read')):
+            if eschema.final or not eschema.may_have_permission('read', req):
                 continue
             etype = eschema.type
             label = display_name(req, etype, 'plural')
--- a/web/views/tableview.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/tableview.py	Fri Nov 20 19:35:54 2009 +0100
@@ -15,6 +15,7 @@
 from cubicweb.selectors import nonempty_rset, match_form_params
 from cubicweb.utils import make_uid
 from cubicweb.view import EntityView, AnyRsetView
+from cubicweb.common import tags
 from cubicweb.common.uilib import toggle_action, limitsize, htmlescape
 from cubicweb.web import jsonize
 from cubicweb.web.htmlwidgets import (TableWidget, TableColumn, MenuWidget,
@@ -179,9 +180,8 @@
 
     def render_actions(self, divid, actions):
         box = MenuWidget('', 'tableActionsBox', _class='', islist=False)
-        label = '<img src="%s" alt="%s"/>' % (
-            self._cw.datadir_url + 'liveclipboard-icon.png',
-            xml_escape(self._cw._('action(s) on this selection')))
+        label = tags.img(src=self._cw.external_resource('PUCE_DOWN'),
+                         alt=xml_escape(self._cw._('action(s) on this selection')))
         menu = PopupBoxMenu(label, isitem=False, link_class='actionsBox',
                             ident='%sActions' % divid)
         box.append(menu)
--- a/web/views/tabs.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/tabs.py	Fri Nov 20 19:35:54 2009 +0100
@@ -20,8 +20,6 @@
 class LazyViewMixin(object):
     """provides two convenience methods for the tab machinery
     can also be used to lazy-load arbitrary views
-    caveat : lazyview is not recursive, i.e : you can't (successfully)
-    lazyload a view that in turns does the same
     """
 
     def _prepare_bindings(self, vid, reloadable):
@@ -190,7 +188,8 @@
     def cell_call(self, row, col):
         entity = self.complete_entity(row, col)
         self.render_entity_title(entity)
-        self.render_entity_metadata(entity)
+        # XXX uncomment this in 3.6
+        #self.render_entity_toolbox(entity)
         self.render_tabs(self.tabs, self.default_tab, entity)
 
 
@@ -203,7 +202,5 @@
 
     def render_entity_title(self, entity):
         pass
-
-    def render_entity_metadata(self, entity):
+    def render_entity_toolbox(self, entity):
         pass
-
--- a/web/views/timetable.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/timetable.py	Fri Nov 20 19:35:54 2009 +0100
@@ -36,14 +36,13 @@
         dates = {}
         users = []
         users_max = {}
-
         # XXX: try refactoring with calendar.py:OneMonthCal
         for row in xrange(self.cw_rset.rowcount):
             task = self.cw_rset.get_entity(row, 0)
-            if len(self.cw_rset[row])>1:
+            if len(self.cw_rset[row]) > 1:
                 user = self.cw_rset.get_entity(row, 1)
             else:
-                user = u"*"
+                user = ALL_USERS
             the_dates = []
             if task.start and task.stop:
                 if task.start.toordinal() == task.stop.toordinal():
@@ -137,10 +136,10 @@
         columns = []
         for user, width in zip(users, widths):
             self.w(u'<th colspan="%s">' % max(MIN_COLS, width))
-            if user != u"*":
+            if user is ALL_USERS:
+                self.w('*')
+            else:
                 user.view('oneline', w=self.w)
-            else:
-                self.w(user)
             self.w(u'</th>')
         self.w(u'</tr>\n')
         return columns
--- a/web/views/treeview.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/treeview.py	Fri Nov 20 19:35:54 2009 +0100
@@ -180,7 +180,7 @@
         # the local node info
         self.wview(vid, self.cw_rset, row=row, col=col, **morekwargs)
         if is_open and not is_leaf: #  => rql is defined
-            self.wview(parentvid, self._cw.execute(rql), subvid=vid,
+            self.wview(parentvid, entity.children(entities=False), subvid=vid,
                        treeid=treeid, initial_load=False, **morekwargs)
         w(u'</li>')
 
--- a/web/views/urlrewrite.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/urlrewrite.py	Fri Nov 20 19:35:54 2009 +0100
@@ -7,6 +7,7 @@
 """
 import re
 
+from cubicweb import typed_eid
 from cubicweb.appobject import AppObject
 
 
@@ -148,10 +149,14 @@
 
 def rgx_action(rql=None, args=None, cachekey=None, argsgroups=(), setuser=False,
                form=None, formgroups=(), transforms={}, controller=None):
-    def do_build_rset(inputurl, uri, req, schema):
+    def do_build_rset(inputurl, uri, req, schema,
+                      cachekey=cachekey # necessary to avoid UnboundLocalError
+                      ):
         if rql:
             kwargs = args and args.copy() or {}
             if argsgroups:
+                if cachekey is not None and isinstance(cachekey, basestring):
+                    cachekey = (cachekey,)
                 match = inputurl.match(uri)
                 for key in argsgroups:
                     value = match.group(key)
@@ -159,6 +164,8 @@
                         kwargs[key] = transforms[key](value)
                     except KeyError:
                         kwargs[key] = value
+                    if cachekey is not None and key in cachekey:
+                        kwargs[key] = typed_eid(value)
             if setuser:
                 kwargs['u'] = req.user.eid
             rset = req.execute(rql, kwargs, cachekey)
--- a/web/views/wdoc.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/wdoc.py	Fri Nov 20 19:35:54 2009 +0100
@@ -237,6 +237,17 @@
         self.w(rest_publish(self, '\n'.join(restdata)))
 
 
+class HelpAction(action.Action):
+    id = 'help'
+    __select__ = yes()
+
+    category = 'footer'
+    order = 0
+    title = _('Help')
+
+    def url(self):
+        return self.req.build_url('doc/main')
+
 class ChangeLogAction(action.Action):
     __regid__ = 'changelog'
     __select__ = yes()
--- a/web/views/workflow.py	Sun Nov 08 21:53:18 2009 +0100
+++ b/web/views/workflow.py	Fri Nov 20 19:35:54 2009 +0100
@@ -16,13 +16,14 @@
 
 from cubicweb import Unauthorized, view
 from cubicweb.selectors import (implements, has_related_entities, one_line_rset,
-                                relation_possible, match_form_params)
+                                relation_possible, match_form_params,
+                                entity_implements)
 from cubicweb.interfaces import IWorkflowable
 from cubicweb.view import EntityView
 from cubicweb.schema import display_name
 from cubicweb.web import uicfg, stdmsgs, action, component, form, action
 from cubicweb.web import formfields as ff, formwidgets as fwdgs
-from cubicweb.web.views import TmpFileViewMixin, forms, primary
+from cubicweb.web.views import TmpFileViewMixin, forms, primary, autoform
 
 _pvs = uicfg.primaryview_section
 _pvs.tag_subject_of(('Workflow', 'initial_state', '*'), 'hidden')
@@ -50,8 +51,8 @@
     __regid__ = 'changestate'
 
     form_renderer_id = 'base' # don't want EntityFormRenderer
-    form_buttons = [fwdgs.SubmitButton(stdmsgs.YES),
-                    fwdgs.Button(stdmsgs.NO, cwaction='cancel')]
+    form_buttons = [fwdgs.SubmitButton(),
+                    fwdgs.Button(stdmsgs.BUTTON_CANCEL, cwaction='cancel')]
 
 
 class ChangeStateFormView(form.FormViewMixIn, view.EntityView):
@@ -169,6 +170,31 @@
 
 # workflow entity types views ##################################################
 
+_pvs = uicfg.primaryview_section
+_pvs.tag_subject_of(('Workflow', 'initial_state', '*'), 'hidden')
+_pvs.tag_object_of(('*', 'state_of', 'Workflow'), 'hidden')
+_pvs.tag_object_of(('*', 'transition_of', 'Workflow'), 'hidden')
+
+_abaa = uicfg.actionbox_appearsin_addmenu
+_abaa.tag_subject_of(('BaseTransition', 'condition', 'RQLExpression'), False)
+_abaa.tag_subject_of(('State', 'allowed_transition', 'BaseTransition'), False)
+_abaa.tag_object_of(('SubWorkflowExitPoint', 'destination_state', 'State'),
+                    False)
+_abaa.tag_object_of(('State', 'state_of', 'Workflow'), True)
+_abaa.tag_object_of(('BaseTransition', 'transition_of', 'Workflow'), False)
+_abaa.tag_object_of(('Transition', 'transition_of', 'Workflow'), True)
+_abaa.tag_object_of(('WorkflowTransition', 'transition_of', 'Workflow'), True)
+
+class WorkflowPrimaryView(primary.PrimaryView):
+    __select__ = implements('Workflow')
+
+    def render_entity_attributes(self, entity):
+        self.w(entity.view('reledit', rtype='description'))
+        self.w(u'<img src="%s" alt="%s"/>' % (
+            xml_escape(entity.absolute_url(vid='wfgraph')),
+            xml_escape(self.req._('graphical workflow for %s') % entity.name)))
+
+
 class CellView(view.EntityView):
     __regid__ = 'cell'
     __select__ = implements('TrInfo')
@@ -187,14 +213,53 @@
                                         row=row, col=col)))
 
 
-class WorkflowPrimaryView(primary.PrimaryView):
-    __select__ = implements('Workflow')
+# workflow entity types edition ################################################
+
+_afs = uicfg.autoform_section
+_afs.tag_subject_of(('TrInfo', 'to_state', '*'), 'generated')
+_afs.tag_subject_of(('TrInfo', 'from_state', '*'), 'generated')
+_afs.tag_object_of(('State', 'allowed_transition', '*'), 'primary')
+_afs.tag_subject_of(('State', 'allowed_transition', '*'), 'primary')
+
+def workflow_items_for_relation(req, wfeid, wfrelation, targetrelation):
+    wf = req.entity_from_eid(wfeid)
+    rschema = req.vreg.schema[targetrelation]
+    return sorted((e.view('combobox'), e.eid)
+                  for e in getattr(wf, 'reverse_%s' % wfrelation)
+                  if rschema.has_perm(req, 'add', toeid=e.eid))
+
+
+class TransitionEditionForm(autoform.AutomaticEntityForm):
+    __select__ = entity_implements('Transition')
 
-    def render_entity_attributes(self, entity):
-        self.w(entity.view('reledit', rtype='description'))
-        self.w(u'<img src="%s" alt="%s"/>' % (
-            xml_escape(entity.absolute_url(vid='wfgraph')),
-            xml_escape(self._cw._('graphical workflow for %s') % entity.name)))
+    def workflow_states_for_relation(self, targetrelation):
+        eids = self.edited_entity.linked_to('transition_of', 'subject')
+        if eids:
+            return workflow_items_for_relation(self.req, eids[0], 'state_of',
+                                               targetrelation)
+        return []
+
+    def subject_destination_state_vocabulary(self, rtype, limit=None):
+        if not self.edited_entity.has_eid():
+            return self.workflow_states_for_relation('destination_state')
+        return self.subject_relation_vocabulary(rtype, limit)
+
+    def object_allowed_transition_vocabulary(self, rtype, limit=None):
+        if not self.edited_entity.has_eid():
+            return self.workflow_states_for_relation('allowed_transition')
+        return self.subject_relation_vocabulary(rtype, limit)
+
+
+class StateEditionForm(autoform.AutomaticEntityForm):
+    __select__ = entity_implements('State')
+
+    def subject_allowed_transition_vocabulary(self, rtype, limit=None):
+        if not self.edited_entity.has_eid():
+            eids = self.edited_entity.linked_to('state_of', 'subject')
+            if eids:
+                return workflow_items_for_relation(self.req, eids[0], 'transition_of',
+                                                   'allowed_transition')
+        return []
 
 
 # workflow images ##############################################################