Plone/Workflow- Is it possible to set the state of a workflow without needing a transition? - workflow

I have a content type (batch) that is tied to multiple instances of a different content (my_item) by an id. The my_item content type has a workflow consisting of draft, pending, and approved. There is a form that creates the batch and "approves" the my_item content type instances, and sets the my_item_instances' batch_id to the batch's batch id (set when the batch is created). The approved state is a final state, where it cannot be edited nor retracted.
I need to be able to change the state of the my_item content type instances back to draft. Since there is no transition for back to draft from the approved state when the item is being deleted (through a subscriber), I need to somehow set the state of the my_items back to "draft" without needing a transition.
There are two methods I tried:
The subscriber is IObjectWillBeRemoved
def my_item_will_be_removed(obj,event)
my_items = catalog.searchResults('batch_id':obj.batch_id)
for i in my_items:
api.content.transition(obj=i,to_state='pending')
This results in an error
InvalidParaemterError: Could not find workflow to set state to draft on
I also tried using:
wf_tool = api.portal.get_tool(name='portal_workflow')
wf_tool.setStatusOf('item_workflow',i,'pending')
For some reason that ends up with the my_item becoming a string.
Is it not possible? If it is possible, how can I do so?
Offtopic, but I guess a workaround I could use for now is:
make a transition "retract_from_approval" that goes from the approved state to the draft state
'can_retract_from_approval' needs to be assigned to the role that can delete the "batch"
In the deletion event, iterate through the my_items, assign the 'can_retract_from_approval' permission to the role responsible for deleting the batch locally on the current iteration
my_items = catalog.searchResults('batch_id',obj.batch_id)
for m in my_items:
mi_obj = m.getObject()
mi_obj.manage_permission('retract_from_approval',['ARole'],obj=mi_obj)
Then use the workflow tool to do the 'retract_from_approval' transition that sends the my_item back into the draft state. And then remove the 'can_retract_from_approval' permission.

This is a snippet of an old migration tool I used for migrate from Plone 2.5 to Plone 3 a lot of years ago.
wtool = getToolByName(obj, 'portal_workflow')
status = {'action': '',
'review_state': old_state,
'actor': 'admin',
'comments': 'Recovery state',
'time': DateTime() }
wtool.setStatusOf(workflow_id, obj, status)
not sure if it still works nowadays
you probably need a reindexObjectSecurity

Related

Microsoft Bot Framework - Multi turn context is lost after the first interaction

