KennyG
Aug 13, 2023
  747
(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
Copy Optimizely SaaS CMS Settings to ENV Format Via Bookmarklet

Do you work with multiple Optimizely SaaS CMS instances? Use a bookmarklet to automatically copy them to your clipboard, ready to paste into your e...

Daniel Isaacs | Dec 22, 2024 | Syndicated blog

Increase timeout for long running SQL queries using SQL addon

Learn how to increase the timeout for long running SQL queries using the SQL addon.

Tomas Hensrud Gulla | Dec 20, 2024 | Syndicated blog

Overriding the help text for the Name property in Optimizely CMS

I recently received a question about how to override the Help text for the built-in Name property in Optimizely CMS, so I decided to document my...

Tomas Hensrud Gulla | Dec 20, 2024 | Syndicated blog

Resize Images on the Fly with Optimizely DXP's New CDN Feature

With the latest release, you can now resize images on demand using the Content Delivery Network (CDN). This means no more storing multiple versions...

Satata Satez | Dec 19, 2024