问题
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.
回答1:
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')
回答2:
Here is what I did in the end:
I refactored my workflow as follows:
- It doesn't choose transitions automatically anymore (for publication levels) but lets the user explicitly specify
for all
,visible
orrestricted
. - I added transitions to switch publication levels (for cases of mistakes).
- These transitions are named differently depending on the direction; e.g., to switch a
restricted
item tovisible
,make_visible
is used, while the transition to make apublished
itemvisible
is calledmake_visible_again
. This way, refered items will be affected in the right direction only (not perfect but an improvement).
The problem with non-working publications of refered objects was caused by transactions; moving those from /temp/
to their public location had involved transaction.commit()
as well.
Finally, a little gotcha: If a workflow is switched, all review states are initialized anew - it doesn't matter that both the variable name and the states remained unchanged. Thus, it doesn't work to rename the workflow at this occasion. The original workflow states can be restored by switching back to the old workflow (with the same name but updated functionality).
来源:https://stackoverflow.com/questions/38440066/plone-workflow-publish-an-object-and-all-used-referred-objects-as-well