cancel
Showing results for 
Search instead for 
Did you mean: 

An alternative way to do a simple workflow.

anweber
Champ in-the-making
Champ in-the-making
Hi,

I will present you an alternative way to implement a simple workflow.  The aim of this post is just to share my experience.  Sorry for my poor English.    If it?s not clear enough, please contact me.

For a proof of concept, we had to develop a workflow.  Currently, the only solution offered by Alfresco about workflows is the ?simple workflow? mechanism. We have implemented our workflow with this ?simple workflow? technique. Our workflow actions have been written in a script (JavaScript).  To launch those actions, we have defined rules that execute this script when the document arrives in a given folder.  The action of our ?simple workflow? moves or copies the document into a given folder, a rule associated to this target folder (or to the parent folder) executes our script, then our script detects the current step and processes the corresponding business actions.  Using this way to implement workflows, we found some limitations:
    The user has the possibility (when it is defined) to launch the execution of a transition between two steps of our workflow via a menu associated to a document.  Then our transition should be in relation with one document and only one document (it?s not possible to define ? as in Documentum ? that our workflow carries more than one document).  In fact, to be honest, I should admit that it was not a limitation in our case.
    Only one or two user actions are allowed for each step of the simple workflow (accept/reject actions).  In our case, we should have to let the user choose amongst: accept, reject to the sender, delegate to someone else.
    When the user launches a transition of the workflow (accept or reject), he can?t give an argument (for example, to specify the delegate).
    In a simple workflow we can just move or copy the current file. But our workflow logic doesn?t always imply such moves or copies of files.
    The messages that we send to the user to explain what he can do on the workflow are very limited (they are short static labels directly shown by the action menu).
    We met some problems when the script executed by the action of a rule defined on the target folder of a step of our simple workflow moves some files into the original folder.  By example, we attach a simple workflow step when a document arrives in the folder F1, the user action on our simple workflow step moves the current file into the folder F2, our script is executed when this document arrives in the folder F2, this script moves the current document back into the folder F1: in this case, the document back into the folder F1 do not get the defined ?simple workflow? step (the user do not have the possibility to execute again the workflow transition).  In this case, we should have a recursive call to the same set of rules associated to the folder F1; it seems to be currently deactivated to prevent infinite loops.
To increase the possibilities during the delay we have to wait for a real workflow engine (expected in version 1.4), I propose an alternative:
    The actors of the workflow will receive HTML messages stored in simple folders into Alfresco.  Such message describes authorized decisions or notifications (for examples ?accept? or ?reject? or ?delegate to ??) with optional parameters (for example the delegate).
    The execution of the transition is launched when the user click on a hyperlink of his workflow message or on a submit button (it depends of the design of the HTML message).
    The execution of the transition is processed by a script (written in JavaScript) that contains the workflow logic.
A possibility to initialize the workflow, is to execute our script when a document arrives in a given folder (a rule associated to this folder specifies that our script will be executed when a document arrives).  The first step of our workflow is then to create the message allowing the first actor to go through the first transition(s) of our workflow.  Of course, it could involve several actors (and several messages).  Those messages are stored in input boxes (implemented as simple folders), each actor has his own input box.
We could decompose the logic executed by our script like this:
    Determine the current transition (current step of our workflow + user choice) ? to help this determination we have stored some hidden HTML input fields with useful information like the step number or the ID of the caller and we have added the ID of the decision made by the user to the HTTP submit (by example through an HTTP-get argument).
    Check if requirements are OK (for example, we check if mandatory arguments to the decision of the user are present and valid) or send back an error message directly to the caller (HTTP answer).
    Process business actions.
    Determine next actors and retrieve information to insert in the messages that will be addressed to those actors.
    Create and send message(s) to next actor(s) in their input boxes.
    Delete the calling message.
    Send back a direct message to the caller (HTTP answer) with information about the result of this transition.
