The revision tagging module

robertDouglass's picture

The revision system.

Drupal has a great feature whereby editing a node can create a revision of that node. Whenever create new revisions is checked under Publishing options, editing a node will result in a new revision. Users with the view revisions or administer nodes permissions will see a Revisions tab on node viewing pages. The Revisions tab interface allows you to view the individual revisions, revert to an earlier revision, and delete revisions.

One interesting detail of the system is the behavior of the reverting mechanism. Let's say you have revisions {1,2,3,current}. If you revert to revision #2, a copy of #2 is made and the copy is set as the current revision. Thus, after reverting you'll have {1,2,3,4,current}, where current is a clone of #2, and #4 is the previous current revision.

Another interesting feature of the revision system is the Log field. The Log field always appears empty in the node editing form, and log messages don't appear in the displayed node. The log messages' purpose are for adding meaning to the revision system. You can see the log messages along with the revisions under the Revisions tab.

The goal

The goal of the Revision tagging module is to allow users to tag individual revisions as having particular meaning. These could be milestones in the development of the node, or alternate versions of the same content. One example use case could be the Drupal handbook on drupal.org. Every release of Drupal makes some part of the handbook obsolete. That section then has to be updated to be current for the newest release, but in doing so, the information that is pertinent to older releases gets buried as revisions, not visible to the general public.

One suggested solution to this problem is using taxonomy to "tag" versions of the handbook, DRUPAL-4-6, DRUPAL-4-7, etc. This is problematic, though, because there is no way to know, when looking at the DRUPAL-4-6 version, that there is an alternate DRUPAL-4-7 version of essentially the same content available.

With revision tagging, the various revisions of that content could be tagged DRUPAL-4-6, DRUPAL-4-7, and whenever one is looking at any version of the content, the other available versions will be listed.

The implementation.

I see the following steps as being necessary when creating this module:

  • create a revisiontags.info file. This is required for Drupal 5.0
  • add permissions tag revisions and view tagged revisions
  • add a database table, revision_tags, that has nid (INT), vid (INT), tag (VARCHAR 127) columns. The proper way to do this is in the hook_install function in a revisiontags.install file.
  • create a revisiontags.module file
  • use hook_form_alter to add a Tag this revision field to node edit forms, if the user has administer nodes or tag revisions permissions.
  • implement CRUD functionality to track revision tags using hook_nodeapi and the revision_tags table. Make sure to be familiar with Drupal's database abstraction layer.
  • implement hook_block to display the available tagged revisions to a user viewing a node (assuming administer nodes, view revisions, or view tagged revisions permissions). See the attached proof of concept module which demonstrates this feature (rename it to revisiontags.zip and unzip).
  • use drupal_set_title to override the default revision title and replace it with the normal node title. See function node_revisions() in node.module.
  • determine a way to visually show that one is looking at a revision. This might be to add the tag to the title in the previous step, or display the tag in some other way. It must be clear, though that a revision is being viewed, and which one.

Those are the steps to get the minimum basic version of this module ready. There are lots of other issues that can be addressed if the module is to become really useful (such as editing tagged revisions), but getting to this point will at least be a good start.

AttachmentSize
revisiontags.zip_.txt1.05 KB

Comments

smells complicated

moshe weitzman's picture

so we want to mimic version control systems now? as long as this is in contrib, it makes good sense and could be useful

Take my example

robertDouglass's picture

... the drupal handbooks. Let's say I search for "adding links to nodes", and the top search result is a handbook page describing hook_links for 4.6. That hook has changed subsequently. How do we provide these related but not-quite-duplicate versions of content, yet let people know that they're looking at a version relevant to a "tag" (drupal4.6 in this case)?

Again, the very act of making a contrived example brings up another shortcoming of the proposed module: only one revision would be in the search index.

Anyway, I'm of the opinion that it would be a cool module to build, and that someone will find a use for it, and that building it will be very instructional.

All design suggestions are very welcome.

This is a very cool

Development Seed's picture

This is a very cool implementation w/ the idea for handbooks. MySQL is doing this w/ their docs now - check out the top-left nav-box where they've got the versions listed, on a page like http://dev.mysql.com/doc/refman/4.1/en/grant.html

Show only one (current) revision?

BioALIEN's picture

