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

Benjamin Schaefer
Mar 17, 2017
  4071
(6 votes)

Implementing Membership Moderation using Episerver Social

 Summary:

The goal of this tutorial is to outline how to implement moderation for group membership admission. The tutorial describes an interconnected relationship between four social services: groups, members, workflows and workflow items.

Prerequisites

A working Episerver SocialAlloy site. This repo can be pulled from the Episerver Github page.

Example of Membership Moderation

Image pic1.png

The Silver Reseller GroupAdmissionBlock on the Silver Reseller CommunityPage in the SocialAlloy starter site has an existing workflow associated with it. All users who request to join this group are entered into membership moderation.

Image pic2.png

A user has been entered into the workflow for the Silver Resellers Group. This group has an existing workflow associated that has an initial state of “Pending.” The two actions that can be taken on that state that are “Accept” and “Ignore”.

Image pic3.png

If the “Accept” action is taken, the member admission entity enters into a new state called “Accepted”. The available actions from this state are “Approve” and “Reject”.

 Image pic4.png

If the “Approve” action is taken, the member enters into a new state called “Approved”. No additional actions can be taken once this state is reached. This is one potential end to the community membership workflow. If a moderator had taken either the “Ignore” or “Reject” actions, the outcome to the member request would be “Rejected”.

Before Creating a Moderation Strategy

It is important to clearly identify the business process that you are looking to translate into a workflow. The Social Framework provides methods to create and interact with a workflow. However, the relevant entities of a workflow can differ greatly between use cases.

For this tutorial, the business process that has been outlined is the moderation of group membership. Within the SocialAlloy site, distinct constructs have been created to abstract the site specific models and logic from those of the Social Framework. Communities encapsulate the relevant information about a group. Communities allow for the membership of CommunityMembers (a class used to convey the details of a member).  Communities that desire a way to moderate the admission of CommunityMembers require the support of a CommunityMembershipWorkflow. The CommunityMembershipWorkflow model defines the essential components for CommityMembership moderation within the SocialAlloy site.

Managing the business specific components with a clearly-defined process in advance of implementing the Social Framework will result in an uneventful and enjoyable development experience.

Building a workflow

The SocialAlloy starter site is seeded with existing groups. These groups have corresponding workflows that allow for membership requests into the group to be moderated. An example of method used to add a workflow can be seen below:

/// <summary>
        /// Adds a workflow to the underlying repository
        /// </summary>
        /// <param name="community">The community that will be associated with the workflow</param>
        public void AddWorkflow(Community community)
        {
            // Define the transitions for workflow:            
            // Pending -> (Accept) -> Accepted
            //     |                      |-- (Approve) -> Approved
            //     |                       `- (Reject)  -> Rejected
            //      `---> (Ignore) -> Rejected

            var workflowTransitions = new List<WorkflowTransition>
            {
                new WorkflowTransition(new WorkflowState("Pending"),  new WorkflowState("Accepted"), new WorkflowAction("Accept")),
                new WorkflowTransition(new WorkflowState("Pending"),  new WorkflowState("Rejected"), new WorkflowAction("Ignore")),
                new WorkflowTransition(new WorkflowState("Accepted"), new WorkflowState("Approved"), new WorkflowAction("Approve")),
                new WorkflowTransition(new WorkflowState("Accepted"), new WorkflowState("Rejected"), new WorkflowAction("Reject"))
            };

            // Save the new workflow with custom extension data which 
            // identifies the community it is intended to be associated with.
          
var membershipWorkflow = new Workflow( "Membership: " + community.Name, workflowTransitions, new WorkflowState("Pending") );
var workflowExtension = new MembershipModeration { Group = community.Id }; if (membershipWorkflow != null) { try { this.workflowService.Add(membershipWorkflow, workflowExtension); } catch (SocialAuthenticationException ex) { throw new SocialRepositoryException("The application failed to authenticate with Episerver Social.", ex); } catch (MaximumDataSizeExceededException ex) { throw new SocialRepositoryException("The application request was deemed too large for Episerver Social.", ex); } catch (SocialCommunicationException ex) { throw new SocialRepositoryException("The application failed to communicate with Episerver Social.", ex); } catch (SocialException ex) { throw new SocialRepositoryException("Episerver Social failed to process the application request.", ex); } } }

