org.sakaiproject.evaluation.logic.EvalEvaluationSetupServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.sakaiproject.evaluation.logic.EvalEvaluationSetupServiceImpl.java

Source

/**
 * Copyright 2005 Sakai Foundation Licensed under the
 * Educational Community License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 *
 * http://www.osedu.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */
package org.sakaiproject.evaluation.logic;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.component.cover.ServerConfigurationService;
import org.sakaiproject.evaluation.beans.EvalBeanUtils;
import org.sakaiproject.evaluation.constant.EvalConstants;
import org.sakaiproject.evaluation.dao.EvaluationDao;
import org.sakaiproject.evaluation.logic.exceptions.BlankRequiredFieldException;
import org.sakaiproject.evaluation.logic.exceptions.InvalidDatesException;
import org.sakaiproject.evaluation.logic.externals.EvalExternalLogic;
import org.sakaiproject.evaluation.logic.externals.EvalJobLogic;
import org.sakaiproject.evaluation.logic.externals.EvalSecurityChecksImpl;
import org.sakaiproject.evaluation.logic.externals.ExternalHierarchyLogic;
import org.sakaiproject.evaluation.logic.model.EvalGroup;
import org.sakaiproject.evaluation.logic.model.EvalHierarchyNode;
import org.sakaiproject.evaluation.logic.model.EvalUser;
import org.sakaiproject.evaluation.model.EvalAdhocGroup;
import org.sakaiproject.evaluation.model.EvalAssignGroup;
import org.sakaiproject.evaluation.model.EvalAssignHierarchy;
import org.sakaiproject.evaluation.model.EvalAssignUser;
import org.sakaiproject.evaluation.model.EvalEmailTemplate;
import org.sakaiproject.evaluation.model.EvalEvaluation;
import org.sakaiproject.evaluation.model.EvalResponse;
import org.sakaiproject.evaluation.model.EvalTemplate;
import org.sakaiproject.evaluation.utils.ArrayUtils;
import org.sakaiproject.evaluation.utils.EvalUtils;
import org.sakaiproject.evaluation.utils.TextTemplateLogicUtils;
import org.sakaiproject.genericdao.api.search.Order;
import org.sakaiproject.genericdao.api.search.Restriction;
import org.sakaiproject.genericdao.api.search.Search;

/**
 * Implementation for EvalEvaluationSetupService
 * (Note for developers - do not modify this without permission from the author)<br/>
 * Modified all the uses of perms to the new user assignments
 * 
 * @author Aaron Zeckoski (aaronz@vt.edu)
 */
public class EvalEvaluationSetupServiceImpl implements EvalEvaluationSetupService {

    private static final Log LOG = LogFactory.getLog(EvalEvaluationSetupServiceImpl.class);

    private final String EVENT_EVAL_CREATE = "eval.evaluation.created";
    private final String EVENT_EVAL_UPDATE = "eval.evaluation.updated";
    private final String EVENT_EVAL_DELETE = "eval.evaluation.deleted";
    private final String EVENT_EVAL_CLOSED = "eval.evaluation.closed.early";

    private static final String SAKAI_PROP_EVALSYS_SECTION_AWARE_DEFAULT = "evalsys.section.aware.default";
    private static final String SAKAI_PROP_EVALSYS_RESULTS_SHARING_DEFAULT = "evalsys.results.sharing.default";
    private static final String SAKAI_PROP_EVALSYS_INSTRUCTOR_VIEW_RESPONSES_DEFAULT = "evalsys.instructor.view.responses.default";
    private static final Boolean EVALSYS_SECTION_AWARE_DEFAULT = ServerConfigurationService
            .getBoolean(SAKAI_PROP_EVALSYS_SECTION_AWARE_DEFAULT, false);
    private static final String EVALSYS_RESULTS_SHARING_DEFAULT = ServerConfigurationService
            .getString(SAKAI_PROP_EVALSYS_RESULTS_SHARING_DEFAULT, EvalConstants.SHARING_VISIBLE);
    private static final boolean EVALSYS_INSTRUCTOR_VIEW_RESPONSES_DEFAULT = ServerConfigurationService
            .getBoolean(SAKAI_PROP_EVALSYS_INSTRUCTOR_VIEW_RESPONSES_DEFAULT, true);

    private EvaluationDao dao;

    public void setDao(EvaluationDao dao) {
        this.dao = dao;
    }

    private EvalCommonLogic commonLogic;

    public void setCommonLogic(EvalCommonLogic common) {
        this.commonLogic = common;
    }

    private EvalSettings settings;

    public void setSettings(EvalSettings settings) {
        this.settings = settings;
    }

    private EvalEvaluationService evaluationService;

    public void setEvaluationService(EvalEvaluationService evaluationService) {
        this.evaluationService = evaluationService;
    }

    private EvalExternalLogic externalLogic;

    public void setExternalLogic(EvalExternalLogic externalLogic) {
        this.externalLogic = externalLogic;
    }

    private EvalSecurityChecksImpl securityChecks;

    public void setSecurityChecks(EvalSecurityChecksImpl securityChecks) {
        this.securityChecks = securityChecks;
    }

    private ExternalHierarchyLogic hierarchyLogic;

    public void setHierarchyLogic(ExternalHierarchyLogic hierarchyLogic) {
        this.hierarchyLogic = hierarchyLogic;
    }

    private EvalAuthoringService authoringService;

    public void setAuthoringService(EvalAuthoringService authoringService) {
        this.authoringService = authoringService;
    }

    private EvalEmailsLogic emails;

    public void setEmails(EvalEmailsLogic emails) {
        this.emails = emails;
    }

    private EvalJobLogic evalJobLogic;

    public void setEvalJobLogic(EvalJobLogic evalJobLogic) {
        this.evalJobLogic = evalJobLogic;
    }

    private EvalBeanUtils evalBeanUtils;

    public void setEvalBeanUtils(EvalBeanUtils evalBeanUtils) {
        this.evalBeanUtils = evalBeanUtils;
    }

    // INIT method
    public void init() {
        LOG.debug("INIT");
        // update evaluation user assignments for evals with none yet (migration code)

        Boolean syncUnassignedGroupsOnServerStartup = (Boolean) settings
                .get(EvalSettings.SYNC_UNASSIGNED_GROUPS_ON_STARTUP);
        LOG.debug("syncUnassignedGroupsOnServerStartup == " + syncUnassignedGroupsOnServerStartup);
        if (syncUnassignedGroupsOnServerStartup == null) {
            // use default, true
            syncUnassignedGroupsOnServerStartup = true;
        }
        if (syncUnassignedGroupsOnServerStartup) {
            List<EvalEvaluation> evals = dao.getEvalsWithoutUserAssignments();
            if (!evals.isEmpty()) {
                LOG.info("Creating user assignments for " + evals.size()
                        + " evals with none yet (auto-migration), may take awhile for a large number of evaluations");
                int counter = 0;
                for (EvalEvaluation evaluation : evals) {
                    List<Long> l = synchronizeUserAssignmentsForced(evaluation, null, false);
                    counter += l.size();
                }
                LOG.info("Synchronized " + counter + " user assignments for " + evals.size()
                        + " evals (auto-migration)");
            }
        }

        // run a timer which ensures that evaluation states are kept up to date
        initiateUpdateStateTimer();
    }

    /**
     * This will start up a timer which will keep the evaluations up to date, the disadvantage here is that
     * it will run every hour and on every server and therefore could increase the load substantially,
     * the other disadvantage is that if an evaluation goes from say active all the way to closed or viewable
     * then this would require a lot of extra logic to handle those cases,
     * holding off on the extra logic -AZ
     * NOTE: uses a DB server lock to ensure that only one server is running this stuff
     * 
     * Does the following:
     * 1) Ensures all evals have the correct state by checking and updating the state as needed
     * 2) Removes any partially created evals that are older than the constant (15 days)
     * 3) Resets the eval settings cache
     */
    public static String EVAL_UPDATE_TIMER = "eval_update_timer";

    protected void initiateUpdateStateTimer() {
        // timer repeats every 60 minutes
        final long repeatInterval = 1000 * 60 * 60;
        // start up a timer after 2 mins + random(10 mins)
        long startDelay = (1000 * 60 * 2) + (1000 * 60 * new Random().nextInt(10));

        TimerTask runStateUpdateTask = new TimerTask() {
            @Override
            public void run() {
                String serverId = commonLogic.getConfigurationSetting(EvalExternalLogic.SETTING_SERVER_ID,
                        "UNKNOWN_SERVER_ID");
                Boolean lockObtained = dao.obtainLock(EVAL_UPDATE_TIMER, serverId, repeatInterval);
                // only execute the code if we have an exclusive lock
                if (lockObtained != null && lockObtained) {
                    // get all evals that are not viewable (i.e. completely done with) or deleted 
                    List<EvalEvaluation> evals = dao.findBySearch(EvalEvaluation.class,
                            new Search(new Restriction[] {
                                    new Restriction("state", EvalConstants.EVALUATION_STATE_VIEWABLE,
                                            Restriction.NOT_EQUALS),
                                    new Restriction("state", EvalConstants.EVALUATION_STATE_DELETED,
                                            Restriction.NOT_EQUALS) }));
                    if (evals.size() > 0) {
                        LOG.info("Checking the state of " + evals.size()
                                + " evaluations to ensure they are all up to date...");
                        // only do partial purge if constant > 0
                        long partialPurgeTime = -1;
                        if (EvalConstants.EVALUATION_PARTIAL_CLEANUP_DAYS > 0) {
                            // set the partial purge number of days to the constant (15)
                            partialPurgeTime = System.currentTimeMillis()
                                    - (EvalConstants.EVALUATION_PARTIAL_CLEANUP_DAYS * 24l * 60l * 60l * 1000l);
                        }

                        // loop through and update the state of the evals if needed
                        int count = 0;
                        for (EvalEvaluation evaluation : evals) {
                            String evalState = evaluationService.returnAndFixEvalState(evaluation, false);
                            if (EvalConstants.EVALUATION_STATE_PARTIAL.equals(evalState) && partialPurgeTime > -1) {
                                // purge out partial evaluations older than the partial purge time
                                if (evaluation.getLastModified().getTime() < partialPurgeTime) {
                                    LOG.info("Purging partial evaluation (" + evaluation.getId() + ") from "
                                            + evaluation.getLastModified());
                                    deleteEvaluation(evaluation.getId(), commonLogic.getAdminUserId());
                                    continue;
                                }
                            }
                            // fix up the state if needed
                            String currentEvalState = evaluation.getState();
                            if (!currentEvalState.equals(evalState)) {
                                evaluationService.returnAndFixEvalState(evaluation, true); // update the state
                                count++;
                                // trigger the jobs logic to look at this since the state changed
                                evalJobLogic.processEvaluationStateChange(evaluation.getId(),
                                        EvalJobLogic.ACTION_UPDATE);
                            }
                        }
                        if (count > 0) {
                            LOG.info("Updated the state of " + count + " evaluations...");
                        }
                    }

                    // finally we will reset the system config cache
                    settings.resetCache(null);
                }
            }
        };

        // now we need to obtain a lock and then run the task if we have it
        Timer timer = new Timer(true);
        LOG.info("Initializing the repeating timer task for evaluation, first run in " + (startDelay / 1000)
                + " seconds " + "and subsequent runs will happen every " + (repeatInterval / 1000)
                + " seconds after that");
        timer.schedule(runStateUpdateTask, startDelay, repeatInterval);
    }

    // EVALUATIONS

