[entity] introduce a new 'adapters' registry
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Thu, 20 May 2010 20:47:55 +0200
changeset 5556 9ab2b4c74baf
parent 5555 a64f48dd5fe4
child 5557 1a534c596bff
[entity] introduce a new 'adapters' registry This changeset introduces the notion in adapters (as in Zope Component Architecture) in a cubicweb way, eg using a specific registry of appobjects. This allows nicer code structure, by avoid clutering entity classes and moving code usually specific to a place of the ui (or something else) together with the code that use the interface. We don't use actual interface anymore, they are implied by adapters (which may be abstract), whose reg id is an interface name. Appobjects that used to 'implements(IFace)' should now be rewritten by: * coding an IFaceAdapter(EntityAdapter) defining (implementing if desired) the interface, usually with __regid__ = 'IFace' * use "adaptable('IFace')" as selector instead Also, the implements_adapter_compat decorator eases backward compatibility with adapter's methods that may still be found on entities implementing the interface. Notice that unlike ZCA, we don't support automatic adapters chain (yagni?). All interfaces defined in cubicweb have been turned into adapters, also some new ones have been introduced to cleanup Entity / AnyEntity classes namespace. At the end, the pluggable mixins mecanism should disappear in favor of adapters as well.
cwvreg.py
devtools/devctl.py
doc/book/en/devrepo/vreg.rst
entities/__init__.py
entities/adapters.py
entities/authobjs.py
entities/lib.py
entities/schemaobjs.py
entities/test/unittest_base.py
entities/test/unittest_wfobjs.py
entities/wfobjs.py
entity.py
goa/appobjects/components.py
hooks/syncschema.py
hooks/workflow.py
interfaces.py
mail.py
mixins.py
req.py
selectors.py
server/migractions.py
server/repository.py
server/sources/native.py
server/test/unittest_ldapuser.py
server/test/unittest_msplanner.py
server/test/unittest_multisources.py
server/test/unittest_repository.py
server/test/unittest_security.py
server/test/unittest_undo.py
sobjects/notification.py
sobjects/test/unittest_notification.py
sobjects/test/unittest_supervising.py
sobjects/textparsers.py
test/unittest_entity.py
view.py
web/__init__.py
web/action.py
web/controller.py
web/test/unittest_views_basecontrollers.py
web/views/basecontrollers.py
web/views/calendar.py
web/views/cwproperties.py
web/views/cwuser.py
web/views/editcontroller.py
web/views/emailaddress.py
web/views/embedding.py
web/views/ibreadcrumbs.py
web/views/idownloadable.py
web/views/igeocodable.py
web/views/iprogress.py
web/views/isioc.py
web/views/massmailing.py
web/views/navigation.py
web/views/old_calendar.py
web/views/schema.py
web/views/tableview.py
web/views/timeline.py
web/views/timetable.py
web/views/treeview.py
web/views/workflow.py
web/views/xmlrss.py
--- a/cwvreg.py	Thu May 20 20:47:13 2010 +0200
+++ b/cwvreg.py	Thu May 20 20:47:55 2010 +0200
@@ -82,7 +82,6 @@
 .. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_all
 .. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_and_replace
 .. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register
-.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_if_interface_found
 .. automethod:: cubicweb.cwvreg.CubicWebVRegistry.unregister
 
 Examples:
@@ -192,6 +191,8 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
+from warnings import warn
+
 from logilab.common.decorators import cached, clear_cache
 from logilab.common.deprecation import  deprecated
 from logilab.common.modutils import cleanup_sys_modules
@@ -211,23 +212,23 @@
 
 def use_interfaces(obj):
     """return interfaces used by the given object by searching for implements
-    selectors, with a bw compat fallback to accepts_interfaces attribute
+    selectors
     """
     from cubicweb.selectors import implements
-    try:
-        # XXX deprecated
-        return sorted(obj.accepts_interfaces)
-    except AttributeError:
-        try:
-            impl = obj.__select__.search_selector(implements)
-            if impl:
-                return sorted(impl.expected_ifaces)
-        except AttributeError:
-            pass # old-style appobject classes with no accepts_interfaces
-        except:
-            print 'bad selector %s on %s' % (obj.__select__, obj)
-            raise
-        return ()
+    impl = obj.__select__.search_selector(implements)
+    if impl:
+        return sorted(impl.expected_ifaces)
+    return ()
+
+def require_appobject(obj):
+    """return interfaces used by the given object by searching for implements
+    selectors
+    """
+    from cubicweb.selectors import appobject_selectable
+    impl = obj.__select__.search_selector(appobject_selectable)
+    if impl:
+        return (impl.registry, impl.regids)
+    return None
 
 
 class CWRegistry(Registry):
@@ -477,6 +478,7 @@
     def reset(self):
         super(CubicWebVRegistry, self).reset()
         self._needs_iface = {}
+        self._needs_appobject = {}
         # two special registries, propertydefs which care all the property
         # definitions, and propertyvals which contains values for those
         # properties
@@ -536,6 +538,7 @@
                 for obj in objects:
                     obj.schema = schema
 
+    @deprecated('[3.9] use .register instead')
     def register_if_interface_found(self, obj, ifaces, **kwargs):
         """register `obj` but remove it if no entity class implements one of
         the given `ifaces` interfaces at the end of the registration process.
@@ -561,7 +564,15 @@
         # XXX bw compat
         ifaces = use_interfaces(obj)
         if ifaces:
+            if not obj.__name__.endswith('Adapter') and \
+                   any(iface for iface in ifaces if not isinstance(iface, basestring)):
+                warn('[3.9] %s: interfaces in implements selector are '
+                     'deprecated in favor of adapters / appobject_selectable '
+                     'selector' % obj.__name__, DeprecationWarning)
             self._needs_iface[obj] = ifaces
+        depends_on = require_appobject(obj)
+        if depends_on is not None:
+            self._needs_appobject[obj] = depends_on
 
     def register_objects(self, path, force_reload=False):
         """overriden to remove objects requiring a missing interface"""
@@ -578,13 +589,18 @@
         # we may want to keep interface dependent objects (e.g.for i18n
         # catalog generation)
         if self.config.cleanup_interface_sobjects:
-            # remove appobjects that don't support any available interface
+            # XXX deprecated with cw 3.9: remove appobjects that don't support
+            # any available interface
             implemented_interfaces = set()
             if 'Any' in self.get('etypes', ()):
                 for etype in self.schema.entities():
                     if etype.final:
                         continue
                     cls = self['etypes'].etype_class(etype)
+                    if cls.__implements__:
+                        warn('[3.9] %s: using __implements__/interfaces are '
+                             'deprecated in favor of adapters' % cls.__name__,
+                             DeprecationWarning)
                     for iface in cls.__implements__:
                         implemented_interfaces.update(iface.__mro__)
                     implemented_interfaces.update(cls.__mro__)
@@ -598,9 +614,17 @@
                     self.debug('kicking appobject %s (no implemented '
                                'interface among %s)', obj, ifaces)
                     self.unregister(obj)
-        # clear needs_iface so we don't try to remove some not-anymore-in
-        # objects on automatic reloading
-        self._needs_iface.clear()
+            # since 3.9: remove appobjects which depending on other, unexistant
+            # appobjects
+            for obj, (regname, regids) in self._needs_appobject.items():
+                registry = self[regname]
+                for regid in regids:
+                    if registry.get(regid):
+                        break
+                else:
+                    self.debug('kicking %s (no %s object in registry %s)',
+                               obj, ' or '.join(regids), registry)
+                    self.unregister(obj)
         super(CubicWebVRegistry, self).initialization_completed()
         for rtag in RTAGS:
             # don't check rtags if we don't want to cleanup_interface_sobjects
--- a/devtools/devctl.py	Thu May 20 20:47:13 2010 +0200
+++ b/devtools/devctl.py	Thu May 20 20:47:55 2010 +0200
@@ -15,10 +15,10 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""additional cubicweb-ctl commands and command handlers for cubicweb and cubicweb's
-cubes development
+"""additional cubicweb-ctl commands and command handlers for cubicweb and
+cubicweb's cubes development
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 # *ctl module should limit the number of import to be imported as quickly as
--- a/doc/book/en/devrepo/vreg.rst	Thu May 20 20:47:13 2010 +0200
+++ b/doc/book/en/devrepo/vreg.rst	Thu May 20 20:47:55 2010 +0200
@@ -37,6 +37,7 @@
 .. autoclass:: cubicweb.appobject.yes
 .. autoclass:: cubicweb.selectors.match_kwargs
 .. autoclass:: cubicweb.selectors.appobject_selectable
+.. autoclass:: cubicweb.selectors.adaptable
 
 
 Result set selectors
@@ -75,6 +76,7 @@
 .. autoclass:: cubicweb.selectors.partial_has_related_entities
 .. autoclass:: cubicweb.selectors.has_permission
 .. autoclass:: cubicweb.selectors.has_add_permission
+.. autoclass:: cubicweb.selectors.has_mimetype
 
 
 Logged user selectors
