Automated merge with ssh://centaurus//home/src/public/cubes/keyword
authorJulien Jehannet <julien.jehannet@logilab.fr>
Fri, 06 Nov 2009 20:24:54 +0100
changeset 83 1aa97dd2a2fb
parent 68 3ad8c609eacf (current diff)
parent 82 44b0092247ec (diff)
child 85 a022d5899f19
Automated merge with ssh://centaurus//home/src/public/cubes/keyword
test/unittest_classification.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgtags	Fri Nov 06 20:24:54 2009 +0100
@@ -0,0 +1,6 @@
+f0d826723756ab6a66333dfba616ee464000b72d cubicweb-keyword-version-1.4.0
+42376cba6e5f3e312ccc4846aba832155a9adfb9 cubicweb-keyword-debian-version-1.4.0-1
+f0d826723756ab6a66333dfba616ee464000b72d cubicweb-keyword-version-1.4.0
+cf7bb5b6fba49d35e453f033fbe1607d7fcf9a6f cubicweb-keyword-version-1.4.0
+42376cba6e5f3e312ccc4846aba832155a9adfb9 cubicweb-keyword-debian-version-1.4.0-1
+d3db00ada04f55086064650d52e48d1dc5afe0dc cubicweb-keyword-debian-version-1.4.0-1
--- a/debian/changelog	Thu Nov 05 16:14:43 2009 +0100
+++ b/debian/changelog	Fri Nov 06 20:24:54 2009 +0100
@@ -2,7 +2,7 @@
 
   * new upstream release
 
- --
+ -- Julien Jehannet <julien.jehannet@logilab.fr>  Fri, 23 Oct 2009 13:42:54 +0200
 
 cubicweb-keyword (1.3.0-1) unstable; urgency=low
 
--- a/entities.py	Thu Nov 05 16:14:43 2009 +0100
+++ b/entities.py	Fri Nov 06 20:24:54 2009 +0100
@@ -24,6 +24,10 @@
         """returns the parent entity"""
         return None
 
+    def iterparents(self):
+        """returns parent entities"""
+        yield self
+
     def children(self, entities=True):
         """returns the item's children"""
         return self.related('included_in', 'object', entities=entities)
@@ -55,18 +59,64 @@
 
     @property
     def classification(self):
-        return self.included_in[0]
+        if self.included_in:
+            return self.included_in[0]
+        return None
 
     def parent(self):
+        """IBreadcrumbs implementation"""
         if self.subkeyword_of:
             return self.subkeyword_of[0]
         return self.classification
 
+    def iterparents(self):
+        """returns parent keyword entities
+        """
+        if self.subkeyword_of:
+            parent = self.subkeyword_of[0]
+            while parent is not None:
+                yield parent
+                if parent.subkeyword_of:
+                    parent = parent.subkeyword_of[0]
+                else:
+                    parent = None
+
+    def children(self, entities=True):
+        """returns the item's children
+
+        we have only one direct child by ``subkeyword_of`` relation"""
+        assert 1 == len(self.reverse_subkeyword_of)
+        return iter(self.reverse_subkeyword_of)
+
+    def iterchildren(self):
+        """returns children entities"""
+        if self.reverse_subkeyword_of:
+            child = self.reverse_subkeyword_of[0]
+            while child is not None:
+                yield child
+                if child.reverse_subkeyword_of:
+                    child = child.reverse_subkeyword_of[0]
+                else:
+                    child = None
+
     def is_leaf(self):
         if self.reverse_subkeyword_of:
             return False
         return True
 
+    def children_rql(self):
+        return 'Any K WHERE K is Keyword, K subkeyword_of X, X eid %(x)s'
+
+    """
+    # FIXME unittest
+    def subkeywords(self, recursive=True):
+        rset = self.req.execute(self.children_rql(), {'x': self.eid})
+        subentities = list(rset.entities())
+        if recursive:
+            for entity in subentities[:]:
+                subentities.extend(entity.subkeywords(recursive=True))
+        return subentities
+    """
 
 class CodeKeyword(Keyword):
     id = 'CodeKeyword'
