Index: lams_gradebook/conf/language/lams/ApplicationResources.properties =================================================================== diff -u -re3c7fc1238a17dfc12d57b5e0b8506fe81edbb51 -r8a7a259deba12163c46440a712e41bde926fdaf8 --- lams_gradebook/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision e3c7fc1238a17dfc12d57b5e0b8506fe81edbb51) +++ lams_gradebook/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 8a7a259deba12163c46440a712e41bde926fdaf8) @@ -62,7 +62,8 @@ gradebook.exportcourse.current.activity = Current activity: {0} gradebook.gridtitle.lesson.view = Lesson View gradebook.gridtitle.learner.view = Grades by learner -label.button.export = Export +label.button.export.selected.lessons = Export selected lessons +label.button.export.tbl = Export TBL lessons label.lessons = Lessons label.group = Group label.max.possible = Max mark available Index: lams_gradebook/src/java/org/lamsfoundation/lams/gradebook/service/GradebookService.java =================================================================== diff -u -re57933d5f4b215e037a34229c3d479f707cf5156 -r8a7a259deba12163c46440a712e41bde926fdaf8 --- lams_gradebook/src/java/org/lamsfoundation/lams/gradebook/service/GradebookService.java (.../GradebookService.java) (revision e57933d5f4b215e037a34229c3d479f707cf5156) +++ lams_gradebook/src/java/org/lamsfoundation/lams/gradebook/service/GradebookService.java (.../GradebookService.java) (revision 8a7a259deba12163c46440a712e41bde926fdaf8) @@ -74,6 +74,7 @@ import org.lamsfoundation.lams.learningdesign.ToolActivity; import org.lamsfoundation.lams.learningdesign.dao.IActivityDAO; import org.lamsfoundation.lams.learningdesign.dto.ActivityURL; +import org.lamsfoundation.lams.learningdesign.service.ILearningDesignService; import org.lamsfoundation.lams.lesson.CompletedActivityProgress; import org.lamsfoundation.lams.lesson.CompletedActivityProgressArchive; import org.lamsfoundation.lams.lesson.LearnerProgress; @@ -165,6 +166,7 @@ private ILearnerProgressDAO learnerProgressDAO; private ILessonDAO lessonDAO; private ILessonService lessonService; + private ILearningDesignService learningDesignService; private IUserManagementService userService; private IActivityDAO activityDAO; private MessageService messageService; @@ -1754,6 +1756,76 @@ } @Override + public List exportCourseTBLGradebook(Integer userId, Integer organisationId) { + List sheets = new LinkedList<>(); + + // The entire data list + ExcelSheet sheet = new ExcelSheet(getMessage("gradebook.exportcourse.course.summary")); + sheets.add(sheet); + ExcelRow headerRow = sheet.initRow(); + headerRow.addCell(getMessage("gradebook.export.login"), true); + headerRow.addCell(getMessage("gradebook.export.last.name"), true); + headerRow.addCell(getMessage("gradebook.export.first.name"), true); + + List lessons = getTBLLessons(organisationId, userId); + Map iRatActivityIds = new HashMap<>(); + Map tRatActivityIds = new HashMap<>(); + Set allLearners = new TreeSet<>(); + for (Lesson lesson : lessons) { + headerRow.addCell(lesson.getLessonName() + " iRAT", true); + headerRow.addCell(lesson.getLessonName() + " tRAT", true); + + Map activityTypes = learningDesignService + .getAvailableTBLActivityTypes(lesson.getLearningDesign().getLearningDesignId()); + iRatActivityIds.put(lesson.getLessonId(), (Long) activityTypes.get("iraToolActivityId")); + tRatActivityIds.put(lesson.getLessonId(), (Long) activityTypes.get("traToolActivityId")); + + Set lessonLearners = lesson.getAllLearners(); + allLearners.addAll(lessonLearners); + } + + for (User learner : allLearners) { + ExcelRow userDataRow = sheet.initRow(); + userDataRow.addCell(learner.getLogin()); + userDataRow.addCell(learner.getLastName()); + userDataRow.addCell(learner.getFirstName()); + for (Lesson lesson : lessons) { + Long iRatActivityId = iRatActivityIds.get(lesson.getLessonId()); + Double mark = null; + if (iRatActivityId != null) { + GradebookUserActivity gradebookUserActivity = getGradebookUserActivity(iRatActivityId, + learner.getUserId()); + if (gradebookUserActivity != null && gradebookUserActivity.getMark() != null) { + mark = gradebookUserActivity.getMark(); + } + } + if (mark == null) { + userDataRow.addEmptyCell(); + } else { + userDataRow.addCell(GradebookUtil.niceFormatting(mark)); + } + + Long tRatActivityId = tRatActivityIds.get(lesson.getLessonId()); + mark = null; + if (tRatActivityId != null) { + GradebookUserActivity gradebookUserActivity = getGradebookUserActivity(tRatActivityId, + learner.getUserId()); + if (gradebookUserActivity != null && gradebookUserActivity.getMark() != null) { + mark = gradebookUserActivity.getMark(); + } + } + if (mark == null) { + userDataRow.addEmptyCell(); + } else { + userDataRow.addCell(GradebookUtil.niceFormatting(mark)); + } + } + } + + return sheets; + } + + @Override @SuppressWarnings("unchecked") public List exportCourseGradebook(Integer userId, Integer organisationId) { List sheets = new LinkedList<>(); @@ -2195,6 +2267,20 @@ return sheets; } + private List getTBLLessons(Integer organisationId, Integer userId) { + Set lessons = new TreeSet<>(new LessonComparator()); + lessons.addAll(lessonService.getLessonsByGroupAndUser(userId, organisationId)); + + List tblLessons = new LinkedList<>(); + for (Lesson lesson : lessons) { + boolean isTblLesson = learningDesignService.isTBLSequence(lesson.getLearningDesign().getLearningDesignId()); + if (isTblLesson) { + tblLessons.add(lesson); + } + } + return tblLessons; + } + private void addUsernameCells(User learner, ExcelRow userRow) { //first, last names and login String lastName = (learner.getLastName() == null) ? "" : learner.getLastName().toUpperCase(); @@ -2874,6 +2960,10 @@ this.lessonService = lessonService; } + public void setLearningDesignService(ILearningDesignService learningDesignService) { + this.learningDesignService = learningDesignService; + } + public void setUserService(IUserManagementService userService) { this.userService = userService; } Index: lams_gradebook/src/java/org/lamsfoundation/lams/gradebook/web/controller/GradebookMonitoringController.java =================================================================== diff -u -ra40a77e307317e8038ed9e6b8699c18386286497 -r8a7a259deba12163c46440a712e41bde926fdaf8 --- lams_gradebook/src/java/org/lamsfoundation/lams/gradebook/web/controller/GradebookMonitoringController.java (.../GradebookMonitoringController.java) (revision a40a77e307317e8038ed9e6b8699c18386286497) +++ lams_gradebook/src/java/org/lamsfoundation/lams/gradebook/web/controller/GradebookMonitoringController.java (.../GradebookMonitoringController.java) (revision 8a7a259deba12163c46440a712e41bde926fdaf8) @@ -149,7 +149,8 @@ } if (!securityService.hasOrgRole(organisationID, user.getUserID(), new String[] { Role.GROUP_MANAGER, Role.MONITOR }, "get course gradebook page")) { - response.sendError(HttpServletResponse.SC_FORBIDDEN, "User is not a course manager in the organisation"); + response.sendError(HttpServletResponse.SC_FORBIDDEN, + "User is not a course manager or monitor in the organisation"); return null; } @@ -370,7 +371,7 @@ throws IOException { Long lessonID = WebUtil.readLongParam(request, AttributeNames.PARAM_LESSON_ID); if (!securityService.isLessonMonitor(lessonID, getUser().getUserID(), "export lesson gradebook spreadsheet")) { - response.sendError(HttpServletResponse.SC_FORBIDDEN, "User is not a monitor in the lesson"); + response.sendError(HttpServletResponse.SC_FORBIDDEN, "User is not a group manager in the lesson"); } if (log.isDebugEnabled()) { @@ -394,6 +395,39 @@ } /** + * Exports TBL lessons in course to spreadsheet + */ + @RequestMapping("/exportExcelTBLGradebook") + @ResponseBody + public void exportExcelTBLGradebook(HttpServletRequest request, HttpServletResponse response) throws IOException { + Integer organisationID = WebUtil.readIntParam(request, AttributeNames.PARAM_ORGANISATION_ID); + UserDTO user = getUser(); + if (!securityService.isGroupMonitor(organisationID, user.getUserID(), "get course TBL gradebook spreadsheet")) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "User is not a group manager in the organisation"); + return; + } + + Organisation organisation = (Organisation) userManagementService.findById(Organisation.class, organisationID); + if (log.isDebugEnabled()) { + log.debug("Exporting to a spreadsheet course TBL lessons: " + organisationID); + } + List sheets = gradebookService.exportCourseTBLGradebook(user.getUserID(), organisationID); + + String fileName = organisation.getName().replaceAll(" ", "_") + "_TBL.xlsx"; + fileName = FileUtil.encodeFilenameForDownload(request, fileName); + + response.setContentType("application/x-download"); + response.setHeader("Content-Disposition", "attachment;filename=" + fileName); + + // set cookie that will tell JS script that export has been finished + WebUtil.setFileDownloadTokenCookie(request, response); + + // Code to generate file and write file contents to response + ServletOutputStream out = response.getOutputStream(); + ExcelUtil.createExcel(out, sheets, gradebookService.getMessage("gradebook.export.dateheader"), true); + } + + /** * Exports Course Gradebook into excel. */ @RequestMapping("/exportExcelCourseGradebook") @@ -402,9 +436,10 @@ throws IOException { Integer organisationID = WebUtil.readIntParam(request, AttributeNames.PARAM_ORGANISATION_ID); UserDTO user = getUser(); - if (!securityService.hasOrgRole(organisationID, user.getUserID(), new String[] { Role.GROUP_MANAGER }, - "get course gradebook spreadsheet")) { - response.sendError(HttpServletResponse.SC_FORBIDDEN, "User is not a monitor in the organisation"); + + if (!securityService.isGroupMonitor(organisationID, user.getUserID(), "get course gradebook spreadsheet")) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "User is not a group manager in the organisation"); + return; } Organisation organisation = (Organisation) userManagementService.findById(Organisation.class, organisationID); Index: lams_monitoring/src/java/org/lamsfoundation/lams/monitoring/web/MonitoringController.java =================================================================== diff -u -rd416d14c233164ac4787360cbd490f31227694cd -r8a7a259deba12163c46440a712e41bde926fdaf8 --- lams_monitoring/src/java/org/lamsfoundation/lams/monitoring/web/MonitoringController.java (.../MonitoringController.java) (revision d416d14c233164ac4787360cbd490f31227694cd) +++ lams_monitoring/src/java/org/lamsfoundation/lams/monitoring/web/MonitoringController.java (.../MonitoringController.java) (revision 8a7a259deba12163c46440a712e41bde926fdaf8) @@ -51,8 +51,11 @@ import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.lamsfoundation.lams.authoring.IAuthoringService; +import org.lamsfoundation.lams.flux.FluxMap; +import org.lamsfoundation.lams.flux.FluxRegistry; import org.lamsfoundation.lams.learning.service.ILearnerService; import org.lamsfoundation.lams.learningdesign.Activity; +import org.lamsfoundation.lams.learningdesign.ActivityOrderComparator; import org.lamsfoundation.lams.learningdesign.BranchingActivity; import org.lamsfoundation.lams.learningdesign.ChosenBranchingActivity; import org.lamsfoundation.lams.learningdesign.ComplexActivity; @@ -65,12 +68,16 @@ import org.lamsfoundation.lams.learningdesign.SequenceActivity; import org.lamsfoundation.lams.learningdesign.ToolActivity; import org.lamsfoundation.lams.learningdesign.Transition; +import org.lamsfoundation.lams.learningdesign.dao.IActivityDAO; import org.lamsfoundation.lams.learningdesign.exception.LearningDesignException; import org.lamsfoundation.lams.learningdesign.service.ILearningDesignService; import org.lamsfoundation.lams.lesson.LearnerProgress; import org.lamsfoundation.lams.lesson.Lesson; +import org.lamsfoundation.lams.lesson.dto.ActivityTimeLimitDTO; import org.lamsfoundation.lams.lesson.dto.LessonDetailsDTO; import org.lamsfoundation.lams.lesson.service.ILessonService; +import org.lamsfoundation.lams.lesson.util.LearnerActivityCompleteFluxItem; +import org.lamsfoundation.lams.lesson.util.LearnerLessonJoinFluxItem; import org.lamsfoundation.lams.logevent.LogEvent; import org.lamsfoundation.lams.logevent.service.ILogEventService; import org.lamsfoundation.lams.monitoring.MonitoringConstants; @@ -79,6 +86,7 @@ import org.lamsfoundation.lams.monitoring.service.IMonitoringService; import org.lamsfoundation.lams.security.ISecurityService; import org.lamsfoundation.lams.tool.exception.LamsToolServiceException; +import org.lamsfoundation.lams.tool.service.ICommonScratchieService; import org.lamsfoundation.lams.tool.service.ILamsToolService; import org.lamsfoundation.lams.usermanagement.Organisation; import org.lamsfoundation.lams.usermanagement.Role; @@ -96,17 +104,22 @@ import org.lamsfoundation.lams.web.util.AttributeNames; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.util.HtmlUtils; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; +import reactor.core.publisher.Flux; + /** * The action servlet that provide all the monitoring functionalities. It interact with the teacher via JSP monitoring * interface. @@ -118,7 +131,7 @@ public class MonitoringController { private static Logger log = Logger.getLogger(MonitoringController.class); - private static final DateFormat LESSON_SCHEDULING_DATETIME_FORMAT = new SimpleDateFormat("MM/dd/yy HH:mm"); + private static final DateFormat LESSON_SCHEDULING_DATETIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm"); private static final int LATEST_LEARNER_PROGRESS_LESSON_DISPLAY_LIMIT = 53; private static final int LATEST_LEARNER_PROGRESS_ACTIVITY_DISPLAY_LIMIT = 7; @@ -133,6 +146,8 @@ @Autowired private ISecurityService securityService; @Autowired + private IActivityDAO activityDAO; + @Autowired private IMonitoringFullService monitoringService; @Autowired private IUserManagementService userManagementService; @@ -145,7 +160,30 @@ private MessageService messageService; @Autowired private IAuthoringService authoringService; + @Autowired + @Qualifier("scratchieService") + private ICommonScratchieService commonScratchieService; + public MonitoringController() { + // bind sinks so a learner finishing an activity also triggers an update in lesson progress + FluxRegistry.bindSink(CommonConstants.ACTIVITY_ENTERED_SINK_NAME, CommonConstants.LESSON_PROGRESSED_SINK_NAME, + learnerProgressFluxItem -> ((LearnerActivityCompleteFluxItem) learnerProgressFluxItem).getLessonId()); + // bind sinks so a learner entering a lesson also triggers an update in lesson progress + FluxRegistry.bindSink(CommonConstants.LESSON_JOINED_SINK_NAME, CommonConstants.LESSON_PROGRESSED_SINK_NAME, + lessonJoinedFluxItem -> ((LearnerLessonJoinFluxItem) lessonJoinedFluxItem).getLessonId()); + + FluxRegistry.initFluxMap(MonitoringConstants.CANVAS_REFRESH_FLUX_NAME, + CommonConstants.LESSON_PROGRESSED_SINK_NAME, null, lessonId -> "doRefresh", FluxMap.STANDARD_THROTTLE, + FluxMap.STANDARD_TIMEOUT); + FluxRegistry.initFluxMap(MonitoringConstants.GRADEBOOK_REFRESH_FLUX_NAME, + CommonConstants.LESSON_PROGRESSED_SINK_NAME, null, lessonId -> "doRefresh", FluxMap.STANDARD_THROTTLE, + FluxMap.STANDARD_TIMEOUT); + FluxRegistry.initFluxMap(MonitoringConstants.TIME_LIMIT_REFRESH_FLUX_NAME, + CommonConstants.ACTIVITY_TIME_LIMIT_CHANGED_SINK_NAME, + (Collection key, Collection item) -> key.containsAll(item), toolContentIds -> "doRefresh", + FluxMap.SHORT_THROTTLE, FluxMap.STANDARD_TIMEOUT); + } + private Integer getUserId() { HttpSession ss = SessionManager.getSession(); UserDTO user = (UserDTO) ss.getAttribute(AttributeNames.USER); @@ -162,6 +200,27 @@ return null; } + @RequestMapping(path = "/getLearnerProgressUpdateFlux", method = RequestMethod.GET, produces = MediaType.TEXT_EVENT_STREAM_VALUE) + @ResponseBody + public Flux getLearnerProgressUpdateFlux(@RequestParam long lessonId) + throws JsonProcessingException, IOException { + return FluxRegistry.get(MonitoringConstants.CANVAS_REFRESH_FLUX_NAME, lessonId); + } + + @RequestMapping(path = "/getGradebookUpdateFlux", method = RequestMethod.GET, produces = MediaType.TEXT_EVENT_STREAM_VALUE) + @ResponseBody + public Flux getGradebookUpdateFlux(@RequestParam long lessonId) + throws JsonProcessingException, IOException { + return FluxRegistry.get(MonitoringConstants.GRADEBOOK_REFRESH_FLUX_NAME, lessonId); + } + + @RequestMapping(path = "/getTimeLimitUpdateFlux", method = RequestMethod.GET, produces = MediaType.TEXT_EVENT_STREAM_VALUE) + @ResponseBody + public Flux getTimeLimitUpdateFlux(@RequestParam Set toolContentIds) + throws JsonProcessingException, IOException { + return FluxRegistry.get(MonitoringConstants.TIME_LIMIT_REFRESH_FLUX_NAME, toolContentIds); + } + /** * Initializes a lesson for specific learning design with the given lesson title and lesson description. If * initialization is successful, this method will the ID of new lesson. @@ -976,11 +1035,42 @@ && userManagementService.isUserInRole(user.getUserID(), organisation.getOrganisationId(), Role.AUTHOR); request.setAttribute("enableLiveEdit", enableLiveEdit); request.setAttribute("lesson", lessonDTO); - request.setAttribute("isTBLSequence", learningDesignService.isTBLSequence(lessonDTO.getLearningDesignID())); + boolean isTBLSequence = learningDesignService.isTBLSequence(lessonDTO.getLearningDesignID()); + request.setAttribute("isTBLSequence", isTBLSequence); + if (isTBLSequence) { + Map activityTypesMeta = learningDesignService + .getAvailableTBLActivityTypes(lessonDTO.getLearningDesignID()); + for (Entry entry : activityTypesMeta.entrySet()) { + request.setAttribute(entry.getKey(), entry.getValue()); + } + boolean burningQuestionsEnabled = false; + Long traToolActivityId = (Long) request.getAttribute("traToolActivityId"); + if (traToolActivityId != null) { + long traToolContentId = activityDAO.find(ToolActivity.class, traToolActivityId).getToolContentId(); + burningQuestionsEnabled = commonScratchieService.isBurningQuestionsEnabled(traToolContentId); + } + request.setAttribute("burningQuestionsEnabled", burningQuestionsEnabled); + } + return "monitor"; } + @RequestMapping("/displaySequenceTab") + public String displaySequenceTab() { + return "monitor-sequence-tab"; + } + + @RequestMapping("/displayLearnersTab") + public String displayLearnersTab() { + return "monitor-learners-tab"; + } + + @RequestMapping("/displayGradebookTab") + public String displayGradebookTab() { + return "monitor-gradebook-tab"; + } + /** * Gets users whose progress bars will be displayed in Learner tab in Monitor. */ @@ -993,25 +1083,40 @@ return null; } - String searchPhrase = request.getParameter("searchPhrase"); - Integer pageNumber = WebUtil.readIntParam(request, "pageNumber", true); - if (pageNumber == null || pageNumber < 1) { - pageNumber = 1; + ObjectNode responseJSON = JsonNodeFactory.instance.objectNode(); + Integer searchedLearnerId = WebUtil.readIntParam(request, "searchedLearnerID", true); + List learners = null; + if (searchedLearnerId == null) { + Integer pageNumber = WebUtil.readIntParam(request, "pageNumber", true); + if (pageNumber == null || pageNumber < 1) { + pageNumber = 1; + } + // are the learners sorted by the most completed first? + boolean isProgressSorted = WebUtil.readBooleanParam(request, "isProgressSorted", false); + + // either sort by name or how much a learner progressed into the lesson + learners = isProgressSorted + ? monitoringService.getLearnersByMostProgress(lessonId, null, 10, (pageNumber - 1) * 10) + : lessonService.getLessonLearners(lessonId, null, 10, (pageNumber - 1) * 10, true); + + // get all possible learners matching the given phrase, if any; used for max page number + responseJSON.put("learnerPossibleNumber", lessonService.getCountLessonLearners(lessonId, null)); + } else { + // only one learner is searched + User learner = userManagementService.getUserById(searchedLearnerId); + learners = List.of(learner); + responseJSON.put("learnerPossibleNumber", 1); } - // are the learners sorted by the most completed first? - boolean isProgressSorted = WebUtil.readBooleanParam(request, "isProgressSorted", false); - // either sort by name or how much a learner progressed into the lesson - List learners = isProgressSorted - ? monitoringService.getLearnersByMostProgress(lessonId, searchPhrase, 10, (pageNumber - 1) * 10) - : lessonService.getLessonLearners(lessonId, searchPhrase, 10, (pageNumber - 1) * 10, true); - ObjectNode responseJSON = JsonNodeFactory.instance.objectNode(); for (User learner : learners) { - responseJSON.withArray("learners").add(WebUtil.userToJSON(learner)); + ObjectNode learnerJSON = WebUtil.userToJSON(learner); + LearnerProgress learnerProgress = lessonService.getUserProgressForLesson(learner.getUserId(), lessonId); + learnerJSON.put("completedActivityCount", + learnerProgress == null ? 0 : learnerProgress.getCompletedActivities().size()); + learnerJSON.put("completedLesson", learnerProgress != null && learnerProgress.isComplete()); + responseJSON.withArray("learners").add(learnerJSON); } - // get all possible learners matching the given phrase, if any; used for max page number - responseJSON.put("learnerPossibleNumber", lessonService.getCountLessonLearners(lessonId, searchPhrase)); response.setContentType("application/json;charset=utf-8"); return responseJSON.toString(); } @@ -1066,11 +1171,6 @@ indfm.format(tzFinishDate) + " " + user.getTimeZone().getDisplayName(userLocale)); } - List contributeActivities = getContributeActivities(lessonId, false, false); - if (contributeActivities != null) { - responseJSON.set("contributeActivities", JsonUtil.readArray(contributeActivities)); - } - response.setContentType("application/json;charset=utf-8"); return responseJSON.toString(); } @@ -1086,21 +1186,24 @@ Integer notCompletedLearnersCount = possibleLearnersCount - completedLearnersCount - startedLearnersCount; ObjectNode responseJSON = JsonNodeFactory.instance.objectNode(); - ObjectNode notStartedJSON = JsonNodeFactory.instance.objectNode(); - notStartedJSON.put("name", messageService.getMessage("lesson.chart.not.completed")); - notStartedJSON.put("value", Math.round(notCompletedLearnersCount.doubleValue() / possibleLearnersCount * 100)); - responseJSON.withArray("data").add(notStartedJSON); - ObjectNode startedJSON = JsonNodeFactory.instance.objectNode(); startedJSON.put("name", messageService.getMessage("lesson.chart.started")); startedJSON.put("value", Math.round((startedLearnersCount.doubleValue()) / possibleLearnersCount * 100)); + startedJSON.put("raw", startedLearnersCount); responseJSON.withArray("data").add(startedJSON); ObjectNode completedJSON = JsonNodeFactory.instance.objectNode(); completedJSON.put("name", messageService.getMessage("lesson.chart.completed")); completedJSON.put("value", Math.round(completedLearnersCount.doubleValue() / possibleLearnersCount * 100)); + completedJSON.put("raw", completedLearnersCount); responseJSON.withArray("data").add(completedJSON); + ObjectNode notStartedJSON = JsonNodeFactory.instance.objectNode(); + notStartedJSON.put("name", messageService.getMessage("lesson.chart.not.completed")); + notStartedJSON.put("value", Math.round(notCompletedLearnersCount.doubleValue() / possibleLearnersCount * 100)); + notStartedJSON.put("raw", notCompletedLearnersCount); + responseJSON.withArray("data").add(notStartedJSON); + response.setContentType("application/json;charset=utf-8"); return responseJSON.toString(); } @@ -1136,7 +1239,7 @@ } ObjectNode responseJSON = JsonNodeFactory.instance.objectNode(); - List contributeActivities = getContributeActivities(lessonId, true, true); + List contributeActivities = getContributeActivities(lessonId, true, false); if (contributeActivities != null) { responseJSON.set("contributeActivities", JsonUtil.readArray(contributeActivities)); } @@ -1306,6 +1409,11 @@ } } + List absoluteTimeLimits = lessonService.getRunningAbsoluteTimeLimits(lessonId); + if (!absoluteTimeLimits.isEmpty()) { + responseJSON.set("timeLimits", JsonUtil.readArray(absoluteTimeLimits)); + } + response.setContentType("application/json;charset=utf-8"); return responseJSON.toString(); @@ -1545,6 +1653,22 @@ return "timer"; } + @GetMapping("/getTimeLimits") + @ResponseBody + public String getTimeLimits(@RequestParam long lessonID, HttpServletResponse response) throws IOException { + ArrayNode responseJSON = null; + + List absoluteTimeLimits = lessonService.getRunningAbsoluteTimeLimits(lessonID); + if (absoluteTimeLimits.isEmpty()) { + responseJSON = JsonNodeFactory.instance.arrayNode(); + } else { + responseJSON = JsonUtil.readArray(absoluteTimeLimits); + } + + response.setContentType("application/json;charset=utf-8"); + return responseJSON.toString(); + } + @RequestMapping(path = "/isLearningDesignHasGroupings", method = RequestMethod.GET) @ResponseBody public String isLearningDesignHasGroupings(@RequestParam long learningDesignId) { @@ -1640,4 +1764,66 @@ } return updatedLatestLearners; } -} + + private List getLessonActivities(Lesson lesson) { + /* + * Hibernate CGLIB is failing to load the first activity in the sequence as a ToolActivity for some mysterious + * reason Causes a ClassCastException when you try to cast it, even if it is a ToolActivity. + * + * THIS IS A HACK to retrieve the first tool activity manually so it can be cast as a ToolActivity - if it is + * one + */ + Activity firstActivity = activityDAO + .getActivityByActivityId(lesson.getLearningDesign().getFirstActivity().getActivityId()); + List activities = new ArrayList<>(); + sortActivitiesByLearningDesignOrder(firstActivity, activities); + + return activities; + } + + @SuppressWarnings("unchecked") + private void sortActivitiesByLearningDesignOrder(Activity activity, List sortedActivities) { + sortedActivities.add(activity); + + //in case of branching activity - add all activities based on their orderId + if (activity.isBranchingActivity()) { + BranchingActivity branchingActivity = (BranchingActivity) activity; + Set sequenceActivities = new TreeSet<>(new ActivityOrderComparator()); + sequenceActivities.addAll((Set) (Set) branchingActivity.getActivities()); + for (Activity sequenceActivityNotInitialized : sequenceActivities) { + SequenceActivity sequenceActivity = (SequenceActivity) monitoringService + .getActivityById(sequenceActivityNotInitialized.getActivityId()); + Set childActivities = new TreeSet<>(new ActivityOrderComparator()); + childActivities.addAll(sequenceActivity.getActivities()); + + //add one by one in order to initialize all activities + for (Activity childActivity : childActivities) { + Activity activityInit = monitoringService.getActivityById(childActivity.getActivityId()); + sortedActivities.add(activityInit); + } + } + + // In case of complex activity (parallel, help or optional activity) add all its children activities. + // They will be sorted by orderId + } else if (activity.isComplexActivity()) { + ComplexActivity complexActivity = (ComplexActivity) activity; + Set childActivities = new TreeSet<>(new ActivityOrderComparator()); + childActivities.addAll(complexActivity.getActivities()); + + // add one by one in order to initialize all activities + for (Activity childActivity : childActivities) { + Activity activityInit = monitoringService.getActivityById(childActivity.getActivityId()); + sortedActivities.add(activityInit); + } + } + + Transition transitionFrom = activity.getTransitionFrom(); + if (transitionFrom != null) { + // query activity from DB as transition holds only proxied activity object + Long nextActivityId = transitionFrom.getToActivity().getActivityId(); + Activity nextActivity = monitoringService.getActivityById(nextActivityId); + + sortActivitiesByLearningDesignOrder(nextActivity, sortedActivities); + } + } +} \ No newline at end of file Index: lams_monitoring/src/java/org/lamsfoundation/lams/monitoring/web/TblMonitoringController.java =================================================================== diff -u -r55a55392afb8af4300cef189d6cc2acc153f0827 -r8a7a259deba12163c46440a712e41bde926fdaf8 --- lams_monitoring/src/java/org/lamsfoundation/lams/monitoring/web/TblMonitoringController.java (.../TblMonitoringController.java) (revision 55a55392afb8af4300cef189d6cc2acc153f0827) +++ lams_monitoring/src/java/org/lamsfoundation/lams/monitoring/web/TblMonitoringController.java (.../TblMonitoringController.java) (revision 8a7a259deba12163c46440a712e41bde926fdaf8) @@ -9,6 +9,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; @@ -17,19 +18,15 @@ import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.lamsfoundation.lams.learningdesign.Activity; -import org.lamsfoundation.lams.learningdesign.ActivityOrderComparator; -import org.lamsfoundation.lams.learningdesign.BranchingActivity; -import org.lamsfoundation.lams.learningdesign.ComplexActivity; import org.lamsfoundation.lams.learningdesign.ContributionTypes; import org.lamsfoundation.lams.learningdesign.GateActivity; import org.lamsfoundation.lams.learningdesign.Group; import org.lamsfoundation.lams.learningdesign.Grouping; import org.lamsfoundation.lams.learningdesign.GroupingActivity; import org.lamsfoundation.lams.learningdesign.PermissionGateActivity; -import org.lamsfoundation.lams.learningdesign.SequenceActivity; import org.lamsfoundation.lams.learningdesign.ToolActivity; -import org.lamsfoundation.lams.learningdesign.Transition; import org.lamsfoundation.lams.learningdesign.dao.IActivityDAO; +import org.lamsfoundation.lams.learningdesign.service.ILearningDesignService; import org.lamsfoundation.lams.lesson.Lesson; import org.lamsfoundation.lams.lesson.service.ILessonService; import org.lamsfoundation.lams.monitoring.dto.ContributeActivityDTO; @@ -74,6 +71,8 @@ @Autowired private ILessonService lessonService; @Autowired + private ILearningDesignService learningDesignService; + @Autowired private IMonitoringFullService monitoringService; @Autowired private IQbService qbService; @@ -98,8 +97,11 @@ long lessonId = WebUtil.readLongParam(request, AttributeNames.PARAM_LESSON_ID); Lesson lesson = lessonService.getLesson(lessonId); - List lessonActivities = getLessonActivities(lesson); - TblMonitoringController.setupAvailableActivityTypes(request, lessonActivities); + Map activityTypesMeta = learningDesignService + .getAvailableTBLActivityTypes(lesson.getLearningDesign().getLearningDesignId()); + for (Entry entry : activityTypesMeta.entrySet()) { + request.setAttribute(entry.getKey(), entry.getValue()); + } boolean isTraAvailable = (request.getAttribute("isScratchieAvailable") != null) && ((Boolean) request.getAttribute("isScratchieAvailable")); boolean isIraAvailable = request.getAttribute("isIraAvailable") != null @@ -510,77 +512,6 @@ return "tblmonitor/aes"; } - /** - * Returns lesson activities sorted by the learning design order. - */ - private List getLessonActivities(Lesson lesson) { - /* - * Hibernate CGLIB is failing to load the first activity in the sequence as a ToolActivity for some mysterious - * reason Causes a ClassCastException when you try to cast it, even if it is a ToolActivity. - * - * THIS IS A HACK to retrieve the first tool activity manually so it can be cast as a ToolActivity - if it is - * one - */ - Activity firstActivity = activityDAO - .getActivityByActivityId(lesson.getLearningDesign().getFirstActivity().getActivityId()); - List activities = new ArrayList<>(); - sortActivitiesByLearningDesignOrder(firstActivity, activities); - - return activities; - } - - /** - * Sort all activities by the learning design order. - * - * @param activity - * @param sortedActivities - */ - @SuppressWarnings("unchecked") - private void sortActivitiesByLearningDesignOrder(Activity activity, List sortedActivities) { - sortedActivities.add(activity); - - //in case of branching activity - add all activities based on their orderId - if (activity.isBranchingActivity()) { - BranchingActivity branchingActivity = (BranchingActivity) activity; - Set sequenceActivities = new TreeSet<>(new ActivityOrderComparator()); - sequenceActivities.addAll((Set) (Set) branchingActivity.getActivities()); - for (Activity sequenceActivityNotInitialized : sequenceActivities) { - SequenceActivity sequenceActivity = (SequenceActivity) monitoringService - .getActivityById(sequenceActivityNotInitialized.getActivityId()); - Set childActivities = new TreeSet<>(new ActivityOrderComparator()); - childActivities.addAll(sequenceActivity.getActivities()); - - //add one by one in order to initialize all activities - for (Activity childActivity : childActivities) { - Activity activityInit = monitoringService.getActivityById(childActivity.getActivityId()); - sortedActivities.add(activityInit); - } - } - - // In case of complex activity (parallel, help or optional activity) add all its children activities. - // They will be sorted by orderId - } else if (activity.isComplexActivity()) { - ComplexActivity complexActivity = (ComplexActivity) activity; - Set childActivities = new TreeSet<>(new ActivityOrderComparator()); - childActivities.addAll(complexActivity.getActivities()); - - // add one by one in order to initialize all activities - for (Activity childActivity : childActivities) { - Activity activityInit = monitoringService.getActivityById(childActivity.getActivityId()); - sortedActivities.add(activityInit); - } - } - - Transition transitionFrom = activity.getTransitionFrom(); - if (transitionFrom != null) { - // query activity from DB as transition holds only proxied activity object - Long nextActivityId = transitionFrom.getToActivity().getActivityId(); - Activity nextActivity = monitoringService.getActivityById(nextActivityId); - - sortActivitiesByLearningDesignOrder(nextActivity, sortedActivities); - } - } - private GroupingActivity getGroupingActivity(Lesson lesson) { Set activities = new TreeSet<>(); @@ -605,86 +536,6 @@ return null; } - public static void setupAvailableActivityTypes(HttpServletRequest request, List activities) { - //check if there is Scratchie activity. It's used only in case of LKC TBL monitoring, when all assessment are treated as AEs - boolean isScratchieAvailable = false; - for (Activity activity : activities) { - if (activity instanceof ToolActivity) { - ToolActivity toolActivity = (ToolActivity) activity; - String toolSignature = toolActivity.getTool().getToolSignature(); - if (CommonConstants.TOOL_SIGNATURE_SCRATCHIE.equals(toolSignature)) { - isScratchieAvailable = true; - break; - } - } - } - - boolean scratchiePassed = false; - boolean iraPassed = false; - String aeToolContentIds = ""; - String aeToolTypes = ""; - String aeActivityTitles = ""; - for (Activity activity : activities) { - if (activity instanceof ToolActivity) { - ToolActivity toolActivity = (ToolActivity) activity; - String toolSignature = toolActivity.getTool().getToolSignature(); - Long toolContentId = toolActivity.getToolContentId(); - Long toolActivityId = toolActivity.getActivityId(); - String toolTitle = toolActivity.getTitle(); - - //count only the first Assessmnet as iRA - if (!iraPassed && isScratchieAvailable - && CommonConstants.TOOL_SIGNATURE_ASSESSMENT.equals(toolSignature)) { - iraPassed = true; - request.setAttribute("isIraAvailable", true); - request.setAttribute("iraToolContentId", toolContentId); - request.setAttribute("iraToolActivityId", toolActivityId); - - continue; - } - - //aes are counted only after Scratchie activity, or for LKC TBL monitoring - if ((scratchiePassed || !isScratchieAvailable) - && (CommonConstants.TOOL_SIGNATURE_ASSESSMENT.equals(toolSignature) - || CommonConstants.TOOL_SIGNATURE_DOKU.equals(toolSignature))) { - request.setAttribute("isAeAvailable", true); - //prepare assessment details to be passed to Assessment tool - aeToolContentIds += toolContentId + ","; - aeToolTypes += CommonConstants.TOOL_SIGNATURE_DOKU.equals(toolSignature) ? "d," : "a,"; - aeActivityTitles += toolTitle + "\\,"; - - } else if (CommonConstants.TOOL_SIGNATURE_FORUM.equals(toolSignature)) { - request.setAttribute("isForumAvailable", true); - request.setAttribute("forumActivityId", toolActivityId); - - } else if (CommonConstants.TOOL_SIGNATURE_PEER_REVIEW.equals(toolSignature)) { - request.setAttribute("isPeerreviewAvailable", true); - request.setAttribute("peerreviewToolContentId", toolContentId); - - //tRA is the first scratchie activity - } else if (!scratchiePassed && CommonConstants.TOOL_SIGNATURE_SCRATCHIE.equals(toolSignature)) { - scratchiePassed = true; - - request.setAttribute("isScratchieAvailable", true); - request.setAttribute("traToolContentId", toolContentId); - request.setAttribute("traToolActivityId", toolActivityId); - } - - if (CommonConstants.TOOL_SIGNATURE_LEADERSELECTION.equals(toolSignature)) { - request.setAttribute("leaderselectionToolActivityId", toolActivityId); - request.setAttribute("leaderselectionToolContentId", toolContentId); - } - - } else if (activity instanceof GateActivity) { - request.setAttribute("isGatesAvailable", true); - } - } - - request.setAttribute("aeToolContentIds", aeToolContentIds); - request.setAttribute("aeToolTypes", aeToolTypes); - request.setAttribute("aeActivityTitles", aeActivityTitles); - } - /** * Redirects to monitoring page */ Index: lams_tool_assessment/conf/language/lams/ApplicationResources.properties =================================================================== diff -u -r6481b3403e4e91e51c67207de071ba83335103de -r8a7a259deba12163c46440a712e41bde926fdaf8 --- lams_tool_assessment/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 6481b3403e4e91e51c67207de071ba83335103de) +++ lams_tool_assessment/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 8a7a259deba12163c46440a712e41bde926fdaf8) @@ -360,6 +360,7 @@ admin.return = Return to maintain LAMS admin.button.save = Save admin.hide.titles = Hide question titles for learners +admin.autoexpand.justification = Expand justification panel on question answer label.answer.queue = Answer queue label.drag.and.drop = drag and drop answer to tick or cross label.correct = Correct Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/AssessmentConstants.java =================================================================== diff -u -rc1e3ca12a9ccb265363e2330dd91ce7bedbcfa35 -r8a7a259deba12163c46440a712e41bde926fdaf8 --- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/AssessmentConstants.java (.../AssessmentConstants.java) (revision c1e3ca12a9ccb265363e2330dd91ce7bedbcfa35) +++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/AssessmentConstants.java (.../AssessmentConstants.java) (revision 8a7a259deba12163c46440a712e41bde926fdaf8) @@ -211,6 +211,8 @@ // configuration keys public static final String CONFIG_KEY_HIDE_TITLES = "hideTitles"; + + public static final String CONFIG_KEY_AUTO_EXPAND_JUSTIFICATION = "autoexpandJustification"; public static final String ATTR_IS_QUESTION_ETHERPAD_ENABLED = "isQuestionEtherpadEnabled"; Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/LearningController.java =================================================================== diff -u -ra40a77e307317e8038ed9e6b8699c18386286497 -r8a7a259deba12163c46440a712e41bde926fdaf8 --- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/LearningController.java (.../LearningController.java) (revision a40a77e307317e8038ed9e6b8699c18386286497) +++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/LearningController.java (.../LearningController.java) (revision 8a7a259deba12163c46440a712e41bde926fdaf8) @@ -418,6 +418,8 @@ sessionMap.put(AssessmentConstants.CONFIG_KEY_HIDE_TITLES, Boolean.valueOf(service.getConfigValue(AssessmentConstants.CONFIG_KEY_HIDE_TITLES))); + sessionMap.put(AssessmentConstants.CONFIG_KEY_AUTO_EXPAND_JUSTIFICATION, + Boolean.valueOf(service.getConfigValue(AssessmentConstants.CONFIG_KEY_AUTO_EXPAND_JUSTIFICATION))); if (!codeStyles.isEmpty()) { request.setAttribute(AssessmentConstants.ATTR_CODE_STYLES, codeStyles); Index: lams_tool_assessment/web/pages/admin/config.jsp =================================================================== diff -u -r5b9f590b301c276f8df06b30c26981b0eb634e69 -r8a7a259deba12163c46440a712e41bde926fdaf8 --- lams_tool_assessment/web/pages/admin/config.jsp (.../config.jsp) (revision 5b9f590b301c276f8df06b30c26981b0eb634e69) +++ lams_tool_assessment/web/pages/admin/config.jsp (.../config.jsp) (revision 8a7a259deba12163c46440a712e41bde926fdaf8) @@ -26,9 +26,16 @@
+ + +
+
+ +
Index: lams_tool_scratchie/conf/language/lams/ApplicationResources.properties =================================================================== diff -u -r51254ff2df01d1941265996bbc17c7a31f4942ef -r8a7a259deba12163c46440a712e41bde926fdaf8 --- lams_tool_scratchie/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 51254ff2df01d1941265996bbc17c7a31f4942ef) +++ lams_tool_scratchie/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 8a7a259deba12163c46440a712e41bde926fdaf8) @@ -275,6 +275,9 @@ label.authoring.advanced.reveal.double.click = Require double click to reveal an answer label.learning.reveal.double.click = Note: you need to double click on an answer to reveal its result. label.authoring.advanced.reveal.double.click.tooltip = When this option is enabled, leaders will require to double click on an answer in order to reveal the result. This option might be useful for summative assessments. +label.authoring.advanced.require.all.answers = Require to reveal all correct answers before proceeding +label.authoring.advanced.require.all.answers.tooltip = When this option is enabled, leaders will require to find all the correct answers in multiple choice questions before they can complete the activity. Otherwise they will be only presented with a warning if no all correct answers are found. +label.learning.require.all.answers = Make sure you find all the correct answers for multiple choice questions before finishing. label.monitoring.change.leader = Change leader label.monitoring.leader.successfully.changed = Leader changed label.monitoring.leader.not.changed = Leader was not changed Index: lams_tool_scratchie/web/pages/monitoring/studentChoices.jsp =================================================================== diff -u -ra40a77e307317e8038ed9e6b8699c18386286497 -r8a7a259deba12163c46440a712e41bde926fdaf8 --- lams_tool_scratchie/web/pages/monitoring/studentChoices.jsp (.../studentChoices.jsp) (revision a40a77e307317e8038ed9e6b8699c18386286497) +++ lams_tool_scratchie/web/pages/monitoring/studentChoices.jsp (.../studentChoices.jsp) (revision 8a7a259deba12163c46440a712e41bde926fdaf8) @@ -16,23 +16,43 @@ -webkit-overflow-scrolling: touch; } - /*---- fixed first column ----*/ - #questions-data { - position: relative; + #questions-header-sticky { + display: none; + position: sticky; + top: 0; + z-index: 1; + background: #FFF; } - #questions-data thead th { - position: -webkit-sticky; /* for Safari */ - position: sticky; - top: 0; - background: #FFF; + #questions-header-sticky.stick { + display: table; } + /* Use strick column widths so sticky and standard header look the same. */ + #questions-header-sticky thead th:first-child, #questions-data thead th:first-child { - left: 0; - z-index: 1; + min-width: 17rem; + max-width: 17rem; + width: 17rem; } + .question-header-cell { + min-width: 10rem; + max-width: 10rem; + width: 10rem; + } + + .question-header-summary-cell { + min-width: 7rem; + max-width: 7rem; + width: 7rem; + } + + #questions-data { + position: relative; + background: #FFF; + } + #questions-data tbody th { position: -webkit-sticky; /* for Safari */ position: sticky; @@ -69,6 +89,22 @@ @@ -129,19 +165,14 @@ - -
-
-
-
-
- + +
- - +
+ ${i.index + 1} @@ -158,17 +189,47 @@ - +   "> + %
+ + + +
+ + + + + + + + + + + + + + + @@ -271,7 +332,7 @@ - + @@ -338,10 +399,6 @@
+ + ${i.index + 1} + + +   + "> + + % +
-
-
-
-
Index: lams_tool_scratchie/web/pages/monitoring/studentChoices5.jsp =================================================================== diff -u -rf619aa5955dca265aae7744ea3319f3ec468801e -r8a7a259deba12163c46440a712e41bde926fdaf8 --- lams_tool_scratchie/web/pages/monitoring/studentChoices5.jsp (.../studentChoices5.jsp) (revision f619aa5955dca265aae7744ea3319f3ec468801e) +++ lams_tool_scratchie/web/pages/monitoring/studentChoices5.jsp (.../studentChoices5.jsp) (revision 8a7a259deba12163c46440a712e41bde926fdaf8) @@ -16,21 +16,13 @@ -webkit-overflow-scrolling: touch; } - /*---- fixed first column ----*/ #questions-data { position: relative; - } - - #questions-data thead th { - position: -webkit-sticky; /* for Safari */ - position: sticky; - top: 0; background: #FFF; } - #questions-data thead th:first-child { - left: 0; - z-index: 1; + #questions-data thead { + background: #FFF; } #questions-data tbody th { @@ -77,6 +69,25 @@ }); $('#time-limit-panel-placeholder').load('${timeLimitPanelUrl}'); + + + // Add sticky column headers to student choices table. + // Standard sticky header CSS solution does not work as it is page which is being scrolled, not the table itself + + window.onscroll = function() { + let studentChoicesTable = $('#questions-data'), + studentChoicesStickyHeader = $('thead', studentChoicesTable), + studentChoicesTableTopOffset = studentChoicesTable.offset().top, + studentChoicesTableHeight = studentChoicesTable.height(); + if (window.pageYOffset > studentChoicesTableTopOffset + 20 + && window.pageYOffset < studentChoicesTableTopOffset + studentChoicesTableHeight - 20) { + studentChoicesStickyHeader + .css('transform', 'translateY(' + (window.pageYOffset - studentChoicesTableTopOffset - 2) + 'px)'); + } else { + studentChoicesStickyHeader.css('transform', 'none'); + } + } + }); @@ -109,8 +120,6 @@
- -
Index: lams_tool_scratchie/web/pages/monitoring/studentChoicesTable.jsp =================================================================== diff -u -r997fdd2943e80dbf4e00dfeffb195503646d3447 -r8a7a259deba12163c46440a712e41bde926fdaf8 --- lams_tool_scratchie/web/pages/monitoring/studentChoicesTable.jsp (.../studentChoicesTable.jsp) (revision 997fdd2943e80dbf4e00dfeffb195503646d3447) +++ lams_tool_scratchie/web/pages/monitoring/studentChoicesTable.jsp (.../studentChoicesTable.jsp) (revision 8a7a259deba12163c46440a712e41bde926fdaf8) @@ -1,199 +1,198 @@ <%@ include file="/common/taglibs.jsp"%> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
- - ${i.index + 1} - - - - - - - - - - - -   - - - - - % -
- - - - - - ${item.correctAnswerLetter} - -
- -
+ + + + - - - - - - - - - - - - - - - - bg-success - bg-danger text-white - bg-warning - - - - - - - - - - - - - - - - + + + + + + + - - - bg-success - bg-danger text-white - bg-warning - - - - - - - - - - bg-success - bg-danger text-white - bg-warning - - - - + + - - - - + + + + + + + + + + - bg-success - bg-danger text-white - bg-warning + + + + + + + + + + + + + + bg-success + bg-danger text-white + bg-warning + + + + + + + - + + + + + + + + + + + + bg-success + bg-danger text-white + bg-warning + + + + + + + - - - - - - - -
- - - - ${sessionDto.sessionName} - - - - - - - - ${sessionDto.sessionName} - - - + + ${i.index + 1} + + + + + + + + + - - - successful-response wrong-response"> - - - - - - - - - - - - - ${sessionDto.mark} - - % -
  + + + +   - + + % +
+ + + + - ${item.correctOnFirstAttemptCount} + + + ${item.correctAnswerLetter} + -   - - - - -
%
+ +
+ + + + ${sessionDto.sessionName} + + + + + + + + ${sessionDto.sessionName} + + + + + + + + successful-response wrong-response"> + + + + + + + + + + + + + ${sessionDto.mark} + + % +
  + + + + + ${item.correctOnFirstAttemptCount} + - % + + + + bg-success + bg-danger text-white + bg-warning + + + +   + + + - -  %  - - - -
\ No newline at end of file + - + + + % + + + + bg-success + bg-danger text-white + bg-warning + + + + + % + + + - + +  %  + + + + + + + +