+1-888-365-2779
Try Now
More in this section

Forums / Developing with Sitefinity / Custom Workflow for content silos

Custom Workflow for content silos

9 posts, 0 answered
  1. HRC
    HRC avatar
    44 posts
    Registered:
    16 Mar 2009
    16 Jul 2012
    Link to this post
    Wondering if anyone has done anything like this: 

    1) I'd like to able to restrict a workflow to a section within the website:

     Let's say i have 3 sections within my site, each with child sections. I have 3 groups of 6 groups of users: a group of content AUTHORS and content APPROVERS for each section. Each section has specific permissions on it to only allow editing by authors and approvers of that section. 

    I'd like this separation to also carry over to the workflow. Currently, when an author from any section sends an page for approval, the approvers from every section are notified. 

    2) I'd like to customize the NotifyGroup message to something more useful then "You have new items waiting approval."

    I've been able integrate "PagesApprovalWorkflow.xamlx" into my project, modify it by following this post: 
    http://www.sitefinity.com/documentation/documentationarticles/developers-guide/sitefinity-essentials/modules/workflow-for-content-modules/custom-workflow  as a test.

    but I'm wondering if anyone has accomplished the goals i have described above? 

    thanks.


  2. Stanislav Velikov
    Stanislav Velikov avatar
    1113 posts
    Registered:
    05 Dec 2016
    19 Jul 2012
    Link to this post
    Hello,

    1) Basically to be able to do this you should use customized workflows and most probably replace our GuardActivity with your custom GuardActivity. Our guard workflow activity is generally blocking access to actions based on whether a user is in a role associated with the approves or publishers workflow groups.
    You can make your own logic there and throw WorkflowSecurityException if the conditions are not met. The downside of this approach is that our workflow menu rendering will not recognize your activity, so each user will see all the action buttons  (DecisionActivities) that are possible for the current workflow status.
    Once they click them they will receive  the warning message that they are not allowed to say Approve this page etc.

    Let's take pages for example:

    My opinion is that the easiest way to do this is to use a Role naming convention.
    For example create some roles named “WorkFlow-Courts”, “Workflow-TakingAction”, “Workflow-CurrentBusiness”.
    Assign the respective users to these roles.

    In your custom guard activity first check what the current page parent group is. If you have nested hierarchy you have to traverse the ancestors of the page node to find the group if not directly get PageNode.Parent.

    Once you have the name of the group try to get the user roles for the user and check if the user has a role for this page group.

    Please find below a sample guard activity that can be used. Replace our GuardActivity in your customized PagesApprovalWorkflow.xamlx your custom activity.

    If your are going to have one level of approval – you can use just the StandardOneLevel approval flowchart and remove the Switch and the other flowcharts.

    Please be aware that if you assign a role to a user the user first has to sign out and sign in – in order to receive this role
    public class MyGuardActivity : CodeActivity
        {
         
               protected override void Execute(CodeActivityContext context)
            {
                var property = context.DataContext.GetProperties()["workflowItem"];
                if (property != null)
                {
                    var securedItem = property.GetValue(context.DataContext);
                    var pageNode = securedItem as Pages.Model.PageNode;
                    string groupName=null;
                    while (pageNode.Parent != null)
                    {
                        var parent = pageNode.Parent;
                        if (parent.NodeType == Pages.Model.NodeType.Group) // you can add a check if it is one of your top groups
                        {
                            groupName = parent.UrlName;
                            break;
                        }
                        pageNode = pageNode.Parent;
                    }
                    if (string.IsNullOrEmpty(groupName)) return;
         
                    string neededRole = string.Concat("Workflow-", groupName);
                    var roles = SecurityManager.GetCurrentUser().GetRoles();
                    bool isInRole=false;
                    foreach(var role in roles)
                    {
                        if (role.Equals(neededRole, StringComparison.InvariantCultureIgnoreCase))
                        {
                            isInRole = true;
                            break;
                        }
                    }
                    if (!isInRole)
                    {
                       throw new WorkflowSecurityException(String.Format("User should be in role {0}", neededRole));
                    }
         
                }
         
            }

    2) Yes we have a blog post that describes the changing of the content of the email that is sent when a page or content item is sent for publishing. The blog post includes workflow files form Sitefinity 4.4 so I have attached the workflow files for Sitefinity 5 since they were updated. Also the workflow files are included in Sitefinity SDK for Sitefinity 5.


    All the best,
    Stanislav Velikov
    the Telerik team
    Do you want to have your say in the Sitefinity development roadmap? Do you want to know when a feature you requested is added or when a bug fixed? Explore the Telerik Public Issue Tracking system and vote to affect the priority of the items
  3. David
    David avatar
    16 posts
    Registered:
    10 Apr 2012
    18 Oct 2012 in reply to HRC
    Link to this post
    Were you able to get this functionality working?  If so, do you have any code samples that you could share?  I'm having the same type of issue.  I need to have authors and approvers for departments.  Let's say HR, Sales and IT.  For example, HR authors can create pages in the HR section that can only be approved by HR approvers.

    Another question I have is whether I can avoid creating a custom workflow by using the sync tool. If I create hr.mydomain.com and only allow hr users to author/approve content there, sales.mydomain.com and allow sales users to author/approve content there to be pushed to a production site that includes all sections, is this a realistic approach to handling this issue?
  4. Andrew
    Andrew avatar
    3 posts
    Registered:
    13 Sep 2012
    15 Feb 2013
    Link to this post

    We also have this exact same requirement.  We will have various blogs on our site, each maintained by a different division within our company.  For each blog, we may have different sets of approvers/publishers.  Since workflows only apply to Blogs overall (you can't tell it to apply only to one particular blog, for example), we are stuck with adding all possible approvers to our workflow, and then using blog-level permissions to only allow the particular set of approvers to actually be able to approve/publish blog posts for their division.  This works for the most part, but all of the approvers get the notification email, regardless of whether or not they actually have permission to approve that particular blog post.

    In addition, we may have the need for additional levels of approval (beyond the 2 that come out of the box).  There has even been talk within our company of allowing one approver to decide "hey, this needs to also be routed to groups A, B, and C, for further approval", and therefore essentially dynamically choosing which other approvers might also need to see the content before publishing it. 

    It seems as if customizing workflows for Sitefinity is quite difficult.  Are there future plans to make workflows within Sitefinity more robust out-of-the-box for more enterprise-level content authoring/approving needs?

    We had also thought of the possibility of setting up multiple Sitefinity instances, one for each division, and then bringing together the content via the SiteSync tool, or via RSS feeds.  Although that seems to be a bit cumbersome, especially when it comes to the need to upgrade all of those individual instances and keep them in sync.

    Any guidance for us enterprise-level customers looking for more advanced workflow scenarios would be very much appreciated!

  5. Stanislav Velikov
    Stanislav Velikov avatar
    1113 posts
    Registered:
    05 Dec 2016
    19 Feb 2013
    Link to this post
    Hi,

    At the current moments there are no plans to extend the workflows, I suppose you mean having a workflow editing tool that will allow workflow customizations to be made directly from sitefintiy backend applying changes instantly. The development team is aware of the benefits from having such feature, but for the time beeing this is not available and yet no plans are made towards this functionality.

    Editing sitefintiy workflow and substituting it with custom workflow utilizing activitiy classes developed for specific purposes requires approach one described below, following windows workflow like development fixing the moving parts in the workflow designers with custom activity classes and sitefinity professional editions (custom workflow is available for sitefinity proessional and enterprise editions).

    Sitefintiy have its own activity classes to handle the workflow in sitefintiy context and work with its data types and I think after a while in dealing with the activities and the worklfow designer one can deal with ease in implementing customizations to workflows.

    Concerning having more than 2 level approavl workflow,  the 2 level appraoval workflow when the item is passed trough second level workflow returns the item passed trough workflow with status Published, if the second level approval worklow is modified to return the item as awaiting further approval it will pass trough workflow again The implications come from the fast this is not available out of the box as in Administration->Worklfow there is no option to have third level approver.

    Regards,
    Stanislav Velikov
    the Telerik team
  6. Andrew
    Andrew avatar
    3 posts
    Registered:
    13 Sep 2012
    21 Feb 2013 in reply to Stanislav Velikov
    Link to this post

    Thanks for the suggestions.  I have spent some time really digging into how the workflows work out of the box, and I'm getting more comfortable with them.  One thing we'd really like to do is set up the workflow so that different blogs have different groups of people set up as approvers/publishers.  The approach I was thinking of going with was creating a couple new custom permissions (under Advanced Settings -> Blogs -> Permissions -> BlogPost -> Actions), which would then allow us to assign roles to these new permissions on each blog.  Then, I was planning on modifying the GuardActivity to look for these new permissions at the blog/content level, instead of or in addition to checking the permissions at the workflow level.  When digging into this, I discovered that there is already a ContentPermissions property of the GuardActivity.  I tried adding my new permission to this property (in the format "BlogPost.ApproveBlogPost"), but it doesn't seem to be working correctly.  When I used Reflector to look at the GuardActivity code, it looks like it calls off to a securedContent.IsGranted method, but it doesn't actually use the results of that method to stop the user from going forward with that operation.  And, even if it did, it looks like the code that inspects the workflow to build up the UI of allowed operations wouldn't honor this ContentPermissions property either, as it looks specifically only at the WorkflowPermissions property (this is inside the FlowchartWorkflowInspector.CanPassGuard method).

    I plan on inheriting from the GuardActivity class, and in my version, actually check the ContentPermissions and stop the user from doing something they don't have permission to do, but I can't see a way to create my own implementation of a CanPassGuard method to also check this (there isn't an interface or anything that would allow me to plug in an alternative implementation). Is there a way this can be done?  Or could Telerik implement this as a fix in a future version?

    Finally, I'm also planning on inheriting from the NotifyGroup activity so that we only send the "You have content to approve" emails to the people who actually have the permission to do so (based on my new BlogPost.ApproveBlogPost permission).  I believe that should be pretty straight forward.

    Does this approach sound reasonable?

    Oh, one more item:  I noticed that sf_workflow_scope table has a column named content_filter_expression, which sounds like it might be able to be used to filter the workflow scope down to, say, a particular blog or via some other criteria.  Is that actually implemented, and if so, what sort of syntax could we use in there?

    Thanks!

    Andy

  7. Beth
    Beth avatar
    25 posts
    Registered:
    20 Aug 2012
    13 Aug 2013
    Link to this post
    I must say this conversion is really interesting... We have been working with Workflows for the first time and I'd love to know more about what can be done with the customization of the Sitefinity workflows. Did you get any response or did you have any success with this Andy?
  8. Andrew
    Andrew avatar
    3 posts
    Registered:
    13 Sep 2012
    19 Aug 2013 in reply to Beth
    Link to this post
    We haven't heard much on this back from Telerik.  However, we've had some success going our own way with a few customizations to the workflow logic.  This is what we wanted to accomplish:

    1. Each blog or list should have its own set of people who act as authors, approvers, and publishers (we are using the 2-step workflow). 
    2. Only the approvers or publishers configured for a given blog/list should get a notification email if some content is requiring their review.
    3. If someone outside the configured set of authors, approvers, or publishers for a given blog or list attempts to author, approve, or publish content in that blog/list, they should be prevented from doing so.

    Unfortunately, the built-in workflow functionality in Sitefinity only gives you a broad way to configure a set of approvers and publishers for blogs or lists overall... with no way to have separate sets of people for each individual blog or list.  (It does allow you to have different sets of authors, however, through the use of permissions on each blog or list).

    So, we realized that if authors could be set independently on each  blog/list by using permissions, then perhaps we could create some custom permissions to do the same for approvers and publishers.  So this is what we did:

    1. We created custom "ApproveListItem", "PublishListItem", "ApproveBlogPost", and "PublishBlogPost" permissions.  This can be done via the advanced settings UI, or simply by adding some stuff to the ListsConfig.config and BlogsConfig.config files.  Here's what's inside our BlogsConfig.config file, for example:
    <permissions>
        <permission name="BlogPost">
           <actions>
               <add title="Approve Blog Post As Editor (Workflow Step 1)" type="None" name="ApproveBlogPost" />
               <add title="Publish Blog Post (Workflow Step 2)" type="None" name="PublishBlogPost" />
           </actions>
        </permission>
    </permissions>

    2. Once these custom permissions are created, it's an easy matter of using the backend UI to set up what groups and/or users have each permission, per blog or list.  We also needed to ensure that the users/groups who had the new Approve and Publish permissions also had the "Update this blog and manage its blog posts" permission. 

    3. Of course, we now needed a way to get the system to actually use these permissions somehow... this is where things got a little tricky.  What we ended up doing is creating some custom Activities to plug into the Windows Workflow Foundation (xamlx) files.  We created two classes:

       a. CustomEmailNotifyGroup, which inherits Telerik.Sitefinity.Workflows.Activities.NotifyGroup, and overrides the GetEmails function so that it only gets emails for people who have the Approve or Publish permission.  I put the code for this class at the bottom of this post.
       b. CustomWorkflowGuard, which inherits System.Activities.CodeActivity.  This activity is responsible for ensuring that the user has permission to do whatever the activity is that they are trying to do (approve or publish).  We tried inheriting from Telerik.Sitefinity.Workflow.Activities.GuardActivity, thinking that we could just change the logic inside the existing acitivity that was already being used by the workflows.  However, we ran into problems because unfortunately, that type is specifically being checked for inside Telerik.Sitefinity.Workflow.FlowchartWorkflowInspector.GetVisualDecisions, via this line of code:  if (activity.GetType() == typeof(GuardActivity))  (this is a bad, bad practice, in my opinion, in regards to customizability of the software).   So, instead, we decided to create a separate activity to do the additional checking that we required.  We inserted this activity immediately after the DecisionActivity for the various paths through the workflow.  You can find the code for this class at the bottom of this post.
    4. We enabled the 2-level workflow for the Blog and List content types.  Within the configuration of this workflow, we added the entire set of all users/groups that could act as Approvers or Publishers.

    Now, the one issue we still have with this approach is that, since we weren't able to inherit from the existing GuardActivity, the backend will give the user options to do things that they might not actually have permission to do.  However, once they try to do it, they'll get caught by our CustomWorkflowGuard and be prevented from actually accomplishing it.  Not ideal, but good enough for us.

    We really do hope that a future version of Sitefinity would introduce some more robust Workflow customization options.  It's the one major area of Sitefinity that we feel is lacking, especially for trying to use it in an enterprise environment. 

    Andy

    Example Code:
    Imports Telerik.Sitefinity.Workflow.Activities
    Imports System.Activities
    Imports Telerik.Sitefinity.Fluent.AnyContent.Implementation
    Imports Telerik.Sitefinity.Workflow.Model
    Imports Telerik.Sitefinity.Security.Model
    Imports Telerik.Sitefinity.Security
    Imports Telerik.Sitefinity.Abstractions
    Imports Telerik.Sitefinity.Security.Data
    Imports Telerik.Sitefinity.SitefinityExceptions
    Imports Telerik.Sitefinity.Blogs.Model
    Imports System.ComponentModel
     
    Public Class CustomEmailNotifyGroup
        Inherits NotifyGroup
     
        ''' <summary>
        ''' Custom method that creates the notification message (e-mail) that is sent to approvers/publishers.  This allows for custom e-mails to be sent.
        ''' </summary>
        ''' <param name="context"></param>
        ''' <remarks></remarks>
        Protected Overrides Sub Execute(context As System.Activities.CodeActivityContext)
     
            'Get the item requiring approval
            Dim dataContext As WorkflowDataContext = context.DataContext
            Dim masterFluent As AnyDraftFacade = DirectCast(dataContext.GetProperties()("masterFluent").GetValue(dataContext), AnyDraftFacade)
            Dim item As Telerik.Sitefinity.GenericContent.Model.Content = masterFluent.Get()
     
            'Reset the e-mail text
            MyBase.EmailText = getEmailText(item)
     
            'This sends the message
            MyBase.Execute(context)
     
        End Sub
     
     
        ''' <summary>
        ''' Gets the emails that are defined on the permissions for each content type of blog post, list item, event, image, and document.  Returns empty list if item being approved is of any other type.
        ''' </summary>
        ''' <param name="context"></param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Overrides Function GetEmails(context As System.Activities.CodeActivityContext) As List(Of String)
     
            Dim dataContext As WorkflowDataContext = context.DataContext
            Dim hashSet As System.Collections.Generic.HashSet(Of String) = New System.Collections.Generic.HashSet(Of String)()
            Dim workflowDefinition As WorkflowDefinition = CType(dataContext.GetProperties()("workflowDefinition").GetValue(dataContext), WorkflowDefinition)
            Dim groupName As String = Me.Group
            Dim userManager As UserManager = userManager.GetManager
     
     
            'get the item going through approval
            Dim masterFluent As AnyDraftFacade = DirectCast(dataContext.GetProperties()("masterFluent").GetValue(dataContext), AnyDraftFacade)
            Dim item As Telerik.Sitefinity.GenericContent.Model.Content = masterFluent.Get()
            Dim securedContent As ISecuredObject = TryCast(item, ISecuredObject)
     
            'check if item is a valid type -- if it isn't, return no emails as it is probably something like creating a new blog, which should be a developer action
            If (Not (TypeOf item Is BlogPost Or _
                     TypeOf item Is Telerik.Sitefinity.Lists.Model.ListItem Or _
                     TypeOf item Is Telerik.Sitefinity.Events.Model.Event Or _
                     TypeOf item Is Telerik.Sitefinity.Libraries.Model.Image Or _
                     TypeOf item Is Telerik.Sitefinity.Libraries.Model.Document)) Then
                Return New List(Of String)
            End If
     
     
            'get all permissions on the item
            Dim perms As IQueryable(Of Permission) = securedContent.GetActivePermissions()
     
            'look up all principals on the item - think of these as the actual roles/users that have the valid permissions
            Dim principals As List(Of Guid) = Me.getPrincipalsFromPermissionsForGroup(groupName, perms)
     
            'loop through each role
            For Each principle As Guid In principals
                'try catch in case returned principal type is for a user as opposed to a role -- not likely to happen
     
                Dim providers As ProvidersCollection(Of RoleDataProvider) = RoleManager.GetManager().Providers
                For Each provider As RoleDataProvider In providers
     
     
                    Dim manager As RoleManager = RoleManager.GetManager(provider.Name)
     
                    Try
     
                        'loop through users in that role
                        Dim usersInRole As System.Collections.Generic.IList(Of User) = manager.GetUsersInRole(principle)
                        For Each user As User In usersInRole
                            If user IsNot Nothing AndAlso Not String.IsNullOrEmpty(user.Email) AndAlso Not hashSet.Contains(user.Email) Then
                                hashSet.Add(user.Email)
                            End If
                        Next
                    Catch e As ItemNotFoundException
     
                        'try to find the user that is being referenced by the principle in case the principle references a direct user
                        Dim user As User = userManager.GetUser(principle)
     
                        If user IsNot Nothing Then
                            hashSet.Add(user.Email)
                        End If
     
                    End Try
                Next
            Next
     
            'return all the correct emails
            Return hashSet.ToList()
     
        End Function
     
        ''' <summary>
        ''' Creates a string that is used as text in an email
        ''' </summary>
        ''' <param name="item"></param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Private Function getEmailText(ByVal item As Telerik.Sitefinity.GenericContent.Model.Content) As String
           ' Implementation removed from this sample...
           Return "Some Text"
        End Function
     
        Private Function getPrincipalsFromPermissionsForGroup(ByVal group As String, ByVal perms As IQueryable(Of Permission)) As List(Of Guid)
     
            'Return perms.Where(Function(p) p.SetName = "BlogPost" AndAlso p.IsGranted(New String() {"ApproveBlogPostAsEditor"})).Select(Function(p) p.PrincipalId).ToList()
     
            'TODO bad hard coding, move to config
     
            'if looking at the approve group, pull those roles.  otherwise pull the publish roles
            If (group.Equals("Approve")) Then
                Return perms.Where(Function(p) (p.SetName = "BlogPost" AndAlso p.IsGranted(New String() {"ApproveBlogPost"})) _
                                       OrElse (p.SetName = "ListItem" AndAlso p.IsGranted(New String() {"ApproveListItem"})) _
                                       OrElse (p.SetName = "General" AndAlso p.IsGranted(New String() {"ApproveEvent"})) _
                                       OrElse (p.SetName = "Image" AndAlso p.IsGranted(New String() {"ApproveImage"})) _
                                       OrElse (p.SetName = "Document" AndAlso p.IsGranted(New String() {"ApproveDocument"})) _
                ).Select(Function(p) p.PrincipalId).ToList()
     
            ElseIf (group.Equals("Publish")) Then
                Return perms.Where(Function(p) (p.SetName = "BlogPost" AndAlso p.IsGranted(New String() {"PublishBlogPost"})) _
                                       OrElse (p.SetName = "ListItem" AndAlso p.IsGranted(New String() {"PublishListItem"})) _
                                       OrElse (p.SetName = "General" AndAlso p.IsGranted(New String() {"PublishEvent"})) _
                                       OrElse (p.SetName = "Image" AndAlso p.IsGranted(New String() {"PublishImage"})) _
                                       OrElse (p.SetName = "Document" AndAlso p.IsGranted(New String() {"PublishDocument"})) _
                ).Select(Function(p) p.PrincipalId).ToList()
            Else
                Throw New ArgumentException("Invalid group name passed to custom email notify group in workflow")
            End If
        End Function
     

    End Class

    Imports System.ComponentModel
    Imports Telerik.Sitefinity.Security.Model
    Imports Telerik.Sitefinity.Workflow.Exceptions
    Imports Telerik.Sitefinity.Security
     
    Public Class CustomWorkflowGuard
        Inherits System.Activities.CodeActivity
     
     
        Public Overridable Property ContentPermissions As String
     
        Protected Overrides Sub Execute(context As System.Activities.CodeActivityContext)
     
            'Now, ensure that the user has the appropriate content-level permission to approve/publish
     
            If Not String.IsNullOrEmpty(Me.ContentPermissions) Then
                Dim descriptor As PropertyDescriptor = context.DataContext.GetProperties.Item("workflowItem")
                If (Not descriptor Is Nothing) Then
                    Dim securedContent As ISecuredObject = TryCast(descriptor.GetValue(context.DataContext), ISecuredObject)
                    If (Not securedContent Is Nothing) Then
                        If Not Me.CheckCustomContentPermissions(securedContent) Then
                            Throw New WorkflowSecurityException("User doesn't have one of the following permissions and is denied access: " + Me.ContentPermissions)
                        End If
                    End If
                End If
            End If
     
        End Sub
     
     
        Protected Overridable Function CheckCustomContentPermissions(securedContent As ISecuredObject) As Boolean
            Dim permissionsToCheck = Me.ContentPermissions.Split(New Char() {","c})
     
            For Each perm In permissionsToCheck
                Dim permSplit = perm.Split(New Char() {"."c})
                Dim permissionSet = permSplit(0)
                Dim actionName = permSplit(1)
                If securedContent.SupportedPermissionSets.Contains(permissionSet) Then
                    If securedContent.IsGranted(permissionSet, New String() {actionName}) Then
                        Return True
                    End If
                End If
     
            Next
     
            Return False
     
        End Function
    End Class
     
    Public Class MustHaveApproveContentPermission
        Inherits CustomWorkflowGuard
     
        Public Overrides Property ContentPermissions As String
            Get
                Return "BlogPost.ApproveBlogPost,ListItem.ApproveListItem,General.ApproveEvent,Image.ApproveImage,Document.ApproveDocument"
            End Get
            Set(value As String)
                MyBase.ContentPermissions = value
            End Set
        End Property
     
    End Class
     
    Public Class MustHavePublishContentPermission
        Inherits CustomWorkflowGuard
     
        Public Overrides Property ContentPermissions As String
            Get
                Return "BlogPost.PublishBlogPost,ListItem.PublishListItem,General.PublishEvent,Image.PublishImage,Document.PublishDocument"
            End Get
            Set(value As String)
                MyBase.ContentPermissions = value
            End Set
        End Property
     
    End Class
  9. Gary
    Gary avatar
    3 posts
    Registered:
    11 Sep 2013
    26 Mar 2014 in reply to Andrew
    Link to this post

    Hello Andrew,

     Thank you very much for taking the time to explain and include the code that you used for this. I have a similar challenge, and the information you provided is really helpful.

     Regards,

    Gary

9 posts, 0 answered