I like the proposed idea, another interesting but probably never thought about use for something like this is translating the node into different languages. Same concept, and store translations as revisions of a node.

Going back to your earlier point, I think it's appropriate to only show one entry in the search results for a node and not (all) its revisions. This would make the suggestion about translation correct. Alternatively, you can take google's example and display child results per node (like sub pages belonging to the same site in a google search). This way, you can display revisions of a node as child results under the node itself. Of course there would then be an option to limit how many child search results to display per node.


BioALIEN
Buy/Sell/Trade with other webmasters: <a href="http://www.webmastertrader.com" "target="_blank">WebMasterTrader.com

This was definitely a know consequence

robertDouglass's picture

A bit of extra logic to change locale and you'd have a way to do translatable content. BUT I'm trusting Goba and Roberto Gerola to find a good solution to the localization problem: http://drupal.org/project/localizer

documentum

binduwavell's picture

I work with large content management systems based on Documentum for my real job. This version tagging module could bring drupals version management system closer to what more full featured content management systems have in this area. Seems like it would be great to be able to limit version tags for a node type to some taxonomy term so book nodes only get categories under drupal-versions... I'm not sure if attachments (files) are handled as linked full fledged nodes or if they are bolted on to nodes.... In addition to document versions, Documentum allows you to have document renditions. So your primary document is say an MS word doc, but you can have a PDF rendition. A thumbnail rendition, etc. If attachments can be versioned (as full fledged nodes) then this type of tagging system could also support the concept of renditions. Although having the concepts be orthognal does have some advantages.

-- Bindu Wavell

-- Bindu Wavell

Taxonomy?

Jaza's picture

Tagging of revisions is something that can (quite easily) and that IMHO should be added to the core taxonomy system. With the new revisions system, it's quite easy for any module that stores information per-node-instance to instead store that information per-revision-instance. All that needs to be done is for database tables to have a 'vid' column, rather than just a 'nid' column. The 'book' table already has this, and it wouldn't be too hard for the 'term_node' table to follow suite.

Core support for this would be a much cleaner and more integrated approach than contrib module support. I considered adding revision-tagging support to the category module (when I was first developing it), but in the end I decided to remain consistent with the core taxonomy module in this regard. If core taxonomy gets revision support, then revision support will be added to the category module as well.

Good job on this roadmap, Rob - this is a great starting-point for future development.

Jeremy Epstein - GreenAsh

Jeremy Epstein - GreenAsh

robertDouglass's picture

.... and that is, revision tags should be unique. Taxonomy wouldn't let you do this. Moshe is correct that I want to bring in a small element of version control.

As for what other modules do, the module I'm proposing wouldn't interfere in their decision to support or not support the revision system. Any module that extends node in any way should support the revision system, but this support is patchy.

Can you elaborate on what you had in mind when you wrote "With the new revisions system...."? Are you talking about Gerhard's system that made it into 4.7, or is there something new in 5.0 that I've missed out on?

Two kinds of revision?

Scott's picture

One kind is part of the editorial process. For example, I might revise an article to try a new way of presenting the information. In that case, I don't want the original version to be visible to non-editors, but I want to keep the original version in case I decide to revert to it.

The other kind is like your example of the Drupal Handbook revisions for different versions of Drupal, where you do want the older versions to be accessible to non-editors.

If both kinds of revision are part of a single node's revision history, you'd need some way to indicate which revisions are "published" and which are not.

The code

chx's picture

<?php
function revisiontags_menu($may_cache) {
  if (!
$may_cache) {
   
$items[] = array('path' => 'node/'. arg(1) .'/revision_tags', 'title' => t('Revision tags'),
     
'callback' => 'revisiontags_localtask',
     
'access'   => (user_access('administer nodes') || user_access('tag revisions')) && (db_result(db_query('SELECT COUNT(vid) FROM {node_revisions} WHERE nid = %d', arg(1))) > 1),
     
'weight'   => 3,
     
'type'     => MENU_LOCAL_TASK);
  }
  return
$items;
}

function
revisiontags_perm() {
  return array(
'tag revisions', 'view tagged revisions');
}

