克隆工作流程:发布一个对象,以及所有使用过的/引用的对象 [英] Plone workflow: Publish an object, and all used/referred objects as well

查看:114
本文介绍了克隆工作流程:发布一个对象,以及所有使用过的/引用的对象的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个带有Archetypes对象的Plone站点,该对象引用其他对象(通过UID).例如,发布news对象时,在text属性中引用的所有image对象也应自动发布.

存在三种不同类型的出版物-对所有人"(任何人都可见),可见"(对于经过身份验证的用户)和受限制"(对于某些组的成员).根据对象的类型确定选择这些对象中的哪一个(首先).用户仅批准"该对象,然后自动选择发布的类型.

要实现这一点,我有一个changestate浏览器:它从text/html字段中提取所有使用过的对象的UID,并将适当的工作流程转换应用于它们.

这已经工作了好几年了,但是不再起作用了. 也许是因为某些交易问题?

但是,也许我当前的解决方案太复杂了.

应该是一种非常普遍的情况:发布新闻"时,它的所有页面必要条件" (仅引用而不是包含,因为任何图片都可能被一个以上的新闻使用对象)也应该发布.必须有一些标准解决方案",对吧?

如果还没有标准"或最佳实践"解决方案,那么我也对可能的交易陷阱感兴趣.

解决方案

