gt: 	if (isWeighted) {
context: /****************************************************************
   * =============================================================
   * License Information: http://lamsfoundation.org/licensing/lams/2.0/
   *
   * This program is free software; you can redistribute it and/or modify
   * it under the terms of the GNU General Public License version 2.0
   * as published by the Free Software Foundation.
   *
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License for more details.
   *
   * You should have received a copy of the GNU General Public License
   * along with this program; if not, write to the Free Software
   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 * USA
   *
   * http://www.gnu.org/licenses/gpl.txt
   * ****************************************************************
   */
  
  package org.lamsfoundation.lams.gradebook.service;
  
  import java.nio.file.Files;
  import java.nio.file.Paths;
  import java.text.DateFormat;
  import java.text.SimpleDateFormat;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.Date;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.LinkedHashMap;
  import java.util.LinkedHashSet;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.TimeZone;
  import java.util.TreeSet;
  import java.util.stream.Collectors;
  
  import org.apache.log4j.Logger;
  import org.lamsfoundation.lams.events.IEventNotificationService;
  import org.lamsfoundation.lams.gradebook.GradebookUserActivity;
  import org.lamsfoundation.lams.gradebook.GradebookUserLesson;
  import org.lamsfoundation.lams.gradebook.dao.IGradebookDAO;
  import org.lamsfoundation.lams.gradebook.dto.GBActivityArchiveGridRowDTO;
  import org.lamsfoundation.lams.gradebook.dto.GBActivityGridRowDTO;
  import org.lamsfoundation.lams.gradebook.dto.GBLessonGridRowDTO;
  import org.lamsfoundation.lams.gradebook.dto.GBUserGridRowDTO;
  import org.lamsfoundation.lams.gradebook.dto.GradebookGridRowDTO;
  import org.lamsfoundation.lams.gradebook.model.GradebookUserActivityArchive;
  import org.lamsfoundation.lams.gradebook.model.GradebookUserLessonArchive;
  import org.lamsfoundation.lams.gradebook.util.GBGridView;
  import org.lamsfoundation.lams.gradebook.util.GradebookUtil;
  import org.lamsfoundation.lams.gradebook.util.LessonComparator;
  import org.lamsfoundation.lams.gradebook.util.ReleaseMarksJob;
  import org.lamsfoundation.lams.integration.service.IIntegrationService;
  import org.lamsfoundation.lams.learning.service.ILearnerService;
  import org.lamsfoundation.lams.learningdesign.Activity;
  import org.lamsfoundation.lams.learningdesign.ActivityEvaluation;
  import org.lamsfoundation.lams.learningdesign.ComplexActivity;
  import org.lamsfoundation.lams.learningdesign.FloatingActivity;
  import org.lamsfoundation.lams.learningdesign.Group;
  import org.lamsfoundation.lams.learningdesign.Grouping;
  import org.lamsfoundation.lams.learningdesign.LearningDesign;
  import org.lamsfoundation.lams.learningdesign.OptionsActivity;
  import org.lamsfoundation.lams.learningdesign.ParallelActivity;
  import org.lamsfoundation.lams.learningdesign.SequenceActivity;
  import org.lamsfoundation.lams.learningdesign.ToolActivity;
  import org.lamsfoundation.lams.learningdesign.dao.IActivityDAO;
  import org.lamsfoundation.lams.learningdesign.dto.ActivityURL;
  import org.lamsfoundation.lams.lesson.CompletedActivityProgress;
  import org.lamsfoundation.lams.lesson.CompletedActivityProgressArchive;
  import org.lamsfoundation.lams.lesson.LearnerProgress;
  import org.lamsfoundation.lams.lesson.LearnerProgressArchive;
  import org.lamsfoundation.lams.lesson.Lesson;
  import org.lamsfoundation.lams.lesson.dao.ILearnerProgressDAO;
  import org.lamsfoundation.lams.lesson.dao.ILessonDAO;
  import org.lamsfoundation.lams.lesson.service.ILessonService;
  import org.lamsfoundation.lams.logevent.LogEvent;
  import org.lamsfoundation.lams.logevent.service.ILogEventService;
  import org.lamsfoundation.lams.outcome.Outcome;
  import org.lamsfoundation.lams.outcome.OutcomeMapping;
  import org.lamsfoundation.lams.outcome.OutcomeResult;
  import org.lamsfoundation.lams.outcome.OutcomeScaleItem;
  import org.lamsfoundation.lams.outcome.service.IOutcomeService;
  import org.lamsfoundation.lams.tool.ToolOutput;
  import org.lamsfoundation.lams.tool.ToolOutputValue;
  import org.lamsfoundation.lams.tool.ToolSession;
  import org.lamsfoundation.lams.tool.exception.ToolException;
  import org.lamsfoundation.lams.tool.service.ILamsCoreToolService;
  import org.lamsfoundation.lams.usermanagement.Organisation;
  import org.lamsfoundation.lams.usermanagement.OrganisationType;
  import org.lamsfoundation.lams.usermanagement.Role;
  import org.lamsfoundation.lams.usermanagement.User;
  import org.lamsfoundation.lams.usermanagement.dto.UserDTO;
  import org.lamsfoundation.lams.usermanagement.service.IUserManagementService;
  import org.lamsfoundation.lams.usermanagement.util.LastNameAlphabeticComparator;
  import org.lamsfoundation.lams.util.Configuration;
  import org.lamsfoundation.lams.util.ConfigurationKeys;
  import org.lamsfoundation.lams.util.DateUtil;
  import org.lamsfoundation.lams.util.FileUtil;
  import org.lamsfoundation.lams.util.MessageService;
  import org.lamsfoundation.lams.util.WebUtil;
  import org.lamsfoundation.lams.util.excel.ExcelCell;
  import org.lamsfoundation.lams.util.excel.ExcelRow;
  import org.lamsfoundation.lams.util.excel.ExcelSheet;
  import org.lamsfoundation.lams.web.session.SessionManager;
  import org.lamsfoundation.lams.web.util.AttributeNames;
  import org.quartz.JobBuilder;
  import org.quartz.JobDetail;
  import org.quartz.Scheduler;
  import org.quartz.SchedulerException;
  import org.quartz.Trigger;
  import org.quartz.TriggerBuilder;
  import org.quartz.TriggerKey;
  import org.springframework.web.context.WebApplicationContext;
  import org.springframework.web.context.support.WebApplicationContextUtils;
  import org.springframework.web.util.HtmlUtils;
  
  import com.fasterxml.jackson.databind.node.ArrayNode;
  import com.fasterxml.jackson.databind.node.JsonNodeFactory;
  import com.fasterxml.jackson.databind.node.ObjectNode;
  
  /**
   *
   * This service handles all gradebook-related service calls
   *
   * @author lfoxton
   */
  public class GradebookService implements IGradebookFullService {
      private static Logger logger = Logger.getLogger(GradebookService.class);
  
      private static final Set<String> LESSON_EXPORT_TOOL_ACTIVITIES = new HashSet<>(
  	    Arrays.asList("laasse10", "lascrt11", "lamc11", "ladoku11"));
  
      private static String RELEASE_MARKS_EMAIL_TEMPLATE_CONTENT = null;
      private static final DateFormat RELEASE_MARKS_EMAIL_DATE_FORMAT = new SimpleDateFormat(DateUtil.PRETTY_FORMAT);
  
      private static final String RELEASE_MARKS_EMAIL_PAGE_TITLE_PLACEHOLDER = "[PAGE_TITLE_PLACEHOLDER]";
      private static final String RELEASE_MARKS_EMAIL_TOP_HEADER_PLACEHOLDER = "[TOP_HEADER_PLACEHOLDER]";
      private static final String RELEASE_MARKS_EMAIL_CONTENT_START_PLACEHOLDER = "[CONTENT_START_PLACEHOLDER]";
      private static final String RELEASE_MARKS_EMAIL_CONTENT_LESSON_NAME_PLACEHOLDER = "[CONTENT_LESSON_NAME_PLACEHOLDER]";
      private static final String RELEASE_MARKS_EMAIL_LESSON_NAME_PLACEHOLDER = "[LESSON_NAME_PLACEHOLDER]";
      private static final String RELEASE_MARKS_EMAIL_RELEASE_DATE_PLACEHOLDER = "[RELEASE_DATE_PLACEHOLDER]";
      private static final String RELEASE_MARKS_EMAIL_OVERALL_GRADE_PLACEHOLDER = "[OVERALL_GRADE_PLACEHOLDER]";
      private static final String RELEASE_MARKS_EMAIL_CONTENT_END_PLACEHOLDER = "[CONTENT_END_PLACEHOLDER]";
      private static final String RELEASE_MARKS_EMAIL_CONTENT_THANKS_PLACEHOLDER = "[CONTENT_THANKS_PLACEHOLDER]";
      private static final String RELEASE_MARKS_EMAIL_FOOTER_PLACEHOLDER = "[FOOTER_PLACEHOLDER]";
      private static final String RELEASE_MARKS_EMAIL_ACTIVITY_ROW_START = "[ACTIVITY_ROW_START]";
      private static final String RELEASE_MARKS_EMAIL_ACTIVITY_ROW_END = "[ACTIVITY_ROW_END]";
      private static final String RELEASE_MARKS_EMAIL_ACTIVITY_NAME_PLACEHOLDER = "[ACTIVITY_NAME_PLACEHOLDER]";
      private static final String RELEASE_MARKS_EMAIL_ACTIVITY_PROGRESS_ICON_PLACEHOLDER = "[ACTIVITY_PROGRESS_ICON_PLACEHOLDER]";
      private static final String RELEASE_MARKS_EMAIL_ACTIVITY_GRADE_PLACEHOLDER = "[ACTIVITY_GRADE_PLACEHOLDER]";
  
      // Services
      private ILamsCoreToolService toolService;
      private IGradebookDAO gradebookDAO;
      private ILearnerProgressDAO learnerProgressDAO;
      private ILessonDAO lessonDAO;
      private ILessonService lessonService;
      private IUserManagementService userService;
      private IActivityDAO activityDAO;
      private MessageService messageService;
      private ILogEventService logEventService;
      private static ILearnerService learnerService;
      private IIntegrationService integrationService;
      private IOutcomeService outcomeService;
  
      private Scheduler scheduler;
  
      @Override
      public List<GradebookGridRowDTO> getGBActivityRowsForLearner(Long lessonId, Integer userId, TimeZone userTimezone) {
  	logger.debug("Getting gradebook user data for lesson: " + lessonId + ". For user: " + userId);
  
  	Lesson lesson = lessonService.getLesson(lessonId);
  	User learner = (User) userService.findById(User.class, userId);
  
  	List<GradebookGridRowDTO> gradebookActivityDTOs = new ArrayList<>();
  
  	List<ToolActivity> activities = getLessonActivitiesForLearner(lesson, userId);
  	for (ToolActivity activity : activities) {
  
  	    String groupName = null;
  	    Long groupId = null;
  	    if (activity.getGrouping() != null) {
  		Group group = activity.getGroupFor(learner);
  		if (group != null) {
  		    groupName = group.getGroupName();
  		    groupId = group.getGroupId();
  		}
  	    }
  	    GBActivityGridRowDTO activityDTO = new GBActivityGridRowDTO(activity, groupName, groupId);
  
  	    // Set the possible marks if applicable
  	    activityDTO.setMarksAvailable(toolService.getActivityMaxPossibleMark(activity));
  
  	    GradebookUserActivity gradebookActivity = gradebookDAO
  		    .getGradebookUserDataForActivity(activity.getActivityId(), learner.getUserId());
  	    if (gradebookActivity != null) {
  		activityDTO.setMark(gradebookActivity.getMark());
  		activityDTO.setFeedback(gradebookActivity.getFeedback());
  	    }
  
  	    LearnerProgress learnerProgress = lessonService.getUserProgressForLesson(learner.getUserId(),
  		    lesson.getLessonId());
  	    // Setting status
  	    activityDTO.setStartDate(getActivityStartDate(learnerProgress, activity, userTimezone));
  	    activityDTO.setFinishDate(getActivityFinishDate(learnerProgress, activity, userTimezone));
  	    activityDTO.setTimeTaken(getActivityDuration(learnerProgress, activity));
  	    activityDTO.setStatus(getActivityStatusStr(learnerProgress, activity));
  
  	    // Setting averages
  	    activityDTO.setAverageMark(gradebookDAO.getAverageMarkForActivity(activity.getActivityId()));
  	    activityDTO.setMedianTimeTaken(gradebookDAO.getMedianTimeTakenForActivity(activity.getActivityId()));
  	    activityDTO.setMaxTimeTaken(gradebookDAO.getMaxTimeTakenForActivity(activity.getActivityId()));
  	    activityDTO.setMinTimeTaken(gradebookDAO.getMinTimeTakenForActivity(activity.getActivityId()));
  
  	    // Get the tool outputs for this user if there are any
  	    ToolSession toolSession = toolService.getToolSessionByLearner(learner, activity);
  	    if ((toolSession != null) && (learnerProgress != null)) {
  		byte activityState = learnerProgress.getProgressState(activity);
  		if ((activityState == LearnerProgress.ACTIVITY_ATTEMPTED)
  			|| (activityState == LearnerProgress.ACTIVITY_COMPLETED)) {
  		    // Set the activityLearner URL for this gradebook activity
  		    String activityUrl = Configuration.get(ConfigurationKeys.SERVER_URL)
  			    + activity.getTool().getLearnerProgressUrl();
  		    activityUrl = WebUtil.appendParameterToURL(activityUrl, "userID", learner.getUserId().toString());
  		    activityUrl = WebUtil.appendParameterToURL(activityUrl, "toolSessionID",
  			    toolSession.getToolSessionId().toString());
  		    activityDTO.setActivityUrl(activityUrl);
  		}
  	    }
  
  	    List<OutcomeMapping> outcomeMappings = outcomeService.getOutcomeMappings(null, activity.getToolContentId(),
  		    null, null);
  	    if (!outcomeMappings.isEmpty()) {
  		ArrayNode outcomeMappingsJSON = JsonNodeFactory.instance.arrayNode();
  		for (OutcomeMapping outcomeMapping : outcomeMappings) {
  		    ObjectNode outcomeMappingJSON = JsonNodeFactory.instance.objectNode();
  		    Outcome outcome = outcomeMapping.getOutcome();
  		    outcomeMappingJSON.put("mappingId", outcomeMapping.getMappingId());
  		    outcomeMappingJSON.put("name", outcome.getName());
  		    outcomeMappingJSON.put("code", outcome.getCode());
  		    ArrayNode possibleValues = JsonNodeFactory.instance.arrayNode();
  		    for (OutcomeScaleItem possibleValue : outcome.getScale().getItems()) {
  			possibleValues.add(possibleValue.getName());
  		    }
  		    outcomeMappingJSON.set("possibleValues", possibleValues);
  		    OutcomeResult result = outcomeService.getOutcomeResult(userId, outcomeMapping.getMappingId());
  		    if (result != null) {
  			outcomeMappingJSON.put("value", result.getValue());
  		    }
  		    outcomeMappingsJSON.add(outcomeMappingJSON);
  		}
  		activityDTO.setOutcomes(outcomeMappingsJSON.toString());
  	    }
  
  	    gradebookActivityDTOs.add(activityDTO);
  	}
  
  	return gradebookActivityDTOs;
      }
  
      @Override
      public List<GradebookGridRowDTO> getGBActivityArchiveRowsForLearner(Long activityId, Integer userId,
  	    TimeZone userTimezone) {
  	logger.debug("Getting archive gradebook user data for activity: " + activityId + ". For user: " + userId);
  
  	Activity activity = getActivityById(activityId);
  	Lesson lesson = activity.getLearningDesign().getLessons().iterator().next();
  
  	List<GradebookGridRowDTO> gradebookActivityDTOs = new ArrayList<>();
  	List<GradebookUserLessonArchive> lessonArchives = gradebookDAO.getArchivedLessonMarks(lesson.getLessonId(),
  		userId);
  	int attemptOrder = lessonArchives.size();
  	List<GradebookUserActivityArchive> activityArchives = gradebookDAO.getArchivedActivityMarks(activityId, userId);
  	for (GradebookUserLessonArchive lessonArchive : lessonArchives) {
  	    Date archiveDate = lessonArchive.getArchiveDate();
  	    GBActivityArchiveGridRowDTO activityDTO = new GBActivityArchiveGridRowDTO(attemptOrder,
  		    lessonArchive.getMark());
  	    LearnerProgressArchive learnerProgress = learnerProgressDAO.getLearnerProgressArchive(lesson.getLessonId(),
  		    userId, lessonArchive.getArchiveDate());
  
  	    if (learnerProgress != null) {
  		// Setting status
  		activityDTO.setStartDate(getActivityStartDate(learnerProgress, activity, userTimezone));
  		activityDTO.setFinishDate(getActivityFinishDate(learnerProgress, activity, userTimezone));
  		activityDTO.setTimeTaken(getActivityDuration(learnerProgress, activity));
  		activityDTO.setStatus(getActivityStatusStr(learnerProgress, activity));
  
  		for (GradebookUserActivityArchive activityArchive : activityArchives) {
  		    // if it matches, we found an archived mark for this activity and this attempt
  		    if (archiveDate.equals(activityArchive.getArchiveDate())) {
  			activityDTO.setMark(activityArchive.getMark());
  			activityDTO.setFeedback(activityArchive.getFeedback());
  			break;
  		    }
  		}
  	    }
  
  	    gradebookActivityDTOs.add(activityDTO);
  	    attemptOrder--;
  	}
  
  	return gradebookActivityDTOs;
      }
  
      @Override
      public List<GradebookGridRowDTO> getGBLessonComplete(Long lessonId, Integer userId) {
  	logger.debug("Getting lesson complete gradebook user data for lesson: " + lessonId + ". For user: " + userId);
  
  	Lesson lesson = lessonService.getLesson(lessonId);
  	User learner = (User) userService.findById(User.class, userId);
  	List<GradebookGridRowDTO> gradebookActivityDTOs = new ArrayList<>();
  
  	List<ToolActivity> activities = getLessonActivitiesForLearner(lesson, userId);
  	for (ToolActivity activity : activities) {
  	    String groupName = null;
  	    Long groupId = null;
  	    if (activity.getGrouping() != null) {
  		Group group = activity.getGroupFor(learner);
  		if (group != null) {
  		    groupName = group.getGroupName();
  		    groupId = group.getGroupId();
  		}
  	    }
  	    GBActivityGridRowDTO activityDTO = new GBActivityGridRowDTO(activity, groupName, groupId);
  
  	    GradebookUserActivity gradebookActivity = gradebookDAO
  		    .getGradebookUserDataForActivity(activity.getActivityId(), learner.getUserId());
  	    if (gradebookActivity != null) {
  		activityDTO.setMark(gradebookActivity.getMark());
  	    }
  	    activityDTO.setAverageMark(gradebookDAO.getAverageMarkForActivity(activity.getActivityId()));
  
  	    LearnerProgress learnerProgress = lessonService.getUserProgressForLesson(learner.getUserId(),
  		    lesson.getLessonId());
  	    activityDTO.setStatus(getActivityStatusStr(learnerProgress, activity));
  
  	    gradebookActivityDTOs.add(activityDTO);
  	}
  
  	return gradebookActivityDTOs;
      }
  
      @Override
      public List<GradebookGridRowDTO> getGBActivityRowsForLesson(Long lessonId, TimeZone userTimezone,
  	    boolean escapeTitles) {
  	logger.debug("Getting gradebook data for lesson: " + lessonId);
  
  	Lesson lesson = lessonService.getLesson(lessonId);
  	List<GradebookGridRowDTO> gradebookActivityDTOs = new ArrayList<>();
  
  	List<ToolActivity> activities = getLessonToolActivitiesForLesson(lesson);
  
  	for (ToolActivity activity : activities) {
  
  	    Grouping grouping = activity.getGrouping();
  	    if (grouping != null) {
  		Set<Group> groups = grouping.getGroups();
  		if (groups != null) {
  
  		    for (Group group : groups) {
  			GBActivityGridRowDTO activityDTO = getGradebookActivityDTO(activity, lesson,
  				group.getGroupName(), group.getGroupId(), escapeTitles);
  			gradebookActivityDTOs.add(activityDTO);
  		    }
  
  		}
  	    } else {
  		GBActivityGridRowDTO activityDTO = getGradebookActivityDTO(activity, lesson, null, null, escapeTitles);
  		gradebookActivityDTOs.add(activityDTO);
  	    }
  	}
  
  	return gradebookActivityDTOs;
      }
  
      @Override
      public List<GBUserGridRowDTO> getGBUserRowsForActivity(Lesson lesson, ToolActivity activity, Long groupId, int page,
  	    int size, String sortBy, String sortOrder, String searchString, TimeZone timezone) {
  	Long lessonId = lesson.getLessonId();
  	Long activityId = activity.getActivityId();
  
  	List<GBUserGridRowDTO> gradebookUserDTOs = new ArrayList<>();
  
  	List<User> learners = null;
  	if (groupId != null) {
  	    Group group = (Group) userService.findById(Group.class, groupId);
  	    if (group != null) {
  		learners = gradebookDAO.getUsersByGroup(lessonId, activityId, groupId, page, size, sortBy, sortOrder,
  			searchString);
  	    } else {
  		learners = gradebookDAO.getLearnersByActivity(lessonId, activityId, page, size, sortBy, sortOrder,
  			searchString);
  	    }
  	} else {
  	    learners = gradebookDAO.getLearnersByActivity(lessonId, activityId, page, size, sortBy, sortOrder,
  		    searchString);
  	}
  
  	if (learners != null) {
  	    Map<Integer, LearnerProgress> userToLearnerProgressMap = getUserToLearnerProgressMap(lesson, learners);
  	    Map<Integer, GradebookUserActivity> userToGradebookUserLessonMap = getUserToGradebookUserActivityMap(
  		    activity, learners);
  	    Long maxPossibleMark = toolService.getActivityMaxPossibleMark(activity);
  
  //		List<ToolSession> toolSessions = toolService.getToolSessionsByLesson(lesson);
  //			// find required toolSession from toolSessions (thus we don't querying DB and hence increasing
  //			// efficiency).
  //			ToolSession toolSession = null;
  //			for (ToolSession dbToolSession : toolSessions) {
  //			    if (dbToolSession.getToolActivity().getActivityId().equals(activity.getActivityId())
  //				    && dbToolSession.getLearners().contains(learner)) {
  //				toolSession = dbToolSession;
  //			    }
  //			}
  
  	    for (User learner : learners) {
  		GBUserGridRowDTO gUserDTO = new GBUserGridRowDTO(learner);
  
  		gUserDTO.setMarksAvailable(maxPossibleMark);
  
  		// Set the progress
  		LearnerProgress learnerProgress = userToLearnerProgressMap.get(learner.getUserId());
  		gUserDTO.setStatus(getActivityStatusStr(learnerProgress, activity));
  		gUserDTO.setTimeTaken(getActivityDuration(learnerProgress, activity));
  		gUserDTO.setStartDate(getActivityStartDate(learnerProgress, activity, timezone));
  		gUserDTO.setFinishDate(getActivityFinishDate(learnerProgress, activity, timezone));
  
  		// Get the tool outputs for this user if there are any
  		ToolSession toolSession = toolService.getToolSessionByLearner(learner, activity);
  		if ((toolSession != null) && (learnerProgress != null)) {
  		    // Set the activityLearner URL for this gradebook activity
  		    byte activityState = learnerProgress.getProgressState(activity);
  		    if ((activityState == LearnerProgress.ACTIVITY_ATTEMPTED)
  			    || (activityState == LearnerProgress.ACTIVITY_COMPLETED)) {
  			gUserDTO.setActivityUrl(Configuration.get(ConfigurationKeys.SERVER_URL)
  				+ activity.getTool().getLearnerProgressUrl() + "&userID=" + learner.getUserId()
  				+ "&toolSessionID=" + toolSession.getToolSessionId().toString());
  		    }
  		}
  
  		// Add marks and feedback
  		GradebookUserActivity gradebookUserActivity = userToGradebookUserLessonMap.get(learner.getUserId());
  		if (gradebookUserActivity != null) {
  		    gUserDTO.setFeedback(gradebookUserActivity.getFeedback());
  		    gUserDTO.setMark(gradebookUserActivity.getMark());
  
  		}
  
  		boolean hasArchivedMarks = gradebookDAO.hasArchivedMarks(lesson.getLessonId(), learner.getUserId());
  		gUserDTO.setHasArchivedMarks(hasArchivedMarks);
  
  		gradebookUserDTOs.add(gUserDTO);
  	    }
  	}
  
  	return gradebookUserDTOs;
      }
  
      @Override
      public ArrayList<GBUserGridRowDTO> getGBUserRowsForLesson(Lesson lesson, TimeZone timeZone) {
  	return getGBUserRowsForLesson(lesson, 0, 0, null, null, null, timeZone);
      }
  
      @Override
      @SuppressWarnings("unchecked")
      public ArrayList<GBUserGridRowDTO> getGBUserRowsForLesson(Lesson lesson, int page, int size, String sortBy,
  	    String sortOrder, String searchString, TimeZone userTimeZone) {
  
  	ArrayList<GBUserGridRowDTO> gradebookUserDTOs = new ArrayList<>();
  
  	if (lesson != null) {
  	    List<User> learners;
  	    Map<Integer, LearnerProgress> userToLearnerProgressMap;
  	    Map<Integer, GradebookUserLesson> userToGradebookUserLessonMap;
  	    //size will be 0 in case of excel export
  	    if (size == 0) {
  		learners = new LinkedList<>(lesson.getAllLearners());
  		Collections.sort(learners, new LastNameAlphabeticComparator());
  
  		userToLearnerProgressMap = getUserToLearnerProgressMap(lesson, null);
  		userToGradebookUserLessonMap = getUserToGradebookUserLessonMap(lesson, null);
  
  	    } else {
  		learners = gradebookDAO.getLearnersByLesson(lesson.getLessonId(), page, size, sortBy, sortOrder,
  			searchString);
  		userToLearnerProgressMap = getUserToLearnerProgressMap(lesson, learners);
  		userToGradebookUserLessonMap = getUserToGradebookUserLessonMap(lesson, learners);
  	    }
  
  	    boolean isWeighted = toolService.isWeightedMarks(lesson.getLearningDesign());
  
  	    for (User learner : learners) {
  		LearnerProgress learnerProgress = userToLearnerProgressMap.get(learner.getUserId());
  		GBUserGridRowDTO gradebookUserDTO = new GBUserGridRowDTO(learner);
  		gradebookUserDTOs.add(gradebookUserDTO);
  
  		// Setting the status and time taken for the user's lesson
  		gradebookUserDTO.setStatus(getLessonStatusStr(learnerProgress));
  
  		// set current activity if available
  		if ((learnerProgress != null) && (learnerProgress.getCurrentActivity() != null)) {
  		    gradebookUserDTO.setCurrentActivity(learnerProgress.getCurrentActivity().getTitle());
  		}
  
  		// calculate time taken
  		if (learnerProgress != null) {
  		    Date startDate = learnerProgress.getStartDate();
  		    Date finishDate = learnerProgress.getFinishDate();
  
  		    if (startDate != null && finishDate != null) {
  			gradebookUserDTO.setTimeTaken(finishDate.getTime() - startDate.getTime());
  		    }
  
  		    if (startDate != null) {
  			if (userTimeZone != null) {
  			    startDate = DateUtil.convertToTimeZoneFromDefault(userTimeZone, startDate);
  			}
  			gradebookUserDTO.setStartDate(startDate);
  		    }
  
  		    if (learnerProgress.getFinishDate() != null) {
  			if (userTimeZone != null) {
  			    finishDate = DateUtil.convertToTimeZoneFromDefault(userTimeZone, finishDate);
  			}
  			gradebookUserDTO.setFinishDate(finishDate);
  		    }
  
  		}
  
  		GradebookUserLesson gradebookUserLesson = userToGradebookUserLessonMap.get(learner.getUserId());
  		if (gradebookUserLesson != null) {
  		    gradebookUserDTO.setMark(gradebookUserLesson.getMark());
  		    gradebookUserDTO.setFeedback(gradebookUserLesson.getFeedback());
  		    gradebookUserDTO.setDisplayMarkAsPercent(isWeighted);
  		}
  
  		boolean hasArchivedMarks = gradebookDAO.hasArchivedMarks(lesson.getLessonId(), learner.getUserId());
  		gradebookUserDTO.setHasArchivedMarks(hasArchivedMarks);
  
  	    }
  	}
  
  	return gradebookUserDTOs;
      }
  
      @Override
      public int getCountUsersByLesson(Long lessonId, String searchString) {
  	return gradebookDAO.getCountUsersByLesson(lessonId, searchString);
      }
  
      @Override
      public ArrayList<GBUserGridRowDTO> getGBUserRowsForOrganisation(Organisation organisation, int page, int size,
  	    String sortOrder, String searchString) {
  
  	ArrayList<GBUserGridRowDTO> gradebookUserDTOs = new ArrayList<>();
  
  	if (organisation != null) {
  	    List<User> learners = gradebookDAO.getLearnersFromOrganisation(organisation.getOrganisationId(), page, size,
  		    sortOrder, searchString);
  
  	    if (learners != null) {
  
  		for (User learner : learners) {
  
  		    GBUserGridRowDTO gradebookUserDTO = new GBUserGridRowDTO(learner);
  		    gradebookUserDTOs.add(gradebookUserDTO);
  		}
  	    }
  	}
  
  	return gradebookUserDTOs;
  
      }
  
      @Override
      public int getCountUsersByOrganisation(Integer orgId, String searchString) {
  	return gradebookDAO.getCountUsersByOrganisation(orgId, searchString);
      }
  
      @Override
      public GradebookUserLesson getGradebookUserLesson(Long lessonID, Integer userID) {
  	return gradebookDAO.getGradebookUserDataForLesson(lessonID, userID);
      }
  
      @Override
      public List<GradebookUserLesson> getGradebookUserLesson(Long lessonID) {
  	return gradebookDAO.getGradebookUserDataForLesson(lessonID);
      }
  
      @Override
      public GradebookUserActivity getGradebookUserActivity(Long activityID, Integer userID) {
  	return gradebookDAO.getGradebookUserDataForActivity(activityID, userID);
      }
  
      @Override
      public List<GradebookUserActivity> getGradebookUserActivities(Long activityId) {
  	return gradebookDAO.getAllGradebookUserActivitiesForActivity(activityId);
      }
  
      @Override
      public Double getAverageMarkForActivity(Long activityID, Long groupID) {
  	// return AverageMarkForActivity if groupId is null and AverageMarkForGroupedActivity if groupId is specified
  	Double averageMark;
  	if (groupID == null) {
  	    averageMark = gradebookDAO.getAverageMarkForActivity(activityID);
  	} else {
  	    averageMark = gradebookDAO.getAverageMarkForGroupedActivity(activityID, groupID);
  	}
  	return averageMark;
      }
  
      @Override
      public Double getAverageMarkForLesson(Long lessonID) {
  	return gradebookDAO.getAverageMarkForLesson(lessonID);
      }
  
      @Override
      public void updateUserLessonGradebookMark(Lesson lesson, User learner, Double mark) {
  	if ((lesson != null) && (learner != null)) {
  	    GradebookUserLesson gradebookUserLesson = gradebookDAO.getGradebookUserDataForLesson(lesson.getLessonId(),
  		    learner.getUserId());
  
  	    if (gradebookUserLesson == null) {
  		gradebookUserLesson = new GradebookUserLesson(lesson, learner);
  	    }
  	    String oldMark = (gradebookUserLesson.getMark() == null) ? "-" : gradebookUserLesson.getMark().toString();
  
  	    gradebookUserLesson.setMark(mark);
  	    gradebookDAO.insertOrUpdate(gradebookUserLesson);
  
  	    // audit log changed gradebook mark
  	    UserDTO monitorUser = (UserDTO) SessionManager.getSession().getAttribute(AttributeNames.USER);
  	    String[] args = new String[] { learner.getLogin() + "(" + learner.getUserId() + ")",
  		    lesson.getLessonId().toString(), oldMark, mark.toString() };
  	    String message = messageService.getMessage("audit.lesson.change.mark", args);
  	    logEventService.logEvent(LogEvent.TYPE_MARK_UPDATED, monitorUser.getUserID(), learner.getUserId(),
  		    lesson.getLessonId(), null, message);
  	}
      }
  
      @Override
      public void recalculateTotalMarksForLesson(Long lessonId) throws Exception {
  	Lesson lesson = lessonDAO.getLesson(lessonId);
  	if (lesson == null) {
  	    return;
  	}
  	boolean weighted = toolService.isWeightedMarks(lesson.getLearningDesign());
  
  	Map<Integer, GradebookUserLesson> userToGradebookUserLessonMap = getUserToGradebookUserLessonMap(lesson, null);
  
  	//update for all users in activity
  	Set<User> users = lesson.getAllLearners();
  	for (User user : users) {
  	    Integer userId = user.getUserId();
  	    GradebookUserLesson gradebookUserLesson = userToGradebookUserLessonMap.get(userId);
  
  	    List<GradebookUserActivity> userActivities = gradebookDAO.getGradebookUserActivitiesForLesson(lessonId,
  		    userId);
  	    Double totalMark = calculateLessonMark(weighted, userActivities, null);
  
  	    if (totalMark != null) {
  
  		if (totalMark > 0 && gradebookUserLesson == null) {
  		    throw new Exception("An error detected: user (userId:" + userId + ") has total mark that equals to "
  			    + totalMark + " but no assocciated gradebookUserLesson exist ");
  		}
  
  		if (gradebookUserLesson != null) {
  		    gradebookUserLesson.setMark(totalMark);
  		    gradebookDAO.insertOrUpdate(gradebookUserLesson);
  		}
  	    }
  	}
      }
  
      @Override
      public void recalculateGradebookMarksForActivity(Activity activity) {
  	Long activityId = activity.getActivityId();
  	Lesson lesson = lessonDAO.getLessonForActivity(activityId);
  
  	if ((lesson == null) || (activity == null) || !(activity instanceof ToolActivity)) {
  	    return;
  	}
  
  	ToolActivity toolActivity = (ToolActivity) activity;
  	ActivityEvaluation eval = activityDAO.getEvaluationByActivityId(toolActivity.getActivityId());
  	if (eval == null) {
  	    return;
  	}
  
  	// Getting the first activity evaluation
  	String toolOutputDefinition = eval.getToolOutputDefinition();
  
  	Map<Integer, GradebookUserActivity> userToGradebookUserActivityMap = getUserToGradebookUserActivityMap(activity,
  		null);
  	Map<Integer, GradebookUserLesson> userToGradebookUserLessonMap = getUserToGradebookUserLessonMap(lesson, null);
  	List<ToolOutput> toolOutputs = toolService.getOutputsFromTool(toolOutputDefinition, toolActivity);
  
  	//update for all users in activity
  	List<User> users = learnerProgressDAO.getLearnersCompletedActivity(activity);
  	for (User user : users) {
  	    //find according toolOutput
  	    ToolOutput toolOutput = null;
  	    for (ToolOutput toolOutputIter : toolOutputs) {
  		if (toolOutputIter.getUserId().equals(user.getUserId())) {
  		    toolOutput = toolOutputIter;
  		}
  	    }
  
  	    //in case of toolOutput == null (which means no results in the tool but user nonetheless has finished activity), assign 0 as a result
  	    Double outputDouble = (toolOutput == null || toolOutput.getValue() == null) ? null
  		    : toolOutput.getValue().getDouble();
  
  	    GradebookUserActivity gradebookUserActivity = userToGradebookUserActivityMap.get(user.getUserId());
  	    GradebookUserLesson gradebookUserLesson = userToGradebookUserLessonMap.get(user.getUserId());
  
  	    // Only set the mark if it hasnt previously been set by a teacher
  	    if ((gradebookUserActivity == null) || !gradebookUserActivity.getMarkedInGradebook()) {
  		updateUserActivityGradebookMark(lesson, user, toolActivity, outputDouble, false, false,
  			gradebookUserActivity, gradebookUserLesson);
  	    }
  	}
      }
  
      @Override
      public void updateGradebookUserActivityMark(Lesson lesson, Activity activity, User learner) {
  	ToolSession toolSession = toolService.getToolSessionByLearner(learner, activity);
  
  	if ((toolSession == null) || (toolSession == null) || (learner == null) || (lesson == null)
  		|| (activity == null) || !(activity instanceof ToolActivity)) {
  	    return;
  	}
  	ToolActivity toolActivity = (ToolActivity) activity;
  
  	// Getting the first activity evaluation
  	ActivityEvaluation eval = activityDAO.getEvaluationByActivityId(toolActivity.getActivityId());
  	if (eval == null) {
  	    return;
  	}
  
  	try {
  	    ToolOutput toolOutput = toolService.getOutputFromTool(eval.getToolOutputDefinition(), toolSession,
  		    learner.getUserId());
  
  	    if (toolOutput != null) {
  		ToolOutputValue outputVal = toolOutput.getValue();
  		if (outputVal != null) {
  		    Double outputDouble = outputVal.getDouble();
  
  		    GradebookUserActivity gradebookUserActivity = getGradebookUserActivity(toolActivity.getActivityId(),
  			    learner.getUserId());
  
  		    // Only set the mark if it hasnt previously been set by a teacher
  		    if ((gradebookUserActivity == null) || !gradebookUserActivity.getMarkedInGradebook()) {
  			updateGradebookUserActivityMark(lesson, learner, toolActivity, outputDouble, false, false);
  		    }
  		}
  	    }
  	} catch (ToolException e) {
  	    logger.debug(
  		    "Runtime exception when attempted to get outputs for activity: " + toolActivity.getActivityId(), e);
  	}
      }
  
      @Override
      public void updateGradebookUserActivityMark(Lesson lesson, User learner, Activity activity, Double mark,
  	    Boolean markedInGradebook, boolean isAuditLogRequired) {
  	GradebookUserActivity gradebookUserActivity = gradebookDAO
  		.getGradebookUserDataForActivity(activity.getActivityId(), learner.getUserId());
  
  	GradebookUserLesson gradebookUserLesson = gradebookDAO.getGradebookUserDataForLesson(lesson.getLessonId(),
  		learner.getUserId());
  
  	updateUserActivityGradebookMark(lesson, learner, activity, mark, markedInGradebook, isAuditLogRequired,
  		gradebookUserActivity, gradebookUserLesson);
      }
  
      @Override
      public boolean isWeightedMarks(Long lessonId) {
  	Lesson lesson = lessonService.getLesson(lessonId);
  	return toolService.isWeightedMarks(lesson.getLearningDesign());
      }
  
      @Override
      public List<String[]> getWeights(LearningDesign design) {
  	List<String[]> evaluations = new ArrayList<>();
  	Set<Activity> activities = design.getActivities();
  	for (Activity activity : activities) {
  	    if (activity.isToolActivity()) {
  		// fetch real object, otherwise there is a cast error
  		ToolActivity act = (ToolActivity) activityDAO.getActivityByActivityId(activity.getActivityId());
  		ActivityEvaluation eval = activityDAO.getEvaluationByActivityId(act.getActivityId());
  		if (eval != null && eval.getWeight() != null && eval.getWeight() > 0) {
  		    String[] evaluation = new String[3];
  		    evaluation[0] = act.getTitle();
  		    evaluation[1] = eval.getToolOutputDefinition();
  		    evaluation[2] = eval.getWeight().toString() + '%';
  		    evaluations.add(evaluation);
  		}
  	    }
  	}
  	return evaluations;
      }
  
      /*
       * TODO Method is not in use. Remove it?
       *
       * private void updateUserActivityGradebookMark(Lesson lesson, Activity activity, User learner) {
       * ToolSession toolSession = toolService.getToolSessionByLearner(learner, activity);
       *
       * if ((toolSession == null) || (toolSession == null) || (learner == null) || (lesson == null)
       * || (activity == null) || !(activity instanceof ToolActivity)
       * || (((ToolActivity) activity).getEvaluation() == null)) {
       * return;
       * }
       * ToolActivity toolActivity = (ToolActivity) activity;
       *
       * // Getting the first activity evaluation
       * ActivityEvaluation eval = toolActivity.getEvaluation();
       *
       * try {
       * ToolOutput toolOutput = toolService.getOutputFromTool(eval.getToolOutputDefinition(), toolSession,
       * learner.getUserId());
       *
       * if (toolOutput != null) {
       * ToolOutputValue outputVal = toolOutput.getValue();
       * if (outputVal != null) {
       * Double outputDouble = outputVal.getDouble();
       *
       * GradebookUserActivity gradebookUserActivity = getGradebookUserActivity(toolActivity.getActivityId(),
       * learner.getUserId());
       *
       * // Only set the mark if it hasnt previously been set by a teacher
       * if ((gradebookUserActivity == null) || !gradebookUserActivity.getMarkedInGradebook()) {
       * updateGradebookUserActivityMark(lesson, learner, toolActivity, outputDouble, false, false);
       * }
       * }
       * }
       *
       * } catch (ToolException e) {
       * logger.debug(
       * "Runtime exception when attempted to get outputs for activity: " + toolActivity.getActivityId(), e);
       * }
       * }
       */
  
      /**
       * It's the same method as above, it only also accepts gradebookUserActivity and gradebookUserLesson as parameters.
       */
      private void updateUserActivityGradebookMark(Lesson lesson, User learner, Activity activity, Double mark,
  	    Boolean markedInGradebook, boolean isAuditLogRequired, GradebookUserActivity gradebookUserActivity,
  	    GradebookUserLesson gradebookUserLesson) {
  	if ((lesson != null) && (activity != null) && (learner != null) && activity.isToolActivity()) {
  
  	    // First, update the mark for the activity
  	    if (gradebookUserActivity == null) {
  		gradebookUserActivity = new GradebookUserActivity((ToolActivity) activity, learner);
  	    }
  	    String oldMark = (gradebookUserActivity.getMark() == null) ? "-"
  		    : gradebookUserActivity.getMark().toString();
  	    Double oldActivityMark = gradebookUserActivity.getMark();
  
  	    gradebookUserActivity.setMark(mark);
  	    gradebookUserActivity.setUpdateDate(new Date());
  	    gradebookUserActivity.setMarkedInGradebook(markedInGradebook);
  	    gradebookDAO.insertOrUpdate(gradebookUserActivity);
  	    //flush the session in order to make updated mark be available at calculating lesson total mark
  	    gradebookDAO.flush();
  
  	    // Now update the lesson mark
  	    if (gradebookUserLesson == null) {
  		gradebookUserLesson = new GradebookUserLesson(lesson, learner);
  		gradebookDAO.insertOrUpdate(gradebookUserLesson);
  		//flush the session to delay the same gradebookUserLesson from being inserted by another user (it  primarily targets situation of leader updating marks for all non-leaders in a group)
  		gradebookDAO.flush();
  	    }
  
  	    boolean isWeightedMarks = toolService.isWeightedMarks(lesson.getLearningDesign());
  	    aggregateTotalMarkForLesson(isWeightedMarks, gradebookUserLesson, gradebookUserActivity, oldActivityMark);
  
  	    // audit log changed gradebook mark
  	    if (isAuditLogRequired) {
  		UserDTO monitorUser = (UserDTO) SessionManager.getSession().getAttribute(AttributeNames.USER);
  		String markStr = mark == null ? "" : mark.toString();
  		String[] args = new String[] { learner.getLogin() + "(" + learner.getUserId() + ")",
  			lesson.getLessonId().toString(), activity.getActivityId().toString(), oldMark.toString(),
  			markStr };
  		String message = messageService.getMessage("audit.activity.change.mark", args);
  		logEventService.logEvent(LogEvent.TYPE_MARK_UPDATED, monitorUser.getUserID(), learner.getUserId(),
  			lesson.getLessonId(), activity.getActivityId(), message);
  	    }
  
  	    //propagade mark to integration server, if the lesson has been finished by the learner
  	    LearnerProgress learnerProgress = lessonService.getUserProgressForLesson(learner.getUserId(),
  		    lesson.getLessonId());
  	    if (learnerProgress != null && learnerProgress.isComplete()) {
  		integrationService.pushMarkToLtiConsumer(learner, lesson, gradebookUserLesson.getMark());
  	    }
  	}
      }
  
      @Override
      public void updateUserLessonGradebookFeedback(Lesson lesson, User learner, String feedback) {
  
  	GradebookUserLesson gradebookUserLesson = gradebookDAO.getGradebookUserDataForLesson(lesson.getLessonId(),
  		learner.getUserId());
  
  	if (gradebookUserLesson == null) {
  	    gradebookUserLesson = new GradebookUserLesson(lesson, learner);
  	}
  
  	gradebookUserLesson.setFeedback(feedback);
  	gradebookDAO.insertOrUpdate(gradebookUserLesson);
      }
  
      @Override
      public void updateGradebookUserActivityFeedback(Activity activity, User learner, String feedback) {
  
  	GradebookUserActivity gradebookUserActivity = gradebookDAO
  		.getGradebookUserDataForActivity(activity.getActivityId(), learner.getUserId());
  
  	if (gradebookUserActivity == null) {
  	    gradebookUserActivity = new GradebookUserActivity((ToolActivity) activity, learner);
  	}
  
  	gradebookUserActivity.setFeedback(feedback);
  	gradebookUserActivity.setUpdateDate(new Date());
  	gradebookDAO.insertOrUpdate(gradebookUserActivity);
      }
  
      private void updateUserActivityGradebookFeedback(Activity activity, User learner, String feedback) {
  
  	GradebookUserActivity gradebookUserActivity = gradebookDAO
  		.getGradebookUserDataForActivity(activity.getActivityId(), learner.getUserId());
  
  	if (gradebookUserActivity == null) {
  	    gradebookUserActivity = new GradebookUserActivity((ToolActivity) activity, learner);
  	}
  
  	gradebookUserActivity.setFeedback(feedback);
  	gradebookUserActivity.setUpdateDate(new Date());
  	gradebookDAO.insertOrUpdate(gradebookUserActivity);
      }
  
      @Override
      public void toggleMarksReleased(Long lessonId) {
  
  	Lesson lesson = lessonService.getLesson(lessonId);
  
  	boolean isMarksReleased = !lesson.getMarksReleased();
  	lesson.setMarksReleased(isMarksReleased);
  	userService.save(lesson);
  
  	if (logger.isDebugEnabled()) {
  	    if (isMarksReleased) {
  		logger.debug("Marks were released for lesson ID: " + lessonId);
  	    } else {
  		logger.debug("Marks were hidden for lesson ID: " + lessonId);
  	    }
  	}
  
  	// audit log marks released
  	UserDTO monitor = (UserDTO) SessionManager.getSession().getAttribute(AttributeNames.USER);
  	String messageKey = (isMarksReleased) ? "audit.marks.released.on" : "audit.marks.released.off";
  	String message = messageService.getMessage(messageKey, new String[] { lessonId.toString() });
  	logEventService.logEvent(LogEvent.TYPE_MARK_RELEASED, monitor.getUserID(), null, lessonId, null, message);
  
  	try {
  	    scheduleReleaseMarks(lessonId, null, false, null);
  	} catch (SchedulerException e) {
  	    logger.error("Error when canceling scheduled learner marks release for lesson ID " + lessonId);
  	}
      }
  
      @Override
      public boolean releaseMarks(Long lessonId, int userId) {
  	Lesson lesson = lessonService.getLesson(lessonId);
  
  	boolean wasMarksReleased = lesson.getMarksReleased();
  	if (wasMarksReleased) {
  	    return false;
  	}
  
  	lesson.setMarksReleased(true);
  	userService.save(lesson);
  
  	// audit log marks released
  	String message = messageService.getMessage("audit.marks.released.on", new String[] { lessonId.toString() });
  	logEventService.logEvent(LogEvent.TYPE_MARK_RELEASED, userId, null, lessonId, null, message);
  
  	if (logger.isDebugEnabled()) {
  	    logger.debug("Marks were released for lesson ID: " + lessonId);
  	}
  
  	try {
  	    scheduleReleaseMarks(lessonId, null, false, null);
  	} catch (SchedulerException e) {
  	    logger.error("Error when canceling scheduled learner marks release for lesson ID " + lessonId);
  	}
  
  	return true;
      }
  
      @Override
      public Map<String, Object> getReleaseMarksSchedule(long lessonId, int currentUserId) {
  	String triggerName = "releaseMarksTrigger:" + lessonId;
  	try {
  	    Trigger releaseMarksTrigger = scheduler.getTrigger(TriggerKey.triggerKey(triggerName));
  	    if (releaseMarksTrigger == null) {
  		return null;
  	    }
  
  	    Date scheduleDate = releaseMarksTrigger.getFireTimeAfter(new Date());
  	    if (scheduleDate == null) {
  		return null;
  	    }
  
  	    Map<String, Object> result = new HashMap<>(releaseMarksTrigger.getJobDataMap());
  	    User user = gradebookDAO.find(User.class, currentUserId);
  	    TimeZone userTimeZone = TimeZone.getTimeZone(user.getTimeZone());
  	    result.put("userTimeZoneScheduleDate", DateUtil.convertToTimeZoneFromDefault(userTimeZone, scheduleDate));
  	    result.put("sendEmails",
  		    scheduler.getJobDetail(releaseMarksTrigger.getJobKey()).getJobDataMap().get("sendEmails"));
  	    return result;
  
  	} catch (SchedulerException e) {
  	    logger.error("Error while fetching Quartz trigger \"" + triggerName + "\"", e);
  	}
  
  	return null;
      }
  
      @Override
      public void scheduleReleaseMarks(long lessonId, Integer currentUserId, boolean sendEmails, Date scheduleDate)
  	    throws SchedulerException {
  	String triggerName = "releaseMarksTrigger:" + lessonId;
  
  	Trigger releaseMarksTrigger = scheduler.getTrigger(TriggerKey.triggerKey(triggerName));
  	if (releaseMarksTrigger == null) {
  	    if (scheduleDate == null) {
  		// the job was not scheduled and we do not want to schedule it anyway, so just return
  		return;
  	    }
  	} else {
  	    scheduler.deleteJob(releaseMarksTrigger.getJobKey());
  	    logger.debug("Unscheduled release marks for lesson ID " + lessonId);
  	    if (scheduleDate == null) {
  		return;
  	    }
  	    releaseMarksTrigger = null;
  	}
  
  	// setup the message for scheduling job
  	JobDetail releaseMarksJob = JobBuilder.newJob(ReleaseMarksJob.class).withIdentity("releaseMarks:" + lessonId)
  		.withDescription("Release marks for lesson " + lessonId + " by user " + currentUserId)
  		.usingJobData(AttributeNames.PARAM_LESSON_ID, lessonId)
  		.usingJobData(AttributeNames.PARAM_USER_ID, currentUserId).usingJobData("sendEmails", sendEmails)
  		.build();
  
  	User user = gradebookDAO.find(User.class, currentUserId);
  	TimeZone userTimeZone = TimeZone.getTimeZone(user.getTimeZone());
  	Date tzScheduleDate = scheduleDate == null ? null
  		: DateUtil.convertFromTimeZoneToDefault(userTimeZone, scheduleDate);
  	// create customised triggers
  	releaseMarksTrigger = TriggerBuilder.newTrigger().withIdentity(triggerName).startAt(tzScheduleDate).build();
  	scheduler.scheduleJob(releaseMarksJob, releaseMarksTrigger);
  
  	if (logger.isDebugEnabled()) {
  	    logger.debug("Scheduled release marks for lesson ID " + lessonId + " by user ID " + currentUserId
  		    + " for date " + scheduleDate);
  	}
  
  	return;
      }
  
      @Override
      public String getReleaseMarksEmailContent(long lessonID, int userID) {
  	// temporary comment so template gets loaded every time
  	// eventually it will be loaded just once
  //	if (RELEASE_MARKS_EMAIL_TEMPLATE_CONTENT == null) {
  	try {
  	    RELEASE_MARKS_EMAIL_TEMPLATE_CONTENT = Files
  		    .readString(Paths.get(Configuration.get(ConfigurationKeys.LAMS_EAR_DIR), FileUtil.LAMS_WWW_DIR,
  			    "gradebookReleaseLessonMarksEmailTemplate.html"));
  	} catch (Exception e) {
  	    throw new RuntimeException("Can not read release marks email template", e);
  	}
  //	}
  
  	User user = userService.getUserById(userID);
  	Lesson lesson = lessonService.getLesson(lessonID);
  	StringBuilder content = new StringBuilder(RELEASE_MARKS_EMAIL_TEMPLATE_CONTENT);
  
  	int placeholderStart = content.indexOf(RELEASE_MARKS_EMAIL_PAGE_TITLE_PLACEHOLDER);
  	int placeholderEnd = placeholderStart + RELEASE_MARKS_EMAIL_PAGE_TITLE_PLACEHOLDER.length();
  	content.replace(placeholderStart, placeholderEnd, messageService.getMessage(
  		"gradebook.monitor.releasemarks.email.content.subject", new Object[] { lesson.getLessonName() }));
  
  	placeholderStart = content.indexOf(RELEASE_MARKS_EMAIL_TOP_HEADER_PLACEHOLDER);
  	placeholderEnd = placeholderStart + RELEASE_MARKS_EMAIL_TOP_HEADER_PLACEHOLDER.length();
  	content.replace(placeholderStart, placeholderEnd,
  		messageService.getMessage("gradebook.monitor.releasemarks.email.content.top.header"));
  
  	placeholderStart = content.indexOf(RELEASE_MARKS_EMAIL_CONTENT_START_PLACEHOLDER);
  	placeholderEnd = placeholderStart + RELEASE_MARKS_EMAIL_CONTENT_START_PLACEHOLDER.length();
  	content.replace(placeholderStart, placeholderEnd,
  		messageService.getMessage("gradebook.monitor.releasemarks.email.content.start",
  			new Object[] { user.getFirstName() + " " + user.getLastName() }));
  
  	placeholderStart = content.indexOf(RELEASE_MARKS_EMAIL_CONTENT_LESSON_NAME_PLACEHOLDER);
  	placeholderEnd = placeholderStart + RELEASE_MARKS_EMAIL_CONTENT_LESSON_NAME_PLACEHOLDER.length();
  	content.replace(placeholderStart, placeholderEnd, messageService.getMessage(
  		"gradebook.monitor.releasemarks.email.content.lesson.name", new Object[] { lesson.getLessonName() }));
  
  	placeholderStart = content.indexOf(RELEASE_MARKS_EMAIL_LESSON_NAME_PLACEHOLDER);
  	placeholderEnd = placeholderStart + RELEASE_MARKS_EMAIL_LESSON_NAME_PLACEHOLDER.length();
  	content.replace(placeholderStart, placeholderEnd, lesson.getLessonName());
  
  	placeholderStart = content.indexOf(RELEASE_MARKS_EMAIL_RELEASE_DATE_PLACEHOLDER);
  	placeholderEnd = placeholderStart + RELEASE_MARKS_EMAIL_RELEASE_DATE_PLACEHOLDER.length();
  	content.replace(placeholderStart, placeholderEnd, RELEASE_MARKS_EMAIL_DATE_FORMAT.format(new Date()));
  
  	boolean isWeighted = isWeightedMarks(lessonID);
  	GradebookUserLesson gradebookUserLesson = getGradebookUserLesson(lessonID, userID);
  	placeholderStart = content.indexOf(RELEASE_MARKS_EMAIL_OVERALL_GRADE_PLACEHOLDER);
  	placeholderEnd = placeholderStart + RELEASE_MARKS_EMAIL_OVERALL_GRADE_PLACEHOLDER.length();
  	content.replace(placeholderStart, placeholderEnd,
  		messageService.getMessage("gradebook.monitor.releasemarks.email.content.overall.grade",
  			new Object[] { gradebookUserLesson == null || gradebookUserLesson.getMark() == null ? "-"
  				: GradebookUtil.niceFormatting(gradebookUserLesson.getMark(), isWeighted) }));
  
  	placeholderStart = content.indexOf(RELEASE_MARKS_EMAIL_CONTENT_END_PLACEHOLDER);
  	placeholderEnd = placeholderStart + RELEASE_MARKS_EMAIL_CONTENT_END_PLACEHOLDER.length();
  	content.replace(placeholderStart, placeholderEnd,
  		messageService.getMessage("gradebook.monitor.releasemarks.email.content.end"));
  
  	placeholderStart = content.indexOf(RELEASE_MARKS_EMAIL_CONTENT_THANKS_PLACEHOLDER);
  	placeholderEnd = placeholderStart + RELEASE_MARKS_EMAIL_CONTENT_THANKS_PLACEHOLDER.length();
  	content.replace(placeholderStart, placeholderEnd,
  		messageService.getMessage("gradebook.monitor.releasemarks.email.content.thanks"));
  
  	placeholderStart = content.indexOf(RELEASE_MARKS_EMAIL_FOOTER_PLACEHOLDER);
  	placeholderEnd = placeholderStart + RELEASE_MARKS_EMAIL_FOOTER_PLACEHOLDER.length();
  	content.replace(placeholderStart, placeholderEnd,
  		messageService.getMessage("gradebook.monitor.releasemarks.email.content.footer"));
  
  	placeholderStart = content.indexOf(RELEASE_MARKS_EMAIL_ACTIVITY_ROW_START);
  	placeholderEnd = content.indexOf(RELEASE_MARKS_EMAIL_ACTIVITY_ROW_END)
  		+ RELEASE_MARKS_EMAIL_ACTIVITY_ROW_END.length();
  
  	String activityRowTemplate = content.substring(placeholderStart, placeholderEnd)
  		.replace(RELEASE_MARKS_EMAIL_ACTIVITY_ROW_START, "");
  
  	content.replace(placeholderStart, placeholderEnd, RELEASE_MARKS_EMAIL_ACTIVITY_ROW_END);
  
  	List<GradebookGridRowDTO> gradebookActivityDTOs = getGBLessonComplete(lessonID, userID);
  	for (GradebookGridRowDTO activityDTO : gradebookActivityDTOs) {
  	    StringBuilder activityRowContent = new StringBuilder(activityRowTemplate);
  
  	    placeholderStart = activityRowContent.indexOf(RELEASE_MARKS_EMAIL_ACTIVITY_NAME_PLACEHOLDER);
  	    placeholderEnd = placeholderStart + RELEASE_MARKS_EMAIL_ACTIVITY_NAME_PLACEHOLDER.length();
  	    activityRowContent.replace(placeholderStart, placeholderEnd, activityDTO.getRowName());
  
  	    String icon = "";
  	    if (activityDTO.getStatus().contains("success")) {
  		icon = "&#10004;";
  	    } else if (activityDTO.getStatus().contains("cog")) {
  		icon = "&#9881;";
  	    }
  
  	    placeholderStart = activityRowContent.indexOf(RELEASE_MARKS_EMAIL_ACTIVITY_PROGRESS_ICON_PLACEHOLDER);
  	    placeholderEnd = placeholderStart + RELEASE_MARKS_EMAIL_ACTIVITY_PROGRESS_ICON_PLACEHOLDER.length();
  	    activityRowContent.replace(placeholderStart, placeholderEnd, icon);
  
  	    placeholderStart = activityRowContent.indexOf(RELEASE_MARKS_EMAIL_ACTIVITY_GRADE_PLACEHOLDER);
  	    placeholderEnd = placeholderStart + RELEASE_MARKS_EMAIL_ACTIVITY_GRADE_PLACEHOLDER.length();
  	    activityRowContent.replace(placeholderStart, placeholderEnd,
  		    activityDTO.getMark() == null ? "-" : GradebookUtil.niceFormatting(activityDTO.getMark()));
  
  	    placeholderStart = content.indexOf(RELEASE_MARKS_EMAIL_ACTIVITY_ROW_END);
  	    placeholderEnd = placeholderStart + RELEASE_MARKS_EMAIL_ACTIVITY_ROW_END.length();
  	    content.replace(placeholderStart, placeholderEnd, activityRowContent.toString());
  	}
  
  	placeholderStart = content.indexOf(RELEASE_MARKS_EMAIL_ACTIVITY_ROW_END);
  	placeholderEnd = placeholderStart + RELEASE_MARKS_EMAIL_ACTIVITY_ROW_END.length();
  	content.replace(placeholderStart, placeholderEnd, "");
  
  	return content.toString();
      }
  
      @SuppressWarnings("unchecked")
      @Override
      public void sendReleaseMarksEmails(long lessonId, Collection<Integer> recipientIDs,
  	    IEventNotificationService eventNotificationService) {
  	Lesson lesson = lessonService.getLesson(lessonId);
  	String emailSubject = messageService.getMessage("gradebook.monitor.releasemarks.email.content.subject",
  		new Object[] { lesson.getLessonName() });
  
  	if (recipientIDs == null) {
  	    recipientIDs = ((List<User>) lessonService.getActiveLessonLearners(lessonId)).stream()
  		    .collect(Collectors.mapping(User::getUserId, Collectors.toSet()));
  	}
  
  	for (Integer recipientId : recipientIDs) {
  	    eventNotificationService.sendMessage(null, recipientId, IEventNotificationService.DELIVERY_METHOD_MAIL,
  		    emailSubject, getReleaseMarksEmailContent(lessonId, recipientId), true);
  	}
  
  	if (logger.isDebugEnabled()) {
  	    logger.debug("Sent emails with marks to learners of lesson ID: " + lessonId);
  	}
      }
  
      @Override
      public List<GBLessonGridRowDTO> getGBLessonRows(Organisation organisation, User user, User viewer,
  	    boolean isGroupManager, GBGridView view, int page, int size, String sortBy, String sortOrder,
  	    String searchString, TimeZone userTimeZone) {
  	List<GBLessonGridRowDTO> lessonRows = new ArrayList<>();
  	Integer userId = user.getUserId();
  	Integer orgId = organisation.getOrganisationId();
  
  	if (organisation != null) {
  	    List<Lesson> lessons = (view == GBGridView.MON_COURSE || view == GBGridView.LIST
  		    || view == GBGridView.MON_USER)
  			    ? gradebookDAO.getLessonsByGroupAndUser(isGroupManager ? null : viewer.getUserId(), true,
  				    orgId, page, size, sortBy, sortOrder, searchString)
  			    : lessonService.getLessonsByGroupAndUser(userId, orgId);
  
  	    if (lessons != null) {
  
  		for (Lesson lesson : lessons) {
  
  		    // For My Grades gradebook page: don't include lesson in list if the user doesn't have permission.
  		    if (view == GBGridView.LRN_COURSE) {
  			boolean marksReleased = lesson.getMarksReleased();
  			boolean hasLearnerPermission = lesson.getAllLearners().contains(user);
  			if (!hasLearnerPermission || !marksReleased) {
  			    continue;
  			}
  		    }
  
  		    GBLessonGridRowDTO lessonRow = new GBLessonGridRowDTO();
  		    lessonRows.add(lessonRow);
  		    lessonRow.setLessonName(lesson.getLessonName());
  		    lessonRow.setId(lesson.getLessonId().toString());
  		    boolean isWeightedMarks = toolService.isWeightedMarks(lesson.getLearningDesign());
  		    lessonRow.setDisplayMarkAsPercent(isWeightedMarks);
  
  		    if (view == GBGridView.LIST) {
  			continue;
  
  		    } else if (view == GBGridView.MON_COURSE) {
  
  			// Setting the averages for monitor view
  			lessonRow.setMedianTimeTaken(gradebookDAO.getMedianTimeTakenLesson(lesson.getLessonId()));
  			lessonRow.setAverageMark(gradebookDAO.getAverageMarkForLesson(lesson.getLessonId()));
  
  			// Set the gradebook monitor url
  			String gbMonURL = Configuration.get(ConfigurationKeys.SERVER_URL)
  				+ "gradebook/gradebookMonitoring.do?lessonID=" + lesson.getLessonId().toString();
  			lessonRow.setGradebookMonitorURL(gbMonURL);
  
  			Date startDate = lesson.getStartDateTime();
  			if (startDate != null) {
  			    if (userTimeZone != null) {
  				startDate = DateUtil.convertToTimeZoneFromDefault(userTimeZone, startDate);
  			    }
  			    lessonRow.setStartDate(startDate);
  			}
  
  		    } else if ((view == GBGridView.LRN_COURSE) || (view == GBGridView.MON_USER)) {
  
  			GradebookUserLesson gbLesson = gradebookDAO.getGradebookUserDataForLesson(lesson.getLessonId(),
  				userId);
  
  			lessonRow.setMedianTimeTaken(gradebookDAO.getMedianTimeTakenLesson(lesson.getLessonId()));
  			lessonRow.setAverageMark(gradebookDAO.getAverageMarkForLesson(lesson.getLessonId()));
  
  			if (gbLesson != null) {
  			    lessonRow.setMark(gbLesson.getMark());
  			    lessonRow.setFeedback(gbLesson.getFeedback());
  			}
  
  			LearnerProgress learnerProgress = lessonService.getUserProgressForLesson(userId,
  				lesson.getLessonId());
  			lessonRow.setStatus(getLessonStatusStr(learnerProgress));
  			if (learnerProgress != null) {
  			    Date startDate = learnerProgress.getStartDate();
  			    Date finishDate = learnerProgress.getFinishDate();
  
  			    if (startDate != null && finishDate != null) {
  				lessonRow.setTimeTaken(finishDate.getTime() - startDate.getTime());
  			    }
  
  			    if (startDate != null) {
  				if (userTimeZone != null) {
  				    startDate = DateUtil.convertToTimeZoneFromDefault(userTimeZone, startDate);
  				}
  				lessonRow.setStartDate(startDate);
  			    }
  
  			    if (learnerProgress.getFinishDate() != null) {
  				if (userTimeZone != null) {
  				    finishDate = DateUtil.convertToTimeZoneFromDefault(userTimeZone, finishDate);
  				}
  				lessonRow.setFinishDate(finishDate);
  			    }
  			}
  		    }
  
  		    if (lesson.getOrganisation().getOrganisationId() != organisation.getOrganisationId()) {
  			lessonRow.setSubGroup(lesson.getOrganisation().getName());
  		    } else {
  			lessonRow.setSubGroup("");
  		    }
  		}
  	    }
  	}
  
  	return lessonRows;
      }
  
      private Map<ToolActivity, List<GBUserGridRowDTO>> getDataForLessonGradebookExport(Lesson lesson) {
  
  	Map<ToolActivity, List<GBUserGridRowDTO>> activityToUserDTOMap = new LinkedHashMap<>();
  
  	Set<User> learners = new TreeSet<User>(new LastNameAlphabeticComparator());
  	if (lesson.getAllLearners() != null) {
  	    learners.addAll(lesson.getAllLearners());
  	}
  
  	Map<Integer, LearnerProgress> userToLearnerProgressMap = getUserToLearnerProgressMap(lesson, null);
  	List<Activity> activities = getLessonActivitiesForLesson(lesson);
  
  	for (Activity activity : activities) {
  	    getActivityDataForLessonGradebookExport(activityToUserDTOMap, learners, userToLearnerProgressMap, activity);
  	}
  
  	return activityToUserDTOMap;
      }
  
      @SuppressWarnings("unchecked")
      private void getActivityDataForLessonGradebookExport(Map<ToolActivity, List<GBUserGridRowDTO>> activityToUserDTOMap,
  	    Set<User> learners, Map<Integer, LearnerProgress> userToLearnerProgressMap, Activity activity) {
  
  	if (activity.isToolActivity()) {
  	    List<GBUserGridRowDTO> userDTOs = getToolActivityDataForLessonGradebookExport(learners,
  		    userToLearnerProgressMap, (ToolActivity) activity);
  	    activityToUserDTOMap.put((ToolActivity) activity, userDTOs);
  
  	} else if (activity instanceof ComplexActivity) {
  	    // encountered a sequence, branch, optional or parallel within a branching sequence
  	    Set<User> complexLearners = learners;
  
  	    if (activity instanceof SequenceActivity) {
  		// use only a subset of learners for this branch of the branching activity based on who has started the branch
  		complexLearners = new TreeSet<User>(new LastNameAlphabeticComparator());
  		for (User learner : learners) {
  		    LearnerProgress learnerProgress = userToLearnerProgressMap.get(learner.getUserId());
  		    if (learnerProgress != null && (learnerProgress.getCompletedActivities().get(activity) != null
  			    || learnerProgress.getAttemptedActivities().get(activity) != null)) {
  			complexLearners.add(learner);
  		    }
  		}
  	    }
  
  	    ComplexActivity sequence = (ComplexActivity) activity;
  	    Set<Activity> childActivities = sequence.getActivities();
  	    for (Activity childActivity : childActivities) {
  		getActivityDataForLessonGradebookExport(activityToUserDTOMap, complexLearners, userToLearnerProgressMap,
  			activityDAO.getActivityByActivityId(childActivity.getActivityId()));
  	    }
  	}
      }
  
      private List<GBUserGridRowDTO> getToolActivityDataForLessonGradebookExport(Set<User> learners,
  	    Map<Integer, LearnerProgress> userToLearnerProgressMap, ToolActivity toolActivity) {
  
  	Map<Integer, GradebookUserActivity> userToGradebookUserActivityMap = getUserToGradebookUserActivityMap(
  		toolActivity, null);
  
  	List<GBUserGridRowDTO> userDTOs = new ArrayList<>();
  
  	for (User learner : learners) {
  	    GBUserGridRowDTO userDTO = new GBUserGridRowDTO(learner);
  
  	    // Set the progress
  	    LearnerProgress learnerProgress = userToLearnerProgressMap.get(learner.getUserId());
  	    userDTO.setTimeTaken(getActivityDuration(learnerProgress, toolActivity));
  	    userDTO.setStartDate(getActivityStartDate(learnerProgress, toolActivity, null));
  	    userDTO.setFinishDate(getActivityFinishDate(learnerProgress, toolActivity, null));
  
  	    // Add marks and feedback
  	    GradebookUserActivity gradebookUserActivity = userToGradebookUserActivityMap.get(learner.getUserId());
  	    if (gradebookUserActivity != null) {
  		userDTO.setMark(gradebookUserActivity.getMark());
  	    }
  	    userDTOs.add(userDTO);
  	}
  
  	return userDTOs;
      }
  
      @Override
      @SuppressWarnings("unchecked")
      public List<ExcelSheet> exportLessonGradebook(Lesson lesson) {
  	boolean isWeighted = toolService.isWeightedMarks(lesson.getLearningDesign());
  
  	List<ExcelSheet> sheets = new LinkedList<>();
  
  	// -------------------- process summary excel page --------------------------------
  
  	ExcelSheet summarySheet = new ExcelSheet(getMessage("gradebook.export.lesson.summary"));
  	sheets.add(summarySheet);
  
  	// Adding the lesson average data to the summary
  	Double lessonAverageMarkValue = getAverageMarkForLesson(lesson.getLessonId());
  	ExcelRow lessonAverageMark = summarySheet.initRow();
  	lessonAverageMark.addCell(getMessage("gradebook.export.average.lesson.mark"), true);
  