Review Signoff Tutorial

From KnowledgeTree Community

Jump to: navigation, search

Building plugins for KnowledgeTree can seem to be rather daunting initially. In reality, they're extremely simple to create, deploy and maintain. This tutorial will take you through the entire process and leave you with a working plugin that you can extend and customise. Certain functionality is intentionally left out to provide you with a clear set of guidelines on how to improve the code, and places where complete solutions to various problems aren't included are clearly indicated.

Contents

Objectives

The plugin we're going to develop will:

  1. provide a workflow trigger, complete with installation and customisation, that requires a certain number of users with a given role to "sign off" on a document
  2. provide the "document action" which performs those signoffs
  3. include clear explanations of the steps required to turn these into a plugin
  4. demonstrate how to add new tables to KnowledgeTree safely and how to make it easy to access those tables from PHP.


Step 1: The core plugin

KnowledgeTree uses a registration approach to plugins: all you have to do is have the plugin directory inside KT's /plugins/ directory, and it will take care of finding it and allowing the user to select it from the Plugin Management page. For this to work, you need to create a file called something like:

MyNewPlugin.php 

What's important is that it ends with ...Plugin.php - that's what's required to cause it to be picked up. The next step (obviously) is to include the basic code that gets called by KT:

<?php

/*
* Copyright available from the forge version, omitted for brevity.
*/

require_once(KT_LIB_DIR . '/plugins/pluginregistry.inc.php');
require_once(KT_LIB_DIR . '/plugins/plugin.inc.php');

This gives us the basic calls we need. KT_LIB_DIR is a constant variable which always points to the right place to find the library files, e.g. /lib/ inside the KT dir.

class ReviewSignoffsPlugin extends KTPlugin {
 var $sNamespace = "brad.reviewsignoffs.plugin";

Almost all components in KnowledgeTree use a namespace to indicate what they are and where they come from. By convention, the plugin name is something like:

AUTHOR_OR_MAJOR_COMPONENT.PACKAGENAME.SUB.COM.PON.ENT

We'll see more examples of this later. What's really important is that this is unique across all different plugins out there - there are no other "brad.reviewsignoffs.plugin" plugins.

    function ReviewSignoffsPlugin($filename = null) {
    $res = parent::KTPlugin($filename);
    $this->sFriendlyName = _kt('Review Signoffs Workflow Trigger');        
    return($res);
}

A fairly standard constructor. All your basic plugins will have a constructor like this:

  1. _kt() is the translation function. It tries to find the text inside it in the translation DB. You don't need to worry too much about that here, but for more details you can read about internationalisation (or i18n).
   function setup() {
        //templates
	$oTemplating =& KTTemplating::getSingleton();
	$oTemplating->addLocation('ReviewSignoffPlugin', '/plugins/reviewsignoffplugin/templates/');
    
       $this->registerWorkflowTrigger('brad.reviewsignoffs.guardtrigger', 'SignOffGuardTrigger', 'reviewSignoff.php'); 
    $this->registerAction('documentaction', 'ReviewSignoffsAction', 'brad.reviewsignoffs.docaction', 'signoffaction.inc.php');
}
}

This function is empty at the moment, but we'll be adding to it as we go along. It does all the "registration" of components (actions, triggers, templating, etc.) that are needed. Also notice that it is the last function in the list, so we close out the class.

    $oRegistry =& KTPluginRegistry::getSingleton();
 $oRegistry->registerPlugin('ReviewSignoffsPlugin', "brad.reviewsignoffs.plugin", __FILE__);

?>

This we use to actually tell KnowledgeTree that the plugin is in place, and where to get it. A line you'll see a lot of (or rather, you'll see lots of similar lines) is:

$oRegistry =& KTPluginRegistry::getSingleton();