--- a/entities/__init__.py	Thu May 20 20:47:13 2010 +0200
+++ b/entities/__init__.py	Thu May 20 20:47:55 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""base application's entities class implementation: `AnyEntity`
+"""base application's entities class implementation: `AnyEntity`"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from warnings import warn
@@ -28,33 +27,13 @@
 from cubicweb import Unauthorized, typed_eid
 from cubicweb.entity import Entity
 
-from cubicweb.interfaces import IBreadCrumbs, IFeed
-
 
 class AnyEntity(Entity):
     """an entity instance has e_schema automagically set on the class and
     instances have access to their issuing cursor
     """
     __regid__ = 'Any'
-    __implements__ = (IBreadCrumbs, IFeed)
-
-    fetch_attrs = ('modification_date',)
-    @classmethod
-    def fetch_order(cls, attr, var):
-        """class method used to control sort order when multiple entities of
-        this type are fetched
-        """
-        return cls.fetch_unrelated_order(attr, var)
-
-    @classmethod
-    def fetch_unrelated_order(cls, attr, var):
-        """class method used to control sort order when multiple entities of
-        this type are fetched to use in edition (eg propose them to create a
-        new relation on an edited entity).
-        """
-        if attr == 'modification_date':
-            return '%s DESC' % var
-        return None
+    __implements__ = ()
 
     # meta data api ###########################################################
 
@@ -120,32 +99,6 @@
         except (Unauthorized, IndexError):
             return None
 
-    def breadcrumbs(self, view=None, recurs=False):
-        path = [self]
-        if hasattr(self, 'parent'):
-            parent = self.parent()
-            if parent is not None:
-                try:
-                    path = parent.breadcrumbs(view, True) + [self]
-                except TypeError:
-                    warn("breadcrumbs method's now takes two arguments "
-                         "(view=None, recurs=False), please update",
-                         DeprecationWarning)
-                    path = parent.breadcrumbs(view) + [self]
-        if not recurs:
-            if view is None:
-                if 'vtitle' in self._cw.form:
-                    # embeding for instance
-                    path.append( self._cw.form['vtitle'] )
-            elif view.__regid__ != 'primary' and hasattr(view, 'title'):
-                path.append( self._cw._(view.title) )
-        return path
-
-    ## IFeed interface ########################################################
-
-    def rss_feed_url(self):
-        return self.absolute_url(vid='rss')
-
     # abstractions making the whole things (well, some at least) working ######
 
     def sortvalue(self, rtype=None):
@@ -189,35 +142,8 @@
         self.__linkto[(rtype, role)] = linkedto
         return linkedto
 
-    # edit controller callbacks ###############################################
-
-    def after_deletion_path(self):
-        """return (path, parameters) which should be used as redirect
-        information when this entity is being deleted
-        """
-        if hasattr(self, 'parent') and self.parent():
-            return self.parent().rest_path(), {}
-        return str(self.e_schema).lower(), {}
-
-    def pre_web_edit(self):
-        """callback called by the web editcontroller when an entity will be
-        created/modified, to let a chance to do some entity specific stuff.
-
-        Do nothing by default.
-        """
-        pass
-
     # server side helpers #####################################################
 
-    def notification_references(self, view):
-        """used to control References field of email send on notification
-        for this entity. `view` is the notification view.
-
-        Should return a list of eids which can be used to generate message ids
-        of previously sent email
-        """
-        return ()
-
 # XXX:  store a reference to the AnyEntity class since it is hijacked in goa
 #       configuration and we need the actual reference to avoid infinite loops
 #       in mro
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/entities/adapters.py	Thu May 20 20:47:55 2010 +0200
@@ -0,0 +1,166 @@
+# copyright 2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""some basic entity adapter implementations, for interfaces used in the
+framework itself.
+"""
+
+__docformat__ = "restructuredtext en"
+
+from cubicweb.view import EntityAdapter, implements_adapter_compat
+from cubicweb.selectors import implements, relation_possible
+from cubicweb.interfaces import IDownloadable
+
+
+class IEmailableAdapter(EntityAdapter):
+    __regid__ = 'IEmailable'
+    __select__ = relation_possible('primary_email') | relation_possible('use_email')
+
+    def get_email(self):
+        if getattr(self.entity, 'primary_email', None):
+            return self.entity.primary_email[0].address
+        if getattr(self.entity, 'use_email', None):
+            return self.entity.use_email[0].address
+        return None
+
+    def allowed_massmail_keys(self):
+        """returns a set of allowed email substitution keys
+
+        The default is to return the entity's attribute list but you might
+        override this method to allow extra keys.  For instance, a Person
+        class might want to return a `companyname` key.
+        """
+        return set(rschema.type
+                   for rschema, attrtype in self.entity.e_schema.attribute_definitions()
+                   if attrtype.type not in ('Password', 'Bytes'))
+
+    def as_email_context(self):
+        """returns the dictionary as used by the sendmail controller to
+        build email bodies.
+
+        NOTE: the dictionary keys should match the list returned by the
+        `allowed_massmail_keys` method.
+        """
+        return dict( (attr, getattr(self.entity, attr))
+                     for attr in self.allowed_massmail_keys() )
+
+
+class INotifiableAdapter(EntityAdapter):
+    __regid__ = 'INotifiable'
+    __select__ = implements('Any')
+
+    @implements_adapter_compat('INotifiableAdapter')
+    def notification_references(self, view):
+        """used to control References field of email send on notification
+        for this entity. `view` is the notification view.
+
+        Should return a list of eids which can be used to generate message
+        identifiers of previously sent email(s)
+        """
+        itree = self.entity.cw_adapt_to('ITree')
+        if itree is not None:
+            return itree.path()[:-1]
+        return ()
+
+
+class IFTIndexableAdapter(EntityAdapter):
+    __regid__ = 'IFTIndexable'
+    __select__ = implements('Any')
+
+    def fti_containers(self, _done=None):
+        if _done is None:
+            _done = set()
+        entity = self.entity
+        _done.add(entity.eid)
+        containers = tuple(entity.e_schema.fulltext_containers())
+        if containers:
+            for rschema, target in containers:
+                if target == 'object':
+                    targets = getattr(entity, rschema.type)
+                else:
+                    targets = getattr(entity, 'reverse_%s' % rschema)
+                for entity in targets:
+                    if entity.eid in _done:
+                        continue
+                    for container in entity.cw_adapt_to('IFTIndexable').fti_containers(_done):
+                        yield container
+                        yielded = True
+        else:
+            yield entity
+
+    def get_words(self):
+        """used by the full text indexer to get words to index
+
+        this method should only be used on the repository side since it depends
+        on the logilab.database package
+
+        :rtype: list
+        :return: the list of indexable word of this entity
+        """
+        from logilab.database.fti import tokenize
+        # take care to cases where we're modyfying the schema
+        entity = self.entity
+        pending = self._cw.transaction_data.setdefault('pendingrdefs', set())
+        words = []
+        for rschema in entity.e_schema.indexable_attributes():
+            if (entity.e_schema, rschema) in pending:
+                continue
+            try:
+                value = entity.printable_value(rschema, format='text/plain')
+            except TransformError:
+                continue
+            except:
+                self.exception("can't add value of %s to text index for entity %s",
+                               rschema, entity.eid)
+                continue
+            if value:
+                words += tokenize(value)
+        for rschema, role in entity.e_schema.fulltext_relations():
+            if role == 'subject':
+                for entity in getattr(entity, rschema.type):
+                    words += entity.cw_adapt_to('IFTIndexable').get_words()
+            else: # if role == 'object':
+                for entity in getattr(entity, 'reverse_%s' % rschema.type):
+                    words += entity.cw_adapt_to('IFTIndexable').get_words()
+        return words
+
+
+class IDownloadableAdapter(EntityAdapter):
+    """interface for downloadable entities"""
+    __regid__ = 'IDownloadable'
+    __select__ = implements(IDownloadable) # XXX for bw compat, else should be abstract
+
+    @implements_adapter_compat('IDownloadable')
+    def download_url(self): # XXX not really part of this interface
+        """return an url to download entity's content"""
+        raise NotImplementedError
+    @implements_adapter_compat('IDownloadable')
+    def download_content_type(self):
+        """return MIME type of the downloadable content"""
+        raise NotImplementedError
+    @implements_adapter_compat('IDownloadable')
+    def download_encoding(self):
+        """return encoding of the downloadable content"""
+        raise NotImplementedError
+    @implements_adapter_compat('IDownloadable')
+    def download_file_name(self):
+        """return file name of the downloadable content"""
+        raise NotImplementedError
+    @implements_adapter_compat('IDownloadable')
+    def download_data(self):
+        """return actual data of the downloadable content"""
+        raise NotImplementedError
--- a/entities/authobjs.py	Thu May 20 20:47:13 2010 +0200
+++ b/entities/authobjs.py	Thu May 20 20:47:55 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""entity classes user and group entities
+"""entity classes user and group entities"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from logilab.common.decorators import cached
--- a/entities/lib.py	Thu May 20 20:47:13 2010 +0200
+++ b/entities/lib.py	Thu May 20 20:47:55 2010 +0200
@@ -48,13 +48,13 @@
 
     @property
     def email_of(self):
-        return self.reverse_use_email and self.reverse_use_email[0]
+        return self.reverse_use_email and self.reverse_use_email[0] or None
 
     @property
     def prefered(self):
         return self.prefered_form and self.prefered_form[0] or self
 
-    @deprecated('use .prefered')
+    @deprecated('[3.6] use .prefered')
     def canonical_form(self):
         return self.prefered_form and self.prefered_form[0] or self
 
@@ -89,14 +89,6 @@
             return self.display_address()
         return super(EmailAddress, self).printable_value(attr, value, attrtype, format)
 
-    def after_deletion_path(self):
-        """return (path, parameters) which should be used as redirect
-        information when this entity is being deleted
-        """
-        if self.email_of:
-            return self.email_of.rest_path(), {}
-        return super(EmailAddress, self).after_deletion_path()
-
 
 class Bookmark(AnyEntity):
     """customized class for Bookmark entities"""
@@ -133,12 +125,6 @@
         except UnknownProperty:
             return u''
 
-    def after_deletion_path(self):
-        """return (path, parameters) which should be used as redirect
-        information when this entity is being deleted
-        """
-        return 'view', {}
-
 
 class CWCache(AnyEntity):
     """Cache"""
--- a/entities/schemaobjs.py	Thu May 20 20:47:13 2010 +0200
+++ b/entities/schemaobjs.py	Thu May 20 20:47:55 2010 +0200
@@ -115,14 +115,6 @@
             scard, self.relation_type[0].name, ocard,
             self.to_entity[0].name)
 
-    def after_deletion_path(self):
-        """return (path, parameters) which should be used as redirect
-        information when this entity is being deleted
-        """
-        if self.relation_type:
-            return self.relation_type[0].rest_path(), {}
-        return super(CWRelation, self).after_deletion_path()
-
     @property
     def rtype(self):
         return self.relation_type[0]
@@ -139,6 +131,7 @@
         rschema = self._cw.vreg.schema.rschema(self.rtype.name)
         return rschema.rdefs[(self.stype.name, self.otype.name)]
 
+
 class CWAttribute(CWRelation):
     __regid__ = 'CWAttribute'
 
@@ -160,14 +153,6 @@
     def dc_title(self):
         return '%s(%s)' % (self.cstrtype[0].name, self.value or u'')
 
-    def after_deletion_path(self):
-        """return (path, parameters) which should be used as redirect
-        information when this entity is being deleted
-        """
-        if self.reverse_constrained_by:
-            return self.reverse_constrained_by[0].rest_path(), {}
-        return super(CWConstraint, self).after_deletion_path()
-
     @property
     def type(self):
         return self.cstrtype[0].name
@@ -201,14 +186,6 @@
     def check_expression(self, *args, **kwargs):
         return self._rqlexpr().check(*args, **kwargs)
 
-    def after_deletion_path(self):
-        """return (path, parameters) which should be used as redirect
-        information when this entity is being deleted
-        """
-        if self.expression_of:
-            return self.expression_of.rest_path(), {}
-        return super(RQLExpression, self).after_deletion_path()
-
 
 class CWPermission(AnyEntity):
     __regid__ = 'CWPermission'
@@ -218,12 +195,3 @@
         if self.label:
             return '%s (%s)' % (self._cw._(self.name), self.label)
         return self._cw._(self.name)
-
-    def after_deletion_path(self):
-        """return (path, parameters) which should be used as redirect
-        information when this entity is being deleted
-        """
-        permissionof = getattr(self, 'reverse_require_permission', ())
-        if len(permissionof) == 1:
-            return permissionof[0].rest_path(), {}
-        return super(CWPermission, self).after_deletion_path()
--- a/entities/test/unittest_base.py	Thu May 20 20:47:13 2010 +0200
+++ b/entities/test/unittest_base.py	Thu May 20 20:47:55 2010 +0200
@@ -106,7 +106,7 @@
     def test_allowed_massmail_keys(self):
         e = self.execute('CWUser U WHERE U login "member"').get_entity(0, 0)
         # Bytes/Password attributes should be omited
-        self.assertEquals(e.allowed_massmail_keys(),
+        self.assertEquals(e.cw_adapt_to('IEmailable').allowed_massmail_keys(),
                           set(('surname', 'firstname', 'login', 'last_login_time',
                                'creation_date', 'modification_date', 'cwuri', 'eid'))
                           )
--- a/entities/test/unittest_wfobjs.py	Thu May 20 20:47:13 2010 +0200
+++ b/entities/test/unittest_wfobjs.py	Thu May 20 20:47:55 2010 +0200
@@ -100,35 +100,38 @@
 
     def test_workflow_base(self):
         e = self.create_user('toto')
-        self.assertEquals(e.state, 'activated')
-        e.change_state('deactivated', u'deactivate 1')
+        iworkflowable = e.cw_adapt_to('IWorkflowable')
+        self.assertEquals(iworkflowable.state, 'activated')
+        iworkflowable.change_state('deactivated', u'deactivate 1')
         self.commit()
-        e.change_state('activated', u'activate 1')
+        iworkflowable.change_state('activated', u'activate 1')
         self.commit()
-        e.change_state('deactivated', u'deactivate 2')
+        iworkflowable.change_state('deactivated', u'deactivate 2')
         self.commit()
         e.clear_related_cache('wf_info_for', 'object')
         self.assertEquals([tr.comment for tr in e.reverse_wf_info_for],
                           ['deactivate 1', 'activate 1', 'deactivate 2'])
-        self.assertEquals(e.latest_trinfo().comment, 'deactivate 2')
+        self.assertEquals(iworkflowable.latest_trinfo().comment, 'deactivate 2')
 
     def test_possible_transitions(self):
         user = self.execute('CWUser X').get_entity(0, 0)
-        trs = list(user.possible_transitions())
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
+        trs = list(iworkflowable.possible_transitions())
         self.assertEquals(len(trs), 1)
         self.assertEquals(trs[0].name, u'deactivate')
         self.assertEquals(trs[0].destination(None).name, u'deactivated')
         # test a std user get no possible transition
         cnx = self.login('member')
         # fetch the entity using the new session
-        trs = list(cnx.user().possible_transitions())
+        trs = list(cnx.user().cw_adapt_to('IWorkflowable').possible_transitions())
         self.assertEquals(len(trs), 0)
 
     def _test_manager_deactivate(self, user):
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
         user.clear_related_cache('in_state', 'subject')
         self.assertEquals(len(user.in_state), 1)
-        self.assertEquals(user.state, 'deactivated')
-        trinfo = user.latest_trinfo()
+        self.assertEquals(iworkflowable.state, 'deactivated')
+        trinfo = iworkflowable.latest_trinfo()
         self.assertEquals(trinfo.previous_state.name, 'activated')
         self.assertEquals(trinfo.new_state.name, 'deactivated')
         self.assertEquals(trinfo.comment, 'deactivate user')
@@ -137,7 +140,8 @@
 
     def test_change_state(self):
         user = self.user()
-        user.change_state('deactivated', comment=u'deactivate user')
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
+        iworkflowable.change_state('deactivated', comment=u'deactivate user')
         trinfo = self._test_manager_deactivate(user)
         self.assertEquals(trinfo.transition, None)
 
@@ -154,33 +158,36 @@
 
     def test_fire_transition(self):
         user = self.user()
-        user.fire_transition('deactivate', comment=u'deactivate user')
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('deactivate', comment=u'deactivate user')
         user.clear_all_caches()
-        self.assertEquals(user.state, 'deactivated')
+        self.assertEquals(iworkflowable.state, 'deactivated')
         self._test_manager_deactivate(user)
         trinfo = self._test_manager_deactivate(user)
         self.assertEquals(trinfo.transition.name, 'deactivate')
 
     def test_goback_transition(self):
-        wf = self.session.user.current_workflow
+        wf = self.session.user.cw_adapt_to('IWorkflowable').current_workflow
         asleep = wf.add_state('asleep')
-        wf.add_transition('rest', (wf.state_by_name('activated'), wf.state_by_name('deactivated')),
-                               asleep)
+        wf.add_transition('rest', (wf.state_by_name('activated'),
+                                   wf.state_by_name('deactivated')),
+                          asleep)
         wf.add_transition('wake up', asleep)
         user = self.create_user('stduser')
-        user.fire_transition('rest')
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('rest')
         self.commit()
-        user.fire_transition('wake up')
+        iworkflowable.fire_transition('wake up')
         self.commit()
-        self.assertEquals(user.state, 'activated')
-        user.fire_transition('deactivate')
+        self.assertEquals(iworkflowable.state, 'activated')
+        iworkflowable.fire_transition('deactivate')
         self.commit()
-        user.fire_transition('rest')
+        iworkflowable.fire_transition('rest')
         self.commit()
-        user.fire_transition('wake up')
+        iworkflowable.fire_transition('wake up')
         self.commit()
         user.clear_all_caches()
-        self.assertEquals(user.state, 'deactivated')
+        self.assertEquals(iworkflowable.state, 'deactivated')
 
     # XXX test managers can change state without matching transition
 
@@ -189,18 +196,18 @@
         self.create_user('tutu')
         cnx = self.login('tutu')
         req = self.request()
-        member = req.entity_from_eid(self.member.eid)
+        iworkflowable = req.entity_from_eid(self.member.eid).cw_adapt_to('IWorkflowable')
         ex = self.assertRaises(ValidationError,
-                               member.fire_transition, 'deactivate')
+                               iworkflowable.fire_transition, 'deactivate')
         self.assertEquals(ex.errors, {'by_transition-subject': "transition may not be fired"})
         cnx.close()
         cnx = self.login('member')
         req = self.request()
-        member = req.entity_from_eid(self.member.eid)
-        member.fire_transition('deactivate')
+        iworkflowable = req.entity_from_eid(self.member.eid).cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('deactivate')
         cnx.commit()
         ex = self.assertRaises(ValidationError,
-                               member.fire_transition, 'activate')
+                               iworkflowable.fire_transition, 'activate')
         self.assertEquals(ex.errors, {'by_transition-subject': "transition may not be fired"})
 
     def test_fire_transition_owned_by(self):
@@ -250,43 +257,44 @@
                                       [(swfstate2, state2), (swfstate3, state3)])
         self.assertEquals(swftr1.destination(None).eid, swfstate1.eid)
         # workflows built, begin test
-        self.group = self.request().create_entity('CWGroup', name=u'grp1')
+        group = self.request().create_entity('CWGroup', name=u'grp1')
         self.commit()
-        self.assertEquals(self.group.current_state.eid, state1.eid)
-        self.assertEquals(self.group.current_workflow.eid, mwf.eid)
-        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
-        self.assertEquals(self.group.subworkflow_input_transition(), None)
-        self.group.fire_transition('swftr1', u'go')
+        iworkflowable = group.cw_adapt_to('IWorkflowable')
+        self.assertEquals(iworkflowable.current_state.eid, state1.eid)
+        self.assertEquals(iworkflowable.current_workflow.eid, mwf.eid)
+        self.assertEquals(iworkflowable.main_workflow.eid, mwf.eid)
+        self.assertEquals(iworkflowable.subworkflow_input_transition(), None)
+        iworkflowable.fire_transition('swftr1', u'go')
         self.commit()
-        self.group.clear_all_caches()
-        self.assertEquals(self.group.current_state.eid, swfstate1.eid)
-        self.assertEquals(self.group.current_workflow.eid, swf.eid)
-        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
-        self.assertEquals(self.group.subworkflow_input_transition().eid, swftr1.eid)
-        self.group.fire_transition('tr1', u'go')
+        group.clear_all_caches()
+        self.assertEquals(iworkflowable.current_state.eid, swfstate1.eid)
+        self.assertEquals(iworkflowable.current_workflow.eid, swf.eid)
+        self.assertEquals(iworkflowable.main_workflow.eid, mwf.eid)
+        self.assertEquals(iworkflowable.subworkflow_input_transition().eid, swftr1.eid)
+        iworkflowable.fire_transition('tr1', u'go')
         self.commit()
-        self.group.clear_all_caches()
-        self.assertEquals(self.group.current_state.eid, state2.eid)
-        self.assertEquals(self.group.current_workflow.eid, mwf.eid)
-        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
-        self.assertEquals(self.group.subworkflow_input_transition(), None)
+        group.clear_all_caches()
+        self.assertEquals(iworkflowable.current_state.eid, state2.eid)
+        self.assertEquals(iworkflowable.current_workflow.eid, mwf.eid)
+        self.assertEquals(iworkflowable.main_workflow.eid, mwf.eid)
+        self.assertEquals(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,
-                               self.group.change_state, swfstate1, u'gadget')
+                               iworkflowable.change_state, swfstate1, u'gadget')
         self.assertEquals(ex.errors, {'to_state-subject': "state doesn't belong to entity's workflow"})
         self.rollback()
         # force back to state1
-        self.group.change_state('state1', u'gadget')
-        self.group.fire_transition('swftr1', u'au')
-        self.group.clear_all_caches()
-        self.group.fire_transition('tr2', u'chapeau')
+        iworkflowable.change_state('state1', u'gadget')
+        iworkflowable.fire_transition('swftr1', u'au')
+        group.clear_all_caches()
+        iworkflowable.fire_transition('tr2', u'chapeau')
         self.commit()
-        self.group.clear_all_caches()
-        self.assertEquals(self.group.current_state.eid, state3.eid)
-        self.assertEquals(self.group.current_workflow.eid, mwf.eid)
-        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
-        self.assertListEquals(parse_hist(self.group.workflow_history),
+        group.clear_all_caches()
+        self.assertEquals(iworkflowable.current_state.eid, state3.eid)
+        self.assertEquals(iworkflowable.current_workflow.eid, mwf.eid)
+        self.assertEquals(iworkflowable.main_workflow.eid, mwf.eid)
+        self.assertListEquals(parse_hist(iworkflowable.workflow_history),
                               [('state1', 'swfstate1', 'swftr1', 'go'),
                                ('swfstate1', 'swfstate2', 'tr1', 'go'),
                                ('swfstate2', 'state2', 'swftr1', 'exiting from subworkflow subworkflow'),
@@ -337,8 +345,9 @@
         self.commit()
         group = self.request().create_entity('CWGroup', name=u'grp1')
         self.commit()
+        iworkflowable = group.cw_adapt_to('IWorkflowable')
         for trans in ('identify', 'release', 'close'):
-            group.fire_transition(trans)
+            iworkflowable.fire_transition(trans)
             self.commit()
 
 
@@ -362,6 +371,7 @@
         self.commit()
         group = self.request().create_entity('CWGroup', name=u'grp1')
         self.commit()
+        iworkflowable = group.cw_adapt_to('IWorkflowable')
         for trans, nextstate in (('identify', 'xsigning'),
                                  ('xabort', 'created'),
                                  ('identify', 'xsigning'),
@@ -369,10 +379,10 @@
                                  ('release', 'xsigning'),
                                  ('xabort', 'identified')
                                  ):
-            group.fire_transition(trans)
+            iworkflowable.fire_transition(trans)
             self.commit()
             group.clear_all_caches()
-            self.assertEquals(group.state, nextstate)
+            self.assertEquals(iworkflowable.state, nextstate)
 
 
 class CustomWorkflowTC(CubicWebTC):
@@ -389,35 +399,38 @@
         self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
                      {'wf': wf.eid, 'x': self.member.eid})
         self.member.clear_all_caches()
-        self.assertEquals(self.member.state, 'activated')# no change before commit
+        iworkflowable = self.member.cw_adapt_to('IWorkflowable')
+        self.assertEquals(iworkflowable.state, 'activated')# no change before commit
         self.commit()
         self.member.clear_all_caches()
-        self.assertEquals(self.member.current_workflow.eid, wf.eid)
-        self.assertEquals(self.member.state, 'asleep')
-        self.assertEquals(self.member.workflow_history, ())
+        self.assertEquals(iworkflowable.current_workflow.eid, wf.eid)
+        self.assertEquals(iworkflowable.state, 'asleep')
+        self.assertEquals(iworkflowable.workflow_history, ())
 
     def test_custom_wf_replace_state_keep_history(self):
         """member in inital state with some history, state is redirected and
         state change is recorded to history
         """
-        self.member.fire_transition('deactivate')
-        self.member.fire_transition('activate')
+        iworkflowable = self.member.cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('deactivate')
+        iworkflowable.fire_transition('activate')
         wf = add_wf(self, 'CWUser')
         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})
         self.commit()
         self.member.clear_all_caches()
-        self.assertEquals(self.member.current_workflow.eid, wf.eid)
-        self.assertEquals(self.member.state, 'asleep')
-        self.assertEquals(parse_hist(self.member.workflow_history),
+        self.assertEquals(iworkflowable.current_workflow.eid, wf.eid)
+        self.assertEquals(iworkflowable.state, 'asleep')
+        self.assertEquals(parse_hist(iworkflowable.workflow_history),
                           [('activated', 'deactivated', 'deactivate', None),
                            ('deactivated', 'activated', 'activate', None),
                            ('activated', 'asleep', None, 'workflow changed to "CWUser"')])
 
     def test_custom_wf_no_initial_state(self):
         """try to set a custom workflow which has no initial state"""
-        self.member.fire_transition('deactivate')
+        iworkflowable = self.member.cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('deactivate')
         wf = add_wf(self, 'CWUser')
         wf.add_state('asleep')
         self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
@@ -438,7 +451,8 @@
         """member in some state shared by the new workflow, nothing has to be
         done
         """
-        self.member.fire_transition('deactivate')
+        iworkflowable = self.member.cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('deactivate')
         wf = add_wf(self, 'CWUser')
         wf.add_state('asleep', initial=True)
         self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
@@ -447,12 +461,12 @@
         self.execute('DELETE X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
                      {'wf': wf.eid, 'x': self.member.eid})
         self.member.clear_all_caches()
-        self.assertEquals(self.member.state, 'asleep')# no change before commit
+        self.assertEquals(iworkflowable.state, 'asleep')# no change before commit
         self.commit()
         self.member.clear_all_caches()
-        self.assertEquals(self.member.current_workflow.name, "default user workflow")
-        self.assertEquals(self.member.state, 'activated')
-        self.assertEquals(parse_hist(self.member.workflow_history),
+        self.assertEquals(iworkflowable.current_workflow.name, "default user workflow")
+        self.assertEquals(iworkflowable.state, 'activated')
+        self.assertEquals(parse_hist(iworkflowable.workflow_history),
                           [('activated', 'deactivated', 'deactivate', None),
                            ('deactivated', 'asleep', None, 'workflow changed to "CWUser"'),
                            ('asleep', 'activated', None, 'workflow changed to "default user workflow"'),])
@@ -473,28 +487,29 @@
     def test_auto_transition_fired(self):
         wf = self.setup_custom_wf()
         user = self.create_user('member')
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
         self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
                      {'wf': wf.eid, 'x': user.eid})
         self.commit()
         user.clear_all_caches()
-        self.assertEquals(user.state, 'asleep')
-        self.assertEquals([t.name for t in user.possible_transitions()],
+        self.assertEquals(iworkflowable.state, 'asleep')
+        self.assertEquals([t.name for t in iworkflowable.possible_transitions()],
                           ['rest'])
-        user.fire_transition('rest')
+        iworkflowable.fire_transition('rest')
         self.commit()
         user.clear_all_caches()
-        self.assertEquals(user.state, 'asleep')
-        self.assertEquals([t.name for t in user.possible_transitions()],
+        self.assertEquals(iworkflowable.state, 'asleep')
+        self.assertEquals([t.name for t in iworkflowable.possible_transitions()],
                           ['rest'])
-        self.assertEquals(parse_hist(user.workflow_history),
+        self.assertEquals(parse_hist(iworkflowable.workflow_history),
                           [('asleep', 'asleep', 'rest', None)])
         user.set_attributes(surname=u'toto') # fulfill condition
         self.commit()
-        user.fire_transition('rest')
+        iworkflowable.fire_transition('rest')
         self.commit()
         user.clear_all_caches()
-        self.assertEquals(user.state, 'dead')
-        self.assertEquals(parse_hist(user.workflow_history),
+        self.assertEquals(iworkflowable.state, 'dead')
+        self.assertEquals(parse_hist(iworkflowable.workflow_history),
                           [('asleep', 'asleep', 'rest', None),
                            ('asleep', 'asleep', 'rest', None),
                            ('asleep', 'dead', 'sick', None),])
@@ -505,7 +520,8 @@
         self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
                      {'wf': wf.eid, 'x': user.eid})
         self.commit()
-        self.assertEquals(user.state, 'dead')
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
+        self.assertEquals(iworkflowable.state, 'dead')
 
     def test_auto_transition_initial_state_fired(self):
         wf = self.execute('Any WF WHERE ET default_workflow WF, '
@@ -517,14 +533,15 @@
         self.commit()
         user = self.create_user('member', surname=u'toto')
         self.commit()
-        self.assertEquals(user.state, 'dead')
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
+        self.assertEquals(iworkflowable.state, 'dead')
 
 
 class WorkflowHooksTC(CubicWebTC):
 
     def setUp(self):
         CubicWebTC.setUp(self)
-        self.wf = self.session.user.current_workflow
+        self.wf = self.session.user.cw_adapt_to('IWorkflowable').current_workflow
         self.session.set_pool()
         self.s_activated = self.wf.state_by_name('activated').eid
         self.s_deactivated = self.wf.state_by_name('deactivated').eid
@@ -572,8 +589,9 @@
     def test_transition_checking1(self):
         cnx = self.login('stduser')
         user = cnx.user(self.session)
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
         ex = self.assertRaises(ValidationError,
-                               user.fire_transition, 'activate')
+                               iworkflowable.fire_transition, 'activate')
         self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']),
                           u"transition isn't allowed from")
         cnx.close()
@@ -581,8 +599,9 @@
     def test_transition_checking2(self):
         cnx = self.login('stduser')
         user = cnx.user(self.session)
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
         ex = self.assertRaises(ValidationError,
-                               user.fire_transition, 'dummy')
+                               iworkflowable.fire_transition, 'dummy')
         self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']),
                           u"transition isn't allowed from")
         cnx.close()
@@ -591,15 +610,16 @@
         cnx = self.login('stduser')
         session = self.session
         user = cnx.user(session)
-        user.fire_transition('deactivate')
+        iworkflowable = user.cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('deactivate')
         cnx.commit()
         session.set_pool()
         ex = self.assertRaises(ValidationError,
-                               user.fire_transition, 'deactivate')
+                               iworkflowable.fire_transition, 'deactivate')
         self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']),
                                             u"transition isn't allowed from")
         # get back now
-        user.fire_transition('activate')
+        iworkflowable.fire_transition('activate')
         cnx.commit()
         cnx.close()
 
--- a/entities/wfobjs.py	Thu May 20 20:47:13 2010 +0200
+++ b/entities/wfobjs.py	Thu May 20 20:47:55 2010 +0200
@@ -15,9 +15,13 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""workflow definition and history related entities
+"""workflow handling:
 
+* entity types defining workflow (Workflow, State, Transition...)
+* workflow history (TrInfo)
+* adapter for workflowable entities (IWorkflowableAdapter)
 """
+
 __docformat__ = "restructuredtext en"
 
 from warnings import warn
@@ -27,7 +31,8 @@
 from logilab.common.compat import any
 
 from cubicweb.entities import AnyEntity, fetch_config
-from cubicweb.interfaces import IWorkflowable
+from cubicweb.view import EntityAdapter
+from cubicweb.selectors import relation_possible
 from cubicweb.mixins import MI_REL_TRIGGERS
 
 class WorkflowException(Exception): pass
@@ -47,15 +52,6 @@
         return any(et for et in self.reverse_default_workflow
                    if et.name == etype)
 
-    # XXX define parent() instead? what if workflow of multiple types?
-    def after_deletion_path(self):
-        """return (path, parameters) which should be used as redirect
-        information when this entity is being deleted
-        """
-        if self.workflow_of:
-            return self.workflow_of[0].rest_path(), {'vid': 'workflow'}
-        return super(Workflow, self).after_deletion_path()
-
     def iter_workflows(self, _done=None):
         """return an iterator on actual workflows, eg this workflow and its
         subworkflows
@@ -226,14 +222,6 @@
             return False
         return True
 
-    def after_deletion_path(self):
-        """return (path, parameters) which should be used as redirect
-        information when this entity is being deleted
-        """
-        if self.transition_of:
-            return self.transition_of[0].rest_path(), {}
-        return super(BaseTransition, self).after_deletion_path()
-
     def set_permissions(self, requiredgroups=(), conditions=(), reset=True):
         """set or add (if `reset` is False) groups and conditions for this
         transition
@@ -277,7 +265,7 @@
         try:
             return self.destination_state[0]
         except IndexError:
-            return entity.latest_trinfo().previous_state
+            return entity.cw_adapt_to('IWorkflowable').latest_trinfo().previous_state
 
     def potential_destinations(self):
         try:
@@ -288,9 +276,6 @@
                     for previousstate in tr.reverse_allowed_transition:
                         yield previousstate
 
-    def parent(self):
-        return self.workflow
-
 
 class WorkflowTransition(BaseTransition):
     """customized class for WorkflowTransition entities"""
@@ -331,7 +316,7 @@
             return None
         if tostateeid is None:
             # go back to state from which we've entered the subworkflow
-            return entity.subworkflow_input_trinfo().previous_state
+            return entity.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo().previous_state
         return self._cw.entity_from_eid(tostateeid)
 
     @cached
@@ -358,9 +343,6 @@
     def destination(self):
         return self.destination_state and self.destination_state[0] or None
 
-    def parent(self):
-        return self.reverse_subworkflow_exit[0]
-
 
 class State(AnyEntity):
     """customized class for State entities"""
@@ -371,10 +353,7 @@
     @property
     def workflow(self):
         # take care, may be missing in multi-sources configuration
-        return self.state_of and self.state_of[0]
-
-    def parent(self):
-        return self.workflow
+        return self.state_of and self.state_of[0] or None
 
 
 class TrInfo(AnyEntity):
@@ -399,22 +378,99 @@
     def transition(self):
         return self.by_transition and self.by_transition[0] or None
 
-    def parent(self):
-        return self.for_entity
-
 
 class WorkflowableMixIn(object):
     """base mixin providing workflow helper methods for workflowable entities.
     This mixin will be automatically set on class supporting the 'in_state'
     relation (which implies supporting 'wf_info_for' as well)
     """
-    __implements__ = (IWorkflowable,)
+
+    @property
+    @deprecated('[3.5] use printable_state')
+    def displayable_state(self):
+        return self._cw._(self.state)
+    @property
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').main_workflow")
+    def main_workflow(self):
+        return self.cw_adapt_to('IWorkflowable').main_workflow
+    @property
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').current_workflow")
+    def current_workflow(self):
+        return self.cw_adapt_to('IWorkflowable').current_workflow
+    @property
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').current_state")
+    def current_state(self):
+        return self.cw_adapt_to('IWorkflowable').current_state
+    @property
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').state")
+    def state(self):
+        return self.cw_adapt_to('IWorkflowable').state
+    @property
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').printable_state")
+    def printable_state(self):
+        return self.cw_adapt_to('IWorkflowable').printable_state
+    @property
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').workflow_history")
+    def workflow_history(self):
+        return self.cw_adapt_to('IWorkflowable').workflow_history
+
+    @deprecated('[3.5] get transition from current workflow and use its may_be_fired method')
+    def can_pass_transition(self, trname):
+        """return the Transition instance if the current user can fire the
+        transition with the given name, else None
+        """
+        tr = self.current_workflow and self.current_workflow.transition_by_name(trname)
+        if tr and tr.may_be_fired(self.eid):
+            return tr
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').cwetype_workflow()")
+    def cwetype_workflow(self):
+        return self.cw_adapt_to('IWorkflowable').main_workflow()
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').latest_trinfo()")
+    def latest_trinfo(self):
+        return self.cw_adapt_to('IWorkflowable').latest_trinfo()
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').possible_transitions()")
+    def possible_transitions(self, type='normal'):
+        return self.cw_adapt_to('IWorkflowable').possible_transitions(type)
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').fire_transition()")
+    def fire_transition(self, tr, comment=None, commentformat=None):
+        return self.cw_adapt_to('IWorkflowable').fire_transition(tr, comment, commentformat)
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').change_state()")
+    def change_state(self, statename, comment=None, commentformat=None, tr=None):
+        return self.cw_adapt_to('IWorkflowable').change_state(statename, comment, commentformat, tr)
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo()")
+    def subworkflow_input_trinfo(self):
+        return self.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo()
+    @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').subworkflow_input_transition()")
+    def subworkflow_input_transition(self):
+        return self.cw_adapt_to('IWorkflowable').subworkflow_input_transition()
+
+
+MI_REL_TRIGGERS[('in_state', 'subject')] = WorkflowableMixIn
+
+
+
+class IWorkflowableAdapter(WorkflowableMixIn, EntityAdapter):
+    """base adapter providing workflow helper methods for workflowable entities.
+    """
+    __regid__ = 'IWorkflowable'
+    __select__ = relation_possible('in_state')
+
+    @cached
+    def cwetype_workflow(self):
+        """return the default workflow for entities of this type"""
+        # XXX CWEType method
+        wfrset = self._cw.execute('Any WF WHERE ET default_workflow WF, '
+                                  'ET name %(et)s', {'et': self.entity.__regid__})
+        if wfrset:
+            return wfrset.get_entity(0, 0)
+        self.warning("can't find any workflow for %s", self.entity.__regid__)
+        return None
 
     @property
     def main_workflow(self):
         """return current workflow applied to this entity"""
-        if self.custom_workflow:
-            return self.custom_workflow[0]
+        if self.entity.custom_workflow:
+            return self.entity.custom_workflow[0]
         return self.cwetype_workflow()
 
     @property
@@ -425,14 +481,14 @@
     @property
     def current_state(self):
         """return current state entity"""
-        return self.in_state and self.in_state[0] or None
+        return self.entity.in_state and self.entity.in_state[0] or None
 
     @property
     def state(self):
         """return current state name"""
         try:
-            return self.in_state[0].name
-        except IndexError:
+            return self.current_state.name
+        except AttributeError:
             self.warning('entity %s has no state', self)
             return None
 
@@ -449,26 +505,15 @@
         """return the workflow history for this entity (eg ordered list of
         TrInfo entities)
         """
-        return self.reverse_wf_info_for
+        return self.entity.reverse_wf_info_for
 
     def latest_trinfo(self):
         """return the latest transition information for this entity"""
         try:
-            return self.reverse_wf_info_for[-1]
+            return self.workflow_history[-1]
         except IndexError:
             return None
 
-    @cached
-    def cwetype_workflow(self):
-        """return the default workflow for entities of this type"""
-        # XXX CWEType method
-        wfrset = self._cw.execute('Any WF WHERE ET default_workflow WF, '
-                                  'ET name %(et)s', {'et': self.__regid__})
-        if wfrset:
-            return wfrset.get_entity(0, 0)
-        self.warning("can't find any workflow for %s", self.__regid__)
-        return None
-
     def possible_transitions(self, type='normal'):
         """generates transition that MAY be fired for the given entity,
         expected to be in this state