    public void saveEvaluation(EvalEvaluation evaluation, String userId, boolean created) {
        LOG.debug("evalId: " + evaluation.getId() + ",userId: " + userId);

        // NOTE: The defaults for the evaluation should be set by calling this when evals are created and before they are saved
        //EvalBeanUtils.setEvaluationDefaults(evaluation, EvalConstants.EVALUATION_TYPE_EVALUATION);

        // set the date modified
        evaluation.setLastModified(new Date());

        if (created && EvalUtils.checkStateAfter(evaluation.getState(), EvalConstants.EVALUATION_STATE_PARTIAL,
                false)) {
            // created can only be true when this eval is in partial state
            created = false;
        }

        // check for required fields first
        if (EvalUtils.isBlank(evaluation.getTitle())) {
            throw new BlankRequiredFieldException("Cannot save an evaluation with a blank title", "title");
        }

        if (evaluation.getStartDate() == null) {
            throw new BlankRequiredFieldException("Cannot save an evaluation with a null startDate", "startDate");
        }

        // test date ordering first (for the dates that are set) - this should be externalized
        EvalBeanUtils.validateEvalDates(evaluation);

        boolean isNew = false;
        boolean isClosed = false;
        if (evaluation.getId() == null) {
            isNew = true;
            // assure that all the defaults are set correctly for new evals
            evalBeanUtils.setEvaluationDefaults(evaluation, null);
        } else {
            // should we also see if this eval already exists?
            //            EvalEvaluation evalCheck = evaluationService.getEvaluationById(evaluation.getId());
            //            if (evalCheck == null) {
            //                isNew = true;
            //            }
            if (EvalUtils.checkStateAfter(evaluation.getState(), EvalConstants.EVALUATION_STATE_CLOSED, true)) {
                // check if we are or have closed the evaluation (or some state after that)
                isClosed = true;
            }
        }

        // assure valid date handling but only after the dates are checked during saving
        evalBeanUtils.fixupEvaluationDates(evaluation, isClosed);

        // now perform checks depending on whether this is new or existing
        Calendar calendar = GregorianCalendar.getInstance();
        calendar.add(Calendar.MINUTE, -30); // put today a bit in the past (30 minutes)
        Date today = calendar.getTime();
        if (isNew) {
            // creating new evaluation
            if (evaluation.getDueDate() != null && evaluation.getDueDate().before(today)) {
                throw new InvalidDatesException(
                        "due date (" + evaluation.getDueDate() + ") cannot occur in the past for new evaluations",
                        "dueDate");
            }
            if (evaluation.getStopDate() != null && evaluation.getStopDate().before(today)) {
                throw new InvalidDatesException(
                        "stop date (" + evaluation.getStopDate() + ") cannot occur in the past for new evaluations",
                        "stopDate");
            }

            // test if new evaluation occurs in the past
            if (evaluation.getStartDate().before(today)) {
                LOG.warn("Evaluation was set to start in the past (" + evaluation.getStartDate()
                        + "), it has been reset to start now...");
                evaluation.setStartDate(new Date());
            }

            // check user permissions (uses public method)
            if (!evaluationService.canBeginEvaluation(userId)) {
                throw new SecurityException(
                        "User (" + userId + ") attempted to create evaluation without permissions");
            }

            // force new evals to respect the settings
            Boolean enableSelection = (Boolean) settings.get(EvalSettings.ENABLE_INSTRUCTOR_ASSISTANT_SELECTION);
            if (!enableSelection) {
                // force it to null
                evaluation.setSelectionSettings(null);
            }

        } else {
            // updating existing evaluation
            if (!securityChecks.canUserControlEvaluation(userId, evaluation)) {
                throw new SecurityException("User (" + userId + ") attempted to update existing evaluation ("
                        + evaluation.getId() + ") without permissions");
            }

            // All other checks have been moved to the tool (bad I know)
        }

        // make sure the state is set correctly (does not override special states)
        if (created) {
            // eval was just created so we will force it out of partial state
            evaluation.setState(EvalConstants.EVALUATION_STATE_INQUEUE);
        }
        String evalState = evaluationService.returnAndFixEvalState(evaluation, false);

        // make sure we are not using a blank template here and get the template without using lazy loading
        EvalTemplate template = null;
        if (evaluation.getTemplate() == null || evaluation.getTemplate().getId() == null) {
            throw new IllegalArgumentException("Evaluations must include a template (it cannot be null)");
        } else {
            // template is set so check that it has items in it
            template = authoringService.getTemplateById(evaluation.getTemplate().getId());
            if (template.getTemplateItems() == null || template.getTemplateItems().size() <= 0) {
                throw new IllegalArgumentException(
                        "Evaluations must include a template with at least one item in it");
            }
        }

        /** OLD WAY / new way is to copy the template when the eval is first saved
        // make sure the template is copied if not in partial state, it is ok to have the original template while in partial state
        if ( EvalUtils.checkStateAfter(evalState, EvalConstants.EVALUATION_STATE_PARTIAL, false) ) {
            
        Nov 2010 -- Adding check for new setting EvalSettings.ENABLE_TEMPLATE_COPYING that is true by 
        default and allows an instance not to make copies of templates, template-items, items and scales
        when creating a new template.  http://jira.sakaiproject.org/browse/EVALSYS-863 
         **/
        if (isNew && ((Boolean) settings.get(EvalSettings.ENABLE_TEMPLATE_COPYING))) {
            // copy the template on first time saved - http://jira.sakaiproject.org/jira/browse/EVALSYS-647
            if (template.getCopyOf() == null || template.isHidden() == false) {
                // not a hidden copy so make one
                Long copiedTemplateId = authoringService.copyTemplate(template.getId(), null, evaluation.getOwner(),
                        true, true);
                EvalTemplate copy = authoringService.getTemplateById(copiedTemplateId);
                evaluation.setTemplate(copy);
                template = copy; // set the new template to the template variable
            }
        }

        // fill in any default values and nulls here
        if (evaluation.getLocked() == null) {
            evaluation.setLocked(Boolean.FALSE);
        }
        if (evaluation.getResultsSharing() == null) {
            if (!EvalConstants.SHARING_VISIBLE.equals(EVALSYS_RESULTS_SHARING_DEFAULT)
                    && !EvalConstants.SHARING_PRIVATE.equals(EVALSYS_RESULTS_SHARING_DEFAULT)
                    && !EvalConstants.SHARING_PUBLIC.equals(EVALSYS_RESULTS_SHARING_DEFAULT)) {
                evaluation.setResultsSharing(EvalConstants.SHARING_VISIBLE);
            } else {
                evaluation.setResultsSharing(EVALSYS_RESULTS_SHARING_DEFAULT);
            }
        }

        // Instructors view results default controlled by sakai.property
        if ((Boolean) evaluation.getInstructorViewResults() == null) {
            evaluation.setInstructorViewResults(EVALSYS_INSTRUCTOR_VIEW_RESPONSES_DEFAULT);
        }
        if (evaluation.getInstructorViewAllResults() == null) {
            evaluation.setInstructorViewAllResults(EVALSYS_INSTRUCTOR_VIEW_RESPONSES_DEFAULT);
        }

        if (evaluation.getAuthControl() == null) {
            evaluation.setAuthControl(EvalConstants.EVALUATION_AUTHCONTROL_AUTH_REQ);
        }

        // Section awareness default controlled by sakai.property
        if (evaluation.getSectionAwareness() == null) {
            evaluation.setSectionAwareness(EVALSYS_SECTION_AWARE_DEFAULT);
        }

        // make sure the evaluation type required field is set
        if (evaluation.getType() == null) {
            evaluation.setType(EvalConstants.EVALUATION_TYPE_EVALUATION);
        }

        if (evaluation.getEvalCategory() != null && evaluation.getEvalCategory().length() <= 0) {
            evaluation.setEvalCategory(null);
        }

        // system setting checks for things like allowing users to modify responses
        Boolean systemModifyResponses = (Boolean) settings.get(EvalSettings.STUDENT_MODIFY_RESPONSES);
        if (systemModifyResponses == null) {
            if (evaluation.getModifyResponsesAllowed() == null) {
                evaluation.setModifyResponsesAllowed(Boolean.FALSE);
            }
        } else {
            evaluation.setModifyResponsesAllowed(systemModifyResponses);
        }

        Boolean systemBlankResponses = (Boolean) settings.get(EvalSettings.STUDENT_ALLOWED_LEAVE_UNANSWERED);
        if (systemBlankResponses == null) {
            if (evaluation.getBlankResponsesAllowed() == null) {
                evaluation.setBlankResponsesAllowed(Boolean.FALSE);
            }
        } else {
            evaluation.setBlankResponsesAllowed(systemBlankResponses);
        }

        // TODO - disabled for now
        //      String systemInstructorOpt = (String) settings.get( EvalSettings.INSTRUCTOR_MUST_USE_EVALS_FROM_ABOVE );
        //      if ( systemInstructorOpt == null ) {
        //         if (evaluation.getInstructorOpt() == null) {
        //            evaluation.setInstructorOpt( EvalConstants.INSTRUCTOR_OPT_OUT );
        //         }
        //      } else {
        //         evaluation.setInstructorOpt( systemInstructorOpt );
        //      }
        evaluation.setInstructorOpt(EvalConstants.INSTRUCTOR_REQUIRED);

        // cleanup for XSS scripting and strings
        evaluation.setTitle(commonLogic.cleanupUserStrings(evaluation.getTitle()));
        evaluation.setInstructions(commonLogic.cleanupUserStrings(evaluation.getInstructions()));
        evaluation.setReminderFromEmail(commonLogic.cleanupUserStrings(evaluation.getReminderFromEmail()));
        evaluation.setEvalCategory(commonLogic.cleanupUserStrings(evaluation.getEvalCategory()));

        // initialize new evaluation with available email sent false
        if (evaluation.getId() == null) {
            evaluation.setAvailableEmailSent(false);
        }

        // save the eval
        dao.save(evaluation);
        LOG.info("User (" + userId + ") saved evaluation (" + evaluation.getId() + "), state="
                + evaluation.getState() + ", title: " + evaluation.getTitle());

        // initialize the scheduling for the eval jobs (only if state is not partial)
        if (EvalUtils.checkStateAfter(evalState, EvalConstants.EVALUATION_STATE_PARTIAL, false)) {
            if (created) {
                commonLogic.registerEntityEvent(EVENT_EVAL_CREATE, evaluation);
                // call logic to manage Quartz scheduled jobs
                evalJobLogic.processEvaluationStateChange(evaluation.getId(), EvalJobLogic.ACTION_CREATE);
            } else {
                commonLogic.registerEntityEvent(EVENT_EVAL_UPDATE, evaluation);
                // call logic to manage Quartz scheduled jobs
                evalJobLogic.processEvaluationStateChange(evaluation.getId(), EvalJobLogic.ACTION_UPDATE);
            }
        }

        // support for autoUse insertion of items on eval creation
        if (created) {
            if (!EvalUtils.isBlank(evaluation.getAutoUseInsertion())
                    && !EvalUtils.isBlank(evaluation.getAutoUseTag())) {
                // the tag and the AutoUseInsertion values are set so we should try to find any autoUse items and add them in
                Long templateId = evaluation.getTemplate().getId();
                authoringService.doAutoUseInsertion(evaluation.getAutoUseTag(), templateId,
                        evaluation.getAutoUseInsertion(), true);
            }
        }

        // effectively we are locking the evaluation when a user replies to it, otherwise the chain can be changed
        if (evaluation.getLocked()) {
            // lock evaluation and associated template
            LOG.info("Locking evaluation (" + evaluation.getId() + ") and associated template ("
                    + evaluation.getTemplate().getId() + ")");
            dao.lockEvaluation(evaluation, true);
        }

    }