This grabs a Singleton - a class of which there's only ever one. These are used everywhere in KnowledgeTree where you need a single, non-database backed list of items across the entire system: actions, components, triggers, templates all use Singletons. For more information about the general design pattern, see Wikipedia's entry on the Singleton pattern. Very important: always use reference assignment ( =& ), not normal assignment ( = ), or you'll create a copy of the singleton which will cause bugs you don't expect.

The last function has a signature of:

$oRegistry->registerPlugin(classname,namespace,file_required_to_use_this);

Easy enough? The next thing to do is to put the "stub" of a workflow trigger together. This will introduce us to a bit more registration code, as well as the basics of templating.

Step 2: Trigger me

You're going to want to read the Workflow Triggers page before going through the code below: this is a concrete example of the ideas presented there. This is all going to need to be done in stages, since to require signoffs we need to be able to have signoffs stored in the database! For now, we'll concentrate on being able to store the configuration for the signoff-trigger, since the actual "is this authorised" part is very quick to do.


<?php

/*
* Copyright omitted for brevity.
*/

require_once(KT_LIB_DIR . '/workflow/workflowtrigger.inc.php');
require_once(KT_LIB_DIR . '/widgets/fieldWidgets.php');


class SignoffGuardTrigger extends KTWorkflowTrigger {
var $sNamespace = 'brad.reviewsignoffs.guardtrigger';

// generic requirements - both can be true
var $bIsGuard = true;
var $bIsAction = false;

function SignoffGuardTrigger() {
    $this->sFriendlyName = _kt("Signoff Guard");
    $this->sDescription = _kt("Require users with a particular role to signoff before the document can follow the transition.");
}

Absolute boilerplate so far: we require a few libraries that we'll need, and set a few items on the plugin. $bIsGuard and $bIsAction are predominantly only used for UI displays - all triggers are called during both the check and action phases of a transition.

The next thing we need is a page which allows the user to edit the configuration for this trigger. This is done using the "displayConfiguration" method. This acts similarly to "do_main" on a Dispatchers, in that it returns a string (typically the output of a template) which is displayed to the user.

It always takes a $args parameter - an array of key => value pairs which should be added to the "submit" form as hidden inputs. These take care of ensuring that all the right "magic" variables are passed in - the trigger instance id, the workflow id, the action, etc.

   function displayConfiguration($args) {
    // The configuration requires 2 things:
    //
    //  1. a "role" which must perform the signoff
    //  2. a number of signoffs required.


    // get all the info needed to build the form
    $aRoles = Role::getList();

    // these items come from the config array assigned to this trigger
    // this is stored on the trigger instance.        
    $current_perms = array();
    $role_id = KTUtil::arrayGet($this->aConfig, 'role_id');
    $sign_offs_required = KTUtil::arrayGet($this->aConfig, 'signoffs_required', 2);
    $review_name = KTUtil::arrayGet($this->aConfig, 'review_name', _kt('Signoff'));

    // by RNP - improvement - automatic trasition: says if the workflow will take the first trasition automatically when all signoffs are done!
     $auto_transition = KTUtil::arrayGet($this->aConfig, 'auto_transition', 0);

Key to the way workflow triggers work is the $this->aConfig array. This is set on the workflow trigger when its created by the workflow machinery, and is derived from the $oTriggerInstance variable's configuration array. More about setting properties later, for now, lets just follow the display.

       // we use KT's simple form machinery to make this a little less
    // hassle to code.  KT 3.2 should have a more detailed form and UI
    // management layer.
    
    $config_form = array();
    
    // a simple string field for the name
    $config_form[] = new KTStringWidget(_kt('Review Name'), _kt('A simple name for the review process.'), 'fName', $review_name, $this->oPage, true);
    
    // a simple string field for the number of items required
    $config_form[] = new KTStringWidget(_kt('Sign-offs required'), _kt('How many signoffs are required.'), 'fSignOffsRequired', $sign_offs_required, $this->oPage, true);
    
    // a more complex lookup field for the role associated
    $aOptions = array();
    $vocab = array();
    foreach($aRoles as $oRole) {
        $vocab[$oRole->getId()] = $oRole->getName();
    } 
    $aOptions['vocab'] = $vocab;        
    $config_form[] = new KTLookupWidget(_kt('Role Required'), _kt('Which role must do the signoff?'), 'fRoleId', $role_id, $this->oPage, true, null, null, $aOptions);
    
    // by RNP - improvement - automatic trasition: says if the workflow will take the first trasition automatically when all signoffs are done!
     $config_form[] = new KTCheckboxWidget(_kt('Automatic Transiction'), _kt('The workflow will take the next step automatically.'), 'fAutoTransiction', $auto_transition, $this->oPage, true);

A full introduction to how KnowledgeTree's forms work is out of place here, especially since it'll be clearer and easier in 3.2. Nonetheless, this essentially creates a set of "widget" objects which can be rendered by the template.

       $oTemplating =& KTTemplating::getSingleton();
    $oTemplate = $oTemplating->loadTemplate("reviewsignoffs/triggerconfig");
    $aTemplateData = array(
          "context" => $this,
          'config_form' => $config_form,
          'args' => $args,
    );
    return $oTemplate->render($aTemplateData);    
}

"context" is the convention for passing the actual item calling the template to the template. It allows the template to use utility methods on that context object if there's more complex analysis or rendering to be done than makes sense in a template.

Now we'll deal with storing the configuration. This is relatively simple: for each of the items, grab it from the request ($_REQUEST) and check that its valid. If its invalid, raise an error. If its valid, store it in the configuration array. Finally, update the trigger instance with a new array, and then return whether that was successful.

