Loading

Workflow Approval Delegation

In my previous post I presented the approach for configuring workflow approvers on document level, so many documents could reuse the same workflow, having different approvers. In this post I want to suggest minor improvement for the approval process - delegation.

There are situations, especially during summer, or winter holidays, when many people take vacation to have a good time with their family and friends. Of course all we really like this period as well as having vacation. However, this might significantly slow down work processes. Let's imagine there is a document, that needs to be approver by 5, or even 10, persons sequentially, but 2 or 3 of those approvers are on vacation - this process might take 3-5 weeks instead of 3-5 days. In some case this is just unacceptable!

So how can we improve this process and allow pages/content to move smoothly through a workflow? Approval delegation should address this issue. Basically there are two steps to enable it: allow users to delegate approvals and handle workflow security appropriately. So let's start!
 

Approval Delegation

In order to configure delegation, first of all, we need to configure time period, we want to delegate our approval rights. I suggest adding two Date fields to the UserSettings class of the Membership module: Vacation Start Date and Vacation End Date. Kentico already has appropriate form control - Calendar, which makes date selection much easier and does not require any development efforts. 
Once we can setup our vacation period, we need to have a possibility to specify who will substitute for us. To address this, we need to add one more field: Delegate Approval To and use user selector as a form control for it. This will provide us with possibility to select any user within the system and, again, with no development efforts.
Next step here is to provide users with the UI to setup delegation. Luckily, users, who approve content, should have access to Kentico Administration as well as to My Profile application within it. If you'll check this application, you'll notice, that new fields already appeared there:
 

delegation.jpg
 

So, congratulations - we are done with this. Let's move forward to workflow security.
 

Workflow Security

Doesn't matter if you have approvers configured on the document level, like described in previous post, or using default workflow configuration you'll have to get all the approvers for the current step and check if any of them is on vacation and, if someone is, retrieve user, who substitutes for original approver, and add him to a set of possible/potential approvers. Let's see how code, that handles this, looks like:

 

[assembly: RegisterCustomManager(typeof(CustomWorkflowManager))]
public class CustomWorkflowManager : WorkflowManager
{
    protected override bool CheckStepPermissionsInternal(TreeNode node, UserInfo user, WorkflowActionEnum action)
    {
        if (action == WorkflowActionEnum.Approve)
        {
            // get users allowed to approve from the document
            var approvers = CacheHelper.Cache(cs => GetDocumentApprovers(document, cs), new CacheSettings(60, string.Format("{0}|{1}", "documentapprovers", document.DocumentID)));
            if (approvers.Contains(user))
            {
                return true;
            }  
        }

        return base.CheckStepPermissionsInternal(node, user, action);
    }

    /// <summary>
    /// Retrieves approvers, or their delegates for a given document
    /// </summary>
    /// <param name="document">Document</param>
    /// <param name="cs">Cache settings</param>
    /// <param name="round">Approval round</param>
    /// <returns>Collection of users, who have to approve document</returns>

    private static List<UserInfo> GetDocumentApprovers(TreeNode document, CacheSettings cs)
    {
        var approvers = GetOriginalDocumentApprovers(document); //--> I'm not providing implementation of this method, as it depends on the particular approach         

        var result = GetDelegates(approvers);

        if ((result != null && result.Count > 0) && cs.Cached)
        {
            cs.CacheDependency = CacheHelper.GetCacheDependency("documentid|" + document.DocumentID);
        }

        return result;
    }

    /// <summary>
    /// Substitutes original document approvers in specified list with their delegates
    /// </summary>
    /// <param name="approvers">Original document approvers</param>
    /// <returns>List of actual approvers</returns>
    private static List<UserInfo> GetDelegates(List<UserInfo> approvers)
    {
        List<UserInfo> delegates = new List<UserInfo>();
        //add approvers, those are not on vacation
        delegates.AddRange(approvers.Where(a => !IsUserOnVacation(a)));

        //add approvers, those are not on vacation
        var onVacation = approvers.Where(a => IsUserOnVacation(a));

        foreach (UserInfo user in onVacation)
        {
            int delegateID = user.GetIntegerValue(Constant.USER_DELEGATE_APPROVAL_FIELD_NAME, -1);
            if (delegateID > 0)
            {
                delegates.Add(UserInfoProvider.GetUserInfo(delegateID));
            }
            // no delegate
            else
            {
                delegates.Add(user);
            }
        }

        return delegates;
    }

    /// <summary>
    /// Checks whether given user is currently on vacation
    /// </summary>
    /// <param name="user">User</param>
    /// <returns>Is on vacation</returns>
    private static bool IsUserOnVacation(UserInfo user)
    {
        var vacationStartDate = user.GetDateTimeValue(Constant.USER_VACATION_START_DATE_FIELD_NAME, DateTime.Now.AddHours(-1));
        var vacationEndDate = user.GetDateTimeValue(Constant.USER_VACATION_END_DATE_FIELD_NAME, DateTime.Now.AddHours(-1));
        return (DateTime.Now >= vacationStartDate && DateTime.Now <= vacationEndDate);
    }
}


So this will take care about workflow security and allow delegate to approve a document. However, this does not cover any other permissions, which means, that delegate should have the same level of access as original approver has besides workflow. This includes access to Administration area, Pages application and document(s).
 

Conclusion

We've got a handy tool that allows us to delegate workflow approval with a minimum effort. I'd like to make a point here that whenever you need to extend Kentico default behavior, you have to look for appropriate way or place to do so, like we did with CustomWorkflowManager in this case. No need to jump on Visual Studio and reinvent the wheel and re-develop entire modules. 

Please feel free to leave your feedback or share your experience with similar problems - we highly appreciate this!