There are four separate sections to this method that are important to take note of.

  1. The construction of the list of WorkflowTransitions.
    • The Workflow object takes a list of WorkflowTransitions. A WorkflowTransition describes the action required to move from one state to another within the workflow A WorkflowTransition consists of two WorkflowStates and one WorkflowAction. The WorkflowStates reflect where the entity in the workflow is coming from and where it is going to when the associated WorkflowAction is taken.
    • The transitions for a workflow cannot contain duplicate entries with identical combinations of From and To state.
    • An example of a WorkflowTransition is when an entity is in a “Pending” state and can be moved into an “Accepted” state by taking the WorkflowAction of “Accept”.
  2. The construction of the Workflow object.
    • In addition to a list of WorkflowTransitions, the Workflow object needs a name and an initial state to be properly constructed. An initial WorkflowState is the starting state for any entity entered into this workflow.
    • It is important to note that the initial state of the workflow being added must match either the From or the To state of at least one of the workflow transitions.
  3. The construction of the MembershipModeration extension data.
    • The MembershipModeration extension data is used to persist relevant information about the group with which the workflow is associated.
  4. The adding of the workflow and the workflow extension data to the Social system.
    • When using any Social Services, you must anticipate the possibility that a Social exception might be thrown. Organizations need to determine in advance how to handle Social exceptions.
    • While there is a return value for a Workflow Service Add, this demo was designed to not use the persisted composite.

Adding a Moderated Member

The GroupAdmissionBlock facilitates the addition of members to groups within SocialAlloy. When a group is created with membership moderation, the block internally calls the AddAModeratedMember method on the CommunityMemberModerationRepository when a user requests membership to the group.

 /// <summary>
        /// Submits a membership request to the specified group's
        /// moderation workflow for approval.
        /// </summary>
        /// <param name="member">The member information for the membership request</param>
        public void AddAModeratedMember(CommunityMember member)
        {
            // Define a unique reference representing the entity
            // under moderation. Note that this entity may be
            // transient or may not yet have been assigned a
            // unique identifier. Defining an item reference allows
            // you to bridge this gap.

            // For example: "members:/{group-id}/{user-reference}"

            var targetReference = this.CreateUri(member.GroupId, member.User);

            // Retrieve the workflow supporting moderation of
            // membership for the group to which the user is
            // being added.

            var moderationWorkflow = this.GetWorkflowFor(member.GroupId);

            // The workflow defines the intial (or 'start') state
            // for moderation.

            var initialState = moderationWorkflow.InitialState;

            // Create a new workflow item...

            var workflowItem = new WorkflowItem(
                WorkflowId.Create(moderationWorkflow.Id),      // ...under the group's moderation workflow
                new WorkflowState(initialState),               // ...in the workflow's initial state
                Reference.Create(targetReference)              // ...identified with this reference
            );

            var memberRequest = memberAdapter.Adapt(member);
                try
                {
                    this.workflowItemService.Add(workflowItem, memberRequest);
                }
                catch (SocialAuthenticationException ex)
                {
                    throw new SocialRepositoryException("The application failed to authenticate with Episerver Social.", ex);
                }
                catch (MaximumDataSizeExceededException ex)
                {
                    throw new SocialRepositoryException("The application request was deemed too large for Episerver Social.", ex);
                }
                catch (SocialCommunicationException ex)
                {
                    throw new SocialRepositoryException("The application failed to communicate with Episerver Social.", ex);
                }
                catch (SocialException ex)
                {
                    throw new SocialRepositoryException("Episerver Social failed to process the application request.", ex);
                }
        }

The AddAModeratedMember method can be divided into four distinct sections:

  1. Creation of a unique URI used to track the progression of a member being moderated for group admission.
  2. Retrieval of the workflow for the group to which a user is requesting membership.
  3. A Workflow item is constructed to encapsulate three important parts:
    • the unique identifier for the workflow associated with the group
    • the initial state of the associated workflow
    • the unique URI to track progression of an entity through the workflow
  4. The addition of the WorkflowItem to the Social system by using the WorkflowItem Service Add method. A WorkflowItem captures the state and data of an entity within a moderation workflow.

Interacting with existing workflows