    /* (non-Javadoc)
     * @see org.sakaiproject.evaluation.logic.EvalEvaluationSetupService#deleteEvaluation(java.lang.Long, java.lang.String)
     */
    public void deleteEvaluation(Long evaluationId, String userId) {
        LOG.debug("evalId: " + evaluationId + ",userId: " + userId);

        EvalEvaluation evaluation = evaluationService.getEvaluationById(evaluationId);
        if (evaluation == null) {
            LOG.warn("Cannot find evaluation to delete with id: " + evaluationId);
            return;
        }

        if (securityChecks.canUserRemoveEval(userId, evaluation)) {

            EvalEmailTemplate available = evaluation.getAvailableEmailTemplate();
            EvalEmailTemplate reminder = evaluation.getReminderEmailTemplate();

            // unlock the evaluation (this will clear the other locks)
            dao.lockEvaluation(evaluation, false);

            boolean removeTemplate = false;
            // check for related responses and answers
            List<Long> responseIds = dao.getResponseIds(evaluation.getId(), null, null, null);
            if (responseIds.size() > 0) {
                // cannot remove this evaluation or assignments, there are responses, we will just set the state to deleted
                evaluation.setState(EvalConstants.EVALUATION_STATE_DELETED);
                // clear email templates
                evaluation.setAvailableEmailTemplate(null);
                evaluation.setReminderEmailTemplate(null);
                // save the new state
                dao.save(evaluation);

                // old method was to actually remove data
                // dao.removeResponses( responseIds.toArray(new Long[responseIds.size()]) );
            } else {
                // no responses so cleanup all the assignments
                // remove associated AssignGroups
                List<EvalAssignGroup> acs = dao.findBySearch(EvalAssignGroup.class,
                        new Search("evaluation.id", evaluationId));
                Set<EvalAssignGroup> assignGroupSet = new HashSet<>(acs);
                dao.deleteSet(assignGroupSet);

                // remove associated assigned hierarchy nodes
                List<EvalAssignHierarchy> ahs = dao.findBySearch(EvalAssignHierarchy.class,
                        new Search("evaluation.id", evaluationId));
                Set<EvalAssignHierarchy> assignHierSet = new HashSet<>(ahs);
                dao.deleteSet(assignHierSet);

                // remove associated assigned users
                List<EvalAssignUser> eus = dao.findBySearch(EvalAssignUser.class,
                        new Search("evaluation.id", evaluationId));
                Set<EvalAssignUser> eusSet = new HashSet<>(eus);
                dao.deleteSet(eusSet);

                // remove the evaluation and copied template since there are no responses
                removeTemplate = true;
                dao.delete(evaluation);
            }

            // fire the evaluation deleted event
            commonLogic.registerEntityEvent(EVENT_EVAL_DELETE, evaluation);

            // remove any remaining scheduled jobs
            evalJobLogic.processEvaluationStateChange(evaluationId, EvalJobLogic.ACTION_DELETE);

            // this has to be after the removal of the evaluation

            // remove associated unused email templates
            Set<EvalEmailTemplate> emailSet = new HashSet<>();
            if (available != null) {
                if (available.getDefaultType() == null) {
                    // only remove non-default templates
                    long evalsUsingTemplate = dao.countBySearch(EvalEvaluation.class,
                            new Search("availableEmailTemplate.id", available.getId()));
                    if (evalsUsingTemplate <= 1l) {
                        // template was only used in this evaluation
                        emailSet.add(available);
                    }
                }
            }
            if (reminder != null) {
                if (reminder.getDefaultType() == null) {
                    long evalsUsingTemplate = dao.countBySearch(EvalEvaluation.class,
                            new Search("reminderEmailTemplate.id", reminder.getId()));
                    if (evalsUsingTemplate <= 1l) {
                        // template was only used in this evaluation
                        emailSet.add(reminder);
                    }
                }
            }
            dao.deleteSet(emailSet);

            if (removeTemplate) {
                // remove template if it is a copy
                if (EvalUtils.checkStateAfter(evaluation.getState(), EvalConstants.EVALUATION_STATE_PARTIAL,
                        false)) {
                    // this is not partial (partials do not have copies made yet)
                    // remove the associated template if it is a copy (it should be)
                    EvalTemplate template;
                    if (evaluation.getTemplate() != null || evaluation.getTemplate().getId() != null) {
                        // there is a template so get it and check to see if it needs to be removed
                        template = authoringService.getTemplateById(evaluation.getTemplate().getId());
                        if (template.getCopyOf() != null || template.isHidden() == true) {
                            // this is a copy so remove it and all children
                            if (securityChecks.checkUserControlTemplate(userId, template)) {
                                authoringService.deleteTemplate(template.getId(), userId);
                            } else {
                                LOG.warn("Could not remove the template (" + template.getId()
                                        + ") associated with this " + "eval (" + evaluationId
                                        + ") since this user has no permission, continuing to remove evaluation anyway");
                            }
                        }
                    }
                }
            }

            LOG.info("User (" + userId + ") removed evaluation (" + evaluationId + "), title: "
                    + evaluation.getTitle());
            return;
        }

        // should not get here so die if we do
        throw new RuntimeException("User (" + userId + ") could NOT delete evaluation (" + evaluationId + ")");
    }

    /* (non-Javadoc)
     * @see org.sakaiproject.evaluation.logic.EvalEvaluationSetupService#closeEvaluation(java.lang.Long, java.lang.String)
     */
    public EvalEvaluation closeEvaluation(Long evaluationId, String userId) {
        EvalEvaluation evaluation = evaluationService.getEvaluationById(evaluationId);
        if (evaluation == null) {
            throw new IllegalArgumentException("Invalid evaluation id, cannot find evaluation: " + evaluationId);
        }

        String evalState = evaluationService.returnAndFixEvalState(evaluation, true);
        if (EvalUtils.checkStateBefore(evalState, EvalConstants.EVALUATION_STATE_CLOSED, false)) {
            // set closing date to now
            Calendar cal = new GregorianCalendar();
            cal.setTime(new Date());
            cal.add(Calendar.SECOND, -1);
            Date now = cal.getTime();
            evaluation.forceDueDate(now);

            // fix stop and view dates if needed
            evaluation.setStopDate(null);
            if (evaluation.getViewDate() != null && evaluation.getViewDate().before(now)) {
                evaluation.setViewDate(null);
            }

            // fix up state (should go to closed)
            evaluationService.returnAndFixEvalState(evaluation, false);

            // save evaluation (should also update the email sending)
            saveEvaluation(evaluation, userId, false);

            // fire the evaluation closed event
            commonLogic.registerEntityEvent(EVENT_EVAL_CLOSED, evaluation);
        } else {
            LOG.warn(userId + " tried to close eval that is already closed (" + evaluationId + "): "
                    + evaluation.getTitle());
        }

        return evaluation;
    }

    /* (non-Javadoc)
     * @see org.sakaiproject.evaluation.logic.EvalEvaluationSetupService#getVisibleEvaluationsForUser(java.lang.String, boolean, boolean, boolean)
     */
    public List<EvalEvaluation> getVisibleEvaluationsForUser(String userId, boolean recentOnly,
            boolean showNotOwned, boolean includePartial) {
        return getVisibleEvaluationsForUser(userId, recentOnly, showNotOwned, includePartial, 0);
    }

    /* (non-Javadoc)
     * @see org.sakaiproject.evaluation.logic.EvalEvaluationSetupService#getVisibleEvaluationsForUser(java.lang.String, boolean, boolean, boolean)
     */
    public List<EvalEvaluation> getVisibleEvaluationsForUser(String userId, boolean recentOnly,
            boolean showNotOwned, boolean includePartial, int maxAgeToDisplay) {

        Date recentClosedDate = null;
        if (recentOnly) {
            // only get recently closed evals, check system setting to get "recent" value
            Integer recentlyClosedDays = (Integer) settings.get(EvalSettings.EVAL_RECENTLY_CLOSED_DAYS);
            if (recentlyClosedDays == null) {
                recentlyClosedDays = 10;
            }
            Calendar calendar = GregorianCalendar.getInstance();
            calendar.add(Calendar.DATE, -1 * recentlyClosedDays);
            recentClosedDate = calendar.getTime();
        }

        // Limit the recentClosedDate if necessary based on maxAgeToDisplay (in months). This will
        // only come into play for the "My Evaluations" screen for the purpose of limiting closed evals.
        if (maxAgeToDisplay != 0) {
            Calendar calendar = GregorianCalendar.getInstance();
            calendar.add(Calendar.MONTH, -1 * maxAgeToDisplay); // subtracts maxAgeToDisplay from months
            recentClosedDate = calendar.getTime();
        }

        String[] evalGroupIds = null;
        if (commonLogic.isUserAdmin(userId) || commonLogic.isUserReadonlyAdmin(userId)) {
            // null out the userId so we get all evaluations
            userId = null;
        } else {
            if (showNotOwned) {
                // get the list of all assignments where this user is instructor
                List<EvalAssignUser> userEvaluateeAssignments = evaluationService.getParticipantsForEval(null,
                        userId, null, EvalAssignUser.TYPE_EVALUATEE, null, null, null);
                Set<String> egidSet = EvalUtils.getGroupIdsFromUserAssignments(userEvaluateeAssignments);
                if (!egidSet.isEmpty()) {
                    // create array of all assigned groupIds where this user is instructor
                    evalGroupIds = egidSet.toArray(new String[egidSet.size()]);
                }
            }
        }

        List<EvalEvaluation> l = dao.getEvaluationsForOwnerAndGroups(userId, evalGroupIds, recentClosedDate, 0, 0,
                includePartial);
        return l;
    }

    /* (non-Javadoc)
     * @see edu.vt.sakai.evaluation.logic.EvalEvaluationsLogic#getEvaluationsForUser(java.lang.String, boolean, boolean)
     */
    public List<EvalEvaluation> getEvaluationsForUser(String userId, Boolean activeOnly, Boolean untakenOnly,
            Boolean includeAnonymous) {
        if (userId == null) {
            throw new IllegalArgumentException("userId must be set");
        }

        String[] evalGroupIds = getGroupIdsForUserToEvaluate(userId, (activeOnly != null));

        // get the evaluations
        List<EvalEvaluation> evals = dao.getEvaluationsByEvalGroups(evalGroupIds, activeOnly, true,
                includeAnonymous, 0, 0);

        if (evals.size() > 0) {
            // filter out taken/untaken if desired
            if (untakenOnly != null) {
                // create an array of the evaluation ids
                Long[] evalIds = new Long[evals.size()];
                for (int j = 0; j < evals.size(); j++) {
                    evalIds[j] = evals.get(j).getId();
                }

                // now get the responses for all the returned evals
                List<EvalResponse> l = dao.findBySearch(EvalResponse.class, new Search(new Restriction[] {
                        new Restriction("owner", userId), new Restriction("evaluation.id", evalIds) }));

                // Iterate through and remove the evals this user already took
                for (int i = 0; i < l.size(); i++) {
                    Long evalIdTaken = l.get(i).getEvaluation().getId();
                    for (int j = 0; j < evals.size(); j++) {
                        if (evalIdTaken.equals(evals.get(j).getId())) {
                            if (untakenOnly) {
                                // filter out the evaluations this user already took
                                evals.remove(j);
                            }
                        } else {
                            if (!untakenOnly) {
                                // filter out the evaluations this user hasn't taken
                                evals.remove(j);
                            }
                        }
                    }
                }
            }
        }

        return evals;
    }