@@ -483,16 +528,44 @@
             {'x': self.current_state.eid, 'type': type,
              'wfeid': self.current_workflow.eid})
         for tr in rset.entities():
-            if tr.may_be_fired(self.eid):
+            if tr.may_be_fired(self.entity.eid):
                 yield tr
 
+    def subworkflow_input_trinfo(self):
+        """return the TrInfo which has be recorded when this entity went into
+        the current sub-workflow
+        """
+        if self.main_workflow.eid == self.current_workflow.eid:
+            return # doesn't make sense
+        subwfentries = []
+        for trinfo in self.workflow_history:
+            if (trinfo.transition and
+                trinfo.previous_state.workflow.eid != trinfo.new_state.workflow.eid):
+                # entering or leaving a subworkflow
+                if (subwfentries and
+                    subwfentries[-1].new_state.workflow.eid == trinfo.previous_state.workflow.eid and
+                    subwfentries[-1].previous_state.workflow.eid == trinfo.new_state.workflow.eid):
+                    # leave
+                    del subwfentries[-1]
+                else:
+                    # enter
+                    subwfentries.append(trinfo)
+        if not subwfentries:
+            return None
+        return subwfentries[-1]
+
+    def subworkflow_input_transition(self):
+        """return the transition which has went through the current sub-workflow
+        """
+        return getattr(self.subworkflow_input_trinfo(), 'transition', None)
+
     def _add_trinfo(self, comment, commentformat, treid=None, tseid=None):
         kwargs = {}
         if comment is not None:
             kwargs['comment'] = comment
             if commentformat is not None:
                 kwargs['comment_format'] = commentformat
-        kwargs['wf_info_for'] = self
+        kwargs['wf_info_for'] = self.entity
         if treid is not None:
             kwargs['by_transition'] = self._cw.entity_from_eid(treid)
         if tseid is not None:
@@ -532,51 +605,3 @@
             stateeid = state.eid
         # XXX try to find matching transition?
         return self._add_trinfo(comment, commentformat, tr and tr.eid, stateeid)
-
-    def subworkflow_input_trinfo(self):
-        """return the TrInfo which has be recorded when this entity went into
-        the current sub-workflow
-        """
-        if self.main_workflow.eid == self.current_workflow.eid:
-            return # doesn't make sense
-        subwfentries = []
-        for trinfo in self.workflow_history:
-            if (trinfo.transition and
-                trinfo.previous_state.workflow.eid != trinfo.new_state.workflow.eid):
-                # entering or leaving a subworkflow
-                if (subwfentries and
-                    subwfentries[-1].new_state.workflow.eid == trinfo.previous_state.workflow.eid and
-                    subwfentries[-1].previous_state.workflow.eid == trinfo.new_state.workflow.eid):
-                    # leave
-                    del subwfentries[-1]
-                else:
-                    # enter
-                    subwfentries.append(trinfo)
-        if not subwfentries:
-            return None
-        return subwfentries[-1]
-
-    def subworkflow_input_transition(self):
-        """return the transition which has went through the current sub-workflow
-        """
-        return getattr(self.subworkflow_input_trinfo(), 'transition', None)
-
-    def clear_all_caches(self):
-        super(WorkflowableMixIn, self).clear_all_caches()
-        clear_cache(self, 'cwetype_workflow')
-
-    @deprecated('[3.5] get transition from current workflow and use its may_be_fired method')
-    def can_pass_transition(self, trname):
-        """return the Transition instance if the current user can fire the
-        transition with the given name, else None
-        """
-        tr = self.current_workflow and self.current_workflow.transition_by_name(trname)
-        if tr and tr.may_be_fired(self.eid):
-            return tr
-
-    @property
-    @deprecated('[3.5] use printable_state')
-    def displayable_state(self):
-        return self._cw._(self.state)
-
-MI_REL_TRIGGERS[('in_state', 'subject')] = WorkflowableMixIn
--- a/entity.py	Thu May 20 20:47:13 2010 +0200
+++ b/entity.py	Thu May 20 20:47:55 2010 +0200
@@ -107,10 +107,10 @@
                     if not interface.implements(cls, iface):
                         interface.extend(cls, iface)
             if role == 'subject':
-                setattr(cls, rschema.type, SubjectRelation(rschema))
+                attr = rschema.type
             else:
                 attr = 'reverse_%s' % rschema.type
-                setattr(cls, attr, ObjectRelation(rschema))
+            setattr(cls, attr, Relation(rschema, role))
         if mixins:
             # see etype class instantation in cwvreg.ETypeRegistry.etype_class method:
             # due to class dumping, cls is the generated top level class with actual
@@ -125,6 +125,24 @@
             cls.__bases__ = tuple(mixins)
             cls.info('plugged %s mixins on %s', mixins, cls)
 
+    fetch_attrs = ('modification_date',)
+    @classmethod
+    def fetch_order(cls, attr, var):
+        """class method used to control sort order when multiple entities of
+        this type are fetched
+        """
+        return cls.fetch_unrelated_order(attr, var)
+
+    @classmethod
+    def fetch_unrelated_order(cls, attr, var):
+        """class method used to control sort order when multiple entities of
+        this type are fetched to use in edition (eg propose them to create a
+        new relation on an edited entity).
+        """
+        if attr == 'modification_date':
+            return '%s DESC' % var
+        return None
+
     @classmethod
     def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
                   settype=True, ordermethod='fetch_order'):
@@ -378,6 +396,23 @@
         for attr, value in values.items():
             self[attr] = value # use self.__setitem__ implementation
 
+    def cw_adapt_to(self, interface):
+        """return an adapter the entity to the given interface name.
+
+        return None if it can not be adapted.
+        """
+        try:
+            cache = self._cw_adapters_cache
+        except AttributeError:
+            self._cw_adapters_cache = cache = {}
+        try:
+            return cache[interface]
+        except KeyError:
+            adapter = self._cw.vreg['adapters'].select_or_none(
+                interface, self._cw, entity=self)
+            cache[interface] = adapter
+            return adapter
+
     def rql_set_value(self, attr, value):
         """call by rql execution plan when some attribute is modified
 
@@ -949,6 +984,10 @@
             del self.__unique
         except AttributeError:
             pass
+        try:
+            del self._cw_adapters_cache
+        except AttributeError:
+            pass
 
     # raw edition utilities ###################################################
 
@@ -1038,61 +1077,6 @@
         self.e_schema.check(self, creation=creation, _=_,
                             relations=relations)
 
-    def fti_containers(self, _done=None):
-        if _done is None:
-            _done = set()
-        _done.add(self.eid)
-        containers = tuple(self.e_schema.fulltext_containers())
-        if containers:
-            for rschema, target in containers:
-                if target == 'object':
-                    targets = getattr(self, rschema.type)
-                else:
-                    targets = getattr(self, 'reverse_%s' % rschema)
-                for entity in targets:
-                    if entity.eid in _done:
-                        continue
-                    for container in entity.fti_containers(_done):
-                        yield container
-                        yielded = True
-        else:
-            yield self
-
-    def get_words(self):
-        """used by the full text indexer to get words to index
-
-        this method should only be used on the repository side since it depends
-        on the logilab.database package
-
-        :rtype: list
-        :return: the list of indexable word of this entity
-        """
-        from logilab.database.fti import tokenize
-        # take care to cases where we're modyfying the schema
-        pending = self._cw.transaction_data.setdefault('pendingrdefs', set())
-        words = []
-        for rschema in self.e_schema.indexable_attributes():
-            if (self.e_schema, rschema) in pending:
-                continue
-            try:
-                value = self.printable_value(rschema, format='text/plain')
-            except TransformError:
-                continue
-            except:
-                self.exception("can't add value of %s to text index for entity %s",
-                               rschema, self.eid)
-                continue
-            if value:
-                words += tokenize(value)
-        for rschema, role in self.e_schema.fulltext_relations():
-            if role == 'subject':
-                for entity in getattr(self, rschema.type):
-                    words += entity.get_words()
-            else: # if role == 'object':
-                for entity in getattr(self, 'reverse_%s' % rschema.type):
-                    words += entity.get_words()
-        return words
-
 
 # attribute and relation descriptors ##########################################
 
@@ -1111,13 +1095,13 @@
     def __set__(self, eobj, value):
         eobj[self._attrname] = value
 
+
 class Relation(object):
     """descriptor that controls schema relation access"""
-    _role = None # for pylint
 
-    def __init__(self, rschema):
-        self._rschema = rschema
+    def __init__(self, rschema, role):
         self._rtype = rschema.type
+        self._role = role
 
     def __get__(self, eobj, eclass):
         if eobj is None:
@@ -1129,14 +1113,6 @@
         raise NotImplementedError
 
 
-class SubjectRelation(Relation):
-    """descriptor that controls schema relation access"""
-    _role = 'subject'
-
-class ObjectRelation(Relation):
-    """descriptor that controls schema relation access"""
-    _role = 'object'
-
 from logging import getLogger
 from cubicweb import set_log_methods
 set_log_methods(Entity, getLogger('cubicweb.entity'))
--- a/goa/appobjects/components.py	Thu May 20 20:47:13 2010 +0200
+++ b/goa/appobjects/components.py	Thu May 20 20:47:55 2010 +0200
@@ -98,7 +98,7 @@
 def sendmail(self, recipient, subject, body):
     sender = '%s <%s>' % (
         self.req.user.dc_title() or self.config['sender-name'],
-        self.req.user.get_email() or self.config['sender-addr'])
+        self.req.user.cw_adapt_to('IEmailable').get_email() or self.config['sender-addr'])
     mail.send_mail(sender=sender, to=recipient,
                    subject=subject, body=body)
 
--- a/hooks/syncschema.py	Thu May 20 20:47:13 2010 +0200
+++ b/hooks/syncschema.py	Thu May 20 20:47:55 2010 +0200
@@ -1175,7 +1175,7 @@
             still_fti = list(schema[etype].indexable_attributes())
             for entity in rset.entities():
                 source.fti_unindex_entity(session, entity.eid)
-                for container in entity.fti_containers():
+                for container in entity.cw_adapt_to('IFTIndexable').fti_containers():
                     if still_fti or container is not entity:
                         source.fti_unindex_entity(session, entity.eid)
                         source.fti_index_entity(session, container)
--- a/hooks/workflow.py	Thu May 20 20:47:13 2010 +0200
+++ b/hooks/workflow.py	Thu May 20 20:47:55 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Core hooks: workflow related hooks
+"""Core hooks: workflow related hooks"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from datetime import datetime
@@ -25,8 +24,7 @@
 from yams.schema import role_name
 
 from cubicweb import RepositoryError, ValidationError
-from cubicweb.interfaces import IWorkflowable
-from cubicweb.selectors import implements
+from cubicweb.selectors import implements, adaptable
 from cubicweb.server import hook
 
 
@@ -51,11 +49,12 @@
     def precommit_event(self):
         session = self.session
         entity = self.entity
+        iworkflowable = entity.cw_adapt_to('IWorkflowable')
         # if there is an initial state and the entity's state is not set,
         # use the initial state as a default state
         if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \
-               and entity.current_workflow:
-            state = entity.current_workflow.initial
+               and iworkflowable.current_workflow:
+            state = iworkflowable.current_workflow.initial
             if state:
                 session.add_relation(entity.eid, 'in_state', state.eid)
                 _FireAutotransitionOp(session, entity=entity)
@@ -65,10 +64,11 @@
 
     def precommit_event(self):
         entity = self.entity
-        autotrs = list(entity.possible_transitions('auto'))
+        iworkflowable = entity.cw_adapt_to('IWorkflowable')
+        autotrs = list(iworkflowable.possible_transitions('auto'))
         if autotrs:
             assert len(autotrs) == 1
-            entity.fire_transition(autotrs[0])
+            iworkflowable.fire_transition(autotrs[0])
 
 
 class _WorkflowChangedOp(hook.Operation):
@@ -82,29 +82,30 @@
         if self.eid in pendingeids:
             return
         entity = session.entity_from_eid(self.eid)
+        iworkflowable = entity.cw_adapt_to('IWorkflowable')
         # check custom workflow has not been rechanged to another one in the same
         # transaction
-        mainwf = entity.main_workflow
+        mainwf = iworkflowable.main_workflow
         if mainwf.eid == self.wfeid:
             deststate = mainwf.initial
             if not deststate:
                 qname = role_name('custom_workflow', 'subject')
                 msg = session._('workflow has no initial state')
                 raise ValidationError(entity.eid, {qname: msg})
-            if mainwf.state_by_eid(entity.current_state.eid):
+            if mainwf.state_by_eid(iworkflowable.current_state.eid):
                 # nothing to do
                 return
             # if there are no history, simply go to new workflow's initial state
-            if not entity.workflow_history:
-                if entity.current_state.eid != deststate.eid:
+            if not iworkflowable.workflow_history:
+                if iworkflowable.current_state.eid != deststate.eid:
                     _change_state(session, entity.eid,
-                                  entity.current_state.eid, deststate.eid)
+                                  iworkflowable.current_state.eid, deststate.eid)
                     _FireAutotransitionOp(session, entity=entity)
                 return
             msg = session._('workflow changed to "%s"')
             msg %= session._(mainwf.name)
             session.transaction_data[(entity.eid, 'customwf')] = self.wfeid
-            entity.change_state(deststate, msg, u'text/plain')
+            iworkflowable.change_state(deststate, msg, u'text/plain')
 
 
 class _CheckTrExitPoint(hook.Operation):
@@ -125,9 +126,10 @@
     def precommit_event(self):
         session = self.session
         forentity = self.forentity
+        iworkflowable = forentity.cw_adapt_to('IWorkflowable')
         trinfo = self.trinfo
         # we're in a subworkflow, check if we've reached an exit point
-        wftr = forentity.subworkflow_input_transition()
+        wftr = iworkflowable.subworkflow_input_transition()
         if wftr is None:
             # inconsistency detected
             qname = role_name('to_state', 'subject')
@@ -137,9 +139,9 @@
         if tostate is not None:
             # reached an exit point
             msg = session._('exiting from subworkflow %s')
-            msg %= session._(forentity.current_workflow.name)
+            msg %= session._(iworkflowable.current_workflow.name)
             session.transaction_data[(forentity.eid, 'subwfentrytr')] = True
-            forentity.change_state(tostate, msg, u'text/plain', tr=wftr)
+            iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr)
 
 
 # hooks ########################################################################
@@ -151,7 +153,7 @@
 
 class SetInitialStateHook(WorkflowHook):
     __regid__ = 'wfsetinitial'
-    __select__ = WorkflowHook.__select__ & implements(IWorkflowable)
+    __select__ = WorkflowHook.__select__ & adaptable('IWorkflowable')
     events = ('after_add_entity',)
 
     def __call__(self):
@@ -189,18 +191,19 @@
             msg = session._('mandatory relation')
             raise ValidationError(entity.eid, {qname: msg})
         forentity = session.entity_from_eid(foreid)
+        iworkflowable = forentity.cw_adapt_to('IWorkflowable')
         # then check it has a workflow set, unless we're in the process of changing
         # entity's workflow
         if session.transaction_data.get((forentity.eid, 'customwf')):
             wfeid = session.transaction_data[(forentity.eid, 'customwf')]
             wf = session.entity_from_eid(wfeid)
         else:
-            wf = forentity.current_workflow
+            wf = iworkflowable.current_workflow
         if wf is None:
             msg = session._('related entity has no workflow set')
             raise ValidationError(entity.eid, {None: msg})
         # then check it has a state set
-        fromstate = forentity.current_state
+        fromstate = iworkflowable.current_state
         if fromstate is None:
             msg = session._('related entity has no state')
             raise ValidationError(entity.eid, {None: msg})
@@ -278,8 +281,9 @@
         _change_state(self._cw, trinfo['wf_info_for'],
                       trinfo['from_state'], trinfo['to_state'])
         forentity = self._cw.entity_from_eid(trinfo['wf_info_for'])
-        assert forentity.current_state.eid == trinfo['to_state']
-        if forentity.main_workflow.eid != forentity.current_workflow.eid:
+        iworkflowable = forentity.cw_adapt_to('IWorkflowable')
+        assert iworkflowable.current_state.eid == trinfo['to_state']
+        if iworkflowable.main_workflow.eid != iworkflowable.current_workflow.eid:
             _SubWorkflowExitOp(self._cw, forentity=forentity, trinfo=trinfo)
 
 
@@ -297,7 +301,8 @@
             # state changed through TrInfo insertion, so we already know it's ok
             return
         entity = session.entity_from_eid(self.eidfrom)
-        mainwf = entity.main_workflow
+        iworkflowable = entity.cw_adapt_to('IWorkflowable')
+        mainwf = iworkflowable.main_workflow
         if mainwf is None:
             msg = session._('entity has no workflow set')
             raise ValidationError(entity.eid, {None: msg})
@@ -309,7 +314,7 @@
             msg = session._("state doesn't belong to entity's workflow. You may "
                             "want to set a custom workflow for this entity first.")
             raise ValidationError(self.eidfrom, {qname: msg})
-        if entity.current_workflow and wf.eid != entity.current_workflow.eid:
+        if iworkflowable.current_workflow and wf.eid != iworkflowable.current_workflow.eid:
             qname = role_name('in_state', 'subject')
             msg = session._("state doesn't belong to entity's current workflow")
             raise ValidationError(self.eidfrom, {qname: msg})
@@ -359,7 +364,7 @@
 
     def __call__(self):
         entity = self._cw.entity_from_eid(self.eidfrom)
-        typewf = entity.cwetype_workflow()
+        typewf = entity.cw_adapt_to('IWorkflowable').cwetype_workflow()
         if typewf is not None:
             _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=typewf.eid)
 
--- a/interfaces.py	Thu May 20 20:47:13 2010 +0200
+++ b/interfaces.py	Thu May 20 20:47:55 2010 +0200
@@ -15,68 +15,24 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-Standard interfaces.
+"""Standard interfaces. Deprecated in favor of adapters.
 
 .. note::
 
-  The `implements` selector matches not only entity classes but also
-  their interfaces. Writing __select__ = implements('IGeocodable') is
-  a perfectly fine thing to do.
+  The `implements` selector used to match not only entity classes but also their
+  interfaces. This will disappear in a future version. You should define an
+  adapter for that interface and use `adaptable('MyIFace')` selector on appobjects
+  that require that interface.
+
 """
 __docformat__ = "restructuredtext en"
 
 from logilab.common.interface import Interface
 
-class IEmailable(Interface):
-    """interface for emailable entities"""
 
-    def get_email(self):
-        """return email address"""
-
-    @classmethod
-    def allowed_massmail_keys(cls):
-        """returns a set of allowed email substitution keys
-
-        The default is to return the entity's attribute list but an
-        entity class might override this method to allow extra keys.
-        For instance, the Person class might want to return a `companyname`
-        key.
-        """
-
-    def as_email_context(self):
-        """returns the dictionary as used by the sendmail controller to
-        build email bodies.
-
-        NOTE: the dictionary keys should match the list returned by the
-        `allowed_massmail_keys` method.
-        """
-
-
-class IWorkflowable(Interface):
-    """interface for entities dealing with a specific workflow"""
-    # XXX to be completed, see cw.entities.wfobjs.WorkflowableMixIn
-
-    @property
-    def state(self):
-        """return current state name"""
-
-    def change_state(self, stateeid, trcomment=None, trcommentformat=None):
-        """change the entity's state to the state of the given name in entity's
-        workflow
-        """
-
-    def latest_trinfo(self):
-        """return the latest transition information for this entity
-        """
-
-
+# XXX deprecates in favor of IProgressAdapter
 class IProgress(Interface):
-    """something that has a cost, a state and a progression
-
-    Take a look at cubicweb.mixins.ProgressMixIn for some
-    default implementations
-    """
+    """something that has a cost, a state and a progression"""
 
     @property
     def cost(self):
@@ -112,7 +68,7 @@
     def progress(self):
         """returns the % progress of the task item"""
 
-
+# XXX deprecates in favor of IMileStoneAdapter
 class IMileStone(IProgress):
     """represents an ITask's item"""
 
@@ -135,7 +91,132 @@
     def contractors(self):
         """returns the list of persons supposed to work on this task"""
 
+# XXX deprecates in favor of IEmbedableAdapter
+class IEmbedable(Interface):
+    """interface for embedable entities"""
 
