Snippets

Exalate Approval Logic - the core functionality of the solution

Created by Francis Martens
package com.idalko.custom

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.event.issue.IssueEventManager
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.security.JiraAuthenticationContext
import com.atlassian.jira.user.ApplicationUser
import com.idalko.jira.groovy.FieldValueUtil
import org.apache.log4j.Logger
import java.text.SimpleDateFormat

class ApprovalLogic {
    private static Logger log = Logger.getLogger("com.idalko.plugins.custom.ApprovalLogic");

    private static final String REVIEW_PROCESS_MARKER = "\n----\n";

    // access all customfields in use for the approval logic
    
    private static FieldValueUtil fvuToApprove = new FieldValueUtil("To approve");
    private static FieldValueUtil fvuApprovers = new FieldValueUtil("Approvers");
    private static FieldValueUtil fvuHasAccepted = new FieldValueUtil("Has accepted");
    private static FieldValueUtil fvuHasDeclined = new FieldValueUtil("Has declined");
    private static FieldValueUtil fvuDelegateTo = new FieldValueUtil("Delegate to");
    private static FieldValueUtil fvuDelegateToEmail = new FieldValueUtil("ZZdte");
    private static FieldValueUtil fvuApprovalComment = new FieldValueUtil("Approval comment");

    private static JiraAuthenticationContext jac = ComponentAccessor.getJiraAuthenticationContext();

    /**
     * Remove the current user from the list of 'To approve'
     * Remove the current user from the list of 'Has approved'
     * Add the current user to the list of 'Has declined'
     *
     * Add his comment to the 'Approval comment'
     *
     * @param issue
     */
    public static void doDecline(Issue issue, String comment) {
        ApplicationUser currentUser = jac.getUser();

        removeUser(fvuToApprove, issue, currentUser);
        removeUser(fvuHasAccepted, issue, currentUser);
        addUser(fvuHasDeclined, issue, currentUser);
        addComment(issue, currentUser, "declined", comment);

        FieldValueUtil.persistIssueModifications(issue, currentUser.getName());

    }

    /**
     * Remove the current user from the list of 'To approve'
     * Remove the current user from the list of 'Has declined'
     * Add the current user to the list of 'Has accepted'
     * Add his comment to the 'Approval comment'
     *
     * @param issue
     */

    public static void doAccept(Issue issue, String comment) {
        ApplicationUser currentUser = jac.getUser();

        removeUser(fvuToApprove, issue, currentUser);
        removeUser(fvuHasDeclined, issue, currentUser);
        addUser(fvuHasAccepted, issue, currentUser);
        addComment(issue, currentUser, "accepted", comment);
        FieldValueUtil.persistIssueModifications(issue, currentUser.getName());
    }


    /**
     * Delegate function will remove the currentuser from the toApprove and add
     * the users contained in the 'Delegate to' custom field if not yet there
     *
     */

    public static void delegateTo(MutableIssue issue, String comment) {
        String delegateNames = "";
        String sep = "";
        ApplicationUser currentUser = jac.getUser();
        List<ApplicationUser> delegates = fvuDelegateTo.getFieldValue(issue);

        if (delegates == null || delegates.size() == 0)  return;


        fvuDelegateToEmail.setFieldValue(issue, null);
        removeUser(fvuToApprove, issue, currentUser)
        for (ApplicationUser delegate:delegates) {
            delegateNames += sep + delegate.getDisplayName();
            sep = ", ";
            addUser(fvuToApprove, issue, delegate)
            addUser(fvuDelegateToEmail, issue, delegate)
        }

        fvuDelegateTo.setFieldValue(issue, null);
        addComment(issue,currentUser, "Delegated to " + delegateNames, comment)
        FieldValueUtil.persistIssueModifications(issue, currentUser.getName());
    }

    /**
     * The list of approvers is being reset to a new list
     *
     * For all the new users (which are not listed in 'to approve', 'has rejected', 'has approved')
     * Add the user to 'Approvers', 'to approve'
     *
     * For all the users which are not listed anymore (ie. the diff between the new approvers and the union
     * of 'to approve', 'has rejected', 'has approved')
     * Remove the user from the 'Approvers', 'to approve', 'has rejected', 'has approved'
     *
     * @param issue
     */
    public static void changeApprovers(MutableIssue issue, String comment) {
        ApplicationUser currentUser = jac.getUser();
        List<ApplicationUser> newApprovers = fvuApprovers.getFieldValue(issue);
        List<ApplicationUser> currentApprovers = collectApprovers(issue);



        log.debug("Changing approvers");
        for (ApplicationUser approver:newApprovers) {
            if (! inList(currentApprovers, approver)) {
                // this is a new approver
                addUser(fvuToApprove, issue, approver);
            }
        }

        for (ApplicationUser approver:currentApprovers) {

            if (! inList(newApprovers, approver)) {
                // this approver should be removed from the list
                removeUser(fvuToApprove, issue, approver);
                removeUser(fvuHasAccepted, issue, approver);
                removeUser(fvuHasDeclined, issue, approver);
                removeUser(fvuDelegateToEmail, issue, approver);
            }
        }

        addComment(issue, currentUser, "changed approvers ", comment)
        FieldValueUtil.persistIssueModifications(issue, currentUser.getName());
    }

    // The bypass function is provided such that a document can be accepted even if not fully approved
    public static void byPass(MutableIssue issue) {
        ApplicationUser currentUser = jac.getUser();
        addComment(issue, currentUser, "bypassed", null);
    }

    /**
     * checks if the user is member of the list
     * @param applicationUsers
     * @param applicationUser
     * @return
     *
     */

