Source code for collective.multiworkflow.querystring.query_index_modifiers

"""Redirect ``review_state`` collection queries that name a workflow onto the chain
index.

A query is moved only when one of its values is qualified. ``review_state`` is
still a catalog index of its own, maintained by whichever workflow drives each
object's ``review_state``, so a criterion written before this add-on was
installed — carrying a bare ``published`` — is left on it and answers exactly as
it always did. The collection editor offers only qualified values, so a criterion
saved from it targets ``workflow_states`` from then on.
"""

from ..utils.workflow import format_state
from ..utils.workflow import STATE_SEPARATOR
from ..utils.workflow import WORKFLOW_STATES
from collections.abc import Iterator
from plone import api
from plone.app.querystring.interfaces import IParsedQueryIndexModifier
from Products.CMFPlone.WorkflowTool import WorkflowTool
from typing import Any
from zope.interface import implementer


#: The stock catalog index the modifier is registered for.
REVIEW_STATE = "review_state"

#: The keys of a parsed query whose contents are state values.
VALUE_KEYS = ("query", "not")


[docs] def qualify(state: str) -> str: """Name the workflow a bare state id belongs to. Values in the index read ``<workflow-id>|<state-id>``, so a bare ``published`` matches nothing there. :class:`ReviewStateModifier` needs this only for a query mixing bare and qualified values, which it has to move as a whole: the catalog intersects the results of different indexes, so splitting the values between ``review_state`` and ``workflow_states`` would turn an *any of* into an *all of*. So a bare id is read as belonging to the site's default workflow, which is the same workflow the index always lists first. :param state: a state id, qualified or not. :returns: the value as the index holds it; unchanged when it was already qualified, or when the site declares no default chain to attribute it to. """ if STATE_SEPARATOR in state: return state # The tool is typed as the interface rather than the implementation. wftool: WorkflowTool = api.portal.get_tool("portal_workflow") # type: ignore[assignment] chain = wftool.getDefaultChain() if not chain: return state return format_state(chain[0], state)
[docs] @implementer(IParsedQueryIndexModifier) class ReviewStateModifier: """Move a parsed ``review_state`` query that names a workflow onto ``workflow_states``.""" def __call__(self, value: dict[str, Any]) -> tuple[str, dict[str, Any]]: """Choose the index to query, qualifying bare state ids when moving. A query none of whose values is qualified stays on ``review_state``, untouched. That index still holds what it held before this add-on was installed, so the query answers for every type, whichever workflow drives its ``review_state``. Qualifying it with the default workflow instead would silently drop the types configured with another one. A query with a qualified value moves to ``workflow_states``. Every key but the value-carrying ones is passed on untouched, and that matters more than it looks: ``plone.app.querystring`` parses the *all of* operation to ``{"query": [...], "operator": "and"}`` and *excludes* to ``{"not": [...]}``. Rebuilding the dict from ``query`` alone would drop the operator — silently turning an AND into a KeywordIndex's default OR — and would turn an exclusion into an empty query. :param value: the parsed query for the ``review_state`` index. :returns: the index to query, and the query to use. """ if not any(STATE_SEPARATOR in state for state in self._states(value)): return (REVIEW_STATE, value) modified = dict(value) for key in VALUE_KEYS: if key in modified: modified[key] = self._qualified(modified[key]) return (WORKFLOW_STATES, modified) def _states(self, value: dict[str, Any]) -> Iterator[str]: """Yield every state id a parsed query carries. :param value: the parsed query for the ``review_state`` index. :returns: the string values of its value-carrying keys, whether each key holds a lone string or a sequence of them. """ for key in VALUE_KEYS: item = value.get(key) if isinstance(item, str): yield item elif isinstance(item, list | tuple): yield from (state for state in item if isinstance(state, str)) def _qualified(self, value: Any) -> Any: """Qualify one parsed value, whatever shape it arrived in. A lone string becomes a one-item list: the index is a KeywordIndex, so callers reading the query back get a consistent shape either way. :param value: a state id, a sequence of them, or something else entirely — a date range, say, which is passed through untouched. :returns: the qualified equivalent. """ if isinstance(value, str): return [qualify(value)] if isinstance(value, list | tuple): return [qualify(item) if isinstance(item, str) else item for item in value] return value