Below is an example of a Moderation View. This is a standard moderation interface that allows a user to select from a list of existing Workflows, displays existing membership requests for a workflow, and acts on available actions for each member request.

 <div class="row">
        <div class="span12">
            <div>
                @using (Html.BeginForm("Index", "Moderation", FormMethod.Get))
                {
                   <div>Choose a group to moderate:</div>
                    <div>
                        @Html.DropDownListFor(x => x.SelectedWorkflow,
                                              new SelectList(
                                                 Model.Workflows,
                                                  "Id",
                                                  "Name", 
                                                  Model.Workflows.First().Id), new { @class = "MakeWide" })
                    </div>
                   <div><input type="submit" value="View" /></div>
                }
            </div>
        </div>
    </div>
    <div class="row">
        <div class="span12">
            <div>
                <h4>Membership Requests</h4>
                <p>
                    There are membership requests which may be pending your approval.
                </p>
            </div>
            <table class="table table-striped">
                <thead>
                    <tr>
                        <th>User</th>
                        <th>State</th>
                        <th>Date</th>
                        <th>Actions</th>
                    </tr>
                </thead>
                @foreach (var item in Model.Items)
                {
                    <tr>
                        <td>
                            @item.UserName
                        </td>
                        <td>
                            @item.State
                        </td>
                        <td>
                            @item.Created
                        </td>
                        <td>
                            @using (Html.BeginForm("Index", "Moderation", FormMethod.Post))
                            {
                                <input type="hidden" name="userId" value="@Html.AttributeEncode(item.User)" />
                                <input type="hidden" name="communityId" value="@Html.AttributeEncode(item.Group)" />
                                <input type="hidden" name="workflow" value="@Html.AttributeEncode(Model.SelectedWorkflow.Id)" />

                                if (item.Actions.Count() == 0)
                                {
                                    <p class="muted">No actions available</p>
                                }

                                foreach (var action in item.Actions)
                                {
                                    <input type="submit" name="workflowAction" value="@action" />
                                }
                            }
                        </td>
                    </tr>
                }
            </table>
        </div>
    </div>

The CommunityMemberModerationRepository was built with a Get method that returns everything you need to build a moderation screen.

        /// <summary>
        /// Returns a view model supporting the presentation of group
        /// membership moderation information.
        /// </summary>
        /// <param name="workflowId">Identifier for the selected membership moderation workflow</param>
        /// <returns>View model of moderation information</returns>
        public CommunityModerationViewModel Get(string workflowId)
        {
            try
            {
                // Retrieve a collection of all workflows in the system with MembershipModeration extension data.
                var allWorkflows = this.GetWorkflows();
                // Retrieve the workflow specified as the selected one.
                // If no workflow is selected, default to the first
                // available workflow.

                var selectedWorkflow = string.IsNullOrWhiteSpace(workflowId)
                    ? allWorkflows.FirstOrDefault()
                    : allWorkflows.FirstOrDefault(w => w.Id.ToString() == workflowId);

                // Retrieve the current state for all membership requests
                // under the selected moderation workflow.

                var currentWorkflowItems = this.GetWorkflowItemsFor(selectedWorkflow);
            
var workflowItemAdapter = new CommunityMembershipRequestAdapter(selectedWorkflow, this.userRepository);
return new CommunityModerationViewModel { Workflows = allWorkflows.Select(workflowAdapter.Adapt), SelectedWorkflow = workflowAdapter.Adapt(selectedWorkflow), Items = currentWorkflowItems.Select(workflowItemAdapter.Adapt) }; } catch (SocialAuthenticationException ex) { throw new SocialRepositoryException("The application failed to authenticate with Episerver Social.", ex); } catch (MaximumDataSizeExceededException ex) { throw new SocialRepositoryException("The application request was deemed too large for Episerver Social.", ex); } catch (SocialCommunicationException ex) { throw new SocialRepositoryException("The application failed to communicate with Episerver Social.", ex); } catch (SocialException ex) { throw new SocialRepositoryException("Episerver Social failed to process the application request.", ex); } }

The retrieval of the CommunityModerationViewModel consists of three major parts.

  1. Retrieving all available workflows in the system by calling the GetWorkflows method in the CommMakeWide" }) </div> <div><input type="submit" value="View" /></div> } unityMemberModerationRepository. This method uses the Workflow Service Get method. When all workflows are returned, it is possible to select the one that matches the workflow id provided. Also, by retrieving all workflows, allow the UI to display available workflows in the moderation selection dropdown.
  2. GetWorkflowItemsFor method is then called and invokes the WorkflowItem Service Get method using CompositeCriteria. CompositeCriteria encapsulates the specifications by which platform entities, composed with extension data, should be retrieved. The CompositeCriteria used here look similar to this:

       var criteria = new CompositeCriteria<WorkflowItemFilter, AddMemberRequest>
       {
                 Filter = new WorkflowItemFilter
                 {
                     ExcludeHistoricalItems = true,      // Include only the current state for the requests
                     Workflow = workflow.Id,             // Include only items for the selected group's workflow
                  },
                  PageInfo = new PageInfo { PageSize = 30 },   // Limit to 30 items
    }; // Order the results alphabetically by their state and then // by the date on which they were created. criteria.OrderBy.Add(new SortInfo(WorkflowItemSortFields.State, true)); criteria.OrderBy.Add(new SortInfo(WorkflowItemSortFields.Created, true));
  • The Get request only returns WorkflowItems in their current state that are associated with the requested workflow.
  • The values being returned are ordered by state name then by WorkflowItem creation date.

       3. Populating the ModerationViewModel requires:

    • all workflows that were retrieved in step 1
    • the populated selected workflow that was requested
    • all WorkflowItems associated with the requested Workflow

