dev should happen on default branch
authorSamuel Trégouët <samuel.tregouet@logilab.fr>
Wed, 27 Sep 2017 09:08:03 +0200
changeset 187 2e7dc26727f6
parent 132 a5c5d224031c (current diff)
parent 184 cd1c2bf45f5d (diff)
child 188 6fb2d871eb7f
dev should happen on default branch
__init__.py
__pkginfo__.py
data/cubes.keyword.js
entities.py
hooks.py
i18n/en.po
i18n/fr.po
migration/1.0.2_Any.py
migration/1.4.0_Any.py
migration/postcreate.py
schema.py
sobjects.py
views.py
--- a/.hgtags	Wed Mar 05 18:27:01 2014 +0100
+++ b/.hgtags	Wed Sep 27 09:08:03 2017 +0200
@@ -15,3 +15,6 @@
 baf594692e6222789845f973c56a910c62a47e04 cubicweb-keyword-centos-version-1.7.0-1
 baf594692e6222789845f973c56a910c62a47e04 cubicweb-keyword-version-1.7.0
 baf594692e6222789845f973c56a910c62a47e04 cubicweb-keyword-debian-version-1.7.0-1
+9b11d709b6e22b0e5cbb2d626ea55ff04dbd4632 cubicweb-keyword-version-1.7.1
+9b11d709b6e22b0e5cbb2d626ea55ff04dbd4632 cubicweb-keyword-debian-version-1.7.1-1
+9b11d709b6e22b0e5cbb2d626ea55ff04dbd4632 cubicweb-keyword-centos-version-1.7.1-1
--- a/MANIFEST.in	Wed Mar 05 18:27:01 2014 +0100
+++ b/MANIFEST.in	Wed Sep 27 09:08:03 2017 +0200
@@ -1,6 +1,7 @@
-include *.py
-recursive-include data external_resources *.gif *.js
-recursive-include i18n *.pot *.po
-recursive-include migration *.py
-recursive-include test *.py
-recursive-include test/data bootstrap_cubes  *.py
+include setup.py
+include README
+include *.ini
+recursive-include cubicweb_keyword *.py
+recursive-include cubicweb_keyword/data external_resources *.gif *.js
+recursive-include cubicweb_keyword/i18n *.pot *.po
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README	Wed Sep 27 09:08:03 2017 +0200
@@ -0,0 +1,27 @@
+Summary
+-------
+The `keyword` cube provides classification by using hierarchies of keywords to
+classify content.
+
+Each classification is represented using a `Classification` entity, which will
+hold a keywords tree.
+
+There is two types of keywords:
+
+- `Keyword` which contains a description,
+
+- `CodeKeyword` which contains the keyword description and the associated code.
+
+In order to link an entity to a keyword, you have to add a relation
+ `applied_to` in the schema.
+
+Each keyword has the `subkeyword_of` relation definition. This allows to
+navigate in the classification without a Modified Preorder Tree Traversal
+representation of the data.
+
+Some methods are defined in order to get parents and children or get the status
+of a keyword (leaf or root).
+
+See also `cubicweb-tag`_ as another (simpler) way to classify content.
+
+.. _`cubicweb-tag`: http://www.cubicweb.org/project/cubicweb-tag
\ No newline at end of file
--- a/__init__.py	Wed Mar 05 18:27:01 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-"""cubicweb-keyword"""
--- a/__pkginfo__.py	Wed Mar 05 18:27:01 2014 +0100
+++ b/__pkginfo__.py	Wed Sep 27 09:08:03 2017 +0200
@@ -1,88 +1,1 @@
-# pylint: disable-msg=W0622
-"""cubicweb-classification-schemes packaging information"""
-
-modname = 'keyword'
-distname = "cubicweb-keyword"
-
-numversion = (1, 7, 0)
-version = '.'.join(str(num) for num in numversion)
-
-license = 'LGPL'
-copyright = '''Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE).
-http://www.logilab.fr/ -- mailto:contact@logilab.fr'''
-
-author = "Logilab"
-author_email = "contact@logilab.fr"
-web = 'http://www.cubicweb.org/project/%s' % distname
-
-short_desc = "classification schemes system for the Cubicweb framework"
-long_desc = """Summary
--------
-The `keyword` cube provides classification by using hierarchies of keywords to
-classify content.
-
-Each classification is represented using a `Classification` entity, which will
-hold a keywords tree.
-
-There is two types of keywords:
-
-- `Keyword` which contains a description,
-
-- `CodeKeyword` which contains the keyword description and the associated code.
-
-In order to link an entity to a keyword, you have to add a relation `applied_to`
-in the schema.
-
-Each keyword has the `subkeyword_of` relation definition. This allows to
-navigate in the classification without a Modified Preorder Tree Traversal
-representation of the data.
-
-Some methods are defined in order to get parents and children or get the status
-of a keyword (leaf or root).
-
-See also `cubicweb-tag`_ as another (simpler) way to classify content.
-
-.. _`cubicweb-tag`: http://www.cubicweb.org/project/cubicweb-tag
-"""
-
-classifiers = [
-    'Environment :: Web Environment',
-    'Framework :: CubicWeb',
-    'Programming Language :: Python',
-    'Programming Language :: JavaScript',
-    ]
-
-__depends_cubes__ = {}
-__depends__ = {'cubicweb': '>= 3.15.0'}
-__use__ = tuple(__depends_cubes__)
-
-from os import listdir as _listdir
-from os.path import join, isdir
-from glob import glob
-
-THIS_CUBE_DIR = join('share', 'cubicweb', 'cubes', modname)
-
-def listdir(dirpath):
-    return [join(dirpath, fname) for fname in _listdir(dirpath)
-            if fname[0] != '.' and not fname.endswith('.pyc')
-            and not fname.endswith('~')
-            and not isdir(join(dirpath, fname))]
-try:
-    data_files = [
-        # common files
-        [THIS_CUBE_DIR, [fname for fname in glob('*.py') if fname != 'setup.py']],
-        [join(THIS_CUBE_DIR, 'test'), [fname for fname in glob('test/*.py')]],
-
-    ]
-    for dname in ('data', 'i18n', 'migration', ):
-        if isdir(dname):
-            data_files.append([join(THIS_CUBE_DIR, dname), listdir(dname)])
-
-    # Note: here, you'll need to add subdirectories if you want
-    # them to be included in the debian package
-    for dname in ('data', ):
-        data_files.append([join(THIS_CUBE_DIR, 'test', dname), listdir(join('test', dname))])
-
-except OSError:
-    # we are in an installed directory
-    pass
+cubicweb_keyword/__pkginfo__.py
\ No newline at end of file
--- a/cubicweb-keyword.spec	Wed Mar 05 18:27:01 2014 +0100
+++ b/cubicweb-keyword.spec	Wed Sep 27 09:08:03 2017 +0200
@@ -9,7 +9,7 @@
 %{!?_python_sitelib: %define _python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")}
 
 Name:           cubicweb-keyword
-Version:        1.7.0
+Version:        1.8.0
 Release:        logilab.1%{?dist}
 Summary:        keyword component for the CubicWeb framework
 Group:          Applications/Internet
@@ -20,7 +20,7 @@
 BuildRoot:      %{_tmppath}/%{name}-%{version}-%{release}-buildroot
 
 BuildRequires:  %{python} %{python}-setuptools
-Requires:       cubicweb >= 3.15.0
+Requires:       cubicweb >= 3.24.0
 
 %description
 keyword component for the CubicWeb framework
@@ -35,14 +35,11 @@
 %endif
 
 %install
-NO_SETUPTOOLS=1 %{__python} setup.py --quiet install --no-compile --prefix=%{_prefix} --root="$RPM_BUILD_ROOT"
-# remove generated .egg-info file
-rm -rf $RPM_BUILD_ROOT/usr/lib/python*
-
+%{__python} setup.py --quiet install --no-compile --prefix=%{_prefix} --root="$RPM_BUILD_ROOT"
 
 %clean
 rm -rf $RPM_BUILD_ROOT
 
 %files
 %defattr(-, root, root)
-/*
+%{_python_sitelib}/*
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb_keyword/__init__.py	Wed Sep 27 09:08:03 2017 +0200
@@ -0,0 +1,1 @@
+"""cubicweb-keyword"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb_keyword/__pkginfo__.py	Wed Sep 27 09:08:03 2017 +0200
@@ -0,0 +1,28 @@
+# pylint: disable-msg=W0622
+"""cubicweb-classification-schemes packaging information"""
+
+modname = 'keyword'
+distname = "cubicweb-keyword"
+
+numversion = (1, 8, 0)
+version = '.'.join(str(num) for num in numversion)
+
+license = 'LGPL'
+copyright = '''Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE).
+http://www.logilab.fr/ -- mailto:contact@logilab.fr'''
+
+author = "Logilab"
+author_email = "contact@logilab.fr"
+web = 'http://www.cubicweb.org/project/%s' % distname
+
+description = "classification schemes system for the Cubicweb framework"
+
+classifiers = [
+    'Environment :: Web Environment',
+    'Framework :: CubicWeb',
+    'Programming Language :: Python',
+    'Programming Language :: JavaScript',
+    ]
+
+__depends_cubes__ = {}
+__depends__ = {'cubicweb': '>= 3.24.0'}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb_keyword/data/cubes.keyword.js	Wed Sep 27 09:08:03 2017 +0200
@@ -0,0 +1,44 @@
+/*
+ *  :organization: Logilab
+ *  :copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+ */
+
+CubicWeb.require('python.js');
+CubicWeb.require('ajax.js');
+CubicWeb.require('widgets.js');
+
+var kwform = null;
+
+/* callback called when tag-edition is finished */
+function validateKeywords(kwform, kwlist) {
+    var eid = kwform.eid;
+    d = asyncRemoteExec('add_keywords', eid, kwlist);
+    d.addCallback(function(msg) {
+	log('got message', msg);
+ 	reloadComponent('keywordsbar', 'Any X WHERE X eid '+eid, 'contentnavigation');
+  	document.location.hash = '#header';
+ 	kwform.destroy();
+ 	updateMessage(msg);
+    });
+}
+
+
+function onkwformDestroy() { kwform = null;}
+
+/* builds the tagselection widget */
+function showKeywordSelector(eid, oklabel, cancellabel) {
+    // if tagform is already on screen, destroy it
+    if (kwform) { kwform.destroy(); }
+    else {
+	kwform = new Widgets.SuggestForm('kwinput', 'possible_keywords', [eid],
+					  validateKeywords, {multi : true,
+							     oklabel : oklabel,
+							     cancellabel : cancellabel});
+	kwform.eid = eid // gruik !
+	connect(kwform, 'destroy', onkwformDestroy);
+	kwform.show($('kwformholder'));
+    }
+}
+
+Cubicweb.provide('keywords.js');
Binary file cubicweb_keyword/doc/schema.png has changed
Binary file cubicweb_keyword/doc/screenshot_keyword_tree.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb_keyword/entities.py	Wed Sep 27 09:08:03 2017 +0200
@@ -0,0 +1,125 @@
+"""entity classes for classification schemes entities
+
+:organization: Logilab
+:copyright: 2007-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from logilab.common.deprecation import  deprecated
+
+from cubicweb.entities import AnyEntity, fetch_config, adapters
+from cubicweb.view import EntityAdapter
+from cubicweb.predicates import is_instance
+
+class Classification(AnyEntity):
+    __regid__ = 'Classification'
+    fetch_attrs, cw_fetch_order = fetch_config(['name'])
+
+
+class ClassificationITreeAdapter(EntityAdapter):
+    __regid__ = 'ITree'
+    __select__ = is_instance('Classification')
+
+    def root(self):
+        """returns the root object"""
+        return None
+
+    def parent(self):
+        """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.entity.related('included_in', 'object', entities=entities)
+
+    def children_rql(self):
+        """XXX returns RQL to get children"""
+        return self.entity.cw_related_rql('included_in', 'object')
+
+    def is_leaf(self):
+        """returns true if this node as no child"""
+        return bool(self.children())
+
+    def is_root(self):
+        """returns true if this node has no parent"""
+        return True
+
+    @deprecated('[3.6] was specific to external project')
+    def first_level_keywords(self):
+        return self.req.execute('Any K,N ORDERBY N WHERE K included_in C, '
+                                'NOT K subkeyword_of KK, K name N, '
+                                'C eid %(x)s', {'x': self.eid})
+
+
+class Keyword(AnyEntity):
+    __regid__ = 'Keyword'
+    fetch_attrs, cw_fetch_order = fetch_config(['name'])
+
+
+class KeywordITreeAdapter(adapters.ITreeAdapter):
+    __select__ = is_instance('Keyword', 'CodeKeyword')
+    tree_relation = 'subkeyword_of'
+
+    @property
+    def classification(self):
+        if self.entity.included_in:
+            return self.entity.included_in[0]
+        return None
+
+    def parent(self):
+        """ITree + IBreadcrumbs implementation"""
+        try:
+            return self.entity.related(self.tree_relation, self.child_role,
+                                entities=True)[0]
+        except (KeyError, IndexError):
+            return self.classification
+
+    def iterparents(self):
+        """returns parent keyword entities,
+           without the root classification
+        """
+        if self.entity.subkeyword_of:
+            parent = self.entity.subkeyword_of[0]
+            while parent is not None:
+                yield parent
+                if parent.subkeyword_of:
+                    parent = parent.subkeyword_of[0]
+                else:
+                    parent = None
+
+    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 recurse_children(self, _done=None):
+        """returns strict descendents"""
+        if _done is not None and self.entity.eid in _done:
+            return
+        if _done is not None:
+            _done.add(self.entity.eid)
+            yield self.entity
+        else:
+            _done = set()
+        for child in self.children():
+            for entity in child.cw_adapt_to('ITree').recurse_children(_done):
+                yield entity
+
+class CodeKeyword(Keyword):
+    __regid__ = 'CodeKeyword'
+    rest_attr = 'code'
+    fetch_attrs, cw_fetch_order = fetch_config(['code','name'])
+
+    def dc_title(self):
+        return u'%s - %s' % (self.code, self.name)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb_keyword/hooks.py	Wed Sep 27 09:08:03 2017 +0200
@@ -0,0 +1,121 @@
+"""specific hooks for Classification and Keyword entities
+
+:organization: Logilab
+:copyright: 2007-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+
+from itertools import chain
+
+from cubicweb import ValidationError
+from cubicweb.server.hook import Hook, Operation, match_rtype
+from yams.schema import role_name
+
+class BeforeAddDescendantOf(Hook):
+    """check indirect cycle for ``descendant_of`` relation
+    """
+    __regid__ = 'beforeadddescendant'
+    events = ('before_add_relation', )
+    __select__ = Hook.__select__ & match_rtype('descendant_of')
+
+    def __call__(self):
+        entity = self._cw.entity_from_eid(self.eidfrom)
+        parent = self._cw.entity_from_eid(self.eidto)
+        parents = set([x.eid for x in chain([parent,], parent.cw_adapt_to('ITree').iterparents())])
+        children = set([x.eid for x in chain([entity], entity.cw_adapt_to('ITree').recurse_children())])
+        if children & parents:
+            msg = _('detected descendant_of cycle')
+            raise ValidationError(self.eidfrom, {role_name(self.rtype, 'subject'): msg})
+
+
+class AfterAddSubKeywordOf(Hook):
+    """sets ``descendant_of`` relation
+    """
+    __regid__ = 'afteradddescendant'
+    events = ('after_add_relation', )
+    __select__ = Hook.__select__ & match_rtype('subkeyword_of')
+
+    def __call__(self):
+        entity = self._cw.entity_from_eid(self.eidfrom)
+        parent = self._cw.entity_from_eid(self.eidto)
+        SetDescendantOfKeywordOp(self._cw, parent=parent, entity=entity)
+
+class SetIncludedInRelationHook(Hook):
+    """sets the included_in relation on a subkeyword if not already set
+    """
+    __regid__ = 'setincludedinrelationhook'
+    events = ('before_add_relation',)
+    __select__ = Hook.__select__ & match_rtype('subkeyword_of')
+
+    def __call__(self):
+        # immediate test direct cycles
+        if self.eidfrom == self.eidto:
+            msg = self._cw._('keyword cannot be subkeyword of himself')
+            raise ValidationError(self.eidfrom, {role_name(self.rtype, 'subject') : msg})
+        SetIncludedInRelationOp(self._cw, vreg=self._cw.vreg,
+                                eidfrom=self.eidfrom, eidto=self.eidto)
+
+
+
+class RemoveDescendantOfRelation(Hook):
+    """removes ``descendant_of`` relation
+
+    we delete the relation for entity's parents recursively
+    """
+    __regid__ = 'removedescendantofrelation'
+    events = ('after_delete_relation',)
+    __select__ = Hook.__select__ & match_rtype('subkeyword_of')
+
+    def __call__(self):
+        parent = self._cw.entity_from_eid(self.eidto)
+        for parent in chain([parent], parent.cw_adapt_to('ITree').iterparents()):
+            self._cw.execute('DELETE K descendant_of P WHERE K eid %(k)s, '
+                            'P eid %(p)s', {'p':parent.eid, 'k': self.eidfrom})
+
+
+## operations #################################################################
+class SetIncludedInRelationOp(Operation):
+    """delay this operation to commit to avoid conflict with a late rql query
+    already setting the relation
+    """
+    def precommit_event(self):
+        session = self.session
+        # test for indirect cycles
+        self.check_cycle()
+        subkw = session.entity_from_eid(self.eidfrom)
+        if subkw.included_in:
+            kw = session.entity_from_eid(self.eidto)
+            if subkw.included_in[0].eid != kw.included_in[0].eid:
+                msgid = "keywords %(subkw)s and %(kw)s belong to different classifications"
+                raise ValidationError(subkw.eid, {role_name('subkeyword_of', 'subject'): session._(msgid) %
+                                                  {'subkw' : subkw.eid, 'kw' : kw.eid}})
+        else:
+            session.execute('SET SK included_in C WHERE SK eid %(x)s, '
+                            'SK subkeyword_of K, K included_in C',
+                            {'x': subkw.eid})
+
+    def check_cycle(self):
+        parents = set([self.eidto])
+        parent = self.session.entity_from_eid(self.eidto)
+        while parent.subkeyword_of:
+            parent = parent.subkeyword_of[0]
+            if parent.eid in parents:
+                msg = self.session._('detected subkeyword cycle')
+                raise ValidationError(self.eidfrom, {role_name('subkeyword_of', 'subject'): msg})
+            parents.add(parent.eid)
+
+
+class SetDescendantOfKeywordOp(Operation):
+    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.cw_adapt_to('ITree').iterparents()):
+            for child in chain([entity], entity.cw_adapt_to('ITree').recurse_children()):
+                if child.eid != parent.eid:
+                    closure.add((child, parent))
+        for child, parent in closure:
+            descendants = [p.eid for p in child.descendant_of]
+            if not parent.eid in descendants:
+                child.cw_set(descendant_of=parent)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb_keyword/i18n/en.po	Wed Sep 27 09:08:03 2017 +0200
@@ -0,0 +1,250 @@
+msgid ""
+msgstr ""
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: cubicweb-devtools\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+msgid "A CodeKeyword is a keyword with a code and a name"
+msgstr ""
+
+msgid ""
+"A keyword is like a tag but is application specific and used to define a "
+"classification scheme"
+msgstr ""
+
+# schema pot file, generated on 2007-10-18 15:50:10
+#
+# singular and plural forms for each entity type
+msgid "Classification"
+msgstr "Classification"
+
+msgid "Classification_plural"
+msgstr "Classifications"
+
+msgid "CodeKeyword"
+msgstr ""
+
+msgid "CodeKeyword_plural"
+msgstr ""
+
+msgid "Keyword"
+msgstr "Keyword"
+
+msgid "Keyword_plural"
+msgstr "Keywords"
+
+msgid "New Classification"
+msgstr "New classification"
+
+msgid "New CodeKeyword"
+msgstr ""
+
+msgid "New Keyword"
+msgstr "New keyword"
+
+msgid "No suitable classification scheme found"
+msgstr ""
+
+msgid "This Classification"
+msgstr "This classification"
+
+msgid "This CodeKeyword"
+msgstr ""
+
+msgid "This Keyword"
+msgstr "This keyword"
+
+msgid "a keyword can specialize another keyword"
+msgstr ""
+
+msgid "a keyword is included in a classification scheme"
+msgstr ""
+
+msgid "add CodeKeyword included_in Classification object"
+msgstr ""
+
+msgid "add CodeKeyword subkeyword_of CodeKeyword object"
+msgstr ""
+
+# add related box generated message
+msgid "add Keyword included_in Classification object"
+msgstr "keyword"
+
+msgid "add Keyword subkeyword_of Keyword object"
+msgstr "subkeyword"
+
+msgid "add keywords"
+msgstr ""
+
+# subject and object forms for each relation type
+# (no object form for final relation types)
+msgid "applied_to"
+msgstr "applied to"
+
+msgid "applied_to_object"
+msgstr "keywords"
+
+msgid "classification schemes are used by users to classify entities."
+msgstr ""
+
+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"
+
+msgid "contentnavigation_addkeywords_description"
+msgstr "component to add keywords to an entity"
+
+msgid "contentnavigation_keywordsbar"
+msgstr "show keywords"
+
+msgid "contentnavigation_keywordsbar_description"
+msgstr "component to display keywords of an entity"
+
+msgid ""
+"creating CodeKeyword (CodeKeyword included_in Classification %(linkto)s)"
+msgstr ""
+
+msgid "creating CodeKeyword (CodeKeyword subkeyword_of CodeKeyword %(linkto)s)"
+msgstr ""
+
+msgid "creating Keyword (Keyword included_in Classification %(linkto)s)"
+msgstr "creating keyword for classification scheme %(linkto)s"
+
+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 "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 ""
+
+msgid ""
+"entity types classified by the classification. Only entity type supporting "
+"the applied_to relation can be selectioned"
+msgstr ""
+
+msgid "facets_applied-to-facet"
+msgstr ""
+
+msgid "facets_applied-to-facet_description"
+msgstr ""
+
+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"
+
+msgid "keyword cannot be subkeyword of himself"
+msgstr ""
+
+#, python-format
+msgid "keyword name changed from %(oldname)s to %(kw)s"
+msgstr ""
+
+#, python-format
+msgid "keyword name changed from %s to %s"
+msgstr ""
+
+msgid "keywords applied"
+msgstr ""
+
+msgid "keywords:"
+msgstr ""
+
+msgctxt "Classification"
+msgid "name"
+msgstr ""
+
+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 "which keyword (if any) this keyword specializes"
+msgstr ""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb_keyword/i18n/fr.po	Wed Sep 27 09:08:03 2017 +0200
@@ -0,0 +1,258 @@
+# cubicweb-classification-scheme i18n catalog
+# Copyright 2007-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# Logilab <contact@logilab.fr>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: cubicweb-classification-schemes 0.11.0\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"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "A CodeKeyword is a keyword with a code and a name"
+msgstr "Un code de mot-clé est un mot-clé avec un code et un nom"
+
+msgid ""
+"A keyword is like a tag but is application specific and used to define a "
+"classification scheme"
+msgstr ""
+"Un mot-clé est similaire à une étiquette mais il fait parti d'un plan de "
+"classement hiérarchisé"
+
+# schema pot file, generated on 2007-10-18 15:50:10
+#
+# singular and plural forms for each entity type
+msgid "Classification"
+msgstr "Plan de classement"
+
+msgid "Classification_plural"
+msgstr "Plans de classement"
+
+msgid "CodeKeyword"
+msgstr "Code de mot-clé"
+
+msgid "CodeKeyword_plural"
+msgstr "Codes de mots-clé"
+
+msgid "Keyword"
+msgstr "Mot-clé"
+
+msgid "Keyword_plural"
+msgstr "Mots-clé"
+
+msgid "New Classification"
+msgstr "Nouveau plan de classement"
+
+msgid "New CodeKeyword"
+msgstr "Nouveau code de mot-clé"
+
+msgid "New Keyword"
+msgstr "Nouveau mot-clé"
+
+msgid "No suitable classification scheme found"
+msgstr "Aucun plan de classement adéquat n'a été trouvé"
+
+msgid "This Classification"
+msgstr "Ce plan de classement"
+
+msgid "This CodeKeyword"
+msgstr "Ce code de mot-clé"
+
+msgid "This Keyword"
+msgstr "Ce mot-clé"
+
+msgid "a keyword can specialize another keyword"
+msgstr "un mot-clé peut spécialiser un autre mot-clé"
+
+msgid "a keyword is included in a classification scheme"
+msgstr "un mot-clé est inclus dans un plan de classement"
+
+msgid "add CodeKeyword included_in Classification object"
+msgstr "ajouter un code de mot-clé"
+
+msgid "add CodeKeyword subkeyword_of CodeKeyword object"
+msgstr "ajouter un sous-code de mot-clé"
+
+# add related box generated message
+msgid "add Keyword included_in Classification object"
+msgstr "ajouter un mot-clé"
+
+msgid "add Keyword subkeyword_of Keyword object"
+msgstr "ajouter un sous mot-clé"
+
+msgid "add keywords"
+msgstr "ajouter les mot-clés"
+
+# subject and object forms for each relation type
+# (no object form for final relation types)
+msgid "applied_to"
+msgstr "appliqué à"
+
+msgid "applied_to_object"
+msgstr "mot-clés"
+
+msgid "classification schemes are used by users to classify entities."
+msgstr "les plans de classement sont utilisés pour classifier les entités"
+
+msgid "classifies"
+msgstr "classe"
+
+msgctxt "Classification"
+msgid "classifies"
+msgstr "classifie"
+
+msgctxt "CWEType"
+msgid "classifies_object"
+msgstr "classifié par"
+
+msgid "classifies_object"
+msgstr "classifié par"
+
+msgid "code"
+msgstr "code"
+
+msgctxt "CodeKeyword"
+msgid "code"
+msgstr "code"
+
+msgid "contentnavigation_addkeywords"
+msgstr "ajout de mots-clés"
+
+msgid "contentnavigation_addkeywords_description"
+msgstr "composant pour ajouter des mots-clés"
+
+msgid "contentnavigation_keywordsbar"
+msgstr "affichage des mots-clés"
+
+msgid "contentnavigation_keywordsbar_description"
+msgstr "composant de navigation affichant les mots-clés associés à une entité"
+
+msgid ""
+"creating CodeKeyword (CodeKeyword included_in Classification %(linkto)s)"
+msgstr "création d'un code de mot-clé pour le plan de classement %(linkto)s"
+
+msgid "creating CodeKeyword (CodeKeyword subkeyword_of CodeKeyword %(linkto)s)"
+msgstr "création d'un sous code de mot-clé de %(linkto)s"
+
+msgid "creating Keyword (Keyword included_in Classification %(linkto)s)"
+msgstr "création d'un mot-clé pour le plan de classement %(linkto)s"
+
+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 "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é"
+
+msgid ""
+"entity types classified by the classification. Only entity type supporting "
+"the applied_to relation can be selectioned"
+msgstr ""
+
+msgid "facets_applied-to-facet"
+msgstr ""
+
+msgid "facets_applied-to-facet_description"
+msgstr ""
+
+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 "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"
+
+#, python-format
+msgid "keyword name changed from %(oldname)s to %(kw)s"
+msgstr "le mot-clé %(oldname)s a été renommé en %(kw)s"
+
+#, python-format
+msgid "keyword name changed from %s to %s"
+msgstr "le mot-clé %s a été renommé en %s"
+
+msgid "keywords applied"
+msgstr "mot-clé(s) appliqué(s)"
+
+msgid "keywords:"
+msgstr "mot-clés :"
+
+msgctxt "Classification"
+msgid "name"
+msgstr "nom"
+
+msgctxt "Keyword"
+msgid "name"
+msgstr "nom"
+
+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 "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 "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/cubicweb_keyword/migration/1.0.2_Any.py	Wed Sep 27 09:08:03 2017 +0200
@@ -0,0 +1,1 @@
+synchronize_eschema('Classification') # index removed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb_keyword/migration/1.4.0_Any.py	Wed Sep 27 09:08:03 2017 +0200
@@ -0,0 +1,5 @@
+add_entity_type('CodeKeyword')
+
+add_relation_definition('Keyword', 'descendant_of', 'Keyword')
+sync_schema_props_perms('Keyword')
+checkpoint()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb_keyword/migration/postcreate.py	Wed Sep 27 09:08:03 2017 +0200
@@ -0,0 +1,2 @@
+# postcreate script. You could setup a workflow here for example
+# no explicit initial state for Keywords since it depends on the user's groups
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb_keyword/schema.py	Wed Sep 27 09:08:03 2017 +0200
@@ -0,0 +1,82 @@
+"""This cube handles classification schemes.
+
+:organization: Logilab
+:copyright: 2007-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+from yams.buildobjs import (EntityType, RelationType,
+                            SubjectRelation, ObjectRelation, String)
+
+from cubicweb.schema import RQLConstraint, ERQLExpression
+
+
+_ = unicode
+
+class Classification(EntityType):
+    """classification schemes are used by users to classify entities.
+    """
+    __permissions__ = {
+        'read' : ('managers', 'users', 'guests'),
+        'add' : ('managers',),
+        'delete' : ('managers',),
+        'update' : ('managers',),
+        }
+    name = String(required=True, fulltextindexed=True, unique=True,
+                  maxsize=128)
+    classifies = SubjectRelation('CWEType',
+                     # see relation type docstring
+                     constraints = [RQLConstraint('RDEF to_entity O,'
+                                                  'RDEF relation_type R,'
+                                                  'R name "applied_to"',
+                     msg="Classification is trying to classifies an EntityType "
+                         "without applied_to relation")])
+
+
+class classifies(RelationType):
+    """entity types classified by the classification. Only entity type
+    supporting the applied_to relation can be selectioned
+    """
+
+
+class Keyword(EntityType):
+    """A keyword is like a tag but is application specific
+    and used to define a classification scheme
+    """
+    __permissions__ = {
+        'read' : ('managers', 'users', 'guests'),
+        'add' : ('managers', 'users'),
+        'delete' : ('managers',),
+        'update' : ('managers',),
+        }
+    name = String(required=True, fulltextindexed=True, indexed=True, maxsize=128)
+
+    subkeyword_of = SubjectRelation('Keyword', cardinality='?*',
+                                    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*')
+
+
+class CodeKeyword(Keyword):
+    """A CodeKeyword is a keyword with a code and a name
+    """
+    __specializes_schema__ = True
+    code = String(required=True, fulltextindexed=True, indexed=True, maxsize=128)
+
+class subkeyword_of(RelationType):
+    """a keyword can specialize another keyword"""
+
+
+class included_in(RelationType):
+    """a keyword is included in a classification scheme"""
+    inlined = True
+
+
+# define in parent application which entity types may be linked to a keyword
+# by the applied_to relation
+
+class applied_to(RelationType):
+    """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/cubicweb_keyword/setup.py	Wed Sep 27 09:08:03 2017 +0200
@@ -0,0 +1,82 @@
+#!/usr/bin/env python
+# pylint: disable-msg=W0404,W0622,W0704,W0613,W0152
+# Copyright (c) 2003-2009 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+""" Generic Setup script, takes package info from __pkginfo__.py file """
+
+import os
+import sys
+import shutil
+from distutils.core import setup
+from distutils import command
+from distutils.command import install_lib
+from os.path import isdir, exists, join, walk
+
+# import required features
+from __pkginfo__ import distname, version, license, short_desc, long_desc, \
+     web, author, author_email
+# import optional features
+try:
+    from __pkginfo__ import distname
+except ImportError:
+    distname = distname
+try:
+    from __pkginfo__ import scripts
+except ImportError:
+    scripts = []
+try:
+    from __pkginfo__ import data_files
+except ImportError:
+    data_files = None
+try:
+    from __pkginfo__ import include_dirs
+except ImportError:
+    include_dirs = []
+
+BASE_BLACKLIST = ('CVS', 'debian', 'dist', 'build', '__buildlog')
+IGNORED_EXTENSIONS = ('.pyc', '.pyo', '.elc')
+
+
+def ensure_scripts(linux_scripts):
+    """creates the proper script names required for each platform
+    (taken from 4Suite)
+    """
+    from distutils import util
+    if util.get_platform()[:3] == 'win':
+        scripts_ = [script + '.bat' for script in linux_scripts]
+    else:
+        scripts_ = linux_scripts
+    return scripts_
+
+
+
+def install(**kwargs):
+    """setup entry point"""
+    #kwargs['distname'] = modname
+    return setup(name=distname,
+                 version=version,
+                 license =license,
+                 description=short_desc,
+                 long_description=long_desc,
+                 author=author,
+                 author_email=author_email,
+                 url=web,
+                 scripts=ensure_scripts(scripts),
+                 data_files=data_files,
+                 **kwargs)
+
+if __name__ == '__main__' :
+    install()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb_keyword/sobjects.py	Wed Sep 27 09:08:03 2017 +0200
@@ -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.predicates import is_instance
+from cubicweb.sobjects.notification import NotificationView
+
+
+class KeywordNotificationView(NotificationView):
+    __select__ = is_instance('Keyword')
+    msgid_timestamp = True
+
+    def recipients(self):
+        """Returns the project's interested people (entities)"""
+        creator = self.cw_rset.get_entity(0, 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.cw_rset.get_entity(0, 0)
+        context['kw'] = entity.name
+        return context
+
+
+class KeywordNameChanged(KeywordNotificationView):
+    __regid__ = '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.cw_rset.get_entity(0, 0)
+        context = KeywordNotificationView.context(self, **kwargs)
+        context['oldname'] = self.get_oldname(entity)
+        return context
+
+    def subject(self):
+        entity = self.cw_rset.get_entity(0, 0)
+        return self.req._('keyword name changed from %s to %s') % (
+            self.get_oldname(entity), entity.name)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb_keyword/views.py	Wed Sep 27 09:08:03 2017 +0200
@@ -0,0 +1,275 @@
+"""Specific 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 monkeypatch
+from logilab.mtconverter import xml_escape
+
+from cubicweb import Unauthorized
+from cubicweb.predicates import is_instance, rql_condition, relation_possible
+from cubicweb.view import EntityView
+from cubicweb.web import stdmsgs, component, facet
+from cubicweb.web.views import primary, basecontrollers, treeview
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
+
+from cubicweb.web.views import uicfg
+
+
+_pvs = uicfg.primaryview_section
+_pvs.tag_object_of(('*', 'applied_to', '*'), 'hidden')
+_pvs.tag_subject_of(('*', 'applied_to', '*'), 'hidden')
+_pvs.tag_object_of(('*', 'included_in', 'Classification'), 'hidden')
+
+_abaa = uicfg.actionbox_appearsin_addmenu
+_abaa.tag_object_of(('CodeKeyword', 'included_in', 'Classification'), True)
+_abaa.tag_object_of(('Keyword', 'included_in', 'Classification'), True)
+_abaa.tag_object_of(('CodeKeyword', 'subkeyword_of', 'CodeKeyword'), True)
+_abaa.tag_object_of(('Keyword', 'subkeyword_of', 'Keyword'), True)
+
+
+# classification views ########################################################
+
+class ClassificationPrimaryView(primary.PrimaryView):
+    __select__ = is_instance('Classification')
+
+    def render_entity_attributes(self, entity):
+        pass
+
+    def render_entity_relations(self, entity):
+        rset = self._cw.execute('Any K ORDERBY N WHERE K included_in C, '
+                                'NOT K subkeyword_of KK, K name N, '
+                                'C eid %(x)s', {'x': entity.eid})
+        self.wview('treeview', rset, 'null')
+
+
+# keyword views ###############################################################
+
+class KeywordPrimaryView(primary.PrimaryView):
+    __select__ = is_instance('Keyword')
+
+    def cell_call(self, row, col, **kwargs):
+        entity = self.cw_rset.get_entity(row, col)
+        self.w(u'<h1 class="titleUnderline">%s</h1>'
+               % xml_escape(entity.dc_long_title()))
+        rset = entity.related('subkeyword_of','object')
+        self.wview('treeview', rset, 'null')
+
+
+class KeywordComboBoxView(treeview.TreePathView):
+    """display keyword in edition's combobox"""
+    __regid__ = 'combobox'
+    __select__ = is_instance('Keyword', 'Classification')
+
+    item_vid = 'text'
+    separator = u' > '
+
+# skos views ############################################################
+
+SKOS_OPENING_ROOT=u'''<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE rdfs [
+	<!ENTITY rdf "http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+	<!ENTITY rdfs "http://www.w3.org/2000/01/rdf-schema#">
+	<!ENTITY dc "http://purl.org/dc/elements/1.1/">
+	<!ENTITY dct "http://purl.org/dc/terms/">
+]>
+<rdf:RDF  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+          xmlns:skos="http://www.w3.org/2004/02/skos/core#">
+'''
+SKOS_CLOSING_ROOT = u'</rdf:RDF>'
+
+class SkosView(EntityView):
+    __regid__ = 'skos'
+    content_type = 'application/xml'
+    templatable = False
+    __select__ = is_instance('Keyword', 'Classification')
+
+    def call(self, **kwargs):
+        self.w(SKOS_OPENING_ROOT)
+        for i in xrange(self.rset.rowcount):
+             self.cell_call(i, 0)
+        self.w(SKOS_CLOSING_ROOT)
+
+    def cell_call(self, row, col):
+        self.wview('skositemview', self.rset, row=row, col=col)
+
+class SkosItemView(EntityView):
+    __regid__ = 'skositemview'
+    content_type = 'application/xml'
+    __select__ = is_instance('Keyword', 'Classification')
+
+    def cell_call(self, row, col, show_parent=True, stop=False):
+        w = self.w
+        entity = self.complete_entity(row, col)
+        eschema = entity.e_schema
+        w(u'<skos:%s>' % eschema)
+        w(u'<skos:prefLabel>%s</skos:prefLabel>' % xml_escape(entity.name))
+        if not stop:
+            if show_parent and not entity.is_root():
+                par = entity.parent()
+                w(u'<skos:broader>')
+                par.view('skositemview', show_parent=False, stop=True, w=self.w)
+                w(u'</skos:broader>')
+            for child in entity.children(entities=True):
+                w(u'<skos:narrower>')
+                self.wview('skositemview', child.as_rset(), show_parent=False)
+                w(u'</skos:narrower>')
+        w(u'</skos:%s>' % eschema)
+
+
+# keyword component ###########################################################
+
+class KeywordBarVComponent(component.EntityCtxComponent):
+    """the keywords path bar: display keywords of a tagged entity"""
+    __regid__ = 'keywordsbar'
+    __select__ = (component.EntityCtxComponent.__select__ &
+                  relation_possible('applied_to', 'object', 'Keyword'))
+    context = 'header'
+    order = 152
+    htmlclass = 'navigation'
+
+    def get_keywords(self):
+        """helper method for subclasses redefinition"""
+        return self.entity.related('applied_to', 'object')
+
+    def render_body(self, w):
+        rset = self.get_keywords()
+        if rset:
+            w(u'<div class="%s" id="%s">\n' % (self.cssclass, self.domid))
+            w(u'<span>%s</span>&nbsp;' % self._cw._('keywords:'))
+            self._cw.view('csv', rset, 'null', w=w)
+            w(u'</div>\n')
+        else:
+            w(u'<div class="%s hidden" id="%s"></div>\n' % (
+                self.cssclass, self.domid))
+
+class AddKeywordVComponent(component.EntityCtxComponent):
+    """the 'add keyword' component"""
+    __regid__ = 'addkeywords'
+    __select__ = component.EntityCtxComponent.__select__ & \
+                 relation_possible('applied_to', 'object', 'Keyword', action='add') & \
+                 rql_condition('X is ET, CL classifies ET')
+
+    context = 'header'
+    order = 153
+    htmlclass = 'navigation'
+
+    def render_body(self, w):
+        entity = self.entity
+        self._cw.add_js(['cubicweb.widgets.js', 'cubes.keyword.js'])
+        self._cw.add_css('cubicweb.suggest.css')
+        w(u'<table><tr><td>')
+        w(u'<a class="button sglink" href="javascript: showKeywordSelector(%s, \'%s\', \'%s\');">%s</a></td>' % (
+            entity.eid, self._cw._(stdmsgs.BUTTON_OK[0]),
+            self._cw._(stdmsgs.BUTTON_CANCEL[0]), self._cw._('add keywords')))
+        w(u'<td><div id="kwformholder"></div>')
+        w(u'</td></tr></table>')
+
+
+# applied_to relation facet ####################################################
+
+class AppliedToFacet(facet.RelationFacet):
+    __regid__ = 'applied-to-facet'
+    rtype = 'applied_to'
+    role = 'object'
+    target_attr = 'name'
+
+    def rset_vocabulary(self, rset):
+        _ = self._cw._
+        vocab = []
+        scheme = None
+        for e in sorted(rset.entities(),
+                        key=lambda e: (e.cw_adapt_to('ITree').classification.name,
+                                       e.view('combobox'))):
+            classification_name = e.cw_adapt_to('ITree').classification.name
+            if scheme != classification_name:
+                vocab.append( (_(classification_name), None) )
+            vocab.append( (e.view('combobox'), e.eid) )
+        return vocab
+
+
+class ClassificationFacet(facet.RelationFacet):
+    """abstract per-classification facet
+
+    subclasses must define their own id the classification name, e.g :
+
+    class Classifaction1Facet(ClassificationFacet):
+        __regid__ = 'classif1'
+        classification = u'classification1'
+
+    """
+    __abstract__ = True
+    classification = None
+    rtype = 'applied_to'
+    role = 'object'
+
+    def vocabulary(self):
+        """return vocabulary for this facet, eg a list of 2-uple (label, value)
+        """
+        rqlst = self.rqlst
+        rqlst.save_state()
+        try:
+            mainvar = self.filtered_variable # X
+            keyword_var = rqlst.make_variable() # K
+            keyword_name_var = rqlst.make_variable() # KN
+            classif_var = rqlst.make_variable() # C
+            classif_name_var = rqlst.make_variable() # CN
+            rqlst.add_relation(keyword_var, 'applied_to', mainvar) # K applied_to X
+            rqlst.add_relation(keyword_var, 'name', keyword_name_var) # K name KN
+            rqlst.add_relation(keyword_var, 'included_in', classif_var) # K included_in C
+            # C name "classification-name"
+            rqlst.add_constant_restriction(classif_var, 'name', self.classification, 'String')
+            rqlst.add_selected(keyword_var)
+            rqlst.add_selected(keyword_name_var)
+            # ORDERBY KN
+            rqlst.add_sort_var(keyword_name_var, True)
+            try:
+                rset = self._cw.execute(rqlst.as_string(), self.rset.args,
+                                        self.rset.cachekey)
+            except Unauthorized:
+                return []
+        finally:
+            rqlst.recover()
+        return self.rset_vocabulary(rset)
+
+    @property
+    def title(self):
+        return self._cw._(self.classification)
+
+    def support_and(self):
+        return False
+
+# add some classification schema related methods to the Jsoncontroller ########
+
+@ajaxfunc(output_type='json')
+def js_possible_keywords(self, eid):
+    rql = ('DISTINCT Any N WHERE K is Keyword, K name N, NOT K applied_to X, '
+           'X eid %(x)s, K included_in C, C classifies ET, X is ET')
+    rset = self.cursor.execute(rql, {'x' : eid, 'u' : self._cw.user.eid}, 'x')
+    return [name for (name,) in rset]
+
+@ajaxfunc(output_type='json')
+def js_add_keywords(self, eid, kwlist):
+    msg = self._cw._('keywords applied')
+    kwrset = self.cursor.execute('Any K,N,C WHERE K is Keyword, K name N, K included_in C, '
+                                 'C classifies ET, X eid %(x)s, X is ET',
+                                 {'x' : eid}, 'x')
+    if not kwrset:
+        return self._cw._('No suitable classification scheme found')
+    classification = kwrset[0][2] # XXX what if we have several classifications ?
+    valid_keywords = set(kwname for _, kwname,_ in kwrset)
+    user_keywords = set(kwlist)
+    invalid_keywords = sorted(user_keywords - valid_keywords)
+    kweids = dict( (kwname, str(kweid)) for kweid, kwname, _ in kwrset if kwname in user_keywords )
+    if invalid_keywords:
+        for keyword in invalid_keywords:
+            neweid = self.cursor.execute('INSERT Keyword K: K name %(name)s, K included_in C WHERE C eid %(c)s',
+                                         {'name' : keyword, 'c' : classification}, 'c')[0][0]
+            kweids[keyword] = str(neweid)
+    if kweids:
+        self.cursor.execute('SET KW applied_to X WHERE X eid %%(x)s, KW eid IN (%s)'
+                            % ','.join(kweids.values()), {'x' : eid}, 'x')
+    return msg
--- a/data/cubes.keyword.js	Wed Mar 05 18:27:01 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,44 +0,0 @@
-/*
- *  :organization: Logilab
- *  :copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
- *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
- */
-
-CubicWeb.require('python.js');
-CubicWeb.require('ajax.js');
-CubicWeb.require('widgets.js');
-
-var kwform = null;
-
-/* callback called when tag-edition is finished */
-function validateKeywords(kwform, kwlist) {
-    var eid = kwform.eid;
-    d = asyncRemoteExec('add_keywords', eid, kwlist);
-    d.addCallback(function(msg) {
-	log('got message', msg);
- 	reloadComponent('keywordsbar', 'Any X WHERE X eid '+eid, 'contentnavigation');
-  	document.location.hash = '#header';
- 	kwform.destroy();
- 	updateMessage(msg);
-    });
-}
-
-
-function onkwformDestroy() { kwform = null;}
-
-/* builds the tagselection widget */
-function showKeywordSelector(eid, oklabel, cancellabel) {
-    // if tagform is already on screen, destroy it
-    if (kwform) { kwform.destroy(); }
-    else {
-	kwform = new Widgets.SuggestForm('kwinput', 'possible_keywords', [eid],
-					  validateKeywords, {multi : true,
-							     oklabel : oklabel,
-							     cancellabel : cancellabel});
-	kwform.eid = eid // gruik !
-	connect(kwform, 'destroy', onkwformDestroy);
-	kwform.show($('kwformholder'));
-    }
-}
-
-Cubicweb.provide('keywords.js');
--- a/debian/changelog	Wed Mar 05 18:27:01 2014 +0100
+++ b/debian/changelog	Wed Sep 27 09:08:03 2017 +0200
@@ -1,3 +1,15 @@
+cubicweb-keyword (1.8.0-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Samuel TRÉGOUËT <samuel.tregouet@logilab.fr>  Fri, 22 Sep 2017 15:20:18 +0200
+
+cubicweb-keyword (1.7.1-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Katia Saurfelt <katia.saurfelt@logilab.fr>  Wed, 07 May 2014 15:14:15 +0200
+
 cubicweb-keyword (1.7.0-1) unstable; urgency=low
 
   * new upstream release
--- a/debian/compat	Wed Mar 05 18:27:01 2014 +0100
+++ b/debian/compat	Wed Sep 27 09:08:03 2017 +0200
@@ -1,1 +1,1 @@
-7
+9
--- a/debian/control	Wed Mar 05 18:27:01 2014 +0100
+++ b/debian/control	Wed Sep 27 09:08:03 2017 +0200
@@ -3,17 +3,22 @@
 Priority: optional
 Maintainer: Logilab Packaging Team <contact@logilab.fr>
 Uploaders: Sylvain Thenault <sylvain.thenault@logilab.fr>
-Build-Depends: debhelper (>= 7), python (>= 2.6), python-support
+Homepage: http://www.cubicweb.org/project/cubicweb-keyword
+Build-Depends:
+ debhelper (>= 7),
+ dh-python,
+ python (>= 2.6.5),
+ python-setuptools,
 Standards-Version: 3.9.3
-Homepage: http://www.cubicweb.org/project/cubicweb-keyword
-XS-Python-Version: >= 2.6
+X-Python-Version: >= 2.6
 
 Package: cubicweb-keyword
 Conflicts: cubicweb-keywords
 Replaces: cubicweb-keywords
 Architecture: all
 Depends:
- cubicweb-common (>= 3.15.0),
+ python-cubicweb (>= 3.24.0),
+ python-six (>= 1.4.0),
  ${python:Depends},
  ${misc:Depends},
 Description: keyword component for the Cubicweb framework
--- a/debian/rules	Wed Mar 05 18:27:01 2014 +0100
+++ b/debian/rules	Wed Sep 27 09:08:03 2017 +0200
@@ -1,56 +1,4 @@
 #!/usr/bin/make -f
-# Sample debian/rules that uses debhelper.
-# GNU copyright 1997 to 1999 by Joey Hess.
-
-# Uncomment this to turn on verbose mode.
-#export DH_VERBOSE=1
-build: build-arch build-indep
-build-arch:
-	# Nothing to do
-
-build-indep: build-stamp
-build-stamp:
-	dh_testdir
-	python setup.py -q build
-	>$@
-
-clean:
-	dh_testdir
-	rm -f build-stamp
-	rm -rf build
-	find . -name "*.pyc" -delete
-	dh_clean
 
-install: build
-	dh_testdir
-	dh_testroot
-	dh_clean -k
-	dh_installdirs -i
-	python setup.py -q install --no-compile --prefix=debian/cubicweb-keyword/usr/
-	# remove generated .egg-info file
-	rm -rf debian/cubicweb-keyword/usr/lib/python*
-
-# Build architecture-independent files here.
-binary-indep: build install
-	dh_testdir
-	dh_testroot
-	dh_install -i
-	dh_installchangelogs -i
-	dh_installexamples -i
-	dh_installdocs -i
-	dh_installman -i
-	dh_pysupport -i /usr/share/cubicweb
-	dh_link -i
-	dh_compress -i -X.py -X.ini -X.xml -Xtest
-	dh_fixperms -i
-	dh_installdeb -i
-	dh_gencontrol -i
-	dh_md5sums -i
-	dh_builddeb -i
-
-
-# Build architecture-dependent files here.
-binary-arch:
-
-binary: binary-indep
-.PHONY: build clean binary-arch binary-indep binary
+%:
+	dh $@ --with python2
--- a/entities.py	Wed Mar 05 18:27:01 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,140 +0,0 @@
-"""entity classes for classification schemes entities
-
-:organization: Logilab
-:copyright: 2007-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-"""
-__docformat__ = "restructuredtext en"
-
-from logilab.common.deprecation import  deprecated
-
-from cubicweb.mixins import MI_REL_TRIGGERS, TreeMixIn
-from cubicweb.entities import AnyEntity, fetch_config, adapters
-from cubicweb.interfaces import ITree
-from cubicweb.view import EntityAdapter
-from cubicweb.predicates import is_instance
-
-class Classification(AnyEntity):
-    __regid__ = 'Classification'
-    fetch_attrs, cw_fetch_order = fetch_config(['name'])
-
-
-class ClassificationITreeAdapter(EntityAdapter):
-    __regid__ = 'ITree'
-    __select__ = is_instance('Classification')
-
-    def root(self):
-        """returns the root object"""
-        return None
-
-    def parent(self):
-        """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.entity.related('included_in', 'object', entities=entities)
-
-    def children_rql(self):
-        """XXX returns RQL to get children"""
-        return self.entity.related_rql('included_in', 'object')
-
-    def is_leaf(self):
-        """returns true if this node as no child"""
-        return bool(self.children())
-
-    def is_root(self):
-        """returns true if this node has no parent"""
-        return True
-
-    @deprecated('[3.6] was specific to external project')
-    def first_level_keywords(self):
-        return self.req.execute('Any K,N ORDERBY N WHERE K included_in C, '
-                                'NOT K subkeyword_of KK, K name N, '
-                                'C eid %(x)s', {'x': self.eid})
-
-
-class Keyword(AnyEntity):
-    __regid__ = 'Keyword'
-    fetch_attrs, cw_fetch_order = fetch_config(['name'])
-
-
-class KeywordITreeAdapter(adapters.ITreeAdapter):
-    __select__ = is_instance('Keyword', 'CodeKeyword')
-    tree_relation = 'subkeyword_of'
-
-    @property
-    def classification(self):
-        if self.entity.included_in:
-            return self.entity.included_in[0]
-        return None
-
-    def parent(self):
-        """ITree + IBreadcrumbs implementation"""
-        try:
-            return self.entity.related(self.tree_relation, self.child_role,
-                                entities=True)[0]
-        except (KeyError, IndexError):
-            return self.classification
-
-    def iterparents(self):
-        """returns parent keyword entities,
-           without the root classification
-        """
-        if self.entity.subkeyword_of:
-            parent = self.entity.subkeyword_of[0]
-            while parent is not None:
-                yield parent
-                if parent.subkeyword_of:
-                    parent = parent.subkeyword_of[0]
-                else:
-                    parent = None
-
-    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 recurse_children(self, _done=None):
-        """returns strict descendents"""
-        if _done is not None and self.entity.eid in _done:
-            return
-        if _done is not None:
-            _done.add(self.entity.eid)
-            yield self.entity
-        else:
-            _done = set()
-        for child in self.children():
-            for entity in child.cw_adapt_to('ITree').recurse_children(_done):
-                yield entity
-
-class CodeKeyword(Keyword):
-    __regid__ = 'CodeKeyword'
-    rest_attr = 'code'
-    fetch_attrs, cw_fetch_order = fetch_config(['code','name'])
-
-    def dc_title(self):
-        return u'%s - %s' % (self.code, self.name)
-
-
-class ClassifiableMixIn(object):
-    """mixin automatically plugged to entity types supporting the applied_to
-    relation
-    """
-    def classification_keywords(self, name):
-        """return keywords of the given classification linked to this entity"""
-        return self.req.execute('Any K, KN WHERE K applied_to X, X eid %(x)s, '
-                                'K name KN, K included_in CS, CS name %(name)s',
-                                {'x': self.eid, 'name': name}, 'x')
-
-MI_REL_TRIGGERS[('applied_to', 'object')] = ClassifiableMixIn
--- a/hooks.py	Wed Mar 05 18:27:01 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,121 +0,0 @@
-"""specific hooks for Classification and Keyword entities
-
-:organization: Logilab
-:copyright: 2007-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-"""
-
-from itertools import chain
-
-from cubicweb import ValidationError
-from cubicweb.server.hook import Hook, Operation, match_rtype
-from yams.schema import role_name
-
-class BeforeAddDescendantOf(Hook):
-    """check indirect cycle for ``descendant_of`` relation
-    """
-    __regid__ = 'beforeadddescendant'
-    events = ('before_add_relation', )
-    __select__ = Hook.__select__ & match_rtype('descendant_of')
-
-    def __call__(self):
-        entity = self._cw.entity_from_eid(self.eidfrom)
-        parent = self._cw.entity_from_eid(self.eidto)
-        parents = set([x.eid for x in chain([parent,], parent.cw_adapt_to('ITree').iterparents())])
-        children = set([x.eid for x in chain([entity], entity.cw_adapt_to('ITree').recurse_children())])
-        if children & parents:
-            msg = _('detected descendant_of cycle')
-            raise ValidationError(self.eidfrom, {role_name(self.rtype, 'subject'): msg})
-
-
-class AfterAddSubKeywordOf(Hook):
-    """sets ``descendant_of`` relation
-    """
-    __regid__ = 'afteradddescendant'
-    events = ('after_add_relation', )
-    __select__ = Hook.__select__ & match_rtype('subkeyword_of')
-
-    def __call__(self):
-        entity = self._cw.entity_from_eid(self.eidfrom)
-        parent = self._cw.entity_from_eid(self.eidto)
-        SetDescendantOfKeywordOp(self._cw, parent=parent, entity=entity)
-
-class SetIncludedInRelationHook(Hook):
-    """sets the included_in relation on a subkeyword if not already set
-    """
-    __regid__ = 'setincludedinrelationhook'
-    events = ('before_add_relation',)
-    __select__ = Hook.__select__ & match_rtype('subkeyword_of')
-
-    def __call__(self):
-        # immediate test direct cycles
-        if self.eidfrom == self.eidto:
-            msg = self._cw._('keyword cannot be subkeyword of himself')
-            raise ValidationError(self.eidfrom, {role_name(self.rtype, 'subject') : msg})
-        SetIncludedInRelationOp(self._cw, vreg=self._cw.vreg,
-                                eidfrom=self.eidfrom, eidto=self.eidto)
-
-
-
-class RemoveDescendantOfRelation(Hook):
-    """removes ``descendant_of`` relation
-
-    we delete the relation for entity's parents recursively
-    """
-    __regid__ = 'removedescendantofrelation'
-    events = ('after_delete_relation',)
-    __select__ = Hook.__select__ & match_rtype('subkeyword_of')
-
-    def __call__(self):
-        parent = self._cw.entity_from_eid(self.eidto)
-        for parent in chain([parent], parent.cw_adapt_to('ITree').iterparents()):
-            self._cw.execute('DELETE K descendant_of P WHERE K eid %(k)s, '
-                            'P eid %(p)s', {'p':parent.eid, 'k': self.eidfrom})
-
-
-## operations #################################################################
-class SetIncludedInRelationOp(Operation):
-    """delay this operation to commit to avoid conflict with a late rql query
-    already setting the relation
-    """
-    def precommit_event(self):
-        session = self.session
-        # test for indirect cycles
-        self.check_cycle()
-        subkw = session.entity_from_eid(self.eidfrom)
-        if subkw.included_in:
-            kw = session.entity_from_eid(self.eidto)
-            if subkw.included_in[0].eid != kw.included_in[0].eid:
-                msgid = "keywords %(subkw)s and %(kw)s belong to different classifications"
-                raise ValidationError(subkw.eid, {role_name('subkeyword_of', 'subject'): session._(msgid) %
-                                                  {'subkw' : subkw.eid, 'kw' : kw.eid}})
-        else:
-            session.execute('SET SK included_in C WHERE SK eid %(x)s, '
-                            'SK subkeyword_of K, K included_in C',
-                            {'x': subkw.eid})
-
-    def check_cycle(self):
-        parents = set([self.eidto])
-        parent = self.session.entity_from_eid(self.eidto)
-        while parent.subkeyword_of:
-            parent = parent.subkeyword_of[0]
-            if parent.eid in parents:
-                msg = self.session._('detected subkeyword cycle')
-                raise ValidationError(self.eidfrom, {role_name('subkeyword_of', 'subject'): msg})
-            parents.add(parent.eid)
-
-
-class SetDescendantOfKeywordOp(Operation):
-    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.cw_adapt_to('ITree').iterparents()):
-            for child in chain([entity], entity.cw_adapt_to('ITree').recurse_children()):
-                if child.eid != parent.eid:
-                    closure.add((child, parent))
-        for child, parent in closure:
-            descendants = [p.eid for p in child.descendant_of]
-            if not parent.eid in descendants:
-                child.set_relations(descendant_of=parent)
--- a/i18n/en.po	Wed Mar 05 18:27:01 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,250 +0,0 @@
-msgid ""
-msgstr ""
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: cubicweb-devtools\n"
-"Plural-Forms: nplurals=2; plural=(n > 1);\n"
-
-msgid "A CodeKeyword is a keyword with a code and a name"
-msgstr ""
-
-msgid ""
-"A keyword is like a tag but is application specific and used to define a "
-"classification scheme"
-msgstr ""
-
-# schema pot file, generated on 2007-10-18 15:50:10
-#
-# singular and plural forms for each entity type
-msgid "Classification"
-msgstr "Classification"
-
-msgid "Classification_plural"
-msgstr "Classifications"
-
-msgid "CodeKeyword"
-msgstr ""
-
-msgid "CodeKeyword_plural"
-msgstr ""
-
-msgid "Keyword"
-msgstr "Keyword"
-
-msgid "Keyword_plural"
-msgstr "Keywords"
-
-msgid "New Classification"
-msgstr "New classification"
-
-msgid "New CodeKeyword"
-msgstr ""
-
-msgid "New Keyword"
-msgstr "New keyword"
-
-msgid "No suitable classification scheme found"
-msgstr ""
-
-msgid "This Classification"
-msgstr "This classification"
-
-msgid "This CodeKeyword"
-msgstr ""
-
-msgid "This Keyword"
-msgstr "This keyword"
-
-msgid "a keyword can specialize another keyword"
-msgstr ""
-
-msgid "a keyword is included in a classification scheme"
-msgstr ""
-
-msgid "add CodeKeyword included_in Classification object"
-msgstr ""
-
-msgid "add CodeKeyword subkeyword_of CodeKeyword object"
-msgstr ""
-
-# add related box generated message
-msgid "add Keyword included_in Classification object"
-msgstr "keyword"
-
-msgid "add Keyword subkeyword_of Keyword object"
-msgstr "subkeyword"
-
-msgid "add keywords"
-msgstr ""
-
-# subject and object forms for each relation type
-# (no object form for final relation types)
-msgid "applied_to"
-msgstr "applied to"
-
-msgid "applied_to_object"
-msgstr "keywords"
-
-msgid "classification schemes are used by users to classify entities."
-msgstr ""
-
-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"
-
-msgid "contentnavigation_addkeywords_description"
-msgstr "component to add keywords to an entity"
-
-msgid "contentnavigation_keywordsbar"
-msgstr "show keywords"
-
-msgid "contentnavigation_keywordsbar_description"
-msgstr "component to display keywords of an entity"
-
-msgid ""
-"creating CodeKeyword (CodeKeyword included_in Classification %(linkto)s)"
-msgstr ""
-
-msgid "creating CodeKeyword (CodeKeyword subkeyword_of CodeKeyword %(linkto)s)"
-msgstr ""
-
-msgid "creating Keyword (Keyword included_in Classification %(linkto)s)"
-msgstr "creating keyword for classification scheme %(linkto)s"
-
-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 "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 ""
-
-msgid ""
-"entity types classified by the classification. Only entity type supporting "
-"the applied_to relation can be selectioned"
-msgstr ""
-
-msgid "facets_applied-to-facet"
-msgstr ""
-
-msgid "facets_applied-to-facet_description"
-msgstr ""
-
-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"
-
-msgid "keyword cannot be subkeyword of himself"
-msgstr ""
-
-#, python-format
-msgid "keyword name changed from %(oldname)s to %(kw)s"
-msgstr ""
-
-#, python-format
-msgid "keyword name changed from %s to %s"
-msgstr ""
-
-msgid "keywords applied"
-msgstr ""
-
-msgid "keywords:"
-msgstr ""
-
-msgctxt "Classification"
-msgid "name"
-msgstr ""
-
-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 "which keyword (if any) this keyword specializes"
-msgstr ""
--- a/i18n/fr.po	Wed Mar 05 18:27:01 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,258 +0,0 @@
-# cubicweb-classification-scheme i18n catalog
-# Copyright 2007-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# Logilab <contact@logilab.fr>
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: cubicweb-classification-schemes 0.11.0\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"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-
-msgid "A CodeKeyword is a keyword with a code and a name"
-msgstr "Un code de mot-clé est un mot-clé avec un code et un nom"
-
-msgid ""
-"A keyword is like a tag but is application specific and used to define a "
-"classification scheme"
-msgstr ""
-"Un mot-clé est similaire à une étiquette mais il fait parti d'un plan de "
-"classement hiérarchisé"
-
-# schema pot file, generated on 2007-10-18 15:50:10
-#
-# singular and plural forms for each entity type
-msgid "Classification"
-msgstr "Plan de classement"
-
-msgid "Classification_plural"
-msgstr "Plans de classement"
-
-msgid "CodeKeyword"
-msgstr "Code de mot-clé"
-
-msgid "CodeKeyword_plural"
-msgstr "Codes de mots-clé"
-
-msgid "Keyword"
-msgstr "Mot-clé"
-
-msgid "Keyword_plural"
-msgstr "Mots-clé"
-
-msgid "New Classification"
-msgstr "Nouveau plan de classement"
-
-msgid "New CodeKeyword"
-msgstr "Nouveau code de mot-clé"
-
-msgid "New Keyword"
-msgstr "Nouveau mot-clé"
-
-msgid "No suitable classification scheme found"
-msgstr "Aucun plan de classement adéquat n'a été trouvé"
-
-msgid "This Classification"
-msgstr "Ce plan de classement"
-
-msgid "This CodeKeyword"
-msgstr "Ce code de mot-clé"
-
-msgid "This Keyword"
-msgstr "Ce mot-clé"
-
-msgid "a keyword can specialize another keyword"
-msgstr "un mot-clé peut spécialiser un autre mot-clé"
-
-msgid "a keyword is included in a classification scheme"
-msgstr "un mot-clé est inclus dans un plan de classement"
-
-msgid "add CodeKeyword included_in Classification object"
-msgstr "ajouter un code de mot-clé"
-
-msgid "add CodeKeyword subkeyword_of CodeKeyword object"
-msgstr "ajouter un sous-code de mot-clé"
-
-# add related box generated message
-msgid "add Keyword included_in Classification object"
-msgstr "ajouter un mot-clé"
-
-msgid "add Keyword subkeyword_of Keyword object"
-msgstr "ajouter un sous mot-clé"
-
-msgid "add keywords"
-msgstr "ajouter les mot-clés"
-
-# subject and object forms for each relation type
-# (no object form for final relation types)
-msgid "applied_to"
-msgstr "appliqué à"
-
-msgid "applied_to_object"
-msgstr "mot-clés"
-
-msgid "classification schemes are used by users to classify entities."
-msgstr "les plans de classement sont utilisés pour classifier les entités"
-
-msgid "classifies"
-msgstr "classe"
-
-msgctxt "Classification"
-msgid "classifies"
-msgstr "classifie"
-
-msgctxt "CWEType"
-msgid "classifies_object"
-msgstr "classifié par"
-
-msgid "classifies_object"
-msgstr "classifié par"
-
-msgid "code"
-msgstr "code"
-
-msgctxt "CodeKeyword"
-msgid "code"
-msgstr "code"
-
-msgid "contentnavigation_addkeywords"
-msgstr "ajout de mots-clés"
-
-msgid "contentnavigation_addkeywords_description"
-msgstr "composant pour ajouter des mots-clés"
-
-msgid "contentnavigation_keywordsbar"
-msgstr "affichage des mots-clés"
-
-msgid "contentnavigation_keywordsbar_description"
-msgstr "composant de navigation affichant les mots-clés associés à une entité"
-
-msgid ""
-"creating CodeKeyword (CodeKeyword included_in Classification %(linkto)s)"
-msgstr "création d'un code de mot-clé pour le plan de classement %(linkto)s"
-
-msgid "creating CodeKeyword (CodeKeyword subkeyword_of CodeKeyword %(linkto)s)"
-msgstr "création d'un sous code de mot-clé de %(linkto)s"
-
-msgid "creating Keyword (Keyword included_in Classification %(linkto)s)"
-msgstr "création d'un mot-clé pour le plan de classement %(linkto)s"
-
-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 "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é"
-
-msgid ""
-"entity types classified by the classification. Only entity type supporting "
-"the applied_to relation can be selectioned"
-msgstr ""
-
-msgid "facets_applied-to-facet"
-msgstr ""
-
-msgid "facets_applied-to-facet_description"
-msgstr ""
-
-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 "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"
-
-#, python-format
-msgid "keyword name changed from %(oldname)s to %(kw)s"
-msgstr "le mot-clé %(oldname)s a été renommé en %(kw)s"
-
-#, python-format
-msgid "keyword name changed from %s to %s"
-msgstr "le mot-clé %s a été renommé en %s"
-
-msgid "keywords applied"
-msgstr "mot-clé(s) appliqué(s)"
-
-msgid "keywords:"
-msgstr "mot-clés :"
-
-msgctxt "Classification"
-msgid "name"
-msgstr "nom"
-
-msgctxt "Keyword"
-msgid "name"
-msgstr "nom"
-
-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 "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 "which keyword (if any) this keyword specializes"
-msgstr "le mot-clé que celui-ci spécialise (le cas échéant)"
--- a/migration/1.0.2_Any.py	Wed Mar 05 18:27:01 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-synchronize_eschema('Classification') # index removed
--- a/migration/1.4.0_Any.py	Wed Mar 05 18:27:01 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-add_entity_type('CodeKeyword')
-
-add_relation_definition('Keyword', 'descendant_of', 'Keyword')
-sync_schema_props_perms('Keyword')
-checkpoint()
--- a/migration/postcreate.py	Wed Mar 05 18:27:01 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-# postcreate script. You could setup a workflow here for example
-# no explicit initial state for Keywords since it depends on the user's groups
--- a/schema.py	Wed Mar 05 18:27:01 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,82 +0,0 @@
-"""This cube handles classification schemes.
-
-:organization: Logilab
-:copyright: 2007-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-"""
-from yams.buildobjs import (EntityType, RelationType,
-                            SubjectRelation, ObjectRelation, String)
-
-from cubicweb.schema import RQLConstraint, ERQLExpression
-
-
-_ = unicode
-
-class Classification(EntityType):
-    """classification schemes are used by users to classify entities.
-    """
-    __permissions__ = {
-        'read' : ('managers', 'users', 'guests'),
-        'add' : ('managers',),
-        'delete' : ('managers',),
-        'update' : ('managers',),
-        }
-    name = String(required=True, fulltextindexed=True, unique=True,
-                  maxsize=128)
-    classifies = SubjectRelation('CWEType',
-                     # see relation type docstring
-                     constraints = [RQLConstraint('RDEF to_entity O,'
-                                                  'RDEF relation_type R,'
-                                                  'R name "applied_to"',
-                     msg="Classification is trying to classifies an EntityType "
-                         "without applied_to relation")])
-
-
-class classifies(RelationType):
-    """entity types classified by the classification. Only entity type
-    supporting the applied_to relation can be selectioned
-    """
-
-
-class Keyword(EntityType):
-    """A keyword is like a tag but is application specific
-    and used to define a classification scheme
-    """
-    __permissions__ = {
-        'read' : ('managers', 'users', 'guests'),
-        'add' : ('managers', 'users'),
-        'delete' : ('managers',),
-        'update' : ('managers',),
-        }
-    name = String(required=True, fulltextindexed=True, indexed=True, maxsize=128)
-
-    subkeyword_of = SubjectRelation('Keyword', cardinality='?*',
-                                    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*')
-
-
-class CodeKeyword(Keyword):
-    """A CodeKeyword is a keyword with a code and a name
-    """
-    __specializes_schema__ = True
-    code = String(required=True, fulltextindexed=True, indexed=True, maxsize=128)
-
-class subkeyword_of(RelationType):
-    """a keyword can specialize another keyword"""
-
-
-class included_in(RelationType):
-    """a keyword is included in a classification scheme"""
-    inlined = True
-
-
-# define in parent application which entity types may be linked to a keyword
-# by the applied_to relation
-
-class applied_to(RelationType):
-    """tagged objects"""
-    fulltext_container = 'object'
-    constraints = [RQLConstraint('S included_in CS, O is ET, CS classifies ET')]
--- a/setup.py	Wed Mar 05 18:27:01 2014 +0100
+++ b/setup.py	Wed Sep 27 09:08:03 2017 +0200
@@ -1,82 +1,82 @@
 #!/usr/bin/env python
-# pylint: disable-msg=W0404,W0622,W0704,W0613,W0152
-# Copyright (c) 2003-2009 LOGILAB S.A. (Paris, FRANCE).
-# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+# pylint: disable=W0142,W0403,W0404,W0613,W0622,W0622,W0704,R0904,C0103,E0611
+#
+# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of a cubicweb-keyword.
 #
-# This program is free software; you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free Software
-# Foundation; either version 2 of the License, or (at your option) any later
-# version.
+# 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.
 #
-# This program is distributed in the hope that it will be useful, but WITHOUT
+# 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 General Public License for more details.
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
 #
-# You should have received a copy of the GNU General Public License along with
-# this program; if not, write to the Free Software Foundation, Inc.,
-# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
-""" Generic Setup script, takes package info from __pkginfo__.py file """
+# You should have received a copy of the GNU Lesser General Public License
+# along with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""cubicweb_keyword setup module using data from
+cubicweb_keyword/__pkginfo__.py file
+"""
 
-import os
-import sys
-import shutil
-from distutils.core import setup
-from distutils import command
-from distutils.command import install_lib
-from os.path import isdir, exists, join, walk
+from os.path import join, dirname
 
-# import required features
-from __pkginfo__ import distname, version, license, short_desc, long_desc, \
-     web, author, author_email
-# import optional features
-try:
-    from __pkginfo__ import distname
-except ImportError:
-    distname = distname
-try:
-    from __pkginfo__ import scripts
-except ImportError:
-    scripts = []
-try:
-    from __pkginfo__ import data_files
-except ImportError:
-    data_files = None
-try:
-    from __pkginfo__ import include_dirs
-except ImportError:
-    include_dirs = []
-
-BASE_BLACKLIST = ('CVS', 'debian', 'dist', 'build', '__buildlog')
-IGNORED_EXTENSIONS = ('.pyc', '.pyo', '.elc')
+from setuptools import find_packages, setup
 
 
-def ensure_scripts(linux_scripts):
-    """creates the proper script names required for each platform
-    (taken from 4Suite)
-    """
-    from distutils import util
-    if util.get_platform()[:3] == 'win':
-        scripts_ = [script + '.bat' for script in linux_scripts]
-    else:
-        scripts_ = linux_scripts
-    return scripts_
+here = dirname(__file__)
+
+# load metadata from the __pkginfo__.py file so there is no risk of conflict
+# see https://packaging.python.org/en/latest/single_source_version.html
+pkginfo = join(here, 'cubicweb_keyword', '__pkginfo__.py')
+__pkginfo__ = {}
+with open(pkginfo) as f:
+    exec(f.read(), __pkginfo__)
 
+# get required metadatas
+distname = __pkginfo__['distname']
+version = __pkginfo__['version']
+license = __pkginfo__['license']
+description = __pkginfo__['description']
+web = __pkginfo__['web']
+author = __pkginfo__['author']
+author_email = __pkginfo__['author_email']
+classifiers = __pkginfo__['classifiers']
+
+with open(join(here, 'README')) as f:
+    long_description = f.read()
+
+# get optional metadatas
+data_files = __pkginfo__.get('data_files', None)
+dependency_links = __pkginfo__.get('dependency_links', ())
+
+requires = {}
+for entry in ("__depends__",):  # "__recommends__"):
+    requires.update(__pkginfo__.get(entry, {}))
+install_requires = ["{0} {1}".format(d, v and v or "").strip()
+                    for d, v in requires.items()]
 
 
-def install(**kwargs):
-    """setup entry point"""
-    #kwargs['distname'] = modname
-    return setup(name=distname,
-                 version=version,
-                 license =license,
-                 description=short_desc,
-                 long_description=long_desc,
-                 author=author,
-                 author_email=author_email,
-                 url=web,
-                 scripts=ensure_scripts(scripts),
-                 data_files=data_files,
-                 **kwargs)
-
-if __name__ == '__main__' :
-    install()
+setup(
+    name=distname,
+    version=version,
+    license=license,
+    description=description,
+    long_description=long_description,
+    author=author,
+    author_email=author_email,
+    url=web,
+    classifiers=classifiers,
+    packages=find_packages(exclude=['test']),
+    install_requires=install_requires,
+    include_package_data=True,
+    entry_points={
+        'cubicweb.cubes': [
+            'keyword=cubicweb_keyword',
+        ],
+    },
+    zip_safe=False,
+)
--- a/sobjects.py	Wed Mar 05 18:27:01 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-"""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.predicates import is_instance
-from cubicweb.sobjects.notification import NotificationView
-
-
-class KeywordNotificationView(NotificationView):
-    __select__ = is_instance('Keyword')
-    msgid_timestamp = True
-
-    def recipients(self):
-        """Returns the project's interested people (entities)"""
-        creator = self.cw_rset.get_entity(0, 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.cw_rset.get_entity(0, 0)
-        context['kw'] = entity.name
-        return context
-
-
-class KeywordNameChanged(KeywordNotificationView):
-    __regid__ = '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.cw_rset.get_entity(0, 0)
-        context = KeywordNotificationView.context(self, **kwargs)
-        context['oldname'] = self.get_oldname(entity)
-        return context
-
-    def subject(self):
-        entity = self.cw_rset.get_entity(0, 0)
-        return self.req._('keyword name changed from %s to %s') % (
-            self.get_oldname(entity), entity.name)
--- a/test/test_keyword.py	Wed Mar 05 18:27:01 2014 +0100
+++ b/test/test_keyword.py	Wed Sep 27 09:08:03 2017 +0200
@@ -2,12 +2,12 @@
 from cubicweb.devtools.testlib import AutomaticWebTest
 
 class AutomaticWebTest(AutomaticWebTest):
-
+    no_auto_populate = ('Keyword', 'CodeKeyword')
     ignored_relations = set(('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'))
+        # only test the cube related entities
+        return set(('Classification', 'Keyword', 'CodeKeyword'))
 
     def list_startup_views(self):
         return ()
--- a/test/unittest_descendant_of.py	Wed Mar 05 18:27:01 2014 +0100
+++ b/test/unittest_descendant_of.py	Wed Sep 27 09:08:03 2017 +0200
@@ -20,10 +20,10 @@
         self.commit()
         parent = kw1
         child = kw5
-        self.assertItemsEqual([kw.name for kw in child.cw_adapt_to('ITree').iterparents()], ['kw4', 'kw3', 'kw2', 'kw1'])
-        self.assertItemsEqual([kw.name for kw in child.descendant_of], ['kw4', 'kw3', 'kw2', 'kw1'])
-        self.assertItemsEqual([kw.name for kw in parent.reverse_descendant_of], ['kw5', 'kw4', 'kw3', 'kw2'])
-        self.assertItemsEqual([kw.name for kw in parent.cw_adapt_to('ITree').recurse_children()], ['kw5', 'kw4', 'kw3', 'kw2'])
+        self.assertCountEqual([kw.name for kw in child.cw_adapt_to('ITree').iterparents()], ['kw4', 'kw3', 'kw2', 'kw1'])
+        self.assertCountEqual([kw.name for kw in child.descendant_of], ['kw4', 'kw3', 'kw2', 'kw1'])
+        self.assertCountEqual([kw.name for kw in parent.reverse_descendant_of], ['kw5', 'kw4', 'kw3', 'kw2'])
+        self.assertCountEqual([kw.name for kw in parent.cw_adapt_to('ITree').recurse_children()], ['kw5', 'kw4', 'kw3', 'kw2'])
 
     def test_keyword_add2(self):
         req = self.request()
@@ -38,10 +38,10 @@
         self.commit()
         parent = kw1
         child = kw5
-        self.assertItemsEqual([kw.name for kw in child.cw_adapt_to('ITree').iterparents()], ['kw4', 'kw3', 'kw2', 'kw1'])
-        self.assertItemsEqual([kw.name for kw in child.descendant_of], ['kw4', 'kw3', 'kw2', 'kw1'])
-        self.assertItemsEqual([kw.name for kw in parent.reverse_descendant_of], ['kw5', 'kw4', 'kw3', 'kw2'])
-        self.assertItemsEqual([kw.name for kw in parent.cw_adapt_to('ITree').recurse_children()], ['kw5', 'kw4', 'kw3', 'kw2'])
+        self.assertCountEqual([kw.name for kw in child.cw_adapt_to('ITree').iterparents()], ['kw4', 'kw3', 'kw2', 'kw1'])
+        self.assertCountEqual([kw.name for kw in child.descendant_of], ['kw4', 'kw3', 'kw2', 'kw1'])
+        self.assertCountEqual([kw.name for kw in parent.reverse_descendant_of], ['kw5', 'kw4', 'kw3', 'kw2'])
+        self.assertCountEqual([kw.name for kw in parent.cw_adapt_to('ITree').recurse_children()], ['kw5', 'kw4', 'kw3', 'kw2'])
 
     def test_keyword_add3(self):
         req = self.request()
@@ -58,11 +58,11 @@
         self.commit()
         child  = kw5
         parent = kw1
-        self.assertItemsEqual([kw.name for kw in child.descendant_of], ['kw4', 'kw3', 'kw2', 'kw1'])
+        self.assertCountEqual([kw.name for kw in child.descendant_of], ['kw4', 'kw3', 'kw2', 'kw1'])
         # XXX check the order of iterparents
-        self.assertItemsEqual([kw.name for kw in child.cw_adapt_to('ITree').iterparents()], ['kw4', 'kw3', 'kw2', 'kw1'])
-        self.assertItemsEqual([kw.name for kw in parent.cw_adapt_to('ITree').recurse_children()], ['kw2', 'kw3', 'kw4', 'kw5'])
-        self.assertItemsEqual([kw.name for kw in parent.reverse_descendant_of], ['kw2', 'kw3', 'kw4', 'kw5'])
+        self.assertCountEqual([kw.name for kw in child.cw_adapt_to('ITree').iterparents()], ['kw4', 'kw3', 'kw2', 'kw1'])
+        self.assertCountEqual([kw.name for kw in parent.cw_adapt_to('ITree').recurse_children()], ['kw2', 'kw3', 'kw4', 'kw5'])
+        self.assertCountEqual([kw.name for kw in parent.reverse_descendant_of], ['kw2', 'kw3', 'kw4', 'kw5'])
 
     def test_keyword_add4(self):
         req = self.request()
@@ -75,22 +75,22 @@
         self.execute('SET K3 subkeyword_of K2 WHERE K3 eid %(kw3)s, K2 eid %(kw2)s',
                      {'kw2': kw2.eid, 'kw3': kw3.eid})
         self.commit();
-        self.assertItemsEqual([kw.name for kw in kw3.descendant_of], ['kw1', 'kw2'])
+        self.assertCountEqual([kw.name for kw in kw3.descendant_of], ['kw1', 'kw2'])
         self.execute('SET K3 descendant_of K0 WHERE K3 eid %(kw3)s, K0 eid %(kw0)s',
                       {'kw3': kw3.eid, 'kw0': kw0.eid})
         self.commit()
         kw3.cw_clear_all_caches()
-        self.assertItemsEqual([kw.name for kw in kw3.descendant_of], ['kw0', 'kw1', 'kw2'])
+        self.assertCountEqual([kw.name for kw in kw3.descendant_of], ['kw0', 'kw1', 'kw2'])
         self.execute('SET K3 descendant_of K4 WHERE K3 eid %(kw3)s, K4 eid %(kw4)s',
                       {'kw3': kw3.eid, 'kw4': kw4.eid})
         self.commit()
         kw3.cw_clear_all_caches()
-        self.assertItemsEqual([kw.name for kw in kw3.descendant_of], ['kw0', 'kw1', 'kw2', 'kw4'])
+        self.assertCountEqual([kw.name for kw in kw3.descendant_of], ['kw0', 'kw1', 'kw2', 'kw4'])
         self.execute('SET K3 descendant_of K5 WHERE K3 eid %(kw3)s, K5 eid %(kw5)s',
                        {'kw3': kw3.eid, 'kw5': kw5.eid})
         self.commit()
         kw3.cw_clear_all_caches()
-        self.assertItemsEqual([kw.name for kw in kw3.descendant_of], ['kw0', 'kw1', 'kw2', 'kw4', 'kw5'])
+        self.assertCountEqual([kw.name for kw in kw3.descendant_of], ['kw0', 'kw1', 'kw2', 'kw4', 'kw5'])
 
     def test_keyword_update1(self):
         req = self.request()
@@ -103,17 +103,17 @@
                       {'kw3': kw3.eid, 'kw2': kw2.eid})
         self.commit();
         kw3 =  req.entity_from_eid(kw3.eid)
-        self.assertItemsEqual([kw.name for kw in kw3.descendant_of], ['kw1', 'kw2'])
+        self.assertCountEqual([kw.name for kw in kw3.descendant_of], ['kw1', 'kw2'])
         self.execute('SET K3 subkeyword_of K4 WHERE K3 eid %(kw3)s, K4 eid %(kw4)s',
                       {'kw3': kw3.eid, 'kw4': kw4.eid})
         self.commit()
         kw3.cw_clear_all_caches()
-        self.assertItemsEqual([kw.name for kw in kw3.descendant_of], ['kw4'])
+        self.assertCountEqual([kw.name for kw in kw3.descendant_of], ['kw4'])
         self.execute('SET K3 subkeyword_of K5 WHERE K3 eid %(kw3)s, K5 eid %(kw5)s',
                      {'kw3': kw3.eid, 'kw5': kw5.eid})
         self.commit()
         kw3.cw_clear_all_caches()
-        self.assertItemsEqual([kw.name for kw in kw3.descendant_of], ['kw4', 'kw5'])
+        self.assertCountEqual([kw.name for kw in kw3.descendant_of], ['kw4', 'kw5'])
 
     def test_keyword_descendant_of(self):
         req = self.request()
@@ -121,10 +121,10 @@
         kw2 = req.create_entity('Keyword', name=u'kw2', subkeyword_of=kw1, included_in=self.classif1)
         kw3 = req.create_entity('Keyword', name=u'kw3', subkeyword_of=kw1, included_in=self.classif1)
         self.commit()
-        self.assertItemsEqual([kw.name for kw in kw2.descendant_of], ['kw1', ])
-        self.assertItemsEqual([kw.name for kw in kw3.descendant_of], ['kw1', ])
-        self.assertItemsEqual([kw.name for kw in kw1.reverse_descendant_of], ['kw3', 'kw2'])
-        self.assertItemsEqual([kw.name for kw in kw1.cw_adapt_to('ITree').recurse_children()], ['kw2', 'kw3'])
+        self.assertCountEqual([kw.name for kw in kw2.descendant_of], ['kw1', ])
+        self.assertCountEqual([kw.name for kw in kw3.descendant_of], ['kw1', ])
+        self.assertCountEqual([kw.name for kw in kw1.reverse_descendant_of], ['kw3', 'kw2'])
+        self.assertCountEqual([kw.name for kw in kw1.cw_adapt_to('ITree').recurse_children()], ['kw2', 'kw3'])
         kw0 = req.create_entity('Keyword', name=u'kw0', included_in=self.classif1)
         self.execute('SET K1 subkeyword_of K0 WHERE K1 eid %(kw1)s, K0 eid %(kw0)s',
                       {'kw1': kw1.eid, 'kw0': kw0.eid})
@@ -132,11 +132,11 @@
         kw1.cw_clear_all_caches()
         kw2.cw_clear_all_caches()
         kw3.cw_clear_all_caches()
-        self.assertItemsEqual([kw.name for kw in kw0.cw_adapt_to('ITree').recurse_children()], ['kw1', 'kw2', 'kw3'])
-        self.assertItemsEqual([kw.name for kw in kw0.reverse_descendant_of], ['kw3', 'kw2', 'kw1'])
-        self.assertItemsEqual([kw.name for kw in kw1.descendant_of], ['kw0',])
-        self.assertItemsEqual([kw.name for kw in kw2.descendant_of], ['kw1', 'kw0'])
-        self.assertItemsEqual([kw.name for kw in kw3.descendant_of], ['kw1', 'kw0'])
+        self.assertCountEqual([kw.name for kw in kw0.cw_adapt_to('ITree').recurse_children()], ['kw1', 'kw2', 'kw3'])
+        self.assertCountEqual([kw.name for kw in kw0.reverse_descendant_of], ['kw3', 'kw2', 'kw1'])
+        self.assertCountEqual([kw.name for kw in kw1.descendant_of], ['kw0',])
+        self.assertCountEqual([kw.name for kw in kw2.descendant_of], ['kw1', 'kw0'])
+        self.assertCountEqual([kw.name for kw in kw3.descendant_of], ['kw1', 'kw0'])
 
 
     def test_keyword_delete(self):
@@ -152,10 +152,10 @@
         self.execute('DELETE K subkeyword_of K3 WHERE K is Keyword, K eid %(kw3)s',
                      {'kw3':kw3.eid})
         self.commit()
-        self.assertItemsEqual([kw.name for kw in kw3.cw_adapt_to('ITree').iterparents()], [])
-        self.assertItemsEqual([kw.name for kw in kw3.descendant_of], [])
-        self.assertItemsEqual([kw.name for kw in kw3.reverse_descendant_of], ['kw5', 'kw4'])
-        self.assertItemsEqual([kw.name for kw in kw3.cw_adapt_to('ITree').recurse_children()], ['kw5', 'kw4'])
+        self.assertCountEqual([kw.name for kw in kw3.cw_adapt_to('ITree').iterparents()], [])
+        self.assertCountEqual([kw.name for kw in kw3.descendant_of], [])
+        self.assertCountEqual([kw.name for kw in kw3.reverse_descendant_of], ['kw5', 'kw4'])
+        self.assertCountEqual([kw.name for kw in kw3.cw_adapt_to('ITree').recurse_children()], ['kw5', 'kw4'])
 
     def test_no_add_descendant_cycle(self):
         """no ``descendant_of`` cycle"""
--- a/views.py	Wed Mar 05 18:27:01 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,274 +0,0 @@
-"""Specific 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 monkeypatch
-from logilab.mtconverter import xml_escape
-
-from cubicweb import Unauthorized
-from cubicweb.predicates import is_instance, rql_condition, relation_possible
-from cubicweb.view import EntityView
-from cubicweb.web import stdmsgs, uicfg, component, facet
-from cubicweb.web.views import primary, basecontrollers, treeview
-from cubicweb.web.views.ajaxcontroller import ajaxfunc
-
-
-_pvs = uicfg.primaryview_section
-_pvs.tag_object_of(('*', 'applied_to', '*'), 'hidden')
-_pvs.tag_subject_of(('*', 'applied_to', '*'), 'hidden')
-_pvs.tag_object_of(('*', 'included_in', 'Classification'), 'hidden')
-
-_abaa = uicfg.actionbox_appearsin_addmenu
-_abaa.tag_object_of(('CodeKeyword', 'included_in', 'Classification'), True)
-_abaa.tag_object_of(('Keyword', 'included_in', 'Classification'), True)
-_abaa.tag_object_of(('CodeKeyword', 'subkeyword_of', 'CodeKeyword'), True)
-_abaa.tag_object_of(('Keyword', 'subkeyword_of', 'Keyword'), True)
-
-
-# classification views ########################################################
-
-class ClassificationPrimaryView(primary.PrimaryView):
-    __select__ = is_instance('Classification')
-
-    def render_entity_attributes(self, entity):
-        pass
-
-    def render_entity_relations(self, entity):
-        rset = self._cw.execute('Any K ORDERBY N WHERE K included_in C, '
-                                'NOT K subkeyword_of KK, K name N, '
-                                'C eid %(x)s', {'x': entity.eid})
-        self.wview('treeview', rset, 'null')
-
-
-# keyword views ###############################################################
-
-class KeywordPrimaryView(primary.PrimaryView):
-    __select__ = is_instance('Keyword')
-
-    def cell_call(self, row, col, **kwargs):
-        entity = self.cw_rset.get_entity(row, col)
-        self.w(u'<h1 class="titleUnderline">%s</h1>'
-               % xml_escape(entity.dc_long_title()))
-        rset = entity.related('subkeyword_of','object')
-        self.wview('treeview', rset, 'null')
-
-
-class KeywordComboBoxView(treeview.TreePathView):
-    """display keyword in edition's combobox"""
-    __regid__ = 'combobox'
-    __select__ = is_instance('Keyword', 'Classification')
-
-    item_vid = 'text'
-    separator = u' > '
-
-# skos views ############################################################
-
-SKOS_OPENING_ROOT=u'''<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE rdfs [
-	<!ENTITY rdf "http://www.w3.org/1999/02/22-rdf-syntax-ns#">
-	<!ENTITY rdfs "http://www.w3.org/2000/01/rdf-schema#">
-	<!ENTITY dc "http://purl.org/dc/elements/1.1/">
-	<!ENTITY dct "http://purl.org/dc/terms/">
-]>
-<rdf:RDF  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-          xmlns:skos="http://www.w3.org/2004/02/skos/core#">
-'''
-SKOS_CLOSING_ROOT = u'</rdf:RDF>'
-
-class SkosView(EntityView):
-    __regid__ = 'skos'
-    content_type = 'application/xml'
-    templatable = False
-    __select__ = is_instance('Keyword', 'Classification')
-
-    def call(self, **kwargs):
-        self.w(SKOS_OPENING_ROOT)
-        for i in xrange(self.rset.rowcount):
-             self.cell_call(i, 0)
-        self.w(SKOS_CLOSING_ROOT)
-
-    def cell_call(self, row, col):
-        self.wview('skositemview', self.rset, row=row, col=col)
-
-class SkosItemView(EntityView):
-    __regid__ = 'skositemview'
-    content_type = 'application/xml'
-    __select__ = is_instance('Keyword', 'Classification')
-
-    def cell_call(self, row, col, show_parent=True, stop=False):
-        w = self.w
-        entity = self.complete_entity(row, col)
-        eschema = entity.e_schema
-        w(u'<skos:%s>' % eschema)
-        w(u'<skos:prefLabel>%s</skos:prefLabel>' % xml_escape(entity.name))
-        if not stop:
-            if show_parent and not entity.is_root():
-                par = entity.parent()
-                w(u'<skos:broader>')
-                par.view('skositemview', show_parent=False, stop=True, w=self.w)
-                w(u'</skos:broader>')
-            for child in entity.children(entities=True):
-                w(u'<skos:narrower>')
-                self.wview('skositemview', child.as_rset(), show_parent=False)
-                w(u'</skos:narrower>')
-        w(u'</skos:%s>' % eschema)
-
-
-# keyword component ###########################################################
-
-class KeywordBarVComponent(component.EntityVComponent):
-    """the keywords path bar: display keywords of a tagged entity"""
-    __regid__ = 'keywordsbar'
-    __select__ = (component.EntityVComponent.__select__ &
-                  relation_possible('applied_to', 'object', 'Keyword'))
-    context = 'header'
-    order = 152
-    htmlclass = 'navigation'
-
-    def get_keywords(self):
-        """helper method for subclasses redefinition"""
-        entity = self.cw_rset.get_entity(0, 0)
-        return entity.related('applied_to', 'object')
-
-    def call(self, **kwargs):
-        rset = self.get_keywords()
-        if rset:
-            self.w(u'<div class="%s" id="%s">\n' % (self.div_class(), self.div_id()))
-            self.w(u'<span>%s</span>&nbsp;' % self._cw._('keywords:'))
-            self.wview('csv', rset, 'null', done=set())
-            self.w(u'</div>\n')
-        else:
-            self.w(u'<div class="%s hidden" id="%s"></div>\n' % (
-                self.div_class(), self.div_id()))
-
-
-class AddKeywordVComponent(component.EntityVComponent):
-    """the 'add keyword' component"""
-    __regid__ = 'addkeywords'
-    __select__ = component.EntityVComponent.__select__ & \
-                 relation_possible('applied_to', 'object', 'Keyword', action='add') & \
-                 rql_condition('X is ET, CL classifies ET')
-
-    context = 'header'
-    order = 153
-    htmlclass = 'navigation'
-
-    def entity_call(self, entity, view=None):
-        self.add_js(['cubicweb.widgets.js', 'cubes.keyword.js'])
-        self._cw.add_css('cubicweb.suggest.css')
-        self.w(u'<table><tr><td>')
-        self.w(u'<a class="button sglink" href="javascript: showKeywordSelector(%s, \'%s\', \'%s\');">%s</a></td>' % (
-            entity.eid, self._cw._(stdmsgs.BUTTON_OK[0]),
-            self._cw._(stdmsgs.BUTTON_CANCEL[0]), self._cw._('add keywords')))
-        self.w(u'<td><div id="kwformholder"></div>')
-        self.w(u'</td></tr></table>')
-
-
-# applied_to relation facet ####################################################
-
-class AppliedToFacet(facet.RelationFacet):
-    __regid__ = 'applied-to-facet'
-    rtype = 'applied_to'
-    role = 'object'
-    target_attr = 'name'
-
-    def rset_vocabulary(self, rset):
-        _ = self._cw._
-        vocab = []
-        scheme = None
-        for e in sorted(rset.entities(),
-                        key=lambda e: (e.cw_adapt_to('ITree').classification.name,
-                                       e.view('combobox'))):
-            classification_name = e.cw_adapt_to('ITree').classification.name
-            if scheme != classification_name:
-                vocab.append( (_(classification_name), None) )
-            vocab.append( (e.view('combobox'), e.eid) )
-        return vocab
-
-
-class ClassificationFacet(facet.RelationFacet):
-    """abstract per-classification facet
-
-    subclasses must define their own id the classification name, e.g :
-
-    class Classifaction1Facet(ClassificationFacet):
-        __regid__ = 'classif1'
-        classification = u'classification1'
-
-    """
-    __abstract__ = True
-    classification = None
-    rtype = 'applied_to'
-    role = 'object'
-
-    def vocabulary(self):
-        """return vocabulary for this facet, eg a list of 2-uple (label, value)
-        """
-        rqlst = self.rqlst
-        rqlst.save_state()
-        try:
-            mainvar = self.filtered_variable # X
-            keyword_var = rqlst.make_variable() # K
-            keyword_name_var = rqlst.make_variable() # KN
-            classif_var = rqlst.make_variable() # C
-            classif_name_var = rqlst.make_variable() # CN
-            rqlst.add_relation(keyword_var, 'applied_to', mainvar) # K applied_to X
-            rqlst.add_relation(keyword_var, 'name', keyword_name_var) # K name KN
-            rqlst.add_relation(keyword_var, 'included_in', classif_var) # K included_in C
-            # C name "classification-name"
-            rqlst.add_constant_restriction(classif_var, 'name', self.classification, 'String')
-            rqlst.add_selected(keyword_var)
-            rqlst.add_selected(keyword_name_var)
-            # ORDERBY KN
-            rqlst.add_sort_var(keyword_name_var, True)
-            try:
-                rset = self._cw.execute(rqlst.as_string(), self.rset.args,
-                                        self.rset.cachekey)
-            except Unauthorized:
-                return []
-        finally:
-            rqlst.recover()
-        return self.rset_vocabulary(rset)
-
-    @property
-    def title(self):
-        return self._cw._(self.classification)
-
-    def support_and(self):
-        return False
-
-# add some classification schema related methods to the Jsoncontroller ########
-
-@ajaxfunc(output_type='json')
-def js_possible_keywords(self, eid):
-    rql = ('DISTINCT Any N WHERE K is Keyword, K name N, NOT K applied_to X, '
-           'X eid %(x)s, K included_in C, C classifies ET, X is ET')
-    rset = self.cursor.execute(rql, {'x' : eid, 'u' : self._cw.user.eid}, 'x')
-    return [name for (name,) in rset]
-
-@ajaxfunc(output_type='json')
-def js_add_keywords(self, eid, kwlist):
-    msg = self._cw._('keywords applied')
-    kwrset = self.cursor.execute('Any K,N,C WHERE K is Keyword, K name N, K included_in C, '
-                                 'C classifies ET, X eid %(x)s, X is ET',
-                                 {'x' : eid}, 'x')
-    if not kwrset:
-        return self._cw._('No suitable classification scheme found')
-    classification = kwrset[0][2] # XXX what if we have several classifications ?
-    valid_keywords = set(kwname for _, kwname,_ in kwrset)
-    user_keywords = set(kwlist)
-    invalid_keywords = sorted(user_keywords - valid_keywords)
-    kweids = dict( (kwname, str(kweid)) for kweid, kwname, _ in kwrset if kwname in user_keywords )
-    if invalid_keywords:
-        for keyword in invalid_keywords:
-            neweid = self.cursor.execute('INSERT Keyword K: K name %(name)s, K included_in C WHERE C eid %(c)s',
-                                         {'name' : keyword, 'c' : classification}, 'c')[0][0]
-            kweids[keyword] = str(neweid)
-    if kweids:
-        self.cursor.execute('SET KW applied_to X WHERE X eid %%(x)s, KW eid IN (%s)'
-                            % ','.join(kweids.values()), {'x' : eid}, 'x')
-    return msg