+    def embeded_url(self):
+        """embed action interface"""
+
+# XXX deprecates in favor of ICalendarAdapter
+class ICalendarViews(Interface):
+    """calendar views interface"""
+    def matching_dates(self, begin, end):
+        """
+        :param begin: day considered as begin of the range (`DateTime`)
+        :param end: day considered as end of the range (`DateTime`)
+
+        :return:
+          a list of dates (`DateTime`) in the range [`begin`, `end`] on which
+          this entity apply
+        """
+
+# XXX deprecates in favor of ICalendarableAdapter
+class ICalendarable(Interface):
+    """interface for items that do have a begin date 'start' and an end date 'stop'
+    """
+
+    @property
+    def start(self):
+        """return start date"""
+
+    @property
+    def stop(self):
+        """return stop state"""
+
+# XXX deprecates in favor of ICalendarableAdapter
+class ITimetableViews(Interface):
+    """timetable views interface"""
+    def timetable_date(self):
+        """XXX explain
+
+        :return: date (`DateTime`)
+        """
+
+# XXX deprecates in favor of IGeocodableAdapter
+class IGeocodable(Interface):
+    """interface required by geocoding views such as gmap-view"""
+
+    @property
+    def latitude(self):
+        """returns the latitude of the entity"""
+
+    @property
+    def longitude(self):
+        """returns the longitude of the entity"""
+
+    def marker_icon(self):
+        """returns the icon that should be used as the marker"""
+
+# XXX deprecates in favor of ISIOCItemAdapter
+class ISiocItem(Interface):
+    """interface for entities which may be represented as an ISIOC item"""
+
+    def isioc_content(self):
+        """return item's content"""
+
+    def isioc_container(self):
+        """return container entity"""
+
+    def isioc_type(self):
+        """return container type (post, BlogPost, MailMessage)"""
+
+    def isioc_replies(self):
+        """return replies items"""
+
+    def isioc_topics(self):
+        """return topics items"""
+
+# XXX deprecates in favor of ISIOCContainerAdapter
+class ISiocContainer(Interface):
+    """interface for entities which may be represented as an ISIOC container"""
+
+    def isioc_type(self):
+        """return container type (forum, Weblog, MailingList)"""
+
+    def isioc_items(self):
+        """return contained items"""
+
+# XXX deprecates in favor of IEmailableAdapter
+class IFeed(Interface):
+    """interface for entities with rss flux"""
+
+    def rss_feed_url(self):
+        """"""
+
+# XXX deprecates in favor of IDownloadableAdapter
+class IDownloadable(Interface):
+    """interface for downloadable entities"""
+
+    def download_url(self): # XXX not really part of this interface
+        """return an url to download entity's content"""
+    def download_content_type(self):
+        """return MIME type of the downloadable content"""
+    def download_encoding(self):
+        """return encoding of the downloadable content"""
+    def download_file_name(self):
+        """return file name of the downloadable content"""
+    def download_data(self):
+        """return actual data of the downloadable content"""
+
+# XXX deprecates in favor of IPrevNextAdapter
+class IPrevNext(Interface):
+    """interface for entities which can be linked to a previous and/or next
+    entity
+    """
+
+    def next_entity(self):
+        """return the 'next' entity"""
+    def previous_entity(self):
+        """return the 'previous' entity"""
+
+# XXX deprecates in favor of IBreadCrumbsAdapter
+class IBreadCrumbs(Interface):
+
+    def breadcrumbs(self, view, recurs=False):
+        pass
+
+# XXX deprecates in favor of ITreeAdapter
 class ITree(Interface):
 
     def parent(self):
@@ -159,141 +240,3 @@
     def root(self):
         """returns the root object"""
 
-
-## web specific interfaces ####################################################
-
-
-class IPrevNext(Interface):
-    """interface for entities which can be linked to a previous and/or next
-    entity
-    """
-
-    def next_entity(self):
-        """return the 'next' entity"""
-    def previous_entity(self):
-        """return the 'previous' entity"""
-
-
-class IBreadCrumbs(Interface):
-    """interface for entities which can be "located" on some path"""
-
-    # XXX fix recurs !
-    def breadcrumbs(self, view, recurs=False):
-        """return a list containing some:
-
-        * tuple (url, label)
-        * entity
-        * simple label string
-
-        defining path from a root to the current view
-
-        the main view is given as argument so breadcrumbs may vary according
-        to displayed view (may be None). When recursing on a parent entity,
-        the `recurs` argument should be set to True.
-        """
-
-
-class IDownloadable(Interface):
-    """interface for downloadable entities"""
-
-    def download_url(self): # XXX not really part of this interface
-        """return an url to download entity's content"""
-    def download_content_type(self):
-        """return MIME type of the downloadable content"""
-    def download_encoding(self):
-        """return encoding of the downloadable content"""
-    def download_file_name(self):
-        """return file name of the downloadable content"""
-    def download_data(self):
-        """return actual data of the downloadable content"""
-
-
-class IEmbedable(Interface):
-    """interface for embedable entities"""
-
-    def embeded_url(self):
-        """embed action interface"""
-
-class ICalendarable(Interface):
-    """interface for items that do have a begin date 'start' and an end date 'stop'
-    """
-
-    @property
-    def start(self):
-        """return start date"""
-
-    @property
-    def stop(self):
-        """return stop state"""
-
-class ICalendarViews(Interface):
-    """calendar views interface"""
-    def matching_dates(self, begin, end):
-        """
-        :param begin: day considered as begin of the range (`DateTime`)
-        :param end: day considered as end of the range (`DateTime`)
-
-        :return:
-          a list of dates (`DateTime`) in the range [`begin`, `end`] on which
-          this entity apply
-        """
-
-class ITimetableViews(Interface):
-    """timetable views interface"""
-    def timetable_date(self):
-        """XXX explain
-
-        :return: date (`DateTime`)
-        """
-
-class IGeocodable(Interface):
-    """interface required by geocoding views such as gmap-view"""
-
-    @property
-    def latitude(self):
-        """returns the latitude of the entity"""
-
-    @property
-    def longitude(self):
-        """returns the longitude of the entity"""
-
-    def marker_icon(self):
-        """returns the icon that should be used as the marker
-        (returns None for default)
-        """
-
-class IFeed(Interface):
-    """interface for entities with rss flux"""
-
-    def rss_feed_url(self):
-        """return an url which layout sub-entities item
-        """
-
-class ISiocItem(Interface):
-    """interface for entities (which are item
-    in sioc specification) with sioc views"""
-
-    def isioc_content(self):
-        """return content entity"""
-
-    def isioc_container(self):
-        """return container entity"""
-
-    def isioc_type(self):
-        """return container type (post, BlogPost, MailMessage)"""
-
-    def isioc_replies(self):
-        """return replies items"""
-
-    def isioc_topics(self):
-        """return topics items"""
-
-class ISiocContainer(Interface):
-    """interface for entities (which are container
-    in sioc specification) with sioc views"""
-
-    def isioc_type(self):
-        """return container type (forum, Weblog, MailingList)"""
-
-    def isioc_items(self):
-        """return contained items"""
--- a/mail.py	Thu May 20 20:47:13 2010 +0200
+++ b/mail.py	Thu May 20 20:47:55 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Common utilies to format / semd emails.
+"""Common utilies to format / semd emails."""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from base64 import b64encode, b64decode
@@ -182,7 +181,7 @@
             # previous email
             if not self.msgid_timestamp:
                 refs = [self.construct_message_id(eid)
-                        for eid in entity.notification_references(self)]
+                        for eid in entity.cw_adapt_to('INotifiable').notification_references(self)]
             else:
                 refs = ()
             msgid = self.construct_message_id(entity.eid)
@@ -196,7 +195,7 @@
             if isinstance(something, Entity):
                 # hi-jack self._cw to get a session for the returned user
                 self._cw = self._cw.hijack_user(something)
-                emailaddr = something.get_email()
+                emailaddr = something.cw_adapt_to('IEmailable').get_email()
             else:
                 emailaddr, lang = something
                 self._cw.set_language(lang)
@@ -244,7 +243,8 @@
     # email generation helpers #################################################
 
     def construct_message_id(self, eid):
-        return construct_message_id(self._cw.vreg.config.appid, eid, self.msgid_timestamp)
+        return construct_message_id(self._cw.vreg.config.appid, eid,
+                                    self.msgid_timestamp)
 
     def format_field(self, attr, value):
         return ':%(attr)s: %(value)s' % {'attr': attr, 'value': value}
--- a/mixins.py	Thu May 20 20:47:13 2010 +0200
+++ b/mixins.py	Thu May 20 20:47:55 2010 +0200
@@ -21,9 +21,10 @@
 from itertools import chain
 
 from logilab.common.decorators import cached
+from logilab.common.deprecation import deprecated, class_deprecated
 
 from cubicweb.selectors import implements
-from cubicweb.interfaces import IEmailable, ITree
+from cubicweb.interfaces import ITree
 
 
 class TreeMixIn(object):
@@ -33,6 +34,9 @@
     tree_attribute, parent_target and children_target class attribute to
     benefit from this default implementation
     """
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.9] TreeMixIn is deprecated, use/override ITreeAdapter instead'
+
     tree_attribute = None
     # XXX misnamed
     parent_target = 'subject'
@@ -117,16 +121,6 @@
             return chain([self], _uptoroot(self))
         return _uptoroot(self)
 
-    def notification_references(self, view):
-        """used to control References field of email send on notification
-        for this entity. `view` is the notification view.
-
-        Should return a list of eids which can be used to generate message ids
-        of previously sent email
-        """
-        return self.path()[:-1]
-
-
     ## ITree interface ########################################################
     def parent(self):
         """return the parent entity if any, else None (e.g. if we are on the
@@ -171,8 +165,7 @@
     NOTE: The default implementation is based on the
     primary_email / use_email scheme
     """
-    __implements__ = (IEmailable,)
-
+    @deprecated("[3.9] use entity.cw_adapt_to('IEmailable').get_email()")
     def get_email(self):
         if getattr(self, 'primary_email', None):
             return self.primary_email[0].address
@@ -180,28 +173,6 @@
             return self.use_email[0].address
         return None
 
-    @classmethod
-    def allowed_massmail_keys(cls):
-        """returns a set of allowed email substitution keys
-
-        The default is to return the entity's attribute list but an
-        entity class might override this method to allow extra keys.
-        For instance, the Person class might want to return a `companyname`
-        key.
-        """
-        return set(rschema.type
-                   for rschema, attrtype in cls.e_schema.attribute_definitions()
-                   if attrtype.type not in ('Password', 'Bytes'))
-
-    def as_email_context(self):
-        """returns the dictionary as used by the sendmail controller to
-        build email bodies.
-
-        NOTE: the dictionary keys should match the list returned by the
-        `allowed_massmail_keys` method.
-        """
-        return dict( (attr, getattr(self, attr)) for attr in self.allowed_massmail_keys() )
-
 
 """pluggable mixins system: plug classes registered in MI_REL_TRIGGERS on entity
 classes which have the relation described by the dict's key.
@@ -216,26 +187,14 @@
 
 
 
-def _done_init(done, view, row, col):
-    """handle an infinite recursion safety belt"""
-    if done is None:
-        done = set()
-    entity = view.cw_rset.get_entity(row, col)
-    if entity.eid in done:
-        msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % {
-            'rel': entity.tree_attribute,
-            'eid': entity.eid
-            }
-        return None, msg
-    done.add(entity.eid)
-    return done, entity
-
-
 class TreeViewMixIn(object):
     """a recursive tree view"""
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.9] TreeViewMixIn is deprecated, use/override BaseTreeView instead'
+
     __regid__ = 'tree'
+    __select__ = implements(ITree)
     item_vid = 'treeitem'
-    __select__ = implements(ITree)
 
     def call(self, done=None, **kwargs):
         if done is None:
@@ -262,6 +221,8 @@
 
 class TreePathMixIn(object):
     """a recursive path view"""
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.9] TreePathMixIn is deprecated, use/override TreePathView instead'
     __regid__ = 'path'
     item_vid = 'oneline'
     separator = u'&#160;&gt;&#160;'
@@ -286,6 +247,8 @@
 
 class ProgressMixIn(object):
     """provide a default implementations for IProgress interface methods"""
+    __metaclass__ = class_deprecated
+    __deprecation_warning__ = '[3.9] ProgressMixIn is deprecated, use/override IProgressAdapter instead'
 
     @property
     def cost(self):
--- a/req.py	Thu May 20 20:47:13 2010 +0200
+++ b/req.py	Thu May 20 20:47:55 2010 +0200
@@ -279,7 +279,7 @@
         user = self.user
         userinfo['login'] = user.login
         userinfo['name'] = user.name()
-        userinfo['email'] = user.get_email()
+        userinfo['email'] = user.cw_adapt_to('IEmailable').get_email()
         return userinfo
 
     def is_internal_session(self):
--- a/selectors.py	Thu May 20 20:47:13 2010 +0200
+++ b/selectors.py	Thu May 20 20:47:55 2010 +0200
@@ -301,6 +301,7 @@
             if iface is basecls:
                 return index + 3
         return 0
+    # XXX iface in implements deprecated in 3.9
     if implements_iface(cls_or_inst, iface):
         # implenting an interface takes precedence other special Any interface
         return 2
@@ -527,18 +528,33 @@
 
     * `registry`, a registry name
 
-    * `regid`, an object identifier in this registry
+    * `regids`, object identifiers in this registry, one of them should be
+      selectable.
     """
-    def __init__(self, registry, regid):
+    def __init__(self, registry, *regids):
         self.registry = registry
-        self.regid = regid
+        self.regids = regids
 
     def __call__(self, cls, req, **kwargs):
-        try:
-            req.vreg[self.registry].select(self.regid, req, **kwargs)
-            return 1
-        except NoSelectableObject:
-            return 0
+        for regid in self.regids:
+            try:
+                req.vreg[self.registry].select(regid, req, **kwargs)
+                return 1
+            except NoSelectableObject:
+                return 0
+
+
+class adaptable(appobject_selectable):
+    """Return 1 if another appobject is selectable using the same input context.
+
+    Initializer arguments:
+
+    * `regids`, adapter identifiers (e.g. interface names) to which the context
+      (usually entities) should be adaptable. One of them should be selectable
+      when multiple identifiers are given.
+    """
+    def __init__(self, *regids):
+        super(adaptable, self).__init__('adapters', *regids)
 
 
 # rset selectors ##############################################################
@@ -731,7 +747,12 @@
 
     .. note:: when interface is an entity class, the score will reflect class
               proximity so the most specific object will be selected.
+
+    .. note:: with cubicweb >= 3.9, you should use adapters instead of
+              interface, so no interface should be given to this selector. Use
+              :class:`adaptable` instead.
     """
+
     def score_class(self, eclass, req):
         return self.score_interfaces(req, eclass, eclass)
 
@@ -758,6 +779,26 @@
         self.score_entity = intscore
 
 
+class has_mimetype(EntitySelector):
+    """Return 1 if the entity adapt to IDownloadable and has the given MIME type.
+
+    You can give 'image/' to match any image for instance, or 'image/png' to match
+    only PNG images.
+    """
+    def __init__(self, mimetype, once_is_enough=False):
+        super(has_mimetype, self).__init__(once_is_enough)
+        self.mimetype = mimetype
+
+    def score_entity(self, entity):
+        idownloadable =  entity.cw_adapt_to('IDownloadable')
+        if idownloadable is None:
+            return 0
+        mt = idownloadable.download_content_type()
+        if not (mt and mt.startswith(self.mimetype)):
+            return 0
+        return 1
+
+
 class relation_possible(EntitySelector):
     """Return 1 for entity that supports the relation, provided that the
     request's user may do some `action` on it (see below).
@@ -1283,17 +1324,18 @@
 class is_in_state(score_entity):
     """return 1 if entity is in one of the states given as argument list
 
-    you should use this instead of your own score_entity x: x.state == 'bla'
-    selector to avoid some gotchas:
+    you should use this instead of your own :class:`score_entity` selector to
+    avoid some gotchas:
 
     * possible views gives a fake entity with no state
-    * you must use the latest tr info, not entity.state for repository side
+    * you must use the latest tr info, not entity.in_state for repository side
       checking of the current state
     """
     def __init__(self, *states):
         def score(entity, states=set(states)):
+            trinfo = entity.cw_adapt_to('IWorkflowable').latest_trinfo()
             try:
-                return entity.latest_trinfo().new_state.name in states
+                return trinfo.new_state.name in states
             except AttributeError:
                 return None
         super(is_in_state, self).__init__(score)
--- a/server/migractions.py	Thu May 20 20:47:13 2010 +0200
+++ b/server/migractions.py	Thu May 20 20:47:55 2010 +0200
@@ -1157,10 +1157,10 @@
         if commit:
             self.commit()
 
-    @deprecated('[3.5] use entity.fire_transition("transition") or entity.change_state("state")',
-                stacklevel=3)
+    @deprecated('[3.5] use iworkflowable.fire_transition("transition") or '
+                'iworkflowable.change_state("state")', stacklevel=3)
     def cmd_set_state(self, eid, statename, commit=False):
-        self._cw.entity_from_eid(eid).change_state(statename)
+        self._cw.entity_from_eid(eid).cw_adapt_to('IWorkflowable').change_state(statename)
         if commit:
             self.commit()
 
--- a/server/repository.py	Thu May 20 20:47:13 2010 +0200
+++ b/server/repository.py	Thu May 20 20:47:55 2010 +0200
@@ -407,7 +407,7 @@
             raise AuthenticationError('authentication failed with all sources')
         cwuser = self._build_user(session, eid)
         if self.config.consider_user_state and \
-               not cwuser.state in cwuser.AUTHENTICABLE_STATES:
+               not cwuser.cw_adapt_to('IWorkflowable').state in cwuser.AUTHENTICABLE_STATES:
             raise AuthenticationError('user is not in authenticable state')
         return cwuser
 
--- a/server/sources/native.py	Thu May 20 20:47:13 2010 +0200
+++ b/server/sources/native.py	Thu May 20 20:47:55 2010 +0200
@@ -1165,7 +1165,8 @@
         try:
             # use cursor_index_object, not cursor_reindex_object since
             # unindexing done in the FTIndexEntityOp
-            self.dbhelper.cursor_index_object(entity.eid, entity,
+            self.dbhelper.cursor_index_object(entity.eid,
+                                              entity.cw_adapt_to('IFTIndexable'),
                                               session.pool['system'])
         except Exception: # let KeyboardInterrupt / SystemExit propagate
             self.exception('error while reindexing %s', entity)
@@ -1190,7 +1191,8 @@
                 # processed
                 return
             done.add(eid)
-            for container in session.entity_from_eid(eid).fti_containers():
+            iftindexable = session.entity_from_eid(eid).cw_adapt_to('IFTIndexable')
+            for container in iftindexable.fti_containers():
                 source.fti_unindex_entity(session, container.eid)
                 source.fti_index_entity(session, container)
 
--- a/server/test/unittest_ldapuser.py	Thu May 20 20:47:13 2010 +0200
+++ b/server/test/unittest_ldapuser.py	Thu May 20 20:47:55 2010 +0200
@@ -178,12 +178,13 @@
         cnx = self.login(SYT, password='dummypassword')
         cu = cnx.cursor()
         adim = cu.execute('CWUser X WHERE X login %(login)s', {'login': ADIM}).get_entity(0, 0)
-        adim.fire_transition('deactivate')
+        iworkflowable = adim.cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('deactivate')
         try:
             cnx.commit()
             adim.clear_all_caches()
             self.assertEquals(adim.in_state[0].name, 'deactivated')
-            trinfo = adim.latest_trinfo()
+            trinfo = iworkflowable.latest_trinfo()
             self.assertEquals(trinfo.owned_by[0].login, SYT)
             # select from_state to skip the user's creation TrInfo
             rset = self.sexecute('Any U ORDERBY D DESC WHERE WF wf_info_for X,'
@@ -195,7 +196,7 @@
             # restore db state
             self.restore_connection()
             adim = self.sexecute('CWUser X WHERE X login %(login)s', {'login': ADIM}).get_entity(0, 0)
-            adim.fire_transition('activate')
+            adim.cw_adapt_to('IWorkflowable').fire_transition('activate')
             self.sexecute('DELETE X in_group G WHERE X login %(syt)s, G name "managers"', {'syt': SYT})
 
     def test_same_column_names(self):
--- a/server/test/unittest_msplanner.py	Thu May 20 20:47:13 2010 +0200
+++ b/server/test/unittest_msplanner.py	Thu May 20 20:47:55 2010 +0200
@@ -1722,8 +1722,9 @@
                     ])
 
     def test_nonregr2(self):
-        self.session.user.fire_transition('deactivate')
-        treid = self.session.user.latest_trinfo().eid
+        iworkflowable = self.session.user.cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('deactivate')
+        treid = iworkflowable.latest_trinfo().eid
         self._test('Any X ORDERBY D DESC WHERE E eid %(x)s, E wf_info_for X, X modification_date D',
                    [('FetchStep', [('Any X,D WHERE X modification_date D, X is Note',
                                     [{'X': 'Note', 'D': 'Datetime'}])],
--- a/server/test/unittest_multisources.py	Thu May 20 20:47:13 2010 +0200
+++ b/server/test/unittest_multisources.py	Thu May 20 20:47:55 2010 +0200
@@ -307,8 +307,9 @@
                      {'x': affaire.eid, 'u': ueid})
 
     def test_nonregr2(self):
-        self.session.user.fire_transition('deactivate')
-        treid = self.session.user.latest_trinfo().eid
+        iworkflowable = self.session.user.cw_adapt_to('IWorkflowable')
+        iworkflowable.fire_transition('deactivate')
+        treid = iworkflowable.latest_trinfo().eid
         rset = self.sexecute('Any X ORDERBY D DESC WHERE E eid %(x)s, E wf_info_for X, X modification_date D',
                             {'x': treid})
         self.assertEquals(len(rset), 1)
--- a/server/test/unittest_repository.py	Thu May 20 20:47:13 2010 +0200
+++ b/server/test/unittest_repository.py	Thu May 20 20:47:55 2010 +0200
@@ -205,7 +205,7 @@
         session = repo._get_session(cnxid)
         session.set_pool()
         user = session.user
-        user.fire_transition('deactivate')
+        user.cw_adapt_to('IWorkflowable').fire_transition('deactivate')
         rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': user.eid})
         self.assertEquals(len(rset), 1)
         repo.rollback(cnxid)
--- a/server/test/unittest_security.py	Thu May 20 20:47:13 2010 +0200
+++ b/server/test/unittest_security.py	Thu May 20 20:47:55 2010 +0200
@@ -384,7 +384,7 @@
         # Note.para attribute editable by managers or if the note is in "todo" state
         note = self.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0)
         self.commit()
-        note.fire_transition('markasdone')
+        note.cw_adapt_to('IWorkflowable').fire_transition('markasdone')
         self.execute('SET X para "truc" WHERE X eid %(x)s', {'x': note.eid})
         self.commit()
         cnx = self.login('iaminusersgrouponly')
@@ -393,13 +393,13 @@
         self.assertRaises(Unauthorized, cnx.commit)
         note2 = cu.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0)
         cnx.commit()
-        note2.fire_transition('markasdone')
+        note2.cw_adapt_to('IWorkflowable').fire_transition('markasdone')
         cnx.commit()
         self.assertEquals(len(cu.execute('Any X WHERE X in_state S, S name "todo", X eid %(x)s', {'x': note2.eid})),
                           0)
         cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid})
         self.assertRaises(Unauthorized, cnx.commit)
-        note2.fire_transition('redoit')
+        note2.cw_adapt_to('IWorkflowable').fire_transition('redoit')
         cnx.commit()
         cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid})
         cnx.commit()
@@ -435,7 +435,7 @@
         cnx.commit()
         self.restore_connection()
         affaire = self.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
-        affaire.fire_transition('abort')
+        affaire.cw_adapt_to('IWorkflowable').fire_transition('abort')
         self.commit()
         self.assertEquals(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01"')),
                           1)
@@ -537,14 +537,15 @@
             cu = cnx.cursor()
             self.schema['Affaire'].set_action_permissions('read', ('users',))
             aff = cu.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
-            aff.fire_transition('abort')
+            aff.cw_adapt_to('IWorkflowable').fire_transition('abort')
             cnx.commit()
             # though changing a user state (even logged user) is reserved to managers
             user = cnx.user(self.session)
             # XXX wether it should raise Unauthorized or ValidationError is not clear
             # the best would probably ValidationError if the transition doesn't exist
             # from the current state but Unauthorized if it exists but user can't pass it
-            self.assertRaises(ValidationError, user.fire_transition, 'deactivate')
+            self.assertRaises(ValidationError,
+                              user.cw_adapt_to('IWorkflowable').fire_transition, 'deactivate')
         finally:
             # restore orig perms
             for action, perms in affaire_perms.iteritems():
@@ -552,15 +553,16 @@
 
     def test_trinfo_security(self):
         aff = self.execute('INSERT Affaire X: X ref "ARCT01"').get_entity(0, 0)
+        iworkflowable = aff.cw_adapt_to('IWorkflowable')
         self.commit()
-        aff.fire_transition('abort')
+        iworkflowable.fire_transition('abort')
         self.commit()
         # can change tr info comment
         self.execute('SET TI comment %(c)s WHERE TI wf_info_for X, X ref "ARCT01"',
                      {'c': u'bouh!'})
         self.commit()
         aff.clear_related_cache('wf_info_for', 'object')
-        trinfo = aff.latest_trinfo()
+        trinfo = iworkflowable.latest_trinfo()
         self.assertEquals(trinfo.comment, 'bouh!')
         # but not from_state/to_state
         aff.clear_related_cache('wf_info_for', role='object')
--- a/server/test/unittest_undo.py	Thu May 20 20:47:13 2010 +0200
+++ b/server/test/unittest_undo.py	Thu May 20 20:47:55 2010 +0200
@@ -160,8 +160,8 @@
         self.failUnless(self.execute('Any X WHERE X eid %(x)s', {'x': toto.eid}))
         self.failUnless(self.execute('Any X WHERE X eid %(x)s', {'x': e.eid}))
         self.failUnless(self.execute('Any X WHERE X has_text "toto@logilab"'))
-        self.assertEquals(toto.state, 'activated')
-        self.assertEquals(toto.get_email(), 'toto@logilab.org')
+        self.assertEquals(toto.cw_adapt_to('IWorkflowable').state, 'activated')
+        self.assertEquals(toto.cw_adapt_to('IEmailable').get_email(), 'toto@logilab.org')
         self.assertEquals([(p.pkey, p.value) for p in toto.reverse_for_user],
                           [('ui.default-text-format', 'text/rest')])
         self.assertEquals([g.name for g in toto.in_group],
--- a/sobjects/notification.py	Thu May 20 20:47:13 2010 +0200
+++ b/sobjects/notification.py	Thu May 20 20:47:55 2010 +0200
@@ -46,7 +46,8 @@
         mode = self._cw.vreg.config['default-recipients-mode']
         if mode == 'users':
             execute = self._cw.execute
-            dests = [(u.get_email(), u.property_value('ui.language'))
+            dests = [(u.cw_adapt_to('IEmailable').get_email(),
+                      u.property_value('ui.language'))
                      for u in execute(self.user_rql, build_descr=True).entities()]
         elif mode == 'default-dest-addrs':
             lang = self._cw.vreg.property_value('ui.language')
--- a/sobjects/test/unittest_notification.py	Thu May 20 20:47:13 2010 +0200
+++ b/sobjects/test/unittest_notification.py	Thu May 20 20:47:55 2010 +0200
@@ -85,7 +85,7 @@
     def test_status_change_view(self):
         req = self.request()
         u = self.create_user('toto', req=req)
-        u.fire_transition('deactivate', comment=u'yeah')
+        u.cw_adapt_to('IWorkflowable').fire_transition('deactivate', comment=u'yeah')
         self.failIf(MAILBOX)
         self.commit()
         self.assertEquals(len(MAILBOX), 1)
--- a/sobjects/test/unittest_supervising.py	Thu May 20 20:47:13 2010 +0200
+++ b/sobjects/test/unittest_supervising.py	Thu May 20 20:47:55 2010 +0200
@@ -84,7 +84,7 @@
         self.assertEquals(op.to_send[0][1], ['test@logilab.fr'])
         self.commit()
         # some other changes #######
-        user.fire_transition('deactivate')
+        user.cw_adapt_to('IWorkflowable').fire_transition('deactivate')
         sentops = [op for op in session.pending_operations
                    if isinstance(op, SupervisionMailOp)]
         self.assertEquals(len(sentops), 1)
--- a/sobjects/textparsers.py	Thu May 20 20:47:13 2010 +0200
+++ b/sobjects/textparsers.py	Thu May 20 20:47:55 2010 +0200
@@ -74,10 +74,14 @@
             if not hasattr(entity, 'in_state'):
                 self.error('bad change state instruction for eid %s', eid)
                 continue
-            tr = entity.current_workflow and entity.current_workflow.transition_by_name(trname)
+            iworkflowable = entity.cw_adapt_to('IWorkflowable')
+            if iworkflowable.current_workflow:
+                tr = iworkflowable.current_workflow.transition_by_name(trname)
+            else:
+                tr = None
             if tr and tr.may_be_fired(entity.eid):
                 try:
-                    trinfo = entity.fire_transition(tr)
+                    trinfo = iworkflowable.fire_transition(tr)
                     caller.fire_event('state-changed', {'trinfo': trinfo,
                                                         'entity': entity})
                 except:
--- a/test/unittest_entity.py	Thu May 20 20:47:13 2010 +0200
+++ b/test/unittest_entity.py	Thu May 20 20:47:55 2010 +0200
@@ -97,14 +97,14 @@
         user = self.execute('INSERT CWUser X: X login "toto", X upassword %(pwd)s, X in_group G WHERE G name "users"',
                            {'pwd': 'toto'}).get_entity(0, 0)
         self.commit()
-        user.fire_transition('deactivate')
+        user.cw_adapt_to('IWorkflowable').fire_transition('deactivate')
         self.commit()
         eid2 = self.execute('INSERT CWUser X: X login "tutu", X upassword %(pwd)s', {'pwd': 'toto'})[0][0]
         e = self.execute('Any X WHERE X eid %(x)s', {'x': eid2}).get_entity(0, 0)
         e.copy_relations(user.eid)
         self.commit()
         e.clear_related_cache('in_state', 'subject')
-        self.assertEquals(e.state, 'activated')
+        self.assertEquals(e.cw_adapt_to('IWorkflowable').state, 'activated')
 
     def test_related_cache_both(self):
         user = self.execute('Any X WHERE X eid %(x)s', {'x':self.user().eid}).get_entity(0, 0)
@@ -435,7 +435,7 @@
         e['data_format'] = 'text/html'
         e['data_encoding'] = 'ascii'
         e._cw.transaction_data = {} # XXX req should be a session
-        self.assertEquals(set(e.get_words()),
+        self.assertEquals(set(e.cw_adapt_to('IFTIndexable').get_words()),
                           set(['an', 'html', 'file', 'du', 'html', 'some', 'data']))
 
 
--- a/view.py	Thu May 20 20:47:13 2010 +0200
+++ b/view.py	Thu May 20 20:47:55 2010 +0200
@@ -520,3 +520,34 @@
     # XXX a generic '%s%s' % (self.__regid__, self.__registry__.capitalize()) would probably be nicer
     def div_id(self):
         return '%sComponent' % self.__regid__
+
+
+class Adapter(AppObject):
+    """base class for adapters"""
+    __registry__ = 'adapters'
+
+
+class EntityAdapter(Adapter):
+    """base class for entity adapters (eg adapt an entity to an interface)"""
+    def __init__(self, _cw, **kwargs):
+        try:
+            self.entity = kwargs.pop('entity')
+        except KeyError:
+            self.entity = kwargs['rset'].get_entity(kwargs.get('row') or 0,
+                                                    kwargs.get('col') or 0)
+        Adapter.__init__(self, _cw, **kwargs)
+
+
+def implements_adapter_compat(iface):
+    def _pre39_compat(func):
+        def decorated(self, *args, **kwargs):
+            entity = self.entity
+            if hasattr(entity, func.__name__):
+                warn('[3.9] %s method is deprecated, define it on a custom '
+                     '%s for %s instead' % (func.__name__, iface,
+                                            entity.__class__),
+                     DeprecationWarning)
+                return getattr(entity, func.__name__)(*args, **kwargs)
+            return func(self, *args, **kwargs)
+        return decorated
+    return _pre39_compat
--- a/web/__init__.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/__init__.py	Thu May 20 20:47:55 2010 +0200
@@ -17,9 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """CubicWeb web client core. You'll need a apache-modpython or twisted
 publisher to get a full CubicWeb web application
