Use collective.taxonomy and collective.collectionfilter together

Example: I have a taxonomy and the same subitems in every main category.

  • dog
    • green
    • red
    • blue
  • cat
    • green
    • red
    • blue
  • horse
    • green
    • red
    • blue

The tagging on the contentitem via multiselection (dog->green & horse ->red)

I need a collectionfilter portlet with


dog
cat
horse



green
red
blue


Results are: all green cats.

Is something like that realizable with this two add-ons? Or is this the wrong way?

I have never used collective.taxonomy in production, we have used our own simpler just 2 level taxonomy add'on collective.classifiers.

Last year we built this website with a view similar to your request with collective.collectionfilter: Vergelijk bekkens — Stroomgebiedbeheerplannen 2022-2027

There are graph which are labedl with a region, but also with environmental parameters, with 2 levels, like your animal/color example.

So technically/UI collective collectionfilter does have this option, but I think the technical thing to check is how your taxonomy is indexed. classifiers stores the whole path, so every selection combination is a unique path.

I did a comparison with collectionfilter and facetednavigation in a talk at the PloneConf last year. they both have their strengths.

(There's a Plone 5 branch on collective.classifiers. I haven't made a release as the resource registry its dict/modal edit widget is 3/4 broken in Plone 5.2. The add'on uses that as a control panel to edit the taxonomy, so you can only use GS to load the tags.)

Thanks for hint, a watched the talk on youtube. but i missing the interesting part of the custom group_by modifier for the collection portlet :wink: i will test it and report it.

Collectionfilter is about what you have in the metadata column of your index. I used c.taxonomy with a two level taxonomy and created two separate indexes (and indexers) to use it with collectionfilter.

1 Like

As far as i remember, the taxonomy allows you to define nested tags but saves them flattened as strings like this:

Tag Level 01 > Tag Level 02

So in the simples case you just use it in one index.
Or you create custom indexes which slit them as @agitator probably did.

The groupby modifiers take a bit more effort to understand but are necessary to get some things
working. As a full example, this is the code for the groupby modifiers for the screenshot above.

there are 2 indexes: classifier_themes that feeds the locations list (bekkens) and classifier_categories (parameters) that contains pollutants.

the categories is a 2 level one, but the as @MrTango mentioned to make selections it's flattened to keywords/strings. The trick is to include al sub levels as well.
So an item that has 'drukken > stikstof' is twice in the index, one for key of top level 'drukken' and for for key 'drukken > stikstof'

I can't remember if I made this one up myself, most likely is I copied/adapted something, but I'm using a helper class to create the groupby modifier where I pass in the vocabulary that is already available for my two indexes by collective.classifiers, collective.taxonomy will likely also make vocabularies available? It demonstrates using the display modifier and the sorting.

<adapter factory=".collectionfilter.groupby_modifier" name="modifier_1" />

does the zcml registration.

from collective.collectionfilter.interfaces import IGroupByCriteria
from collective.collectionfilter.interfaces import IGroupByModifier
from zope.component import adapter, getUtility
from zope.interface import implementer, provider
from zope.schema.interfaces import IVocabularyFactory
from zope.schema.interfaces import IContextAwareDefaultFactory
from zope.component.hooks import getSite

import logging

logger = logging.getLogger("sgbp.content")

class VocabularyLookup(object):

    vocabulary = None
    flatten = False

    def clean_title(self, term):
        result = term.title.split(" > ", 1)
        if len(result) > 1:
            return result[-1]
        elif len(result) == 1:
            return term.title

    def __init__(self, vocab, flatten=False, context=None):
        factory = getUtility(IVocabularyFactory, vocab)
        if not context:
            self.vocabulary = factory(getSite())
        else:
            self.vocabulary = factory(context)
        self.flatten = flatten
        self.terms = [
            self.clean_title(term) if flatten else term.title
            for term in self.vocabulary._terms
        ]
        self.sort_map = dict([(term[1], term[0]) for term in enumerate(self.terms)])

    def display(self, value):
        try:
            term = self.vocabulary.getTerm(value)
        except LookupError:
            logger.warn(
                "Could not find '{0}' in vocabulary {1}".format(value, self.vocabulary)
            )
            return value
        if self.flatten:
            return self.clean_title(term)
        return term.title

    def sortmap(self, value):
        val = value["title"]
        return self.sort_map.get(val, 0)


@implementer(IGroupByModifier)
@adapter(IGroupByCriteria)
def groupby_modifier(groupby):

    themes_label = VocabularyLookup("collective.classifiers.themes", flatten=True)
    categories_label = VocabularyLookup(
        "collective.classifiers.categories", flatten=False
    )

    groupby._groupby["classifiers_themes"] = {
        "index": "classifiers_themes",
        "metadata": "classifiers_themes",
        "display_modifier": themes_label.display,
        "sort_key_function": themes_label.sortmap,
        # "value_blacklist": ["bekkens", "grondwatersystemen"],
    }

    groupby._groupby["classifiers_categories"] = {
        "index": "classifiers_categories",
        "metadata": "classifiers_categories",
        "display_modifier": categories_label.display,
        "sort_key_function": categories_label.sortmap,
    }

I have found a working solution for me, thanks to all!

<configure
    xmlns="http://namespaces.zope.org/zope"
    xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
    xmlns:i18n="http://namespaces.zope.org/i18n"
    xmlns:plone="http://namespaces.plone.org/plone"
    xmlns:zcml="http://namespaces.zope.org/zcml">

  <adapter
      name="animal"
      factory=".indexers.AnimalIndexer" />

  <adapter
      name="color"
      factory=".indexers.ColorIndexer" />

</configure>
# indexer.py
from collective.taxonomy import PRETTY_PATH_SEPARATOR
from plone import api
from plone.dexterity.interfaces import IDexterityContent
from plone.indexer.decorator import indexer

def extract_taxonomy_item(context=None, name=None, level=0):
    TAXONOMY = name
    result = []
    tax_attribute = getattr(context, "taxonomy_{}".format(TAXONOMY), None)
    if tax_attribute is not None:
        view = api.portal.get().restrictedTraverse("++taxonomy++{}".format(TAXONOMY))
        keys, labels = zip(*view())
        tax_dict = dict(zip(keys, labels))
        for tax in tax_attribute:
            splitted = tax_dict[tax].split(PRETTY_PATH_SEPARATOR)
            if len(splitted) > level:
                label = splitted[level]
                result.append(label)
    return list(set(result))

@indexer(IDexterityContent)
def AnimalIndexer(obj):
    return extract_taxonomy_item(obj, "animal_color", 0)

@indexer(IDexterityContent)
def ColorIndexer(obj):
    return extract_taxonomy_item(obj, "animal_color", 1)
<!-- catalog.xml-->
<?xml version="1.0"?>
<object name="portal_catalog">
  <column value="color" />
  <column value="animal" />
</object>
# setuphandlers.py
def add_catalog_indexes(context):
    setup = api.portal.get_tool("portal_setup")
    setup.runImportStepFromProfile(PROFILE_ID, "catalog")
    catalog = api.portal.get_tool("portal_catalog")
    indexes = catalog.indexes()
    wanted = (
        ("color", "KeywordIndex"),
        ("animal", "KeywordIndex"),
    )
    indexables = []
    for name, meta_type in wanted:
        if name not in indexes:
            catalog.addIndex(name, meta_type)
            indexables.append(name)
            logger.info("Added %s for field %s.", meta_type, name)
    if len(indexables) > 0:
        logger.info("Indexing new indexes %s.", ", ".join(indexables))
        catalog.manage_reindexIndex(ids=indexables)
1 Like

BTW I integrated for a current project, collective.taxonomy better in collective.collectionfilter, so that all taxonomies are useable in collectionfilter portlets for example.
A release of both packages is coming soon.
It's a powerful combination for users, i must say.

2 Likes

Plone Foundation Code of Conduct