Sitecore Workflow – Multiple Users Approving an Item

Today we are going to create a workflow that allows multiple users to approve an item.

What we won’t cover in this post is how to setup security on fields and workflow commands.  For that, I recommend John’s post on Security Access Rights.

Based on the business requirements, there are a few things I need to do:

  • Checks to make sure that at least one user is required for the approval
  • Make it easy for editors to determine who needs to approve the content

Workflow

I’ve gone ahead and built out my workflow with the needed commands and actions and selected some amazing icons.

MultiApprove Workflow
MultiApprove Workflow

User List Field Type

First, we need to build a custom Field Type that allows us to select the users we want to require.  I’ve called mine User List.  John blogged about a users droplist to accomplish this, so we’re going to build it as a multilist using another blog showing how to create a custom multilist field.  I’ve created a txt of Lookup.cs as it’s a bit long.

Now, actually setup the Field Type in Sitecore.  In the core database, I’ve created mine here:
/sitecore/system/Field types/List Types/User List using the template Template Field Type

Set the Assembly and Class appropriately.  Mine were:
Assembly: sandbox.BusinessLogic
Class: sandbox.BusinessLogic.Common.UserLookup

Workflow Template

With the new user list created, we can create our template like so.  I’ve also removed editing permissions to Everyone on the Approved By and the Awaiting Approval From fields as we don’t want editors to be able to change these.

MultiApproveTemplate
MultiApprove Template

Once you’ve created this, make sure you inherit it to either your site’s Standard Template  or Page Template.  In my case, my News Page inherits it.

MultiApprove Template Inheritance
MultiApprove Template Inheritance

Users Assigned Check Action

In my case, we want to prevent items from progressing in the workflow if they have not selected any users to approve it.  So, we want to create an Action under the Draft’s Submit command.  I’ve named mine UsersAssignedCheck.

User Assigned Check Action
User Assigned Check Action

In the Type String field, fill in the classNamespace.Classname, AssemblyName. For this example, we’ll be using:
sandbox.BusinessLogic.Workflows.MultiApprove.UsersAssignedCheck, sandbox.BusinessLogic

UserAssignedCheck Code:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Web.UI.Sheer;
using System;
using sandbox.BusinessLogic.Helpers;
using Sitecore.Workflows.Simple;
 
namespace sandbox.BusinessLogic.Workflows.Multiapprove
{
    public class UsersAssignedCheck
    {
        public void Process(WorkflowPipelineArgs args)
        {
            Assert.ArgumentNotNull((object)args, "args");
            ProcessorItem processorItem = args.ProcessorItem;
            if (processorItem == null)
                return;
            var item = args.DataItem;
 
            // if no users selected, throw warning
            if (String.IsNullOrEmpty(item.Fields[MultiApproveConstants.Sitecore_UsersFieldName].Value)))
            {
                SheerResponse.Alert("Assign at least 1 Sitecore User to the MultiApprove Sitecore Users.", false, "Error");
 
                // abort to prevent Next State
                args.AbortPipeline();
            }
        }
    }
}

Screenshot:

Assign One User Error
Assign One User Error

Approve Action

The approve action will:

  • Prevent the next state if all users have not approved
  • Update the Approved By field
  • Update the Awaiting Approval From field
  • Clear Approved By and Awaiting Approval From  fields if all users have approved

The String Type for this action is:
sandbox.BusinessLogic.Workflows.MultiApprove.Approve, sandbox.BusinessLogic

Approve Action Code:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using System;
using System.IO;
using sandbox.BusinessLogic.Helpers;
using Sitecore.Workflows.Simple;
using Sitecore.Security.Authentication;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using sandbox.BusinessLogic.Models.sitecore.templates.Common.Workflows;
using Sitecore.Web.UI.Sheer;
 
