Using Drupal's Workflow and Revision Moderation Modules Together
Submitted by dave on Tue, 2007-09-04 18:31.
Recently a client requested this relatively simple publishing model:
- Some users are Editors. They may add and edit pages.
- Other users are Publishers. Publishers may review the changes made by editors, and if acceptable, make them visible to the public.
- Editors cannot publish content, and Publishers cannot edit. This important rule means that no single user can (maliciously or accidentally) add erroneous content to the site.
- Published content may be edited (by Editors). The changes are not visible the public until a Publisher approves them.
As an experienced Drupal developer, and I thought this model would be easy to build. I would use Drupal's roles and permissions to enforce the first three rules above, and the Revision Moderation module to enforce the fourth. I'd also use the Workflow module to facilitate communication between Editors and Publishers, and the Actions module to make publishing a one-step operation. Easy, right? If only it were, I wouldn't be writing this now.
Two things proved especially difficult. First, Drupal's roles and persmissions are not flexible enough to allow the first three rules above. And second, modules like Workflow and Actions do not work well with Revision Moderation. It seems it is not enough to be "revision aware"; modules must be "revision moderation aware". I discuss this in more detail, and share my solutions below.
Editor and Publisher Permissions
I created two roles, called "Editor" and "Publisher", with the goal that Editors may create and modify while Publishers can make the changes visible to the public, but not vice versa. Giving editors permission to edit, but not publish pages was easy enough. But the Publisher was difficult.
First, its worth noting that by not giving Publishers edit permission, they would never see the node edit form, and therefore never see the "published" checkbox under "Publishing options". This OK however, because they'd use Workflow and Actions to set that flag. In fact that is the preferred way in my scheme.
But there was an immediate problem with the process. Using my Editor account, I'd create a node. Then, I'd log in as Publisher to review the node and publish it. But Publisher could not even view the node, because it's not published yet! Even though my Publisher could modify the workflow at q=node/NNN/workflow, they could not see the node first at q=node/NNN. And who would approve for public view a page they have not even seen?
My first attempt to fix this was to give Publisher the "administer nodes" permission. This seemed to solve the problem in that Publisher could now view unpublished nodes. But it created other problems, in particular giving the publisher way too much permission. He could now edit all nodes, even edit the "publishing options" flags, and on revision pages he could publish or revert revisions. I wanted to ensure that Publishers always used the Workflow states to control the publishing. This keeps things relatively simple for the Publisher, who is not necessarily a Drupal expert. So I took away the "administer nodes" permission, and again the Publisher could not view the not-yet-published pages.
My next approach was to create my own menu callback. That is, a URL where my publishers can view the unpublished nodes. In fact, I made it show them the very latest revision of any node, whether it is published or not, because viewing the latest revision is also useful when a published node has been editing and revision moderation is enabled. The result is that by visiting ?q=node/NNN/latest_revision, the Publisher can review changes to a node. And once there, they also see the Workflow tab at ?q=node/NNN/workflow. The Workflow tab allows them to publish the latest changes if they pass the review. Here's some code showing how this is done, from my custom module.
<?php
define('CUSTOM_WORKFLOW_PERM_EDIT', 'edit custom content');
define('CUSTOM_WORKFLOW_PERM_ADMIN', 'administer custom content');
define('CUSTOM_WORKFLOW_PERM_PUBLISH', 'publish custom content');
define('CUSTOM_WORKFLOW_STATE_DRAFT', 2);
function custom_workflow_perm() {
return array(CUSTOM_WORKFLOW_PERM_EDIT, CUSTOM_WORKFLOW_PERM_ADMIN, CUSTOM_WORKFLOW_PERM_PUBLISH);
}
function custom_workflow_menu($may_cache) {
$items = array();
if (!$may_cache) {
if (arg(0) == 'node' && is_numeric(arg(1))) {
$node = node_load(arg(1));
if ($node->nid) {
$access = user_access(CUSTOM_WORKFLOW_PERM_EDIT) || user_access(CUSTOM_WORKFLOW_PERM_PUBLISH) || user_access(CUSTOM_WORKFLOW_PERM_ADMIN);
$items[] = array('path' => 'node/'. arg(1) . '/latest_revision',
'title' => t('Latest Revision'),
'type' => MENU_LOCAL_TASK,
'access' => $access,
'callback' => 'custom_workflow_latest_revision_page',
'callback arguments' => array(arg(1)),
);
}
}
}
return $items;
}
function custom_workflow_latest_revision_page($nid) {
// Which is the latest revision?
$vid = db_result(db_query("SELECT MAX(vid) FROM node_revisions WHERE nid=%d",
$nid));
$node = node_load($nid, $vid);
drupal_set_title(check_plain($node->title));
return node_show($node, NULL);
}
?>OK, to quickly review: now Publishers (and Editors too) can view the latest changes to a node at ?q=node/NNN/latest_revision. These users can also visit the Workflow tab, where Publishers (but not Editors) can move the node to the "published" state. And when that happens an Action sets the published flag (more on that later). So far so good, but still there is a problem. There are no links to ?q=node/NNN/latest revision. How would a Publisher or Editor ever get there?
In my case, I already planned to use Views to show Publishers all content in the "ready to be published" Workflow state. So a natural place to add a link to the latest revision was in that view.
Readers familiar with Views know that there a number of Views Fields that provide links to a node. For example, "Node: Title", "Node: View Link", "Node: Edit Link", etc. I needed to create one more of these. Since there is clearly a need for a variety of type of links, I created a generic one, in which the Field's option field specifies exactly where the link leads. Here's the code:
<?php
function custom_workflow_views_tables() {
$tables['custom_node'] =
array('name' => 'node',
'provider' => 'internal',
'fields' =>
array('custom_node_link' =>
array('name' => t('Custom: link built from node'),
'handler' => 'custom_workflow_handler_field_custom_link',
'query_handler' => 'custom_workflow_handler_query_custom_link',
'sortable' => TRUE,
'sort_handler' => 'custom_workflow_handler_sort_custom_link',
'option' => 'string',
'notafield' => TRUE,
'help' => t('Creates a link using attributes of the node.'),
),
),
);
return $tables;
}
function custom_workflow_handler_field_custom_link($info, $field, $value, $data) {
if (preg_match('/\[([^|]*)\|([^\]]*)\]/', $field['options'], $matches)) {
$params = array('!nid' => $data->nid,
'!title' => $data->custom_link_title,
);
$title = t($matches[2], $params);
$url = t($matches[1], $params);
return l($title, $url);
}
else {
return 'ereg failed.';
}
}
function custom_workflow_handler_query_custom_link($data, $info, &$query) {
$query->add_field('title', 'node', 'custom_link_title');
}
function custom_workflow_handler_sort_custom_link($data, $info) {
// sort by title, even if our custom link display something else.
return 'custom_link_title';
}
?>By including this Field in a Table View, and giving it the option "[node/!nid/latest_revision|!title latest revision]", the View now includes a link to the node's latest revision.
At this point I've solved the first of my problems. Editors can modify content, while Publishers can review content and use Workflow to publish changes. But Publishers themselves cannot edit the content. But when Revision Moderation is turned on, there are more problems.
Revision Moderation vs. Workflow and Actions
The Revision Moderation module is wonderful. It allows changes to be made to a published node. The changes are stored in a new revision which only privileged users can see. Regular users continue to see the old, previously published revision until a privileged user makes the updated revision current.
The Workflow and Actions modules are also incredibly useful. Workflow associates a state with a node. For example, I use states like "needs changes", "ready for publish" and "published", so that Editors and Publishers know which nodes they need to act on. When Workflow states change, Actions may be performed. For example sending an email when a page is "ready for publish", or setting the published flag when the state becomes "published".
The bad news is that Revision Moderation is relatively new and uses a little known, rarely used feature of Drupal's revisions. All modules that make changes to nodes stand a good chance of breaking the Revision Moderation features, and this includes Workflow and Actions.
For an example, see this issue I recently submitted: http://drupal.org/node/171210. When a module calls node_save(), the node is not prepared in exactly the way it would be from the node edit form, and there can be unintended consequences. Usually, the module has called node_load() before node_save(), and loaded the current revision of a node, not the latest revision of the node. When Revision Moderation is enabled, these two are not the same. And then when the node is saved, mistakes are made, like changing the order of revisions and reverting changes that had been made to later revisions. That's why, without the patch, Revision Moderation and Workflow cannot be used together.
Even with the patch, you have to avoid other erroneous calls to node_save(). In particular, just about every node Action calls this. In my custom workflow, I included the "Publish node" action. Until I realized it too would not work properly. Instead, I built a custom action to do just what I wanted. But I have to be extra careful now that some other administrator does not come along and add Actions to the Workflow. Here's the custom action I use to publish the latest revision of a node. It does not create a new revision, it merely updated which revision is current and ensures the published flag is on.
<?php
/**
* Implementation of a Drupal action. Makes the latest revision of a
* node the active one. And ensures the node is published.
*
*/
function action_custom_workflow_publish_latest($op, $edit = array(), &$node) {
switch ($op) {
case 'metadata':
return array(
'description' => t('Publish the latest revision of a node.'),
'type' => t('Custom Workflow'),
'batchable' => FALSE,
'configurable' => FALSE,
);
break;
case 'do':
// Determine latest revision.
$latest_vid = db_result(db_query("SELECT MAX(vid) FROM {node_revisions} nr WHERE nr.nid=%d GROUP BY nr.nid", $node->nid));
$message = t("Publishing revision %vid of !link",
array('%vid' => $latest_vid,
'!link' => l($node->title, "node/$node->nid")));
drupal_set_message($message);
watchdog('custom_workflow', $message);
db_query("UPDATE {node} SET vid = %d, status = 1 WHERE nid = %d",
$latest_vid, $node->nid);
break;
}
}
?>Summary
In building this custom workflow I ran into a lot of problems. And spent a lot of time figuring out what went wrong and how I might solve the problem. I'm sharing information here so that others might spend less time, and might use the same solutions I did.
I also hope that future versions of Drupal are better behaved. There are plans now to integrate Actions into Drupal core. This is great news, and I hope it leads to a higher level of support for Actions along with Revision Moderation.
Ideally, the code snippets I share above would be patches submitted to Drupal.org. But some are so dependent on my custom permission scheme, that I couldn't figure out the best way to make them useful on any site. I encourage other module developers to improve on this and submit patches to the contributed modules.
Menu rebuild
Submitted by webrascal@drupal.org on Sun, 2008-02-10 23:53.
Hello again, and thanks once more for sharing this information.
In the short term I'm using a modified approach to what you've outlined above to cover the workflow we'll be using on our latest project and I'm impressed how you've covered it and integrated it so well. You should be proud.
Unfortunately I've run into a glitch involving the menus and pathauto. I'm using the Pathauto module to generate consistent URL's for all the pages within the site and they're done automatically and without the editors or publishers input. For some reason changing the state of the page to published using the workflows tab changes this field in an unexpected way. To fix this I've dumbed down the URL's a bit to match but it's something I'll look into more.
I'm also using the handy module remove_nonviewable_menu_items to keep unpublished pages off the menu and this is causing further problems. It works quite admirably but again publishing the page through the workflow tab causes more problems. For some reason some users as well as the anonymous kind the menu item simply won't show. That is, until the publisher (who has editing rights in my solution) submits the page from the edit screen. It's a tricky little problem that is quite tricky to explain exactly how it works.
Anyway, I think I've solved that problem by including a menu_rebuild() call in the workflow module, in the function workflow_tab_form_submit($form_id, $form_values), just before the function returns. This appears to keep the menu up-to-date which I'm assuming happens in some way via the edit screen that the workflow tab leaves off.
Thanks again!
Here's a thread that may
Submitted by dave on Tue, 2008-02-12 23:49.
Here's a thread that may interest you, regarding unwanted side effects of the workflow tab:
http://drupal.org/node/171210. Give my patch a +1 if it helps you.
Regarding my original post here, I ended up not using Revision Moderation at all. Instead I took the most important line of code (which re-writes the title and vid in the node table after a node is submitted) and included it in a custom module. I did this because I had to use a customized permission scheme. I needed users to edit unpublished content without giving them administer nodes permission, or something to that effect. It's all so complicated that its hard to remember the exact reasons for every change. The final result works well for the client. I've been wanting to write it up in more detail but haven't found time.
Thanks for your comments.
Excellent Write-Up
Submitted by light-blue on Tue, 2008-03-25 21:18.
Thanks for the Excellent Write-Up!
Given your scenario, and especially the issues with core revisions (http://drupal.org/node/105011), I was wondering if it might have been faster to write a custom module to handle a custom node and form(s), with permission enforcement with hook_access() / hook_perm(), and proper revisions. Then again, maybe not, given the workflow/action requirement(s), though the latter modules caused so much trouble. Regardless, awesome, awesome writeup! Thanks so much!


I'm still surprised...
Submitted by webrascal@drupal.org on Thu, 2008-02-07 04:02.
... that this hasn't been seriously tackled before now. I've used drupal for other clients before and a content morderation and workflow is always something I've said is possible to my clients but now I'm tackling it I'm finding how surprisingly unimpressive it handles it.
Thanks for sharing your information in what you've found out for this. I think I'll make it a goal to get a project together that pulls these concerns together and solves them, if only I can find the time! :)
I'm currently having troubles backing out of my investigations into Workflows, Revision Moderation, and Non-viewable Menu Items... for some reason any new content made with these modules disabled, whether they're published or not are not viewable by the public or in fact anyone but the administrator!
I'll keep you posted with my progress if any on tackling the content-editing-workflow problem!
login or register to post comments