Custom Workflow for content silos

Posted by Community Admin on 04-Aug-2018 21:27

Custom Workflow for content silos

All Replies

Posted by Community Admin on 16-Jul-2012 00:00

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.


Posted by Community Admin on 19-Jul-2012 00:00

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

Posted by Community Admin on 18-Oct-2012 00:00

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?

Posted by Community Admin on 15-Feb-2013 00:00

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!

Posted by Community Admin on 19-Feb-2013 00:00

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

Posted by Community Admin on 21-Feb-2013 00:00

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

Posted by Community Admin on 13-Aug-2013 00:00

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?

Posted by Community Admin on 19-Aug-2013 00:00

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

Posted by Community Admin on 26-Mar-2014 00:00

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

This thread is closed