function
revisiontags_localtask() {
  if (
is_numeric(arg(1)) && arg(2) == 'revision_tags') {
    if (!
user_access('administer nodes') && !user_access('tag revisions')) {
     
drupal_access_denied();
      return;
    }
   
$op = arg(4) ? arg(4) : 'overview';
    switch (
$op) {
      case
'overview':
       
$node = node_load(arg(1));
        return
revisiontags_overview($node);
      case
'view':
        if (
is_numeric(arg(3))) {
         
$node = node_load(arg(1), arg(3));
          if (
$node->nid) {
            if (
node_access('view', $node)) {
             
$revisiontag = _get_revision_tag($node->nid, arg(3));
              if (!empty(
$revisiontag) && !empty($revisiontag->tag)) {
               
drupal_set_title(t('%title (Tag: %tag)', array('%title' => $node->title, '%tag' => $revisiontag->tag)));
              }
              else {
               
drupal_set_title(t('Revision of %title from %date', array('%title' => $node->title, '%date' => format_date($node->revision_timestamp))));
              }
              return
node_show($node, arg(2));
            }
           
drupal_access_denied();
            return;
          }
        }
        break;
      case
'edit':
        if (
is_numeric(arg(3))) {
         
$node = node_load(arg(1));
          if (!empty(
$node->nid)) {
           
$revisiontag = _get_revision_tag($node->nid, arg(3));
           
$revisiontag->current_vid = $node->vid;
            if (!empty(
$revisiontag->tag)) {
             
drupal_set_title(t('%title (Tag: %tag)', array('%title' => $node->title, '%tag' => $revisiontag->tag)));
            }
            else {
             
drupal_set_title(t('Revision of %title from %date', array('%title' => $node->title, '%date' => format_date($node->revision_timestamp))));
            }
            return
drupal_get_form('revisiontags_edit', $revisiontag);
          }
        }
        break;
    }
  }
 
drupal_not_found();
}

function
revisiontags_overview($node) {
 
drupal_set_title(t('Revisions for %title', array('%title' => $node->title)));

 
$header = array(t('Revision'), t('Tag'), t('Public'), array('data' => t('Operations'), 'colspan' => 2));

 
$revisions = node_revision_list($node);
 
$rows = array();
  foreach (
$revisions as $revision) {
   
$row = array();
   
$operations = array();
   
$tag = array('');
   
$public = array('');
   
$revisiontag = _get_revision_tag($node->nid, $revision->vid);
    if (!empty(
$revisiontag)) {
     
$tag = array($revisiontag->tag);
      if (!empty(
$revisiontag->tag) && $revisiontag->public == 1) {
       
$public = array(t('public'));
      }
    }
   
$row[] = t('!date by !username', array('!date' => l(format_date($revision->timestamp, 'small'), "node/$node->nid/revisions/$revision->vid/view"), '!username' => theme('username', $revision)))
             . ((
$revision->log != '') ? '<p class="revision-log">'. filter_xss($revision->log) .'</p>' : '');
   
$operations[] = l(t('edit'), "node/$node->nid/revision_tags/$revision->vid/edit");
   
$rows[] = array_merge($row, $tag, $public, $operations);
  }
 
$output .= theme('table', $header, $rows);

  return
$output;
}

/<
strong>
*
revision tag editing form
**/
function
revisiontags_edit($revisiontag) {
 
$form['tag'] = array(
   
'#type'          => 'textfield',
   
'#title'         => t('Tag of the revision'),
   
'#default_value' => $revisiontag->tag,
  );
 
$form['public'] = array(
   
'#type'          => 'checkbox',
   
'#title'         => t('Is this a tagged revision that is available to the public?'),
   
'#description'   => t('The current revision will always be public'),
   
'#default_value' => $revisiontag->current_vid == arg(3) ? 1 : $revisiontag->public,
  );
  if (
$revisiontag->current_vid == arg(3)) {
   
$form['public']['#disabled'] = TRUE;
  }
 
$form['submit'] = array(
   
'#type'          => 'submit',
   
'#default_value' => t('Save'),
  );
 
$form['#redirect'] = arg(0) .'/'. arg(1) .'/'. arg(2) .'/'. arg(3);
  return
$form;
}