I recently moved my code from SDK v3 to v4 and I am trying to take advantage of the multi-turn features.
I have looked over the samples from GitHub. The samples work well for multi-turn but one issue I noticed is that it recognizes the context only if the prompt is clicked immediately after the initial answer (with prompts) is shown.
I would like to be able to identify, at any given time that a prompt is clicked. I am storing all the previous prompts in the state object (dialogInstance.State) already. I have a custom host, which sends the replytoid and using that I can get the appropriate state.
The problem is though, I am not able to get to a point where I can use the dialoginstance.State.
The sample code uses the DialogExtensions class. The "DialogExtensions" class tries to gather the previous context by checking if the result from the ContinueDialogAsync method returns null or not.
DialogExtensions class with multi-turn
When there is no previous context (no previous answer with prompts), then the call to the ContinueDialogAsync returns a result with Empty Status.
I am thinking this where I need to check the dialogstate and if the new message refers to any of the old messages at any given point, it can then start to continue the old conversation.
I am not sure if that is even possible.
Any help/pointers would be appreciated.
thanks,
I eventually ended up implementing something that will work for custom bot host/direct channel bot client.
The whole point is that the call to the qnamaker api should happen with the old context object, whenever an option is chosen, even if it is out-of-context.
First let me explain how it works in the current version of the code.
The way the bot code was trying to solve multi turn dialog, was by storing the current answer if it had prompts/options and returning the conversation state in "waiting" mode. When the next question is received, it would automatically assume that the new question is part of the prompts/options of the old question. It would then pass the oldstate along to the QnAMaker.
What I noticed is that, even if the question in the second turn is not part of the prompts/options (something the user has typed manually and is a completely different question), it would still send the oldstate object to the QnAMaker.
The QnAMaker api call seem to ignore oldstate if the new question is not part of the prompts/options of the oldstate. It will work correctly by fetching the answer for the new question that was typed manually.
This was the key. If we can focus on what gets to the qnamaker then we can solve our original problem.
I realized that having the bot return a waiting state is only a mechanism to create a condition to extract the oldstate in the next turn. However, if I can rebuild the oldstate anytime when there is an option chosen, then the call to the qnamaker would work equally well.
This is what I have done now.
In my custom bot host code (which is a direct line client), I am sending the ReplyToID field populated with the original question whenever a prompt is clicked. Then in the bot code, I have changed it so that if there is a replytoid present, then build a new oldstate object with the data from the reply to id. Below is the QnABotState class that represents the oldstate. its a very simple class containing previous qna question id and the question text.
public int PreviousQnaId { get; set; }
public string PreviousUserQuery { get; set; }
QnABoState class
Now, the problem was the Activity object contains ReplyToId but does not contain ReplyToQuery (or something like that). Activity object is used to send data from bot client to the bot. So, either I would have to use a different field or send the PreviousUserQuery as an empty string. I had a hunch that it would work with just the previousqnaid.
//starting the process to get the old context (create an object that will hold the Process function's current state from the dialog state)
//if there is replyToId field is present, then it is a direct channel request who is replying to an old context
//get the reply to id from summary field
var curReplyToId = "";
curReplyToId = dialogContext.Context.Activity.ReplyToId;
var curReplyToQuery = "";
var oldState = GetPersistedState(dialogContext.ActiveDialog);
//if oldstate is null also check if there is replytoid populated, if it is then it maybe a new conversation but it is actually an "out of turn option" selection.
if (oldState == null)
{
if (!string.IsNullOrEmpty(curReplyToId))
{
//curReplyToId is not empty. this is an option that was selected out-of-context
int prevQnaId = -1;
int.TryParse(curReplyToId, out prevQnaId);
oldState = new QnABotState() { PreviousQnaId = prevQnaId, PreviousUserQuery = curReplyToQuery };
}
}
With that in place, my call to the qnamaker api would receive an oldstate object even if it is called out-of-context.
I tried the code and it worked. Not having the previous qna query did not make a difference. It worked with just the PreviousQnaId field being populated.
However, please note, this will not work for other channels. It would work for channels where you can set the ReplyToId field, such as the Direct Channel Client.
here is the code from my bot host:
// to create a new message
Activity userMessage = new Activity
{
From = new ChannelAccount(User.Identity.Name),
Text = questionToBot,
Type = ActivityTypes.Message,
Value = paramChatCode,// + "|" + "ShahID-" + DateTime.Now.TimeOfDay,
Id = "ShahID-" + DateTime.Now.TimeOfDay,
ChannelData = botHostId//this will be added as the bot host identifier
};
//userMessage.Type = "imBack";
if (paramPreviousChatId > 0)
{
//we have a valid replytoid (as a part of dialog)
userMessage.ReplyToId = paramPreviousChatId.ToString();
}

How can I create and update pages dynamically in Sulu CMS?