** It is important to note that this example uses an adapter class that works with available objects to produce a different, client suitable object that the view can interpret.

Moderate a workflowitem

When looking to moderate a specific workflow item, an interface needs to display available items to moderate and actions that may be taken on them. This shows an example of how displaying those items and actions could be done.

  @foreach (var item in Model.Items)
  {
         <tr>
             <td>
                @item.UserName
             </td>
             <td>
                 @item.State
              </td>
              <td>
                  @item.Created
              </td>
              <td>
                    @using (Html.BeginForm("Index", "Moderation", FormMethod.Post))
                    {
                        <input type="hidden" name="userId" value="@Html.AttributeEncode(item.User)" />
                        <input type="hidden" name="communityId" value="@Html.AttributeEncode(item.Group)" />
                        <input type="hidden" name="workflow" value="@Html.AttributeEncode(Model.SelectedWorkflow.Id)" />

if (item.Actions.Count() == 0) { <p class="muted">No actions available</p> } foreach (var action in item.Actions) { <input type="submit" name="workflowAction" value="@action" /> } } </td> </tr> }
 

Once a moderator has taken an action on an item in moderation, that action must be managed in a meaningful way within the Social Framework. For this tutorial, a Moderate method was created in the CommunityMembershipModerationRepository to account for what should happen when an item in moderation is acted upon.

        /// <summary>
        /// Takes action on the specified workflow item, representing a
        /// membership request.
        /// </summary>
        /// <param name="workflowId">The id of the workflow </param>
        /// <param name="action">The moderation action to be taken</param>
        /// <param name="userId">The unique id of the user under moderation.</param>
        /// <param name="communityId">The unique id of the community to which membership has been requested.</param>
        public void Moderate(string workflowId, string action, string userId, string communityId)
        {
            var membershipRequest = GetMembershipRequest(userId, communityId);
            var populatedWorkflowId = WorkflowId.Create(workflowId);

            var requestReference = Reference.Create(CreateUri(membershipRequest.Group, membershipRequest.User));

            try
            {
                var transitionToken = this.workflowService.BeginTransitionSession(populatedWorkflowId, requestReference);
                try
                {
                    // Retrieve the moderation workflow associated with
                    // the item to be acted upon.

                    var workflow = this.workflowService.Get(populatedWorkflowId);

                    // Leverage the workflow to determine what the
                    // resulting state of the item will be upon taking 
                    // the specified action.

                    //retrieve the current state of the workflow item once the begintransitionsession begins.
                    var filter = new WorkflowItemFilter { Target = requestReference };
                    var criteria = new Criteria<WorkflowItemFilter> { Filter = filter };
                    var workflowItem = this.workflowItemService.Get(criteria).Results.Last();

                    // Example: Current State: "Pending", Action: "Approve" => Transitioned State: "Approved"
                    var transitionedState = workflow.Transition(workflowItem.State, new WorkflowAction(action));

                    var subsequentWorkflowItem = new WorkflowItem(
                        workflow.Id,
                        transitionedState,
                        requestReference
                    );

                    this.workflowItemService.Add(subsequentWorkflowItem, membershipRequest, transitionToken);

                    // Perform any application logic given the item's
                    // new state.

                    if (this.IsApproved(subsequentWorkflowItem.State))
                    {
                        memberRepository.Add(memberAdapter.Adapt(membershipRequest));
                    }
                }
                finally
                {
                    this.workflowService.EndTransitionSession(transitionToken);
                }
            }
            catch (SocialAuthenticationException ex)
            {
                throw new SocialRepositoryException("The application failed to authenticate with Episerver Social.", ex);
            }
            catch (MaximumDataSizeExceededException ex)
            {
                throw new SocialRepositoryException("The application request was deemed too large for Episerver Social.", ex);
            }
            catch (SocialCommunicationException ex)
            {
                throw new SocialRepositoryException("The application failed to communicate with Episerver Social.", ex);
            }
            catch (SocialException ex)
            {
                throw new SocialRepositoryException("Episerver Social failed to process the application request.", ex);
            }
        }