-
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
--- a/web/action.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/action.py	Thu May 20 20:47:55 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""abstract action classes for CubicWeb web client
+"""abstract action classes for CubicWeb web client"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
--- a/web/controller.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/controller.py	Thu May 20 20:47:55 2010 +0200
@@ -106,6 +106,16 @@
         view.set_http_cache_headers()
         self._cw.validate_cache()
 
+    def sendmail(self, recipient, subject, body):
+        senderemail = self._cw.user.cw_adapt_to('IEmailable').get_email()
+        msg = format_mail({'email' : senderemail,
+                           'name' : self._cw.user.dc_title(),},
+                          [recipient], body, subject)
+        if not self._cw.vreg.config.sendmails([(msg, [recipient])]):
+            msg = self._cw._('could not connect to the SMTP server')
+            url = self._cw.build_url(__message=msg)
+            raise Redirect(url)
+
     def reset(self):
         """reset form parameters and redirect to a view determinated by given
         parameters
--- a/web/test/unittest_views_basecontrollers.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/test/unittest_views_basecontrollers.py	Thu May 20 20:47:55 2010 +0200
@@ -128,7 +128,7 @@
         self.assertEquals(e.firstname, u'Th\xe9nault')
         self.assertEquals(e.surname, u'Sylvain')
         self.assertEquals([g.eid for g in e.in_group], groupeids)
-        self.assertEquals(e.state, 'activated')
+        self.assertEquals(e.cw_adapt_to('IWorkflowable').state, 'activated')
 
 
     def test_create_multiple_linked(self):
--- a/web/views/basecontrollers.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/basecontrollers.py	Thu May 20 20:47:55 2010 +0200
@@ -18,22 +18,17 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Set of base controllers, which are directly plugged into the application
 object to handle publication.
-
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
-from smtplib import SMTP
-
-from logilab.common.decorators import cached
 from logilab.common.date import strptime
 
 from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError,
                       AuthenticationError, typed_eid)
-from cubicweb.utils import CubicWebJsonEncoder
 from cubicweb.selectors import authenticated_user, match_form_params
-from cubicweb.mail import format_mail
-from cubicweb.web import Redirect, RemoteCallFailed, DirectResponse, json_dumps, json
+from cubicweb.web import (Redirect, RemoteCallFailed, DirectResponse,
+                          json, json_dumps)
 from cubicweb.web.controller import Controller
 from cubicweb.web.views import vid_from_rset, formrenderers
 
@@ -250,7 +245,7 @@
         errback = str(self._cw.form.get('__onfailure', 'null'))
         cbargs = str(self._cw.form.get('__cbargs', 'null'))
         self._cw.set_content_type('text/html')
-        jsargs = json.dumps((status, args, entity), cls=CubicWebJsonEncoder)
+        jsargs = json_dumps((status, args, entity))
         return """<script type="text/javascript">
  window.parent.handleFormValidationResponse('%s', %s, %s, %s, %s);
 </script>""" %  (domid, callback, errback, jsargs, cbargs)
@@ -568,42 +563,8 @@
 
 
 # XXX move to massmailing
-class SendMailController(Controller):
-    __regid__ = 'sendmail'
-    __select__ = authenticated_user() & match_form_params('recipient', 'mailbody', 'subject')
 
-    def recipients(self):
-        """returns an iterator on email's recipients as entities"""
-        eids = self._cw.form['recipient']
-        # eids may be a string if only one recipient was specified
-        if isinstance(eids, basestring):
-            rset = self._cw.execute('Any X WHERE X eid %(x)s', {'x': eids})
-        else:
-            rset = self._cw.execute('Any X WHERE X eid in (%s)' % (','.join(eids)))
-        return rset.entities()
-
-    def sendmail(self, recipient, subject, body):
-        msg = format_mail({'email' : self._cw.user.get_email(),
-                           'name' : self._cw.user.dc_title(),},
-                          [recipient], body, subject)
-        if not self._cw.vreg.config.sendmails([(msg, [recipient])]):
-            msg = self._cw._('could not connect to the SMTP server')
-            url = self._cw.build_url(__message=msg)
-            raise Redirect(url)
-
-    def publish(self, rset=None):
-        # XXX this allows users with access to an cubicweb instance to use it as
-        # a mail relay
-        body = self._cw.form['mailbody']
-        subject = self._cw.form['subject']
-        for recipient in self.recipients():
-            text = body % recipient.as_email_context()
-            self.sendmail(recipient.get_email(), subject, text)
-        url = self._cw.build_url(__message=self._cw._('emails successfully sent'))
-        raise Redirect(url)
-
-
-class MailBugReportController(SendMailController):
+class MailBugReportController(Controller):
     __regid__ = 'reportbug'
     __select__ = match_form_params('description')
 
@@ -614,7 +575,7 @@
         raise Redirect(url)
 
 
-class UndoController(SendMailController):
+class UndoController(Controller):
     __regid__ = 'undo'
     __select__ = authenticated_user() & match_form_params('txuuid')
 
--- a/web/views/calendar.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/calendar.py	Thu May 20 20:47:55 2010 +0200
@@ -15,20 +15,36 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""html calendar views
+"""html calendar views"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
 from datetime import datetime, date, timedelta
 
 from logilab.mtconverter import xml_escape
-from logilab.common.date import strptime, date_range, todate, todatetime
+from logilab.common.date import ONEDAY, strptime, date_range, todate, todatetime
 
 from cubicweb.interfaces import ICalendarable
-from cubicweb.selectors import implements
-from cubicweb.view import EntityView
+from cubicweb.selectors import implements, adaptable
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+
+
+class ICalendarableAdapter(EntityAdapter):
+    __regid__ = 'ICalendarable'
+    __select__ = implements(ICalendarable) # XXX for bw compat, should be abstract
+
+    @property
+    @implements_adapter_compat('ICalendarable')
+    def start(self):
+        """return start date"""
+        raise NotImplementedError
+
+    @property
+    @implements_adapter_compat('ICalendarable')
+    def stop(self):
+        """return stop state"""
+        raise NotImplementedError
 
 
 # useful constants & functions ################################################
@@ -52,7 +68,7 @@
 
         Does apply to ICalendarable compatible entities
         """
-        __select__ = implements(ICalendarable)
+        __select__ = adaptable('ICalendarable')
         paginable = False
         content_type = 'text/calendar'
         title = _('iCalendar')
@@ -66,10 +82,11 @@
                 event = ical.add('vevent')
                 event.add('summary').value = task.dc_title()
                 event.add('description').value = task.dc_description()
-                if task.start:
-                    event.add('dtstart').value = task.start
-                if task.stop:
-                    event.add('dtend').value = task.stop
+                icalendarable = task.cw_adapt_to('ICalendarable')
+                if icalendarable.start:
+                    event.add('dtstart').value = icalendarable.start
+                if icalendarable.stop:
+                    event.add('dtend').value = icalendarable.stop
 
             buff = ical.serialize()
             if not isinstance(buff, unicode):
@@ -85,7 +102,7 @@
     Does apply to ICalendarable compatible entities
     """
     __regid__ = 'hcal'
-    __select__ = implements(ICalendarable)
+    __select__ = adaptable('ICalendarable')
     paginable = False
     title = _('hCalendar')
     #templatable = False
@@ -98,10 +115,15 @@
             self.w(u'<h3 class="summary">%s</h3>' % xml_escape(task.dc_title()))
             self.w(u'<div class="description">%s</div>'
                    % task.dc_description(format='text/html'))
-            if task.start:
-                self.w(u'<abbr class="dtstart" title="%s">%s</abbr>' % (task.start.isoformat(), self._cw.format_date(task.start)))
-            if task.stop:
-                self.w(u'<abbr class="dtstop" title="%s">%s</abbr>' % (task.stop.isoformat(), self._cw.format_date(task.stop)))
+            icalendarable = task.cw_adapt_to('ICalendarable')
+            if icalendarable.start:
+                self.w(u'<abbr class="dtstart" title="%s">%s</abbr>'
+                       % (icalendarable.start.isoformat(),
+                          self._cw.format_date(icalendarable.start)))
+            if icalendarable.stop:
+                self.w(u'<abbr class="dtstop" title="%s">%s</abbr>'
+                       % (icalendarable.stop.isoformat(),
+                          self._cw.format_date(icalendarable.stop)))
             self.w(u'</div>')
         self.w(u'</div>')
 
@@ -113,10 +135,15 @@
         task = self.cw_rset.complete_entity(row, 0)
         task.view('oneline', w=self.w)
         if dates:
-            if task.start and task.stop:
-                self.w('<br/>' % self._cw._('from %(date)s' % {'date': self._cw.format_date(task.start)}))
-                self.w('<br/>' % self._cw._('to %(date)s' % {'date': self._cw.format_date(task.stop)}))
-                self.w('<br/>to %s'%self._cw.format_date(task.stop))
+            icalendarable = task.cw_adapt_to('ICalendarable')
+            if icalendarable.start and icalendarable.stop:
+                self.w('<br/> %s' % self._cw._('from %(date)s')
+                       % {'date': self._cw.format_date(icalendarable.start)})
+                self.w('<br/> %s' % self._cw._('to %(date)s')
+                       % {'date': self._cw.format_date(icalendarable.stop)})
+            else:
+                self.w('<br/>%s'%self._cw.format_date(icalendarable.start
+                                                      or icalendarable.stop))
 
 class CalendarLargeItemView(CalendarItemView):
     __regid__ = 'calendarlargeitem'
@@ -128,22 +155,25 @@
         self.color = color
         self.index = index
         self.length = 1
+        icalendarable = task.cw_adapt_to('ICalendarable')
+        self.start = icalendarable.start
+        self.stop = icalendarable.stop
 
     def in_working_hours(self):
         """predicate returning True is the task is in working hours"""
-        if todatetime(self.task.start).hour > 7 and todatetime(self.task.stop).hour < 20:
+        if todatetime(self.start).hour > 7 and todatetime(self.stop).hour < 20:
             return True
         return False
 
     def is_one_day_task(self):
-        task = self.task
-        return task.start and task.stop and task.start.isocalendar() ==  task.stop.isocalendar()
+        return self.start and self.stop and self.start.isocalendar() == self.stop.isocalendar()
 
 
 class OneMonthCal(EntityView):
     """At some point, this view will probably replace ampm calendars"""
     __regid__ = 'onemonthcal'
-    __select__ = implements(ICalendarable)
+    __select__ = adaptable('ICalendarable')
+
     paginable = False
     title = _('one month')
 
@@ -181,13 +211,14 @@
             else:
                 user = None
             the_dates = []
-            tstart = task.start
+            icalendarable = task.cw_adapt_to('ICalendarable')
+            tstart = icalendarable.start
             if tstart:
-                tstart = todate(task.start)
+                tstart = todate(icalendarable.start)
                 if tstart > lastday:
                     continue
                 the_dates = [tstart]
-            tstop = task.stop
+            tstop = icalendarable.stop
             if tstop:
                 tstop = todate(tstop)
                 if tstop < firstday:
@@ -199,7 +230,7 @@
                         the_dates = [tstart]
                 else:
                     the_dates = date_range(max(tstart, firstday),
-                                           min(tstop, lastday))
+                                           min(tstop + ONEDAY, lastday))
             if not the_dates:
                 continue
 
@@ -335,7 +366,8 @@
 class OneWeekCal(EntityView):
     """At some point, this view will probably replace ampm calendars"""
     __regid__ = 'oneweekcal'
-    __select__ = implements(ICalendarable)
+    __select__ = adaptable('ICalendarable')
+
     paginable = False
     title = _('one week')
 
@@ -368,8 +400,9 @@
                 continue
             done_tasks.append(task)
             the_dates = []
-            tstart = task.start
-            tstop = task.stop
+            icalendarable = task.cw_adapt_to('ICalendarable')
+            tstart = icalendarable.start
+            tstop = icalendarable.stop
             if tstart:
                 tstart = todate(tstart)
                 if tstart > lastday:
@@ -382,7 +415,7 @@
                 the_dates = [tstop]
             if tstart and tstop:
                 the_dates = date_range(max(tstart, firstday),
-                                       min(tstop, lastday))
+                                       min(tstop + ONEDAY, lastday))
             if not the_dates:
                 continue
 
@@ -462,7 +495,7 @@
     def _build_calendar_cell(self, date, task_descrs):
         inday_tasks = [t for t in task_descrs if t.is_one_day_task() and  t.in_working_hours()]
         wholeday_tasks = [t for t in task_descrs if not t.is_one_day_task()]
-        inday_tasks.sort(key=lambda t:t.task.start)
+        inday_tasks.sort(key=lambda t:t.start)
         sorted_tasks = []
         for i, t in enumerate(wholeday_tasks):
             t.index = i
@@ -470,7 +503,7 @@
         while inday_tasks:
             t = inday_tasks.pop(0)
             for i, c in enumerate(sorted_tasks):
-                if not c or c[-1].task.stop <= t.task.start:
+                if not c or c[-1].stop <= t.start:
                     c.append(t)
                     t.index = i+ncols
                     break
@@ -491,15 +524,15 @@
             start_min = 0
             stop_hour = 20
             stop_min = 0
-            if task.start:
-                if date < todate(task.start) < date + ONEDAY:
-                    start_hour = max(8, task.start.hour)
-                    start_min = task.start.minute
-            if task.stop:
-                if date < todate(task.stop) < date + ONEDAY:
-                    stop_hour = min(20, task.stop.hour)
+            if task_desc.start:
+                if date < todate(task_desc.start) < date + ONEDAY:
+                    start_hour = max(8, task_desc.start.hour)
+                    start_min = task_desc.start.minute
+            if task_desc.stop:
+                if date < todate(task_desc.stop) < date + ONEDAY:
+                    stop_hour = min(20, task_desc.stop.hour)
                     if stop_hour < 20:
-                        stop_min = task.stop.minute
+                        stop_min = task_desc.stop.minute
 
             height = 100.0*(stop_hour+stop_min/60.0-start_hour-start_min/60.0)/(20-8)
             top = 100.0*(start_hour+start_min/60.0-8)/(20-8)
@@ -518,7 +551,7 @@
             self.w(u'<div class="tooltip" ondblclick="stopPropagation(event); window.location.assign(\'%s\'); return false;">' % xml_escape(url))
             task.view('tooltip', w=self.w)
             self.w(u'</div>')
-            if task.start is None:
+            if task_desc.start is None:
                 self.w(u'<div class="bottommarker">')
                 self.w(u'<div class="bottommarkerline" style="margin: 0px 3px 0px 3px; height: 1px;">')
                 self.w(u'</div>')
--- a/web/views/cwproperties.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/cwproperties.py	Thu May 20 20:47:55 2010 +0200
@@ -35,7 +35,7 @@
 from cubicweb.web.formfields import FIELDS, StringField
 from cubicweb.web.formwidgets import (Select, TextInput, Button, SubmitButton,
                                       FieldWidget)
-from cubicweb.web.views import primary, formrenderers
+from cubicweb.web.views import primary, formrenderers, editcontroller
 
 uicfg.primaryview_section.tag_object_of(('*', 'for_user', '*'), 'hidden')
 
@@ -396,6 +396,15 @@
         w(u'</div>')
 
 
+class CWPropertyIEditControlAdapter(editcontroller.IEditControlAdapter):
+    __select__ = implements('CWProperty')
+
+    def after_deletion_path(self):
+        """return (path, parameters) which should be used as redirect
+        information when this entity is being deleted
+        """
+        return 'view', {}
+
 _afs = uicfg.autoform_section
 _afs.tag_subject_of(('*', 'for_user', '*'), 'main', 'hidden')
 _afs.tag_object_of(('*', 'for_user', '*'), 'main', 'hidden')
--- a/web/views/cwuser.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/cwuser.py	Thu May 20 20:47:55 2010 +0200
@@ -80,7 +80,7 @@
         if entity.firstname:
             self.w(u'<foaf:givenname>%s</foaf:givenname>\n'
                    % xml_escape(entity.firstname))
-        emailaddr = entity.get_email()
+        emailaddr = entity.cw_adapt_to('IEmailable').get_email()
         if emailaddr:
             self.w(u'<foaf:mbox>%s</foaf:mbox>\n' % xml_escape(emailaddr))
         self.w(u'</foaf:Person>\n')
--- a/web/views/editcontroller.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/editcontroller.py	Thu May 20 20:47:55 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""The edit controller, handling form submitting.
+"""The edit controller, automatically handling entity form submitting"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from warnings import warn
@@ -27,9 +26,37 @@
 from logilab.common.textutils import splitstrip
 
 from cubicweb import Binary, ValidationError, typed_eid