--- a/hooks.py	Thu Nov 05 16:14:43 2009 +0100
+++ b/hooks.py	Fri Nov 06 20:24:54 2009 +0100
@@ -4,13 +4,54 @@
 :copyright: 2007-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 """
-from logilab.common.decorators import cached
-
 from cubicweb import ValidationError
-from cubicweb.selectors import implements
 from cubicweb.server.hooksmanager import Hook
 from cubicweb.server.pool import PreCommitOperation
-from cubicweb.sobjects.notification import NotificationView
+from itertools import chain
+
+
+class SetDescendantOfKeywordOp(PreCommitOperation):
+    def precommit_event(self):
+        """transitive closure of ``descendant_of`` relations to current entity"""
+        closure = set()
+        entity = self.entity
+        parent = self.parent
+        for parent in chain([parent, entity], parent.iterparents()):
+            for child in chain([entity], entity.iterchildren()):
+                if child.eid != parent.eid:
+                    closure.add((child, parent))
+        for child, parent in closure:
+            if not parent in child.descendant_of:
+                self.session.execute('SET C descendant_of P WHERE C eid %(c)s, P eid %(p)s',
+                                     {'c': child.eid, 'p':parent.eid})
+
+
+class BeforeAddDescendantOf(Hook):
+    """check indirect cycle for ``descendant_of`` relation
+    """
+    events = ('before_add_relation', )
+    accepts = ('descendant_of',)
+
+    def call(self, session, fromeid, rtype, toeid):
+        entity = session.entity_from_eid(fromeid)
+        parent = session.entity_from_eid(toeid)
+        parents = set([x.eid for x in chain([parent,], parent.iterparents())])
+        children = set([x.eid for x in chain([entity], entity.iterchildren())])
+        if children & parents:
+            msg = _('detected descendant_of cycle')
+            raise ValidationError(fromeid, {'descendant_of': msg})
+
+
+class AfterAddSubKeywordOf(Hook):
+    """sets ``descendant_of`` relation
+    """
+    events = ('after_add_relation', )
+    accepts = ('subkeyword_of',)
+
+    def call(self, session, fromeid, rtype, toeid):
+        entity = session.entity_from_eid(fromeid)
+        parent = session.entity_from_eid(toeid)
+        SetDescendantOfKeywordOp(session, parent=parent, entity=entity)
 
 
 class SetIncludedInRelationOp(PreCommitOperation):
@@ -54,50 +95,23 @@
         # immediate test direct cycles
         if fromeid == toeid:
             msg = session._('keyword cannot be subkeyword of himself')
-            raise ValidationError(fromeid, {'subkeyword_of' : msg})
+            raise ValidationError(fromeid, {rtype : msg})
         SetIncludedInRelationOp(session, vreg=self.vreg,
                                 fromeid=fromeid, toeid=toeid)
 
 
-class KeywordNotificationView(NotificationView):
-    __select__ = implements('Keyword')
-    msgid_timestamp = True
+class RemoveDescendantOfRelation(Hook):
+    """removes ``descendant_of`` relation
 