To simplify the creation of the messages, I have developed a simple technique to use: ?HTML templates with server-side JavaScript? evaluated by our script (see my previous post : http://forums.alfresco.com/viewtopic.php?t=2378).
I also have defined a custom type to represent our workflow instances and to keep their status and other useful information. 
This way to implement workflows is still limited to simple scenarios (the logic of our workflow is hard coded in our script) but offers some advantages compared to the ?simple workflow? process originally offered by Alfresco:
    Each actor receives messages from the workflow in his input box (like in Documentum).
    We could ask an actor to choose amongst an arbitrary number of decisions/notifications.
    We could ask the user to add some arguments to some decisions.
    We do not have to move or copy documents to trigger transitions of our workflow.
    We could associate zero, one or more documents to each transition (we just have to insert links to those documents in our workflow messages).
    We do not meet the problem about recursive execution of the same set of rules associated to a folder because we do not use such rules (except to create a new workflow instance and to initialize it ? but it concerns a single call to our script).
    We have the possibility to address more information to the actor about what the workflow is expecting from him (and about the current status of the process).
Skeleton of our script:

main(){
   if (document != null) {
      // execution by a rule, it should be on the INBOUND event in the folder where incoming documents arrive
      //    workflow instance initialization
      //      - we create a new workflow instance (using a custom type)
      …
      //      - we initialize the properties of this object
      …
      //   send HTML stored message to the first actor(s)
      …
      return;
      
   } else {
      // this script has been called by an HTTP get from a HTML stored message
      
      // get info from args (caller, current step in the workflow, action name, action arguments, ?
      …
      // retrieve the workflow object and get info on current workflow instance
      …
      // treat actions
      switch(workFlowStep){
         case 1:
            switch(actionName){
               case "accept": // for example
                  // do some business process
                  …
                  // create and send stored messages to next actors
                  …
                  break;
               …
            }
            break;
         case 2:
            …
            break;
         …
         Case <n>:
            …
            break;
      }
      If (success){
         // if all is OK, we delete the calling stored HTML message
         …
         // we send a direct answer to the user
         return("<HTML>Your request has been treated successfully</HTML>");  // for example
      } else {
         return(errorMessage);
      }
}

main();

If you are interested by this way to implement workflows, I would be happy to show you a more concrete example.

   Regards,

      Andr
5 REPLIES 5

davidc
Star Contributor
Star Contributor
Hi,

This is a really creative use of javascript and custom types.

I'm very interested in understanding the workflows you wish to support i.e. the steps, transitions, decision points, actors involved, content involved, UI involved.

As you know, we're adding more workflow capabilities in v1.4, and I'm after real-life workflow use cases to ensure we hit the spot.

If the easiest way to describe your workflows is via your javascript, then that's fine.

anweber
Champ in-the-making
Champ in-the-making
Hi,

In this post, I will describe an example of usage of my alternative way to develop a simple workFlow.
I first introduce the scenario of this example, it's a vote process:
  • An author writes a document and the wishes to submit this document to vote.  He first selects the voters and then he submits it to vote.
  •    
  • Each voter receives a message from the workflow process to invite him to vote.
  •       The workflow message (an HTML-form page) offers the user interface necessary to submit the user choice (accept/reject/noOpinion).
          When a voter submits his vote, the workflow updates the workflow status: if all voters have submitted their votes, the workflow is finished and then the author receives a notification about the result.
I applied the following principles:
  • The workflow is managed by a single script written in JavaScript named "voteWf.js"
  •    
  • The workflow messages are HTML pages stored in Alfresco (in INBOX folders) and are constructed using a model (or template) that is also stored in the repository.
  •    
  • The votes are communicated by voters in answer to their workflow messages (HTML submit).  Those answers are also processed by our "voteWf.js" script.
  •    
  • A document submitted to vote should have the "votable" custom aspect : the author creates documents with the type "votableDocument".
  •       The "votable" aspect defines an association "votersAssoc".  The author haves to give the list of voter editing this association.
       
  • The information on the state of the workflow instance is stored into an instance of a custom type names "vote_wf".

There is my custom data model:

<model name="vote:contentmodel" xmlns="http://www.alfresco.org/model/dictionary/1.0">
   
   <description>Vote Workflow Content Domain Model</description>
   <author>A.Weber</author>
   <published>2006-07-18 20:00</published>
   <version>1.0</version>
   
   <imports>
      <import uri="http://www.alfresco.org/model/dictionary/1.0" prefix="d"/>
      <import uri="http://www.alfresco.org/model/content/1.0" prefix="cm"/>
   </imports>
   
   <namespaces>
      <namespace uri="vote_wf.model.content.1.0" prefix="vote"/>
   </namespaces>
   
   <!– ===== –>
   <!– Types –>
   <!– ===== –>
   <types>   
      <!– ========= –>
      <!– Documents –>
      <!– ========= –>

      <!– object used to store information about a vote workflow –>
      <type name="vote:vote_wf">
         <title>vote WF instance data</title>
         <parent>cm:content</parent>
         <properties>
            <property name="vote:status">
               <title>status of the workflow to treat a vote</title>
               <type>d:text</type>
            </property>
            <property name="vote:voters">
               <title>List of users called to vote</title>
               <type>d:text</type>
            </property>
            <property name="vote:refusers">
               <title>List of users having refused the submitted doc vote</title>
               <type>d:text</type>
            </property>
            <property name="vote:accepters">
               <title>List of users called having accepted the submitted doc</title>
               <type>d:text</type>
            </property>
            <property name="vote:votersWithoutOpinion">
               <title>List of users having no option</title>
               <type>d:text</type>
            </property>
         </properties>
      </type>
   
      <!– type for a votable document –>
      <type name="vote:votableDocument">
         <title>VotableDocument</title>
         <parent>cm:content</parent>
         <mandatory-aspects>
            <aspect>vote:votable</aspect>
         </mandatory-aspects>
      </type>   
      
   </types>

    <aspects>     
      <!– Definition of new Content Aspect: votable –>
      <aspect name="vote:votable">
         <title>votable</title>
      <associations>      
            <association name="vote:votersAssoc">
               <title>voters</title>
               <source>
                  <role>vote:voteObject</role>
                  <mandatory>false</mandatory>
                  <many>true</many>
               </source>
               <target>
                  <class>cm:person</class>
                  <role>vote:voter</role>
                  <mandatory>false</mandatory>
                  <many>true</many>
               </target>
            </association>      
      </associations>
      </aspect>     
   </aspects>            
</model>


I have modified the "web-client-config-custom.xml" :

<alfresco-config>
   
   <!– ====================================== –>
   <!– Custom Content Types in the Web Client –>
   <!– ====================================== –>
   <config evaluator="string-compare" condition="Content Wizards">
     <content-types>
         <!– vote WF –>
      <type name="vote:votableDocument"/>
     </content-types>
   </config>
   
   <!– ====================================== –>
   <!– Node Type Properties in the Web Client –>
   <!– ====================================== –>
   <!– . Vote Workflow data –>
   <config evaluator="node-type" condition="vote:vote_wf">
      <property-sheet>
        <show-property name="vote:status"
                    show-in-edit-mode="false"/>
        <show-property name="vote:voters"
                    show-in-edit-mode="false"/>
        <show-property name="vote:accepters"
                    show-in-edit-mode="false"/>
      <show-property name="vote:refusers"
                    show-in-edit-mode="false"/>
         <show-property name="vote:votersWithoutOpinion"
                    show-in-edit-mode="false"/>
      </property-sheet>
   </config>
  


   <!– =============================== –>
   <!– Custom "Add Aspect" Rule Action –>
   <!– =============================== –>
   <config evaluator="string-compare" condition="Action Wizards">
      <!– List of types shown in the is-subtype condition –>
     <subtypes>
      <type name="vote:votable"/>
     </subtypes>
     
      <!– List of content types shown in the specialize-type action –>
     <specialise-types>
      <type name="vote:votableDocument"/>
     </specialise-types>

     <!– List of aspects to show in the add/remove features action –>
     <!– and the has aspect condition                              –>
     <aspects>
      <aspect name="vote:votable"/>
    </aspects>
   </config>
      
   <!– =================================== –>
   <!– Aspect Properties in the Web Client –>
   <!– =================================== –>
   <!– Votable –>
   <config evaluator="aspect-name" condition="vote:votable">
      <property-sheet>
        <show-association name="vote:votersAssoc"
                        show-in-edit-mode="true"
               show-in-view-mode="true"/>
      </property-sheet>
   </config>

</alfresco-config>

I have created the following folder structure in Alfresco :
  • company_home
  •    
    • editor_space : home space of the editor (author)
    •       
      • INBOX_WF_VOTE : folder where messages from the workflow will arrive.
      •          
      • toVote : folder where documents to vote are stored (during the time where the workflow is in progress)
      •          
      • accepted : folder where documents voted and accepted are stored (when the workflow is finished).
      •          
      • refused : : folder where documents voted and refused are stored (when the workflow is finished).
      •          
      • balance : folder where documents voted without decision are stored (when the workflow is finished).
            
    • voter1_space : home space of a voter.  It should be defined as the homeFolder for the corresponding user because the workflow will retrieve the path of this folder through the definition of the user.
    •       
      • INBOX_WF_VOTE : folder where messages from the workflow will arrive.
            
    • voter2_space : same usage and structure than voter1_space
    •       
    •       
    • voter<n>_space : same usage and structure than voter1_space
    •       
    • wf_vote_data : space where we store necessary information to manage our workflow
    •       
      • HtmlTemplates : folder where models of HTML messages are stored.
      •          
      • ManagementData : folder where instances of "vote_wf" are stored (instances used to keep information about current workflow processes).
To initiate the workflow process, we use a folder rule associated to the INBOUND event in the "toVote" folder - this rule executes our "voteWf.js" script. 
A second folder rule associated to the "editor_space" attach a simple workflow to an INBOUND document, this simple workflow has a single action titled "sendToVoters" that will move the current document into the "toVote" subfolder.

Then the scenario for the author is:
  1. Create a document with the type "votableDocument".
  2.    
  3. Edit the association "votersAssoc" through the "contentProperties" screen.
  4.    
  5. Submit his document to vote through the "sendToVoters" action.
  6.    
  7. Wait for a message about the results of this vote into the INBOX_WF_VOTE folder
Then the scenario for a voter is:
  1. Check if he received a request to vote in his INBOX_WF_VOTE folder.
  2.    
  3. Open the received message (HTML form).
  4.    This form includes a link to the document to vote (the user will simply click on it to see this document), a radio group of buttons to select a decision (accept/reject/noOpinion), a sumit button to submit this decision.
       
  5. When the voter has a decision to submit, with the received HTML form, he selects his decision and submits it.
There is the code of my models for workflow messages.
  • msgVote.html :

<html>
   <head>
      <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
   </head>
   <body>
      <form action="<js>getUrlForScriptCall();</js>">
         <input type="hidden" name="msgPath" value="<js>msgPath</js>">
         <input type="hidden" name="votedDocName" value="<js>document.name</js>">
         <input type="hidden" name="votedDocPath" value="<js>document.displayPath</js>">
         The user <js>person.properties["cm:userName"]</js> submit the document <a href="/alfresco<js>document.url</js>"><js>document.name</js></a> to vote<br></br>
         <input type="radio" name="voteDecision" value="accepted">Your vote is : Yes<br></br>
         <input type="radio" name="voteDecision" value="rejected"> Your vote is : NO<br></br>
         <input type="radio" name="voteDecision" value="noOpinion">You have no opinion
         <input type="submit" name="submit" value="Submit vote">
      </form>
   </body>
</html>      
  • msgVoteResult.html :

<html>
   <head>
      <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
   </head>
   <body>
         The document <js>document.name</js> obtained the following result in the vote process : <js>wfData.properties["vote:status"]</js><BR></BR>         
         This document has been submitted to : <js>wfData.properties["vote:voters"]</js><BR></BR>
         This document has been accepted by : <js>wfData.properties["vote:accepters"]</js><BR></BR>
         This document has been refused by :  <js>wfData.properties["vote:refusers"]</js><BR></BR>
         The following voters have no opinion : <js>wfData.properties["vote:votersWithoutOpinion"]</js><BR></BR>
   </body>
</html>      


There is the code of my script "voteWf.js":

//========================================================
// log management
//========================================================
var debug = false;  // should be set to false when this script is stored in CVS
var logFile = null;
var logFolderPath = "test_vote_WF";
var logFilename = script.name + ".log";



// Log into a file with prefix: timestamp
function log(msg)
{
   // find or create the log file
   var logFileFolder;
   
   if (logFile == null){
      logFileFolder = companyhome.childByNamePath(logFolderPath);
      if (logFileFolder == null) return;
      logFile = logFileFolder.childByNamePath(logFilename);
   }
   if (logFile == null){
      if (true || logFileFolder.hasPermission("Write")){
            logFile = logFileFolder.createFile(logFilename);
         }
   }
   // append with a timestamp
   if (logFile != null && logFile.hasPermission("Write")){
     logFile.content += new Date().toLocaleString() +
                       "; " + msg +
                       "\r\n";
   }
}


//========================================================
// Manage lists
//========================================================


function getNbItemsInList(list){
   if (list == null) return(0);
   if (list == "") return(0);
   if (list.indexOf(",") <= 0) return(1);
   return(list.split(",").length);
}


/*
   Merge two list of strings concatenated in a string with the char "," as separator
   No doublons are allowed
*/
function mergeTwoLists(list1,list2){
   if (list1 == null || list1=="") return(list2);
   if (list2 == null || list2=="") return(list1);
   var list2Items = list2.split(",");
   var i;
   var list2Item;
   for (i in list2Items){
      list2Item = list2Items[i];
      // if the item is not in list1, we add it
      if ( ("," + list1 + ",").indexOf("," + list2Item + ",") < 0 ){
         list1 += "," + list2Item;
      }
   }
   return(list1);
}

/*
   check if a string item is in a list
*/
function isInList(list1,item){
   if(list1 == null || list1=="") return(false);
   if(item==null || item=="") return(false);
   return( ("," + list1 + ",").indexOf("," + item + ",") >= 0);
}


/*
   add an item to a list if this item is not in the list
*/
function addItemToList(list1,item){
   if (list1 == null) list1 = "";
   if (item == null || item == "") return(list1);
   if (list1 == "") return(item);
   list1 = list1 + "," + item;
   return(list1);
}

/*
   remove an item from a list (if this item is in the list)
*/
function removeItemFromList(list1,item){
   var len;
   if (list1 == null || list1=="") return(list1);
   if (item == null || item=="") return(list1);
   list1 = "," + list1 + ",";
   list1 = list1.replace(","+item+",",",");
   list1 = list1.substr(1);
   len = list1.length;
   list1 = list1.substr(0,len-1);
   return(list1);
}

/*
   add two exclusive lists (items in list1, can't be in list2)
*/
function addExclusiveLists(list1,list2){
   var resultList = "";
   if (list1 != "") {
      resultList = list1;
      if (list2 != ""){
         resultList += "," + list2;
      }
   } else {
      resultList = list2;
   }
   return(resultList);
}

/*
   Check if all elements of list1 are in list2 and vice versa
*/
function areListsEqual(list1, list2){
   var items;
   var i;
   var item;
   if (list1 == "" && list2 == "") return(true);
   if (list1 == "" && list2 != "") return(false);
   if (list2 == "" && list1 != "") return(false);
   if (list1.length != list2.length) return(false);
   items = list1.split(",");
   for (i in items){
      item = items[i];
      if (item != "" && !isInList(list2,item)) return(false);
   }
   return(true);
}

/*
   Return elements present in list1 and in list2
*/
function intersectionList(list1, list2){
   var items;
   var i;
   var item;
   var resultList = "";
   if (list1 == "") return("");
   if (list2 == "") return("");
   items = list1.split(",");
   for (i in items){
      item = items[i];
      if (isInList(list2,item)){
         if (resultList == "") resultList = item;
         else resultList += "," + item;
      }
   }
   return(resultList);
}


// ===================================================================
// HTML template management
// ===================================================================


function replaceHostedJavaScript(content){
   var posBeg;
   var posEnd;
   var jsExpr;
   var jsExprEvaluated;
   var tryCatch= false;
   while(true){
      posBeg = content.indexOf("<js>");
      if(posBeg>0){
         posEnd = content.indexOf("</js>",posBeg);
         if (posEnd>0){
            jsExpr = content.substr(posBeg+4, posEnd-posBeg-4);
            if (tryCatch){
               try{
                  jsExprEvaluated = eval(jsExpr + ";");
               }
               catch(exception){
                  jsExprEvaluated = "exception in the evaluation of " + jsExpr + " :" + exception;
               }
            } else    jsExprEvaluated = eval(jsExpr + ";");            
            content = content.substr(0,posBeg) +  jsExprEvaluated + content.substr(posEnd+5);
         }
      }
      else return(content);
   }
   return(content);
}



function getUrlForScriptCall(){
   var url = "";
   url += "/alfresco/command/script/execute/" + script.nodeRef;
   var expr = /\:\/\//;
   url = url.replace(expr,"/");
   return(url);
}

// ==================================================================


function getFileContent(folder, fileName){
   return("" + folder.childByNamePath(fileName).content);
}

/*
   Send a message in a mailBox
*/
function sendMsg(msgName, htmlFileName,mailBoxFolderPath){
    var htmlFolder;
   var mailBoxFolder;
   var content;
   var msgFile;

   htmlFolder = companyhome.childByNamePath(HTML_TEMPLATE_FOLDER_PATH);
   if (htmlFolder== null) {
      log("sendMsg() - ERROR - htmlFolder not found (path=\"" + HTML_TEMPLATE_FOLDER_PATH + "\")");
      return;
   }
   mailBoxFolder = companyhome.childByNamePath(mailBoxFolderPath);
   if (mailBoxFolder== null) {
      log("sendMsg() - ERROR - mailBoxFolder not found");
      return;
   }
   msgFile = mailBoxFolder.childByNamePath(msgName);
   if (msgFile != null) msgFile.remove();
   msgFile = mailBoxFolder.createFile(msgName);
   if (msgFile== null) {
      log("sendMsg() - ERROR -can't create msg file");
      return;
   }
   
   msgPath = mailBoxFolderPath + "/" + msgName;// var used by the JS embedded in the HTML template   
   
   content = getFileContent(htmlFolder,htmlFileName);
   content = replaceHostedJavaScript(content);
   msgFile.content = content;
   return("");
}

/*
   Delete a message from a mailBox
*/
function removeMsg(msgName, mailBoxFolderPath){
   var mailBoxFolder;
   var msgFile;

   htmlFolder = companyhome.childByNamePath(HTML_TEMPLATE_FOLDER_PATH);
   if (htmlFolder== null) {
      log("removeMsg() - ERROR - htmlFolder not found (path=\"" + HTML_TEMPLATE_FOLDER_PATH + "\")");
      return;
   }
   mailBoxFolder = companyhome.childByNamePath(mailBoxFolderPath);
   if (mailBoxFolder== null) {
      log("removeMsg() - ERROR - mailBoxFolder not found");
      return;
   }
   msgFile = mailBoxFolder.childByNamePath(msgName);
   if (msgFile != null) msgFile.remove();
   return("");
}



// ==================================================================


/*
   Retrieve the names the voters referred in the "vote:votersAssoc" association
   @param votableDocument document with the aspect "vote:votable"
   @return this list or null if it fails
*/
function getVotersNames(votableDocument){
   var voterName;
   var voters;
   var folderRef;
   var folderName;
   var votersNames = new Array();
   var i;
   var iFolder=0;
   if (!votableDocument.hasAspect("vote:votable")) return(null);
   voters = votableDocument.assocs["vote:votersAssoc"];
   for (i in voters){
      voterName = voters[i].properties["cm:userName"];
      votersNames[iFolder++] = voterName;
   }
   return(votersNames);
}



/*
   Retrieve the names the home folder of each voter referred in the "vote:votersAssoc" association
   @param votableDocument document with the aspect "vote:votable"
   @return this list or null if it fails
*/
function getVotersHomeFolders(votableDocument){
   var voter;
   var voters;
   var folderRef;
   var folderName;
   var votersFolders = new Array();
   var i;
   var iFolder=0;
   if (!votableDocument.hasAspect("vote:votable")) return(null);
   voters = votableDocument.assocs["vote:votersAssoc"];
   for (i in voters){
      voter = voters[i];
      folderRef = voter.properties["cm:homeFolder"];
      if (folderRef != null && folderRef.hasPermission("Read")){
         folderName = folderRef.parent.name + "/" + folderRef.name;
         votersFolders[iFolder++] = folderName;
      } 
   }
   return(votersFolders);
}


sendWfMessagesToVoters
/*
   Send a WF message to each voter
   Attention : I suppose the homeFolders are grand children of the "company_home" folder (TO REVIEW).
   
*/
function sendWfMessagesToVoters(docToVote){
   var votersFoldersNames;
   var i;
   var voterFolderName;
   var targetFolder;
   
   votersFoldersNames = getVotersHomeFolders(docToVote);
   for (i in votersFoldersNames){
      voterFolderName = votersFoldersNames[i];
      if (voterFolderName != null &&  voterFolderName != ""){
         log("sendWfMessagesToVoters(), send a message to voterFolder : " + voterFolderName + "/" + INBOX_WF_VOTE_NAME);            
         sendMsg("vote_for_" + docToVote.name + ".html", "msgVote.html", voterFolderName + "/" + INBOX_WF_VOTE_NAME);
      }
   }
}



/*
   At the end of the workflow process, sends a WF message to the user that initiated the current workflow process
   Attention : I presume the homeFolders are grand children of the "company_home" folder (TO REVIEW).
   
*/
function sendWfFinishedMessageToAuthor(docToVote, wfNode){
   var targetFolderPath;
   targetFolderPath = args["votedDocPath"].replace("/Company Home/","").replace("/toVote","/" + INBOX_WF_VOTE_NAME);
   log("sendWfFinishedMessageToAuthor(), send a message to author in folder :" + targetFolderPath);
   wfData = wfNode;            
   sendMsg("result_of_vote_for_" + docToVote.name + ".html", "msgVoteResult.html", targetFolderPath);
}


/*
   Create object to store information about a new vote workflow instance
*/
function createWfManagementData(docToVote){
   var targetFolder;
   var wfNode;
   targetFolder = companyhome.childByNamePath(WF_DATA_FOLDER_PATH);
   if (targetFolder != null) {
         wfNode = targetFolder.createNode("wf_" + docToVote.name , "vote:vote_wf");
         if (wfNode!=null){
            wfNode.properties["vote:status"]= "inProgress";
            wfNode.properties["vote:voters"]= getVotersNames(docToVote).join(",");
            wfNode.properties["vote:refusers"]= "";
            wfNode.properties["vote:accepters"]= "";            
            wfNode.properties["vote:votersWithoutOpinion"]= "";            
            wfNode.save();
         } else {
            log("createWfManagementData() - ERROR : fail to create object to store info about new WF instance");
         }      
   }   
}


/*
   Treat a document coming from the editor to the "toVote" folder.   
*/
function treatIncomingVotableDocFromEditor(){
   log("treatIncomingVotableDocFromEditor(), ==== BEGIN ===");
   sendWfMessagesToVoters(document);
   createWfManagementData(document);
   log("treatIncomingVotableDocFromEditor(), ==== END =====");   
}


function getWfDataNode(votedDocName){
   var wfDataFolder;
   var wfNode;
   wfDataFolder = companyhome.childByNamePath(WF_DATA_FOLDER_PATH);
   if (wfDataFolder != null) {
         wfNode = wfDataFolder.childByNamePath("wf_" + votedDocName);
         if (wfNode==null){
            log("getWfDataNode() - ERROR : object where info about WF instance are stored not found");
         }      
   }
   return(wfNode);   
      
}


/*
    Calculate the new Status of the workflow
*/
function calculateNewWfStatus_BasedOnVoteOnMajority(oldStatus, votersList, acceptersList, refusersList, votersWithoutOpinionList){
   var nbRefusers;
   var nbAccepters;
   log("calculateNewWfStatus_BasedOnVoteOnMajority() - INFO : acceptersList= \"" + acceptersList + "\"");
   log("calculateNewWfStatus_BasedOnVoteOnMajority() - INFO : refusersList= \"" + refusersList + "\"");
   nbAccepters = getNbItemsInList(acceptersList);
   nbRefusers = getNbItemsInList(refusersList);
   
   log("calculateNewWfStatus_BasedOnVoteOnMajority() - INFO : nbAccepters= " + nbAccepters );
   log("calculateNewWfStatus_BasedOnVoteOnMajority() - INFO : nbRefusers= " + nbRefusers );
   
   
   if (nbAccepters > nbRefusers) return("accepted");
   if (nbAccepters < nbRefusers) return("refused");
   return("balance");   
}

/*
   Check if the workflow is finished
*/
function isVoteFinished_BasedOnVoteOnMajority(votersList, votersHavingVotedList, acceptersList, refusersList, votersWithoutOpinionList){
   return(areListsEqual(votersList, votersHavingVotedList));
}


/*
   Finish the workflow with a given status
*/
function finishWf(originalDoc, wfNode){
   var targetFolderPath;
   var targetFolder;
   var author;
   var wfStatus;

   wfStatus = wfNode.properties["vote:status"];
   targetFolder = originalDoc.parent.parent.childByNamePath(wfStatus);
   if (targetFolder == null){
      log(
         "finishWf() - INFO : folder where we should move the voted document not found, a folder named " +
         wfStatus +
         " and localized as a brother of the folder where the voted document is was expected"
      );
   } else if (!targetFolder.hasPermission("Write")) {
      log("finishWf() - INFO : the current user has no enough rights on the target folder :" + targetFolder.displayPath  + "/" + targetFolder.name );
   } else {
      log("finishWf() - INFO : we move the original voted doc from " + originalDoc.displayPath + " to :" + targetFolder.displayPath  + "/" + targetFolder.name );
      originalDoc.move(targetFolder);
      wfNode.remove();
   }
}

/*
  Treat decision of the voter
  Decision = "accepted", "rejected", "noOpinion"
*/
function treatVoterChoice(originalDoc, wfNode, voterName, decision){
   var votersList;
   var acceptersList;
   var refusersList;
   var votersWithoutOpinionList;
   var votersHavingVotedList;
   var wfNodeModified = false;
   var wfStatus;
   var wfFinish = false;
   
   if (wfNode == null) return(false);
   
   votersList = wfNode.properties["vote:voters"];
   acceptersList = wfNode.properties["vote:accepters"];
   refusersList = wfNode.properties["vote:refusers"];
   votersWithoutOpinionList = wfNode.properties["vote:votersWithoutOpinion"];
   votersHavingVotedList = mergeTwoLists(acceptersList,refusersList );
   votersHavingVotedList = mergeTwoLists(votersHavingVotedList,votersWithoutOpinionList );
   wfStatus = wfNode.properties["vote:status"];
   
               
   if ( !isInList(votersList, voterName) ){
      // the voter is not in the list of voters
      log("treatVoterChoice() - ERROR : the voter(\"" + voterName + "\") is not in the list of voters");
      return(false);
   }
   if ( isInList(votersHavingVotedList, voterName) ){
      // the voter has already voted
      log("treatVoterChoice() - ERROR : the voter(\"" + voterName + "\") has already voted ");
      return(false);
   }
   
   log("treatVoterChoice() - INFO : the voter(\"" + voterName + "\") gives his vote : \"" + decision + "\"");
   switch("" +  decision){
      case "accepted":
            log("treatVoterChoice() - INFO : treat accepted vote");
            acceptersList = addItemToList(acceptersList, voterName);
            wfNode.properties["vote:accepters"] = acceptersList;
            wfNodeModified = true;
            break;
      case "rejected":
            log("treatVoterChoice() - INFO : treat rejected vote");
            refusersList = addItemToList(refusersList, voterName);
            wfNode.properties["vote:refusers"] = refusersList;
            wfNodeModified = true;
            break;
      case "noOpinion":
            log("treatVoterChoice() - INFO : treat noOpinion vote");
            votersWithoutOpinionList = addItemToList(votersWithoutOpinionList, voterName);
            wfNode.properties["vote:votersWithoutOpinion"] = votersWithoutOpinionList;
            wfNodeModified = true;
            break;
      default:
            log("treatVoterChoice() - INFO : unexpected decision");
   }
   
   if (wfNodeModified){
      votersHavingVotedList = addItemToList(votersHavingVotedList, voterName);
      // set new status
      wfStatus = calculateNewWfStatus_BasedOnVoteOnMajority(wfStatus, votersList, acceptersList, refusersList, votersWithoutOpinionList);
      log("treatVoterChoice() - INFO : new status for the workflow :" + wfStatus);
      wfNode.properties["vote:status"] = wfStatus;
      // save the wfNode
      wfNode.save();
      // check if the vote is finished
      wfFinish = isVoteFinished_BasedOnVoteOnMajority(votersList, votersHavingVotedList, acceptersList, refusersList, votersWithoutOpinionList);
   }
      
   return(wfFinish);
   
}


/*
   Treat a document coming from a voter to a folder named "accepted" or "rejected" or "noOpinion".   
*/
function treatVoteByHtmlSubmit(){
   log("treatVoteByHtmlSubmit(), ==== BEGIN ===");
   var htmlPageToReturn = "<HTML>Your vote has been submitted</HTML>";
   var docName = args["votedDocName"];
   var decision = args["voteDecision"];
   var voterName = person.properties["cm:userName"];
   var docPath = args["votedDocPath"];
   var msgPath = args["msgPath"];
   var originalDoc;
   var msgFile;
   var wfFinish;
   
   docPath= docPath.replace("/Company Home","");
   originalDoc = companyhome.childByNamePath(docPath + "/" + docName);
   if (originalDoc == null){
      log("treatVoteByHtmlSubmit(), orginal doc not found");
   }
   else {
      wfNode = getWfDataNode(docName);
      document = originalDoc;
      wfFinish = treatVoterChoice(originalDoc, wfNode, voterName, decision);
      if (wfFinish){
         // the wf is finished, we send a notification to the initiator
         log("treatVoteByHtmlSubmit() - INFO : the vote is finished");
         sendWfFinishedMessageToAuthor(originalDoc, wfNode);
         finishWf(originalDoc, wfNode);         
      }
   }         
   msgFile = companyhome.childByNamePath(msgPath);
   if (msgFile != null) msgFile.remove();
   
   log("treatVoteByHtmlSubmit(), ==== END =====");   

   return(htmlPageToReturn);
}


// ***********************************************************************

// CONSTANTS
var WF_DATA_FOLDER_PATH = "test_vote_WF/wf_vote_data/ManagementData";  // path related to the companyHome folder
var HTML_TEMPLATE_FOLDER_PATH = "test_vote_WF/wf_vote_data/HtmlTemplates"; // path related to the companyHome folder
var INBOX_WF_VOTE_NAME = "INBOX_WF_VOTE";
// GLOBAL VARS
var document;
var msgPath = "";
var wfData= null; // node where data about current workflow instance are stored, used by the evaluation of the "msgVoteResult"

/*
   MAIN
*/
function main(){
   log("main(), ========= BEGIN ============");
   if (document != null){
      log("main(), called from rule action");
      // execution by a simple WF action
      var currentFolderName;
      currentFolderName = document.parent.name;
      if (currentFolderName == "toVote"){
         treatIncomingVotableDocFromEditor();
      } else {
         log("main(), error : case not treated");
      }
   } else {
      log("main(), called from servlet - INFO … votedDocName=" + args["votedDocName"] + ",votedDocPath=" + args["votedDocPath"]+ ",voteDecision=" + args["voteDecision"]);
      // arguments =    votedDocName, votedDocPath, voteDecision
      return(treatVoteByHtmlSubmit());      
   }
   log("main(), ========= END ============");      
}

main();



That's all folks!  I hope it will be useful to some of you.  It's just an example of what it's possible to do (the code I have developed is not clean enough to be used in production, in particuliar it should be necessary to solve some security aspects). Please contact me, if you need more explanations.

                Regards,

                           Andre

paulhh
Champ in-the-making
Champ in-the-making
Hi Andre

Thanks for posting this example - it starts to show how much you can do to extend Alfresco without changing its internals.

I know you say this isn't production quality, but I would suggest that it's an excellent example/sample - have you considered creating a forge project for script examples?  These would be easier for people to find and experiment with (and only a little more work for you).

Cheers
Paul.

peebles
Champ in-the-making
Champ in-the-making
So I have a question ,,, its there a better way to return to the alfresco UI when the script is finished?  For example, drop the user back into some space?

peebles
Champ in-the-making
Champ in-the-making