backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Mon, 28 Sep 2009 12:37:01 +0200
changeset 3503 06bced8edddf
parent 3491 48b26bfd6f02 (current diff)
parent 3502 9638213410e9 (diff)
child 3504 aadeab85af7c
backport stable
cwctl.py
etwist/server.py
schemas/workflow.py
server/sources/native.py
web/formfields.py
web/uicfg.py
web/views/actions.py
web/views/autoform.py
web/views/basetemplates.py
web/views/startup.py
web/views/wdoc.py
web/views/workflow.py
--- a/cwctl.py	Sat Sep 26 11:44:35 2009 +0200
+++ b/cwctl.py	Mon Sep 28 12:37:01 2009 +0200
@@ -172,8 +172,7 @@
         """run the command with its specific arguments"""
         if args:
             raise BadCommandUsage('Too much arguments')
-        print 'CubicWeb version:', cwcfg.cubicweb_version()
-        print 'Detected mode:', cwcfg.mode
+        print 'CubicWeb %s (%s mode)' % (cwcfg.cubicweb_version(), cwcfg.mode)
         print
         print 'Available configurations:'
         for config in CONFIGURATIONS:
--- a/devtools/dataimport.py	Sat Sep 26 11:44:35 2009 +0200
+++ b/devtools/dataimport.py	Mon Sep 28 12:37:01 2009 +0200
@@ -151,7 +151,7 @@
         return len(self.items) - 1
 
     def add(self, type, item):
-        assert isinstance(item, dict), item
+        assert isinstance(item, dict), 'item is not a dict but a %s' % type(item)
         eid = item['eid'] = self._put(type, item)
         self.eids[eid] = item
         self.types.setdefault(type, []).append(eid)
@@ -172,7 +172,7 @@
 
     def get_one(self, name, key):
         eids = self.indexes[name].get(key, [])
-        assert len(eids) == 1
+        assert len(eids) == 1, 'expected a single one got %i' % len(eids)
         return eids[0]
 
     def find(self, type, key, value):
--- a/doc/book/en/admin/setup.rst	Sat Sep 26 11:44:35 2009 +0200
+++ b/doc/book/en/admin/setup.rst	Mon Sep 28 12:37:01 2009 +0200
@@ -63,19 +63,26 @@
 
 .. _`ftp site`: http://ftp.logilab.org/pub/cubicweb/
 
