November Happy Hour will be moved to Thursday December 5th.

KennyG
Aug 13, 2023
  721
(4 votes)

Programmatically Exempt A PageType From Content Approval

We've got a use case that I think is a little advanced for what you get with Content Approval out of the box. We're a home builder and our commerce catalog is structured as Brand > State > City > Community > Lots (homes) and Plans.

While we want to require content approval at the community level (which numbers in the hundreds) we might not want to require approval at the lot and plan level (which numbers in the thousands). So if you set up Content Approval at the State level it is going to inherit all of the way down. We want to break it at certain points programmatically.

I've tested this out in a fresh Alloy site. Enable Content Approval at the About Us node and it will apply to the entire branch.

Let's exempt the NewsPage page type from Content Approval.

You can see that it gets its definition from the parent node.

We create an Initialization Module so we can hook into the the SavingContent event. 

        public void Initialize(InitializationEngine context)
        {
            _contentEvents = context.Locate.Advanced.GetInstance<IContentEvents>();
            _approvalDefinitionRepository = context.Locate.Advanced.GetInstance<IApprovalDefinitionRepository>();
            _contentRepository = context.Locate.Advanced.GetInstance<IContentRepository>();

            _contentEvents.SavingContent += ContentEvents_SavingContent_DisableContentApproval;
        }

        private async void ContentEvents_SavingContent_DisableContentApproval(object sender, ContentEventArgs e)
        {
            if (e.Content is NewsPage)
            {
                // Gets the latest version of a definition by resolving a ContentReference.  
                var definitionResolveResult = await _approvalDefinitionRepository.ResolveAsync(e.ContentLink);

                // The Resolve-method returns a result with a definition and a flag specifying if the definition was found on an ancestor
                var definition = definitionResolveResult.Definition as ContentApprovalDefinition;
                var isInherited = definitionResolveResult.IsInherited;

                if (definition != null && definition.IsEnabled)
                {
                    CreateOrUpdateDefinition(definition, e.ContentLink, false, isInherited);
                }
            }
        }

For any pagetype that is a NewsPage we use the ContentReference to get the definition (ApprovalDefinitionResolveResult) that applies to that specific node. ApprovalDefinitionResolveResult is an object that contains the Definition (ApprovalDefinition) and an IsInherited flag (bool) that signifies whether the definition comes from this node or is inherited from an ancestor.

    public class ApprovalDefinitionResolveResult
    {
        //
        // Summary:
        //     The resolved approval definition or null is no is found
        public ApprovalDefinition Definition { get; set; }

        //
        // Summary:
        //     Specifies if the definition was inherited from a content ancestor
        public bool IsInherited { get; set; }
    }

 We cast this definition as a ContentApprovalDefinition. We check to make sure the definition isn't null and that it is enabled (active). Then we pass it to a CreateOrUpdateDefinition method. When using this method if a definition does not exist it creates one, otherwise it updates it.

        private void CreateOrUpdateDefinition(ContentApprovalDefinition definition, ContentReference contentLink, bool isEnabled, bool isInherited)
        {
            if (definition == null)
            {
                CreateContentDefinition(contentLink, isEnabled);
                return;
            }

            //first pass definition down to any children to break inheritance
            var children = _contentRepository.GetChildren<PageData>(contentLink);

            if (children != null && children.Any())
            {

                foreach (var child in children)
                {
                    var definitionResolveResult = _approvalDefinitionRepository.ResolveAsync(child.ContentLink).Result;

                    if (definitionResolveResult.IsInherited)
                    {
                        UpdateDefinition(definitionResolveResult.Definition as ContentApprovalDefinition, child.ContentLink, definitionResolveResult.Definition.IsEnabled);
                    }
                }
            }

            //update page 
            UpdateDefinition(definition, contentLink, isEnabled);

        }

We check to see if the current node has any children, if so (and they are of a non-exempt page type) we need to pass the definition down to each one to break the inheritance. We loop through the children and update the definition for each one. Now that each child is now the root of its own approval branch we can update the node itself and turn off inheritance for that node by using the UpdateDefinition method.

        private void UpdateDefinition(ContentApprovalDefinition definition, ContentReference contentLink, bool isEnabled)
        {
            var newDefinition = definition.CreateWritableClone() as ContentApprovalDefinition;
            newDefinition.IsEnabled = isEnabled;
            newDefinition.ContentLink = contentLink;

            _approvalDefinitionRepository.SaveAsync(newDefinition).ConfigureAwait(false);
        }

We clone the definition that has been passed in (this definition comes from the ancestor), set it as enabled, give it the current node link, and save it. The method for creating a definition is very similar.

        private void CreateContentDefinition(ContentReference contentLink, bool isEnabled)
        {
            var newDefinition = new ContentApprovalDefinition
            {
                ContentLink = contentLink,
                IsEnabled = isEnabled
            };

            _approvalDefinitionRepository.SaveAsync(newDefinition).ConfigureAwait(false);
        }

Now you may be saying to yourself, none of this comes into effect until the page is saved. That's what is nice about this approach, you don't have to loop through the entire tree to apply it. It kicks in for any node of the exempt type when an editor starts editing and applies during the first AutoSave. 

Here is the entire Init Module:

using EPiServer.Approvals.ContentApprovals;
using EPiServer.Approvals;
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.ServiceLocation;
using EPiServer.Web;
using Opti_Alloy.Models.Pages;

namespace Opti_Alloy.Business.Initialization
{
    [ModuleDependency(typeof(InitializationModule))]
    public class ExemptPageTypesInitialization : IInitializableModule
    {
        private IContentEvents _contentEvents;
        private IApprovalDefinitionRepository _approvalDefinitionRepository;
        private IContentRepository _contentRepository;

        public void Initialize(InitializationEngine context)
        {
            _contentEvents = context.Locate.Advanced.GetInstance<IContentEvents>();
            _approvalDefinitionRepository = context.Locate.Advanced.GetInstance<IApprovalDefinitionRepository>();
            _contentRepository = context.Locate.Advanced.GetInstance<IContentRepository>();

            _contentEvents.SavingContent += ContentEvents_SavingContent_DisableContentApproval;
        }

        private async void ContentEvents_SavingContent_DisableContentApproval(object sender, ContentEventArgs e)
        {
            if (e.Content is NewsPage)
            {
                // Gets the latest version of a definition by resolving a ContentReference.  
                var definitionResolveResult = await _approvalDefinitionRepository.ResolveAsync(e.ContentLink);

                // The Resolve-method returns a result with a definition and a flag specifying if the definition was found on an ancestor
                var definition = definitionResolveResult.Definition as ContentApprovalDefinition;
                var isInherited = definitionResolveResult.IsInherited;

                if (definition != null && definition.IsEnabled)
                {
                    CreateOrUpdateDefinition(definition, e.ContentLink, false, isInherited);
                }
            }
        }

        private void CreateOrUpdateDefinition(ContentApprovalDefinition definition, ContentReference contentLink, bool isEnabled, bool isInherited)
        {
            if (definition == null)
            {
                CreateContentDefinition(contentLink, isEnabled);
                return;
            }

            //first pass definition down to any children to break inheritance
            var children = _contentRepository.GetChildren<PageData>(contentLink);

            if (children != null && children.Any())
            {

                foreach (var child in children)
                {
                    var definitionResolveResult = _approvalDefinitionRepository.ResolveAsync(child.ContentLink).Result;

                    if (definitionResolveResult.IsInherited)
                    {
                        UpdateDefinition(definitionResolveResult.Definition as ContentApprovalDefinition, child.ContentLink, definitionResolveResult.Definition.IsEnabled);
                    }
                }
            }

            //update page 
            UpdateDefinition(definition, contentLink, isEnabled);

        }

        private void UpdateDefinition(ContentApprovalDefinition definition, ContentReference contentLink, bool isEnabled)
        {
            var newDefinition = definition.CreateWritableClone() as ContentApprovalDefinition;
            newDefinition.IsEnabled = isEnabled;
            newDefinition.ContentLink = contentLink;

            _approvalDefinitionRepository.SaveAsync(newDefinition).ConfigureAwait(false);
        }

        private void CreateContentDefinition(ContentReference contentLink, bool isEnabled)
        {
            var newDefinition = new ContentApprovalDefinition
            {
                ContentLink = contentLink,
                IsEnabled = isEnabled
            };

            _approvalDefinitionRepository.SaveAsync(newDefinition).ConfigureAwait(false);
        }

        public void Uninitialize(InitializationEngine context)
        {
            _contentEvents.SavingContent -= ContentEvents_SavingContent_DisableContentApproval;
        }
    }
}

Going back to our Alloy example, if we edit something on the Events page (NewsPage) we can see that it is ready for direct publish, because the definitions were updated as soon as it Autosaved.

Checking the Content Approval settings, we see that the process is disabled at this node.

And then checking that node's children, we see that each of them is now the root of a new approval sequence copied from the ancestor.

There may be other approaches but I like that this one only kicks in when needed and can be done at any point in the branch. My initial attempt only worked for the end nodes (leaves) but this approach is much more flexible since it copies the definitions down to the children before updating the current node.

Let me know what you think in the comments!

Aug 13, 2023

Comments

Please login to comment.
Latest blogs
Set Default Culture in Optimizely CMS 12

Take control over culture-specific operations like date and time formatting.

Tomas Hensrud Gulla | Nov 15, 2024 | Syndicated blog

I'm running Optimizely CMS on .NET 9!

It works 🎉

Tomas Hensrud Gulla | Nov 12, 2024 | Syndicated blog

Recraft's image generation with AI-Assistant for Optimizely

Recraft V3 model is outperforming all other models in the image generation space and we are happy to share: Recraft's new model is now available fo...

Luc Gosso (MVP) | Nov 8, 2024 | Syndicated blog

ExcludeDeleted(): Prevent Trashed Content from Appearing in Search Results

Introduction In Optimizely CMS, content that is moved to the trash can still appear in search results if it’s not explicitly excluded using the...

Ashish Rasal | Nov 7, 2024

CMS + CMP + Graph integration

We have just released a new package https://nuget.optimizely.com/package/?id=EPiServer.Cms.WelcomeIntegration.Graph which changes the way CMS fetch...

Bartosz Sekula | Nov 5, 2024

Block type selection doesn't work

Imagine you're trying to create a new block in a specific content area. You click the "Create" link, expecting to see a CMS modal with a list of...

Damian Smutek | Nov 4, 2024 | Syndicated blog