I have the following situation:
A database stores information about houses (address, number of rooms, date built, last selling price, etc.)
This database is being manipulated through an app (let's call that app the "backend house app") that cannot be directly integrated in a Sulu-driven app. I can access the stored data through an API that gives me JSON-representations of House-objects. I can also have the app launch some sort of call to a Sulu-driven app when a house is created, updated or deleted.
The Sulu-driven app (let's call that the "frontend house app") with templates for "house", "room", etc., is connected to a different database on a different server. This Sulu-driven app's website-environment shows house-pages with room-pages where some content is pre-filled through a connection to the "backend house app". Other content only exists on the database of the "frontend house app", like user comments, appraisals of interior design, etc., according to configured aspects of the Sulu-templates.
What I want to achieve, is a way to automate the creation, updating and deletion of "frontend house app"-pages based on activity in the "backend house app".
For instance, when a new house is added in the "backend house app", I want it to notify the "frontend house app" so that the "frontend house app" will automatically create the entire node-tree for the newly added house. Meaning: a "house"-page with the required data filled in, "room"-pages for each room, etc., so that the content manager of the "frontend house app" can see the entire tree of the newly added house in the workspace and can start manipulating content in the already available templates. In addition to automatically creating these pages, I also want to pre-set the rights to update and create, since the content manager of the "frontend house app" must not be able to create new rooms or change the name of the house, for instance.
I did not manage to get it working, I'll just add what I already done to show where I got stuck.
I started out with the following code, in a controller that extends Sulu's own WebsiteController:
$documentManager = $this->get('sulu_document_manager.document_manager');
$nodeManager = $this->get('sulu_document_manager.node_manager');
$parentHousesDocument = $documentManager->find('/cmf/immo/routes/nl/huizen', 'nl');
$newHouseDocument = $documentManager->create('page');
// The backendApi just gives a House object with data from the backend
// In this case we get an existing House with id 1
$house = $backendApi->getHouseWithId(1);
$newHouseDocument->setTitle($house->getName()); // For instance 'Smurfhouse'
$newHouseDocument->setLocale('nl'); // Nl is the only locale we have
$newHouseDocument->setParent($parentHouseDocument); // A default page where all the houses are listed
$newHouseDocument->setStructureType('house'); // Since we have a house.xml template
// I need to grab the structure to fill it with values from the House object
$structure = $newHouseDocument->getStructure();
$structure->bind([
'title' => $house->getName(),
'houseId' => $house->getId(),
]);
$newHouseDocument->setWorkflowStage(WorkflowStage::PUBLISHED); // You would expect this to automatically publish the document, but apparently it doesn't... I took it from a test I reverse-engineered in trying to create a page, I have no clue what it is supposed to change.
$nodeManager->createPath('/cmf/immo/routes/nl/huizen/' . $house->getId());
$documentManager->persist(
$newHouseDocument,
'nl',
[
'path' => '/cmf/immo/contents/huizen/' . Slugifier::slugify($house->getName()), // Assume for argument's sake that the Slugifier just slugifies the name...
'auto_create' => true, // Took this value from a test that creates pages, don't know whether it is necessary
'load_ghost_content' => false, // Idem
]
);
$documentManager->flush();
Now, when I fire the controller action, I first get the exception
Property "url" in structure "house" is required but no value was given.
I tried to fix this by just manually binding the property 'url' with value '/huizen/' . $house->getId() to $structure, at the point where I bind the other values. But this doesn't fix it, as apparently the url value is overwritten somewhere in the persist event chain, and I haven't yet found where.
However, I can, just for testing purposes, manually override the url in the StructureSubscriber that handles the mapping for this particular persist event. If I do this, something gets created in the Sulu-app-database - hurray!
My phpcr_nodes table lists two extra records, one for the RouteDocument referring to /cmf/immo/routes/nl/huizen/1, and one for the PageDocument referring to /cmf/immo/contents/huizen/smurfhouse. Both have the workspace_name column filled with the value default_live. However, as long as there are not also records that are complete duplicates of these two records except with the value default in the workspace_name column, the pages will not appear in the Sulu admin CMS environment. Needless to say, they will also not appear on the public website proper.
Furthermore, when I let the DocumentManager in my controller action try to ->find my newly created document, I get a document of the class UnknownDocument. Hence, I cannot have the DocumentManager go ->publish on it; an Exception ensues. If I visit the pages in the Sulu admin environment, they are hence unpublished; once I publish them there, they can be found by the DocumentManager in the controller action - even if I later unpublish them. They are no longer UnknownDocument, for some reason. However, even if they can be found, I cannot have the DocumentManager go ->unpublish nor ->publish - that just has NO effect on the actual documents.
I was hoping there would be a Sulu cookbook-recipe or another piece of documentation that extensively describes how to create fully published pages dynamically, thus without going through the 'manual labor' of the actual CMS environment, but so far I haven't found one... All help is much appreciated :)
PS: For the purposes of being complete: we're running Sulu on a Windows server environment on PHP 7.1; dbase is PostgreSQL, Sulu being a local forked version of release tag 1.4.7 because I had to make some changes to the way Sulu handles uploaded files to get it to work on a Windows environment.
EDIT: a partial solution for making a new house page if none exists already (not explicitly using the AdminKernel, but should of course be run in a context where the AdminKernel is active):
public function getOrCreateHuisPagina(Huis $huis)
{
$parent = $this->documentManager->find('/cmf/immo/routes/nl/huizen', 'nl'); // This is indeed the route document for the "collector page" of all the houses, but this doesn't seem to give any problems (see below)
try {
$document = $this->documentManager->find('/cmf/immo/routes/nl/huizen/' . $huis->id(), 'nl'); // Here I'm checking whether the page already exists
} catch(DocumentNotFoundException $e) {
$document = $this->setupPublishedPage();
$document->setTitle($huis->naam());
$document->setStructureType('huis_detail');
$document->setResourceSegment('/huizen');
$document->setParent($parent);
$document->getStructure()->bind([
'title' => $huis->naam(), // Not sure if this is required seeing as I already set the title
'huis_id' => $huis->id(),
]);
$this->documentManager->persist(
$document,
'nl',
[
'parent_path' => '/cmf/immo/contents/huizen', // Explicit path to the content document of the parnt
]
);
}
$this->documentManager->publish($document, 'nl');
return $document;
}
First of all I think the following line does not load what you want it to load:
$parentHousesDocument = $documentManager->find('/cmf/immo/routes/nl/huizen', 'nl');
It loads the route instead of the page document, so it should look like the following:
$parentHousesDocument = $documentManager->find('/cmf/immo/contents/nl/huizen', 'nl');
Regarding your error with the URL, instead of overriding the StructureSubscriber you should simple use the setResourceSegment method of the document, which does exactly what you need :-)
And the default_live workspace is wrong, is it possible that you are running these commands on the website kernel? The thing is that the WebsiteKernel has the default_live workspace as default, and therefore writes the content in this workspace. If you run the command with the AdminKernel it should land in the default workspace, and you should be able to copy it into the default_live workspace with the publish method of the DocumentManager.
I hope that helps :-)