假设您确定,隐式发布引用不会导致意想不到的结果(想象一个编辑器会像这样:我将其放在草稿中,声明并用机密注释修饰它,这是临时的,直到准备出版,到底是谁出版的?"),并为所有内容类型分配了工作流程,下面的代码是您所描述的算法的实现.

如果您有未分配工作流的内容类型项,则在分配了工作流的下一个上级父项上,必须进行隐式发布.但是,如果第一位父母也没有分配工作流程,这也会更改该项目的兄弟姐妹甚至表亲和姨妈的继承权限.但是,您可以这样做,在这种情况下,在代码中搜索无情"并按照注释的说明进行操作,但是在这里,将工作流分配给所有内容类型似乎更可取.

要考虑向后引用,当将引用项更改为比当前状态低的公共状态时,将向用户发出警告,提示引用项的读者可能无法再访问该项,因此自动正如卢卡·法布里(Luca Fabbri)指出的那样,向下发布"是不可取的.

位于哪个状态的定义比另一个状态更公开,位于PublicRank.states中,您需要将其调整为所使用的工作流程的状态.

好,所以涉及到两个文件,首先在your.addon/your/addon/configure.zcml中注册两个事件处理程序:

  <!--  A state has changed, execute 'publishReferences': -->
  <subscriber for="Products.CMFCore.interfaces.IContentish
                   Products.CMFCore.interfaces.IActionSucceededEvent"
          handler=".subscriber.publishReferences" />

  <!--  A state is about to be changed, execute 'warnAbout...': -->
  <subscriber for="Products.CMFCore.interfaces.IContentish
                   Products.DCWorkflow.interfaces.IBeforeTransitionEvent"
          handler=".subscriber.warnAboutPossiblyInaccessibleBackReferences" />

然后添加具有以下内容的your.addon/your/addon/subscriber.py:

from Products.statusmessages.interfaces import IStatusMessage
from zope.globalrequest import getRequest


class PublicRank:
    """
    Define which state is to be considered more public than another,
    most public first. Assume for now, only Plone's default workflow
    'simple_publication_workflow' is used in the portal.
    """
    states = ['published', 'pending', 'private']

def isMorePublic(state_one, state_two):
    """
    Check if state_one has a lesser index in the rank than state_two.
    """
    states = PublicRank.states
    if states.index(state_one) < states.index(state_two): return True
    else: return False

def getState(obj):
    """
    Return workflow-state-id or None, if no workflow is assigned.
    Show possible error on the console and log it.
    """
    if hasWorkflow(obj):
        try: return obj.portal_workflow.getInfoFor(obj, 'review_state')
        except ExceptionError as err: obj.plone_log(err)
    else: return None

def getTransitions(obj):
    """
    Return the identifiers of the available transitions as a list.
    """
    transitions = []
    trans_dicts = obj.portal_workflow.getTransitionsFor(obj)
    for trans_dict in trans_dicts:
        transitions.append(trans_dict['id'])
    return transitions

def hasWorkflow(obj):
    """
    Return boolean, indicating whether obj has a workflow assigned, or not.
    """
    return len(obj.portal_workflow.getWorkflowsFor(obj)) > 0

def hasTransition(obj, transition):
    if transition in getTransitions(obj): return True
    else: return False

def isSite(obj):
    return len(obj.getPhysicalPath()) == 2

def publishReferences(obj, eve, RUHTLESS=False):
    """
    If an obj gets published, publish its references, too.
    If an item doesn't have a workflow assigned and RUHTLESS
    is passed to be True, publish next upper parent with a workflow.
    """
    states = PublicRank.states
    state = getState(obj)
    transition = eve.action

    if state in states:
        refs = obj.getRefs()
        for ref in refs:
            ref_state = getState(ref)
            if ref_state:
                if isMorePublic(state, ref_state):
                    setState(ref, transition)
            else: # no workflow assigned
                if RUTHLESS:
                    setStateRelentlessly(ref, transition)

def setState(obj, transition):
    """
    Execute transition, return possible error as an UI-message,
    instead of consuming the whole content-area with a raised Exeption.
    """
    path = '/'.join(obj.getPhysicalPath())
    messages = IStatusMessage(getRequest())
    if hasWorkflow(obj):
        if hasTransition(obj, transition):
            try:
                obj.portal_workflow.doActionFor(obj, transition)
            except Exception as error:
                messages.add(error, type=u'error')
        else:
            message = 'The transition "%s" is not available for "%s".'\
                       % (transition, path)
            messages.add(message, type=u'warning')
    else:
        message = 'No workflow retrievable for "%s".' % path
        messages.add(message, type=u'warning')

def setStateRelentlessly(obj, transition):
    """
    If obj has no workflow, change state of next
    upper parent which has a workflow, instead.
    """
    while not getState(obj, state):
        obj = obj.getParentNode()
        if isSite(obj): break
    setState(obj, transition)

def warnAboutPossiblyInaccessibleBackReferences(obj, eve):
    """
    If an obj is about to switch to a lesser public state than it
    has and is referenced of other item(s), show a warning message
    with the URL(s) of the referencing item(s), so the user can check,
    if the link is still accessible for the intended audience.
    """
    states = PublicRank.states
    item_path = '/'.join(obj.getPhysicalPath())[2:]
    target_state = str(eve.new_state).split(' ')[-1][:-1]
    refs = obj.getBackReferences()

    for ref in refs:
        ref_state = getState(ref)
        if isMorePublic(ref_state, target_state):
            ref_path = '/'.join(ref.getPhysicalPath())[2:]
            messages = IStatusMessage(getRequest())
            message = u'This item "%s" is now in a less published state than \
            item "%s" of which it is referenced by. You might want to check, \
            if this item can still be accessed by the intended audience.' \
            % (item_path, ref_path)
            messages.add(message, type=u'warning')

I have a Plone site with Archetypes objects which refer to other objects (by UID). When, say, a news object is published, all image objects which are referred in the text attribute, should automatically be published, too.

There are three different kinds of publication - "for all" (visible for anyone), "visible" (for authenticated users), and "restricted" (for members of certain groups). Which one of these is chosen (in the first place) is determined from the type of the objects. The user only "approves" the object, and the kind of publication is chosen automatically.

To achieve this, I have a changestate browser: It extracts the UIDs of all used objects from the text/html fields and applies the appropriate workflow transitions to them.

This has worked for some years but doesn't work anymore; perhaps because of some transaction problems?

However, perhaps my current solution is far too complicated.

It should be a quite common situation: When a "news" is published, all "page requisites" of it (which are only referred, rather than contained, since any image might be used by more than one news object) should be published as well. There must be some "standard solution", right?

If there is no "standard" or "best practice" solution yet, I'm interested in possible transaction gotchas etc. as well.

解决方案

Assuming you're sure, that implicitly publishing references doesn't lead to unintended results (imagine an editor go like: "I'd put this in draft-state and decorated it with confidential comments, meant to be temporary until ready for publication, who the heck published this?") and that all content-types have a workflow assigned, the code below is a realization of the algorithm you describe.

In case you have content-type-items which don't have a workflow assigned, an implicit publication would be necessary on the next upper parent with a workflow assigned. But that also changes the inherited permissions of the item's siblings or even cousins and aunts, if the first parent also doesn't have a workflow assigned. However, you could do that, search for "ruthless" in the code and follow the comment's instruction, in that case, but assigning a workflow to all content-types seems more recommendable, here.

To regard back-references, when changing a referenced item to a lower public state than the current state, the user will be informed with a warning that it might not be accessible anymore to the audience of the referencing item, thus an automatic "down-publishing" isn't desirable, as Luca Fabbri points out.

The definition of which state is considered to be more public than another, lives in PublicRank.states, you'd need to adjust that to the states of the workflow(s) you use.

Ok, so it's about two files involved, first register two event-handlers in your.addon/your/addon/configure.zcml:

  <!--  A state has changed, execute 'publishReferences': -->
  <subscriber for="Products.CMFCore.interfaces.IContentish
                   Products.CMFCore.interfaces.IActionSucceededEvent"
          handler=".subscriber.publishReferences" />

  <!--  A state is about to be changed, execute 'warnAbout...': -->
  <subscriber for="Products.CMFCore.interfaces.IContentish
                   Products.DCWorkflow.interfaces.IBeforeTransitionEvent"
          handler=".subscriber.warnAboutPossiblyInaccessibleBackReferences" />

And then add your.addon/your/addon/subscriber.py with the following content:

from Products.statusmessages.interfaces import IStatusMessage
from zope.globalrequest import getRequest


class PublicRank:
    """
    Define which state is to be considered more public than another,
    most public first. Assume for now, only Plone's default workflow
    'simple_publication_workflow' is used in the portal.
    """
    states = ['published', 'pending', 'private']

def isMorePublic(state_one, state_two):
    """
    Check if state_one has a lesser index in the rank than state_two.
    """
    states = PublicRank.states
    if states.index(state_one) < states.index(state_two): return True
    else: return False

def getState(obj):
    """
    Return workflow-state-id or None, if no workflow is assigned.
    Show possible error on the console and log it.
    """
    if hasWorkflow(obj):
        try: return obj.portal_workflow.getInfoFor(obj, 'review_state')
        except ExceptionError as err: obj.plone_log(err)
    else: return None

def getTransitions(obj):
    """
    Return the identifiers of the available transitions as a list.
    """
    transitions = []
    trans_dicts = obj.portal_workflow.getTransitionsFor(obj)
    for trans_dict in trans_dicts:
        transitions.append(trans_dict['id'])
    return transitions

def hasWorkflow(obj):
    """
    Return boolean, indicating whether obj has a workflow assigned, or not.
    """
    return len(obj.portal_workflow.getWorkflowsFor(obj)) > 0

def hasTransition(obj, transition):
    if transition in getTransitions(obj): return True
    else: return False

def isSite(obj):
    return len(obj.getPhysicalPath()) == 2

def publishReferences(obj, eve, RUHTLESS=False):
    """
    If an obj gets published, publish its references, too.
    If an item doesn't have a workflow assigned and RUHTLESS
    is passed to be True, publish next upper parent with a workflow.
    """
    states = PublicRank.states
    state = getState(obj)
    transition = eve.action

    if state in states:
        refs = obj.getRefs()
        for ref in refs:
            ref_state = getState(ref)
            if ref_state:
                if isMorePublic(state, ref_state):
                    setState(ref, transition)
            else: # no workflow assigned
                if RUTHLESS:
                    setStateRelentlessly(ref, transition)

def setState(obj, transition):
    """
    Execute transition, return possible error as an UI-message,
    instead of consuming the whole content-area with a raised Exeption.
    """
    path = '/'.join(obj.getPhysicalPath())
    messages = IStatusMessage(getRequest())
    if hasWorkflow(obj):
        if hasTransition(obj, transition):
            try:
                obj.portal_workflow.doActionFor(obj, transition)
            except Exception as error:
                messages.add(error, type=u'error')
        else:
            message = 'The transition "%s" is not available for "%s".'\
                       % (transition, path)
            messages.add(message, type=u'warning')
    else:
        message = 'No workflow retrievable for "%s".' % path
        messages.add(message, type=u'warning')

def setStateRelentlessly(obj, transition):
    """
    If obj has no workflow, change state of next
    upper parent which has a workflow, instead.
    """
    while not getState(obj, state):
        obj = obj.getParentNode()
        if isSite(obj): break
    setState(obj, transition)

def warnAboutPossiblyInaccessibleBackReferences(obj, eve):
    """
    If an obj is about to switch to a lesser public state than it
    has and is referenced of other item(s), show a warning message
    with the URL(s) of the referencing item(s), so the user can check,
    if the link is still accessible for the intended audience.
    """
    states = PublicRank.states
    item_path = '/'.join(obj.getPhysicalPath())[2:]
    target_state = str(eve.new_state).split(' ')[-1][:-1]
    refs = obj.getBackReferences()

    for ref in refs:
        ref_state = getState(ref)
        if isMorePublic(ref_state, target_state):
            ref_path = '/'.join(ref.getPhysicalPath())[2:]
            messages = IStatusMessage(getRequest())
            message = u'This item "%s" is now in a less published state than \
            item "%s" of which it is referenced by. You might want to check, \
            if this item can still be accessed by the intended audience.' \
            % (item_path, ref_path)
            messages.add(message, type=u'warning')

这篇关于克隆工作流程:发布一个对象,以及所有使用过的/引用的对象的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