Field Level Audit

Included is an example of how you can implement field-level audit to track all form changes.

FieldAuditSample.nitro_s


Known Issues

1. If the submission fails for any reason, a record in the audit table will be added regardless.  There is no good way to detect if a FEB submissions fails and therefore you run the risk of seeing a record in the audit table even if the form submission fails.  There are a few ways that submissions can fail:


i) Empty required field, incorrect field content, field-level validation. 

Workaround: In the validateButtonPressed event walk the form and check the validity of each field using the isValid() function. If any fields are not valid then don't write the audit record, but still trigger the submit so that it will show the "invalid" fields.  This workaround leverages code already published in this wiki, Recursive Function to Walk all items in a form.  The following code in the validateButtonPressed is an example:


if(pActionId === 'S_Submit') {

  //reset valid flag for multiple submit attempts in one session
  app.getSharedData().isFormValid = true;

  //walk all form items looking for invalid fields
  app.getSharedData().getItem(form, app.getSharedData().processItem);

  if(!app.getSharedData().isFormValid) {
    return true; //we still want it to try to submit that way any errors will be reported to the user
  } else {
    //add audit table entry

    //proceed with the submit
    return true;
  }
}


ii) Email on submit has invalid email address in the "to" field

Workaround: This is usually a design-time error and can be avoided if the email is configured properly.  This is not an area to be concerned with.


iii) Connectivity issues with server

If there are connectivity issues the the form cannot be submitted at all.  I am still unsure if this is an error that needs to be considered.


References

Recursive Functions - 


https://www.ibm.com/developerworks/community/wikis/home?lang=en#!/wiki/W65fd19fc117a_4d18_87e4_5f7b8a6727cc/page/JavaScript%20Functions?section=Recursive%20Function%20to%20Walk%20all

HCL Leap  JS API

 JavaScript Used

I am including the code for those that cannot import the 8.6.2 sample.

onLoad


/**********************************************************************
 * GLOBAL VARIABLES
 **********************************************************************/
app.getSharedData().fieldMap = new Array(); //array that holds field ID and current value, populated on form load
app.getSharedData().theAuditTable = BO.F_Table1; //the ID of the table used for the audit info
app.getSharedData().changeString = ""; //the string that will be used to store all the form changed, used in approach 1a, 2 and 3
app.getSharedData().hdls = new Array(); //array to store the handles of all the dynamic event listeners, in 8.6.2 these must be disconnected in the onDestruct form event

//change this param if you dont want to perform audit in start stage
app.getSharedData().performAuditInStartStage = true; //valid values are true or false

/************************************************************
 * FUNCTIONS TO ADD NEW ROW TO AUDIT TABLE
 *
 * Pick the one that fits the audit scenario you choose and
 * modify so that it uses your field IDs
 ************************************************************/
//Approach #1a - This function is specific to the audit table used in the first approach
app.getSharedData().addRowToAuditTable = function(theTable, theUser, theChange) {
    var r = theTable.createNew();
    r.F_Date1.setValue(new Date());
    r.F_User.setValue(theUser);
    r.F_Changes.setValue(theChange);
    app.getSharedData().theAuditTable.add(r);
}

//Approach #1b - table has all audit data in separate fields
app.getSharedData().addRowToAuditTable3 = function(theTable, theUser, theField, theOld, theNew) {
    var r = theTable.createNew();
    r.F_Timestamp3.setValue(new Date());
    r.F_SingleLine7.setValue(theUser);
    r.F_SingleLine8.setValue(theField); //field
    r.F_SingleLine9.setValue(theOld); //old
    r.F_SingleLine10.setValue(theNew); //new
    theTable.add(r);
}

//Approach #2
app.getSharedData().addRowToAuditTable2 = function(theTable, theUser, theChange) {
    var r = theTable.createNew();
    r.F_Timestamp1.setValue(new Date());
    r.F_SingleLine5.setValue(theUser);
    r.F_Paragraphtext2.setValue(theChange);
    theTable.add(r);
}