    private static boolean inList(List<ApplicationUser> applicationUsers, ApplicationUser searchUser) {

        if (searchUser == null) return false;

        for (ApplicationUser user:applicationUsers) {
            if (user.getUsername().equals(searchUser.getUsername())) return true;
        }
        return false;
    }

    /**
     * Collect all users that are involved in approving this document
     * These are the people that have accepted, declined and/or still need to approve.
     * @param issue
     * @return
     */
    private static List<ApplicationUser> collectApprovers(MutableIssue issue) {
        List<ApplicationUser> userList = new ArrayList<ApplicationUser>();

        userList = addUsers(userList, (List<ApplicationUser>) fvuToApprove.getFieldValue(issue));
        userList = addUsers(userList, (List<ApplicationUser>) fvuHasAccepted.getFieldValue(issue));
        userList = addUsers(userList, (List<ApplicationUser>) fvuHasDeclined.getFieldValue(issue));

        return userList;

    }

    // creates a new user list by merging two other user lists
    public static List<ApplicationUser> addUsers(List<ApplicationUser> applicationUsers, List<ApplicationUser> users) {
        for (ApplicationUser user: users) {
            if (!inList(applicationUsers, user)) {
                applicationUsers.add(user);
            }
        }

        return applicationUsers;

    }

    /**
     * Add a comment to the Approval Comment custom field.  This custom field tracks the specific approval
     * history of the document
     *
     * If multiple review rounds are in place, the rounds are in reverse order, but the actions
     * within a round are ordered first to last.
     *
     * @param issue
     * @param user
     * @param operation
     * @param comment
     */
    private static void addComment(Issue issue, ApplicationUser user, String operation, String comment) {
        SimpleDateFormat sdf = new SimpleDateFormat("dd/MMM/yyyy HH:mm");

        // Calculate what needs to be added
        String userName = user.getDisplayName();
        String inlineComment = (comment != null && comment.length() > 0) ?
                "with following comment\n {quote}" + comment + "{quote}" :
                "";
        String commentExtension = "* ${userName} ${operation} on ${sdf.format(new Date())} ${inlineComment}\n"

        // Split current comment between last review round and the rest. The last review round actions are at the top.
        String currentApprovalComment = fvuApprovalComment.getString(issue);
        String lastReviewRound;
        String otherReviewRounds;
        if(currentApprovalComment.startsWith(REVIEW_PROCESS_MARKER)) {
            lastReviewRound = "";
            otherReviewRounds = currentApprovalComment;
        } else if(currentApprovalComment.contains(REVIEW_PROCESS_MARKER)) {
            int justAfterFirstRound = currentApprovalComment.indexOf(REVIEW_PROCESS_MARKER);
            lastReviewRound = currentApprovalComment.substring(0, justAfterFirstRound);
            otherReviewRounds = currentApprovalComment.substring(justAfterFirstRound);
        } else {
            lastReviewRound = currentApprovalComment;
            otherReviewRounds = "";
        }
        // Add the new comment at the end of the last round
        lastReviewRound += commentExtension;

        // Combine the last round with the rest again.
        String newApprovalComment = lastReviewRound + otherReviewRounds;

        fvuApprovalComment.setFieldValue(issue, newApprovalComment);
    }

    /**
     * Log the start of a document review process.
     *
     * Add a comment who sent out the review request.
     * If this is not the first round, also add a separation marker to the log.
     *
     * @param issue
     */
    public static logStartOfReviewProcess(MutableIssue issue) {
        // RECORD-57 Don't clear the Approval comment, but add a marker to it.
        FieldValueUtil fvuApprovalComment = new FieldValueUtil("Approval comment")
        def oldValue = fvuApprovalComment.getFieldValue(issue);
        if (oldValue != null || oldValue != "") {
            fvuApprovalComment.doSetFieldValue(issue, REVIEW_PROCESS_MARKER + oldValue)
        }

        // Log who sent out the review request
        addComment(issue, jac.getUser(), 'sent documents for review', null)
    }

    // For testing purposes.
    public static clearReviewComment(MutableIssue issue) {
        fvuApprovalComment.clearFieldValue(issue)
    }

    // Remove a single user from the list
    private static void removeUser(FieldValueUtil fieldValueUtil, Issue issue, ApplicationUser targetUser) {
        List<ApplicationUser> userList = fieldValueUtil.getFieldValue(issue);
        if (userList == null) return;

        List<ApplicationUser> newList = new ArrayList<>();
        for (ApplicationUser user:userList) {
            if (user.getName() != targetUser.getName()) {
                newList.add(user);
            }
        }

        fieldValueUtil.doSetFieldValue(issue, newList);
    }

    // add a user from the list
    private static void addUser(FieldValueUtil fieldValueUtil, Issue issue, ApplicationUser targetUser) {

        List<ApplicationUser> userList = fieldValueUtil.getFieldValue(issue);
        if (userList == null) userList = new ArrayList<>();

        for (ApplicationUser user:userList) {
            if (user.getName() == targetUser.getName()) {
                return;
            }
        }

        // the user is not member of the list
        log.debug("Adding user ${targetUser.getName()} to the field ${fieldValueUtil.customField.getName()}");
        userList.add(targetUser);
        fieldValueUtil.doSetFieldValue(issue, userList);
    }
}

Comments (1)

  1. Phill Fox

    Hi

    I am wondering where can I get a copy of the class as referred to here?

    #!
    
    import com.idalko.jira.groovy.FieldValueUtil
    
HTTPS SSH

You can clone a snippet to your computer for local editing. Learn more.