Try our conversational search powered by Generative AI!

KennyG
Aug 13, 2023
  558
(3 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
From Procrastination to Proficiency: Navigating Your Journey to Web Experimentation Certification

Hey there, Optimizely enthusiasts!   Join me in celebrating a milestone – I'm officially a certified web experimentation expert! It's an exhilarati...

Silvio Pacitto | May 17, 2024

GPT-4o Now Available for Optimizely via the AI-Assistant plugin!

I am excited to announce that GPT-4o is now available for Optimizely users through the Epicweb AI-Assistant integration. This means you can leverag...

Luc Gosso (MVP) | May 17, 2024 | Syndicated blog

The downside of being too fast

Today when I was tracking down some changes, I came across this commit comment Who wrote this? Me, almost 5 years ago. I did have a chuckle in my...

Quan Mai | May 17, 2024 | Syndicated blog

Optimizely Forms: Safeguarding Your Data

With the rise of cyber threats and privacy concerns, safeguarding sensitive information has become a top priority for businesses across all...

K Khan | May 16, 2024

The Experimentation Process

This blog is part of the series -   Unlocking the Power of Experimentation: A Marketer's Insight. Welcome back, to another insightful journey into...

Holly Quilter | May 16, 2024

Azure AI Language – Sentiment Analysis in Optimizely CMS

In the following article, I showcase how sentiment analysis, which is part of the Azure AI Language service, can be used to detect the sentiment of...

Anil Patel | May 15, 2024 | Syndicated blog