   function saveConfiguration() {        
    $config = array();
    
    $config['role_id'] = KTUtil::arrayGet($_REQUEST, 'fRoleId');
    if (empty($config['role_id'])) {
        return PEAR::raiseError(_kt('You must select a role.'));
    }        
    
    $config['sign_offs_required'] = (int) KTUtil::arrayGet($_REQUEST, 'fSignOffsRequired');        
    $config['review_name'] = KTUtil::arrayGet($_REQUEST, 'fName');        
    if (empty($config['review_name'])) {
        return PEAR::raiseError(_kt('You must give a name for the review.'));
    }                
    
    // by RNP - improvement - automatic trasition: says if the workflow will take the first trasition automatically when all signoffs are done!
     $config['auto_transition'] = KTUtil::arrayGet($_REQUEST, 'fAutoTransiction');        

    $this->oTriggerInstance->setConfig($config);
    $res = $this->oTriggerInstance->update();
    
    return $res;
}   

Its useful to allow users to get an overview of what a particular trigger's configuration is. This method allows the trigger to return an (html formatted) string explaining what the configuration of the trigger is. Essentially it should make it easy to see the conditions under which a transition will "work".

   function getConfigDescription() {
    $oRole = Role::get($this->aConfig['role_id']);
    if (PEAR::isError($oRole)) {
        return _kt("The role required for this trigger no longer exists:  the transition cannot be performed.");
    }    
    $sign_offs_required = KTUtil::arrayGet($this->aConfig, 'sign_offs_required');
    return sprintf(
        _kt("This transition requires that %d users with the role \"%s\" have signed off on the document."),
        $sign_offs_required,
        $oRole->getName()
    );
}
}


?>

And that's it for the basic trigger. You will notice one major ommission: there is no allowTranstion function: we can't do that until we have a way to store the signoffs that have occurred.

Step 3: SQL as little as possible

So, the next thing we need to do is be able to store the data for signoffs. Essentially this is 3 things:

  1. the user who signed off on the document
  2. the metadata version against which the signoff occurred (if the document or metadata changes, then the old "signoffs" must be rechecked)
  3. the trigger for which we're doing the signoff.

This could naturally be extended with comments, timestamps and a number of other useful things - these are left as an exercise for the reader. The SQL to do this goes in sql/upgradeto1.sql - why that particular name is important will become clear in a second.

sql/upgradeto1.sql