-or keep up to date with on-going development by using Mercurial and its forest
-extension::
+Make sure you have installed the dependencies (see appendixes for the list).
+
+Install from version control system
+```````````````````````````````````
+
+You can keep up to date with on-going development by using Mercurial and its
+forest extension::
 
   hg fclone http://www.logilab.org/hg/forests/cubicweb
 
 See :ref:`MercurialPresentation` for more details about Mercurial.
+
 When cloning a repository, you might be set in a development branch
 (the 'default' branch). You should check that the branches of the
 repositories are set to 'stable' (using `hg up stable` for each one)
 if you do not intend to develop the framework itself.
 
-In both cases, make sure you have installed the dependencies (see appendixes for
-the list).
+Do not forget to update the forest itself (using `cd path/to/forest ; hg up`).
+
+Make sure you have installed the dependencies (see appendixes for the list).
 
 Windows installation
 ````````````````````
--- a/doc/book/en/annexes/depends.rst	Sat Sep 26 11:44:35 2009 +0200
+++ b/doc/book/en/annexes/depends.rst	Mon Sep 28 12:37:01 2009 +0200
@@ -47,5 +47,17 @@
 * 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
+
+* docsutils - http://docutils.sourceforge.net/ - http://pypi.python.org/pypi/docutils
+
+For the google-appengine extension to be available, you also need:
+
+* dateutil - http://labix.org/python-dateutil -
+  http://pypi.python.org/pypi/python-dateutil/
+
+* 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.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/development/devcore/cwconfig.rst	Mon Sep 28 12:37:01 2009 +0200
@@ -0,0 +1,5 @@
+:mod:`Configuration <cubicweb.cwconfig>`
+----------------------------------------
+
+.. automodule:: cubicweb.cwconfig
+   :members:
--- a/doc/book/en/development/devcore/index.rst	Sat Sep 26 11:44:35 2009 +0200
+++ b/doc/book/en/development/devcore/index.rst	Mon Sep 28 12:37:01 2009 +0200
@@ -8,10 +8,5 @@
    appobject.rst
    selectors.rst
    dbapi.rst
-
+   cwconfig.rst
 
-:mod:`Configuration <cubicweb.cwconfig>`
-----------------------------------------
-
-.. automodule:: cubicweb.cwconfig
-   :members:
--- a/doc/book/en/development/devweb/js.rst	Sat Sep 26 11:44:35 2009 +0200
+++ b/doc/book/en/development/devweb/js.rst	Mon Sep 28 12:37:01 2009 +0200
@@ -23,7 +23,7 @@
 
 XXX external_resources variable (which needs love)
 
-CubicWeb javascrip api
+CubicWeb javascript api
 ~~~~~~~~~~~~~~~~~~~~~~
 
 Javascript resources are typically loaded on demand, from views. The
--- a/doc/book/en/development/devweb/property.rst	Sat Sep 26 11:44:35 2009 +0200
+++ b/doc/book/en/development/devweb/property.rst	Mon Sep 28 12:37:01 2009 +0200
@@ -1,4 +1,4 @@
-The property mecanims
+The property mecanism
 ---------------------
 XXX CWProperty and co
 
@@ -9,4 +9,4 @@
 
 Registering and using your own property
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-XXX feed me
\ No newline at end of file
+XXX feed me
--- a/etwist/server.py	Sat Sep 26 11:44:35 2009 +0200
+++ b/etwist/server.py	Mon Sep 28 12:37:01 2009 +0200
@@ -10,10 +10,10 @@
 import sys
 import os
 import select
+import hotshot
 from time import mktime
 from datetime import date, timedelta
 from urlparse import urlsplit, urlunsplit
-import hotshot
 
 from twisted.application import strports
 from twisted.internet import reactor, task, threads
@@ -55,20 +55,9 @@
 
 def start_task(interval, func):
     lc = task.LoopingCall(func)
-    lc.start(interval)
-
-def start_looping_tasks(repo):
-    for interval, func, args in repo._looping_tasks:
-        repo.info('starting twisted task %s with interval %.2fs',
-                  func.__name__, interval)
-        def catch_error_func(repo=repo, func=func, args=args):
-            try:
-                func(*args)
-            except:
-                repo.exception('error in looping task')
-        start_task(interval, catch_error_func)
-    # ensure no tasks will be further added
-    repo._looping_tasks = ()
+    # wait until interval has expired to actually start the task, else we have
+    # to wait all task to be finished for the server to be actually started
+    lc.start(interval, now=False)
 
 def host_prefixed_baseurl(baseurl, host):
     scheme, netloc, url, query, fragment = urlsplit(baseurl)
@@ -122,14 +111,14 @@
             reactor.addSystemEventTrigger('before', 'shutdown',
                                           self.shutdown_event)
             # monkey patch start_looping_task to get proper reactor integration
-            self.appli.repo.__class__.start_looping_tasks = start_looping_tasks
+            #self.appli.repo.__class__.start_looping_tasks = start_looping_tasks
             if config.pyro_enabled():
                 # if pyro is enabled, we have to register to the pyro name
                 # server, create a pyro daemon, and create a task to handle pyro
                 # requests
                 self.pyro_daemon = self.appli.repo.pyro_register()
                 self.pyro_listen_timeout = 0.02
-                start_task(1, self.pyro_loop_event)
+                self.appli.repo.looping_task(1, self.pyro_loop_event)
             self.appli.repo.start_looping_tasks()
         self.set_url_rewriter()
         CW_EVENT_MANAGER.bind('after-registry-reload', self.set_url_rewriter)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.5.3_Any.py	Mon Sep 28 12:37:01 2009 +0200
@@ -0,0 +1,2 @@
+sync_schema_props_perms('state_of')
+sync_schema_props_perms('transition_of')
--- a/rqlrewrite.py	Sat Sep 26 11:44:35 2009 +0200
+++ b/rqlrewrite.py	Mon Sep 28 12:37:01 2009 +0200
@@ -14,8 +14,74 @@
 
 from logilab.common.compat import any
 
-from cubicweb import Unauthorized, server, typed_eid
-from cubicweb.server.ssplanner import add_types_restriction
+from cubicweb import Unauthorized, typed_eid
+
+
+def add_types_restriction(schema, rqlst, newroot=None, solutions=None):
+    if newroot is None:
+        assert solutions is None
+        if hasattr(rqlst, '_types_restr_added'):
+            return
+        solutions = rqlst.solutions
+        newroot = rqlst
+        rqlst._types_restr_added = True
+    else:
+        assert solutions is not None
+        rqlst = rqlst.stmt
+    eschema = schema.eschema
+    allpossibletypes = {}
+    for solution in solutions:
+        for varname, etype in solution.iteritems():
+            if not varname in newroot.defined_vars or eschema(etype).is_final():
+                continue
+            allpossibletypes.setdefault(varname, set()).add(etype)
+    for varname in sorted(allpossibletypes):
+        try:
+            var = newroot.defined_vars[varname]
+        except KeyError:
+            continue
+        stinfo = var.stinfo
+        if stinfo.get('uidrels'):
+            continue # eid specified, no need for additional type specification
+        try:
+            typerels = rqlst.defined_vars[varname].stinfo.get('typerels')
+        except KeyError:
+            assert varname in rqlst.aliases
+            continue
+        if newroot is rqlst and typerels:
+            mytyperel = iter(typerels).next()
+        else:
+            for vref in newroot.defined_vars[varname].references():
+                rel = vref.relation()
+                if rel and rel.is_types_restriction():
+                    mytyperel = rel
+                    break
+            else:
+                mytyperel = None
+        possibletypes = allpossibletypes[varname]
+        if mytyperel is not None:
+            # variable as already some types restriction. new possible types
+            # can only be a subset of existing ones, so only remove no more
+            # possible types
+            for cst in mytyperel.get_nodes(n.Constant):
+                if not cst.value in possibletypes:
+                    cst.parent.remove(cst)
+                    try:
+                        stinfo['possibletypes'].remove(cst.value)
+                    except KeyError:
+                        # restriction on a type not used by this query, may
+                        # occurs with X is IN(...)
+                        pass
+        else:
+            # we have to add types restriction
+            if stinfo.get('scope') is not None:
+                rel = var.scope.add_type_restriction(var, possibletypes)
+            else:
+                # tree is not annotated yet, no scope set so add the restriction
+                # to the root
+                rel = newroot.add_type_restriction(var, possibletypes)
+            stinfo['typerels'] = frozenset((rel,))
+            stinfo['possibletypes'] = possibletypes
 
 
 def remove_solutions(origsolutions, solutions, defined):
@@ -73,8 +139,6 @@
         snippets: (varmap, list of rql expression)
                   with varmap a *tuple* (select var, snippet var)
         """
-        if server.DEBUG:
-            print '---- rewrite', select, snippets, solutions
         self.select = self.insert_scope = select
         self.solutions = solutions
         self.kwargs = kwargs
@@ -102,8 +166,6 @@
             newsolutions = self.remove_ambiguities(snippets, newsolutions)
         select.solutions = newsolutions
         add_types_restriction(self.schema, select)
-        if server.DEBUG:
-            print '---- rewriten', select
 
     def insert_snippets(self, snippets, varexistsmap=None):
         self.rewritten = {}
@@ -134,8 +196,6 @@
                     try:
                         new = self.insert_snippet(varmap, rqlexpr.snippet_rqlst, parent)
                     except Unsupported:
-                        import traceback
-                        traceback.print_exc()
                         continue
                     inserted = True
                     if new is not None:
@@ -322,28 +382,38 @@
                 # no more references, undefine the variable
                 del self.select.defined_vars[vref.name]
 
-    def _may_be_shared(self, relation, target, searchedvarname):
-        """return True if the snippet relation can be skipped to use a relation
-        from the original query
+    def _may_be_shared_with(self, sniprel, target, searchedvarname):
+        """if the snippet relation can be skipped to use a relation from the
+        original query, return that relation node
         """
-        # if cardinality is in '?1', we can ignore the relation and use variable
-        # from the original query
-        rschema = self.schema.rschema(relation.r_type)
-        if target == 'object':
-            cardindex = 0
-            ttypes_func = rschema.objects
-            rprop = rschema.rproperty
-        else: # target == 'subject':
-            cardindex = 1
-            ttypes_func = rschema.subjects
-            rprop = lambda x, y, z: rschema.rproperty(y, x, z)
+        rschema = self.schema.rschema(sniprel.r_type)
+        try:
+            if target == 'object':
+                orel = self.varinfo['lhs_rels'][sniprel.r_type]
+                cardindex = 0
+                ttypes_func = rschema.objects
+                rprop = rschema.rproperty
+            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)
+        except KeyError, ex:
+            # may be raised by self.varinfo['xhs_rels'][sniprel.r_type]
+            return None
+        # can't share neged relation or relations with different outer join
+        if (orel.neged(strict=True) or sniprel.neged(strict=True)
+            or (orel.optional and orel.optional != sniprel.optional)):
+            return None
+        # if cardinality is in '?1', we can ignore the snippet relation and use
+        # 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 '+*':
-                    return False
-        return True
+                    return None
+        return orel
 
-    def _use_outer_term(self, snippet_varname, term):
+    def _use_orig_term(self, snippet_varname, term):
         key = (self.current_expr, self.varmap, snippet_varname)
         if key in self.rewritten:
             insertedvar = self.select.defined_vars.pop(self.rewritten[key])
@@ -417,27 +487,17 @@
             key = (self.current_expr, self.varmap, rhs.name)
             self.pending_keys.append( (key, action) )
             return
-        if lhs.name in self.revvarmap:
-            # on lhs
-            # see if we can reuse this relation
-            rels = self.varinfo['lhs_rels']
-            if (node.r_type in rels and isinstance(rhs, n.VariableRef)
-                and rhs.name != 'U' and not rels[node.r_type].neged(strict=True)
-                and self._may_be_shared(node, 'object', lhs.name)):
-                # ok, can share variable
-                term = rels[node.r_type].children[1].children[0]
-                self._use_outer_term(rhs.name, term)
-                return
-        elif isinstance(rhs, n.VariableRef) and rhs.name in self.revvarmap and lhs.name != 'U':
-            # on rhs
-            # see if we can reuse this relation
-            rels = self.varinfo['rhs_rels']
-            if (node.r_type in rels and not rels[node.r_type].neged(strict=True)
-                and self._may_be_shared(node, 'subject', rhs.name)):
-                # ok, can share variable
-                term = rels[node.r_type].children[0]
-                self._use_outer_term(lhs.name, term)
-                return
+        if isinstance(rhs, n.VariableRef):
+            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:
+                    self._use_orig_term(rhs.name, orel.children[1].children[0])
+                    return
+            elif rhs.name in self.revvarmap and lhs.name != 'U':
+                orel = self._may_be_shared_with(node, 'subject', rhs.name)
+                if orel is not None:
+                    self._use_orig_term(lhs.name, orel.children[0])
+                    return
         rel = n.Relation(node.r_type, node.optional)
         for c in node.children:
             rel.append(c.accept(self))
--- a/schemas/workflow.py	Sat Sep 26 11:44:35 2009 +0200
+++ b/schemas/workflow.py	Mon Sep 28 12:37:01 2009 +0200
@@ -57,7 +57,7 @@
     allowed_transition = SubjectRelation('BaseTransition', cardinality='**',
                                          constraints=[RQLConstraint('S state_of WF, O transition_of WF')],
                                          description=_('allowed transitions from this state'))
-    state_of = SubjectRelation('Workflow', cardinality='1*',
+    state_of = SubjectRelation('Workflow', cardinality='1*', composite='object',
                                description=_('workflow to which this state belongs'),
                                constraints=[RQLUniqueConstraint('S name N, Y state_of O, Y name N')])
 
@@ -80,7 +80,7 @@
     require_group = SubjectRelation('CWGroup', cardinality='**',
                                     description=_('group in which a user should be to be '
                                                   'allowed to pass this transition'))
-    transition_of = SubjectRelation('Workflow', cardinality='1*',
+    transition_of = SubjectRelation('Workflow', cardinality='1*', composite='object',
                                     description=_('workflow to which this transition belongs'),
                                     constraints=[RQLUniqueConstraint('S name N, Y transition_of O, Y name N')])
 
@@ -223,7 +223,7 @@
     }
     inlined = True
 
-    cardinality='1*'
+    cardinality = '1*'
     composite = 'object'
     fulltext_container = composite
     subject = 'TrInfo'
--- a/server/sources/native.py	Sat Sep 26 11:44:35 2009 +0200
+++ b/server/sources/native.py	Mon Sep 28 12:37:01 2009 +0200
@@ -554,7 +554,7 @@
         """
         try:
             self.indexer.cursor_unindex_object(eid, session.pool['system'])
-        except:
+        except Exception: # let KeyboardInterrupt / SystemExit propagate
             if self.indexer is not None:
                 self.exception('error while unindexing %s', eid)
 
@@ -565,7 +565,7 @@
         try:
             self.indexer.cursor_reindex_object(entity.eid, entity,
                                                session.pool['system'])
-        except:
+        except Exception: # let KeyboardInterrupt / SystemExit propagate
             if self.indexer is not None:
                 self.exception('error while reindexing %s', entity)
         # update entities.mtime
--- a/server/ssplanner.py	Sat Sep 26 11:44:35 2009 +0200
+++ b/server/ssplanner.py	Mon Sep 28 12:37:01 2009 +0200
@@ -14,72 +14,8 @@
 
 from cubicweb import QueryError, typed_eid
 from cubicweb.schema import VIRTUAL_RTYPES
+from cubicweb.rqlrewrite import add_types_restriction
 
-def add_types_restriction(schema, rqlst, newroot=None, solutions=None):
-    if newroot is None:
-        assert solutions is None
-        if hasattr(rqlst, '_types_restr_added'):
-            return
-        solutions = rqlst.solutions
-        newroot = rqlst
-        rqlst._types_restr_added = True
-    else:
-        assert solutions is not None
-        rqlst = rqlst.stmt
-    eschema = schema.eschema
-    allpossibletypes = {}
-    for solution in solutions:
-        for varname, etype in solution.iteritems():
-            if not varname in newroot.defined_vars or eschema(etype).is_final():
-                continue
-            allpossibletypes.setdefault(varname, set()).add(etype)
-    for varname in sorted(allpossibletypes):
-        try:
-            var = newroot.defined_vars[varname]
-        except KeyError:
-            continue
-        stinfo = var.stinfo
-        if stinfo.get('uidrels'):
-            continue # eid specified, no need for additional type specification
-        try:
-            typerels = rqlst.defined_vars[varname].stinfo.get('typerels')
-        except KeyError:
-            assert varname in rqlst.aliases
-            continue
-        if newroot is rqlst and typerels:
-            mytyperel = iter(typerels).next()
-        else:
-            for vref in newroot.defined_vars[varname].references():
-                rel = vref.relation()
-                if rel and rel.is_types_restriction():
-                    mytyperel = rel
-                    break
-            else:
-                mytyperel = None
-        possibletypes = allpossibletypes[varname]
-        if mytyperel is not None:
-            # variable as already some types restriction. new possible types
-            # can only be a subset of existing ones, so only remove no more
-            # possible types
-            for cst in mytyperel.get_nodes(Constant):
-                if not cst.value in possibletypes:
-                    cst.parent.remove(cst)
-                    try:
-                        stinfo['possibletypes'].remove(cst.value)
-                    except KeyError:
-                        # restriction on a type not used by this query, may
-                        # occurs with X is IN(...)
-                        pass
-        else:
-            # we have to add types restriction
-            if stinfo.get('scope') is not None:
-                rel = var.scope.add_type_restriction(var, possibletypes)
-            else:
-                # tree is not annotated yet, no scope set so add the restriction
-                # to the root
-                rel = newroot.add_type_restriction(var, possibletypes)
-            stinfo['typerels'] = frozenset((rel,))
-            stinfo['possibletypes'] = possibletypes
 
 class SSPlanner(object):
     """SingleSourcePlanner: build execution plan for rql queries
--- a/skeleton/__pkginfo__.py.tmpl	Sat Sep 26 11:44:35 2009 +0200
+++ b/skeleton/__pkginfo__.py.tmpl	Mon Sep 28 12:37:01 2009 +0200
@@ -43,8 +43,8 @@
 # Note: here, you'll need to add subdirectories if you want
 # them to be included in the debian package
 
-
-cube_eid = None # <=== FIXME if you need direct bug-subscription
+__depends_cubes__ = {}
+__depends__ = {'cubicweb': '>= 3.5.0'}
 __use__ = (%(dependancies)s)
 __recommend__ = ()
 
--- a/skeleton/migration/precreate.py	Sat Sep 26 11:44:35 2009 +0200
+++ b/skeleton/migration/precreate.py	Mon Sep 28 12:37:01 2009 +0200
@@ -7,4 +7,4 @@
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
 # You could create your own groups here, like in :
-#   add_entity('CWGroup', name=u'mygroup')
+#   create_entity('CWGroup', name=u'mygroup')
--- a/test/unittest_rqlrewrite.py	Sat Sep 26 11:44:35 2009 +0200
+++ b/test/unittest_rqlrewrite.py	Mon Sep 28 12:37:01 2009 +0200
@@ -136,19 +136,72 @@
                              "(Any C,T WHERE C in_state B, D in_group F, G require_state B, G name 'read', "
                              "G require_group F, C title T, D eid %(A)s, C is Card)")
 
-    def test_relation_optimization(self):
+    def test_relation_optimization_1_lhs(self):
         # since Card in_state State as monovalued cardinality, the in_state
         # relation used in the rql expression can be ignored and S replaced by
         # the variable from the incoming query
-        card_constraint = ('X in_state S, U in_group G, P require_state S,'
-                           'P name "read", P require_group G')
+        snippet = ('X in_state S, S name "hop"')
         rqlst = parse('Card C WHERE C in_state STATE')
-        rewrite(rqlst, {('C', 'X'): (card_constraint,)}, {})
+        rewrite(rqlst, {('C', 'X'): (snippet,)}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             "Any C WHERE C in_state STATE, C is Card, "
+                             "EXISTS(STATE name 'hop'), STATE is State")
+    def test_relation_optimization_1_rhs(self):
+        snippet = ('TW subworkflow_exit X, TW name "hop"')
+        rqlst = parse('WorkflowTransition C WHERE C subworkflow_exit EXIT')
+        rewrite(rqlst, {('EXIT', 'X'): (snippet,)}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             "Any C WHERE C subworkflow_exit EXIT, C is WorkflowTransition, "
+                             "EXISTS(C name 'hop'), EXIT is SubWorkflowExitPoint")
+
+    def test_relation_optimization_2_lhs(self):
+        # optional relation can be shared if also optional in the snippet
+        snippet = ('X in_state S?, S name "hop"')
+        rqlst = parse('Card C WHERE C in_state STATE?')
+        rewrite(rqlst, {('C', 'X'): (snippet,)}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             "Any C WHERE C in_state STATE?, C is Card, "
+                             "EXISTS(STATE name 'hop'), STATE is State")
+    def test_relation_optimization_2_rhs(self):
+        snippet = ('TW? subworkflow_exit X, TW name "hop"')
+        rqlst = parse('SubWorkflowExitPoint EXIT WHERE C? subworkflow_exit EXIT')
+        rewrite(rqlst, {('EXIT', 'X'): (snippet,)}, {})
         self.failUnlessEqual(rqlst.as_string(),
-                             u"Any C WHERE C in_state STATE, C is Card, A eid %(B)s, "
-                             "EXISTS(A in_group D, E require_state STATE, "
-                             "E name 'read', E require_group D, D is CWGroup, E is CWPermission), "
-                             "STATE is State")
+                             "Any EXIT WHERE C? subworkflow_exit EXIT, EXIT is SubWorkflowExitPoint, "
+                             "EXISTS(C name 'hop'), C is WorkflowTransition")
+
+    def test_relation_optimization_3_lhs(self):
+        # optional relation in the snippet but not in the orig tree can be shared
+        snippet = ('X in_state S?, S name "hop"')
+        rqlst = parse('Card C WHERE C in_state STATE')
+        rewrite(rqlst, {('C', 'X'): (snippet,)}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             "Any C WHERE C in_state STATE, C is Card, "
+                             "EXISTS(STATE name 'hop'), STATE is State")
+    def test_relation_optimization_3_rhs(self):
+        snippet = ('TW? subworkflow_exit X, TW name "hop"')
+        rqlst = parse('WorkflowTransition C WHERE C subworkflow_exit EXIT')
+        rewrite(rqlst, {('EXIT', 'X'): (snippet,)}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             "Any C WHERE C subworkflow_exit EXIT, C is WorkflowTransition, "
+                             "EXISTS(C name 'hop'), EXIT is SubWorkflowExitPoint")
+
+    def test_relation_non_optimization_1_lhs(self):
+        # but optional relation in the orig tree but not in the snippet can't be shared
+        snippet = ('X in_state S, S name "hop"')
+        rqlst = parse('Card C WHERE C in_state STATE?')
+        rewrite(rqlst, {('C', 'X'): (snippet,)}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             "Any C WHERE C in_state STATE?, C is Card, "
+                             "EXISTS(C in_state A, A name 'hop', A is State), STATE is State")
+    def test_relation_non_optimization_1_rhs(self):
+        snippet = ('TW subworkflow_exit X, TW name "hop"')
+        rqlst = parse('SubWorkflowExitPoint EXIT WHERE C? subworkflow_exit EXIT')
+        rewrite(rqlst, {('EXIT', 'X'): (snippet,)}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             "Any EXIT WHERE C? subworkflow_exit EXIT, EXIT is SubWorkflowExitPoint, "
+                             "EXISTS(A subworkflow_exit EXIT, A name 'hop', A is WorkflowTransition), "
+                             "C is WorkflowTransition")
 
     def test_unsupported_constraint_1(self):
         # CWUser doesn't have require_permission
--- a/web/data/cubicweb.css	Sat Sep 26 11:44:35 2009 +0200
+++ b/web/data/cubicweb.css	Mon Sep 28 12:37:01 2009 +0200
@@ -482,22 +482,6 @@
   padding-right: 1em;
 }
 
-div.boxPref {
-  margin: 10px 0px 0px;
-}
-
-div.boxPrefTitle {
-  font-weight: bold;
-  background: #cfceb7;
-  margin-bottom: 6px;
-  padding-bottom: 0.2em;
-  overflow: hidden;
-}
-
-div.boxPrefTitle span{
- padding:0px 5px;
-}
-
 input.rqlsubmit{
   background: #fffff8 url("go.png") 50% 50% no-repeat;
   width: 20px;
--- a/web/data/cubicweb.htmlhelpers.js	Sat Sep 26 11:44:35 2009 +0200
+++ b/web/data/cubicweb.htmlhelpers.js	Mon Sep 28 12:37:01 2009 +0200
@@ -154,7 +154,7 @@
 
 CubicWeb.rounded = [
 		    ['div.sideBoxBody', 'bottom 6px'],
-		    ['div.boxTitle, div.boxPrefTitle, div.sideBoxTitle, th.month', 'top 6px']
+		    ['div.boxTitle, div.sideBoxTitle, th.month', 'top 6px']
 		    ];
 
 function roundedCorners(node) {
--- a/web/formfields.py	Sat Sep 26 11:44:35 2009 +0200
+++ b/web/formfields.py	Mon Sep 28 12:37:01 2009 +0200
@@ -336,10 +336,12 @@
     widget = FileInput
     needs_multipart = True
 
-    def __init__(self, format_field=None, encoding_field=None, **kwargs):
+    def __init__(self, format_field=None, encoding_field=None, name_field=None,
+                 **kwargs):
         super(FileField, self).__init__(**kwargs)
         self.format_field = format_field
         self.encoding_field = encoding_field
+        self.name_field = name_field
 
     def actual_fields(self, form):
         yield self
@@ -347,6 +349,8 @@
             yield self.format_field
         if self.encoding_field:
             yield self.encoding_field
+        if self.name_field:
+            yield self.name_field
 
     def render(self, form, renderer):
         wdgs = [self.get_widget(form).render(form, self, renderer)]
@@ -358,6 +362,8 @@
                          xml_escape(form._cw.build_url('data/puce_down.png')),
                          form._cw._('show advanced fields')))
             wdgs.append(u'<div id="%s" class="hidden">' % divid)
+            if self.name_field:
+                wdgs.append(self.render_subfield(form, self.name_field, renderer))
             if self.format_field:
                 wdgs.append(self.render_subfield(form, self.format_field, renderer))
             if self.encoding_field:
@@ -626,7 +632,7 @@
                     kwargs['max_length'] = cstr.max
             return StringField(**kwargs)
         if fieldclass is FileField:
-            for metadata in ('format', 'encoding'):
+            for metadata in ('format', 'encoding', 'name'):
                 metaschema = eschema.has_metadata(rschema, metadata)
                 if metaschema is not None:
                     kwargs['%s_field' % metadata] = guess_field(eschema, metaschema,
--- a/web/uicfg.py	Sat Sep 26 11:44:35 2009 +0200
+++ b/web/uicfg.py	Mon Sep 28 12:37:01 2009 +0200
@@ -207,12 +207,24 @@
             else:
                 self.setdefault(eschema, 'application')
 
+indexview_etype_section = {'EmailAddress': 'subobject',
+                           'CWUser': 'system',
+                           'CWGroup': 'system',
+                           'CWPermission': 'system',
+                           'CWCache': 'system',
+                           'Workflow': 'system',
+                           'State': 'hidden',
+                           'BaseTransition': 'hidden',
+                           'Transition': 'hidden',
+                           'WorkflowTransition': 'hidden',
+                           }
+
 indexview_etype_section = InitializableDict(EmailAddress='subobject',
                                             CWUser='system',
                                             CWGroup='system',
                                             CWPermission='system',
                                             CWCache='system',
-                                            BaseTransition='hidden',
+                                            Workflow='system',
                                             )
 
 # autoform.AutomaticEntityForm configuration ##################################
--- a/web/views/actions.py	Sat Sep 26 11:44:35 2009 +0200
+++ b/web/views/actions.py	Mon Sep 28 12:37:01 2009 +0200
@@ -9,7 +9,7 @@
 _ = unicode
 
 from cubicweb.appobject import objectify_selector
-from cubicweb.selectors import (EntitySelector,
+from cubicweb.selectors import (EntitySelector, yes,
     one_line_rset, two_lines_rset, one_etype_rset, relation_possible,
     nonempty_rset, non_final_entity,
     authenticated_user, match_user_groups, match_search_state,
@@ -363,6 +363,18 @@
     order = 30
 
 
+class PoweredByAction(Action):
+    id = 'poweredby'
+    __select__ = yes()
+
+    category = 'footer'
+    order = 3
+    title = _('powered by CubicWeb')
+
+    def url(self):
+        return 'http://www.cubicweb.org'
+
+
 from logilab.common.deprecation import class_moved
 from cubicweb.web.views.bookmark import FollowAction
 FollowAction = class_moved(FollowAction)
--- a/web/views/autoform.py	Sat Sep 26 11:44:35 2009 +0200
+++ b/web/views/autoform.py	Mon Sep 28 12:37:01 2009 +0200
@@ -111,6 +111,11 @@
     @property
     def form_needs_multipart(self):
         """true if the form needs enctype=multipart/form-data"""
+        return self._subform_needs_multipart()
+
+    def _subform_needs_multipart(self, _tested=None):
+        if _tested is None:
+            _tested = set()
         if super(AutomaticEntityForm, self).form_needs_multipart:
             return True
         # take a look at inlined forms to check (recursively) if they
@@ -129,11 +134,18 @@
             if len(targettypes) != 1:
                 continue
             targettype = targettypes[0]
+            if targettype in _tested:
+                continue
+            _tested.add(targettype)
             if self.should_inline_relation_form(rschema, targettype, role):
                 entity = self._cw.vreg['etypes'].etype_class(targettype)(self._cw)
                 subform = self._cw.vreg['forms'].select('edition', self._cw,
                                                         entity=entity)
-                if subform.form_needs_multipart:
+                if hasattr(subform, '_subform_needs_multipart'):
+                    needs_multipart = subform._subform_needs_multipart(_tested)
+                else:
+                    needs_multipart = subform.form_needs_multipart
+                if needs_multipart:
                     return True
         return False
 
--- a/web/views/basetemplates.py	Sat Sep 26 11:44:35 2009 +0200
+++ b/web/views/basetemplates.py	Mon Sep 28 12:37:01 2009 +0200
@@ -424,14 +424,13 @@
     def call(self, **kwargs):
         req = self._cw
         self.w(u'<div class="footer">')
-        # XXX Take object from the registry if in there? would be
-        #     better anyway
-        from cubicweb.web.views.wdoc import ChangeLogView
-        self.w(u'<a href="%s">%s</a> | ' % (req.build_url('changelog'),
-                                            req._(ChangeLogView.title).lower()))
-        self.w(u'<a href="%s">%s</a> | ' % (req.build_url('doc/about'),
-                                            req._('about this site')))
-        self.w(u'<a href="http://www.cubicweb.org">%s</a>' % req._('powered by CubicWeb'))
+        actions = self.vreg['actions'].possible_actions(self.req, rset=self.rset)
+        footeractions = actions.get('footer', ())
+        for i, action in enumerate(footeractions):
+            self.w(u'<a href="%s">%s</a>' % (action.url(),
+                                             self.req._(action.title)))
+            if i < (len(footeractions) - 1):
+                self.w(u' | ')
         self.w(u'</div>')
 
 
@@ -528,8 +527,3 @@
     if config.get('https-url'):
         return req.url().replace(req.base_url(), config['https-url'])
     return req.url()
-
-
-## vregistry registration callback ############################################
-def registration_callback(vreg):
-    vreg.register_all(globals().values(), __name__)
--- a/web/views/startup.py	Sat Sep 26 11:44:35 2009 +0200
+++ b/web/views/startup.py	Mon Sep 28 12:37:01 2009 +0200
@@ -21,6 +21,7 @@
     __regid__ = 'manage'
     title = _('manage')
     http_cache_manager = httpcache.EtagHTTPCacheManager
+    add_etype_links = ()
 
     def display_folders(self):
         return False
@@ -68,6 +69,16 @@
         self.w(u'<h4>%s</h4>\n' % self._cw._('Browse by category'))
         self._cw.vreg['views'].select('tree', self._cw).render(w=self.w)
 
+    def create_links(self):
+        self.w(u'<ul class="createLink">')
+        for etype in self.add_etype_links:
+            eschema = self.schema.eschema(etype)
+            if eschema.has_perm(self.req, 'add'):
+                self.w(u'<li><a href="%s">%s</a></li>' % (
+                        self.req.build_url('add/%s' % eschema),
+                        self.req.__('add a %s' % eschema).capitalize()))
+        self.w(u'</ul>')
+
     def startup_views(self):
         self.w(u'<h4>%s</h4>\n' % self._cw._('Startup views'))
         self.startupviews_table()
--- a/web/views/wdoc.py	Sat Sep 26 11:44:35 2009 +0200
+++ b/web/views/wdoc.py	Mon Sep 28 12:37:01 2009 +0200
@@ -15,12 +15,11 @@
 from logilab.common.changelog import ChangeLog
 from logilab.mtconverter import CHARSET_DECL_RGX
 
-from cubicweb.selectors import match_form_params
+from cubicweb.selectors import match_form_params, yes
 from cubicweb.view import StartupView
 from cubicweb.utils import strptime, todate
 from cubicweb.common.uilib import rest_publish
-from cubicweb.web import NotFound
-
+from cubicweb.web import NotFound, action
 _ = unicode
 
 # table of content management #################################################
@@ -235,3 +234,28 @@
                     break
         w('') # blank line
         self.w(rest_publish(self, '\n'.join(restdata)))
+
+
+class ChangeLogAction(action.Action):
+    id = 'changelog'
+    __select__ = yes()
+
+    category = 'footer'
+    order = 1
+    title = ChangeLogView.title
+
+    def url(self):
+        return self.req.build_url('changelog')
+
+
+class AboutAction(action.Action):
+    id = 'about'
+    __select__ = yes()
+
+    category = 'footer'
+    order = 2
+    title = _('about this site')
+
+    def url(self):
+        return self.req.build_url('doc/about')
+
--- a/web/views/workflow.py	Sat Sep 26 11:44:35 2009 +0200
+++ b/web/views/workflow.py	Mon Sep 28 12:37:01 2009 +0200
@@ -24,6 +24,11 @@
 from cubicweb.web import formfields as ff, formwidgets as fwdgs
 from cubicweb.web.views import TmpFileViewMixin, forms, primary
 
+_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)