CQ5 / AEM5.6 Workflow: Access workflow instance properties from inside OR Split

TL;DR version:
In CQ workflows, is there a difference between what's available to the OR Split compared to the Process Step?
Is it possible to access the /history/ nodes of a workflow instance from within an OR Split?
How?!
The whole story:
I'm working on a workflow in CQ5 / AEM5.6.
In this workflow I have a custom dialog, which stores a couple of properties on the workflow instance.
The path to the property I'm having trouble with is: /workflow/instances/[this instance]/history/[workItem id]/workItem/metaData and I've called the property "reject-or-approve".
The dialog sets the property fine (via a dropdown that lets you set it to "reject" or "approve"), and I can access other properties on this node via a process step (in ecma script) using:
var actionReason;
var history = workflowSession.getHistory(workItem.getWorkflow());
// loop backwards through workItems
// and as soon as we find a Action Reason that is not empty
// store that as 'actionReason' and break.
for (var index = history.size() - 1; index >= 0; index--) {
var previous = history.get(index);
var tempActionReason = previous.getWorkItem().getMetaDataMap().get('action-message');
if ((tempActionReason != '')&&(tempActionReason != null)) {
actionReason = tempActionReason;
break;
}
}
The process step is not the problem though. Where I'm having trouble is when I try to do the same thing from inside an OR Split.
When I try the same workflowSession.getHistory(workItem.getWorkflow()) in an OR Split, it throws an error saying workItem is not defined.
I've tried storing this property on the payload instead (i.e. storing it under the page's jcr:content), and in that case the property does seem to be available to the OR Split, but my problems with that are:
This reject-or-approve property is only relevant to the current workflow instance, so storing it on the page's jcr:content doesn't really make sense. jcr:content properties will persist after the workflow is closed, and will be accessible to future workflow instances. I could work around this (i.e. don't let workflows do anything based on the property unless I'm sure this instance has written to the property already), but this doesn't feel right and is probably error-prone.
For some reason, when running through the custom dialog in my workflow, only the Admin user group seems to be able to write to the jcr:content property. When I use the dialog as any other user group (which I need to do for this workflow design), the dialog looks as though it's working, but never actually writes to the jcr:content property.
So for a couple of different reasons I'd rather keep this property local to the workflow instance instead of storing it on the page's jcr:content -- however, if anyone can think of a reason why my dialog isn't setting the property on the jcr:content when I use any group other than admin, that would give me a workaround even if it's not exactly the solution I'm looking for.
Thanks in advance if anyone can help! I know this is kind of obscure, but I've been stuck on it for ages.
a couple of days ago i ran into the same issue. The issue here is that you don't have the workItem object, because you don't really have an existing workItem. Imagine the following: you are going through the workflow, you got a couple of workItems, with means, either process step, either inbox item. When you are in an or split, you don't have existing workItems, you can ensure by visiting the /workItems node of the workflow instance. Your workaround seems to be the only way to go through this "issue".
I've solved it. It's not all that elegant looking, but it seems to be a pretty solid solution.
Here's some background:
Dialogs seem to reliably let you store properties either on:
the payload's jcr:content node (which wasn't practical for me, because the payload is locked during the workflow, and doesn't let non-admins write to its jcr:content)
the workItem/metaData for the current workflow step
However, Split steps don't have access to workItem. I found a fairly un-helpful confirmation of that here: http://blogs.adobe.com/dmcmahon/2013/03/26/cq5-failure-running-script-etcworkflowscriptscaworkitem-ecma-referenceerror-workitem-is-not-defined/
So basically the issue was, the Dialog step could store the property, but the OR Split couldn't access it.
My workaround was to add a Process step straight after the Dialog in my workflow. Process steps do have access to workItem, so they can read the property set by the Dialog. I never particularly wanted to store this data on the payload's jcr:content, so I looked for another location. It turns out the workflow metaData (at the top level of the workflow instance node, rather than workItem/metaData, which is inside the /history sub-node) is accessible to both the Process step and the OR Split. So, my Process step now reads the workItem's approveReject property (set by the Dialog), and then writes it to the workflow's metaData node. Then, the OR Split reads the property from its new location, and does its magic.
The way you access the workflow metaData from the Process step and the OR Split is not consistent, but you can get there from both.
Here's some code: (complete with comments. You're welcome)
In the dialog where you choose to approve or reject, the name of the field is set to rejectApprove. There's no ./ or anything before it. This tells it to store the property on the workItem/metaData node for the current workflow step under /history/.
Straight after the dialog, a Process step runs this:
var rejectApprove;
var history = workflowSession.getHistory(workItem.getWorkflow());
// loop backwards through workItems
// and as soon as we find a rejectApprove that is not empty
// store that as 'rejectApprove' and break.
for (var index = history.size() - 1; index >= 0; index--) {
var previous = history.get(index);
var tempRejectApprove = previous.getWorkItem().getMetaDataMap().get('rejectApprove');
if ((tempRejectApprove != '')&&(tempRejectApprove != null)) {
rejectApprove = tempRejectApprove;
break;
}
}
// steps up from the workflow step into the workflow metaData,
// and stores the rejectApprove property there
// (where it can be accessed by an OR Split)
workItem.getWorkflowData().getMetaData().put('rejectApprove', rejectApprove);
Then after the Process step, the OR Split has the following in its tabs:
function check() {
var match = 'approve';
if (workflowData.getMetaData().get('rejectApprove') == match) {
return true;
} else {
return false;
}
}
Note: use this for the tab for the "approve" path, then copy it and replace var match = 'approve' with var match = 'reject'
So the key here is that from a Process step:
workItem.getWorkflowData().getMetaData().put('rejectApprove', rejectApprove);
writes to the same property that:
workflowData.getMetaData().get('rejectApprove') reads from when you execute it in an OR Split.
To suit our business requirements, there's more to the workflow I've implemented than just this, but the method above seems to be a pretty reliable way to get values that are entered in a dialog, and access them from within an OR Split.
It seems pretty silly that the OR Split can't access the workItem directly, and I'd be interested to know if there's a less roundabout way of doing this, but for now this has solved my problem.
I really hope someone else has this same problem, and finds this useful, because it took me waaay to long to figure out, to only apply it once!