CREATE TABLE `plugin_review_signoffs` (
  `id`         int(11) not null default "0",
   PRIMARY KEY (id),
  `user_id`    int(11) not null default "0",
  `metadata_version_id` int(11) not null default "0",
  `trigger_id`    int(10) unsigned not null default "0",
  INDEX `idx_metadata_version_id` (`metadata_version_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;  

Pretty standard stuff so far. This creates a simple table to store the data, as we would if we were creating the table in any other SQL environment. There is more, however:

ALTER TABLE `plugin_review_signoffs`
ADD CONSTRAINT `review_signoffs_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE;
ALTER TABLE `plugin_review_signoffs`
ADD CONSTRAINT `review_signoffs_ibfk_2` FOREIGN KEY (`metadata_version_id`) REFERENCES `document_metadata_version` (`id`) ON DELETE CASCADE;
ALTER TABLE `plugin_review_signoffs`
ADD CONSTRAINT `review_signoffs_ibfk_3` FOREIGN KEY (`trigger_id`) REFERENCES `workflow_trigger_instances` (`id`) ON DELETE CASCADE; 

These ensure that whatever user_id, metadata_version_id and trigger_id are, they are valid answers. if the item they refer to is deleted, this row will also be deleted. This way we're sure that our database is always correct, and doesn't have broken data. Finally, however, we need a way to manage the "id" entries for each row. KTEntity handles this using so called "zseq" tables - a table called "zseq_my_table_name" which contains only an "id" int. In MySQL, this is an auto_increment column, but in other databases it would have different semantics.

CREATE TABLE `zseq_plugin_review_signoffs` (
`id` int(10) unsigned NOT NULL auto_increment,
PRIMARY KEY  (`id`)
) ENGINE=MyISAM;

And that is that for the SQL. Now, how does that get used by KnowledgeTree? Well, to do that you need to tell KT about the objects which use this table. For more detailed info on KTEntity and related classes, see the appropriate Wiki pages.

signoff.inc.php

<?php
/*
*  Copyright omitted for brevity
*/ 

require_once(KT_LIB_DIR . "/ktentity.inc");

class ReviewSignoff extends KTEntity {
  var $iUserId;
  var $iMetadataVersionId;
  var $iTriggerInstanceId;
  
  var $_aFieldToSelect = array(
     "iId" => "id",
     "iUserId" => "user_id",
     "iMetadataVersionId" => "metadata_version_id",
     "iTriggerInstanceId" => "trigger_id",
 );

 var $_bUsePearError = true;

Very straight forward boilerplate: we indicate what attributes this object has, and which row entries they relate to.

   function getTriggerInstanceId() { return $this->iTriggerInstanceId; }
 function setTriggerInstanceId($iNewValue) { $this->iTriggerInstanceId = $iNewValue; }
 function getUserId() { return $this->iUserId; }
 function setUserId($iNewValue) { $this->iUserId = $iNewValue; }    
 function getMetadataVersionId() { return $this->iMetadataVersionId; }
 function setMetadataVersionId($iNewValue) { $this->iMetadataVersionId = $iNewValue; }

 function _table () {
     return KTUtil::getTableName('plugin_review_signoffs');
 }

More boilerplate code - this tells the system:

  • which table to use
  • how to get and set properties (direct attribute access from outside the entity will not work).
   // STATIC
 function &get($iId) { return KTEntityUtil::get('ReviewSignoff', $iId); }
 function &createFromArray($aOptions) { 
     return KTEntityUtil::createFromArray('ReviewSignoff', $aOptions); 
 }
 function &getList($sWhereClause = null) { return KTEntityUtil::getList2('ReviewSignoff', $sWhereClause); }

The last of the boilerplate: these methods allow us to create, list and get a particular entry from the database (by ID). What comes next are two much more useful functions: getByDocumentAndTrigger and getByDocumentAndTriggerAndUser. Getting a particular row or set of rows depending on the data sent in is a common problem, and one that's handled simply by KnowledgeTree.

   function &getByDocumentAndTrigger($oDocument, $oTriggerInstance) {       
     // using KTUtil::getId() means we can either pass an object or an id in.        
     $iTriggerInstanceId = KTUtil::getId($oTriggerInstance);
     $iDocumentMetadataVersionId = $oDocument->getMetadataVersionId();
     
     $aOptions = KTUtil::meldOptions($aOptions, array(
         'multi' => true,
     ));
     

We indicate that having multiple results is OK - we are not expecting a single answer to the query.

       // here we use KTEntityUtil::getByDict        
     return KTEntityUtil::getByDict('ReviewSignoff', array(
         'trigger_id' => $iTriggerInstanceId,
         'metadata_version_id' => $iDocumentMetadataVersionId,            
     ), $aOptions);        
 }

This indicates that we are "getting by a dictionary" - a set of key value pairs from the database. KT will take care of ensuring that the values are correctly escaped, etc.

    function &getByDocumentAndTriggerAndUser($oDocument, $oTriggerInstance, $oUser) {       
     $iTriggerInstanceId = $oTriggerInstance->getId();
     $iDocumentMetadataVersionId = $oDocument->getMetadataVersionId();
     $iUserId = $oUser->getId();        
     
     $aOptions = KTUtil::meldOptions($aOptions, array(
         'multi' => true,
     ));
     
     // here we use KTEntityUtil::getByDict        
     return KTEntityUtil::getByDict('ReviewSignoff', array(
         'trigger_id' => $iTriggerInstanceId,
         'metadata_version_id' => $iDocumentMetadataVersionId,            
         'user_id' => $iUserId
     ), $aOptions);        
 }    
 
}

?>

Conceptually, the second method here is the same - it just requires an even more rigorous set of requirements.

templates/reviewsignoffs/triggerconfig.smarty

<h2>{i18n}Configuration Review{/i18n}</h2>

<p class="descriptiveText">{i18n}This allows you to configure this specific trigger.{/i18n}</p>

<form action="{$smarty.server.PHP_SELF}" method="POST">
<fieldset>
 <legend>{i18n}Configuration{/i18n}</legend>
 
 {foreach from=$args key=k item=v}
     <input type="hidden" value="{$v}" name="{$k}" />
 {/foreach}    
 
 {foreach from=$config_form item=oWidget}
     {$oWidget->render()}
 {/foreach}    
 
 <div class="form_actions">
     <input type="submit" value="{i18n}Save Configuration{/i18n}" />
 </div>    
 
</fieldset>
</form>

Step 4: The sign-off action - I wanted a block of flats

fixme TODO add in the actual explanations of these files

signoffaction.inc.php

<?php

/*
* Copyright omitted for brevity
*/

require_once(KT_LIB_DIR . '/actions/documentaction.inc.php');
require_once(KT_LIB_DIR . '/documentmanagement/Document.inc');

require_once(dirname(__FILE__) . '/signoff.inc.php');

 // {{{ KTDocumentViewAction
 class ReviewSignoffsAction extends KTDocumentAction {
    var $sName = 'brad.reviewsignoffs.docaction';
    var $sPermission = 'ktcore.permissions.read';
 
    //by RNP - Automatic transition
    function doTransition($oDocument) {
        // we want to get the signoffs for this document metadata version and trigger.
        $aList = ReviewSignoff::getByDocumentAndTrigger($oDocument, $this->_oTriggerInstance);

        if (PEAR::isError($aList)) {
            return $aList;
        }
	  
        //check if the trigger config tell us to go ahead
        $autoTransition = KTUtil::arrayGet($this->_oTriggerInstance->getConfig(), 'auto_transition');

        if($autoTransition != "on"){ return false; }
        
        // if we have enough signoffs, off we go.
        $signoffs = KTUtil::arrayGet($this->_oTriggerInstance->getConfig(), 'sign_offs_required');

        if (count($aList) >= $signoffs) {

                //if everything is ok, we do the default transition!
                $workflowTransitions = KTWorkflowUtil::getTransitionsFrom(KTWorkflowUtil::getWorkflowStateForDocument($this->oDocument));

                KTWorkflowUtil::performTransitionOnDocument($workflowTransitions[0], $this->oDocument, User::get($_SESSION['userID']), "Document received all signoffs required.");

                return true;
        }
    }


    function _show() {
        // first check if something odd has happened, like a missing
        // permission or document.
        $res =  parent::_show();        
        if ($res === false) {
            return $res;
        }
        
        // we need to check a number of things before we can start.
        // firstly, we have to know if *this* document is being
        // "blocked" on a transition by the signoff.
        //
        // to do this, we get the workflow, and the triggers that *could*
        // be next
        $iWorkflowId = $this->oDocument->getWorkflowId();
        if (is_null($iWorkflowId)) { 
            // no workflow means no transitions being blocked.
            return false; 
        }
        
        $iStateId = $this->oDocument->getWorkflowStateId();
        if (is_null($iWorkflowId)) { 
            // no workflow id shouldn't happen.  If it does, then the next
            // steps will fail.
            return false; 
        }        
        $oState = KTWorkflowState::get($iStateId);
        if (PEAR::isError($oState)) {
            // again, we want to be sure.
            return false; 
        }
        
        // we now need a list of possible transitions.  In this case,
        // we don't use "getTransitionsForDocumentAndUser" since that
        // checks the triggers we wan't to check!
                
        $aTransitions = KTWorkflowTransition::getBySourceState($oState);              
                
        foreach ($aTransitions as $oTransition) {
            // now we try to find trigger *instances* related to the actual 
            // transition, with the right namespace.
            $aTriggerInstances =  KTWorkflowTriggerInstance::getByTransition($oTransition);
            foreach ($aTriggerInstances as $oInstance) {
                //by RNP - Want to find only the correct trigger! All others will be ignored...(bug fixed)
                if ($oInstance->getNamespace() != 'brad.reviewsignoffs.guardtrigger') {
                    continue; // we only want items with the right namespace
                }
                // check if this user has the specified role.
                $role_id = KTUtil::arrayGet($oInstance->getConfig(), 'role_id');
                $oRole = Role::get($role_id);
                
                // we need to check both the document and, failing that, the role
                // to see if this user has the role in question.
                $oAllocation = DocumentRoleAllocation::getAllocationsForDocumentAndRole($this->oDocument->getId(), $oRole->getId());
                if (PEAR::isError($oAllocation) || is_null($oAllocation)) {
                    $oAllocation = RoleAllocation::getAllocationsForFolderAndRole($this->oDocument->getFolderID(), $oRole->getId());
                    if (PEAR::isError($oAllocation) || is_null($oAllocation)) {
                        continue; // had a problem, skip.
                    }
                }
                if (!$oAllocation->hasMember($this->oUser)) {
                    continue;
                }
 
                // next we need to check that this user has not already signed off on the document.
                $aSignoffs = ReviewSignoff::getByDocumentAndTriggerAndUser($this->oDocument, $oInstance, $this->oUser);
                if (PEAR::isError($aSignoffs)) {
                    continue; // something is broken - skip on out.
                }                
                if (!empty($aSignoffs)) {
                    // clearly there's already a signoff for this user.
                    continue; 
                }
                
                // since we've found a review trigger available to this user
                // we want to hold onto it (we'll need it in the actual code below)
                // and then say "that's done".
                //
                // as a later extension, i recommend that you extend this
                // to handle the case where there are multiple reviews available
                // for a given state and user.
                //
                // it may also be worthwhile extending this to look at *all*
                // triggers for the transition, and only check the review triggers if
                // all other triggers pass.  These extensions are left as an
                // exercise for the reader.
                
                $this->_oTransition = $oTransition;
                $this->_oTriggerInstance = $oInstance;                
                
                return true;       
            }        
        }
        
        // if we've gotten this far, then there are clearly no matching transitions.
        return false;
    }
 
    function getDisplayName() {
        return _kt('Review Signoffs');
    }

    function do_main() {
        $oTemplate =& $this->oValidator->validateTemplate('reviewsignoffs/docaction');      
 
        // set the breadcrumb information.  The title will also use this.
        // alternatively, you could set the title with "setTitle", but this 
        // is usually sufficient.
        $this->oPage->setBreadcrumbDetails(_kt('Document Review'));
 
        // _show() found and stored the trigger instance we are actively using.
        $config = $this->_oTriggerInstance->getConfig();

        $aSignoffs = ReviewSignoff::getByDocumentAndTrigger($this->oDocument, $this->_oTriggerInstance);
        $present_signoffs = count($aSignoffs);

        $oTemplate->setData(array(
            'context' => $this,
            'document_id' => $this->oDocument->getId(),
            'review_name' => KTUtil::arrayGet($config, 'review_name'),
            'transition_name' => $this->_oTransition->getName(),
            'required_signoffs' => KTUtil::arrayGet($config, 'sign_offs_required'),
            'present_signoffs' => $present_signoffs,
            'signoffs' => $aSignoffs,
        ));
        return $oTemplate->render();
    }
    
    function do_signoff() {
        // here we simply grab the user and add the signoff.
        // after that, we want to redirect the user to the *document details* view.
        $oSignoff = ReviewSignoff::createFromArray(array(
            'userid' => $this->oUser->getId(),
            'metadataversionid' => $this->oDocument->getMetadataVersionId(),
            'triggerinstanceid' => $this->_oTriggerInstance->getId(),
        ));
        if (PEAR::isError($oSignoff)) {
            $this->errorRedirectToMain(sprintf(_kt('Failed to sign off on document: %s'), $oSignoff->getMessage()), sprintf('fDocumentId=%d', $this->oDocument->getId()));
        }
        
        //by RNP - Log the users who signed off. It's a cool feature for the users!
        $oDocumentTransaction = new DocumentTransaction($this->oDocument, "User signed off.", 'ktcore.transactions.collaboration_step_approve');
        $oDocumentTransaction->create();

        //RNP - do the auto_transaction logic!
        $this->doTransition($this->oDocument);

        // now send them to the document view
        redirect(generateControllerLink('viewDocument',sprintf(_kt('fDocumentId=%d'),$this->oDocument->getId())));
    }
    
    // simple helper function for the signoff list.
    function getInfoForSignoff($oSignoff) {
        $oUser = User::get($oSignoff->getUserId());
        return $oUser->getName();
    }
 }
 // }}}
?>


templates/reviewsignoffs/docaction.smarty

 <h2>{i18n arg_reviewname=$review_name}Document Review: #reviewname#{/i18n}</h2>

<p>{i18n arg_usercount=$required_signoffs arg_reviews=$present_signoffs arg_transitionname=$transition_name}The workflow that this document 
has requires  that it be signed off by #usercount# users before
the "#transitionname#" transition can be performed.  So far, #reviews# sign-offs have been noted by the system.
Since you are one of the users who are in a position to do this,  please review the document and click "Sign-off" if it passes 
your review.{/i18n}</p>

<h3>{i18n}Previous Signoffs{/i18n}</h3> 

{if (empty($signoffs))}
<div class="ktInfo"><p>{i18n}No Signoffs have occurred yet.{/i18n}</p></div>
{else}    
   {foreach from=$signoffs item=oSignoff} [[Image:]] {/foreach}
{/if}

<form action="{$smarty.server.PHP_SELF}" method="POST">
<input type="hidden" name="action" value="signoff" />
<input type="hidden" name="fDocumentId" value="{$document_id}" /> 

<input type="submit" value="{i18n}Sign-off{/i18n}" />
</form>

Step 5: Finishing up

fixme TODO

Personal tools