The Moderate method takes:

  • a unique identifier for the associated workflow
  • the action that was taken by the moderator
  • a unique identifier for the user requesting membership to a community
  • the unique identifier for the associated community

The method can be broken down into 8 simple steps.

  1. The construction of a unique URI (the same URI from the step 1 in the AddAModeratedMember method) for the workflow request.
  2. The BeginTransitionSession method in the WorkflowService must be called in advance of any WorkflowItem transition.
    • Requests exclusive access to add workflow items for a target within a particular workflow. If another client already has secured access to that target, a TransitionSessionDeniedException is thrown.
    • The TransitionSessionToken returned in this operation is used in the addition of a new WorkflowItem.
  3. Retrieval of the most recent WorkflowItem by the Workflow Service by using the Get method.
  4. Using the Workflow Transition method, the transitioned state is constructed from the current WorkflowItem state and the requested action.
  5. Construction of a new WorkflowItem with the workflow identifier, the transitioned state, and the unique URI (constructed in step 1).
  6. The new WorkflowItem is added to the system with the populated AddMemberRequest and TransitionSessionToken.
  7. This tutorial has a conditional check when moderating to see if the new WorkflowItem is in a state of approved (one possible end of the workflow). Only when the admission state has reached "accepted" is the member added to the MemberRepository.
  8. Once the Add method has successfully updated the state of the admission request and no error is encountered, the EndTransitionSession method in the WorkflowService should be called.
    • The EndTransitionSession method is passed the TransitionSessionToken that was constructed in step 2.
    • This method ends the transition session, relinquishing exclusive access to add workflow items to the target.

Essential files for Group Membership Moderation Tutorial

File Type: Controller
Location: SocialAlloy/Social/Controllers/
Name / Description:

  • ModerationController.cs – used to update the CommunityModerationViewModel

File Type: Model
Location: SocialAlloy/Social/Models/
Name/ Description:

  • CommunityMembershipWorkflow.cs - encapsulates SocialAlloy specific details of a Group Workflow
  • CommunityModerationViewModel.cs - relays moderation details from the ModerationController to the Moderation view
  • Community.cs - describes a group model used by the SocialAlloy site.
  • CommunityMember.cs – describes a member model used by the SocialAlloy site

File Type: View:
Location: SocialAlloy/Views/Social/Moderation
Name / Description:

  • Index.cshtml – Generates outputs to the user based upon the values of the CommunityModerationViewModel

File Type: Repository
Location: SocialAlloy/Social/Repositories/Moderation
Name / Description:

  • ICommunityMemberModerationRepository.cs – The interface describing operations that manage community membership moderation
  • CommunityMemberModerationRepository.cs - Implements operations that manage community membership moderation with the Episerver Social Framework
  • ICommunityMemberRepostiory.cs – The interface that describes a component capable of persisting and retrieving community member data
  • CommunityMemberRepository.cs - Persists and retrieves community member data to and from the Episerver Social Framework

* Repositories in the SocialAlloy site are responsible for the sole point of interaction with the Social API. Social constructs have been abstracted away by site specific entities in order to separate the business logic of the site from the Social API integration points.

File Type: Extension Data
Location: EPiServer.SocialAlloy.ExtensionData/Membership
Name / Description:

  • AddMemberRequest.cs - a serializable class representing a request for membership to a group. It is intended to support the moderation process around group membership.
  • MemberExtensionData.cs- Custom extension data used in saving member details for members associated with groups.
  • MembershipModeration.cs - Intended to identify the relationship between a moderation workflow and the group which it supports for moderating membership requests. This serves as an extension data added to workflows

* It is important to note that the extension data for the SocialAlloy site has been moved to its own project by design. Compartmentalizing extension data and adding that project into a site solution ideally make namespaces more stable and less susceptible to changing over time. Additionally, by separating the extension data, it should be very easy for a user to reference the same POCOs in their staging site that they are referencing in their production site.

Mar 17, 2017

Comments

Please login to comment.
Latest blogs
Optimizely CMS Roadmap

Explore Optimizely CMS's latest roadmap, packed with developer-focused updates. From SaaS speed to Visual Builder enhancements, developer tooling...

Andy Blyth | Nov 21, 2024 | Syndicated blog

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