Plone 4 - History for second workflow won't show in ##historyview

I have a dexterity content type in Plone 4.2.4. The versioning works fine with the default workflow for this content type, although it is not a workflow shipped with plone, but a custom made.
However, when I enable a second workflow for the same type, everything but the versioning works fine.
additional permissions managed by the second workflow are working
The state changes are working
The difference:
I used different state_variable names for the workflows, which seems to make sense, to have a catalogable field for the state of the second workflow.
I've tried to use the same state variable name, but that didn't help. I have the workflow variable review_history also set in the 2nd workflow and sufficient permissions in the context.
I am (mostly) shure, that I got the permission concept, but I have no clou, how permissions get calculated, when multiple workflows are involved.
Any idea, why the second workflow does not leave a trace in my content types history?
Thanks very much in advance.
Udate
I've reordered the workflows as Ida Ebkes suggested and did see, that all transitions from the 2nd workflow get stored properly. So it seems to be an issue with the historyview.
Since these workflows indeed describe concurrent behaviors of a content type, I really would like to stick with separate workflows and ideally different workflow state variables and catalog indexes.
What I now think needs to be done, is to tweak the historyview.
Here is how I did it. It works for plone 4.2.4 at least.
Since the problem was a display problem, I just had to tweak my historyviewlet. Therefore, I created a folder named viewlets in my product root and created a __init__.py and a configure.zcml file. Then i copied content_history.pt, history_view.pt, review_history.pt and content.py from plone/app/layout/viewlets/ (omelette) to the newly created folder.
The configure.zcml contains two view registrations:
<browser:view
for="*"
name="my-contenthistory"
class=".content.ContentHistoryView"
permission="zope2.View"
/>
<browser:page
for="*"
name="my-historyview"
template="history_view.pt"
permission="zope2.View"
/>
Furthermore, I copied the whole WorkflowHistoryViewlet class from content.py to a different class name. TransferHistory in this case. Then I changed mostly the part that corresponds to the workflow state variable, which was not review_state, but transfer_state. I further found that the initial usage of the 2nd workflow creates also a created entry in the history of the 2nd workflow, that I just filtered .
transfer_history = [x for x in transfer_history if x['action'] != None]
The I corrected the view name in history_view.pt to my new view name.
<div tal:replace="structure here/##my-contenthistory">Content History</div>
Finally, I added my class as parent to the ContentHistoryViewlet class in content.py
class ContentHistoryViewlet(WorkflowHistoryViewlet, TransferHistoryViewlet):
index = ViewPageTemplateFile("content_history.pt")
#memoize
def getUserInfo(self, userid):
[...]
def fullHistory(self):
history = self.workflowHistory() + self.revisionHistory() + self.transferHistory()
if len(history) == 0:
return None
history.sort(key=lambda x: x["time"], reverse=True)
return history
and registered the .zcml in the products configure.zcml
<include package=".viewlets" />
Then I modified content_history.pt and also changed the definition of action_id in the upper part of the file.
[...]
action_id python:item['action'] or item.get('review_state', False) or item.get('transfer_state', False);
[...]
After rebooting the monster and a product reinstall, all state changes from both workflows are shown in the my-historyview.