-    def recipients(self):
-        """Returns the project's interested people (entities)"""
-        creator = self.entity(0).created_by[0]
-        if not creator.is_in_group('managers') and creator.primary_email:
-            return [(creator.primary_email[0].address, 'fr')]
-        return []
+    we delete the relation for entity's parents recursively
+    """
+    events = ('after_delete_relation',)
+    accepts = ('subkeyword_of',)
 
-    def context(self, **kwargs):
-        context = NotificationView.context(self, **kwargs)
-        entity = self.entity(0)
-        context['kw'] = entity.name
-        return context
+    def call(self, session, fromeid, rtype, toeid):
+        parent = session.entity_from_eid(toeid)
+        for parent in chain([parent], parent.iterparents()):
+            session.execute('DELETE K descendant_of P WHERE K eid %(k)s, '
+                            'P eid %(p)s', {'p':parent.eid, 'k': fromeid})
 
 
-class KeywordNameChanged(KeywordNotificationView):
-    id = 'notif_after_update_entity'
-
-    content = _("keyword name changed from %(oldname)s to %(kw)s")
-
-    @cached
-    def get_oldname(self, entity):
-        session = self.req
-        try:
-            return session.execute('Any N WHERE X eid %(x)s, X name N',
-                                   {'x' : entity.eid}, 'x')[0][0]
-        except IndexError:
-            return u'?'
-
-    def context(self, **kwargs):
-        entity = self.entity(0)
-        context = KeywordNotificationView.context(self, **kwargs)
-        context['oldname'] = self.get_oldname(entity)
-        return context
-
-    def subject(self):
-        entity = self.entity(0)
-        return self.req._('keyword name changed from %s to %s') % (
-            self.get_oldname(entity), entity.name)
--- a/i18n/en.po	Thu Nov 05 16:14:43 2009 +0100
+++ b/i18n/en.po	Fri Nov 06 20:24:54 2009 +0100
@@ -75,15 +75,6 @@
 msgid "add Keyword subkeyword_of Keyword object"
 msgstr "subkeyword"
 
-msgid "add a Classification"
-msgstr "add a classification"
-
-msgid "add a CodeKeyword"
-msgstr ""
-
-msgid "add a Keyword"
-msgstr "add a keyword"
-
 msgid "add keywords"
 msgstr ""
 
@@ -101,12 +92,24 @@
 msgid "classifies"
 msgstr ""
 
+msgctxt "Classification"
+msgid "classifies"
+msgstr ""
+
+msgctxt "CWEType"
+msgid "classifies_object"
+msgstr ""
+
 msgid "classifies_object"
 msgstr "classified by"
 
 msgid "code"
 msgstr ""
 
+msgctxt "CodeKeyword"
+msgid "code"
+msgstr ""
+
 msgid "contentnavigation_addkeywords"
 msgstr "add keywords"
 
@@ -132,6 +135,35 @@
 msgid "creating Keyword (Keyword subkeyword_of Keyword %(linkto)s)"
 msgstr "creating subkeyword of %(linkto)s"
 
+msgid "descendant_of"
+msgstr ""
+
+msgctxt "Keyword"
+msgid "descendant_of"
+msgstr ""
+
+msgctxt "CodeKeyword"
+msgid "descendant_of"
+msgstr ""
+
+msgctxt "Classification"
+msgid "descendant_of_object"
+msgstr ""
+
+msgctxt "Keyword"
+msgid "descendant_of_object"
+msgstr ""
+
+msgctxt "CodeKeyword"
+msgid "descendant_of_object"
+msgstr ""
+
+msgid "descendant_of_object"
+msgstr ""
+
+msgid "detected descendant_of cycle"
+msgstr ""
+
 msgid "detected subkeyword cycle"
 msgstr ""
 
@@ -149,6 +181,18 @@
 msgid "included_in"
 msgstr "included in"
 
+msgctxt "Keyword"
+msgid "included_in"
+msgstr ""
+
+msgctxt "CodeKeyword"
+msgid "included_in"
+msgstr ""
+
+msgctxt "Classification"
+msgid "included_in_object"
+msgstr ""
+
 msgid "included_in_object"
 msgstr "includes"
 
@@ -169,26 +213,54 @@
 msgid "keywords:"
 msgstr ""
 
-msgid "remove this Classification"
-msgstr "remove this classification"
-
-msgid "remove this CodeKeyword"
+msgctxt "Classification"
+msgid "name"
 msgstr ""
 
-msgid "remove this Keyword"
-msgstr "remove this keyword"
+msgctxt "Keyword"
+msgid "name"
+msgstr ""
+
+msgctxt "CodeKeyword"
+msgid "name"
+msgstr ""
 
 msgid "subkeyword_of"
 msgstr "subkeyword of"
 
+msgctxt "Keyword"
+msgid "subkeyword_of"
+msgstr ""
+
+msgctxt "CodeKeyword"
+msgid "subkeyword_of"
+msgstr ""
+
+msgctxt "Keyword"
+msgid "subkeyword_of_object"
+msgstr ""
+
+msgctxt "CodeKeyword"
+msgid "subkeyword_of_object"
+msgstr ""
+
 msgid "subkeyword_of_object"
 msgstr "parent keyword of"
 
 msgid "tagged objects"
 msgstr ""
 
-msgid "this keyword can't be applied to this kind of entity"
+msgid "which keyword (if any) this keyword specializes"
 msgstr ""
 
-msgid "which keyword (if any) this keyword specializes"
-msgstr ""
+#~ msgid "add a Classification"
+#~ msgstr "add a classification"
+
+#~ msgid "add a Keyword"
+#~ msgstr "add a keyword"
+
+#~ msgid "remove this Classification"
+#~ msgstr "remove this classification"
+
+#~ msgid "remove this Keyword"
+#~ msgstr "remove this keyword"
--- a/i18n/fr.po	Thu Nov 05 16:14:43 2009 +0100
+++ b/i18n/fr.po	Fri Nov 06 20:24:54 2009 +0100
@@ -5,7 +5,7 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: cubicweb-classification-schemes 0.11.0\n"
-"PO-Revision-Date: 2009-09-17 10:31+0200\n"
+"PO-Revision-Date: 2009-10-23 14:43+0200\n"
 "Last-Translator: Logilab Team <contact@logilab.fr>\n"
 "Language-Team: fr <contact@logilab.fr>\n"
 "MIME-Version: 1.0\n"
@@ -83,15 +83,6 @@
 msgid "add Keyword subkeyword_of Keyword object"
 msgstr "ajouter un sous mot-clé"
 
-msgid "add a Classification"
-msgstr "ajouter un plan de classement"
-
-msgid "add a CodeKeyword"
-msgstr "ajouter un code de mot-clé"
-
-msgid "add a Keyword"
-msgstr "ajouter un mot-clé"
-
 msgid "add keywords"
 msgstr "ajouter les mot-clés"
 
@@ -109,11 +100,23 @@
 msgid "classifies"
 msgstr "classe"
 
+msgctxt "Classification"
+msgid "classifies"
+msgstr "classfie"
+
+msgctxt "CWEType"
+msgid "classifies_object"
+msgstr "classifié par"
+
 msgid "classifies_object"
 msgstr "classifié par"
 
 msgid "code"
-msgstr ""
+msgstr "code"
+
+msgctxt "CodeKeyword"
+msgid "code"
+msgstr "code"
 
 msgid "contentnavigation_addkeywords"
 msgstr "ajout de mots-clés"
@@ -140,6 +143,35 @@
 msgid "creating Keyword (Keyword subkeyword_of Keyword %(linkto)s)"
 msgstr "création d'un sous mot-clé de %(linkto)s"
 
+msgid "descendant_of"
+msgstr "descendant de"
+
+msgctxt "Keyword"
+msgid "descendant_of"
+msgstr "descendant de"
+
+msgctxt "CodeKeyword"
+msgid "descendant_of"
+msgstr "descendant de"
+
+msgctxt "Classification"
+msgid "descendant_of_object"
+msgstr "est parent de"
+
+msgctxt "Keyword"
+msgid "descendant_of_object"
+msgstr "est parent de"
+
+msgctxt "CodeKeyword"
+msgid "descendant_of_object"
+msgstr "est parent de"
+
+msgid "descendant_of_object"
+msgstr "est parent de"
+
+msgid "detected descendant_of cycle"
+msgstr "cycle de dépendance détecté"
+
 msgid "detected subkeyword cycle"
 msgstr "un cycle de mots-clé a été détecté"
 
@@ -157,8 +189,20 @@
 msgid "included_in"
 msgstr "inclus dans"
 
+msgctxt "Keyword"
+msgid "included_in"
+msgstr "inclus dans"
+
+msgctxt "CodeKeyword"
+msgid "included_in"
+msgstr "inclus dans"
+
+msgctxt "Classification"
 msgid "included_in_object"
-msgstr "inclus"
+msgstr "inclut"
+
+msgid "included_in_object"
+msgstr "inclut"
 
 msgid "keyword cannot be subkeyword of himself"
 msgstr "un mot-clé ne peut être un sous mot-clé de lui-même"
@@ -177,26 +221,42 @@
 msgid "keywords:"
 msgstr "mot-clés :"
 
-msgid "remove this Classification"
-msgstr "supprimer ce plan de classement"
+msgctxt "Classification"
+msgid "name"
+msgstr "nom"
 
-msgid "remove this CodeKeyword"
-msgstr "supprimer ce code de mot-clé"
+msgctxt "Keyword"
+msgid "name"
+msgstr "nom"
 
-msgid "remove this Keyword"
-msgstr "supprimer ce mot-clé"
+msgctxt "CodeKeyword"
+msgid "name"
+msgstr "nom"
 
 msgid "subkeyword_of"
 msgstr "sous mot-clé de"
 
+msgctxt "Keyword"
+msgid "subkeyword_of"
+msgstr "sous mot-clé de"
+
+msgctxt "CodeKeyword"
+msgid "subkeyword_of"
+msgstr "sous mot-clé de"
+
+msgctxt "Keyword"
 msgid "subkeyword_of_object"
-msgstr "sous mot-clés"
+msgstr "a comme sous mot-clés"
+
+msgctxt "CodeKeyword"
+msgid "subkeyword_of_object"
+msgstr "a comme sous mot-clés"
+
+msgid "subkeyword_of_object"
+msgstr "a comme sous mot-clés"
 
 msgid "tagged objects"
 msgstr "objets classifiés"
 
-msgid "this keyword can't be applied to this kind of entity"
-msgstr "ce mot-clé ne peut être appliqué à ce type d'entité"
-
 msgid "which keyword (if any) this keyword specializes"
 msgstr "le mot-clé que celui-ci spécialise (le cas échéant)"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/migration/1.4.0_Any.py	Fri Nov 06 20:24:54 2009 +0100
@@ -0,0 +1,4 @@
+for entity in ('Keyword', 'CodeKeyword'):
+    add_relation_definition(entity, 'descendant_of', entity)
+    sync_schema_props_perms(entity)
+checkpoint()
--- a/schema.py	Thu Nov 05 16:14:43 2009 +0100
+++ b/schema.py	Fri Nov 06 20:24:54 2009 +0100
@@ -5,7 +5,7 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 """
 from yams.buildobjs import (EntityType, RelationType,
-                            SubjectRelation, String)
+                            SubjectRelation, ObjectRelation, String)
 
 from cubicweb.schema import RQLConstraint, ERQLExpression
 
