backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Thu, 13 Jan 2011 19:24:21 +0100
changeset 6822 47f4950ff815
parent 6783 5bbf827b6caf (current diff)
parent 6821 945fce560757 (diff)
child 6824 026581ea2b16
backport stable
__pkginfo__.py
i18n/de.po
i18n/en.po
i18n/es.po
i18n/fr.po
server/repository.py
--- a/.hgtags	Wed Jan 05 18:42:21 2011 +0100
+++ b/.hgtags	Thu Jan 13 19:24:21 2011 +0100
@@ -177,3 +177,5 @@
 e2e7410e994777589aec218d31eef9ff8d893f92 cubicweb-debian-version-3.10.5-1
 3c81dbb58ac4d4a6f61b74eef4b943a8316c2f42 cubicweb-version-3.10.6
 1484257fe9aeb29d0210e635c12ae5b3d6118cfb cubicweb-debian-version-3.10.6-1
+1959d97ebf2e6a0f7cd05d4cc48bb955c4351da5 cubicweb-version-3.10.7
+bf5d9a1415e3c9abe6b68ba3b24a8ad741f9de3c cubicweb-debian-version-3.10.7-1
--- a/__pkginfo__.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/__pkginfo__.py	Thu Jan 13 19:24:21 2011 +0100
@@ -42,7 +42,7 @@
 __depends__ = {
     'logilab-common': '>= 0.54.0',
     'logilab-mtconverter': '>= 0.8.0',
-    'rql': '>= 0.27.0',
+    'rql': '>= 0.28.0',
     'yams': '>= 0.30.1',
     'docutils': '>= 0.6',
     #gettext                    # for xgettext, msgcat, etc...
--- a/cwconfig.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/cwconfig.py	Thu Jan 13 19:24:21 2011 +0100
@@ -450,14 +450,15 @@
 
     @classmethod
     def cube_dir(cls, cube):
-        """return the cube directory for the given cube id,
-        raise `ConfigurationError` if it doesn't exists
+        """return the cube directory for the given cube id, raise
+        `ConfigurationError` if it doesn't exist
         """
         for directory in cls.cubes_search_path():
             cubedir = join(directory, cube)
             if exists(cubedir):
                 return cubedir
-        raise ConfigurationError('no cube %s in %s' % (cube, cls.cubes_search_path()))
+        raise ConfigurationError('no cube %r in %s' % (
+            cube, cls.cubes_search_path()))
 
     @classmethod
     def cube_migration_scripts_dir(cls, cube):
@@ -1278,7 +1279,9 @@
             stack[0] = self.source_execute
 
         def as_sql(self, backend, args):
-            raise NotImplementedError('source only callback')
+            raise NotImplementedError(
+                'This callback is only available for BytesFileSystemStorage '
+                'managed attribute. Is FSPATH() argument BFSS managed?')
 
         def source_execute(self, source, session, value):
             fpath = source.binary_to_str(value)
--- a/cwvreg.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/cwvreg.py	Thu Jan 13 19:24:21 2011 +0100
@@ -290,13 +290,18 @@
 
 class ETypeRegistry(CWRegistry):
 
+    def clear_caches(self):
+        clear_cache(self, 'etype_class')
+        clear_cache(self, 'parent_classes')
+        from cubicweb import selectors
+        selectors._reset_is_instance_cache(self.vreg)
+
     def initialization_completed(self):
         """on registration completed, clear etype_class internal cache
         """
         super(ETypeRegistry, self).initialization_completed()
         # clear etype cache if you don't want to run into deep weirdness
-        clear_cache(self, 'etype_class')
-        clear_cache(self, 'parent_classes')
+        self.clear_caches()
 
     def register(self, obj, **kwargs):
         oid = kwargs.get('oid') or class_regid(obj)
--- a/debian/changelog	Wed Jan 05 18:42:21 2011 +0100
+++ b/debian/changelog	Thu Jan 13 19:24:21 2011 +0100
@@ -1,3 +1,9 @@
+cubicweb (3.10.7-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 12 Jan 2011 08:50:29 +0100
+
 cubicweb (3.10.6-1) unstable; urgency=low
 
   * new upstream release
--- a/debian/control	Wed Jan 05 18:42:21 2011 +0100
+++ b/debian/control	Thu Jan 13 19:24:21 2011 +0100
@@ -97,7 +97,7 @@
 Package: cubicweb-common
 Architecture: all
 XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.54.0), python-yams (>= 0.30.1), python-rql (>= 0.27.0), python-lxml
+Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.54.0), python-yams (>= 0.30.1), python-rql (>= 0.28.0), python-lxml
 Recommends: python-simpletal (>= 4.0), python-crypto
 Conflicts: cubicweb-core
 Replaces: cubicweb-core
--- a/debian/cubicweb-ctl.cubicweb.init	Wed Jan 05 18:42:21 2011 +0100
+++ b/debian/cubicweb-ctl.cubicweb.init	Thu Jan 13 19:24:21 2011 +0100
@@ -24,12 +24,12 @@
 
 case $1 in
     force-reload)
-        /usr/bin/cubicweb-ctl reload --force
+        python -W ignore /usr/bin/cubicweb-ctl reload --force
         ;;
     status)
-        /usr/bin/cubicweb-ctl status
+        python -W ignore /usr/bin/cubicweb-ctl status
         ;;
     *)
-        /usr/bin/cubicweb-ctl $1 --force
+        python -W ignore /usr/bin/cubicweb-ctl $1 --force
         ;;
 esac
--- a/devtools/repotest.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/devtools/repotest.py	Thu Jan 13 19:24:21 2011 +0100
@@ -26,6 +26,7 @@
 from pprint import pprint
 
 from logilab.common.decorators import clear_cache
+from logilab.common.testlib import SkipTest
 
 def tuplify(list):
     for i in range(len(list)):
@@ -149,6 +150,15 @@
 class RQLGeneratorTC(TestCase):
     schema = backend = None # set this in concret test
 
+
+    @classmethod
+    def setUpClass(cls):
+        if cls.backend is not None:
+            try:
+                cls.dbhelper = get_db_helper(cls.backend)
+            except ImportError, ex:
+                raise SkipTest(str(ex))
+
     def setUp(self):
         self.repo = FakeRepo(self.schema)
         self.repo.system_source = mock_object(dbdriver=self.backend)
@@ -159,11 +169,7 @@
         ExecutionPlan._check_permissions = _dummy_check_permissions
         rqlannotation._select_principal = _select_principal
         if self.backend is not None:
-            try:
-                dbhelper = get_db_helper(self.backend)
-            except ImportError, ex:
-                self.skipTest(str(ex))
-            self.o = SQLGenerator(self.schema, dbhelper)
+            self.o = SQLGenerator(self.schema, self.dbhelper)
 
     def tearDown(self):
         ExecutionPlan._check_permissions = _orig_check_permissions
@@ -378,7 +384,7 @@
 def _merge_input_maps(*args, **kwargs):
     return sorted(_orig_merge_input_maps(*args, **kwargs))
 
-def _choose_term(self, sourceterms):
+def _choose_term(self, source, sourceterms):
     # predictable order for test purpose
     def get_key(x):
         try:
@@ -391,7 +397,7 @@
             except AttributeError:
                 # const
                 return x.value
-    return _orig_choose_term(self, DumbOrderedDict2(sourceterms, get_key))
+    return _orig_choose_term(self, source, DumbOrderedDict2(sourceterms, get_key))
 
 from cubicweb.server.sources.pyrorql import PyroRQLSource
 _orig_syntax_tree_search = PyroRQLSource.syntax_tree_search
--- a/devtools/testlib.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/devtools/testlib.py	Thu Jan 13 19:24:21 2011 +0100
@@ -480,9 +480,7 @@
             def items(self):
                 return self
         class fake_box(object):
-            def mk_action(self, label, url, **kwargs):
-                return (label, url)
-            def box_action(self, action, **kwargs):
+            def action_link(self, action, **kwargs):
                 return (action.title, action.url())
         submenu = fake_menu()
         action.fill_menu(fake_box(), submenu)
@@ -741,10 +739,8 @@
         :returns: an instance of `cubicweb.devtools.htmlparser.PageInfo`
                   encapsulation the generated HTML
         """
-        output = None
         try:
             output = viewfunc(**kwargs)
-            return self._check_html(output, view, template)
         except (SystemExit, KeyboardInterrupt):
             raise
         except:
@@ -755,22 +751,8 @@
                 msg = '[%s in %s] %s' % (klass, view.__regid__, exc)
             except:
                 msg = '[%s in %s] undisplayable exception' % (klass, view.__regid__)
-            msg = str(msg) # ensure no unicode
-            if output is not None:
-                position = getattr(exc, "position", (0,))[0]
-                if position:
-                    # define filter
-                    output = output.splitlines()
-                    width = int(log(len(output), 10)) + 1
-                    line_template = " %" + ("%i" % width) + "i: %s"
-                    # XXX no need to iterate the whole file except to get
-                    # the line number
-                    output = '\n'.join(line_template % (idx + 1, line)
-                                for idx, line in enumerate(output)
-                                if line_context_filter(idx+1, position))
-                    msg += '\nfor output:\n%s' % output
             raise AssertionError, msg, tcbk
-
+        return self._check_html(output, view, template)
 
     @nocoverage
     def _check_html(self, output, view, template='main-template'):
@@ -794,7 +776,44 @@
         if isinstance(validator, htmlparser.DTDValidator):
             # XXX remove <canvas> used in progress widget, unknown in html dtd
             output = re.sub('<canvas.*?></canvas>', '', output)
-        return validator.parse_string(output.strip())
+        return self.assertWellFormed(validator, output.strip(), context= view.__regid__)
+
+    def assertWellFormed(self, validator, content, context=None):
+        try:
+            return validator.parse_string(content)
+        except (SystemExit, KeyboardInterrupt):
+            raise
+        except:
+            # hijack exception: generative tests stop when the exception
+            # is not an AssertionError
+            klass, exc, tcbk = sys.exc_info()
+            if context is None:
+                msg = u'[%s]' % (klass,)
+            else:
+                msg = u'[%s in %s]' % (klass, context)
+            msg = msg.encode(sys.getdefaultencoding(), 'replace')
+
+            try:
+                str_exc = str(exc)
+            except:
+                str_exc = 'undisplayable exception'
+            msg += str_exc
+            if content is not None:
+                position = getattr(exc, "position", (0,))[0]
+                if position:
+                    # define filter
+                    if isinstance(content, str):
+                        content = unicode(content, sys.getdefaultencoding(), 'replace')
+                    content = content.splitlines()
+                    width = int(log(len(content), 10)) + 1
+                    line_template = " %" + ("%i" % width) + "i: %s"
+                    # XXX no need to iterate the whole file except to get
+                    # the line number
+                    content = u'\n'.join(line_template % (idx + 1, line)
+                                         for idx, line in enumerate(content)
+                                         if line_context_filter(idx+1, position))
+                    msg += u'\nfor content:\n%s' % content
+            raise AssertionError, msg, tcbk
 
     # deprecated ###############################################################
 
--- a/doc/book/en/makefile	Wed Jan 05 18:42:21 2011 +0100
+++ b/doc/book/en/makefile	Thu Jan 13 19:24:21 2011 +0100
@@ -1,10 +1,5 @@
-MKHTML=mkdoc
-MKHTMLOPTS=--doctype article --target html --stylesheet standard
 SRC=.
 
-TXTFILES:= $(wildcard *.txt)
-TARGET := $(TXTFILES:.txt=.html)
-
 # You can set these sphinx variables from the command line.
 SPHINXOPTS    =
 SPHINXBUILD   = sphinx-build
@@ -41,10 +36,7 @@
 	-rm -rf ${BUILDDIR}/*
 	-rm -rf ${BUILDJS}
 
-all: ${TARGET} html
-
-%.html: %.txt
-	${MKHTML} ${MKHTMLOPTS} $<
+all: html
 
 # run sphinx ###
 html: js
--- a/entities/test/unittest_wfobjs.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/entities/test/unittest_wfobjs.py	Thu Jan 13 19:24:21 2011 +0100
@@ -15,7 +15,9 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+
 from __future__ import with_statement
+
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb import ValidationError
 from cubicweb.server.session import security_enabled
@@ -55,8 +57,9 @@
         wf.add_state(u'foo', initial=True)
         self.commit()
         wf.add_state(u'foo')
-        ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEqual(ex.errors, {'name-subject': 'workflow already have a state of that name'})
+        with self.assertRaises(ValidationError) as cm:
+            self.commit()
+        self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a state of that name'})
         # no pb if not in the same workflow
         wf2 = add_wf(self, 'Company')
         foo = wf2.add_state(u'foo', initial=True)
@@ -65,8 +68,9 @@
         bar = wf.add_state(u'bar')
         self.commit()
         bar.set_attributes(name=u'foo')
-        ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEqual(ex.errors, {'name-subject': 'workflow already have a state of that name'})
+        with self.assertRaises(ValidationError) as cm:
+            self.commit()
+        self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a state of that name'})
 
     def test_duplicated_transition(self):
         wf = add_wf(self, 'Company')
@@ -74,8 +78,9 @@
         bar = wf.add_state(u'bar')
         wf.add_transition(u'baz', (foo,), bar, ('managers',))
         wf.add_transition(u'baz', (bar,), foo)
-        ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEqual(ex.errors, {'name-subject': 'workflow already have a transition of that name'})
+        with self.assertRaises(ValidationError) as cm:
+            self.commit()
+        self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a transition of that name'})
         # no pb if not in the same workflow
         wf2 = add_wf(self, 'Company')
         foo = wf.add_state(u'foo', initial=True)
@@ -86,8 +91,9 @@
         biz = wf.add_transition(u'biz', (bar,), foo)
         self.commit()
         biz.set_attributes(name=u'baz')
-        ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEqual(ex.errors, {'name-subject': 'workflow already have a transition of that name'})
+        with self.assertRaises(ValidationError) as cm:
+            self.commit()
+        self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a transition of that name'})
 
 
 class WorkflowTC(CubicWebTC):
@@ -150,10 +156,10 @@
         s = wf.add_state(u'foo', initial=True)
         self.commit()
         with security_enabled(self.session, write=False):
-            ex = self.assertRaises(ValidationError, self.session.execute,
-                               'SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
-                               {'x': self.user().eid, 's': s.eid})
-            self.assertEqual(ex.errors, {'in_state-subject': "state doesn't belong to entity's workflow. "
+            with self.assertRaises(ValidationError) as cm:
+                self.session.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
+                                     {'x': self.user().eid, 's': s.eid})
+            self.assertEqual(cm.exception.errors, {'in_state-subject': "state doesn't belong to entity's workflow. "
                                       "You may want to set a custom workflow for this entity first."})
 
     def test_fire_transition(self):
@@ -197,18 +203,18 @@
         cnx = self.login('tutu')
         req = self.request()
         iworkflowable = req.entity_from_eid(self.member.eid).cw_adapt_to('IWorkflowable')
-        ex = self.assertRaises(ValidationError,
-                               iworkflowable.fire_transition, 'deactivate')
-        self.assertEqual(ex.errors, {'by_transition-subject': "transition may not be fired"})
+        with self.assertRaises(ValidationError) as cm:
+            iworkflowable.fire_transition('deactivate')
+        self.assertEqual(cm.exception.errors, {'by_transition-subject': "transition may not be fired"})
         cnx.close()
         cnx = self.login('member')
         req = self.request()
         iworkflowable = req.entity_from_eid(self.member.eid).cw_adapt_to('IWorkflowable')
         iworkflowable.fire_transition('deactivate')
         cnx.commit()
-        ex = self.assertRaises(ValidationError,
-                               iworkflowable.fire_transition, 'activate')
-        self.assertEqual(ex.errors, {'by_transition-subject': "transition may not be fired"})
+        with self.assertRaises(ValidationError) as cm:
+            iworkflowable.fire_transition('activate')
+        self.assertEqual(cm.exception.errors, {'by_transition-subject': "transition may not be fired"})
 
     def test_fire_transition_owned_by(self):
         self.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", '
@@ -280,9 +286,9 @@
         self.assertEqual(iworkflowable.subworkflow_input_transition(), None)
         # force back to swfstate1 is impossible since we can't any more find
         # subworkflow input transition
-        ex = self.assertRaises(ValidationError,
-                               iworkflowable.change_state, swfstate1, u'gadget')
-        self.assertEqual(ex.errors, {'to_state-subject': "state doesn't belong to entity's workflow"})
+        with self.assertRaises(ValidationError) as cm:
+            iworkflowable.change_state(swfstate1, u'gadget')
+        self.assertEqual(cm.exception.errors, {'to_state-subject': "state doesn't belong to entity's workflow"})
         self.rollback()
         # force back to state1
         iworkflowable.change_state('state1', u'gadget')
@@ -317,8 +323,9 @@
         state3 = mwf.add_state(u'state3')
         mwf.add_wftransition(u'swftr1', swf, state1,
                              [(swfstate2, state2), (swfstate2, state3)])
-        ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEqual(ex.errors, {'subworkflow_exit-subject': u"can't have multiple exits on the same state"})
+        with self.assertRaises(ValidationError) as cm:
+            self.commit()
+        self.assertEqual(cm.exception.errors, {'subworkflow_exit-subject': u"can't have multiple exits on the same state"})
 
     def test_swf_fire_in_a_row(self):
         # sub-workflow
@@ -435,8 +442,9 @@
         wf.add_state('asleep')
         self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
                      {'wf': wf.eid, 'x': self.member.eid})
-        ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEqual(ex.errors, {'custom_workflow-subject': u'workflow has no initial state'})
+        with self.assertRaises(ValidationError) as cm:
+            self.commit()
+        self.assertEqual(cm.exception.errors, {'custom_workflow-subject': u'workflow has no initial state'})
 
     def test_custom_wf_bad_etype(self):
         """try to set a custom workflow which doesn't apply to entity type"""
@@ -444,8 +452,9 @@
         wf.add_state('asleep', initial=True)
         self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
                      {'wf': wf.eid, 'x': self.member.eid})
-        ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEqual(ex.errors, {'custom_workflow-subject': u"workflow isn't a workflow for this type"})
+        with self.assertRaises(ValidationError) as cm:
+            self.commit()
+        self.assertEqual(cm.exception.errors, {'custom_workflow-subject': u"workflow isn't a workflow for this type"})
 
     def test_del_custom_wf(self):
         """member in some state shared by the new workflow, nothing has to be
@@ -590,9 +599,9 @@
         cnx = self.login('stduser')
         user = cnx.user(self.session)
         iworkflowable = user.cw_adapt_to('IWorkflowable')
-        ex = self.assertRaises(ValidationError,
-                               iworkflowable.fire_transition, 'activate')
-        self.assertEqual(self._cleanup_msg(ex.errors['by_transition-subject']),
+        with self.assertRaises(ValidationError) as cm:
+            iworkflowable.fire_transition('activate')
+        self.assertEqual(self._cleanup_msg(cm.exception.errors['by_transition-subject']),
                           u"transition isn't allowed from")
         cnx.close()
 
@@ -600,9 +609,9 @@
         cnx = self.login('stduser')
         user = cnx.user(self.session)
         iworkflowable = user.cw_adapt_to('IWorkflowable')
-        ex = self.assertRaises(ValidationError,
-                               iworkflowable.fire_transition, 'dummy')
-        self.assertEqual(self._cleanup_msg(ex.errors['by_transition-subject']),
+        with self.assertRaises(ValidationError) as cm:
+            iworkflowable.fire_transition('dummy')
+        self.assertEqual(self._cleanup_msg(cm.exception.errors['by_transition-subject']),
                           u"transition isn't allowed from")
         cnx.close()
 
@@ -614,9 +623,9 @@
         iworkflowable.fire_transition('deactivate')
         cnx.commit()
         session.set_pool()
-        ex = self.assertRaises(ValidationError,
-                               iworkflowable.fire_transition, 'deactivate')
-        self.assertEqual(self._cleanup_msg(ex.errors['by_transition-subject']),
+        with self.assertRaises(ValidationError) as cm:
+            iworkflowable.fire_transition('deactivate')
+        self.assertEqual(self._cleanup_msg(cm.exception.errors['by_transition-subject']),
                                             u"transition isn't allowed from")
         cnx.rollback()
         session.set_pool()
--- a/entity.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/entity.py	Thu Jan 13 19:24:21 2011 +0100
@@ -697,7 +697,7 @@
         if not self.has_eid():
             if entities:
                 return []
-            return self.empty_rset()
+            return self._cw.empty_rset()
         rql = self.cw_related_rql(rtype, role)
         rset = self._cw.execute(rql, {'x': self.eid})
         self.cw_set_relation_cache(rtype, role, rset)
@@ -972,7 +972,7 @@
     def set_related_cache(self, rtype, role, rset):
         self.cw_set_relation_cache(rtype, role, rset)
 
-    @deprecated('[3.9] use entity.cw_clear_relation_cache(rtype, role, rset)')
+    @deprecated('[3.9] use entity.cw_clear_relation_cache(rtype, role)')
     def clear_related_cache(self, rtype=None, role=None):
         self.cw_clear_relation_cache(rtype, role)
 
@@ -996,7 +996,7 @@
         return self.cw_edited.skip_security
 
     @property
-    @deprecated('[3.10] use entity.cw_edited.skip_security')
+    @deprecated('[3.10] use entity.cw_edited.querier_pending_relations')
     def querier_pending_relations(self):
         return self.cw_edited.querier_pending_relations
 
--- a/etwist/server.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/etwist/server.py	Thu Jan 13 19:24:21 2011 +0100
@@ -408,7 +408,8 @@
     website = server.Site(root_resource)
     # serve it via standard HTTP on port set in the configuration
     port = config['port'] or 8080
-    reactor.listenTCP(port, website)
+    interface = config['interface']
+    reactor.listenTCP(port, website, interface=interface)
     if not config.debugmode:
         if sys.platform == 'win32':
             raise ConfigurationError("Under windows, you must use the service management "
--- a/etwist/twconfig.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/etwist/twconfig.py	Thu Jan 13 19:24:21 2011 +0100
@@ -45,6 +45,12 @@
           'help': 'http server port number (default to 8080)',
           'group': 'web', 'level': 0,
           }),
+        ('interface',
+         {'type' : 'string',
+          'default': "",
+          'help': 'http server address on which to listen (default to everywhere)',
+          'group': 'web', 'level': 0,
+          }),
         ('max-post-length',
          {'type' : 'bytes',
           'default': '100MB',
--- a/hooks/syncschema.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/hooks/syncschema.py	Thu Jan 13 19:24:21 2011 +0100
@@ -30,7 +30,6 @@
 from yams import buildobjs as ybo, schema2sql as y2sql
 
 from logilab.common.decorators import clear_cache
-from logilab.common.testlib import mock_object
 
 from cubicweb import ValidationError
 from cubicweb.selectors import is_instance
@@ -131,6 +130,11 @@
         raise ValidationError(entity.eid, errors)
 
 
+class _MockEntity(object): # XXX use a named tuple with python 2.6
+    def __init__(self, eid):
+        self.eid = eid
+
+
 class SyncSchemaHook(hook.Hook):
     """abstract class for schema synchronization hooks (in the `syncschema`
     category)
@@ -266,8 +270,8 @@
             sampletype = rschema.subjects()[0]
             desttype = rschema.objects()[0]
             rdef = copy(rschema.rdef(sampletype, desttype))
-            rdef.subject = mock_object(eid=entity.eid)
-            mock = mock_object(eid=None)
+            rdef.subject = _MockEntity(eid=entity.eid)
+            mock = _MockEntity(eid=None)
             ss.execschemarql(session.execute, mock, ss.rdef2rql(rdef, cmap, gmap))
 
     def revertprecommit_event(self):
@@ -701,14 +705,14 @@
             syssource.update_rdef_unique(session, rdef)
             self.unique_changed = True
 
+
 class CWUniqueTogetherConstraintAddOp(MemSchemaOperation):
     entity = None # make pylint happy
     def precommit_event(self):
         session = self.session
         prefix = SQL_PREFIX
         table = '%s%s' % (prefix, self.entity.constraint_of[0].name)
-        cols = ['%s%s' % (prefix, r.rtype.name)
-                for r in self.entity.relations]
+        cols = ['%s%s' % (prefix, r.name) for r in self.entity.relations]
         dbhelper= session.pool.source('system').dbhelper
         sqls = dbhelper.sqls_create_multicol_unique_index(table, cols)
         for sql in sqls:
@@ -718,9 +722,10 @@
 
     def postcommit_event(self):
         eschema = self.session.vreg.schema.schema_by_eid(self.entity.constraint_of[0].eid)
-        attrs = [r.rtype.name for r in self.entity.relations]
+        attrs = [r.name for r in self.entity.relations]
         eschema._unique_together.append(attrs)
 
+
 class CWUniqueTogetherConstraintDelOp(MemSchemaOperation):
     entity = oldcstr = None # for pylint
     cols = [] # for pylint
@@ -743,6 +748,7 @@
                            if set(ut) != cols]
         eschema._unique_together = unique_together
 
+
 # operations for in-memory schema synchronization  #############################
 
 class MemSchemaCWETypeDel(MemSchemaOperation):
@@ -1138,9 +1144,9 @@
         schema = self._cw.vreg.schema
         cstr = self._cw.entity_from_eid(self.eidfrom)
         entity = schema.schema_by_eid(self.eidto)
-        cols = [r.rtype.name
-                for r in cstr.relations]
-        CWUniqueTogetherConstraintDelOp(self._cw, entity=entity, oldcstr=cstr, cols=cols)
+        cols = [r.name for r in cstr.relations]
+        CWUniqueTogetherConstraintDelOp(self._cw, entity=entity,
+                                        oldcstr=cstr, cols=cols)
 
 
 # permissions synchronization hooks ############################################
--- a/hooks/test/unittest_hooks.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/hooks/test/unittest_hooks.py	Thu Jan 13 19:24:21 2011 +0100
@@ -20,6 +20,7 @@
 
 note: most schemahooks.py hooks are actually tested in unittest_migrations.py
 """
+from __future__ import with_statement
 
 from logilab.common.testlib import TestCase, unittest_main
 
@@ -115,8 +116,9 @@
 
     def test_unsatisfied_constraints(self):
         releid = self.execute('SET U in_group G WHERE G name "owners", U login "admin"')[0][0]
-        ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEqual(ex.errors,
+        with self.assertRaises(ValidationError) as cm:
+            self.commit()
+        self.assertEqual(cm.exception.errors,
                           {'in_group-object': u'RQLConstraint NOT O name "owners" failed'})
 
     def test_html_tidy_hook(self):
@@ -227,25 +229,25 @@
 class CWPropertyHooksTC(CubicWebTC):
 
     def test_unexistant_eproperty(self):
-        ex = self.assertRaises(ValidationError,
-                          self.execute, 'INSERT CWProperty X: X pkey "bla.bla", X value "hop", X for_user U')
-        self.assertEqual(ex.errors, {'pkey-subject': 'unknown property key bla.bla'})
-        ex = self.assertRaises(ValidationError,
-                          self.execute, 'INSERT CWProperty X: X pkey "bla.bla", X value "hop"')
-        self.assertEqual(ex.errors, {'pkey-subject': 'unknown property key bla.bla'})
+        with self.assertRaises(ValidationError) as cm:
+            self.execute('INSERT CWProperty X: X pkey "bla.bla", X value "hop", X for_user U')
+        self.assertEqual(cm.exception.errors, {'pkey-subject': 'unknown property key bla.bla'})
+        with self.assertRaises(ValidationError) as cm:
+            self.execute('INSERT CWProperty X: X pkey "bla.bla", X value "hop"')
+        self.assertEqual(cm.exception.errors, {'pkey-subject': 'unknown property key bla.bla'})
 
     def test_site_wide_eproperty(self):
-        ex = self.assertRaises(ValidationError,
-                               self.execute, 'INSERT CWProperty X: X pkey "ui.site-title", X value "hop", X for_user U')
-        self.assertEqual(ex.errors, {'for_user-subject': "site-wide property can't be set for user"})
+        with self.assertRaises(ValidationError) as cm:
+            self.execute('INSERT CWProperty X: X pkey "ui.site-title", X value "hop", X for_user U')
+        self.assertEqual(cm.exception.errors, {'for_user-subject': "site-wide property can't be set for user"})
 
     def test_bad_type_eproperty(self):
-        ex = self.assertRaises(ValidationError,
-                               self.execute, 'INSERT CWProperty X: X pkey "ui.language", X value "hop", X for_user U')
-        self.assertEqual(ex.errors, {'value-subject': u'unauthorized value'})
-        ex = self.assertRaises(ValidationError,
-                          self.execute, 'INSERT CWProperty X: X pkey "ui.language", X value "hop"')
-        self.assertEqual(ex.errors, {'value-subject': u'unauthorized value'})
+        with self.assertRaises(ValidationError) as cm:
+            self.execute('INSERT CWProperty X: X pkey "ui.language", X value "hop", X for_user U')
+        self.assertEqual(cm.exception.errors, {'value-subject': u'unauthorized value'})
+        with self.assertRaises(ValidationError) as cm:
+            self.execute('INSERT CWProperty X: X pkey "ui.language", X value "hop"')
+        self.assertEqual(cm.exception.errors, {'value-subject': u'unauthorized value'})
 
 
 class SchemaHooksTC(CubicWebTC):
--- a/i18n/de.po	Wed Jan 05 18:42:21 2011 +0100
+++ b/i18n/de.po	Thu Jan 13 19:24:21 2011 +0100
@@ -2566,7 +2566,7 @@
 msgid "has_text"
 msgstr "enthält Text"
 
-msgid "header-center"
+msgid "header-left"
 msgstr ""
 
 msgid "header-right"
@@ -3918,6 +3918,13 @@
 msgid "toggle check boxes"
 msgstr "Kontrollkästchen umkehren"
 
+msgid "tr_count"
+msgstr ""
+
+msgctxt "TrInfo"
+msgid "tr_count"
+msgstr ""
+
 msgid "transaction undoed"
 msgstr "Transaktion rückgängig gemacht"
 
--- a/i18n/en.po	Wed Jan 05 18:42:21 2011 +0100
+++ b/i18n/en.po	Thu Jan 13 19:24:21 2011 +0100
@@ -2499,8 +2499,8 @@
 msgid "has_text"
 msgstr "has text"
 
-msgid "header-center"
-msgstr "header (center)"
+msgid "header-left"
+msgstr "header (left)"
 
 msgid "header-right"
 msgstr "header (right)"
@@ -3811,6 +3811,13 @@
 msgid "toggle check boxes"
 msgstr ""
 
+msgid "tr_count"
+msgstr "transition number"
+
+msgctxt "TrInfo"
+msgid "tr_count"
+msgstr "transition number"
+
 msgid "transaction undoed"
 msgstr ""
 
--- a/i18n/es.po	Wed Jan 05 18:42:21 2011 +0100
+++ b/i18n/es.po	Thu Jan 13 19:24:21 2011 +0100
@@ -2594,7 +2594,7 @@
 msgid "has_text"
 msgstr "Contiene el texto"
 
-msgid "header-center"
+msgid "header-left"
 msgstr ""
 
 msgid "header-right"
@@ -3939,6 +3939,13 @@
 msgid "toggle check boxes"
 msgstr "Cambiar valor"
 
+msgid "tr_count"
+msgstr ""
+
+msgctxt "TrInfo"
+msgid "tr_count"
+msgstr ""
+
 msgid "transaction undoed"
 msgstr "Transacciones Anuladas"
 
--- a/i18n/fr.po	Wed Jan 05 18:42:21 2011 +0100
+++ b/i18n/fr.po	Thu Jan 13 19:24:21 2011 +0100
@@ -2596,8 +2596,8 @@
 msgid "has_text"
 msgstr "contient le texte"
 
-msgid "header-center"
-msgstr "en-tête (centre)"
+msgid "header-left"
+msgstr "en-tête (gauche)"
 
 msgid "header-right"
 msgstr "en-tête (droite)"
@@ -3948,6 +3948,13 @@
 msgid "toggle check boxes"
 msgstr "inverser les cases à cocher"
 
+msgid "tr_count"
+msgstr "n° de transition"
+
+msgctxt "TrInfo"
+msgid "tr_count"
+msgstr "n° de transition"
+
 msgid "transaction undoed"
 msgstr "transaction annulées"
 
--- a/misc/migration/3.10.7_Any.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/misc/migration/3.10.7_Any.py	Thu Jan 13 19:24:21 2011 +0100
@@ -1,2 +1,8 @@
+add_relation_definition('CWUniqueTogetherConstraint', 'relations', 'CWRType')
+rql('SET C relations RT WHERE C relations RDEF, RDEF relation_type RT')
+commit()
+drop_relation_definition('CWUniqueTogetherConstraint', 'relations', 'CWAttribute')
+drop_relation_definition('CWUniqueTogetherConstraint', 'relations', 'CWRelation')
+
 add_attribute('TrInfo', 'tr_count')
 sync_schema_props_perms('TrInfo')
--- a/schemas/bootstrap.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/schemas/bootstrap.py	Thu Jan 13 19:24:21 2011 +0100
@@ -159,10 +159,10 @@
     __permissions__ = PUB_SYSTEM_ENTITY_PERMS
     constraint_of = SubjectRelation('CWEType', cardinality='1*', composite='object',
                                     inlined=True)
-    relations = SubjectRelation(('CWAttribute', 'CWRelation'), cardinality='+*',
-                                 constraints=[RQLConstraint(
-           'O from_entity X, S constraint_of X, O relation_type T, '
-           'T final TRUE OR (T final FALSE AND T inlined TRUE)')])
+    relations = SubjectRelation('CWRType', cardinality='+*',
+                                constraints=[RQLConstraint(
+           'S constraint_of ET, RDEF relation_type O, RDEF from_entity ET, '
+           'O final TRUE OR (O final FALSE AND O inlined TRUE)')])
 
 
 class CWConstraintType(EntityType):
--- a/server/hook.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/server/hook.py	Thu Jan 13 19:24:21 2011 +0100
@@ -239,8 +239,8 @@
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 .. autoclass:: cubicweb.server.hook.Hook
 .. autoclass:: cubicweb.server.hook.Operation
-.. autoclass:: cubicweb.server.hook.DataOperation
 .. autoclass:: cubicweb.server.hook.LateOperation
+.. autoclass:: cubicweb.server.hook.DataOperationMixIn
 """
 
 from __future__ import with_statement
--- a/server/migractions.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/server/migractions.py	Thu Jan 13 19:24:21 2011 +0100
@@ -536,38 +536,21 @@
             unique_together = set([frozenset(ut)
                                    for ut in eschema._unique_together])
             for ut in repo_unique_together - unique_together:
-                restrictions  = ', '.join(['C relations R%(i)d, '
-                                           'R%(i)d relation_type T%(i)d, '
-                                           'R%(i)d from_entity X, '
-                                           'T%(i)d name %%(T%(i)d)s' % {'i': i,
-                                                                        'col':col}
-                                           for (i, col) in enumerate(ut)])
-                substs = {'etype': etype}
+                restrictions = []
+                substs = {'x': repoeschema.eid}
                 for i, col in enumerate(ut):
+                    restrictions.append('C relations T%(i)d, '
+                                       'T%(i)d name %%(T%(i)d)s' % {'i': i})
                     substs['T%d'%i] = col
                 self.rqlexec('DELETE CWUniqueTogetherConstraint C '
                              'WHERE C constraint_of E, '
-                             '      E name %%(etype)s,'
-                             '      %s' % restrictions,
+                             '      E eid %%(x)s,'
+                             '      %s' % ', '.join( restrictions),
                              substs)
             for ut in unique_together - repo_unique_together:
-                relations = ', '.join(['C relations R%d' % i
-                                       for (i, col) in enumerate(ut)])
-                restrictions  = ', '.join(['R%(i)d relation_type T%(i)d, '
-                                           'R%(i)d from_entity E, '
-                                           'T%(i)d name %%(T%(i)d)s' % {'i': i,
-                                                                        'col':col}
-                                           for (i, col) in enumerate(ut)])
-                substs = {'etype': etype}
-                for i, col in enumerate(ut):
-                    substs['T%d'%i] = col
-                self.rqlexec('INSERT CWUniqueTogetherConstraint C:'
-                             '       C constraint_of E, '
-                             '       %s '
-                             'WHERE '
-                             '      E name %%(etype)s,'
-                             '      %s' % (relations, restrictions),
-                             substs)
+                rql, substs = ss.uniquetogether2rql(eschema, ut)
+                substs['x'] = repoeschema.eid
+                self.rqlexec(rql, substs)
 
     def _synchronize_rdef_schema(self, subjtype, rtype, objtype,
                                  syncperms=True, syncprops=True):
@@ -1218,8 +1201,14 @@
 
     # Workflows handling ######################################################
 
+    def cmd_make_workflowable(self, etype):
+        """add workflow relations to an entity type to make it workflowable"""
+        self.cmd_add_relation_definition(etype, 'in_state', 'State')
+        self.cmd_add_relation_definition(etype, 'custom_workflow', 'Workflow')
+        self.cmd_add_relation_definition('TrInfo', 'wf_info_for', etype)
+
     def cmd_add_workflow(self, name, wfof, default=True, commit=False,
-                         **kwargs):
+                         ensure_workflowable=True, **kwargs):
         """
         create a new workflow and links it to entity types
          :type name: unicode
@@ -1239,7 +1228,14 @@
                                     **kwargs)
         if not isinstance(wfof, (list, tuple)):
             wfof = (wfof,)
+        def _missing_wf_rel(etype):
+            return 'missing workflow relations, see make_workflowable(%s)' % etype
         for etype in wfof:
+            eschema = self.repo.schema[etype]
+            if ensure_workflowable:
+                assert 'in_state' in eschema.subjrels, _missing_wf_rel(etype)
+                assert 'custom_workflow' in eschema.subjrels, _missing_wf_rel(etype)
+                assert 'wf_info_for' in eschema.objrels, _missing_wf_rel(etype)
             rset = self.rqlexec(
                 'SET X workflow_of ET WHERE X eid %(x)s, ET name %(et)s',
                 {'x': wf.eid, 'et': etype}, ask_confirm=False)
--- a/server/msplanner.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/server/msplanner.py	Thu Jan 13 19:24:21 2011 +0100
@@ -824,7 +824,7 @@
             while sourceterms:
                 # take a term randomly, and all terms supporting the
                 # same solutions
-                term, solindices = self._choose_term(sourceterms)
+                term, solindices = self._choose_term(source, sourceterms)
                 if source.uri == 'system':
                     # ensure all variables are available for the latest step
                     # (missing one will be available from temporary tables
@@ -854,7 +854,7 @@
                 # set of terms which should be additionaly selected when
                 # possible
                 needsel = set()
-                if not self._sourcesterms:
+                if not self._sourcesterms and scope is select:
                     terms += scope.defined_vars.values() + scope.aliases.values()
                     if isinstance(term, Relation) and len(sources) > 1:
                         variants = set()
@@ -867,13 +867,10 @@
                             # before a join with prefetched inputs
                             # (see test_crossed_relation_noeid_needattr in
                             #  unittest_msplanner / unittest_multisources)
-                            needsel2 = needsel.copy()
-                            needsel2.update(variants)
                             lhs, rhs = term.get_variable_parts()
                             steps.append( (sources, [term, getattr(lhs, 'variable', lhs),
                                                      getattr(rhs, 'variable', rhs)],
-                                           solindices, scope,
-                                           needsel2, False) )
+                                           solindices, scope, variants, False) )
                             sources = [self.system_source]
                     final = True
                 else:
@@ -906,7 +903,7 @@
                                 break
                         else:
                             if not scope is select:
-                                self._exists_relation(rel, terms, needsel)
+                                self._exists_relation(rel, terms, needsel, source)
                             # if relation is supported by all sources and some of
                             # its lhs/rhs variable isn't in "terms", and the
                             # other end *is* in "terms", mark it have to be
@@ -950,9 +947,14 @@
                     self._cleanup_sourcesterms(sources, solindices)
                 steps.append((sources, terms, solindices, scope, needsel, final)
                              )
+        if not steps[-1][-1]:
+            # add a final step
+            terms = select.defined_vars.values() + select.aliases.values()
+            steps.append( ([self.system_source], terms, set(self._solindices),
+                           select, set(), True) )
         return steps
 
-    def _exists_relation(self, rel, terms, needsel):
+    def _exists_relation(self, rel, terms, needsel, source):
         rschema = self._schema.rschema(rel.r_type)
         lhs, rhs = rel.get_variable_parts()
         try:
@@ -965,13 +967,24 @@
             # variable is refed by an outer scope and should be substituted
             # using an 'identity' relation (else we'll get a conflict of
             # temporary tables)
-            if rhsvar in terms and not lhsvar in terms and ms_scope(lhsvar) is lhsvar.stmt:
-                self._identity_substitute(rel, lhsvar, terms, needsel)
-            elif lhsvar in terms and not rhsvar in terms and ms_scope(rhsvar) is rhsvar.stmt:
-                self._identity_substitute(rel, rhsvar, terms, needsel)
+            relscope = ms_scope(rel)
+            lhsscope = ms_scope(lhsvar)
+            rhsscope = ms_scope(rhsvar)
+            if rhsvar in terms and not lhsvar in terms and lhsscope is lhsvar.stmt:
+                self._identity_substitute(rel, lhsvar, terms, needsel, relscope)
+            elif lhsvar in terms and not rhsvar in terms and rhsscope is rhsvar.stmt:
+                self._identity_substitute(rel, rhsvar, terms, needsel, relscope)
+            elif self.crossed_relation(source, rel):
+                if lhsscope is not relscope:
+                    self._identity_substitute(rel, lhsvar, terms, needsel,
+                                              relscope, lhsscope)
+                if rhsscope is not relscope:
+                    self._identity_substitute(rel, rhsvar, terms, needsel,
+                                              relscope, rhsscope)
 
-    def _identity_substitute(self, relation, var, terms, needsel):
-        newvar = self._insert_identity_variable(ms_scope(relation), var)
+    def _identity_substitute(self, relation, var, terms, needsel, exist,
+                             idrelscope=None):
+        newvar = self._insert_identity_variable(exist, var, idrelscope)
         # ensure relation is using '=' operator, else we rely on a
         # sqlgenerator side effect (it won't insert an inequality operator
         # in this case)
@@ -979,12 +992,28 @@
         terms.append(newvar)
         needsel.add(newvar.name)
 
-    def _choose_term(self, sourceterms):
+    def _choose_term(self, source, sourceterms):
         """pick one term among terms supported by a source, which will be used
         as a base to generate an execution step
         """
         secondchoice = None
         if len(self._sourcesterms) > 1:
+            # first, return non invariant variable of crossed relation, then the
+            # crossed relation itself
+            for term in sourceterms:
+                if (isinstance(term, Relation)
+                    and self.crossed_relation(source, term)
+                    and not ms_scope(term) is self.rqlst):
+                    for vref in term.get_variable_parts():
+                        try:
+                            var = vref.variable
+                        except AttributeError:
+                            # Constant
+                            continue
+                        if ((len(var.stinfo['relations']) > 1 or var.stinfo['selected'])
+                            and var in sourceterms):
+                            return var, sourceterms.pop(var)
+                    return term, sourceterms.pop(term)
             # priority to variable from subscopes
             for term in sourceterms:
                 if not ms_scope(term) is self.rqlst:
--- a/server/repository.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/server/repository.py	Thu Jan 13 19:24:21 2011 +0100
@@ -1353,8 +1353,9 @@
 
     # pyro handling ###########################################################
 
-    def pyro_register(self, host=''):
-        """register the repository as a pyro object"""
+    @property
+    @cached
+    def pyro_appid(self):
         from logilab.common import pyro_ext as pyro
         config = self.config
         appid = '%s.%s' % pyro.ns_group_and_id(
@@ -1362,13 +1363,27 @@
             config['pyro-ns-group'])
         # ensure config['pyro-instance-id'] is a full qualified pyro name
         config['pyro-instance-id'] = appid
-        daemon = pyro.register_object(self, appid,
-                                      daemonhost=config['pyro-host'],
-                                      nshost=config['pyro-ns-host'])
-        self.info('repository registered as a pyro object %s', appid)
+        return appid
+
+    def pyro_register(self, host=''):
+        """register the repository as a pyro object"""
+        from logilab.common import pyro_ext as pyro
+        daemon = pyro.register_object(self, self.pyro_appid,
+                                      daemonhost=self.config['pyro-host'],
+                                      nshost=self.config['pyro-ns-host'])
+        self.info('repository registered as a pyro object %s', self.pyro_appid)
         self.pyro_registered = True
+        # register a looping task to regularly ensure we're still registered
+        # into the pyro name server
+        self.looping_task(60*10, self._ensure_pyro_ns)
         return daemon
 
+    def _ensure_pyro_ns(self):
+        from logilab.common import pyro_ext as pyro
+        pyro.ns_reregister(self.pyro_appid, nshost=self.config['pyro-ns-host'])
+        self.info('repository re-registered as a pyro object %s',
+                  self.pyro_appid)
+
     # multi-sources planner helpers ###########################################
 
     @cached
--- a/server/schemaserial.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/server/schemaserial.py	Thu Jan 13 19:24:21 2011 +0100
@@ -235,7 +235,7 @@
             uniquecstreid, eeid, releid = values
             eschema = schema.schema_by_eid(eeid)
             relations = unique_togethers.setdefault(uniquecstreid, (eschema, []))
-            relations[1].append(ertidx[releid].rtype.type)
+            relations[1].append(ertidx[releid])
         for eschema, unique_together in unique_togethers.itervalues():
             eschema._unique_together.append(tuple(sorted(unique_together)))
     schema.infer_specialization_rules()
@@ -355,6 +355,7 @@
     for eschema in eschemas:
         for unique_together in eschema._unique_together:
             execschemarql(execute, eschema, [uniquetogether2rql(eschema, unique_together)])
+    # serialize yams inheritance relationships
     for rql, kwargs in specialize2rql(schema):
         execute(rql, kwargs, build_descr=False)
         if pb is not None:
@@ -417,23 +418,17 @@
     restrictions = []
     substs = {}
     for i, name in enumerate(unique_together):
-        rschema = eschema.rdef(name)
-        var = 'R%d' % i
+        rschema = eschema.schema.rschema(name)
         rtype = 'T%d' % i
-        substs[rtype] = rschema.rtype.type
-        relations.append('C relations %s' % var)
-        restrictions.append('%(var)s from_entity X, '
-                            '%(var)s relation_type %(rtype)s, '
-                            '%(rtype)s name %%(%(rtype)s)s' \
-                            % {'var': var,
-                               'rtype':rtype})
+        substs[rtype] = rschema.type
+        relations.append('C relations %s' % rtype)
+        restrictions.append('%(rtype)s name %%(%(rtype)s)s' % {'rtype': rtype})
     relations = ', '.join(relations)
     restrictions = ', '.join(restrictions)
     rql = ('INSERT CWUniqueTogetherConstraint C: '
            '    C constraint_of X, %s  '
            'WHERE '
-           '    X eid %%(x)s, %s' )
-
+           '    X eid %%(x)s, %s')
     return rql % (relations, restrictions), substs
 
 
--- a/server/test/data/migratedapp/schema.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/server/test/data/migratedapp/schema.py	Thu Jan 13 19:24:21 2011 +0100
@@ -15,9 +15,7 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
+"""cw.server.migraction test"""
 from yams.buildobjs import (EntityType, RelationType, RelationDefinition,
                             SubjectRelation,
                             RichString, String, Int, Boolean, Datetime, Date)
@@ -68,7 +66,7 @@
     type = String(maxsize=1)
     unique_id = String(maxsize=1, required=True, unique=True)
     mydate = Date(default='TODAY')
-    shortpara = String(maxsize=64)
+    shortpara = String(maxsize=64, default='hop')
     ecrit_par = SubjectRelation('Personne', constraints=[RQLConstraint('S concerne A, O concerne A')])
     attachment = SubjectRelation('File')
 
--- a/server/test/unittest_fti.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/server/test/unittest_fti.py	Thu Jan 13 19:24:21 2011 +0100
@@ -2,6 +2,8 @@
 
 import socket
 
+from logilab.common.testlib import SkipTest
+
 from cubicweb.devtools import ApptestConfiguration
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.selectors import is_instance
@@ -9,8 +11,6 @@
 
 AT_LOGILAB = socket.gethostname().endswith('.logilab.fr')
 
-from logilab.common.testlib import SkipTest
-
 
 class PostgresFTITC(CubicWebTC):
     config = ApptestConfiguration('data', sourcefile='sources_fti')
--- a/server/test/unittest_hook.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/server/test/unittest_hook.py	Thu Jan 13 19:24:21 2011 +0100
@@ -18,6 +18,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """unit/functional tests for cubicweb.server.hook"""
 
+from __future__ import with_statement
+
 from logilab.common.testlib import TestCase, unittest_main, mock_object
 
 
@@ -101,20 +103,23 @@
     def test_register_bad_hook1(self):
         class _Hook(hook.Hook):
             events = ('before_add_entiti',)
-        ex = self.assertRaises(Exception, self.o.register, _Hook)
-        self.assertEqual(str(ex), 'bad event before_add_entiti on %s._Hook' % __name__)
+        with self.assertRaises(Exception) as cm:
+            self.o.register(_Hook)
+        self.assertEqual(str(cm.exception), 'bad event before_add_entiti on %s._Hook' % __name__)
 
     def test_register_bad_hook2(self):
         class _Hook(hook.Hook):
             events = None
-        ex = self.assertRaises(Exception, self.o.register, _Hook)
-        self.assertEqual(str(ex), 'bad .events attribute None on %s._Hook' % __name__)
+        with self.assertRaises(Exception) as cm:
+            self.o.register(_Hook)
+        self.assertEqual(str(cm.exception), 'bad .events attribute None on %s._Hook' % __name__)
 
     def test_register_bad_hook3(self):
         class _Hook(hook.Hook):
             events = 'before_add_entity'
-        ex = self.assertRaises(Exception, self.o.register, _Hook)
-        self.assertEqual(str(ex), 'bad event b on %s._Hook' % __name__)
+        with self.assertRaises(Exception) as cm:
+            self.o.register(_Hook)
+        self.assertEqual(str(cm.exception), 'bad event b on %s._Hook' % __name__)
 
     def test_call_hook(self):
         self.o.register(AddAnyHook)
--- a/server/test/unittest_migractions.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/server/test/unittest_migractions.py	Thu Jan 13 19:24:21 2011 +0100
@@ -77,6 +77,10 @@
         assert self.cnx is self.mh._cnx
         assert self.session is self.mh.session, (self.session.id, self.mh.session.id)
 
+    def tearDown(self):
+        CubicWebTC.tearDown(self)
+        self.repo.vreg['etypes'].clear_caches()
+
     def test_add_attribute_int(self):
         self.failIf('whatever' in self.schema)
         self.request().create_entity('Note')
@@ -88,8 +92,12 @@
         self.assertEqual(self.schema['whatever'].subjects(), ('Note',))
         self.assertEqual(self.schema['whatever'].objects(), ('Int',))
         self.assertEqual(self.schema['Note'].default('whatever'), 2)
+        # test default value set on existing entities
         note = self.execute('Note X').get_entity(0, 0)
         self.assertEqual(note.whatever, 2)
+        # test default value set for next entities
+        self.assertEqual(self.request().create_entity('Note').whatever, 2)
+        # test attribute order
         orderdict2 = dict(self.mh.rqlexec('Any RTN, O WHERE X name "Note", RDEF from_entity X, '
                                           'RDEF relation_type RT, RDEF ordernum O, RT name RTN'))
         whateverorder = migrschema['whatever'].rdef('Note', 'Int').order
@@ -108,6 +116,9 @@
         self.mh.rollback()
 
     def test_add_attribute_varchar(self):
+        self.failIf('whatever' in self.schema)
+        self.request().create_entity('Note')
+        self.commit()
         self.failIf('shortpara' in self.schema)
         self.mh.cmd_add_attribute('Note', 'shortpara')
         self.failUnless('shortpara' in self.schema)
@@ -117,6 +128,11 @@
         notesql = self.mh.sqlexec("SELECT sql FROM sqlite_master WHERE type='table' and name='%sNote'" % SQL_PREFIX)[0][0]
         fields = dict(x.strip().split()[:2] for x in notesql.split('(', 1)[1].rsplit(')', 1)[0].split(','))
         self.assertEqual(fields['%sshortpara' % SQL_PREFIX], 'varchar(64)')
+        req = self.request()
+        # test default value set on existing entities
+        self.assertEqual(req.execute('Note X').get_entity(0, 0).shortpara, 'hop')
+        # test default value set for next entities
+        self.assertEqual(req.create_entity('Note').shortpara, 'hop')
         self.mh.rollback()
 
     def test_add_datetime_with_default_value_attribute(self):
@@ -173,7 +189,8 @@
 
 
     def test_workflow_actions(self):
-        wf = self.mh.cmd_add_workflow(u'foo', ('Personne', 'Email'))
+        wf = self.mh.cmd_add_workflow(u'foo', ('Personne', 'Email'),
+                                      ensure_workflowable=False)
         for etype in ('Personne', 'Email'):
             s1 = self.mh.rqlexec('Any N WHERE WF workflow_of ET, ET name "%s", WF name N' %
                                  etype)[0][0]
@@ -209,7 +226,8 @@
 
     def test_add_drop_entity_type(self):
         self.mh.cmd_add_entity_type('Folder2')
-        wf = self.mh.cmd_add_workflow(u'folder2 wf', 'Folder2')
+        wf = self.mh.cmd_add_workflow(u'folder2 wf', 'Folder2',
+                                      ensure_workflowable=False)
         todo = wf.add_state(u'todo', initial=True)
         done = wf.add_state(u'done')
         wf.add_transition(u'redoit', done, todo)
@@ -416,7 +434,7 @@
                                            ('nom', 'prenom', 'datenaiss'))
         rset = cursor.execute('Any C WHERE C is CWUniqueTogetherConstraint, C constraint_of ET, ET name "Personne"')
         self.assertEqual(len(rset), 1)
-        relations = [r.rtype.name for r in rset.get_entity(0, 0).relations]
+        relations = [r.name for r in rset.get_entity(0, 0).relations]
         self.assertItemsEqual(relations, ('nom', 'prenom', 'datenaiss'))
 
     def _erqlexpr_rset(self, action, ertype):
@@ -536,8 +554,9 @@
             self.commit()
 
     def test_remove_dep_cube(self):
-        ex = self.assertRaises(ConfigurationError, self.mh.cmd_remove_cube, 'file')
-        self.assertEqual(str(ex), "can't remove cube file, used as a dependency")
+        with self.assertRaises(ConfigurationError) as cm:
+            self.mh.cmd_remove_cube('file')
+        self.assertEqual(str(cm.exception), "can't remove cube file, used as a dependency")
 
     def test_introduce_base_class(self):
         self.mh.cmd_add_entity_type('Para')
--- a/server/test/unittest_msplanner.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/server/test/unittest_msplanner.py	Thu Jan 13 19:24:21 2011 +0100
@@ -15,6 +15,9 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""unit tests for module cubicweb.server.msplanner"""
+
+from __future__ import with_statement
 
 from logilab.common.decorators import clear_cache
 
@@ -2013,15 +2016,15 @@
 
     def test_source_conflict_1(self):
         self.repo._type_source_cache[999999] = ('Note', 'cards', 999999)
-        ex = self.assertRaises(BadRQLQuery,
-                               self._test, 'Any X WHERE X cw_source S, S name "system", X eid %(x)s',
-                               [], {'x': 999999})
-        self.assertEqual(str(ex), 'source conflict for term %(x)s')
+        with self.assertRaises(BadRQLQuery) as cm:
+            self._test('Any X WHERE X cw_source S, S name "system", X eid %(x)s',
+                       [], {'x': 999999})
+        self.assertEqual(str(cm.exception), 'source conflict for term %(x)s')
 
     def test_source_conflict_2(self):
-        ex = self.assertRaises(BadRQLQuery,
-                               self._test, 'Card X WHERE X cw_source S, S name "systeme"', [])
-        self.assertEqual(str(ex), 'source conflict for term X')
+        with self.assertRaises(BadRQLQuery) as cm:
+            self._test('Card X WHERE X cw_source S, S name "systeme"', [])
+        self.assertEqual(str(cm.exception), 'source conflict for term X')
 
     def test_source_conflict_3(self):
         self.skipTest('oops')
@@ -2473,6 +2476,37 @@
                      )]
                    )
 
+    def test_version_crossed_depends_on_4(self):
+        self._test('Any X,AD,AE WHERE EXISTS(E multisource_crossed_rel X), X in_state AD, AD name AE, E is Note',
+                   [('FetchStep',
+                     [('Any X,AD,AE WHERE X in_state AD, AD name AE, AD is State, X is Note',
+                       [{'X': 'Note', 'AD': 'State', 'AE': 'String'}])],
+                     [self.cards, self.cards2, self.system], None,
+                     {'X': 'table0.C0',
+                      'AD': 'table0.C1',
+                      'AD.name': 'table0.C2',
+                      'AE': 'table0.C2'},
+                     []),
+                    ('FetchStep',
+                     [('Any A WHERE E multisource_crossed_rel A, A is Note, E is Note',
+                       [{'A': 'Note', 'E': 'Note'}])],
+                     [self.cards, self.cards2, self.system], None,
+                     {'A': 'table1.C0'},
+                     []),
+                    ('OneFetchStep',
+                     [('Any X,AD,AE WHERE EXISTS(X identity A), AD name AE, A is Note, AD is State, X is Note',
+                       [{'A': 'Note', 'AD': 'State', 'AE': 'String', 'X': 'Note'}])],
+                     None, None,
+                     [self.system],
+                     {'A': 'table1.C0',
+                      'AD': 'table0.C1',
+                      'AD.name': 'table0.C2',
+                      'AE': 'table0.C2',
+                      'X': 'table0.C0'},
+                     []
+                     )]
+                       )
+
     def test_nonregr_dont_cross_rel_source_filtering_1(self):
         self.repo._type_source_cache[999999] = ('Note', 'cards', 999999)
         self._test('Any S WHERE E eid %(x)s, E in_state S, NOT S name "moved"',
--- a/server/test/unittest_repository.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/server/test/unittest_repository.py	Thu Jan 13 19:24:21 2011 +0100
@@ -154,8 +154,9 @@
             self.assertRaises(ValidationError,
                               self.execute, 'SET X name "toto" WHERE X is CWGroup, X name "guests"')
             self.failUnless(self.execute('Any X WHERE X is CWGroup, X name "toto"'))
-            ex = self.assertRaises(QueryError, self.commit)
-            self.assertEqual(str(ex), 'transaction must be rollbacked')
+            with self.assertRaises(QueryError) as cm:
+                self.commit()
+            self.assertEqual(str(cm.exception), 'transaction must be rollbacked')
             self.rollback()
             self.failIf(self.execute('Any X WHERE X is CWGroup, X name "toto"'))
 
@@ -170,8 +171,9 @@
             self.assertRaises(Unauthorized,
                               self.execute, 'SET X name "toto" WHERE X is CWGroup, X name "guests"')
             self.failUnless(self.execute('Any X WHERE X is CWGroup, X name "toto"'))
-            ex = self.assertRaises(QueryError, self.commit)
-            self.assertEqual(str(ex), 'transaction must be rollbacked')
+            with self.assertRaises(QueryError) as cm:
+                self.commit()
+            self.assertEqual(str(cm.exception), 'transaction must be rollbacked')
             self.rollback()
             self.failIf(self.execute('Any X WHERE X is CWGroup, X name "toto"'))
 
@@ -276,8 +278,9 @@
             repo.execute(cnxid, 'DELETE CWUser X WHERE X login "toto"')
             repo.commit(cnxid)
         try:
-            ex = self.assertRaises(Exception, run_transaction)
-            self.assertEqual(str(ex), 'try to access pool on a closed session')
+            with self.assertRaises(Exception) as cm:
+                run_transaction()
+            self.assertEqual(str(cm.exception), 'try to access pool on a closed session')
         finally:
             t.join()
 
@@ -668,8 +671,9 @@
         req.cnx.commit()
         req = self.request()
         req.create_entity('Note', type=u'todo', inline1=a01)
-        ex = self.assertRaises(ValidationError, req.cnx.commit)
-        self.assertEqual(ex.errors, {'inline1-subject': u'RQLUniqueConstraint S type T, S inline1 A1, A1 todo_by C, Y type T, Y inline1 A2, A2 todo_by C failed'})
+        with self.assertRaises(ValidationError) as cm:
+            req.cnx.commit()
+        self.assertEqual(cm.exception.errors, {'inline1-subject': u'RQLUniqueConstraint S type T, S inline1 A1, A1 todo_by C, Y type T, Y inline1 A2, A2 todo_by C failed'})
 
 if __name__ == '__main__':
     unittest_main()
--- a/server/test/unittest_storage.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/server/test/unittest_storage.py	Thu Jan 13 19:24:21 2011 +0100
@@ -75,6 +75,15 @@
                               {'f': entity.eid})[0][0]
         return fspath.getvalue()
 
+    def test_bfss_wrong_fspath_usage(self):
+        f1 = self.create_file()
+        self.execute('Any fspath(D) WHERE F eid %(f)s, F data D', {'f': f1.eid})
+        with self.assertRaises(NotImplementedError) as cm:
+            self.execute('Any fspath(F) WHERE F eid %(f)s', {'f': f1.eid})
+        self.assertEqual(str(cm.exception),
+                         'This callback is only available for BytesFileSystemStorage '
+                         'managed attribute. Is FSPATH() argument BFSS managed?')
+
     def test_bfss_storage(self):
         f1 = self.create_file()
         expected_filepath = osp.join(self.tempdir, '%s_data_%s' %
@@ -114,34 +123,34 @@
             self.create_file()
 
     def test_source_mapped_attribute_error_cases(self):
-        ex = self.assertRaises(QueryError, self.execute,
-                               'Any X WHERE X data ~= "hop", X is File')
-        self.assertEqual(str(ex), 'can\'t use File.data (X data ILIKE "hop") in restriction')
-        ex = self.assertRaises(QueryError, self.execute,
-                               'Any X, Y WHERE X data D, Y data D, '
-                               'NOT X identity Y, X is File, Y is File')
-        self.assertEqual(str(ex), "can't use D as a restriction variable")
+        with self.assertRaises(QueryError) as cm:
+            self.execute('Any X WHERE X data ~= "hop", X is File')
+        self.assertEqual(str(cm.exception), 'can\'t use File.data (X data ILIKE "hop") in restriction')
+        with self.assertRaises(QueryError) as cm:
+            self.execute('Any X, Y WHERE X data D, Y data D, '
+                         'NOT X identity Y, X is File, Y is File')
+        self.assertEqual(str(cm.exception), "can't use D as a restriction variable")
         # query returning mix of mapped / regular attributes (only file.data
         # mapped, not image.data for instance)
-        ex = self.assertRaises(QueryError, self.execute,
-                               'Any X WITH X BEING ('
-                               ' (Any NULL)'
-                               '  UNION '
-                               ' (Any D WHERE X data D, X is File)'
-                               ')')
-        self.assertEqual(str(ex), 'query fetch some source mapped attribute, some not')
-        ex = self.assertRaises(QueryError, self.execute,
-                               '(Any D WHERE X data D, X is File)'
-                               ' UNION '
-                               '(Any D WHERE X title D, X is Bookmark)')
-        self.assertEqual(str(ex), 'query fetch some source mapped attribute, some not')
+        with self.assertRaises(QueryError) as cm:
+            self.execute('Any X WITH X BEING ('
+                         ' (Any NULL)'
+                         '  UNION '
+                         ' (Any D WHERE X data D, X is File)'
+                         ')')
+        self.assertEqual(str(cm.exception), 'query fetch some source mapped attribute, some not')
+        with self.assertRaises(QueryError) as cm:
+            self.execute('(Any D WHERE X data D, X is File)'
+                         ' UNION '
+                         '(Any D WHERE X title D, X is Bookmark)')
+        self.assertEqual(str(cm.exception), 'query fetch some source mapped attribute, some not')
 
         storages.set_attribute_storage(self.repo, 'State', 'name',
                                        storages.BytesFileSystemStorage(self.tempdir))
         try:
-            ex = self.assertRaises(QueryError,
-                                   self.execute, 'Any D WHERE X name D, X is IN (State, Transition)')
-            self.assertEqual(str(ex), 'query fetch some source mapped attribute, some not')
+            with self.assertRaises(QueryError) as cm:
+                self.execute('Any D WHERE X name D, X is IN (State, Transition)')
+            self.assertEqual(str(cm.exception), 'query fetch some source mapped attribute, some not')
         finally:
             storages.unset_attribute_storage(self.repo, 'State', 'name')
 
@@ -172,10 +181,10 @@
         self.assertEqual(rset[1][0], f1.eid)
         self.assertEqual(rset[0][1], len('the-data'))
         self.assertEqual(rset[1][1], len('the-data'))
-        ex = self.assertRaises(QueryError, self.execute,
-                               'Any X,UPPER(D) WHERE X eid %(x)s, X data D',
-                               {'x': f1.eid})
-        self.assertEqual(str(ex), 'UPPER can not be called on mapped attribute')
+        with self.assertRaises(QueryError) as cm:
+            self.execute('Any X,UPPER(D) WHERE X eid %(x)s, X data D',
+                         {'x': f1.eid})
+        self.assertEqual(str(cm.exception), 'UPPER can not be called on mapped attribute')
 
 
     def test_bfss_fs_importing_transparency(self):
--- a/server/test/unittest_undo.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/server/test/unittest_undo.py	Thu Jan 13 19:24:21 2011 +0100
@@ -212,9 +212,10 @@
         self.assertEqual(errors,
                           [u"Can't restore relation in_group, object entity "
                           "%s doesn't exist anymore." % g.eid])
-        ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEqual(ex.entity, self.toto.eid)
-        self.assertEqual(ex.errors,
+        with self.assertRaises(ValidationError) as cm:
+            self.commit()
+        self.assertEqual(cm.exception.entity, self.toto.eid)
+        self.assertEqual(cm.exception.errors,
                           {'in_group-subject': u'at least one relation in_group is '
                            'required on CWUser (%s)' % self.toto.eid})
 
@@ -252,10 +253,10 @@
                                             value=u'text/html')
         tutu.set_relations(use_email=email, reverse_for_user=prop)
         self.commit()
-        ex = self.assertRaises(ValidationError,
-                               self.cnx.undo_transaction, txuuid)
-        self.assertEqual(ex.entity, tutu.eid)
-        self.assertEqual(ex.errors,
+        with self.assertRaises(ValidationError) as cm:
+            self.cnx.undo_transaction(txuuid)
+        self.assertEqual(cm.exception.entity, tutu.eid)
+        self.assertEqual(cm.exception.errors,
                           {None: 'some later transaction(s) touch entity, undo them first'})
 
     def test_undo_creation_integrity_2(self):
@@ -265,17 +266,17 @@
         session.execute('DELETE U in_group G WHERE U eid %(x)s', {'x': self.toto.eid})
         self.toto.set_relations(in_group=g)
         self.commit()
-        ex = self.assertRaises(ValidationError,
-                               self.cnx.undo_transaction, txuuid)
-        self.assertEqual(ex.entity, g.eid)
-        self.assertEqual(ex.errors,
+        with self.assertRaises(ValidationError) as cm:
+            self.cnx.undo_transaction(txuuid)
+        self.assertEqual(cm.exception.entity, g.eid)
+        self.assertEqual(cm.exception.errors,
                           {None: 'some later transaction(s) touch entity, undo them first'})
         # self.assertEqual(errors,
         #                   [u"Can't restore relation in_group, object entity "
         #                   "%s doesn't exist anymore." % g.eid])
-        # ex = self.assertRaises(ValidationError, self.commit)
-        # self.assertEqual(ex.entity, self.toto.eid)
-        # self.assertEqual(ex.errors,
+        # with self.assertRaises(ValidationError) as cm: self.commit()
+        # self.assertEqual(cm.exception.entity, self.toto.eid)
+        # self.assertEqual(cm.exception.errors,
         #                   {'in_group-subject': u'at least one relation in_group is '
         #                    'required on CWUser (%s)' % self.toto.eid})
 
--- a/sobjects/notification.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/sobjects/notification.py	Thu Jan 13 19:24:21 2011 +0100
@@ -27,7 +27,7 @@
 
 from cubicweb.selectors import yes
 from cubicweb.view import Component
-from cubicweb.mail import NotificationView, SkipEmail
+from cubicweb.mail import NotificationView as BaseNotificationView, SkipEmail
 from cubicweb.server.hook import SendMailOp
 
 
@@ -59,7 +59,7 @@
 
 # abstract or deactivated notification views and mixin ########################
 
-class NotificationView(NotificationView):
+class NotificationView(BaseNotificationView):
     """overriden to delay actual sending of mails to a commit operation by
     default
     """
--- a/test/unittest_schema.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/test/unittest_schema.py	Thu Jan 13 19:24:21 2011 +0100
@@ -17,6 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """unit tests for module cubicweb.schema"""
 
+from __future__ import with_statement
+
 import sys
 from os.path import join, isabs, basename, dirname
 
@@ -279,9 +281,9 @@
 
     def _test(self, schemafile, msg):
         self.loader.handle_file(join(DATADIR, schemafile))
-        ex = self.assertRaises(BadSchemaDefinition,
-                               self.loader._build_schema, 'toto', False)
-        self.assertEqual(str(ex), msg)
+        with self.assertRaises(BadSchemaDefinition) as cm:
+            self.loader._build_schema('toto', False)
+        self.assertEqual(str(cm.exception), msg)
 
     def test_rrqlexpr_on_etype(self):
         self._test('rrqlexpr_on_eetype.py',
--- a/web/_exceptions.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/web/_exceptions.py	Thu Jan 13 19:24:21 2011 +0100
@@ -62,7 +62,7 @@
     """raised when a json remote call fails
     """
     def __init__(self, reason=''):
-        super(RequestError, self).__init__()
+        super(RemoteCallFailed, self).__init__()
         self.reason = reason
 
     def dumps(self):
--- a/web/action.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/web/action.py	Thu Jan 13 19:24:21 2011 +0100
@@ -43,7 +43,7 @@
     def fill_menu(self, box, menu):
         """add action(s) to the given submenu of the given box"""
         for action in self.actual_actions():
-            menu.append(box.box_action(action))
+            menu.append(box.action_link(action))
 
     def html_class(self):
         if self._cw.selected(self.url()):
--- a/web/application.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/web/application.py	Thu Jan 13 19:24:21 2011 +0100
@@ -31,7 +31,7 @@
 from cubicweb import set_log_methods, cwvreg
 from cubicweb import (
     ValidationError, Unauthorized, AuthenticationError, NoSelectableObject,
-    RepositoryError, BadConnectionId, CW_EVENT_MANAGER)
+    BadConnectionId, CW_EVENT_MANAGER)
 from cubicweb.dbapi import DBAPISession
 from cubicweb.web import LOGGER, component
 from cubicweb.web import (
@@ -148,8 +148,6 @@
                                                               vreg=self.vreg)
         global SESSION_MANAGER
         SESSION_MANAGER = self.session_manager
-        if not 'last_login_time' in self.vreg.schema:
-            self._update_last_login_time = lambda x: None
         if self.vreg.config.mode != 'test':
             # don't try to reset session manager during test, this leads to
             # weird failures when running multiple tests
@@ -224,46 +222,9 @@
             cookie[sessioncookie]['secure'] = True
         req.set_cookie(cookie, sessioncookie, maxage=None)
         if not session.anonymous_session:
-            self._postlogin(req)
+            self.session_manager.postlogin(req)
         return session
 
-    def _update_last_login_time(self, req):
-        # XXX should properly detect missing permission / non writeable source
-        # and avoid "except (RepositoryError, Unauthorized)" below
-        if req.user.cw_metainformation()['source']['type'] == 'ldapuser':
-            return
-        try:
-            req.execute('SET X last_login_time NOW WHERE X eid %(x)s',
-                        {'x' : req.user.eid})
-            req.cnx.commit()
-        except (RepositoryError, Unauthorized):
-            req.cnx.rollback()
-        except:
-            req.cnx.rollback()
-            raise
-
-    def _postlogin(self, req):
-        """postlogin: the user has been authenticated, redirect to the original
-        page (index by default) with a welcome message
-        """
-        # Update last connection date
-        # XXX: this should be in a post login hook in the repository, but there
-        #      we can't differentiate actual login of automatic session
-        #      reopening. Is it actually a problem?
-        self._update_last_login_time(req)
-        args = req.form
-        for forminternal_key in ('__form_id', '__domid', '__errorurl'):
-            args.pop(forminternal_key, None)
-        args['__message'] = req._('welcome %s !') % req.user.login
-        if 'vid' in req.form:
-            args['vid'] = req.form['vid']
-        if 'rql' in req.form:
-            args['rql'] = req.form['rql']
-        path = req.relative_path(False)
-        if path == 'login':
-            path = 'view'
-        raise Redirect(req.build_url(path, **args))
-
     def logout(self, req, goto_url):
         """logout from the instance by cleaning the session and raising
         `AuthenticationError`
--- a/web/component.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/web/component.py	Thu Jan 13 19:24:21 2011 +0100
@@ -24,7 +24,7 @@
 
 from warnings import warn
 
-from logilab.common.deprecation import class_deprecated, class_renamed
+from logilab.common.deprecation import class_deprecated, class_renamed, deprecated
 from logilab.mtconverter import xml_escape
 
 from cubicweb import Unauthorized, role, target, tags
@@ -36,7 +36,7 @@
                                 non_final_entity, partial_relation_possible,
                                 partial_has_related_entities)
 from cubicweb.appobject import AppObject
-from cubicweb.web import INTERNAL_FIELD_VALUE, htmlwidgets, stdmsgs
+from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs
 
 
 # abstract base class for navigation components ################################
@@ -163,6 +163,57 @@
     rendered
     """
 
+
+class Link(object):
+    """a link to a view or action in the ui.
+
+    Use this rather than `cw.web.htmlwidgets.BoxLink`.
+
+    Note this class could probably be avoided with a proper DOM on the server
+    side.
+    """
+    newstyle = True
+
+    def __init__(self, href, label, **attrs):
+        self.href = href
+        self.label = label
+        self.attrs = attrs
+
+    def __unicode__(self):
+        return tags.a(self.label, href=self.href, **self.attrs)
+
+    def render(self, w):
+        w(tags.a(self.label, href=self.href, **self.attrs))
+
+
+class Separator(object):
+    """a menu separator.
+
+    Use this rather than `cw.web.htmlwidgets.BoxSeparator`.
+    """
+    newstyle = True
+
+    def render(self, w):
+        w(u'<hr class="boxSeparator"/>')
+
+
+def _bwcompatible_render_item(w, item):
+    if hasattr(item, 'render'):
+        if getattr(item, 'newstyle', False):
+            if isinstance(item, Separator):
+                w(u'</ul>')
+                item.render(w)
+                w(u'<ul>')
+            else:
+                w(u'<li>')
+                item.render(w)
+                w(u'</li>')
+        else:
+            item.render(w) # XXX displays <li> by itself
+    else:
+        w(u'<li>%s</li>' % item)
+
+
 class Layout(Component):
     __regid__ = 'layout'
     __abstract__ = True
@@ -289,20 +340,31 @@
         assert items
         w(u'<ul class="%s">' % klass)
         for item in items:
-            if hasattr(item, 'render'):
-                item.render(w) # XXX displays <li> by itself
-            else:
-                w(u'<li>')
-                w(item)
-                w(u'</li>')
+            _bwcompatible_render_item(w, item)
         w(u'</ul>')
 
     def append(self, item):
         self.items.append(item)
 
+    def action_link(self, action):
+        return self.link(self._cw._(action.title), action.url())
+
+    def link(self, title, url, **kwargs):
+        if self._cw.selected(url):
+            try:
+                kwargs['klass'] += ' selected'
+            except KeyError:
+                kwargs['klass'] = 'selected'
+        return Link(url, title, **kwargs)
+
+    def separator(self):
+        return Separator()
+
+    @deprecated('[3.10] use action_link() / link()')
     def box_action(self, action): # XXX action_link
         return self.build_link(self._cw._(action.title), action.url())
 
+    @deprecated('[3.10] use action_link() / link()')
     def build_link(self, title, url, **kwargs):
         if self._cw.selected(url):
             try:
@@ -362,9 +424,9 @@
             items = []
             for i, (eid, label) in enumerate(rset):
                 entity = rset.get_entity(i, 0)
-                items.append(self.build_link(label, entity.absolute_url()))
+                items.append(self.link(label, entity.absolute_url()))
         else:
-            items = [self.build_link(e.dc_title(), e.absolute_url())
+            items = [self.link(e.dc_title(), e.absolute_url())
                      for e in rset.entities()]
         self.render_items(w, items)
 
--- a/web/htmlwidgets.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/web/htmlwidgets.py	Thu Jan 13 19:24:21 2011 +0100
@@ -25,10 +25,12 @@
 from math import floor
 
 from logilab.mtconverter import xml_escape
+from logilab.common.deprecation import class_deprecated
 
 from cubicweb.utils import UStringIO
 from cubicweb.uilib import toggle_action, htmlescape
 from cubicweb.web import jsonize
+from cubicweb.web.component import _bwcompatible_render_item
 
 # XXX HTMLWidgets should have access to req (for datadir / static urls,
 #     i18n strings, etc.)
@@ -54,7 +56,8 @@
         return False
 
 
-class BoxWidget(HTMLWidget):
+class BoxWidget(HTMLWidget): # XXX Deprecated
+
     def __init__(self, title, id, items=None, _class="boxFrame",
                  islist=True, shadow=True, escape=True):
         self.title = title
@@ -107,16 +110,16 @@
         if self.items:
             self.box_begin_content()
             for item in self.items:
-                if hasattr(item, 'render'):
-                    item.render(self.w)
-                else:
-                    self.w(u'<li>%s</li>' % item)
+                _bwcompatible_render_item(self.w, item)
             self.box_end_content()
         self.w(u'</div>')
 
 
 class SideBoxWidget(BoxWidget):
     """default CubicWeb's sidebox widget"""
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'
+
     title_class = u'sideBoxTitle'
     main_div_class = u'sideBoxBody'
     listing_class = ''
@@ -127,6 +130,7 @@
 
 
 class MenuWidget(BoxWidget):
+
     main_div_class = 'menuContent'
     listing_class = 'menuListing'
 
@@ -136,8 +140,9 @@
         self.w(u'</div>\n')
 
 
-class RawBoxItem(HTMLWidget):
+class RawBoxItem(HTMLWidget): # XXX deprecated
     """a simpe box item displaying raw data"""
+
     def __init__(self, label, liclass=None):
         self.label = label
         self.liclass = liclass
@@ -156,6 +161,7 @@
 
 class BoxMenu(RawBoxItem):
     """a menu in a box"""
+
     link_class = 'boxMenu'
 
     def __init__(self, label, items=None, isitem=True, liclass=None, ident=None,
@@ -184,10 +190,7 @@
             toggle_action(ident), self.link_class, self.label))
         self._begin_menu(ident)
         for item in self.items:
-            if hasattr(item, 'render'):
-                item.render(self.w)
-            else:
-                self.w(u'<li>%s</li>' % item)
+            _bwcompatible_render_item(self.w, item)
         self._end_menu()
         if self.isitem:
             self.w(u'</li>')
@@ -208,6 +211,8 @@
 
 class BoxField(HTMLWidget):
     """couples label / value meant to be displayed in a box"""
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'
     def __init__(self, label, value):
         self.label = label
         self.value = value
@@ -219,6 +224,8 @@
 
 class BoxSeparator(HTMLWidget):
     """a menu separator"""
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'
 
     def _render(self):
         self.w(u'</ul><hr class="boxSeparator"/><ul>')
@@ -226,6 +233,8 @@
 
 class BoxLink(HTMLWidget):
     """a link in a box"""
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'
     def __init__(self, href, label, _class='', title='', ident='', escape=False):
         self.href = href
         if escape:
@@ -247,6 +256,8 @@
 
 class BoxHtml(HTMLWidget):
     """a form in a box"""
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'
     def __init__(self, rawhtml):
         self.rawhtml = rawhtml
 
@@ -272,6 +283,7 @@
     def add_attr(self, attr, value):
         self.cell_attrs[attr] = value
 
+
 class SimpleTableModel(object):
     """
     uses a list of lists as a storage backend
@@ -283,7 +295,6 @@
     def __init__(self, rows):
         self._rows = rows
 
-
     def get_rows(self):
         return self._rows
 
--- a/web/test/unittest_application.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/web/test/unittest_application.py	Thu Jan 13 19:24:21 2011 +0100
@@ -298,8 +298,9 @@
 
     def test_login_not_available_to_authenticated(self):
         req = self.request()
-        ex = self.assertRaises(Unauthorized, self.app_publish, req, 'login')
-        self.assertEqual(str(ex), 'log out first')
+        with self.assertRaises(Unauthorized) as cm:
+            self.app_publish(req, 'login')
+        self.assertEqual(str(cm.exception), 'log out first')
 
     def test_fb_login_concept(self):
         """see data/views.py"""
@@ -367,8 +368,9 @@
         # preparing the suite of the test
         # set session id in cookie
         cookie = Cookie.SimpleCookie()
-        cookie['__session'] = req.session.sessionid
-        req._headers['Cookie'] = cookie['__session'].OutputString()
+        sessioncookie = self.app.session_handler.session_cookie(req)
+        cookie[sessioncookie] = req.session.sessionid
+        req._headers['Cookie'] = cookie[sessioncookie].OutputString()
         clear_cache(req, 'get_authorization')
         # reset session as if it was a new incoming request
         req.session = req.cnx = None
--- a/web/test/unittest_views_basecontrollers.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/web/test/unittest_views_basecontrollers.py	Thu Jan 13 19:24:21 2011 +0100
@@ -47,8 +47,9 @@
     def test_noparam_edit(self):
         """check behaviour of this controller without any form parameter
         """
-        ex = self.assertRaises(ValidationError, self.ctrl_publish, self.request())
-        self.assertEqual(ex.errors, {None: u'no selected entities'})
+        with self.assertRaises(ValidationError) as cm:
+            self.ctrl_publish(self.request())
+        self.assertEqual(cm.exception.errors, {None: u'no selected entities'})
 
     def test_validation_unique(self):
         """test creation of two linked entities
@@ -61,8 +62,9 @@
                     'upassword-subject:X': u'toto',
                     'upassword-subject-confirm:X': u'toto',
                     }
-        ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEqual(ex.errors, {'login-subject': 'the value "admin" is already used, use another one'})
+        with self.assertRaises(ValidationError) as cm:
+            self.ctrl_publish(req)
+        self.assertEqual(cm.exception.errors, {'login-subject': 'the value "admin" is already used, use another one'})
 
     def test_user_editing_itself(self):
         """checking that a manager user can edit itself
@@ -205,8 +207,9 @@
                     'login-subject:X': u'toto',
                     'upassword-subject:X': u'toto',
                     }
-        ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEqual(ex.errors, {'upassword-subject': u'password and confirmation don\'t match'})
+        with self.assertRaises(ValidationError) as cm:
+            self.ctrl_publish(req)
+        self.assertEqual(cm.exception.errors, {'upassword-subject': u'password and confirmation don\'t match'})
         req = self.request()
         req.form = {'__cloned_eid:X': u(user.eid),
                     'eid': 'X', '__type:X': 'CWUser',
@@ -215,8 +218,9 @@
                     'upassword-subject:X': u'toto',
                     'upassword-subject-confirm:X': u'tutu',
                     }
-        ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEqual(ex.errors, {'upassword-subject': u'password and confirmation don\'t match'})
+        with self.assertRaises(ValidationError) as cm:
+            self.ctrl_publish(req)
+        self.assertEqual(cm.exception.errors, {'upassword-subject': u'password and confirmation don\'t match'})
 
 
     def test_interval_bound_constraint_success(self):
@@ -230,8 +234,9 @@
                     'amount-subject:X': u'-10',
                     'described_by_test-subject:X': u(feid),
                 }
-        ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEqual(ex.errors, {'amount-subject': 'value must be >= 0'})
+        with self.assertRaises(ValidationError) as cm:
+            self.ctrl_publish(req)
+        self.assertEqual(cm.exception.errors, {'amount-subject': 'value must be >= 0'})
         req = self.request(rollbackfirst=True)
         req.form = {'eid': ['X'],
                     '__type:X': 'Salesterm',
@@ -239,8 +244,9 @@
                     'amount-subject:X': u'110',
                     'described_by_test-subject:X': u(feid),
                     }
-        ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEqual(ex.errors, {'amount-subject': 'value must be <= 100'})
+        with self.assertRaises(ValidationError) as cm:
+            self.ctrl_publish(req)
+        self.assertEqual(cm.exception.errors, {'amount-subject': 'value must be <= 100'})
         req = self.request(rollbackfirst=True)
         req.form = {'eid': ['X'],
                     '__type:X': 'Salesterm',
@@ -421,8 +427,9 @@
                     'alias-subject:Y': u'',
                     'use_email-object:Y': 'X',
                     }
-        ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEqual(ex.errors, {'address-subject': u'required field'})
+        with self.assertRaises(ValidationError) as cm:
+            self.ctrl_publish(req)
+        self.assertEqual(cm.exception.errors, {'address-subject': u'required field'})
 
     def test_nonregr_copy(self):
         user = self.user()
--- a/web/views/basecomponents.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/web/views/basecomponents.py	Thu Jan 13 19:24:21 2011 +0100
@@ -36,8 +36,7 @@
 from cubicweb.utils import wrap_on_write
 from cubicweb.uilib import toggle_action
 from cubicweb.web import component, uicfg
-from cubicweb.web.htmlwidgets import (MenuWidget, PopupBoxMenu, BoxSeparator,
-                                      BoxLink)
+from cubicweb.web.htmlwidgets import MenuWidget, PopupBoxMenu
 
 VISIBLE_PROP_DEF = {
     _('visible'):  dict(type='Boolean', default=True,
@@ -167,13 +166,11 @@
         menu = PopupBoxMenu(self._cw.user.login, isitem=False)
         box.append(menu)
         for action in actions.get('useractions', ()):
-            menu.append(BoxLink(action.url(), self._cw._(action.title),
-                                action.html_class()))
+            menu.append(self.action_link(action))
         if actions.get('useractions') and actions.get('siteactions'):
-            menu.append(BoxSeparator())
+            menu.append(self.separator())
         for action in actions.get('siteactions', ()):
-            menu.append(BoxLink(action.url(), self._cw._(action.title),
-                                action.html_class()))
+            menu.append(self.action_link(action))
         box.render(w=w)
 
 
--- a/web/views/basecontrollers.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/web/views/basecontrollers.py	Thu Jan 13 19:24:21 2011 +0100
@@ -419,7 +419,7 @@
                                               **optional_kwargs(extraargs))
         #except NoSelectableObject:
         #    raise RemoteCallFailed('unselectable')
-        return self._call_view(comp, **extraargs)
+        return self._call_view(comp, **optional_kwargs(extraargs))
 
     @xhtmlize
     def js_render(self, registry, oid, eid=None,
--- a/web/views/basetemplates.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/web/views/basetemplates.py	Thu Jan 13 19:24:21 2011 +0100
@@ -367,13 +367,15 @@
 
 
 class HTMLPageFooter(View):
-    """default html page footer: include footer actions
-    """
+    """default html page footer: include footer actions"""
     __regid__ = 'footer'
 
     def call(self, **kwargs):
-        req = self._cw
         self.w(u'<div id="footer">')
+        self.footer_content()
+        self.w(u'</div>')
+
+    def footer_content(self):
         actions = self._cw.vreg['actions'].possible_actions(self._cw,
                                                             rset=self.cw_rset)
         footeractions = actions.get('footer', ())
@@ -382,8 +384,6 @@
                                              self._cw._(action.title)))
             if i < (len(footeractions) - 1):
                 self.w(u' | ')
-        self.w(u'</div>')
-
 
 class HTMLContentHeader(View):
     """default html page content header:
--- a/web/views/bookmark.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/web/views/bookmark.py	Thu Jan 13 19:24:21 2011 +0100
@@ -98,7 +98,7 @@
         if self.can_delete:
             req.add_js('cubicweb.ajax.js')
         for bookmark in self.bookmarks_rset.entities():
-            label = self.build_link(bookmark.title, bookmark.action_url())
+            label = self.link(bookmark.title, bookmark.action_url())
             if self.can_delete:
                 dlink = u'[<a class="action" href="javascript:removeBookmark(%s)" title="%s">-</a>]' % (
                     bookmark.eid, req._('delete this bookmark'))
@@ -114,7 +114,7 @@
             # default value for bookmark's title
             url = req.vreg['etypes'].etype_class('Bookmark').cw_create_url(
                 req, __linkto=linkto, path=path)
-            menu.append(self.build_link(req._('bookmark this page'), url))
+            menu.append(self.link(req._('bookmark this page'), url))
             if self.bookmarks_rset:
                 if req.user.is_in_group('managers'):
                     bookmarksrql = 'Bookmark B WHERE B bookmarked_by U, U eid %s' % ueid
@@ -127,9 +127,9 @@
                     bookmarksrql %= {'x': ueid}
                 if erset:
                     url = req.build_url(vid='muledit', rql=bookmarksrql)
-                    menu.append(self.build_link(req._('edit bookmarks'), url))
+                    menu.append(self.link(req._('edit bookmarks'), url))
             url = req.user.absolute_url(vid='xaddrelation', rtype='bookmarked_by',
                                         target='subject')
-            menu.append(self.build_link(req._('pick existing bookmarks'), url))
+            menu.append(self.link(req._('pick existing bookmarks'), url))
             self.append(menu)
         self.render_items(w)
--- a/web/views/boxes.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/web/views/boxes.py	Thu Jan 13 19:24:21 2011 +0100
@@ -51,7 +51,7 @@
 class EditBox(component.CtxComponent): # XXX rename to ActionsBox
     """
     box with all actions impacting the entity displayed: edit, copy, delete
-    change state, add related entities
+    change state, add related entities...
     """
     __regid__ = 'edit_box'
     __select__ = component.CtxComponent.__select__ & non_final_entity()
@@ -127,7 +127,7 @@
                 if hasattr(boxlink, 'label'):
                     boxlink.label = u'%s %s' % (submenu.label_prefix, boxlink.label)
                 else:
-                    submenu.items[0] = u'%s %s' % (submenu.label_prefix, boxlink)
+                    boxlink = u'%s %s' % (submenu.label_prefix, boxlink)
             box.append(boxlink)
         elif submenu.items:
             box.append(submenu)
@@ -187,7 +187,7 @@
         for category, views in box.sort_by_category(self.views):
             menu = htmlwidgets.BoxMenu(category)
             for view in views:
-                menu.append(self.box_action(view))
+                menu.append(self.action_link(view))
             self.append(menu)
         self.render_items(w)
 
@@ -218,7 +218,7 @@
 
     @property
     def domid(self):
-        return super(RsetBox, self).domid + unicode(abs(id(self)))
+        return super(RsetBox, self).domid + unicode(abs(id(self))) + unicode(abs(id(self.cw_rset)))
 
     def render_title(self, w):
         w(self.cw_extra_kwargs['title'])
--- a/web/views/idownloadable.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/web/views/idownloadable.py	Thu Jan 13 19:24:21 2011 +0100
@@ -21,6 +21,7 @@
 _ = unicode
 
 from logilab.mtconverter import BINARY_ENCODINGS, TransformError, xml_escape
+from logilab.common.deprecation import class_renamed, deprecated
 
 from cubicweb import tags
 from cubicweb.view import EntityView
@@ -31,7 +32,7 @@
 from cubicweb.web.views import primary, baseviews
 
 
-# XXX deprecated
+@deprecated('[3.10] use a custom IDownloadable adapter instead')
 def download_box(w, entity, title=None, label=None, footer=u''):
     req = entity._cw
     w(u'<div class="sideBox">')
@@ -62,10 +63,12 @@
 
     def render_body(self, w):
         for item in self.items:
+            idownloadable = item.cw_adapt_to('IDownloadable')
             w(u'<a href="%s"><img src="%s" alt="%s"/> %s</a>'
-              % (xml_escape(item.cw_adapt_to('IDownloadable').download_url()),
+              % (xml_escape(idownloadable.download_url()),
                  self._cw.uiprops['DOWNLOAD_ICON'],
-                 self._cw._('download icon'), xml_escape(item.dc_title())))
+                 self._cw._('download icon'),
+                 xml_escape(idownloadable.download_file_name())))
 
 
 class DownloadView(EntityView):
@@ -154,7 +157,7 @@
         return False
 
 
-class IDownloadableLineView(baseviews.OneLineView):
+class IDownloadableOneLineView(baseviews.OneLineView):
     __select__ = adaptable('IDownloadable')
 
     def cell_call(self, row, col, title=None, **kwargs):
@@ -162,11 +165,15 @@
         entity = self.cw_rset.get_entity(row, col)
         url = xml_escape(entity.absolute_url())
         adapter = entity.cw_adapt_to('IDownloadable')
-        name = xml_escape(title or adapter.download_file_name())
+        name = xml_escape(title or entity.dc_title())
         durl = xml_escape(adapter.download_url())
         self.w(u'<a href="%s">%s</a> [<a href="%s">%s</a>]' %
                (url, name, durl, self._cw._('download')))
 
+IDownloadableLineView = class_renamed(
+    'IDownloadableLineView', IDownloadableOneLineView,
+    '[3.10] IDownloadableLineView is deprecated, use %IDownloadableOneLineView')
+
 
 class AbstractEmbeddedView(EntityView):
     __abstract__ = True
--- a/web/views/pyviews.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/web/views/pyviews.py	Thu Jan 13 19:24:21 2011 +0100
@@ -24,6 +24,10 @@
 
 
 class PyValTableView(View):
+    """display a list of list of values into an html table.
+
+    Take care, content is NOT xml-escaped.
+    """
     __regid__ = 'pyvaltable'
     __select__ = match_kwargs('pyvalue')
 
@@ -50,6 +54,10 @@
 
 
 class PyValListView(View):
+    """display a list of values into an html list.
+
+    Take care, content is NOT xml-escaped.
+    """
     __regid__ = 'pyvallist'
     __select__ = match_kwargs('pyvalue')
 
--- a/web/views/sessions.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/web/views/sessions.py	Thu Jan 13 19:24:21 2011 +0100
@@ -21,7 +21,8 @@
 
 __docformat__ = "restructuredtext en"
 
-from cubicweb.web import InvalidSession
+from cubicweb import RepositoryError, Unauthorized
+from cubicweb.web import InvalidSession, Redirect
 from cubicweb.web.application import AbstractSessionManager
 from cubicweb.dbapi import DBAPISession
 
@@ -75,6 +76,44 @@
         req.set_session(session)
         return session
 
+    def postlogin(self, req):
+        """postlogin: the user has been authenticated, redirect to the original
+        page (index by default) with a welcome message
+        """
+        # Update last connection date
+        # XXX: this should be in a post login hook in the repository, but there
+        #      we can't differentiate actual login of automatic session
+        #      reopening. Is it actually a problem?
+        if 'last_login_time' in req.vreg.schema:
+            self._update_last_login_time(req)
+        args = req.form
+        for forminternal_key in ('__form_id', '__domid', '__errorurl'):
+            args.pop(forminternal_key, None)
+        args['__message'] = req._('welcome %s !') % req.user.login
+        if 'vid' in req.form:
+            args['vid'] = req.form['vid']
+        if 'rql' in req.form:
+            args['rql'] = req.form['rql']
+        path = req.relative_path(False)
+        if path == 'login':
+            path = 'view'
+        raise Redirect(req.build_url(path, **args))
+
+    def _update_last_login_time(self, req):
+        # XXX should properly detect missing permission / non writeable source
+        # and avoid "except (RepositoryError, Unauthorized)" below
+        if req.user.cw_metainformation()['source']['type'] == 'ldapuser':
+            return
+        try:
+            req.execute('SET X last_login_time NOW WHERE X eid %(x)s',
+                        {'x' : req.user.eid})
+            req.cnx.commit()
+        except (RepositoryError, Unauthorized):
+            req.cnx.rollback()
+        except:
+            req.cnx.rollback()
+            raise
+
     def close_session(self, session):
         """close session on logout or on invalid session detected (expired out,
         corrupted...)
--- a/web/views/tableview.py	Wed Jan 05 18:42:21 2011 +0100
+++ b/web/views/tableview.py	Thu Jan 13 19:24:21 2011 +0100
@@ -28,8 +28,9 @@
 from cubicweb import tags
 from cubicweb.uilib import toggle_action, limitsize, htmlescape
 from cubicweb.web import jsonize
+from cubicweb.web.component import Link
 from cubicweb.web.htmlwidgets import (TableWidget, TableColumn, MenuWidget,
-                                      PopupBoxMenu, BoxLink)
+                                      PopupBoxMenu)
 from cubicweb.web.facet import prepare_facets_rqlst, filter_hiddens
 
 class TableView(AnyRsetView):
@@ -212,7 +213,7 @@
                             ident='%sActions' % divid)
         box.append(menu)
         for url, label, klass, ident in actions:
-            menu.append(BoxLink(url, label, klass, ident=ident, escape=True))
+            menu.append(Link(url, label, klass=klass, id=ident))
         box.render(w=self.w)
         self.w(u'<div class="clear"/>')