-from cubicweb.web import INTERNAL_FIELD_VALUE, RequestError, NothingToEdit, ProcessFormError
+from cubicweb.view import EntityAdapter, implements_adapter_compat
+from cubicweb.selectors import implements
+from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit,
+                          ProcessFormError)
 from cubicweb.web.views import basecontrollers, autoform
 
+
+class IEditControlAdapter(EntityAdapter):
+    __regid__ = 'IEditControl'
+    __select__ = implements('Any')
+
+    @implements_adapter_compat('IEditControl')
+    def after_deletion_path(self):
+        """return (path, parameters) which should be used as redirect
+        information when this entity is being deleted
+        """
+        parent = self.cw_adapt_to('IBreadCrumbs').parent_entity()
+        if parent is not None:
+            return parent.rest_path(), {}
+        return str(self.e_schema).lower(), {}
+
+    @implements_adapter_compat('IEditControl')
+    def pre_web_edit(self):
+        """callback called by the web editcontroller when an entity will be
+        created/modified, to let a chance to do some entity specific stuff.
+
+        Do nothing by default.
+        """
+        pass
+
+
 def valerror_eid(eid):
     try:
         return typed_eid(eid)
@@ -155,7 +182,7 @@
         entity.eid = formparams['eid']
         is_main_entity = self._cw.form.get('__maineid') == formparams['eid']
         # let a chance to do some entity specific stuff
-        entity.pre_web_edit()
+        entity.cw_adapt_to('IEditControl').pre_web_edit()
         # create a rql query from parameters
         rqlquery = RqlQuery()
         # process inlined relations at the same time as attributes
@@ -276,7 +303,7 @@
         eidtypes = tuple(eidtypes)
         for eid, etype in eidtypes:
             entity = self._cw.entity_from_eid(eid, etype)
-            path, params = entity.after_deletion_path()
+            path, params = entity.cw_adapt_to('IEditControl').after_deletion_path()
             redirect_info.add( (path, tuple(params.iteritems())) )
             entity.delete()
         if len(redirect_info) > 1:
--- a/web/views/emailaddress.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/emailaddress.py	Thu May 20 20:47:55 2010 +0200
@@ -26,7 +26,7 @@
 from cubicweb.selectors import implements
 from cubicweb import Unauthorized
 from cubicweb.web import uicfg
-from cubicweb.web.views import baseviews, primary
+from cubicweb.web.views import baseviews, primary, ibreadcrumbs
 
 _pvs = uicfg.primaryview_section
 _pvs.tag_subject_of(('*', 'use_email', '*'), 'attributes')
@@ -138,3 +138,10 @@
 
     def cell_call(self, row, col, **kwargs):
         self.w(self.cw_rset.get_entity(row, col).display_address())
+
+
+class EmailAddressIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = implements('EmailAddress')
+
+    def parent_entity(self):
+        return self.email_of
--- a/web/views/embedding.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/embedding.py	Thu May 20 20:47:55 2010 +0200
@@ -16,10 +16,8 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Objects interacting together to provides the external page embeding
-functionality.
+functionality."""
 
-
-"""
 __docformat__ = "restructuredtext en"
 
 import re
@@ -29,16 +27,27 @@
 
 from logilab.mtconverter import guess_encoding
 
-from cubicweb.selectors import (one_line_rset, score_entity,
-                                match_search_state, implements)
+from cubicweb.selectors import (one_line_rset, score_entity, implements,
+                                adaptable, match_search_state)
 from cubicweb.interfaces import IEmbedable
-from cubicweb.view import NOINDEX, NOFOLLOW
+from cubicweb.view import NOINDEX, NOFOLLOW, EntityAdapter, implements_adapter_compat
 from cubicweb.uilib import soup2xhtml
 from cubicweb.web.controller import Controller
 from cubicweb.web.action import Action
 from cubicweb.web.views import basetemplates
 
 
+class IEmbedableAdapter(EntityAdapter):
+    """interface for embedable entities"""
+    __regid__ = 'IEmbedable'
+    __select__ = implements(IEmbedable) # XXX for bw compat, should be abstract
+
+    @implements_adapter_compat('IEmbedable')
+    def embeded_url(self):
+        """embed action interface"""
+        raise NotImplementedError
+
+
 class ExternalTemplate(basetemplates.TheMainTemplate):
     """template embeding an external web pages into CubicWeb web interface
     """
@@ -92,7 +101,7 @@
 
 def entity_has_embedable_url(entity):
     """return 1 if the entity provides an allowed embedable url"""
-    url = entity.embeded_url()
+    url = entity.cw_adapt_to('IEmbedable').embeded_url()
     if not url or not url.strip():
         return 0
     allowed = entity._cw.vreg.config['embed-allowed']
@@ -107,14 +116,14 @@
     """
     __regid__ = 'embed'
     __select__ = (one_line_rset() & match_search_state('normal')
-                  & implements(IEmbedable)
+                  & adaptable('IEmbedable')
                   & score_entity(entity_has_embedable_url))
 
     title = _('embed')
 
     def url(self, row=0):
         entity = self.cw_rset.get_entity(row, 0)
-        url = urljoin(self._cw.base_url(), entity.embeded_url())
+        url = urljoin(self._cw.base_url(), entity.cw_adapt_to('IEmbedable').embeded_url())
         if self._cw.form.has_key('rql'):
             return self._cw.build_url('embed', url=url, rql=self._cw.form['rql'])
         return self._cw.build_url('embed', url=url)
--- a/web/views/ibreadcrumbs.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/ibreadcrumbs.py	Thu May 20 20:47:55 2010 +0200
@@ -21,20 +21,78 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
+from warnings import warn
+
 from logilab.mtconverter import xml_escape
 
-from cubicweb.interfaces import IBreadCrumbs
-from cubicweb.selectors import (one_line_rset, implements, one_etype_rset,
-                                multi_lines_rset, any_rset)
-from cubicweb.view import EntityView, Component
+#from cubicweb.interfaces import IBreadCrumbs
+from cubicweb.selectors import (implements, one_line_rset, adaptable,
+                                one_etype_rset, multi_lines_rset, any_rset)
+from cubicweb.view import EntityView, Component, EntityAdapter
 # don't use AnyEntity since this may cause bug with isinstance() due to reloading
 from cubicweb.entity import Entity
 from cubicweb import tags, uilib
 
 
+# ease bw compat
+def ibreadcrumb_adapter(entity):
+    if hasattr(entity, 'breadcrumbs'):
+        warn('[3.9] breadcrumbs() method is deprecated, define a custom '
+             'IBreadCrumbsAdapter for %s instead' % entity.__class__,
+             DeprecationWarning)
+        return entity
+    return entity.cw_adapt_to('IBreadCrumbs')
+
+
+class IBreadCrumbsAdapter(EntityAdapter):
+    """adapters for entities which can be"located" on some path to display in
+    the web ui
+    """
+    __regid__ = 'IBreadCrumbs'
+    __select__ = implements('Any', accept_none=False)
+
+    def parent_entity(self):
+        if hasattr(self.entity, 'parent'):
+            warn('[3.9] parent() method is deprecated, define a '
+                 'custom IBreadCrumbsAdapter/ITreeAdapter for %s instead'
+                 % self.entity.__class__, DeprecationWarning)
+            return self.entity.parent()
+        itree = self.entity.cw_adapt_to('ITree')
+        if itree is not None:
+            return itree.parent()
+        return None
+
+    def breadcrumbs(self, view=None, recurs=False):
+        """return a list containing some:
+
+        * tuple (url, label)
+        * entity
+        * simple label string
+
+        defining path from a root to the current view
+
+        the main view is given as argument so breadcrumbs may vary according
+        to displayed view (may be None). When recursing on a parent entity,
+        the `recurs` argument should be set to True.
+        """
+        path = [self.entity]
+        parent = self.parent_entity()
+        if parent is not None:
+            adapter = ibreadcrumb_adapter(self.entity)
+            path = adapter.breadcrumbs(view, True) + [self.entity]
+        if not recurs:
+            if view is None:
+                if 'vtitle' in self._cw.form:
+                    # embeding for instance
+                    path.append( self._cw.form['vtitle'] )
+            elif view.__regid__ != 'primary' and hasattr(view, 'title'):
+                path.append( self._cw._(view.title) )
+        return path
+
+
 class BreadCrumbEntityVComponent(Component):
     __regid__ = 'breadcrumbs'
-    __select__ = one_line_rset() & implements(IBreadCrumbs, accept_none=False)
+    __select__ = one_line_rset() & adaptable('IBreadCrumbs')
 
     cw_property_defs = {
         _('visible'):  dict(type='Boolean', default=True,
@@ -47,7 +105,8 @@
 
     def call(self, view=None, first_separator=True):
         entity = self.cw_rset.get_entity(0, 0)
-        path = entity.breadcrumbs(view)
+        adapter = ibreadcrumb_adapter(entity)
+        path = adapter.breadcrumbs(view)
         if path:
             self.open_breadcrumbs()
             if first_separator:
@@ -73,7 +132,7 @@
             self.w(u"\n")
             self.wpath_part(parent, contextentity, i == len(path) - 1)
 
-    def wpath_part(self, part, contextentity, last=False):
+    def wpath_part(self, part, contextentity, last=False): # XXX deprecates last argument?
         if isinstance(part, Entity):
             self.w(part.view('breadcrumbs'))
         elif isinstance(part, tuple):
@@ -88,7 +147,7 @@
 
 class BreadCrumbETypeVComponent(BreadCrumbEntityVComponent):
     __select__ = multi_lines_rset() & one_etype_rset() & \
-                 implements(IBreadCrumbs, accept_none=False)
+                 adaptable('IBreadCrumbs')
 
     def render_breadcrumbs(self, contextentity, path):
         # XXX hack: only display etype name or first non entity path part
--- a/web/views/idownloadable.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/idownloadable.py	Thu May 20 20:47:55 2010 +0200
@@ -15,29 +15,21 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Specific views for entities implementing IDownloadable
+"""Specific views for entities adapting to IDownloadable"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
 from logilab.mtconverter import BINARY_ENCODINGS, TransformError, xml_escape
 
 from cubicweb.view import EntityView
-from cubicweb.selectors import (one_line_rset, score_entity,
-                                implements, match_context_prop)
-from cubicweb.interfaces import IDownloadable
+from cubicweb.selectors import (one_line_rset, implements, match_context_prop,
+                                adaptable, has_mimetype)
 from cubicweb.mttransforms import ENGINE
 from cubicweb.web.box import EntityBoxTemplate
 from cubicweb.web.views import primary, baseviews
 
 
-def is_image(entity):
-    mt = entity.download_content_type()
-    if not (mt and mt.startswith('image/')):
-        return 0
-    return 1
-
 def download_box(w, entity, title=None, label=None, footer=u''):
     req = entity._cw
     w(u'<div class="sideBox">')