    /* (non-Javadoc)
     * @see org.sakaiproject.evaluation.logic.EvalEvaluationSetupService#getEvaluationsForEvaluatee(java.lang.String, java.lang.Boolean)
     */
    public List<EvalEvaluation> getEvaluationsForEvaluatee(String userId, Boolean includeRecentlyClosed) {
        if (userId == null) {
            throw new IllegalArgumentException("userId must be set");
        }

        // TODO Shouldn't this actually get the list of groups based on the set of assigned eval groups?
        List<EvalGroup> evalGroups = commonLogic.getEvalGroupsForUser(userId, EvalConstants.PERM_BE_EVALUATED);
        String[] evalGroupIds = new String[evalGroups.size()];
        int i = 0;
        for (EvalGroup evalGroup : evalGroups) {
            evalGroupIds[i++] = evalGroup.evalGroupId;
        }
        List<EvalEvaluation> evals = dao.getEvaluationsByEvalGroups(evalGroupIds, null, null, null, 0, 0);
        // date calculations for recently closed
        Date today = new Date();
        Integer recentlyClosedDays = (Integer) settings.get(EvalSettings.EVAL_EVALUATEE_RECENTLY_CLOSED_DAYS);
        int recentlyClosedHours = recentlyClosedDays * 24;

        for (Iterator<EvalEvaluation> iterator = evals.iterator(); iterator.hasNext();) {
            EvalEvaluation evaluation = iterator.next();
            // fix up the states to ensure they are good
            evaluationService.returnAndFixEvalState(evaluation, true);
            // handle filtering
            if (includeRecentlyClosed != null) {
                // not null so filter (NOTE: if null then just include them all)
                if (includeRecentlyClosed) {
                    // filter out evals older than recently closed
                    int hoursDiff = EvalUtils.getHoursDifference(evaluation.getDueDate(), today);
                    if (hoursDiff > recentlyClosedHours) {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("Dropping Evaluatee eval which is not recently closed: " + evaluation.getId()
                                    + ", due=" + evaluation.getDueDate());
                        }
                        iterator.remove();
                    }
                } else {
                    // filter out all closed evals
                    if (EvalUtils.checkStateAfter(evaluation.getState(), EvalConstants.EVALUATION_STATE_CLOSED,
                            true)) {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("Dropping Evaluatee eval which is closed: " + evaluation.getId());
                        }
                        iterator.remove();
                    }
                }
            }
        }
        // populate the assign groups and eval groups in the evals
        populateEvaluationsGroups(evals, evalGroups);
        return evals;
    }

    /**
     * Special method to populate the assign groups and eval groups non-persistent fields in the evaluation objects
     * 
     * @param evaluations the list of evaluations to populate the groups in (does nothing if this is empty)
     * @param limitEvalGroups if null, populate all assigned groups,
     *                        if empty, populate no assigned groups,
     *                        otherwise, populate only the groups which are assigned and also in this list
     */
    protected void populateEvaluationsGroups(List<EvalEvaluation> evaluations, List<EvalGroup> limitEvalGroups) {
        if (!evaluations.isEmpty()) {
            Long[] evaluationIds = new Long[evaluations.size()];
            int j = 0;
            for (EvalEvaluation eval : evaluations) {
                evaluationIds[j++] = eval.getId();
            }
            Map<Long, List<EvalAssignGroup>> agsMap = evaluationService.getAssignGroupsForEvals(evaluationIds, true,
                    null);
            // now populate the evaluations with the AGs and EGs (or set them to empty lists)
            for (EvalEvaluation eval : evaluations) {
                List<EvalAssignGroup> assignGroups = agsMap.get(eval.getId());
                if (assignGroups != null && !assignGroups.isEmpty()) {
                    if (limitEvalGroups == null) {
                        // include all
                        eval.setEvalAssignGroups(assignGroups);
                        // does not populate the eval groups
                    } else if (limitEvalGroups.isEmpty()) {
                        // include none
                        eval.setEvalAssignGroups(new ArrayList<>(0));
                        eval.setEvalGroups(new ArrayList<>(0));
                    } else {
                        // include the groups passed in only
                        List<EvalAssignGroup> evaluationAssignGroups = new ArrayList<>();
                        List<EvalGroup> evaluationGroups = new ArrayList<>();
                        for (EvalGroup eg : limitEvalGroups) {
                            for (EvalAssignGroup eag : assignGroups) {
                                if (eag.getEvalGroupId().equals(eg.evalGroupId)) {
                                    evaluationAssignGroups.add(eag);
                                    evaluationGroups.add(eg);
                                    break;
                                }
                            }
                        }
                        eval.setEvalAssignGroups(evaluationAssignGroups);
                        eval.setEvalGroups(evaluationGroups);
                    }
                } else {
                    eval.setEvalAssignGroups(new ArrayList<>(0));
                    eval.setEvalGroups(new ArrayList<>(0));
                }
            }
        }
    }

    /**
     * Method which retrieves the groupIds for all groups in which this user has
     * an assignment of type evaluator
     * @param userId the userId of the user (must not be null)
     * @param activeOnly if true then only include active ones, otherwise include all
     * @return the array of all groupIds OR null if the user has permission in none
     */
    private String[] getGroupIdsForUserToEvaluate(String userId, boolean activeOnly) {
        String[] evalGroupIds = null;
        List<EvalAssignUser> userAssignments;

        if (activeOnly) {

            userAssignments = evaluationService.getParticipantsForEval(null, userId, null,
                    EvalAssignUser.TYPE_EVALUATOR, null, null, EvalConstants.EVALUATION_STATE_ACTIVE);

            userAssignments.addAll(evaluationService.getParticipantsForEval(null, userId, null,
                    EvalAssignUser.TYPE_EVALUATOR, null, null, EvalConstants.EVALUATION_STATE_GRACEPERIOD));
        } else {

            userAssignments = evaluationService.getParticipantsForEval(null, userId, null,
                    EvalAssignUser.TYPE_EVALUATOR, null, null, null);
        }

        Set<String> egidSet = EvalUtils.getGroupIdsFromUserAssignments(userAssignments);
        if (!egidSet.isEmpty()) {
            // create array of all assigned groupIds where this user is instructor
            evalGroupIds = egidSet.toArray(new String[egidSet.size()]);
        }
        return evalGroupIds;
    }

    // CATEGORIES

    public String[] getEvalCategories(String userId) {
        LOG.debug("userId: " + userId);

        // return all current categories or only return categories created by this user if not null
        List<String> l = dao.getEvalCategories(userId);
        return (String[]) l.toArray(new String[] {});
    }

    public List<EvalEvaluation> getEvaluationsByCategory(String evalCategory, String userId) {
        LOG.debug("evalCategory: " + evalCategory + ", userId: " + userId);

        if (evalCategory == null || evalCategory.equals("")) {
            throw new IllegalArgumentException("evalCategory cannot be blank or null");
        }

        List<EvalEvaluation> evals = new ArrayList<>();
        if (userId == null) {
            // get all evals for a category
            evals = dao.findBySearch(EvalEvaluation.class,
                    new Search(new Restriction("evalCategory", evalCategory), new Order("startDate")));
        } else {
            // get all evals for a specific user for a category
            //List takeGroups = commonLogic.getEvalGroupsForUser(userId, EvalConstants.PERM_TAKE_EVALUATION);
            String[] evalGroupIds = getGroupIdsForUserToEvaluate(userId, true);

            // this sucks for efficiency -AZ
            List<EvalEvaluation> l = dao.getEvaluationsByEvalGroups(evalGroupIds, true, true, true, 0, 0); // only get active for users
            for (EvalEvaluation evaluation : l) {
                if (evalCategory.equals(evaluation.getEvalCategory())) {
                    evals.add(evaluation);
                }
            }
        }
        return evals;
    }

    // USER ASSIGNMENTS

    public void deleteUserAssignments(Long evaluationId, Long... userAssignmentIds) {
        if (evaluationId == null) {
            throw new IllegalArgumentException("evaluationId must be set");
        }
        if (userAssignmentIds != null && userAssignmentIds.length > 0) {
            // get an eval from the id
            EvalEvaluation eval = getEvaluationOrFail(evaluationId);
            // check permissions
            if (securityChecks.checkRemoveAssignments(null, null, eval)) {
                dao.deleteSet(EvalAssignUser.class, userAssignmentIds);
            }
        }
    }

    public void saveUserAssignments(Long evaluationId, EvalAssignUser... assignUsers) {
        if (evaluationId == null) {
            throw new IllegalArgumentException("evaluationId must be set");
        }
        if (assignUsers != null && assignUsers.length > 0) {
            // get an eval from the id
            EvalEvaluation eval = getEvaluationOrFail(evaluationId);
            saveEvalAssignUsers(eval, assignUsers);
        }
    }

    public List<Long> synchronizeUserAssignments(Long evaluationId, String evalGroupId) {
        if (evaluationId == null) {
            throw new IllegalArgumentException("evaluationId must be set");
        }
        // quick eval state check
        EvalEvaluation eval = getEvaluationOrFail(evaluationId);
        String state = EvalUtils.getEvaluationState(eval, false);
        if (EvalUtils.checkStateAfter(state, EvalConstants.EVALUATION_STATE_ACTIVE, false)) {
            throw new IllegalStateException(
                    "Cannot synchronize evaluation (" + evaluationId + ") which is in the " + state + " state");
        }
        boolean removeAllowed = false;
        if (EvalUtils.checkStateBefore(state, EvalConstants.EVALUATION_STATE_ACTIVE, false)) {
            removeAllowed = true;
        }
        return synchronizeUserAssignmentsForced(eval, evalGroupId, removeAllowed);
    }

    /**
     * Special method which skips the evaluation state checks,
     * this is mostly needed to allow us to force an update at any time <br/>
     * Synchronizes all the user assignments with the assigned groups for this evaluation
     * <br/> Always run as an admin for permissions handling
     * 
     * @param evaluation the evaluation to do assignment updates for
     * @param evalGroupId (OPTIONAL) the internal group id of an eval group,
     * this will cause the synchronize to only affect the assignments related to this group
     * @param removeAllowed if true then will remove assignments as well, otherwise only adds
     * @return the list of {@link EvalAssignUser} ids changed during the synchronization (created, updated, deleted),
     * NOTE: deleted {@link EvalAssignUser} will not be able to be retrieved
     */
    public List<Long> synchronizeUserAssignmentsForced(EvalEvaluation evaluation, String evalGroupId,
            boolean removeAllowed) {
        Long evaluationId = evaluation.getId();
        String currentUserId = commonLogic.getCurrentUserId();
        if (currentUserId == null) {
            currentUserId = commonLogic.getAdminUserId();
        } else {
            // check anon and use admin instead
            EvalUser user = commonLogic.getEvalUserById(currentUserId);
            if (EvalUser.USER_TYPE_ANONYMOUS.equals(user.type) || EvalUser.USER_TYPE_INVALID.equals(user.type)
                    || EvalUser.USER_TYPE_UNKNOWN.equals(user.type)) {
                currentUserId = commonLogic.getAdminUserId();
            }
        }
        ArrayList<Long> changedUserAssignments = new ArrayList<>();
        // now the syncing logic
        HashSet<Long> assignUserToRemove = new HashSet<>();
        HashSet<EvalAssignUser> assignUserToSave = new HashSet<>();
        // get all user assignments for this evaluation (and possibly limit by group)
        String[] limitGroupIds = null;
        if (evalGroupId != null) {
            limitGroupIds = new String[] { evalGroupId };
        }
        // all users assigned to this eval (and group if specified)
        List<EvalAssignUser> assignedUsers = evaluationService.getParticipantsForEval(evaluationId, null,
                limitGroupIds, null, EvalEvaluationService.STATUS_ANY, null, null);
        // keys of all assignments which are unlinked or removed
        HashSet<String> assignUserUnlinkedRemovedKeys = new HashSet<>();
        // all assignments which are linked (groupId => assignments)
        HashMap<String, List<EvalAssignUser>> groupIdLinkedAssignedUsersMap = new HashMap<>();
        for (EvalAssignUser evalAssignUser : assignedUsers) {
            if (EvalAssignUser.STATUS_UNLINKED.equals(evalAssignUser.getStatus())
                    || EvalAssignUser.STATUS_REMOVED.equals(evalAssignUser.getStatus())) {
                String key = makeEvalAssignUserKey(evalAssignUser, false, false);
                assignUserUnlinkedRemovedKeys.add(key);
            }
            String egid = evalAssignUser.getEvalGroupId();
            if (egid != null) {
                if (EvalAssignUser.STATUS_LINKED.equals(evalAssignUser.getStatus())) {
                    List<EvalAssignUser> l = groupIdLinkedAssignedUsersMap.get(egid);
                    if (l == null) {
                        l = new ArrayList<>();
                        groupIdLinkedAssignedUsersMap.put(egid, l);
                    }
                    l.add(evalAssignUser);
                }
            }
        }
        List<EvalAssignGroup> assignedGroups;
        if (evalGroupId == null) {
            // get all the assigned groups for this evaluation
            Map<Long, List<EvalAssignGroup>> m = evaluationService
                    .getAssignGroupsForEvals(new Long[] { evaluationId }, true, null);
            assignedGroups = m.get(evaluationId);
        } else {
            // only dealing with a single assign group (or possibly none if invalid)
            assignedGroups = new ArrayList<>();
            EvalAssignGroup assignGroup = evaluationService.getAssignGroupByEvalAndGroupId(evaluationId,
                    evalGroupId);
            if (assignGroup != null) {
                assignedGroups.add(assignGroup);
            }
        }
        // iterate through all assigned groups (may have been limited to one only)
        Set<String> evalGroupIdsFromEvals = new HashSet<>(assignedGroups.size());
        for (EvalAssignGroup evalAssignGroup : assignedGroups) {
            Long assignGroupId = evalAssignGroup.getId();
            String egid = evalAssignGroup.getEvalGroupId();
            evalGroupIdsFromEvals.add(egid);
            // get all the users who currently have permission for this group
            Set<String> currentEvaluated = commonLogic.getUserIdsForEvalGroup(egid, EvalConstants.PERM_BE_EVALUATED,
                    evaluation.getSectionAwareness());
            Set<String> currentAssistants = commonLogic.getUserIdsForEvalGroup(egid,
                    EvalConstants.PERM_ASSISTANT_ROLE, evaluation.getSectionAwareness());
            Set<String> currentTakers = commonLogic.getUserIdsForEvalGroup(egid, EvalConstants.PERM_TAKE_EVALUATION,
                    evaluation.getSectionAwareness());

            if (evaluation.getAllRolesParticipate()) {
                currentTakers.addAll(currentAssistants);
                currentTakers.addAll(currentEvaluated);
            }

            HashSet<String> currentAll = new HashSet<>();
            currentAll.addAll(currentEvaluated);
            currentAll.addAll(currentAssistants);
            currentAll.addAll(currentTakers);
            /* Resolve the current permissions against the existing assignments,
             * this should only change linked records but should respect unlinked and removed records by not
             * adding a record where one already exists for the given user/group combo,
             * any linked records which do not exist anymore should be trashed if the status is right,
             * any missing records should be added if the evaluation is still active or better
             */
            List<EvalAssignUser> linkedUserAssignsInThisGroup = groupIdLinkedAssignedUsersMap.get(egid);
            if (linkedUserAssignsInThisGroup == null) {
                // this group has not been assigned yet
                linkedUserAssignsInThisGroup = new ArrayList<>();
            }
            // filter out all linked user assignments which match exactly with the existing ones
            for (Iterator<EvalAssignUser> iterator = linkedUserAssignsInThisGroup.iterator(); iterator.hasNext();) {
                EvalAssignUser evalAssignUser = iterator.next();
                String type = evalAssignUser.getType();
                String userId = evalAssignUser.getUserId();
                if (EvalAssignUser.TYPE_EVALUATEE.equals(type)) {
                    if (currentEvaluated.contains(userId)) {
                        currentEvaluated.remove(userId);
                        iterator.remove();
                    }
                } else if (EvalAssignUser.TYPE_ASSISTANT.equals(type)) {
                    if (currentAssistants.contains(userId)) {
                        currentAssistants.remove(userId);
                        iterator.remove();
                    }
                } else if (EvalAssignUser.TYPE_EVALUATOR.equals(type)) {
                    if (currentTakers.contains(userId)) {
                        currentTakers.remove(userId);
                        iterator.remove();
                    }
                } else {
                    throw new IllegalStateException("Do not recognize this user assignment type: " + type);
                }
            }
            // any remaining linked user assignments should be removed if not unlinked
            for (EvalAssignUser evalAssignUser : linkedUserAssignsInThisGroup) {
                if (evalAssignUser.getId() != null) {
                    String key = makeEvalAssignUserKey(evalAssignUser, false, false);
                    if (!assignUserUnlinkedRemovedKeys.contains(key)) {
                        assignUserToRemove.add(evalAssignUser.getId());
                    }
                }
            }
            // any remaining current set items need to be added if they are not unlinked/removed
            assignUserToSave.addAll(makeUserAssignmentsFromUserIdSet(currentEvaluated, egid,
                    EvalAssignUser.TYPE_EVALUATEE, assignGroupId, assignUserUnlinkedRemovedKeys));
            assignUserToSave.addAll(makeUserAssignmentsFromUserIdSet(currentAssistants, egid,
                    EvalAssignUser.TYPE_ASSISTANT, assignGroupId, assignUserUnlinkedRemovedKeys));
            assignUserToSave.addAll(makeUserAssignmentsFromUserIdSet(currentTakers, egid,
                    EvalAssignUser.TYPE_EVALUATOR, assignGroupId, assignUserUnlinkedRemovedKeys));
        }

        // now handle the actual persistent updates and log them
        String message = "Synchronized user assignments for eval (" + evaluationId + ") with "
                + assignedGroups.size() + " assigned groups";
        if (assignUserToRemove.isEmpty() && assignUserToSave.isEmpty()) {
            message += ": no changes to the user assignments (" + assignedUsers.size() + ")";
        } else {
            if (removeAllowed && !assignUserToRemove.isEmpty()) {
                Long[] assignUserToRemoveArray = assignUserToRemove.toArray(new Long[assignUserToRemove.size()]);
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Deleting user eval assignment Ids: " + assignUserToRemove);
                }
                dao.deleteSet(EvalAssignUser.class, assignUserToRemoveArray);
                message += ": removed the following assignments: " + assignUserToRemove;
                changedUserAssignments.addAll(assignUserToRemove);
            }
            if (!assignUserToSave.isEmpty()) {
                for (EvalAssignUser evalAssignUser : assignUserToSave) {
                    setAssignUserDefaults(evalAssignUser, evaluation, currentUserId);
                }
                // this is meant to force the assigned users set to be re-calculated
                assignUserToSave = new HashSet<>(assignUserToSave);
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Saving user eval assignments: " + assignUserToSave);
                }
                dao.saveSet(assignUserToSave);
                message += ": created the following assignments: " + assignUserToSave;
                for (EvalAssignUser evalAssignUser : assignUserToSave) {
                    changedUserAssignments.add(evalAssignUser.getId());
                }
            }
        }

        // one more specialty check to cleanup orphaned user assignments - EVALSYS-703
        Set<String> evalGroupIdsFromUsers = groupIdLinkedAssignedUsersMap.keySet();
        evalGroupIdsFromUsers.removeAll(evalGroupIdsFromEvals);
        if (!evalGroupIdsFromUsers.isEmpty()) {
            // there are users assigned to group ids in this eval which are not part of the assigned groups
            HashSet<Long> orphanedUserAssignments = new HashSet<>();
            for (EvalAssignUser evalAssignUser : assignedUsers) {
                String egid = evalAssignUser.getEvalGroupId();
                if (egid != null && evalGroupIdsFromUsers.contains(egid)) {
                    if (EvalAssignUser.STATUS_LINKED.equals(evalAssignUser.getStatus())) {
                        orphanedUserAssignments.add(evalAssignUser.getId());
                    }
                }
            }
            if (!orphanedUserAssignments.isEmpty()) {
                Long[] orphanedUserAssignmentsArray = orphanedUserAssignments
                        .toArray(new Long[orphanedUserAssignments.size()]);
                dao.deleteSet(EvalAssignUser.class, orphanedUserAssignmentsArray);
                message += ": removed the following orphaned user assignments: " + orphanedUserAssignments;
                changedUserAssignments.addAll(orphanedUserAssignments);
            }
        }

        LOG.info(message);
        return changedUserAssignments;
    }

    /**
     * Creates the EvalAssignUsers based on userIds, evalGroupId, type,
     * will only create ones which do not have a matching key in matchingUserGroupKeys 
     * @return the set of newly created EvalAssignUsers
     */
    private Set<EvalAssignUser> makeUserAssignmentsFromUserIdSet(Set<String> userIds, String evalGroupId,
            String typeConstant, Long assignGroupId, Set<String> matchingUserGroupKeys) {
        HashSet<EvalAssignUser> eaus = new HashSet<>();
        for (String userId : userIds) {
            EvalAssignUser evalAssignUser = new EvalAssignUser(userId, evalGroupId, null, typeConstant,
                    EvalAssignUser.STATUS_LINKED);
            evalAssignUser.setAssignGroupId(assignGroupId);
            String key = makeEvalAssignUserKey(evalAssignUser, false, false);
            if (!matchingUserGroupKeys.contains(key)) {
                eaus.add(evalAssignUser);
            }
        }
        return eaus;
    }

    /**
     * Makes a mapping key which will allow EvalAssignUser to be placed into a map
     * @param evalAssignUser the EAU to make the key from, should not be null
     * @param includeEvaluationId if true then includes the evalId in the key, otherwise it is not part of it
     * @param includeType if true then includes the type in the key, otherwise it is not part of it
     * @return the key (will not be null)
     */
    private String makeEvalAssignUserKey(EvalAssignUser evalAssignUser, boolean includeType,
            boolean includeEvaluationId) {
        if (evalAssignUser == null) {
            throw new IllegalArgumentException("evalAssignUser cannot be null");
        }
        StringBuilder sb = new StringBuilder();
        sb.append(evalAssignUser.getUserId());
        sb.append(":");
        sb.append(evalAssignUser.getEvalGroupId());
        if (includeType) {
            sb.append(":");
            sb.append(evalAssignUser.getType());
        }
        if (includeEvaluationId && evalAssignUser.getEvaluationId() != null) {
            sb.append(":");
            sb.append(evalAssignUser.getEvaluationId());
        }
        return sb.toString();
    }

    /**
     * This method can be called internally to avoid looking up the evaluation again,
     * will check to ensure it does not recreate an existing user assignment
     * @param eval
     * @param assignUsers
     */
    protected void saveEvalAssignUsers(EvalEvaluation eval, EvalAssignUser... assignUsers) {
        // check permissions
        if (securityChecks.checkCreateAssignments(null, eval)) {
            String currentUserId = commonLogic.getCurrentUserId();
            // create the set of user assignments for this eval
            List<EvalAssignUser> assignedUsers = evaluationService.getParticipantsForEval(eval.getId(), null, null,
                    null, EvalEvaluationService.STATUS_ANY, null, null);
            HashMap<String, EvalAssignUser> assignedKeysToEAUs = new HashMap<>();
            for (EvalAssignUser evalAssignUser : assignedUsers) {
                String key = makeEvalAssignUserKey(evalAssignUser, true, false);
                assignedKeysToEAUs.put(key, evalAssignUser);
            }

            // now create the set of all assignments to save (only save the ones that do not already exist though)
            HashSet<EvalAssignUser> eauSet = new HashSet<>();
            for (EvalAssignUser evalAssignUser : assignUsers) {
                evalAssignUser.setLastModified(new Date());
                // switch over to unlinked if the status changes
                if (evalAssignUser.typeChanged) {
                    evalAssignUser.setStatus(EvalAssignUser.STATUS_UNLINKED);
                }
                setAssignUserDefaults(evalAssignUser, eval, currentUserId);
                String key = makeEvalAssignUserKey(evalAssignUser, true, false);
                if (assignedKeysToEAUs.containsKey(key)) {
                    EvalAssignUser existing = assignedKeysToEAUs.get(key);
                    if (!existing.getId().equals(evalAssignUser.getId())) {
                        // trying to save an assignment over top of one that exists already
                        LOG.warn("Found an user assignment that matches an existing one so it will not be saved: "
                                + evalAssignUser);
                        continue; // SKIP
                    }
                }
                eauSet.add(evalAssignUser);
            }
            // save all of the user assignments
            dao.saveSet(eauSet);
        }
    }

    /**
     * Set the default values on assign user objects before they are saved
     * @param evalAssignUser
     * @param eval
     * @param currentUserId
     * @throws IllegalArgumentException if inputs are null
     */
    protected void setAssignUserDefaults(EvalAssignUser evalAssignUser, EvalEvaluation eval, String currentUserId) {
        if (evalAssignUser == null || eval == null || currentUserId == null) {
            throw new IllegalArgumentException("inputs cannot be null");
        }
        evalAssignUser.setEvaluation(eval);
        if (evalAssignUser.getOwner() == null || "".equals(evalAssignUser.getOwner())) {
            evalAssignUser.setOwner(currentUserId);
        }
        // link to the assign groups based on the groupId
        if (evalAssignUser.getAssignGroupId() == null && evalAssignUser.getEvalGroupId() != null) {
            String evalGroupId = evalAssignUser.getEvalGroupId();
            Long evaluationId = eval.getId();
            EvalAssignGroup assignGroup = evaluationService.getAssignGroupByEvalAndGroupId(evaluationId,
                    evalGroupId);
            if (assignGroup != null) {
                evalAssignUser.setAssignGroupId(assignGroup.getId());
            }
            evalAssignUser.setStatus(EvalAssignUser.STATUS_LINKED);
        }
    }

    // HIERARCHY

    public List<EvalAssignHierarchy> setEvalAssignments(Long evaluationId, String[] nodeIds, String[] evalGroupIds,
            boolean appendMode) {

        // get the evaluation
        EvalEvaluation eval = getEvaluationOrFail(evaluationId);

        if (evalGroupIds == null) {
            evalGroupIds = new String[] {};
        }
        if (nodeIds == null) {
            nodeIds = new String[] {};
        }

        if (evalGroupIds.length == 0 && nodeIds.length == 0
                && !EvalConstants.EVALUATION_AUTHCONTROL_NONE.equals(eval.getAuthControl())) {
            throw new IllegalArgumentException(
                    "Cannot assign an evaluation to 0 nodes and 0 groups, you must pass in at least one node id or group id");
        }

        /*
         * EVALSYS-987
         * create a new ad hoc group if the evaluation has no groups and does 
         * not require authentication 
         */
        if (evalGroupIds.length == 0 && nodeIds.length == 0
                && EvalConstants.EVALUATION_AUTHCONTROL_NONE.equals(eval.getAuthControl())) {
            String adhocGroupTitle = "Public ad-hoc group for " + eval.getTitle();
            EvalAdhocGroup group = new EvalAdhocGroup(commonLogic.getCurrentUserId(), adhocGroupTitle);
            commonLogic.saveAdhocGroup(group);
            group.setEvaluateeIds(new ArrayList<String>(Arrays.asList(commonLogic.getCurrentUserId())));
            commonLogic.saveAdhocGroup(group);
            evalGroupIds = new String[] { group.getEvalGroupId() };
        }

        // check if this evaluation can be modified
        String userId = commonLogic.getCurrentUserId();
        if (securityChecks.checkCreateAssignments(userId, eval)) {

            // first we have to get all the assigned hierarchy nodes for this eval
            Set<String> nodeIdsSet = new HashSet<>();
            Set<String> currentNodeIds = new HashSet<>();

            List<EvalAssignHierarchy> currentAssignHierarchies = evaluationService
                    .getAssignHierarchyByEval(evaluationId);
            for (EvalAssignHierarchy assignHierarchy : currentAssignHierarchies) {
                currentNodeIds.add(assignHierarchy.getNodeId());
            }

            nodeIdsSet.addAll(Arrays.asList(nodeIds));

            if (!appendMode) {
                Set<String> selectedNodeIds = new HashSet<>(nodeIdsSet);
                Set<String> existingNodeIds = new HashSet<>(currentNodeIds);
                existingNodeIds.removeAll(selectedNodeIds);
                // now remove all the nodes remaining in the current set
                if (existingNodeIds.size() > 0) {
                    Long[] removeHierarchyIds = new Long[existingNodeIds.size()];
                    int counter = 0;
                    for (EvalAssignHierarchy assignHierarchy : currentAssignHierarchies) {
                        if (existingNodeIds.contains(assignHierarchy.getNodeId())) {
                            removeHierarchyIds[counter] = assignHierarchy.getId();
                            counter++;
                        }
                    }
                    deleteAssignHierarchyNodesById(removeHierarchyIds);
                }
            }

            // then remove the duplicates so we end up with the filtered list to only new ones
            nodeIdsSet.removeAll(currentNodeIds);
            nodeIds = nodeIdsSet.toArray(new String[] {});

            // now we need to create all the persistent hierarchy assignment objects
            Set<EvalAssignHierarchy> nodeAssignments = new HashSet<>();
            for (String nodeId : nodeIdsSet) {
                // set the settings to null to allow the defaults to override correctly
                EvalAssignHierarchy eah = new EvalAssignHierarchy(userId, nodeId, eval);
                // fill in defaults and the values from the evaluation
                setAssignmentDefaults(eval, eah);
                nodeAssignments.add(eah);
            }

            // next we have to get all the assigned eval groups for this eval
            Set<String> evalGroupIdsSet = new HashSet<>();
            Set<String> currentEvalGroupIds = new HashSet<>();

            // get the current list of assigned eval groups
            Map<Long, List<EvalAssignGroup>> groupsMap = evaluationService
                    .getAssignGroupsForEvals(new Long[] { evaluationId }, true, null);
            List<EvalAssignGroup> currentGroups = groupsMap.get(evaluationId);
            for (EvalAssignGroup evalAssignGroup : currentGroups) {
                currentEvalGroupIds.add(evalAssignGroup.getEvalGroupId());
            }

            evalGroupIdsSet.addAll(Arrays.asList(evalGroupIds));

            if (!appendMode) {
                Set<String> selectedGroupIds = new HashSet<>(evalGroupIdsSet);
                Set<String> existingGroupIds = new HashSet<>();
                for (EvalAssignGroup assignGroup : currentGroups) {
                    if (assignGroup.getNodeId() == null) {
                        existingGroupIds.add(assignGroup.getEvalGroupId());
                    }
                }
                existingGroupIds.removeAll(selectedGroupIds);
                // now remove all the groups remaining in the existing set
                if (existingGroupIds.size() > 0) {
                    Set<EvalAssignGroup> removeAssignGroups = new HashSet<>();
                    for (EvalAssignGroup assignGroup : currentGroups) {
                        if (existingGroupIds.contains(assignGroup.getEvalGroupId())) {
                            removeAssignGroups.add(assignGroup);
                        }
                    }
                    //dao.deleteSet(removeAssignGroups);
                    // Ensure the user assignments are cleaned up - EVALSYS-703
                    for (EvalAssignGroup assignGroup : removeAssignGroups) {
                        deleteAssignGroupInternal(assignGroup);
                    }
                }
            }

            // next we need to expand all the assigned hierarchy nodes into a massive set of eval assign groups
            Set<EvalHierarchyNode> nodes = hierarchyLogic.getNodesByIds(nodeIdsSet.toArray(new String[] {}));
            // expand the actual new set of nodes into a complete list of nodes including children
            Set<String> allNodeIds = hierarchyLogic.getAllChildrenNodes(nodes, true);
            allNodeIds.addAll(currentNodeIds);

            Map<String, Set<String>> allEvalGroupIds = new HashMap<>();
            Set<String> evalGroups;
            Set<String> sectionAwareEvalGroups;

            //For an evaluation section-aware, once all the nodes are retrieved
            //transform the site related evalGroups into section related evalGroups
            //whether the node is rule based or external hierarchy based
            if (eval.getSectionAwareness()) {
                for (String nodeid : allNodeIds) {
                    evalGroups = hierarchyLogic.getEvalGroupsForNode(nodeid);

                    sectionAwareEvalGroups = new HashSet<>();
                    for (String evalGroupId : evalGroups) {
                        for (EvalGroup evalGroup : externalLogic
                                .makeEvalGroupObjectsForSectionAwareness(evalGroupId)) {
                            sectionAwareEvalGroups.add(evalGroup.evalGroupId);
                        }
                    }
                    allEvalGroupIds.put(nodeid, sectionAwareEvalGroups);
                }
            } else {
                //Otherwise just get the evalGroups of the nodes
                allEvalGroupIds = hierarchyLogic.getEvalGroupsForNodes(allNodeIds.toArray(new String[] {}));
            }

            // now eliminate the evalgroupids from the evalGroupIds array which happen to be contained in the nodes,
            // this leaves us with only the group ids which are not contained in the nodes which are already assigned
            for (Set<String> egIds : allEvalGroupIds.values()) {
                evalGroupIdsSet.removeAll(egIds);
            }

            // then remove the eval groups ids which are already assigned to this eval so we only have new ones
            evalGroupIdsSet.removeAll(currentEvalGroupIds);
            evalGroupIds = evalGroupIdsSet.toArray(new String[] {});

            // now we need to create all the persistent group assignment objects for the new groups
            Set<EvalAssignGroup> groupAssignments = new HashSet<>();
            groupAssignments.addAll(makeAssignGroups(eval, userId, evalGroupIdsSet, null));

            // finally we add in the groups for all the new expanded assign groups for the expanded nodes set
            for (String nodeId : allNodeIds) {
                if (allEvalGroupIds.containsKey(nodeId)) {
                    groupAssignments.addAll(makeAssignGroups(eval, userId, allEvalGroupIds.get(nodeId), nodeId));
                }
            }

            // save everything at once
            dao.saveMixedSet(new Set[] { nodeAssignments, groupAssignments });
            LOG.info("User (" + userId + ") added nodes (" + ArrayUtils.arrayToString(nodeIds) + ") and groups ("
                    + ArrayUtils.arrayToString(evalGroupIds) + ") to evaluation (" + evaluationId + ")");
            List<EvalAssignHierarchy> results = new ArrayList<>(nodeAssignments);
            results.addAll(groupAssignments);

            Boolean syncUserAssignmentsOnGroupSave = (Boolean) this.settings
                    .get(EvalSettings.SYNC_USER_ASSIGNMENTS_ON_GROUP_SAVE);
            if (syncUserAssignmentsOnGroupSave == null) {
                // use default, true
                syncUserAssignmentsOnGroupSave = true;
            }
            if (syncUserAssignmentsOnGroupSave) {
                // sync all the user assignments after the groups are saved
                synchronizeUserAssignmentsForced(eval, null, !appendMode);
            }

            return results;
        }

        // should not get here so die if we do
        throw new RuntimeException("User (" + userId + ") could NOT create hierarchy assignments for nodes ("
                + ArrayUtils.arrayToString(nodeIds) + ") in evaluation (" + evaluationId + ")");
    }

    public void deleteAssignHierarchyNodesById(Long... assignHierarchyIds) {
        String userId = commonLogic.getCurrentUserId();
        // get the list of hierarchy assignments
        List<EvalAssignHierarchy> l = dao.findBySearch(EvalAssignHierarchy.class,
                new Search("id", assignHierarchyIds));
        if (l.size() > 0) {
            Set<String> nodeIds = new HashSet<>();
            Long evaluationId = l.get(0).getEvaluation().getId();

            Set<EvalAssignHierarchy> eahs = new HashSet<>();
            for (EvalAssignHierarchy evalAssignHierarchy : l) {
                if (evaluationService.canDeleteAssignGroup(userId, evalAssignHierarchy.getId())) {
                    nodeIds.add(evalAssignHierarchy.getNodeId());
                    eahs.add(evalAssignHierarchy);
                }
            }

            // now get the list of assign groups with a nodeId that matches any of these and remove those also
            List<EvalAssignGroup> eags = dao.findBySearch(EvalAssignGroup.class, new Search(new Restriction[] {
                    new Restriction("evaluation.id", evaluationId), new Restriction("nodeId", nodeIds) }));
            Set<EvalAssignGroup> groups = new HashSet<>();
            StringBuilder groupListing = new StringBuilder();
            if (eags.size() > 0) {
                for (EvalAssignGroup evalAssignGroup : groups) {
                    if (evaluationService.canDeleteAssignGroup(userId, evalAssignGroup.getId())) {
                        groups.add(evalAssignGroup);
                        groupListing.append(evalAssignGroup.getEvalGroupId()).append(":");
                    }
                }
            }

            dao.deleteMixedSet(new Set[] { eahs, groups });
            LOG.info("User (" + userId + ") deleted existing hierarchy assignments ("
                    + ArrayUtils.arrayToString(assignHierarchyIds) + ") and groups (" + groupListing.toString()
                    + ")");

            // sync all the user assignments after the groups are saved
            synchronizeUserAssignments(evaluationId, null);

            return;

        }
        // should not get here so die if we do
        throw new RuntimeException("User (" + userId + ") could NOT delete hierarchy assignments ("
                + ArrayUtils.arrayToString(assignHierarchyIds) + ")");
    }

    // GROUPS

    public void saveAssignGroup(EvalAssignGroup assignGroup, String userId) {
        LOG.debug("userId: " + userId + ", evalGroupId: " + assignGroup.getEvalGroupId());

        // set the date modified
        assignGroup.setLastModified(new Date());

        Long evaluationId = assignGroup.getEvaluation() == null ? assignGroup.getEvaluationId()
                : assignGroup.getEvaluation().getId();
        if (evaluationId == null) {
            throw new IllegalStateException("Evaluation or evaluationId is not set or not saved for assignGroup ("
                    + assignGroup.getId() + "), evalgroupId: " + assignGroup.getEvalGroupId());
        }
        EvalEvaluation eval = getEvaluationOrFail(evaluationId);
        if (assignGroup.getEvaluation() == null) {
            assignGroup.setEvaluation(eval);
        }

        setAssignmentDefaults(eval, assignGroup); // set the group defaults before saving

        if (assignGroup.getId() == null) {
            // creating new AC
            if (securityChecks.checkCreateAssignments(userId, eval)) {
                // check for duplicate AC first
                if (checkRemoveDuplicateAssignGroup(assignGroup)) {
                    throw new IllegalStateException(
                            "Duplicate mapping error, there is already an assignGroup that defines a link from evalGroupId: "
                                    + assignGroup.getEvalGroupId() + " to eval: " + eval.getId());
                }

                dao.save(assignGroup);

                // if a late instructor opt-in, notify students in this group that an evaluation is available
                if (EvalConstants.INSTRUCTOR_OPT_IN.equals(eval.getInstructorOpt())
                        && assignGroup.getInstructorApproval()
                        && assignGroup.getEvaluation().getStartDate().before(new Date())) {
                    emails.sendEvalAvailableGroupNotification(assignGroup.getEvaluation().getId(),
                            assignGroup.getEvalGroupId());
                }

                LOG.info("User (" + userId + ") created a new assignGroup (" + assignGroup.getId() + "), "
                        + "linked evalGroupId (" + assignGroup.getEvalGroupId() + ") with eval (" + eval.getId()
                        + ")");

                Boolean syncUserAssignmentsOnGroupSave = (Boolean) this.settings
                        .get(EvalSettings.SYNC_USER_ASSIGNMENTS_ON_GROUP_SAVE);
                if (syncUserAssignmentsOnGroupSave == null) {
                    // use default, true
                    syncUserAssignmentsOnGroupSave = true;
                }
                if (syncUserAssignmentsOnGroupSave) {
                    // sync the user assignments related to this assign group
                    synchronizeUserAssignmentsForced(eval, assignGroup.getEvalGroupId(), false);
                }
            }
        } else {
            // updating an existing AG

            // fetch the existing AG out of the DB to compare it
            EvalAssignGroup existingAG = (EvalAssignGroup) dao.findById(EvalAssignGroup.class, assignGroup.getId());

            // check the user control permissions
            if (!securityChecks.checkControlAssignGroup(userId, assignGroup)) {
                throw new SecurityException("User (" + userId + ") attempted to update existing assignGroup ("
                        + existingAG.getId() + ") without permissions");
            }

            // cannot change the evaluation or evalGroupId so fail if they have been changed
            if (!existingAG.getEvalGroupId().equals(assignGroup.getEvalGroupId())) {
                throw new IllegalArgumentException("Cannot update evalGroupId (" + assignGroup.getEvalGroupId()
                        + ") for an existing AC, evalGroupId (" + existingAG.getEvalGroupId() + ")");
            } else if (!existingAG.getEvaluation().getId().equals(eval.getId())) {
                throw new IllegalArgumentException("Cannot update eval (" + eval.getId()
                        + ") for an existing AC, eval (" + existingAG.getEvaluation().getId() + ")");
            }

            // allow any other changes
            dao.save(assignGroup);
            LOG.info("User (" + userId + ") updated existing assignGroup (" + assignGroup.getId() + ") properties");

            Boolean syncUserAssignmentsOnGroupUpdate = (Boolean) this.settings
                    .get(EvalSettings.SYNC_USER_ASSIGNMENTS_ON_GROUP_UPDATE);
            if (syncUserAssignmentsOnGroupUpdate == null) {
                // if setting is null, use default, false
                syncUserAssignmentsOnGroupUpdate = false;
            }
            if (syncUserAssignmentsOnGroupUpdate) {
                // sync the user assignments related to this assign group
                synchronizeUserAssignmentsForced(eval, assignGroup.getEvalGroupId(), false);
            }

        }
    }

    /* (non-Javadoc)
     * @see org.sakaiproject.evaluation.logic.EvalEvaluationSetupService#deleteAssignGroup(java.lang.Long, java.lang.String)
     */
    public void deleteAssignGroup(Long assignGroupId, String userId) {
        LOG.debug("userId: " + userId + ", assignGroupId: " + assignGroupId);

        // get AC
        EvalAssignGroup assignGroup = evaluationService.getAssignGroupById(assignGroupId);
        if (assignGroup == null) {
            throw new IllegalArgumentException("Cannot find assign evalGroupId with this id: " + assignGroupId);
        }

        EvalEvaluation eval = evaluationService.getEvaluationById(assignGroup.getEvaluation().getId());
        if (eval == null) {
            throw new IllegalArgumentException(
                    "Cannot find evaluation with this id: " + assignGroup.getEvaluation().getId());
        }

        // check and die if not allowed
        securityChecks.checkRemoveAssignments(userId, assignGroup, eval);
        // handle the removal
        deleteAssignGroupInternal(assignGroup);
    }

    /**
     * Internal cleanup and removal of assign groups
     * @param assignGroup the assigngroup to remove
     */
    private void deleteAssignGroupInternal(EvalAssignGroup assignGroup) {
        if (assignGroup == null) {
            throw new IllegalArgumentException("assignGroup to remove cannot be null");
        }
        String userId = commonLogic.getCurrentUserId();
        Long assignGroupId = assignGroup.getId();
        dao.delete(assignGroup);
        LOG.info("User (" + userId + ") deleted existing assign group (" + assignGroup.getId() + ")");

        // also need to remove any user assignments related to this group
        List<EvalAssignUser> assignedUsers = dao.findBySearch(EvalAssignUser.class,
                new Search(new Restriction[] { new Restriction("assignGroupId", assignGroupId),
                        new Restriction("status", EvalAssignUser.STATUS_UNLINKED, Restriction.NOT_EQUALS) }));
        Set<EvalAssignUser> assignedUsersSet = new HashSet<EvalAssignUser>(assignedUsers);
        dao.deleteSet(assignedUsersSet);
        LOG.info("User assignments (" + assignedUsers.size() + ") related to deleted assign group ("
                + assignGroup.getId() + ") were removed for user (" + userId + ")");
    }

    // EMAIL TEMPLATES

    public void saveEmailTemplate(EvalEmailTemplate emailTemplate, String userId) {
        LOG.debug("userId: " + userId + ", emailTemplate: " + emailTemplate.getId());

        if (!TextTemplateLogicUtils.checkTextTemplate(emailTemplate.getSubject())) {
            throw new IllegalArgumentException("Template processing error in email subject when saving template.");
        }

        if (!TextTemplateLogicUtils.checkTextTemplate(emailTemplate.getMessage())) {
            throw new IllegalArgumentException("Template processing error in email text when saving template");
        }

        // set the date modified
        emailTemplate.setLastModified(new Date());

        // check user permissions
        if (!securityChecks.canUserControlEmailTemplate(userId, emailTemplate)) {
            throw new SecurityException("User (" + userId + ") cannot control email template ("
                    + emailTemplate.getId() + ") without permissions");
        }

        // checks to keeps someone from overwriting the default templates
        if (emailTemplate.getId() == null) {
            // null out the defaultType for new templates 
            emailTemplate.setDefaultType(null);

        } else {
            boolean userAdmin = commonLogic.isUserAdmin(userId);
            // existing template

            if (!userAdmin) {
                if (emailTemplate.getDefaultType() != null) {
                    throw new IllegalArgumentException(
                            "Cannot modify default templates or set existing templates to be default unless you are an admin");
                }

                // check if there are evaluations this is used in and if the user can modify this based on them

                // check available templates
                List<EvalEvaluation> l = dao.findBySearch(EvalEvaluation.class,
                        new Search("availableEmailTemplate.id", emailTemplate.getId()));
                for (int i = 0; i < l.size(); i++) {
                    EvalEvaluation eval = (EvalEvaluation) l.get(i);
                    // check eval/template permissions
                    securityChecks.checkEvalTemplateControl(userId, eval, emailTemplate);
                }

                // check reminder templates
                l = dao.findBySearch(EvalEvaluation.class,
                        new Search("reminderEmailTemplate.id", emailTemplate.getId()));
                for (int i = 0; i < l.size(); i++) {
                    EvalEvaluation eval = (EvalEvaluation) l.get(i);
                    // check eval/template permissions
                    securityChecks.checkEvalTemplateControl(userId, eval, emailTemplate);
                }
            } else {
                // admin can modify any templates that they like
            }
        }

        // DO NOT do cleanup for XSS scripting and strings: EVALSYS-994

        // save the template if allowed
        dao.save(emailTemplate);
        LOG.info("User (" + userId + ") saved email template (" + emailTemplate.getId() + ")");
    }

    public void removeEmailTemplate(Long emailTemplateId, String userId) {
        EvalEmailTemplate emailTemplate = evaluationService.getEmailTemplate(emailTemplateId);
        if (emailTemplate != null) {
            if (emailTemplate.getDefaultType() != null) {
                throw new IllegalArgumentException("Cannot remove email templates (" + emailTemplateId
                        + ") which are defaults: " + emailTemplate.getDefaultType());
            }
            securityChecks.checkEvalTemplateControl(userId, null, emailTemplate);
            String emailTemplateType = emailTemplate.getType();

            if (EvalConstants.EMAIL_TEMPLATE_AVAILABLE.equals(emailTemplateType)
                    || EvalConstants.EMAIL_TEMPLATE_REMINDER.equals(emailTemplateType)) {
                String templateTypeEval = "availableEmailTemplate";
                if (EvalConstants.EMAIL_TEMPLATE_REMINDER.equals(emailTemplateType)) {
                    templateTypeEval = "reminderEmailTemplate";
                }
                // get the evals that this template is used in
                List<EvalEvaluation> evals = dao.findBySearch(EvalEvaluation.class,
                        new Search(templateTypeEval + ".id", emailTemplateId));
                for (EvalEvaluation evaluation : evals) {
                    // replace with the default template (that means null it out)
                    if (EvalConstants.EMAIL_TEMPLATE_AVAILABLE.equals(emailTemplateType)) {
                        evaluation.setAvailableEmailTemplate(null);
                    } else if (EvalConstants.EMAIL_TEMPLATE_REMINDER.equals(emailTemplate.getType())) {
                        evaluation.setReminderEmailTemplate(null);
                    }
                    dao.save(evaluation); // save the new template
                }
            }

            // now go ahead and wipe out the template itself
            dao.delete(emailTemplate);
        }
    }

    // INTERNAL METHODS

    /**
     * Retrieve the complete set of eval assign groups for this evaluation
     * @param evaluationId
     * @return
     */
    protected List<EvalAssignGroup> getEvaluationAssignGroups(Long evaluationId) {
        // get all the evalGroupIds for the given eval ids in one storage call
        List<EvalAssignGroup> l = dao.findBySearch(EvalAssignGroup.class,
                new Search("evaluation.id", evaluationId));
        return l;
    }

    /**
     * Get the evaluation or throw an illegal argument exception
     * @param evaluationId
     * @return
     */
    protected EvalEvaluation getEvaluationOrFail(Long evaluationId) {
        EvalEvaluation eval = evaluationService.getEvaluationById(evaluationId);
        if (eval == null) {
            throw new IllegalArgumentException(
                    "Invalid eval id, cannot find evaluation with this id: " + evaluationId);
        }
        return eval;
    }

    /**
     * Check for existing AC which matches this ones linkage
     * @param ac
     * @return true if duplicate found
     */
    protected boolean checkRemoveDuplicateAssignGroup(EvalAssignGroup ac) {
        LOG.debug("assignContext: " + ac.getId());

        List<EvalAssignGroup> l = dao.findBySearch(EvalAssignGroup.class,
                new Search(new Restriction[] { new Restriction("evaluation.id", ac.getEvaluation().getId()),
                        new Restriction("evalGroupId", ac.getEvalGroupId()) }));
        return (ac.getId() == null && l.size() >= 1) || (ac.getId() != null && l.size() >= 2);
    }

    /**
     * Create EvalAssignGroup objects from a set of evalGroupIds for an eval and user
     * @param eval
     * @param userId
     * @param evalGroupIdsSet
     * @param nodeId (optional), null if there is no nodeId association, otherwise set to the associated nodeId
     * @return the set with the new assignments
     */
    protected Set<EvalAssignGroup> makeAssignGroups(EvalEvaluation eval, String userId, Set<String> evalGroupIdsSet,
            String nodeId) {
        Set<EvalAssignGroup> groupAssignments = new HashSet<>();
        for (String evalGroupId : evalGroupIdsSet) {
            String type = EvalConstants.GROUP_TYPE_PROVIDED;
            if (evalGroupId.startsWith(EvalConstants.GROUP_ID_SITE_PREFIX)) {
                // Determine type (section or site)
                if (evalGroupId.contains(EvalConstants.GROUP_ID_SECTION_PREFIX)) {
                    type = EvalConstants.GROUP_TYPE_SECTION;
                } else {
                    type = EvalConstants.GROUP_TYPE_SITE;
                }
            }
            // set the booleans to null to get the correct defaults set
            EvalAssignGroup eag = new EvalAssignGroup(userId, evalGroupId, type, eval);
            eag.setNodeId(nodeId);
            // fill in defaults and the values from the evaluation
            setAssignmentDefaults(eval, eag);
            groupAssignments.add(eag);
        }
        return groupAssignments;
    }

    /**
     * Ensures that the settings for assignments are correct based on the system settings and the evaluation settings,
     * also ensures that none of them are null
     * 
     * @param eval the evaluation associated with this assignment
     * @param eah the assignment object (persistent or non)
     */
    protected void setAssignmentDefaults(EvalEvaluation eval, EvalAssignHierarchy eah) {
        if (eval == null || eah == null) {
            throw new IllegalArgumentException("eval (" + eval + ") and assigment (" + eah + ") must not be null");
        }
        // setInstructorsViewResults
        if (eah.getInstructorsViewResults() == null) {
            Boolean instViewResults = (Boolean) settings.get(EvalSettings.INSTRUCTOR_ALLOWED_VIEW_RESULTS);
            if (instViewResults == null) {
                if (eval.getInstructorViewResults()) {
                    eah.setInstructorsViewResults(Boolean.TRUE);
                } else {
                    eah.setInstructorsViewResults(Boolean.FALSE);
                }
            } else {
                eah.setInstructorsViewResults(instViewResults);
            }
        }
        if (eah.getInstructorsViewAllResults() == null) {
            Boolean instViewAllResults = (Boolean) settings.get(EvalSettings.INSTRUCTOR_ALLOWED_VIEW_ALL_RESULTS);
            if (instViewAllResults == null) {
                if (eval.getInstructorViewAllResults()) {
                    eah.setInstructorsViewAllResults(Boolean.TRUE);
                } else {
                    eah.setInstructorsViewAllResults(Boolean.FALSE);
                }
            } else {
                eah.setInstructorsViewAllResults(instViewAllResults);
            }
        }

        // setStudentsViewResults
        if (eah.getStudentsViewResults() == null) {
            Boolean studViewResults = (Boolean) settings.get(EvalSettings.STUDENT_ALLOWED_VIEW_RESULTS);
            if (studViewResults == null) {
                if (eval.getStudentViewResults()) {
                    eah.setStudentsViewResults(Boolean.TRUE);
                } else {
                    eah.setStudentsViewResults(Boolean.FALSE);
                }
            } else {
                eah.setStudentsViewResults(studViewResults);
            }
        }
        // setInstructorApproval
        // TODO - temporary force to enabled
        eah.setInstructorApproval(Boolean.TRUE);
        //      if (eah.getInstructorApproval() == null) {
        //         String globalEvalOpt = (String) settings.get(EvalSettings.INSTRUCTOR_MUST_USE_EVALS_FROM_ABOVE);
        //         if (globalEvalOpt == null) {
        //            if ( EvalConstants.INSTRUCTOR_OPT_IN.equals(eval.getInstructorOpt()) ) {
        //               eah.setInstructorApproval( Boolean.FALSE );
        //            } else {
        //               // REQUIRED or OPT_OUT set to true
        //               eah.setInstructorApproval( Boolean.TRUE );
        //            }
        //         } else {
        //            if ( EvalConstants.INSTRUCTOR_OPT_IN.equals(globalEvalOpt) ) {
        //               eah.setInstructorApproval( Boolean.FALSE );
        //            } else {
        //               // REQUIRED or OPT_OUT set to true
        //               eah.setInstructorApproval( Boolean.TRUE );
        //            }
        //         }
        //      }
        // selections
        if (eah instanceof EvalAssignGroup) {
            EvalAssignGroup eag = (EvalAssignGroup) eah;
            Boolean enableSelection = (Boolean) settings.get(EvalSettings.ENABLE_INSTRUCTOR_ASSISTANT_SELECTION);
            if (enableSelection) {
                // if not set then inherit from the eval
                if (eag.getSelectionSettings() == null || "".equals(eag.getSelectionSettings())) {
                    eag.setSelectionSettings(eval.getSelectionSettings());
                }
            } else {
                // set to defaults
                eag.setSelectionSettings(null);
            }
        }
    }

    /* (non-Javadoc)
     * @see org.sakaiproject.evaluation.logic.EvalEvaluationSetupService#assignEmailTemplate(java.lang.Long, java.lang.Long, java.lang.String, java.lang.String)
     */
    public void assignEmailTemplate(Long emailTemplateId, Long evaluationId, String emailTemplateTypeConstant,
            String userId) {
        EvalEvaluation eval = getEvaluationOrFail(evaluationId);
        if (!securityChecks.canUserControlEvaluation(userId, eval)) {
            throw new SecurityException("User (" + userId + ") attempted to update existing evaluation ("
                    + eval.getId() + ") without permissions");
        }

        boolean clearAssociation = false;
        if (emailTemplateId == null) {
            if (emailTemplateTypeConstant == null) {
                throw new IllegalArgumentException(
                        "emailTemplateTypeConstant cannot be null when clearing association");
            }
            clearAssociation = true;
        } else {
            EvalEmailTemplate emailTemplate = evaluationService.getEmailTemplate(emailTemplateId);
            // assign to the evaluation
            if (emailTemplate.getDefaultType() == null) {
                // only assign non-default templates of the right type
                if (EvalConstants.EMAIL_TEMPLATE_AVAILABLE.equals(emailTemplate.getType())) {
                    eval.setAvailableEmailTemplate(emailTemplate);
                } else if (EvalConstants.EMAIL_TEMPLATE_REMINDER.equals(emailTemplate.getType())) {
                    eval.setReminderEmailTemplate(emailTemplate);
                } else if (EvalConstants.EMAIL_TEMPLATE_SUBMITTED.equals(emailTemplate.getType())) {
                    eval.setSubmissionConfirmationEmailTemplate(emailTemplate);
                }
                dao.save(eval);
            } else {
                emailTemplateTypeConstant = emailTemplate.getType();
                clearAssociation = true;
            }
        }

        if (clearAssociation && emailTemplateTypeConstant != null) {
            Long checkEmailTemplateId = null;
            String evalTemplateType = null;
            if (EvalConstants.EMAIL_TEMPLATE_AVAILABLE.equals(emailTemplateTypeConstant)) {
                if (eval.getAvailableEmailTemplate() != null) {
                    checkEmailTemplateId = eval.getAvailableEmailTemplate().getId();
                    evalTemplateType = "availableEmailTemplate";
                }
                eval.setAvailableEmailTemplate(null);
            } else if (EvalConstants.EMAIL_TEMPLATE_REMINDER.equals(emailTemplateTypeConstant)) {
                if (eval.getReminderEmailTemplate() != null) {
                    checkEmailTemplateId = eval.getReminderEmailTemplate().getId();
                    evalTemplateType = "reminderEmailTemplate";
                }
                eval.setReminderEmailTemplate(null);
            } else if (EvalConstants.EMAIL_TEMPLATE_SUBMITTED.equals(emailTemplateTypeConstant)) {
                if (eval.getSubmissionConfirmationEmailTemplate() != null) {
                    checkEmailTemplateId = eval.getSubmissionConfirmationEmailTemplate().getId();
                    evalTemplateType = "submissionConfirmationEmailTemplate";
                }
                eval.setSubmissionConfirmationEmailTemplate(null);
            }
            dao.save(eval);

            // also remove the unused template if possible
            if (checkEmailTemplateId != null) {
                EvalEmailTemplate checkTemplate = evaluationService.getEmailTemplate(checkEmailTemplateId);
                if (checkTemplate != null && checkTemplate.getDefaultType() == null) {
                    // only remove non-default templates
                    long evalsUsingTemplate = dao.countBySearch(EvalEvaluation.class,
                            new Search(evalTemplateType + ".id", checkEmailTemplateId));
                    if (evalsUsingTemplate <= 0) {
                        // template was only used in this evaluation
                        dao.delete(checkTemplate);
                    }
                }
            }
        }
    }

}