Source code for collective.multiworkflow.exportimport

"""Patches to ``plone.exportimport``, kept together for upstreaming.

`plone.exportimport` restores an object's workflow state by assigning
``obj.workflow_history`` directly. That fires no transition, so none of the
machinery that keeps workflow variables catalogued ever runs:
``WorkflowTool._reindexWorkflowVariables`` is only reached through
``_invokeWithNotification``.

``review_state`` escapes the problem because ``update_review_state`` performs a
real transition just before. Any *other* workflow variable does not, and its
index keeps whatever value the object was first catalogued with — which is
exactly the state an additional workflow is in.

Measured on this package's own example content, importing an object whose
membership workflow is ``active``::

    object   foundation_member_workflow: active
    catalog  foundation_member_workflow|pending      <- stale

The publication state is right either way, so the staleness reads as a correct
index rather than a broken one.

The fix belongs upstream, in ``update_workflow_history``. Until it lands there,
this module applies it as a wrapper, in one place, shaped so that the body of
:func:`reindex_workflow_variables` can be moved into `plone.exportimport`
unchanged.

The patch is applied when this subpackage's ZCML is loaded, which the root
``configure.zcml`` does.
"""

from .. import logger
from functools import wraps
from plone import api
from plone.dexterity.content import DexterityContent
from Products.CMFPlone.CatalogTool import CatalogTool
from Products.CMFPlone.WorkflowTool import WorkflowTool
from typing import Any


#: Set once the patch is in place, so a second ZCML load cannot wrap the
#: wrapper.
_applied = False


[docs] def reindex_workflow_variables(obj: DexterityContent) -> DexterityContent: """Reindex the catalog indexes named after the object's workflow variables. This is the body proposed for `plone.exportimport`: it names no index of this package's own, and it is a no-op on a site whose content runs a single workflow, because ``review_state`` is already fresh by the time it runs. Only variables the catalog actually has an index for are reindexed. A workflow declaring a state variable no index is named after is therefore left alone here, as it is everywhere else in Plone. :param obj: the object whose workflow history was just restored. :returns: the same object, for chaining. """ # The tools are typed as their interfaces rather than the implementations. wftool: WorkflowTool = api.portal.get_tool("portal_workflow") # type: ignore[assignment] catalog: CatalogTool = api.portal.get_tool("portal_catalog") # type: ignore[assignment] variables = wftool.getCatalogVariablesFor(obj) or {} idxs = [name for name in variables if name in catalog.indexes()] if idxs: obj.reindexObject(idxs=idxs) return obj
[docs] def apply_patches() -> None: """Wrap ``update_workflow_history`` so it reindexes what it changed. ``updaters()`` builds its list by reading the module globals each time it is called, so rebinding the module attribute is enough — no import in `plone.exportimport` holds an earlier reference to the original. Does nothing when `plone.exportimport` is absent. It is not a dependency of this package, nor of ``Products.CMFPlone``; it arrives with ``plone.distribution`` in a standard Plone installation. """ global _applied if _applied: return try: from plone.exportimport.utils.content import import_helpers except ImportError: logger.debug( "plone.exportimport is not installed; workflow variables will not " "be reindexed on import." ) return original = import_helpers.update_workflow_history # wraps() carries the docstring the importer logs as the updater's # description, and sets __wrapped__ — which is what lets the spike reach # the unpatched function and assert that upstream still needs this. @wraps(original) def update_workflow_history(item: dict, obj: Any) -> Any: """Restore the workflow history, then reindex what it changed. The reindex is conditional on the item actually carrying a history: the updater runs for every imported object, and most of them have nothing here to reindex for. """ obj = original(item, obj) if item.get("workflow_history"): reindex_workflow_variables(obj) return obj #: Lets a test assert the patch is in place without comparing function #: identity against a closure. update_workflow_history.patched_by = __name__ # type: ignore[attr-defined] import_helpers.update_workflow_history = update_workflow_history _applied = True logger.debug("Patched plone.exportimport.update_workflow_history.")
apply_patches()