New & renamed workflows with existing content

I have a site with a custom content type Content, which initially had a single workflow attached, content_workflow. There are several thousand existing instances of Content.
I now have a need to add a second workflow to this type, content_beta_workflow. How can I update all existing content to be part of the new workflow?
On a related note: if I want rename the initial workflow to content_alpha_workflow, how can I update all existing content to reflect this change?
If you are simply changing from one workflow to the other, follow these steps:
Go to Site Setup > Types
Select your custom content type from the drop down menu, the page will update to display the current workflow
Select your new workflow from the dropdown, a map will be generated showing each state in the current workflow
For each state, select the state in your new workflow that most closely matches (or is most appropriate)
When you save, all objects of your custom site will be updated to use the new workflow. For each state in the map from the original workflow, existing content in that state will be put into the state you chose in step 4 above. Security settings will be re-indexed and you are done.
As for renaming the old workflow, you can do so in the portal_workflow tool in the ZMI. But only change the human-facing Title of the workflow. Changing the ID may have side effects for the workflow history of your content.
edited
Okay, I see from your comment that you are looking to add a new workflow to a type in addition to the one it already has. Here's a bit of sample code to accomplish that:
my_type = 'Content' # This is your content portal_type name
my_wf = 'content_workflow_beta'
wf_chain = list(wf_tool.getChainForPortalType(my_type))
if my_wf not in wf_chain:
wf_chain.append(my_wf)
wf_tool.setChainForPortalTypes([my_type], wf_chain)
You can add this code in an upgrade step for the package that defines your content type and workflows. Add a call to updateRoleMappings on the workflow tool and you'll be set to use the new workflow through the standard Plone UI in addition to your original workflow.
As you've already found, you can also manually update the workflow history of all objects to rename workflow ID, but that's a pretty invasive step.
As workflow_history is a dict property on each content item, it was a case of adding or updating suitable items as required. First, I copied the GenericSetup for content_workflow to content_alpha_workflow. Next, I created content_beta_workflow and added it to the profile. Then I wrote the following upgrade step:
import logging
from DateTime import DateTime
def modify_content_workflow_history(context, logger=None):
if logger is None: logger = logging.getLogger('my.product')
# import the new workflows
context.portal_setup.runImportStepFromProfile('profile-my.product:default', 'workflow')
# set up some defaults for the new records
_history_defaults = dict(
action = None,
actor = 'admin',
comments = 'automatically created by update v2',
time = DateTime(),
)
_alpha_defaults = dict(review_state = 'alpha_state_1', **_history_defaults)
_beta_defaults = dict(review_state = 'beta_state_1', **_history_defaults)
for parent in context.parents.values():
for content in parent.content.values():
# don't acquire the parent's history
if 'parent_workflow' in content.workflow_history:
content.workflow_history = {}
# copy content_workflow to content_alpha_workflow
if 'content_workflow' in content.workflow_history:
alpha_defaults = context.workflow_history['content_workflow']
del content.workflow_history['content_workflow']
else:
alpha_defaults = (_alpha_defaults,) # must be a tuple
content.workflow_history['ctcc_content_alpha_workflow'] = alpha_defaults
# create the beta workflow with a modified actor
beta_defaults = dict(**_beta_defaults)
beta_defaults['actor'] = u'%suser' % parent.id
content.workflow_history['ctcc_content_beta_workflow'] = (beta_defaults,)
logger.info('Content workflow history updated')