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!
Comments