@@ -47,7 +39,7 @@
       % xml_escape(title))
     w(u'<div class="sideBox downloadBox"><div class="sideBoxBody">')
     w(u'<a href="%s"><img src="%s" alt="%s"/> %s</a>'
-      % (xml_escape(entity.download_url()),
+      % (xml_escape(entity.cw_adapt_to('IDownloadable').download_url()),
          req.uiprops['DOWNLOAD_ICON'],
          _('download icon'), xml_escape(label or entity.dc_title())))
     w(u'%s</div>' % footer)
@@ -58,8 +50,8 @@
     __regid__ = 'download_box'
     # no download box for images
     # XXX primary_view selector ?
-    __select__ = (one_line_rset() & implements(IDownloadable) &
-                  match_context_prop() & ~score_entity(is_image))
+    __select__ = (one_line_rset() & match_context_prop()
+                  & adaptable('IDownloadable') & ~has_mimetype('image/'))
     order = 10
 
     def cell_call(self, row, col, title=None, label=None, **kwargs):
@@ -72,7 +64,7 @@
     downloading of entities providing the necessary interface
     """
     __regid__ = 'download'
-    __select__ = one_line_rset() & implements(IDownloadable)
+    __select__ = one_line_rset() & adaptable('IDownloadable')
 
     templatable = False
     content_type = 'application/octet-stream'
@@ -81,47 +73,52 @@
 
     def set_request_content_type(self):
         """overriden to set the correct filetype and filename"""
-        entity = self.cw_rset.complete_entity(0, 0)
-        encoding = entity.download_encoding()
+        entity = self.cw_rset.complete_entity(self.cw_row or 0, self.cw_col or 0)
+        adapter = entity.cw_adapt_to('IDownloadable')
+        encoding = adapter.download_encoding()
         if encoding in BINARY_ENCODINGS:
             contenttype = 'application/%s' % encoding
             encoding = None
         else:
-            contenttype = entity.download_content_type()
+            contenttype = adapter.download_content_type()
         self._cw.set_content_type(contenttype or self.content_type,
-                                  filename=entity.download_file_name(),
+                                  filename=adapter.download_file_name(),
                                   encoding=encoding)
 
     def call(self):
-        self.w(self.cw_rset.complete_entity(0, 0).download_data())
+        entity = self.cw_rset.complete_entity(self.cw_row or 0, self.cw_col or 0)
+        adapter = entity.cw_adapt_to('IDownloadable')
+        self.w(adapter.download_data())
 
 
 class DownloadLinkView(EntityView):
     """view displaying a link to download the file"""
     __regid__ = 'downloadlink'
-    __select__ = implements(IDownloadable)
+    __select__ = adaptable('IDownloadable')
     title = None # should not be listed in possible views
 
 
     def cell_call(self, row, col, title=None, **kwargs):
         entity = self.cw_rset.get_entity(row, col)
-        url = xml_escape(entity.download_url())
+        url = xml_escape(entity.cw_adapt_to('IDownloadable').download_url())
         self.w(u'<a href="%s">%s</a>' % (url, xml_escape(title or entity.dc_title())))
 
 
 class IDownloadablePrimaryView(primary.PrimaryView):
-    __select__ = implements(IDownloadable)
+    __select__ = adaptable('IDownloadable')
 
     def render_entity_attributes(self, entity):
         super(IDownloadablePrimaryView, self).render_entity_attributes(entity)
         self.w(u'<div class="content">')
-        contenttype = entity.download_content_type()
+        adapter = entity.cw_adapt_to('IDownloadable')
+        contenttype = adapter.download_content_type()
         if contenttype.startswith('image/'):
             self.wview('image', entity.cw_rset, row=entity.cw_row)
         else:
             self.wview('downloadlink', entity.cw_rset, title=self._cw._('download'), row=entity.cw_row)
             try:
                 if ENGINE.has_input(contenttype):
+                    # XXX expect File like schema (access to 'data' attribute)
                     self.w(entity.printable_value('data'))
             except TransformError:
                 pass
@@ -133,21 +130,22 @@
 
 
 class IDownloadableLineView(baseviews.OneLineView):
-    __select__ = implements(IDownloadable)
+    __select__ = adaptable('IDownloadable')
 
     def cell_call(self, row, col, title=None, **kwargs):
         """the oneline view is a link to download the file"""
         entity = self.cw_rset.get_entity(row, col)
         url = xml_escape(entity.absolute_url())
-        name = xml_escape(title or entity.download_file_name())
-        durl = xml_escape(entity.download_url())
+        adapter = entity.cw_adapt_to('IDownloadable')
+        name = xml_escape(title or adapter.download_file_name())
+        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')))
 
 
 class ImageView(EntityView):
     __regid__ = 'image'
-    __select__ = implements(IDownloadable) & score_entity(is_image)
+    __select__ = has_mimetype('image/')
 
     title = _('image')
 
@@ -160,10 +158,11 @@
 
     def cell_call(self, row, col, width=None, height=None, link=False):
         entity = self.cw_rset.get_entity(row, col)
+        adapter = entity.cw_adapt_to('IDownloadable')
         #if entity.data_format.startswith('image/'):
         imgtag = u'<img src="%s" alt="%s" ' % (
-            xml_escape(entity.download_url()),
-            (self._cw._('download %s')  % xml_escape(entity.download_file_name())))
+            xml_escape(adapter.download_url()),
+            (self._cw._('download %s')  % xml_escape(adapter.download_file_name())))
         if width:
             imgtag += u'width="%i" ' % width
         if height:
--- a/web/views/igeocodable.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/igeocodable.py	Thu May 20 20:47:55 2010 +0200
@@ -21,27 +21,59 @@
 __docformat__ = "restructuredtext en"
 
 from cubicweb.interfaces import IGeocodable
-from cubicweb.view import EntityView
-from cubicweb.selectors import implements
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+from cubicweb.selectors import implements, adaptable
 from cubicweb.web import json
 
+class IGeocodableAdapter(EntityAdapter):
+    """interface required by geocoding views such as gmap-view"""
+    __regid__ = 'IGeocodable'
+    __select__ = implements(IGeocodable) # XXX for bw compat, should be abstract
+
+    @property
+    @implements_adapter_compat('IGeocodable')
+    def latitude(self):
+        """returns the latitude of the entity"""
+        raise NotImplementedError
+
+    @property
+    @implements_adapter_compat('IGeocodable')
+    def longitude(self):
+        """returns the longitude of the entity"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('IGeocodable')
+    def marker_icon(self):
+        """returns the icon that should be used as the marker.
+
+        an icon is defined by a 4-uple:
+
+          (icon._url, icon.size,  icon.iconAnchor, icon.shadow)
+        """
+        return (self._cw.uiprops['GMARKER_ICON'], (20, 34), (4, 34), None)
+
+
 class GeocodingJsonView(EntityView):
     __regid__ = 'geocoding-json'
-    __select__ = implements(IGeocodable)
+    __select__ = adaptable('IGeocodable')
 
     binary = True
     templatable = False
     content_type = 'application/json'
 
     def call(self):
-        # remove entities that don't define latitude and longitude
-        self.cw_rset = self.cw_rset.filtered_rset(lambda e: e.latitude and e.longitude)
         zoomlevel = self._cw.form.pop('zoomlevel', 8)
         extraparams = self._cw.form.copy()
         extraparams.pop('vid', None)
         extraparams.pop('rql', None)
-        markers = [self.build_marker_data(rowidx, extraparams)
-                   for rowidx in xrange(len(self.cw_rset))]
+        markers = []
+        for entity in self.cw_rset.entities():
+            igeocodable = entity.cw_adapt_to('IGeocodable')
+            # remove entities that don't define latitude and longitude
+            if not (igeocodable.latitude and igeocodable.longitude):
+                continue
+            markers.append(self.build_marker_data(entity, igeocodable,
+                                                  extraparams))
         center = {
             'latitude': sum(marker['latitude'] for marker in markers) / len(markers),
             'longitude': sum(marker['longitude'] for marker in markers) / len(markers),
@@ -53,24 +85,19 @@
             }
         self.w(json.dumps(geodata))
 
-    def build_marker_data(self, row, extraparams):
-        entity = self.cw_rset.get_entity(row, 0)
-        icon = None
-        if hasattr(entity, 'marker_icon'):
-            icon = entity.marker_icon()
-        else:
-            icon = (self._cw.uiprops['GMARKER_ICON'], (20, 34), (4, 34), None)
-        return {'latitude': entity.latitude, 'longitude': entity.longitude,
+    def build_marker_data(self, entity, igeocodable, extraparams):
+        return {'latitude': igeocodable.latitude,
+                'longitude': igeocodable.longitude,
+                'icon': igeocodable.marker_icon(),
                 'title': entity.dc_long_title(),
-                #icon defines : (icon._url, icon.size,  icon.iconAncho', icon.shadow)
-                'icon': icon,
-                'bubbleUrl': entity.absolute_url(vid='gmap-bubble', __notemplate=1, **extraparams),
+                'bubbleUrl': entity.absolute_url(
+                    vid='gmap-bubble', __notemplate=1, **extraparams),
                 }
 
 
 class GoogleMapBubbleView(EntityView):
     __regid__ = 'gmap-bubble'
-    __select__ = implements(IGeocodable)
+    __select__ = adaptable('IGeocodable')
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -80,16 +107,14 @@
 
 class GoogleMapsView(EntityView):
     __regid__ = 'gmap-view'
-    __select__ = implements(IGeocodable)
+    __select__ = adaptable('IGeocodable')
 
     paginable = False
 
     def call(self, gmap_key, width=400, height=400, uselabel=True, urlparams=None):
         self._cw.demote_to_html()
-        # remove entities that don't define latitude and longitude
-        self.cw_rset = self.cw_rset.filtered_rset(lambda e: e.latitude and e.longitude)
-        self._cw.add_js('http://maps.google.com/maps?sensor=false&file=api&v=2&key=%s' % gmap_key,
-                        localfile=False)
+        self._cw.add_js('http://maps.google.com/maps?sensor=false&file=api&v=2&key=%s'
+                        % gmap_key, localfile=False)
         self._cw.add_js( ('cubicweb.widgets.js', 'cubicweb.gmap.js', 'gmap.utility.labeledmarker.js') )
         rql = self.cw_rset.printable_rql()
         if urlparams is None:
@@ -98,7 +123,8 @@
             loadurl = self._cw.build_url(rql=rql, vid='geocoding-json', **urlparams)
         self.w(u'<div style="width: %spx; height: %spx;" class="widget gmap" '
                u'cubicweb:wdgtype="GMapWidget" cubicweb:loadtype="auto" '
-               u'cubicweb:loadurl="%s" cubicweb:uselabel="%s"> </div>' % (width, height, loadurl, uselabel))
+               u'cubicweb:loadurl="%s" cubicweb:uselabel="%s"> </div>'
+               % (width, height, loadurl, uselabel))
 
 
 class GoogeMapsLegend(EntityView):
--- a/web/views/iprogress.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/iprogress.py	Thu May 20 20:47:55 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Specific views for entities implementing IProgress
+"""Specific views for entities implementing IProgress/IMileStone"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
@@ -26,12 +25,121 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb.utils import make_uid
-from cubicweb.selectors import implements
+from cubicweb.selectors import implements, adaptable
 from cubicweb.interfaces import IProgress, IMileStone
 from cubicweb.schema import display_name
-from cubicweb.view import EntityView
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
 from cubicweb.web.views.tableview import EntityAttributesTableView
 
+
+class IProgressAdapter(EntityAdapter):
+    """something that has a cost, a state and a progression.
+
+    You should at least override progress_info an in_progress methods on concret
+    implementations.
+    """
+    __regid__ = 'IProgress'
+    __select__ = implements(IProgress) # XXX for bw compat, should be abstract
+
+    @property
+    @implements_adapter_compat('IProgress')
+    def cost(self):
+        """the total cost"""
+        return self.progress_info()['estimated']
+
+    @property
+    @implements_adapter_compat('IProgress')
+    def revised_cost(self):
+        return self.progress_info().get('estimatedcorrected', self.cost)
+
+    @property
+    @implements_adapter_compat('IProgress')
+    def done(self):
+        """what is already done"""
+        return self.progress_info()['done']
+
+    @property
+    @implements_adapter_compat('IProgress')
+    def todo(self):
+        """what remains to be done"""
+        return self.progress_info()['todo']
+
+    @implements_adapter_compat('IProgress')
+    def progress_info(self):
+        """returns a dictionary describing progress/estimated cost of the
+        version.
+
+        - mandatory keys are (''estimated', 'done', 'todo')
+
+        - optional keys are ('notestimated', 'notestimatedcorrected',
+          'estimatedcorrected')
+
+        'noestimated' and 'notestimatedcorrected' should default to 0
+        'estimatedcorrected' should default to 'estimated'
+        """
+        raise NotImplementedError
+
+    @implements_adapter_compat('IProgress')
+    def finished(self):
+        """returns True if status is finished"""
+        return not self.in_progress()
+
+    @implements_adapter_compat('IProgress')
+    def in_progress(self):
+        """returns True if status is not finished"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('IProgress')
+    def progress(self):
+        """returns the % progress of the task item"""
+        try:
+            return 100. * self.done / self.revised_cost
+        except ZeroDivisionError:
+            # total cost is 0 : if everything was estimated, task is completed
+            if self.progress_info().get('notestimated'):
+                return 0.
+            return 100
+
+    @implements_adapter_compat('IProgress')
+    def progress_class(self):
+        return ''
+
+
+class IMileStoneAdapter(IProgressAdapter):
+    """represents an ITask's item"""
+    __regid__ = 'IMileStone'
+    __select__ = implements(IMileStone) # XXX for bw compat, should be abstract
+
+    parent_type = None # specify main task's type
+
+    @implements_adapter_compat('IMileStone')
+    def get_main_task(self):
+        """returns the main ITask entity"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('IMileStone')
+    def initial_prevision_date(self):
+        """returns the initial expected end of the milestone"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('IMileStone')
+    def eta_date(self):
+        """returns expected date of completion based on what remains
+        to be done
+        """
+        raise NotImplementedError
+
+    @implements_adapter_compat('IMileStone')
+    def completion_date(self):
+        """returns date on which the subtask has been completed"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('IMileStone')
+    def contractors(self):
+        """returns the list of persons supposed to work on this task"""
+        raise NotImplementedError
+
+
 class ProgressTableView(EntityAttributesTableView):
     """The progress table view is able to display progress information
     of any object implement IMileStone.
@@ -50,8 +158,8 @@
     """
 
     __regid__ = 'progress_table_view'
+    __select__ = adaptable('IMileStone')
     title = _('task progression')
-    __select__ = implements(IMileStone)
     table_css = "progress"
     css_files = ('cubicweb.iprogress.css',)
 
@@ -71,10 +179,7 @@
             else:
                 content = entity.printable_value(col)
             infos[col] = content
-        if hasattr(entity, 'progress_class'):
-            cssclass = entity.progress_class()
-        else:
-            cssclass = u''
+        cssclass = entity.cw_adapt_to('IMileStone').progress_class()
         self.w(u"""<tr class="%s" onmouseover="addElementClass(this, 'highlighted');"
             onmouseout="removeElementClass(this, 'highlighted')">""" % cssclass)
         line = u''.join(u'<td>%%(%s)s</td>' % col for col in self.columns)
@@ -83,18 +188,18 @@
 
     ## header management ######################################################
 
-    def header_for_project(self, ecls):
+    def header_for_project(self, sample):
         """use entity's parent type as label"""
-        return display_name(self._cw, ecls.parent_type)
+        return display_name(self._cw, sample.cw_adapt_to('IMileStone').parent_type)
 
-    def header_for_milestone(self, ecls):
+    def header_for_milestone(self, sample):
         """use entity's type as label"""
-        return display_name(self._cw, ecls.__regid__)
+        return display_name(self._cw, sample.__regid__)
 
     ## cell management ########################################################
     def build_project_cell(self, entity):
         """``project`` column cell renderer"""
-        project = entity.get_main_task()
+        project = entity.cw_adapt_to('IMileStone').get_main_task()
         if project:
             return project.view('incontext')
         return self._cw._('no related project')
@@ -105,15 +210,16 @@
 
     def build_state_cell(self, entity):
         """``state`` column cell renderer"""
-        return xml_escape(self._cw._(entity.state))
+        return xml_escape(entity.cw_adapt_to('IWorkflowable').printable_state)
 
     def build_eta_date_cell(self, entity):
         """``eta_date`` column cell renderer"""
-        if entity.finished():
-            return self._cw.format_date(entity.completion_date())
-        formated_date = self._cw.format_date(entity.initial_prevision_date())
-        if entity.in_progress():
-            eta_date = self._cw.format_date(entity.eta_date())
+        imilestone = entity.cw_adapt_to('IMileStone')
+        if imilestone.finished():
+            return self._cw.format_date(imilestone.completion_date())
+        formated_date = self._cw.format_date(imilestone.initial_prevision_date())
+        if imilestone.in_progress():
+            eta_date = self._cw.format_date(imilestone.eta_date())
             _ = self._cw._
             if formated_date:
                 formated_date += u' (%s %s)' % (_('expected:'), eta_date)
@@ -123,12 +229,14 @@
 
     def build_todo_by_cell(self, entity):
         """``todo_by`` column cell renderer"""
-        return u', '.join(p.view('outofcontext') for p in entity.contractors())
+        imilestone = entity.cw_adapt_to('IMileStone')
+        return u', '.join(p.view('outofcontext') for p in imilestone.contractors())
 
     def build_cost_cell(self, entity):
         """``cost`` column cell renderer"""
         _ = self._cw._
-        pinfo = entity.progress_info()
+        imilestone = entity.cw_adapt_to('IMileStone')
+        pinfo = imilestone.progress_info()
         totalcost = pinfo.get('estimatedcorrected', pinfo['estimated'])
         missing = pinfo.get('notestimatedcorrected', pinfo.get('notestimated', 0))
         costdescr = []
@@ -167,8 +275,9 @@
 class ProgressBarView(EntityView):
     """displays a progress bar"""
     __regid__ = 'progressbar'
+    __select__ = adaptable('IProgress')
+
     title = _('progress bar')
-    __select__ = implements(IProgress)
 
     precision = 0.1
     red_threshold = 1.1
@@ -176,10 +285,10 @@
     yellow_threshold = 1
 
     @classmethod
-    def overrun(cls, entity):
+    def overrun(cls, iprogress):
         """overrun = done + todo - """
-        if entity.done + entity.todo > entity.revised_cost:
-            overrun = entity.done + entity.todo - entity.revised_cost
+        if iprogress.done + iprogress.todo > iprogress.revised_cost:
+            overrun = iprogress.done + iprogress.todo - iprogress.revised_cost
         else:
             overrun = 0
         if overrun < cls.precision:
@@ -187,20 +296,21 @@
         return overrun
 
     @classmethod
-    def overrun_percentage(cls, entity):
+    def overrun_percentage(cls, iprogress):
         """pourcentage overrun = overrun / budget"""
-        if entity.revised_cost == 0:
+        if iprogress.revised_cost == 0:
             return 0
         else:
-            return cls.overrun(entity) * 100. / entity.revised_cost
+            return cls.overrun(iprogress) * 100. / iprogress.revised_cost
 
     def cell_call(self, row, col):
         self._cw.add_css('cubicweb.iprogress.css')
         self._cw.add_js('cubicweb.iprogress.js')
         entity = self.cw_rset.get_entity(row, col)
-        done = entity.done
-        todo = entity.todo
-        budget = entity.revised_cost
+        iprogress = entity.cw_adapt_to('IProgress')
+        done = iprogress.done
+        todo = iprogress.todo
+        budget = iprogress.revised_cost
         if budget == 0:
             pourcent = 100
         else:
@@ -229,25 +339,23 @@
 
         title = u'%s/%s = %i%%' % (done_str, budget_str, pourcent)
         short_title = title
-        if self.overrun_percentage(entity):
-            title += u' overrun +%sj (+%i%%)' % (self.overrun(entity),
-                                                 self.overrun_percentage(entity))
-            overrun = self.overrun(entity)
-            if floor(overrun) == overrun or overrun>100:
-                overrun_str = '%i' % overrun
+        overrunpercent = self.overrun_percentage(iprogress)
+        if overrunpercent:
+            overrun = self.overrun(iprogress)
+            title += u' overrun +%sj (+%i%%)' % (overrun, overrunpercent)
+            if floor(overrun) == overrun or overrun > 100:
+                short_title += u' +%i' % overrun
             else:
-                overrun_str = '%.1f' % overrun
-            short_title += u' +%s' % overrun_str
+                short_title += u' +%.1f' % overrun
         # write bars
         maxi = max(done+todo, budget)
         if maxi == 0:
             maxi = 1
-
         cid = make_uid('progress_bar')
-        self._cw.html_headers.add_onload('draw_progressbar("canvas%s", %i, %i, %i, "%s");' %
-                                         (cid,
-                                          int(100.*done/maxi), int(100.*(done+todo)/maxi),
-                                          int(100.*budget/maxi), color))
+        self._cw.html_headers.add_onload(
+            'draw_progressbar("canvas%s", %i, %i, %i, "%s");' %
+            (cid, int(100.*done/maxi), int(100.*(done+todo)/maxi),
+             int(100.*budget/maxi), color))
         self.w(u'%s<br/>'
                u'<canvas class="progressbar" id="canvas%s" width="100" height="10"></canvas>'
                % (xml_escape(short_title), cid))
--- a/web/views/isioc.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/isioc.py	Thu May 20 20:47:55 2010 +0200
@@ -15,20 +15,70 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Specific views for SIOC interfaces
+"""Specific views for SIOC (Semantically-Interlinked Online Communities)
 
+http://sioc-project.org
 """
+
 __docformat__ = "restructuredtext en"
 
 from logilab.mtconverter import xml_escape
 
-from cubicweb.view import EntityView
-from cubicweb.selectors import implements
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+from cubicweb.selectors import implements, adaptable
 from cubicweb.interfaces import ISiocItem, ISiocContainer
 
+
+class ISIOCItemAdapter(EntityAdapter):
+    """interface for entities which may be represented as an ISIOC items"""
+    __regid__ = 'ISIOCItem'
+    __select__ = implements(ISiocItem) # XXX for bw compat, should be abstract
+
+    @implements_adapter_compat('ISIOCItem')
+    def isioc_content(self):
+        """return item's content"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('ISIOCItem')
+    def isioc_container(self):
+        """return container entity"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('ISIOCItem')
+    def isioc_type(self):
+        """return container type (post, BlogPost, MailMessage)"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('ISIOCItem')
+    def isioc_replies(self):
+        """return replies items"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('ISIOCItem')
+    def isioc_topics(self):
+        """return topics items"""
+        raise NotImplementedError
+
+
+class ISIOCContainerAdapter(EntityAdapter):
+    """interface for entities which may be represented as an ISIOC container"""
+    __regid__ = 'ISIOCContainer'
+    __select__ = implements(ISiocContainer) # XXX for bw compat, should be abstract
+
+    @implements_adapter_compat('ISIOCContainer')
+    def isioc_type(self):
+        """return container type (forum, Weblog, MailingList)"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('ISIOCContainer')
+    def isioc_items(self):
+        """return contained items"""
+        raise NotImplementedError
+
+
 class SIOCView(EntityView):
     __regid__ = 'sioc'
-    __select__ = EntityView.__select__ & implements(ISiocItem, ISiocContainer)
+    __select__ = adaptable('ISIOCItem', 'ISIOCContainer')
     title = _('sioc')
     templatable = False
     content_type = 'text/xml'
@@ -52,48 +102,51 @@
 
 class SIOCContainerView(EntityView):
     __regid__ = 'sioc_element'
-    __select__ = EntityView.__select__ & implements(ISiocContainer)
+    __select__ = adaptable('ISIOCContainer')
     templatable = False
     content_type = 'text/xml'
 
     def cell_call(self, row, col):
         entity = self.cw_rset.complete_entity(row, col)
-        sioct = xml_escape(entity.isioc_type())
+        isioc = entity.cw_adapt_to('ISIOCContainer')
+        isioct = isioc.isioc_type()
         self.w(u'<sioc:%s rdf:about="%s">\n'
-               % (sioct, xml_escape(entity.absolute_url())))
+               % (isioct, xml_escape(entity.absolute_url())))
         self.w(u'<dcterms:title>%s</dcterms:title>'
                % xml_escape(entity.dc_title()))
         self.w(u'<dcterms:created>%s</dcterms:created>'
-               % entity.creation_date)
+               % entity.creation_date) # XXX format
         self.w(u'<dcterms:modified>%s</dcterms:modified>'
-               % entity.modification_date)
+               % entity.modification_date) # XXX format
         self.w(u'<!-- FIXME : here be items -->')#entity.isioc_items()
         self.w(u'</sioc:%s>\n' % sioct)
 
 
 class SIOCItemView(EntityView):
     __regid__ = 'sioc_element'
-    __select__ = EntityView.__select__ & implements(ISiocItem)
+    __select__ = adaptable('ISIOCItem')
     templatable = False
     content_type = 'text/xml'
 
     def cell_call(self, row, col):
         entity = self.cw_rset.complete_entity(row, col)
-        sioct = xml_escape(entity.isioc_type())
+        isioc = entity.cw_adapt_to('ISIOCItem')
+        isioct = isioc.isioc_type()
         self.w(u'<sioc:%s rdf:about="%s">\n'
-               %  (sioct, xml_escape(entity.absolute_url())))
+               % (isioct, xml_escape(entity.absolute_url())))
         self.w(u'<dcterms:title>%s</dcterms:title>'
                % xml_escape(entity.dc_title()))
         self.w(u'<dcterms:created>%s</dcterms:created>'
-               % entity.creation_date)
+               % entity.creation_date) # XXX format
         self.w(u'<dcterms:modified>%s</dcterms:modified>'
-               % entity.modification_date)
-        if entity.content:
-            self.w(u'<sioc:content>%s</sioc:content>'''
-                   % xml_escape(entity.isioc_content()))
-        if entity.related('entry_of'):
+               % entity.modification_date) # XXX format
+        content = isioc.isioc_content()
+        if content:
+            self.w(u'<sioc:content>%s</sioc:content>' % xml_escape(content))
+        container = isioc.isioc_container()
+        if container:
             self.w(u'<sioc:has_container rdf:resource="%s"/>\n'
-                   % xml_escape(entity.isioc_container().absolute_url()))
+                   % xml_escape(container.absolute_url()))
         if entity.creator:
             self.w(u'<sioc:has_creator>\n')
             self.w(u'<sioc:User rdf:about="%s">\n'
--- a/web/views/massmailing.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/massmailing.py	Thu May 20 20:47:55 2010 +0200
@@ -15,18 +15,17 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Mass mailing form views
+"""Mass mailing handling: send mail to entities adaptable to IEmailable"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
 import operator
 
-from cubicweb.interfaces import IEmailable
-from cubicweb.selectors import implements, authenticated_user
+from cubicweb.selectors import (implements, authenticated_user,
+                                adaptable, match_form_params)
 from cubicweb.view import EntityView
-from cubicweb.web import stdmsgs, action, form, formfields as ff
+from cubicweb.web import stdmsgs, controller, action, form, formfields as ff
 from cubicweb.web.formwidgets import CheckBox, TextInput, AjaxWidget, ImgButton
 from cubicweb.web.views import forms, formrenderers
 
@@ -34,8 +33,9 @@
 class SendEmailAction(action.Action):
     __regid__ = 'sendemail'
     # XXX should check email is set as well
-    __select__ = (action.Action.__select__ & implements(IEmailable)
-                  & authenticated_user())
+    __select__ = (action.Action.__select__
+                  & authenticated_user()
+                  & adaptable('IEmailable'))
 
     title = _('send email')
     category = 'mainactions'
@@ -49,9 +49,11 @@
 
 
 def recipient_vocabulary(form, field):
-    vocab = [(entity.get_email(), entity.eid) for entity in form.cw_rset.entities()]
+    vocab = [(entity.cw_adapt_to('IEmailable').get_email(), entity.eid)
+             for entity in form.cw_rset.entities()]
     return [(label, value) for label, value in vocab if label]
 
+
 class MassMailingForm(forms.FieldsForm):
     __regid__ = 'massmailing'
 
@@ -62,10 +64,13 @@
 
     sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}),
                             label=_('From:'),
-                            value=lambda f: '%s <%s>' % (f._cw.user.dc_title(), f._cw.user.get_email()))
+                            value=lambda f: '%s <%s>' % (
+                                f._cw.user.dc_title(),
+                                f._cw.user.cw_adapt_to('IEmailable').get_email()))
     recipient = ff.StringField(widget=CheckBox(), label=_('Recipients:'),
                                choices=recipient_vocabulary,
-                               value= lambda f: [entity.eid for entity in f.cw_rset.entities() if entity.get_email()])
+                               value= lambda f: [entity.eid for entity in f.cw_rset.entities()
+                                                 if entity.cw_adapt_to('IEmailable').get_email()])
     subject = ff.StringField(label=_('Subject:'), max_length=256)
     mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField',
                                                 inputid='mailbody'))
@@ -84,8 +89,8 @@
     def get_allowed_substitutions(self):
         attrs = []
         for coltype in self.cw_rset.column_types(0):
-            eclass = self._cw.vreg['etypes'].etype_class(coltype)
-            attrs.append(eclass.allowed_massmail_keys())
+            entity = self._cw.vreg['etypes'].etype_class(coltype)(self._cw)
+            attrs.append(entity.cw_adapt_to('IEmailable').allowed_massmail_keys())
         return sorted(reduce(operator.and_, attrs))
 
     def build_substitutions_help(self):
@@ -135,9 +140,36 @@
 
 class MassMailingFormView(form.FormViewMixIn, EntityView):
     __regid__ = 'massmailing'
-    __select__ = implements(IEmailable) & authenticated_user()
+    __select__ = authenticated_user() & adaptable('IEmailable')
 
     def call(self):
         form = self._cw.vreg['forms'].select('massmailing', self._cw,
                                              rset=self.cw_rset)
         self.w(form.render())
+
+
+class SendMailController(controller.Controller):
+    __regid__ = 'sendmail'
+    __select__ = authenticated_user() & match_form_params('recipient', 'mailbody', 'subject')
+
+    def recipients(self):
+        """returns an iterator on email's recipients as entities"""
+        eids = self._cw.form['recipient']
+        # eids may be a string if only one recipient was specified
+        if isinstance(eids, basestring):
+            rset = self._cw.execute('Any X WHERE X eid %(x)s', {'x': eids})
+        else:
+            rset = self._cw.execute('Any X WHERE X eid in (%s)' % (','.join(eids)))
+        return rset.entities()
+
+    def publish(self, rset=None):
+        # XXX this allows users with access to an cubicweb instance to use it as
+        # a mail relay
+        body = self._cw.form['mailbody']
+        subject = self._cw.form['subject']
+        for recipient in self.recipients():
+            iemailable = recipient.cw_adapt_to('IEmailable')
+            text = body % iemailable.as_email_context()
+            self.sendmail(iemailable.get_email(), subject, text)
+        url = self._cw.build_url(__message=self._cw._('emails successfully sent'))
+        raise Redirect(url)
--- a/web/views/navigation.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/navigation.py	Thu May 20 20:47:55 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""navigation components definition for CubicWeb web client
+"""navigation components definition for CubicWeb web client"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
@@ -26,11 +25,10 @@
 from logilab.mtconverter import xml_escape
 from logilab.common.deprecation import deprecated
 
-from cubicweb.interfaces import IPrevNext
 from cubicweb.selectors import (paginated_rset, sorted_rset,
-                                primary_view, match_context_prop,
-                                one_line_rset, implements)
+                                adaptable, implements)
 from cubicweb.uilib import cut
+from cubicweb.view import EntityAdapter, implements_adapter_compat
 from cubicweb.web.component import EntityVComponent, NavigationComponent
 
 
@@ -160,20 +158,41 @@
         self.w(u'</div>')
 
 
+from cubicweb.interfaces import IPrevNext
+
+class IPrevNextAdapter(EntityAdapter):
+    """interface for entities which can be linked to a previous and/or next
+    entity
+    """
+    __regid__ = 'IPrevNext'
+    __select__ = implements(IPrevNext) # XXX for bw compat, else should be abstract
+
+    @implements_adapter_compat('IPrevNext')
+    def next_entity(self):
+        """return the 'next' entity"""
+        raise NotImplementedError
+
+    @implements_adapter_compat('IPrevNext')
+    def previous_entity(self):
+        """return the 'previous' entity"""
+        raise NotImplementedError
+
+
 class NextPrevNavigationComponent(EntityVComponent):
     __regid__ = 'prevnext'
     # register msg not generated since no entity implements IPrevNext in cubicweb
     # itself
     title = _('contentnavigation_prevnext')
     help = _('contentnavigation_prevnext_description')
-    __select__ = (one_line_rset() & primary_view()
-                  & match_context_prop() & implements(IPrevNext))
+    __select__ = (EntityVComponent.__select__
+                  & adaptable('IPrevNext'))
     context = 'navbottom'
     order = 10
     def call(self, view=None):
         entity = self.cw_rset.get_entity(0, 0)
-        previous = entity.previous_entity()
-        next = entity.next_entity()
+        adapter = entity.cw_adapt_to('IDownloadable')
+        previous = adapter.previous_entity()
+        next = adapter.next_entity()
         if previous or next:
             textsize = self._cw.property_value('navigation.short-line-size')
             self.w(u'<div class="prevnext">')
--- a/web/views/old_calendar.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/old_calendar.py	Thu May 20 20:47:55 2010 +0200
@@ -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/>.
-"""html calendar views
-
-"""
+"""html calendar views"""
 
 from datetime import date, time, timedelta
 
@@ -26,8 +24,25 @@
                                  next_month, first_day, last_day, date_range)
 
 from cubicweb.interfaces import ICalendarViews
-from cubicweb.selectors import implements
-from cubicweb.view import EntityView
+from cubicweb.selectors import implements, adaptable
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+
+class ICalendarViewsAdapter(EntityAdapter):
+    """calendar views interface"""
+    __regid__ = 'ICalendarViews'
+    __select__ = implements(ICalendarViews) # XXX for bw compat, should be abstract
+
+    @implements_adapter_compat('ICalendarViews')
+    def matching_dates(self, begin, end):
+        """
+        :param begin: day considered as begin of the range (`DateTime`)
+        :param end: day considered as end of the range (`DateTime`)
+
+        :return:
+          a list of dates (`DateTime`) in the range [`begin`, `end`] on which
+          this entity apply
+        """
+        raise NotImplementedError
 
 # used by i18n tools
 WEEKDAYS = [_("monday"), _("tuesday"), _("wednesday"), _("thursday"),
@@ -39,7 +54,7 @@
 
 class _CalendarView(EntityView):
     """base calendar view containing helpful methods to build calendar views"""
-    __select__ = implements(ICalendarViews,)
+    __select__ = adaptable('ICalendarViews')
     paginable = False
 
     # Navigation building methods / views ####################################
@@ -126,7 +141,7 @@
             infos = u'<div class="event">'
             infos += self._cw.view(itemvid, self.cw_rset, row=row)
             infos += u'</div>'
-            for date_ in entity.matching_dates(begin, end):
+            for date_ in entity.cw_adapt_to('ICalendarViews').matching_dates(begin, end):
                 day = date(date_.year, date_.month, date_.day)
                 try:
                     dt = time(date_.hour, date_.minute, date_.second)
@@ -288,7 +303,7 @@
             monthlink = '<a href="%s">%s</a>' % (xml_escape(url), umonth)
             self.w(u'<tr><th colspan="3">%s %s (%s)</th></tr>' \
                   % (_('week'), monday.isocalendar()[1], monthlink))
-            for day in date_range(monday, sunday):
+            for day in date_range(monday, sunday+ONEDAY):
                 self.w(u'<tr>')
                 self.w(u'<td>%s</td>' % _(WEEKDAYS[day.weekday()]))
                 self.w(u'<td>%s</td>' % (day.strftime('%Y-%m-%d')))
@@ -478,7 +493,7 @@
             w(u'<tr>%s</tr>' % (
                 WEEK_TITLE % (_('week'), monday.isocalendar()[1], monthlink)))
             w(u'<tr><th>%s</th><th>&#160;</th></tr>'% _(u'Date'))
-            for day in date_range(monday, sunday):
+            for day in date_range(monday, sunday+ONEDAY):
                 events = schedule.get(day)
                 style = day.weekday() % 2 and "even" or "odd"
                 w(u'<tr class="%s">' % style)
--- a/web/views/schema.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/schema.py	Thu May 20 20:47:55 2010 +0200
@@ -35,7 +35,7 @@
 from cubicweb import tags, uilib
 from cubicweb.web import action, facet, uicfg, schemaviewer
 from cubicweb.web.views import TmpFileViewMixin
-from cubicweb.web.views import primary, baseviews, tabs, tableview, iprogress
+from cubicweb.web.views import primary, baseviews, tabs, tableview, ibreadcrumbs
 
 ALWAYS_SKIP_TYPES = BASE_TYPES | SCHEMA_TYPES
 SKIP_TYPES  = (ALWAYS_SKIP_TYPES | META_RTYPES | SYSTEM_RTYPES | WORKFLOW_TYPES
@@ -680,6 +680,37 @@
         visitor = OneHopRSchemaVisitor(self._cw, rschema)
         s2d.schema2dot(outputfile=tmpfile, visitor=visitor)
 
+# breadcrumbs ##################################################################
+
+class CWRelationIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = implements('CWRelation')
+    def parent_entity(self):
+        return self.entity.rtype
+
+class CWAttributeIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = implements('CWAttribute')
+    def parent_entity(self):
+        return self.entity.stype
+
+class CWConstraintIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = implements('CWConstraint')
+    def parent_entity(self):
+        if self.entity.reverse_constrained_by:
+            return self.entity.reverse_constrained_by[0]
+
+class RQLExpressionIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = implements('RQLExpression')
+    def parent_entity(self):
+        return self.entity.expression_of
+
+class CWPermissionIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = implements('CWPermission')
+    def parent_entity(self):
+        # XXX useless with permission propagation
+        permissionof = getattr(self.entity, 'reverse_require_permission', ())
+        if len(permissionof) == 1:
+            return permissionof[0]
+
 
 # misc: facets, actions ########################################################
 
--- a/web/views/tableview.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/tableview.py	Thu May 20 20:47:55 2010 +0200
@@ -369,9 +369,9 @@
             self._cw.add_css(self.css_files)
         _ = self._cw._
         self.columns = columns or self.columns
-        ecls = self._cw.vreg['etypes'].etype_class(self.cw_rset.description[0][0])
+        sample = self.cw_rset.get_entity(0, 0)
         self.w(u'<table class="%s">' % self.table_css)
-        self.table_header(ecls)
+        self.table_header(sample)
         self.w(u'<tbody>')
         for row in xrange(self.cw_rset.rowcount):
             self.cell_call(row=row, col=0)
@@ -396,16 +396,15 @@
         self.w(line % infos)
         self.w(u'</tr>\n')
 
-    def table_header(self, ecls):
+    def table_header(self, sample):
         """builds the table's header"""
         self.w(u'<thead><tr>')