@@ -48,6 +48,7 @@
                                     description=_('which keyword (if any) this keyword specializes'),
                                     # if no included_in set, it'll be automatically added by a hook
                                     constraints=[RQLConstraint('NOT S included_in CS1 OR EXISTS(S included_in CS2, O included_in CS2)')])
+    descendant_of = SubjectRelation('Keyword')
     included_in = SubjectRelation('Classification', cardinality='1*')
 
 
@@ -57,7 +58,6 @@
     __specializes_schema__ = True
     code = String(required=True, fulltextindexed=True, indexed=True, maxsize=128)
 
-
 class subkeyword_of(RelationType):
     """a keyword can specialize another keyword"""
 
@@ -74,4 +74,3 @@
     """tagged objects"""
     fulltext_container = 'object'
     constraints = [RQLConstraint('S included_in CS, O is ET, CS classifies ET')]
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sobjects.py	Fri Nov 06 20:24:54 2009 +0100
@@ -0,0 +1,56 @@
+"""Notification views for keywords / classification schemes
+
+:organization: Logilab
+:copyright: 2003-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from logilab.common.decorators import cached
+
+from cubicweb.selectors import implements
+from cubicweb.sobjects.notification import NotificationView
+
+
+class KeywordNotificationView(NotificationView):
+    __select__ = implements('Keyword')
+    msgid_timestamp = True
+
+    def recipients(self):
+        """Returns the project's interested people (entities)"""
+        creator = self.entity(0).created_by[0]
+        if not creator.is_in_group('managers') and creator.primary_email:
+            return [(creator.primary_email[0].address, 'fr')]
+        return []
+
+    def context(self, **kwargs):
+        context = NotificationView.context(self, **kwargs)
+        entity = self.entity(0)
+        context['kw'] = entity.name
+        return context
+
+
+class KeywordNameChanged(KeywordNotificationView):
+    id = 'notif_after_update_entity'
+
+    content = _("keyword name changed from %(oldname)s to %(kw)s")
+
+    @cached
+    def get_oldname(self, entity):
+        session = self.req
+        try:
+            return session.execute('Any N WHERE X eid %(x)s, X name N',
+                                   {'x' : entity.eid}, 'x')[0][0]
+        except IndexError:
+            return u'?'
+
+    def context(self, **kwargs):
+        entity = self.entity(0)
+        context = KeywordNotificationView.context(self, **kwargs)
+        context['oldname'] = self.get_oldname(entity)
+        return context
+
+    def subject(self):
+        entity = self.entity(0)
+        return self.req._('keyword name changed from %s to %s') % (
+            self.get_oldname(entity), entity.name)
--- a/test/test_keyword.py	Thu Nov 05 16:14:43 2009 +0100
+++ b/test/test_keyword.py	Fri Nov 06 20:24:54 2009 +0100
@@ -3,6 +3,8 @@
 
 class AutomaticWebTest(AutomaticWebTest):
 