/*
* This is the function where you place the logic that you want to perform on the item that you are currently looking at.
* The recursive function passes the handle to the current item, from which you can then access any of its properties
*/
app.getSharedData().processItem = function(item) {
   
  // store the items current value for comparison later on
  app.getSharedData().fieldMap.push({id:item.getId(),value:item.getValue()});

  //we only want to check input items, so we need to specify them directly, if you do not perform this check, then the code would attempt to create the onItemChanged
  //event listener on every form item, even those that don't have that event like text, section, table, tabbed folder or button items.
  if((item.getType() === "text" || item.getType() === "textArea" || item.getType() === "date" ||
     item.getType() === "checkGroup" || item.getType() === "radioGroup" || item.getType() === "number" ||
     item.getType() === "currency" || item.getType() === "comboBox" || item.getType() === "horizontalSlider" ||
     item.getType() === "choiceSlider" || item.getType() === "time" || item.getType() === "checkGroup" ||
     item.getType() === "radioGroup" || item.getType() === "horizontalSlider" || item.getType() === "choiceSlider" ||
     item.getType() === "surveyQuestion") && item.getId() !== "F_Paragraphtext3" && item.getId() !== "F_RID" && item.getId() !== "F_Timestamp") {
      
    var ev = "onItemChange";

    //add an onItemChange Listener - the code within will be called when the fields value changes
    var hndl = item.connectEvent(ev, function(success)
        {
       var v = "";           
       if(typeof get(app.getSharedData().fieldMap, item.getId()) !== "undefined") {
           v = get(app.getSharedData().fieldMap, item.getId());
       }

       //string to store the change string, this can be formatted however you want and is only used in specific use case
       var s = "";
       s = "~ " + item.getTitle() + "(" + item.getId()+ ") changed from '" + v + "' to '" + item.getValue() + "'";

       //approach #1 - each field change is recorded
       app.getSharedData().addRowToAuditTable(BO.F_Table1, app.getCurrentUser(), s);
       app.getSharedData().addRowToAuditTable3(BO.F_Table3, app.getCurrentUser(), item.getId(), v, item.getValue());

       //approach #2 - all changes recorded in one string on submit
       app.getSharedData().changeString += s + "\n";

       //update the value in the global map
       set(app.getSharedData().fieldMap, item.getId(), item.getValue())
    }); //end of dynamic event listener

     // Store the handle to this listener so that it can be cleaned up when the form is re-loaded
     // if it is determined that the re-load behavior is a defect then this could be removed when
     // its fixed.
     app.getSharedData().hdls.push(hndl);
  }
}

/**********************************************************************
* UTILITY FUNCTIONS - NO NEED TO CHANGE ANY CODE BELOW THIS POINT
***********************************************************************/

/*
* Returns true if the current item has children, otherwise false.
*/
app.getSharedData().hasItems = function(containerID) {
    var list = containerID.getChildren();
    if(list.getLength() > 0) {
     return true;
    } else {
        return false;
    }   
}

/*
* Recursive function used for counting form items.
* containerID: UI item (i.e. page or item)
* processItem: the function that contains the work we want to perform on the item we have accessed
*/
app.getSharedData().getItem = function(containerID, processItem) {
      var itemList;
      var pageList;
      var pageCount = 1;
      debugger;
      
      //check to see if the container is a form as it requires different processing
      if(containerID.getType() === "form") {
          pageList = containerID.getPageIds(); //list of the page IDs - not the actual objects!!
          pageCount = pageList.length;
      } else {
      itemList = containerID.getChildren();    
    }

    //need a loop to account for different pages
    for(var p=0; p<pageCount;p++) {
        if(containerID.getType() === "form") {
          itemList = containerID.getPage(get(pageList, p)).getChildren(); //get the page object from the form
        }
        
        //loop all the items
        for(var i=0; i<itemList.getLength(); i++)
        {
          var theItem = itemList.get(i);
          if(app.getSharedData().hasItems(theItem)) {
              //if container go into it...
                app.getSharedData().getItem(theItem, processItem);   
            } else {
                //other wise do something with the item
                if(dojo.isFunction(processItem)) { //make sure that the parameter passed is a function
                  processItem(theItem);        
                }
            }
        }
      }
}

//call the function
//If you don't want to keep track of field changes in the start stage you can perform a check
if(BO.getCurrentStage() === "ST_Start" && app.getSharedData().performAuditInStartStage === false) {
  //don't attach the listener
} else {
  app.getSharedData().getItem(form, app.getSharedData().processItem);
}



onDestruct


//remove all connected events that were connected in the form onLoad - may not need to do this if bug gets fixed
//iterate through the array that contains all the handles and disconnect each one
for(var i=0; i< app.getSharedData().hdls.length;i++) {
  form.disconnectEvent(get(app.getSharedData().hdls, i));
}

beforeSave

// approach #2 - only store audit record on form submit
// This will create one row in the audit table before the form is submited
app.getSharedData().addRowToAuditTable2(BO.F_Table2, app.getCurrentUser(), app.getSharedData().changeString);

// approach #3 - on submit audit record is stored in other form
// This will set the content into hidden fields and then call the service
// to create the record in the audit form
BO.F_RID.setValue(BO.getDataId());
BO.F_Timestamp.setValue(new Date());
BO.F_Paragraphtext3.setValue(app.getSharedData().changeString);
form.getServiceConfiguration('SC_ServiceConfig0').callService();