-        _ = self._cw._
         for column in self.columns:
             meth = getattr(self, 'header_for_%s' % column, None)
             if meth:
-                colname = meth(ecls)
+                colname = meth(sample)
             else:
-                colname = _(column)
+                colname = self._cw._(column)
             self.w(u'<th>%s</th>' % xml_escape(colname))
         self.w(u'</tr></thead>\n')
 
--- a/web/views/timeline.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/timeline.py	Thu May 20 20:47:55 2010 +0200
@@ -18,14 +18,13 @@
 """basic support for SIMILE's timline widgets
 
 cf. http://code.google.com/p/simile-widgets/
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from logilab.mtconverter import xml_escape
 
-from cubicweb.interfaces import ICalendarable
-from cubicweb.selectors import implements
+from cubicweb.selectors import adaptable
 from cubicweb.view import EntityView, StartupView
 from cubicweb.web import json
 
@@ -37,11 +36,12 @@
     should be properties of entity classes or subviews)
     """
     __regid__ = 'timeline-json'
+    __select__ = adaptable('ICalendarable')
+
     binary = True
     templatable = False
     content_type = 'application/json'
 
-    __select__ = implements(ICalendarable)
     date_fmt = '%Y/%m/%d'
 
     def call(self):
@@ -74,8 +74,9 @@
         'link': 'http://www.allposters.com/-sp/Portrait-of-Horace-Brodsky-Posters_i1584413_.htm'
         }
         """
-        start = entity.start
-        stop = entity.stop
+        icalendarable = entity.cw_adapt_to('ICalendarable')
+        start = icalendarable.start
+        stop = icalendarable.stop
         start = start or stop
         if start is None and stop is None:
             return None
@@ -116,7 +117,7 @@
     """builds a cubicweb timeline widget node"""
     __regid__ = 'timeline'
     title = _('timeline')
-    __select__ = implements(ICalendarable)
+    __select__ = adaptable('ICalendarable')
     paginable = False
     def call(self, tlunit=None):
         self._cw.html_headers.define_var('Timeline_urlPrefix', self._cw.datadir_url)
--- a/web/views/timetable.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/timetable.py	Thu May 20 20:47:55 2010 +0200
@@ -15,16 +15,16 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""html calendar views
+"""html timetable views"""
 
-"""
+__docformat__ = "restructuredtext en"
+_ = unicode
 
 from logilab.mtconverter import xml_escape
-from logilab.common.date import date_range, todatetime
+from logilab.common.date import ONEDAY, date_range, todatetime
 
-from cubicweb.interfaces import ITimetableViews
-from cubicweb.selectors import implements
-from cubicweb.view import AnyRsetView
+from cubicweb.selectors import adaptable
+from cubicweb.view import EntityView
 
 
 class _TaskEntry(object):
@@ -37,10 +37,10 @@
 MIN_COLS = 3  # minimum number of task columns for a single user
 ALL_USERS = object()
 
-class TimeTableView(AnyRsetView):
+class TimeTableView(EntityView):
     __regid__ = 'timetable'
     title = _('timetable')
-    __select__ = implements(ITimetableViews)
+    __select__ = adaptable('ICalendarable')
     paginable = False
 
     def call(self, title=None):
@@ -53,20 +53,22 @@
         # XXX: try refactoring with calendar.py:OneMonthCal
         for row in xrange(self.cw_rset.rowcount):
             task = self.cw_rset.get_entity(row, 0)
+            icalendarable = task.cw_adapt_to('ICalendarable')
             if len(self.cw_rset[row]) > 1:
                 user = self.cw_rset.get_entity(row, 1)
             else:
                 user = ALL_USERS
             the_dates = []
-            if task.start and task.stop:
-                if task.start.toordinal() == task.stop.toordinal():
-                    the_dates.append(task.start)
+            if icalendarable.start and icalendarable.stop:
+                if icalendarable.start.toordinal() == icalendarable.stop.toordinal():
+                    the_dates.append(icalendarable.start)
                 else:
-                    the_dates += date_range( task.start, task.stop )
-            elif task.start:
-                the_dates.append(task.start)
-            elif task.stop:
-                the_dates.append(task.stop)
+                    the_dates += date_range(icalendarable.start,
+                                            icalendarable.stop + ONEDAY)
+            elif icalendarable.start:
+                the_dates.append(icalendarable.start)
+            elif icalendarable.stop:
+                the_dates.append(icalendarable.stop)
             for d in the_dates:
                 d = todatetime(d)
                 d_users = dates.setdefault(d, {})
--- a/web/views/treeview.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/treeview.py	Thu May 20 20:47:55 2010 +0200
@@ -15,22 +15,252 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Set of tree-building widgets, based on jQuery treeview plugin
-
+"""Set of tree views / tree-building widgets, some based on jQuery treeview
+plugin.
 """
 __docformat__ = "restructuredtext en"
 
+from warnings import warn
+
 from logilab.mtconverter import xml_escape
+from logilab.common.decorators import cached
+
 from cubicweb.utils import make_uid
+from cubicweb.selectors import implements, adaptable
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+from cubicweb.web import json
 from cubicweb.interfaces import ITree
-from cubicweb.selectors import implements
-from cubicweb.view import EntityView
-from cubicweb.web import json
+from cubicweb.web.views import baseviews
 
 def treecookiename(treeid):
     return str('%s-treestate' % treeid)
 
+
+class ITreeAdapter(EntityAdapter):
+    """This adapter has to be overriden to be configured using the
+    tree_relation, child_role and parent_role class attributes to
+    benefit from this default implementation
+    """
+    __regid__ = 'ITree'
+    __select__ = implements(ITree) # XXX for bw compat, else should be abstract
+
+    tree_relation = None
+    child_role = 'subject'
+    parent_role = 'object'
+
+    @implements_adapter_compat('ITree')
+    def children_rql(self):
+        """returns RQL to get children
+
+        XXX should be removed from the public interface
+        """
+        return self.entity.related_rql(self.tree_relation, self.parent_role)
+
+    @implements_adapter_compat('ITree')
+    def different_type_children(self, entities=True):
+        """return children entities of different type as this entity.
+
+        according to the `entities` parameter, return entity objects or the
+        equivalent result set
+        """
+        res = self.entity.related(self.tree_relation, self.parent_role,
+                                  entities=entities)
+        eschema = self.entity.e_schema
+        if entities:
+            return [e for e in res if e.e_schema != eschema]
+        return res.filtered_rset(lambda x: x.e_schema != eschema, self.entity.cw_col)
+
+    @implements_adapter_compat('ITree')
+    def same_type_children(self, entities=True):
+        """return children entities of the same type as this entity.
+
+        according to the `entities` parameter, return entity objects or the
+        equivalent result set
+        """
+        res = self.entity.related(self.tree_relation, self.parent_role,
+                                  entities=entities)
+        eschema = self.entity.e_schema
+        if entities:
+            return [e for e in res if e.e_schema == eschema]
+        return res.filtered_rset(lambda x: x.e_schema is eschema, self.entity.cw_col)
+
+    @implements_adapter_compat('ITree')
+    def is_leaf(self):
+        """returns true if this node as no child"""
+        return len(self.children()) == 0
+
+    @implements_adapter_compat('ITree')
+    def is_root(self):
+        """returns true if this node has no parent"""
+        return self.parent() is None
+
+    @implements_adapter_compat('ITree')
+    def root(self):
+        """return the root object"""
+        return self._cw.entity_from_eid(self.path()[0])
+
+    @implements_adapter_compat('ITree')
+    def parent(self):
+        """return the parent entity if any, else None (e.g. if we are on the
+        root)
+        """
+        try:
+            return self.entity.related(self.tree_relation, self.child_role,
+                                       entities=True)[0]
+        except (KeyError, IndexError):
+            return None
+
+    @implements_adapter_compat('ITree')
+    def children(self, entities=True, sametype=False):
+        """return children entities
+
+        according to the `entities` parameter, return entity objects or the
+        equivalent result set
+        """
+        if sametype:
+            return self.same_type_children(entities)
+        else:
+            return self.entity.related(self.tree_relation, self.parent_role,
+                                       entities=entities)
+
+    @implements_adapter_compat('ITree')
+    def iterparents(self, strict=True):
+        def _uptoroot(self):
+            curr = self
+            while True:
+                curr = curr.parent()
+                if curr is None:
+                    break
+                yield curr
+                curr = curr.cw_adapt_to('ITree')
+        if not strict:
+            return chain([self.entity], _uptoroot(self))
+        return _uptoroot(self)
+
+    @implements_adapter_compat('ITree')
+    def iterchildren(self, _done=None):
+        """iterates over the item's children"""
+        if _done is None:
+            _done = set()
+        for child in self.children():
+            if child.eid in _done:
+                self.error('loop in %s tree', child.__regid__.lower())
+                continue
+            yield child
+            _done.add(child.eid)
+
+    @implements_adapter_compat('ITree')
+    def prefixiter(self, _done=None):
+        if _done is None:
+            _done = set()
+        if self.entity.eid in _done:
+            return
+        _done.add(self.entity.eid)
+        yield self.entity
+        for child in self.same_type_children():
+            for entity in child.cw_adapt_to('ITree').prefixiter(_done):
+                yield entity
+
+    @cached
+    @implements_adapter_compat('ITree')
+    def path(self):
+        """returns the list of eids from the root object to this object"""
+        path = []
+        adapter = self
+        entity = adapter.entity
+        while entity is not None:
+            if entity.eid in path:
+                self.error('loop in %s tree', entity.__regid__.lower())
+                break
+            path.append(entity.eid)
+            try:
+                # check we are not jumping to another tree
+                if (adapter.tree_relation != self.tree_relation or
+                    adapter.child_role != self.child_role):
+                    break
+                entity = adapter.parent()
+                adapter = entity.cw_adapt_to('ITree')
+            except AttributeError:
+                break
+        path.reverse()
+        return path
+
+
+def _done_init(done, view, row, col):
+    """handle an infinite recursion safety belt"""
+    if done is None:
+        done = set()
+    entity = view.cw_rset.get_entity(row, col)
+    if entity.eid in done:
+        msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % {
+            'rel': entity.tree_attribute,
+            'eid': entity.eid
+            }
+        return None, msg
+    done.add(entity.eid)
+    return done, entity
+
+
+class BaseTreeView(baseviews.ListView):
+    """base tree view"""
+    __regid__ = 'tree'
+    __select__ = adaptable('ITree')
+    item_vid = 'treeitem'
+
+    def call(self, done=None, **kwargs):
+        if done is None:
+            done = set()
+        super(TreeViewMixIn, self).call(done=done, **kwargs)
+
+    def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
+        done, entity = _done_init(done, self, row, col)
+        if done is None:
+            # entity is actually an error message
+            self.w(u'<li class="badcontent">%s</li>' % entity)
+            return
+        self.open_item(entity)
+        entity.view(vid or self.item_vid, w=self.w, **kwargs)
+        relatedrset = entity.cw_adapt_to('ITree').children(entities=False)
+        self.wview(self.__regid__, relatedrset, 'null', done=done, **kwargs)
+        self.close_item(entity)
+
+    def open_item(self, entity):
+        self.w(u'<li class="%s">\n' % entity.__regid__.lower())
+    def close_item(self, entity):
+        self.w(u'</li>\n')
+
+
+
+class TreePathView(EntityView):
+    """a recursive path view"""
+    __regid__ = 'path'
+    __select__ = adaptable('ITree')
+    item_vid = 'oneline'
+    separator = u'&#160;&gt;&#160;'
+
+    def call(self, **kwargs):
+        self.w(u'<div class="pathbar">')
+        super(TreePathMixIn, self).call(**kwargs)
+        self.w(u'</div>')
+
+    def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
+        done, entity = _done_init(done, self, row, col)
+        if done is None:
+            # entity is actually an error message
+            self.w(u'<span class="badcontent">%s</span>' % entity)
+            return
+        parent = entity.cw_adapt_to('ITree').parent_entity()
+        if parent:
+            parent.view(self.__regid__, w=self.w, done=done)
+            self.w(self.separator)
+        entity.view(vid or self.item_vid, w=self.w)
+
+
+# XXX rename regid to ajaxtree/foldabletree or something like that (same for
+# treeitemview)
 class TreeView(EntityView):
+    """ajax tree view, click to expand folder"""
+
     __regid__ = 'treeview'
     itemvid = 'treeitemview'
     subvid = 'oneline'
@@ -112,7 +342,7 @@
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
-        if ITree.is_implemented_by(entity.__class__) and not entity.is_leaf():
+        if entity.cw_adapt_to('ITree') and not entity.is_leaf():
             self.w(u'<div class="folder">%s</div>\n' % entity.view('oneline'))
         else:
             # XXX define specific CSS classes according to mime types
@@ -120,7 +350,7 @@
 
 
 class DefaultTreeViewItemView(EntityView):
-    """default treeitem view for entities which don't implement ITree"""
+    """default treeitem view for entities which don't adapt to ITree"""
     __regid__ = 'treeitemview'
 
     def cell_call(self, row, col, vid='oneline', treeid=None, **morekwargs):
@@ -131,12 +361,12 @@
 
 
 class TreeViewItemView(EntityView):
-    """specific treeitem view for entities which implement ITree
+    """specific treeitem view for entities which adapt to ITree
 
     (each item should be expandable if it's not a tree leaf)
     """
     __regid__ = 'treeitemview'
-    __select__ = implements(ITree)
+    __select__ = adaptable('ITree')
     default_branch_state_is_open = False
 
     def open_state(self, eeid, treeid):
@@ -150,15 +380,16 @@
                   is_last=False, **morekwargs):
         w = self.w
         entity = self.cw_rset.get_entity(row, col)
+        itree = entity.cw_adapt_to('ITree')
         liclasses = []
         is_open = self.open_state(entity.eid, treeid)
-        is_leaf = not hasattr(entity, 'is_leaf') or entity.is_leaf()
+        is_leaf = not hasattr(entity, 'is_leaf') or itree.is_leaf()
         if is_leaf:
             if is_last:
                 liclasses.append('last')
             w(u'<li class="%s">' % u' '.join(liclasses))
         else:
-            rql = entity.children_rql() % {'x': entity.eid}
+            rql = itree.children_rql() % {'x': entity.eid}
             url = xml_escape(self._cw.build_url('json', rql=rql, vid=parentvid,
                                                 pageid=self._cw.pageid,
                                                 treeid=treeid,
@@ -197,7 +428,7 @@
         # the local node info
         self.wview(vid, self.cw_rset, row=row, col=col, **morekwargs)
         if is_open and not is_leaf: #  => rql is defined
-            self.wview(parentvid, entity.children(entities=False), subvid=vid,
+            self.wview(parentvid, itree.children(entities=False), subvid=vid,
                        treeid=treeid, initial_load=False, **morekwargs)
         w(u'</li>')
 
--- a/web/views/workflow.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/workflow.py	Thu May 20 20:47:55 2010 +0200
@@ -33,13 +33,13 @@
 from cubicweb import Unauthorized, view
 from cubicweb.selectors import (implements, has_related_entities, one_line_rset,
                                 relation_possible, match_form_params,
-                                implements, score_entity)
-from cubicweb.interfaces import IWorkflowable
+                                implements, score_entity, adaptable)
 from cubicweb.view import EntityView
 from cubicweb.schema import display_name
 from cubicweb.web import uicfg, stdmsgs, action, component, form, action
 from cubicweb.web import formfields as ff, formwidgets as fwdgs
-from cubicweb.web.views import TmpFileViewMixin, forms, primary, autoform
+from cubicweb.web.views import TmpFileViewMixin
+from cubicweb.web.views import forms, primary, autoform, ibreadcrumbs
 from cubicweb.web.views.tabs import TabbedPrimaryView, PrimaryTab
 
 _pvs = uicfg.primaryview_section
@@ -89,8 +89,9 @@
 class ChangeStateFormView(form.FormViewMixIn, view.EntityView):
     __regid__ = 'statuschange'
     title = _('status change')
-    __select__ = (one_line_rset() & implements(IWorkflowable)
-                  & match_form_params('treid'))
+    __select__ = (one_line_rset()
+                  & match_form_params('treid')
+                  & adaptable('IWorkflowable'))
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
@@ -99,7 +100,7 @@
         self.w(u'<h4>%s %s</h4>\n' % (self._cw._(transition.name),
                                       entity.view('oneline')))
         msg = self._cw._('status will change from %(st1)s to %(st2)s') % {
-            'st1': entity.printable_state,
+            'st1': entity.cw_adapt_to('IWorkflowable').printable_state,
             'st2': self._cw._(transition.destination(entity).name)}
         self.w(u'<p>%s</p>\n' % msg)
         self.w(form.render())
@@ -128,7 +129,7 @@
 class WFHistoryView(EntityView):
     __regid__ = 'wfhistory'
     __select__ = relation_possible('wf_info_for', role='object') & \
-                 score_entity(lambda x: x.workflow_history)
+                 score_entity(lambda x: x.cw_adapt_to('IWorkflowable').workflow_history)
 
     title = _('Workflow history')
 
@@ -183,22 +184,24 @@
 
     def fill_menu(self, box, menu):
         entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
-        menu.label = u'%s: %s' % (self._cw._('state'), entity.printable_state)
+        menu.label = u'%s: %s' % (self._cw._('state'),
+                                  entity.cw_adapt_to('IWorkflowable').printable_state)
         menu.append_anyway = True
         super(WorkflowActions, self).fill_menu(box, menu)
 
     def actual_actions(self):
         entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
+        iworkflowable = entity.cw_adapt_to('IWorkflowable')
         hastr = False
-        for tr in entity.possible_transitions():
+        for tr in iworkflowable.possible_transitions():
             url = entity.absolute_url(vid='statuschange', treid=tr.eid)
             yield self.build_action(self._cw._(tr.name), url)
             hastr = True
         # don't propose to see wf if user can't pass any transition
         if hastr:
-            wfurl = entity.current_workflow.absolute_url()
+            wfurl = iworkflowable.current_workflow.absolute_url()
             yield self.build_action(self._cw._('view workflow'), wfurl)
-        if entity.workflow_history:
+        if iworkflowable.workflow_history:
             wfurl = entity.absolute_url(vid='wfhistory')
             yield self.build_action(self._cw._('view history'), wfurl)
 
@@ -346,6 +349,27 @@
                                                    'allowed_transition')
         return []
 
+class WorkflowIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = implements('Workflow')
+    # XXX what if workflow of multiple types?
+    def parent_entity(self):
+        return self.entity.workflow_of and self.entity.workflow_of[0] or None
+
+class WorkflowItemIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = implements('BaseTransition', 'State')
+    def parent_entity(self):
+        return self.entity.workflow
+
+class TransitionItemIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = implements('SubWorkflowExitPoint')
+    def parent_entity(self):
+        return self.entity.reverse_subworkflow_exit[0]
+
+class TrInfoIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+    __select__ = implements('TrInfo')
+    def parent_entity(self):
+        return self.entity.for_entity
+
 
 # workflow images ##############################################################
 
--- a/web/views/xmlrss.py	Thu May 20 20:47:13 2010 +0200
+++ b/web/views/xmlrss.py	Thu May 20 20:47:55 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""base xml and rss views
+"""base xml and rss views"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
@@ -25,8 +24,10 @@
 
 from logilab.mtconverter import xml_escape
 
-from cubicweb.selectors import non_final_entity, one_line_rset, appobject_selectable
-from cubicweb.view import EntityView, AnyRsetView, Component
+from cubicweb.selectors import (implements, non_final_entity, one_line_rset,
+                                appobject_selectable, adaptable)
+from cubicweb.view import EntityView, EntityAdapter, AnyRsetView, Component
+from cubicweb.view import implements_adapter_compat
 from cubicweb.uilib import simple_sgml_tag
 from cubicweb.web import httpcache, box
 
@@ -120,6 +121,16 @@
 
 # RSS stuff ###################################################################
 
+class IFeedAdapter(EntityAdapter):
+    __regid__ = 'IFeed'
+    __select__ = implements('Any')
+
+    @implements_adapter_compat('IFeed')
+    def rss_feed_url(self):
+        """return an url to the rss feed for this entity"""
+        return self.absolute_url(vid='rss')
+
+
 class RSSFeedURL(Component):
     __regid__ = 'rss_feed_url'
     __select__ = non_final_entity()
@@ -130,10 +141,11 @@
 
 class RSSEntityFeedURL(Component):
     __regid__ = 'rss_feed_url'
-    __select__ = non_final_entity() & one_line_rset()
+    __select__ = one_line_rset() & adaptable('IFeed')
 
     def feed_url(self):
-        return self.cw_rset.get_entity(0, 0).rss_feed_url()
+        entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
+        return entity.cw_adapt_to('IFeed').rss_feed_url()
 
 
 class RSSIconBox(box.BoxTemplate):