+    ignored_relations = ('descendant_of',)
+
     def to_test_etypes(self):
         # add cwgroup to test keyword components on entity on which they can be applied
         return set(('Classification', 'Keyword', 'CWGroup'))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/unittest_descendant_of.py	Fri Nov 06 20:24:54 2009 +0100
@@ -0,0 +1,152 @@
+from cubicweb.devtools.apptest import EnvBasedTC
+
+from cubicweb.common import ValidationError
+
+
+class KeywordHooksTC(EnvBasedTC):
+
+    def setup_database(self):
+        self.execute('INSERT Classification C: C name "classif1", C classifies ET WHERE ET name "CWGroup"')
+        self.execute('INSERT Classification C: C name "classif2", C classifies ET WHERE ET name "CWUser"')
+        self.kw1 = self.execute('INSERT Keyword K: K name "kwgroup", K included_in C WHERE C name "classif1"')[0][0]
+        self.kw2 = self.execute('INSERT Keyword K: K name "kwuser", K included_in C WHERE C name "classif2"')[0][0]
+
+    def test_keyword_add1(self):
+        self.execute('INSERT Keyword K: K name "kw1", K included_in C WHERE C name "classif1"')
+        self.execute('INSERT Keyword K: K name "kw2", K included_in C, K subkeyword_of KK WHERE C name "classif1", KK name "kw1"')
+        self.execute('INSERT Keyword K: K name "kw3", K included_in C, K subkeyword_of KK WHERE C name "classif1", KK name "kw2"')
+        self.execute('INSERT Keyword K: K name "kw4", K included_in C, K subkeyword_of KK WHERE C name "classif1", KK name "kw3"')
+        self.execute('INSERT Keyword K: K name "kw5", K included_in C, K subkeyword_of KK WHERE C name "classif1", KK name "kw4"')
+        self.commit()
+        parent = self.execute('Any K WHERE K is Keyword, K name "kw1"').get_entity(0, 0)
+        child = self.execute('Any K WHERE K is Keyword, K name "kw5"').get_entity(0, 0)
+        self.assertUnorderedIterableEquals([kw.name for kw in child.iterparents()], ['kw4', 'kw3', 'kw2', 'kw1'])
+        self.assertUnorderedIterableEquals([kw.name for kw in child.descendant_of], ['kw4', 'kw3', 'kw2', 'kw1'])
+        self.assertUnorderedIterableEquals([kw.name for kw in parent.reverse_descendant_of], ['kw5', 'kw4', 'kw3', 'kw2'])
+        self.assertUnorderedIterableEquals([kw.name for kw in parent.iterchildren()], ['kw5', 'kw4', 'kw3', 'kw2'])
+
+    def test_keyword_add2(self):
+        self.execute('INSERT Keyword K: K name "kw1", K included_in C WHERE C name "classif1"')
+        self.execute('INSERT Keyword K: K name "kw2", K included_in C, KK subkeyword_of K WHERE C name "classif1", KK name "kw1"')
+        self.execute('INSERT Keyword K: K name "kw3", K included_in C, KK subkeyword_of K WHERE C name "classif1", KK name "kw2"')
+        self.execute('INSERT Keyword K: K name "kw4", K included_in C, KK subkeyword_of K WHERE C name "classif1", KK name "kw3"')
+        self.execute('INSERT Keyword K: K name "kw5", K included_in C, KK subkeyword_of K WHERE C name "classif1", KK name "kw4"')
+        self.commit()
+        child  = self.execute('Any K WHERE K is Keyword, K name "kw1"').get_entity(0, 0)
+        parent = self.execute('Any K WHERE K is Keyword, K name "kw5"').get_entity(0, 0)
+        self.assertUnorderedIterableEquals([kw.name for kw in child.iterparents()], ['kw2', 'kw3', 'kw4', 'kw5'])
+        self.assertUnorderedIterableEquals([kw.name for kw in child.descendant_of], ['kw2', 'kw3', 'kw4', 'kw5'])
+        self.assertUnorderedIterableEquals([kw.name for kw in parent.reverse_descendant_of], ['kw4', 'kw3', 'kw2', 'kw1'])
+        self.assertUnorderedIterableEquals([kw.name for kw in parent.iterchildren()], ['kw4', 'kw3', 'kw2', 'kw1'])
+
+    def test_keyword_add3(self):
+        self.execute('INSERT Keyword K: K name "kw1", K included_in C WHERE C name "classif1"')
+        self.execute('INSERT Keyword K: K name "kw2", K included_in C, K subkeyword_of KK WHERE C name "classif1", KK name "kw1"')
+        self.execute('INSERT Keyword K: K name "kw3", K included_in C WHERE C name "classif1"')
+        self.execute('INSERT Keyword K: K name "kw4", K included_in C, K subkeyword_of KK WHERE C name "classif1", KK name "kw3"')
+        self.execute('INSERT Keyword K: K name "kw5", K included_in C, K subkeyword_of KK WHERE C name "classif1", KK name "kw4"')
+        self.commit()
+        self.execute('SET K subkeyword_of KK WHERE K name "kw3", KK name "kw2"')
+        self.commit()
+        parent = self.execute('Any K WHERE K is Keyword, K name "kw1"').get_entity(0, 0)
+        child = self.execute('Any K WHERE K is Keyword, K name "kw5"').get_entity(0, 0)
+        self.assertUnorderedIterableEquals([kw.name for kw in child.iterparents()], ['kw4', 'kw3', 'kw2', 'kw1'])
+        self.assertUnorderedIterableEquals([kw.name for kw in child.descendant_of], ['kw4', 'kw3', 'kw2', 'kw1'])
+        self.assertUnorderedIterableEquals([kw.name for kw in parent.reverse_descendant_of], ['kw5', 'kw4', 'kw3', 'kw2'])
+        self.assertUnorderedIterableEquals([kw.name for kw in parent.iterchildren()], ['kw5', 'kw4', 'kw3', 'kw2'])
+
+    def test_keyword_add4(self):
+        self.execute('INSERT Keyword K: K name "kw1", K included_in C WHERE C name "classif1"')
+        self.execute('INSERT Keyword K: K name "kw2", K included_in C, KK subkeyword_of K WHERE C name "classif1", KK name "kw1"')
+        self.execute('INSERT Keyword K: K name "kw3", K included_in C WHERE C name "classif1"')
+        self.execute('INSERT Keyword K: K name "kw4", K included_in C WHERE C name "classif1"')
+        self.execute('INSERT Keyword K: K name "kw5", K included_in C, KK subkeyword_of K WHERE C name "classif1", KK name "kw4"')
+        self.commit()
+        self.execute('SET KK subkeyword_of K WHERE K name "kw3", KK name "kw2"')
+        self.execute('SET KK subkeyword_of K WHERE K name "kw4", KK name "kw3"')
+        self.commit()
+        child  = self.execute('Any K WHERE K is Keyword, K name "kw1"').get_entity(0, 0)
+        parent = self.execute('Any K WHERE K is Keyword, K name "kw5"').get_entity(0, 0)
+        self.assertUnorderedIterableEquals([kw.name for kw in child.descendant_of], ['kw2', 'kw3', 'kw4', 'kw5'])
+        self.assertUnorderedIterableEquals([kw.name for kw in child.iterparents()], ['kw2', 'kw3', 'kw4', 'kw5'])
+        self.assertUnorderedIterableEquals([kw.name for kw in parent.iterchildren()], ['kw4', 'kw3', 'kw2', 'kw1'])
+        self.assertUnorderedIterableEquals([kw.name for kw in parent.reverse_descendant_of], ['kw4', 'kw3', 'kw2', 'kw1'])
+
+    def test_keyword_add5(self):
+        self.execute('INSERT Keyword K: K name "kw0", K included_in C WHERE C name "classif1"')
+        self.execute('INSERT Keyword K: K name "kw1", K included_in C WHERE C name "classif1"')
+        self.execute('INSERT Keyword K: K name "kw2", K included_in C, K subkeyword_of KK WHERE C name "classif1", KK name "kw1"')
+        self.execute('INSERT Keyword K: K name "kw3", K included_in C WHERE C name "classif1"')
+        self.execute('INSERT Keyword K: K name "kw4", K included_in C WHERE C name "classif1"')
+        self.execute('INSERT Keyword K: K name "kw5", K included_in C, K subkeyword_of KK WHERE C name "classif1", KK name "kw4"')
+        self.execute('SET K subkeyword_of KK WHERE K name "kw3", KK name "kw2"')
+        self.commit();
+        keyword = self.execute('Any K WHERE K is Keyword, K name "kw3"').get_entity(0, 0)
+        self.assertUnorderedIterableEquals([kw.name for kw in keyword.descendant_of], ['kw1', 'kw2'])
+        self.execute('SET K descendant_of KK WHERE K name "kw3", KK name "kw0"')
+        self.commit()
+        keyword = self.execute('Any K WHERE K is Keyword, K name "kw3"').get_entity(0, 0)
+        self.assertUnorderedIterableEquals([kw.name for kw in keyword.descendant_of], ['kw0', 'kw1', 'kw2'])
+        self.execute('SET K descendant_of KK WHERE K name "kw3", KK name "kw4"')
+        self.commit()
+        keyword = self.execute('Any K WHERE K is Keyword, K name "kw3"').get_entity(0, 0)
+        self.assertUnorderedIterableEquals([kw.name for kw in keyword.descendant_of], ['kw0', 'kw1', 'kw2', 'kw4'])
+        self.execute('SET K descendant_of KK WHERE K name "kw3", KK name "kw5"')
+        self.commit()
+        keyword = self.execute('Any K WHERE K is Keyword, K name "kw3"').get_entity(0, 0)
+        self.assertUnorderedIterableEquals([kw.name for kw in keyword.descendant_of], ['kw0', 'kw1', 'kw2', 'kw4', 'kw5'])
+
+    def test_keyword_update1(self):
+        self.execute('INSERT Keyword K: K name "kw1", K included_in C WHERE C name "classif1"')
+        self.execute('INSERT Keyword K: K name "kw2", K included_in C, K subkeyword_of KK WHERE C name "classif1", KK name "kw1"')
+        self.execute('INSERT Keyword K: K name "kw3", K included_in C WHERE C name "classif1"')
+        self.execute('INSERT Keyword K: K name "kw4", K included_in C WHERE C name "classif1"')
+        self.execute('INSERT Keyword K: K name "kw5", K included_in C, K subkeyword_of KK WHERE C name "classif1", KK name "kw4"')
+        self.execute('SET K subkeyword_of KK WHERE K name "kw3", KK name "kw2"')
+        self.commit();
+        keyword = self.execute('Any K WHERE K is Keyword, K name "kw3"').get_entity(0, 0)
+        self.assertUnorderedIterableEquals([kw.name for kw in keyword.descendant_of], ['kw1', 'kw2'])
+        self.execute('SET K subkeyword_of KK WHERE K name "kw3", KK name "kw4"')
+        self.commit()
+        keyword = self.execute('Any K WHERE K is Keyword, K name "kw3"').get_entity(0, 0)
+        self.assertUnorderedIterableEquals([kw.name for kw in keyword.descendant_of], ['kw4'])
+        self.execute('SET K subkeyword_of KK WHERE K name "kw3", KK name "kw5"')
+        self.commit()
+        keyword = self.execute('Any K WHERE K is Keyword, K name "kw3"').get_entity(0, 0)
+        self.assertUnorderedIterableEquals([kw.name for kw in keyword.descendant_of], ['kw4', 'kw5'])
+
+    def test_keyword_delete(self):
+        """*after_delete_relation* of ``subkeyword_of``
+        """
+        self.execute('INSERT Keyword K: K name "kw1", K included_in C WHERE C name "classif1"')
+        self.execute('INSERT Keyword K: K name "kw2", K included_in C, K subkeyword_of KK WHERE C name "classif1", KK name "kw1"')
+        self.execute('INSERT Keyword K: K name "kw3", K included_in C, K subkeyword_of KK WHERE C name "classif1", KK name "kw2"')
+        self.execute('INSERT Keyword K: K name "kw4", K included_in C, K subkeyword_of KK WHERE C name "classif1", KK name "kw3"')
+        self.execute('INSERT Keyword K: K name "kw5", K included_in C, K subkeyword_of KK WHERE C name "classif1", KK name "kw4"')
+        self.commit()
+        self.execute('DELETE K subkeyword_of KK WHERE K is Keyword, K name "kw3"')
+        self.commit()
+        keyword = self.execute('Any K WHERE K is Keyword, K name "kw3"').get_entity(0, 0)
+        self.assertUnorderedIterableEquals([kw.name for kw in keyword.iterparents()], [])
+        self.assertUnorderedIterableEquals([kw.name for kw in keyword.descendant_of], [])
+        self.assertUnorderedIterableEquals([kw.name for kw in keyword.reverse_descendant_of], ['kw5', 'kw4'])
+        self.assertUnorderedIterableEquals([kw.name for kw in keyword.iterchildren()], ['kw5', 'kw4'])
+
+    def test_no_add_descendant_cycle(self):
+        """no ``descendant_of`` cycle"""
+        self.execute('INSERT Keyword K: K name "kw1", K included_in C WHERE C name "classif1"')
+        self.execute('INSERT Keyword K: K name "kw2", K included_in C, K subkeyword_of KK WHERE C name "classif1", KK name "kw1"')
+        self.execute('INSERT Keyword K: K name "kw3", K included_in C, K subkeyword_of KK WHERE C name "classif1", KK name "kw2"')
+        self.commit()
+        rql = 'SET K descendant_of KK WHERE K name "kw1", KK name "kw3"'
+        self.assertRaises(ValidationError, self.execute, rql)
+        self.execute('INSERT Keyword K: K name "kw4", K included_in C WHERE C name "classif1"')
+        self.execute('INSERT Keyword K: K name "kw5", K included_in C, K subkeyword_of KK WHERE C name "classif1", KK name "kw4"')
+        self.commit()
+        rql = 'SET K descendant_of KK WHERE K name "kw4", KK name "kw5"'
+        self.assertRaises(ValidationError, self.execute, rql)
+
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
--- a/views.py	Thu Nov 05 16:14:43 2009 +0100
+++ b/views.py	Fri Nov 06 20:24:54 2009 +0100
@@ -14,7 +14,8 @@
 from cubicweb.view import EntityView
 from cubicweb.common.mixins import TreePathMixIn
 from cubicweb.web import stdmsgs, uicfg, component, facet
-from cubicweb.web.views import primary, baseviews, basecontrollers
+from cubicweb.web.views import primary, basecontrollers
+
 
 _pvs = uicfg.primaryview_section
 _pvs.tag_object_of(('*', 'applied_to', '*'), 'hidden')