Content approvals
Describes developer concepts for reviewing and approving content.
Approvals are a core system for the process of reviewing and approving. Specific approval types are built on top of this. The most common case is content approvals used in the editorial UI to review and approve content items before publishing, which is also the main reason the approval system exists. Custom approval types similar to how content approvals are defined are possible, but this is currently not supported and is not covered here.
NoteTo create content approvals in Optimizely CMS 13 UI, see Approve content.
Create approval
Create an approval when something needs review (for example, a content item before publishing). Connect approvals to a definition, and create multiple approvals for each definition.
Running an approval is the process of stepping through the definition, where each step's reviewers decide whether to approve or reject. Steps must be approved for the whole approval process. If a step is rejected, then the whole approval is rejected.
A URI-based reference identifies an approval, and it must have a definition.
A started approval is locked to the definition version that was the current version when the approval was started. It does not switch to a new definition version if the definition is changed, but new approvals use the new version.
IApprovalRepository is the base interface for saving, deleting, and listing approvals and their decisions. IApprovalEngine is built on top of IApprovalRepository to streamline the handling of approvals. Use the repository for listing approvals and use the engine for handling approvals. The engine also raises events that are available in IApprovalEngineEvents.
Reviewers
Reviewers validate content changes, and the four-eyes principle prevents users from approving their own edits.
Implement the four-eyes principle and prevent users from approving their changes when configuring an approval sequence. The default value is false, letting users approve their changes.
When a user approves or rejects a content item, a validation is made to see if the user is part of the step, either as a user or as a member of a role. Call SecurityEntityProvider to validate if a user is part of a role. This calls the underlying user or role provider configured for the site (AspNet Identity Provider).
NotesIt is only the role name that is part of the definition, not the users in the role. The validation to see if a user is part of a role is made at the moment it is needed. This means that a user is added to a role or removed from one, and that affects an already started approval.
The underlying user and role providers have their own restrictions. For example, the user must log in for the roles to update.
Approval flow
The normal state flow for a content version that is being approved is as follows:
- CheckedOut – The editor has created a version that is being edited.
- AwaitingApproval – The editor is done and marks the version ready for approval.
- Rejected – If a reviewer does not approve the version, it transitions to Rejected. After the editor has made changes, it is set for approval again.
- CheckedIn – The version is approved and is ready for publishing.
- Published – The version is published.
Hook up events
[InitializableModule]
public class ApprovalLogger: IInitializableModule {
private IApprovalEngineEvents _approvalEngineEvents;
public void Initialize(InitializationEngine context) {
_approvalEngineEvents = context.Services.GetRequiredService<IApprovalEngineEvents>();
_approvalEngineEvents.Approved += OnApproved;
}
public void Uninitialize(InitializationEngine context) => _approvalEngineEvents.Approved -= OnApproved;
private void OnApproved(ApprovalEventArgs e) => LogManager.GetLogger(typeof (ApprovalLogger)).Debug("Approve");
}Start an approval
Content approval is not started by saving an approval, but by saving a content item with SaveAction.RequestApproval. This automatically creates and saves a ContentApproval for this content item, if a definition is resolved.
IContent content;
IContentRepository contentRepository;
var approvalContentLink = contentRepository.Save(content, SaveAction.RequestApproval);Abort an approval
This deletes an approval and raises an aborted event:
Approval approval;
IApprovalEngine approvalEngine;
await approvalEngine.AbortAsync(approval.ID, "user");Make a decision
Use the engine to decide whether to approve or reject a step or the whole approval. Add an optional comment to explain the reason for the decision. If the decision finishes the approval (approves the last step or rejects a step), this comment is saved on the approval as CompletedComment.
Approval approval;
IApprovalEngine approvalEngine;
// Approve a step if user is part of the current definition step
await approvalEngine.ApproveAsync(approval.ID, "user", 1, ApprovalDecisionScope.Step);
// Reject a step if user is part of the current definition step, adding a comment
await approvalEngine.RejectAsync(approval.ID, "user", 1, ApprovalDecisionScope.Step, "This is why I did it");
// Force approve a step whether the user is part of the current definition step or not
await approvalEngine.ApproveAsync(approval.ID, "user", 1, ApprovalDecisionScope.ForceStep);
// Force approve the whole approval
await approvalEngine.ApproveAsync(approval.ID, "user", 1, ApprovalDecisionScope.Force);There are also several more explicitly named extension methods to the engine, for example, ApproveStepAsync and ForceApproveAsync.
List approvals
Use the GetAsync or GetItemsAsync methods on IApprovalRepository to get specific approvals using ID or ContentReference:
using EPiServer.Approvals.ContentApprovals;
ContentReference contentLink;
IApprovalRepository approvalRepository;
var approval = await approvalRepository.GetAsync(contentLink);The ListAsync method takes an ApprovalQuery or ContentApprovalQuery object which searches the approvals based on the specified query data and returns a filtered list, which is optionally paged.
This example gets approvals in review for a user based on a specific definition.
using EPiServer.Approvals.ContentApprovals;
ContentApprovalDefinition definition;
IApprovalRepository approvalRepository;
var approvals = await approvalRepository.ListAsync(new ContentApprovalQuery {
Username = "user",
Status = ApprovalStatus.InReview,
DefinitionID = definition.ID
});This example lists decisions made for an approval:
Approval approval;
IApprovalRepository approvalRepository;
var decisions = await approvalRepository.ListDecisionsAsync(approval.ID);Create an approval definition
Approval definitions establish the sequence of review steps required before content is published.
An approval definition is a series of steps to be approved in sequence. Each step has one or more reviewers. One reviewer in each step must approve the content before it is moved to the next step in the definition. If the site is multi-language, set the definition so that reviewers approve different languages in a step.
Definitions are mostly static. Rarely create and change them.
A definition has a URI-based reference to an object, usually a content item. Use a saved definition with a reference for that reference and other references, such as a content item and its descendants. The resolve method searches for a definition and checks if a reference has access to a definition. In the case of content approvals, that means traversing the content tree upwards and looking for a definition.
Each time a definition is saved, a definition version is created. The latest saved version is the current definition and also the version returned when asking for a definition through IApprovalDefinitionRepository. It is also possible to get a specific version through the IApprovalDefinitionVersionRepository.
This example shows the creation and saving of a definition:
using System.Globalization;
using System.Linq;
using EPiServer.Approvals.ContentApprovals;
ContentReference contentLink;
IApprovalDefinitionRepository definitionRepository;
var langEN = new CultureInfo[] {
CultureInfo.GetCultureInfo("en")
};
var langSV = new CultureInfo[] {
CultureInfo.GetCultureInfo("sv")
};
// Creates a content approval definition
var definition = new ContentApprovalDefinition {
ContentLink = contentLink,
Steps = new List<ApprovalDefinitionStep> {
new ApprovalDefinitionStep("step1", new ApprovalDefinitionReviewer[] {
new ApprovalDefinitionReviewer("user1a", langEN),
new ApprovalDefinitionReviewer("user1b", langSV),
}),
new ApprovalDefinitionStep("step2", new ApprovalDefinitionReviewer[] {
new ApprovalDefinitionReviewer("user2a", langEN.Union(langSV))
})
}
};
// Saves the definition
await definitionRepository.SaveAsync(definition);Update an approval definition
In this example, a definition is updated with a reviewer in the second step. This reviewer approves items in languages using CultureInfo.InvariantCulture.
using EPiServer.Approvals.ContentApprovals;
ContentReference contentLink;
IApprovalDefinitionRepository definitionRepository;
var langInvariant = new CultureInfo[] {
CultureInfo.InvariantCulture
};
// Gets a definition
ApprovalDefinition definition = await definitionRepository.GetAsync(contentLink);
definition = definition.CreateWritableClone();
definition.Steps[1].Reviewers.Add(new ApprovalDefinitionReviewer("user2b", langInvariant));
// Saves a definition
await definitionRepository.SaveAsync(definition);Add a role to an approval definition
In this example, a definition is updated with a role reviewer in the first step. This role approves items in the Swedish language.
using EPiServer.Approvals.ContentApprovals;
ContentReference contentLink;
IApprovalDefinitionRepository definitionRepository;
var langSV = new CultureInfo[] {
CultureInfo.GetCultureInfo("sv")
};
// Gets a definition
ApprovalDefinition definition = await definitionRepository.GetAsync(contentLink);
definition = definition.CreateWritableClone();
definition.Steps[0].Reviewers.Add(new ApprovalDefinitionReviewer("managers", langSV, ApprovalDefinitionReviewerType.Role));
// Saves a definition
await definitionRepository.SaveAsync(definition);Delete an approval definition
This example deletes a definition using ID:
ApprovalDefinition definition;
IApprovalDefinitionRepository definitionRepository;
// Deletes a definition
await definitionRepository.DeleteAsync(definition.ID);
NoteDeleting a definition is not possible if running approvals exist. All approval instances must be completed or aborted before deleting an approval definition.
Get an approval definition
Gets a definition in a couple of different ways:
using EPiServer.Approvals.ContentApprovals;
ContentReference contentLink;
ApprovalDefinition definition;
Approval approval;
IApprovalDefinitionRepository definitionRepository;
IApprovalDefinitionVersionRepository definitionVersionRepository;
// Gets the latest version of a definition using a definition id.
var definition1 = await definitionRepository.GetAsync(definition.ID);
// Gets a specific version of a definition using a version id.
var definition2 = await definitionVersionRepository.GetAsync(approval.VersionID);
// Gets the latest version of a definition using a ContentReference.
var definition3 = await definitionRepository.GetAsync(contentLink);
// Gets the latest version of a definition by resolving a ContentReference.
var definitionResolveResult = await definitionRepository.ResolveAsync(contentLink);
// The Resolve-method returns a result with a definition and a flag specifying if the definition was found on an ancestor
var definition4 = definitionResolveResult.Definition as ContentApprovalDefinition;
var isInherited = definitionResolveResult.IsInherited;Content approvals namespace
The content approvals namespace provides the API surface for managing approval definitions and approval instances.
Access the content approvals feature through the EPiServer.Approvals.ContentApprovals namespace, which gives access to content versions of the classes ApprovalDefinition and Approval, and also extension methods with ContentReferences in the repository interfaces. The ContentReference is converted to the URI reference under the hood.
NoteOnly use the methods in this namespace for content approvals. If another approval system for content is used that also wants to identify by
ContentReference, it should have its own set of extension methods in its own namespace.
Create a definition on a content item in a content tree that is used by the descendants of that content item. Use ResolveAsync to find a definition.
NoteA content definition is shared for all versions and languages of a content item. Content approvals are created for a specific content version and language. That means it is possible to have multiple approvals for a content item running at the same time if they have different languages.
A content approval is started by setting the status of a content item to AwaitingApproval. The referenced content item is transitioned to status CheckedIn when content approval is approved. If rejected, the content item is transitioned to status Rejected.
Email notifications
Email notifications keep reviewers informed about pending approvals and approval decisions.
Configure emails for content approval notifications to send immediately or periodically as a batch.
NoteConfigure the SMTP settings for CMS 13 to send out email notifications. See Configure email server.
The behavior of content approval notification emails is to send users emails periodically. This means that notifications are batched into a single email sent regularly.
Configure content approval notification emails to send immediately or periodically to users.
Because the content approval notification emails are sent periodically by default, only configure this when that is not the desired behavior. To configure emails to send immediately, configure the ApprovalNotificationOptions accordingly. Do this during application initialization. Configuring the dispatch interval for emails is handled in the scheduled job.
The following example shows how to configure the options during application initialization. A module dependency on FrameworkInitialization is required to ensure correct execution order.
[InitializableModule]
[ModuleDependency(typeof (FrameworkInitialization))]
public class ApprovalNotificationInitialization: IConfigurableModule {
public void ConfigureContainer(ServiceConfigurationContext context) {
context.Services.Configure<ApprovalNotificationOptions>(options => options.Immediate = true);
}
public void Initialize(InitializationEngine context) {
// Required by the module interface; no additional initialization needed here.
}
public void Uninitialize(InitializationEngine context) {
// Required by the module interface; no cleanup is needed in this sample.
}
}A scheduled job, Notification Dispatcher, dispatches notification emails at regular intervals. If content approval notification emails are configured to send periodically, then the configuration of the scheduled job determines the interval for email dispatches.
The job is enabled by default and set to run every 30 minutes.
Updated 18 days ago