function
revisiontags_edit_submit($form_id, $form) {
 
$node = node_load(arg(1));
  if (
$node->vid == arg(3)) {
   
$form['public'] = 1;
  }
 
$revisiontag = _get_revision_tag(arg(1), arg(3));
  if (empty(
$revisiontag)) {
   
db_query("INSERT INTO {revision_tags} (nid, vid, tag, public) VALUES(%d, %d, '%s', %d)", arg(1), arg(3), $form['tag'], $form['public']);
  }
  else {
   
db_query("UPDATE {revision_tags} SET tag = '%s', public = %d WHERE nid = %d AND vid = %d", $form['tag'], $form['public'], arg(1), arg(3));
  }
}

function
revisiontags_block($op = 'list', $delta = 0, $edit = array()) {
  if (
$op == 'list') {
   
$blocks[0] = array('info' => t('Tagged revisions'));
    return
$blocks;
  }
  else if (
$op == 'view' && (user_access('tag revisions') || user_access('view tagged revisions'))) {
    switch(
$delta) {
      case
0:
       
$block = array('subject' => t('Other versions available'),
         
'content' => theme('revisiontags_tags'));
        break;
    }
    return
$block;
  }
}

/</
strong>
* For
this to work, you'll need to create a node (nid = 1) and several revisions of it!
<strong>/
function theme_revisiontags_tags($nid = NULL) {
  if (is_null($nid)) {
    // try to infer the nid from the path
      if (arg(0) == '
node' && is_numeric(arg(1))) {
      $nid = arg(1);
   }
      else {
       // we didn'
t figure it out, so get out of here
       
return;
      }
  }

 
$rs = db_query("SELECT vid, tag FROM {revision_tags} WHERE nid = %d AND public = 1", $nid);
 
$list = array();
  while(
$row = db_fetch_object($rs)) {
   
$list[] = l($row->tag, "node/$nid/revision_tags/$row->vid/view");
  }
  return (
count($list) < 2) ? '' : theme('item_list', $list);
}

/</
strong>
*
Gives back the tag of a revision
**/
function
_get_revision_tag($nid, $vid) {
 
$result = db_fetch_object(db_query("SELECT tag, public FROM {revision_tags} WHERE nid = %d AND vid = %d", $nid, $vid));
  if (!empty(
$result)) {
   
$result->tag = trim($result->tag);
  }
  return
$result;
}
?>

Smaller bits

chx's picture

name = Revision Tags
description = Tag node revisions and provide an interface for browsing them

<?php
function revisiontags_install() {
       
db_query('CREATE TABLE {revision_tags} (
                nid      INT UNSIGNED NOT NULL,
                vid      INT UNSIGNED NOT NULL,
                tag      VARCHAR(127) NOT NULL,
                public   TINYINT UNSIGNED NOT NULL DEFAULT 0,
                PRIMARY KEY (nid, vid)
                )'
 
);
}

function
revisiontags_uninstall() {
 
db_query('DROP TABLE {revision_tags}');
}
?>

revisiontags module now available

moshe weitzman's picture

robert has gone ahead and published a real revision tags module. lets see how folks use it.

"Users with the view

matthewv789's picture

"Users with the view revisions or administer nodes permissions will see a Revisions tab on node viewing pages."

I think the interface design here could use some improvement. This is a similar problem to the broken block editing interface: since the feature does not work in the admin theme, there's a good chance it may not work at all.

Like block editing, this only works if the front-end theme supports it. If a site uses a stock admin theme, but has a custom front-end theme that (sensibly enough) does not support admin-specific functionality (since we never intend to edit through the front end of the site, which is why we chose to use a different admin theme in the first place), there is no way to view revisions or perform any actions on them.

If the interface has a way to save revisions in the admin theme, it should have a way to view and manipulate revisions through the admin theme.

As it is, I have to consider that Drupal simply doesn't have a usable revisions feature, though it does have a non-functional checkbox marked "Revisions", which I end up removing from the interface of the admin theme to avoid confusion.

Redesign idea posted

colan's picture

I'll admit I don't completely understand the resistance to using Taxonomy. Perhaps this was only relevant before Drupal 6? Please find my proposal over at Redesign module to use Taxonomy & Rules.

Revision tagging module

Group organizers

Group notifications

This group offers an RSS feed. Or subscribe to these personalized, sitewide feeds:

Hot content this week