namespace sandbox.BusinessLogic.Workflows.Multiapprove
{
    public class Approve
    {
        public void Process(WorkflowPipelineArgs args)
        {
            Assert.ArgumentNotNull((object)args, "args");
            ProcessorItem processorItem = args.ProcessorItem;
            if (processorItem == null)
                return;
 
            // this is the context item
            var item = args.DataItem;
 
            // the active user
            string activeUserName = AuthenticationManager.GetActiveUser().Name.ToLower();
 
            // required Approver List
            List<string> requiredApprovers = new List<string>(item.Fields[IMultiApproveConstants.Sitecore_UsersFieldName].Value.ToLower().Split('|'));
 
            // update fields if this user is an approver
            if (requiredApprovers.Any(str => str.Contains(activeUserName)))
            {
                AddToApprovedBy(item, activeUserName);
                UpdateAwaitingApprovalFrom(item, requiredApprovers);
            }
 
            // if we're still waiting for approval, display a message telling the user they're not the last approver
            if (!String.IsNullOrEmpty(item.Fields[IMultiApproveConstants.Awaiting_Approval_FromFieldName].Value))
            {
                // not all users approved
                SheerResponse.Alert(
                    "You have approved this item, however, the following users still need to approve this content:\n"
                    item.Fields[IMultiApproveConstants.Awaiting_Approval_FromFieldName].Value, false, "Approval");
 
                // abort the pipeline, because we don't want to reach the "Next State" execution
                args.AbortPipeline();
            }
            else
            {
                // clear Approved By and Awaiting Approval From fields
                // as we should clear it before the item is edited again
                ClearApprovals(item);
            }
 
        }
 
        private void AddToApprovedBy(Item item, string user)
        {
            if (ContainsUser(item.Fields[IMultiApproveConstants.Approved_ByFieldName].Value, user)) return;
            item.Editing.BeginEdit();
            try
            {
                item[IMultiApproveConstants.Approved_ByFieldName] = AddUserToMultiline(item[IMultiApproveConstants.Approved_ByFieldName], user);
                item.Editing.EndEdit();
            }
            catch (Exception)
            {
                item.Editing.CancelEdit();
            }
        }
 
        private void UpdateAwaitingApprovalFrom(Item item, List<string> requiredApprovers)
        {
            StringBuilder awaitingApprovalUsers = new StringBuilder();
            List<string> approvedBy = new List<string>(item.Fields[ IMultiApproveConstants.Approved_ByFieldName].Value.Split('\n'));
 
            foreach (string approver in requiredApprovers)
            {
                if (!approvedBy.Any(str => str.Contains(approver)))
                {
                    awaitingApprovalUsers.AppendLine(approver);
                }
            }
            item.Editing.BeginEdit();
            try
            {
                item[IMultiApproveConstants.Awaiting_Approval_FromFieldName] = awaitingApprovalUsers.ToString();
                item.Editing.EndEdit();
            }
            catch (Exception)
            {
                item.Editing.CancelEdit();
            }
        }
 
        private string AddUserToMultiline(string multiline, string user)
        {
            StringBuilder sb = new StringBuilder(multiline);
            sb.AppendLine(user);
            return sb.ToString();
        }
 
        private Boolean ContainsUser(string multiline, string user)
        {
            using (StringReader reader = new StringReader(multiline))
            {
                string line;
                while ((line = reader.ReadLine()) != null)
                {
                    if (line == user)
                    {
                        return true;
                    }
                }
            }
            return false;
        }
 
        private void ClearApprovals(Item item)
        {
            item.Editing.BeginEdit();
            try
            {
                item[IMultiApproveConstants.Approved_ByFieldName] = "";
                item[IMultiApproveConstants.Awaiting_Approval_FromFieldName] = "";
                item.Editing.EndEdit();
            }
            catch (Exception)
            {
                item.Editing.CancelEdit();
            }
 
        }
    }
 
}

Screenshots:

This is the notification you will receive if you’re not the last required approver.

Approval Notification
Approval Notification

The item is updated, so all users can easily see what approvals are still required.  You can see that I have approved it.  Note: I am an admin during this test.  Normal content editors would not have the option to edit these fields at this point.

Test News Page
Test News Page

We’re done! Or are we?

This is a working proof of concept.  Ideally you won’t have hard-coded strings or display unnecessary users.  Maybe we want to hide the commands to those users who have already approved, that’s for another post!  Enjoy!

Built on Sitecore 8.0 Update 4

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *