Index: lams_common/src/java/org/lamsfoundation/lams/qb/dao/IQbDAO.java =================================================================== diff -u -rd578cfde655760533b6ede422e7cf675a8c4ca6d -reaf86cc891c4a0dbcc71fa7bd87f0657a250c730 --- lams_common/src/java/org/lamsfoundation/lams/qb/dao/IQbDAO.java (.../IQbDAO.java) (revision d578cfde655760533b6ede422e7cf675a8c4ca6d) +++ lams_common/src/java/org/lamsfoundation/lams/qb/dao/IQbDAO.java (.../IQbDAO.java) (revision eaf86cc891c4a0dbcc71fa7bd87f0657a250c730) @@ -8,6 +8,7 @@ import org.lamsfoundation.lams.dao.IBaseDAO; import org.lamsfoundation.lams.learningdesign.ToolActivity; +import org.lamsfoundation.lams.qb.dto.QbAnswersForOptionDTO; import org.lamsfoundation.lams.qb.model.QbCollection; import org.lamsfoundation.lams.qb.model.QbQuestion; import org.lamsfoundation.lams.tool.ToolContent; @@ -50,8 +51,10 @@ Map getAnswerStatsForQuestion(long qbQuestionUid); - Map getAnswersForActivity(long activityId, long qbQuestionUid); + Map getAnswersForActivityAndQuestion(long activityId, long qbQuestionUid); + List getAnswerCountForOptions(long toolContentId); + Map getBurningQuestions(long qbQuestionUid); List getPagedQuestions(String questionTypes, String collectionUids, int page, int size, String sortBy, Index: lams_common/src/java/org/lamsfoundation/lams/qb/dao/hibernate/QbDAO.java =================================================================== diff -u -rd578cfde655760533b6ede422e7cf675a8c4ca6d -reaf86cc891c4a0dbcc71fa7bd87f0657a250c730 --- lams_common/src/java/org/lamsfoundation/lams/qb/dao/hibernate/QbDAO.java (.../QbDAO.java) (revision d578cfde655760533b6ede422e7cf675a8c4ca6d) +++ lams_common/src/java/org/lamsfoundation/lams/qb/dao/hibernate/QbDAO.java (.../QbDAO.java) (revision eaf86cc891c4a0dbcc71fa7bd87f0657a250c730) @@ -5,8 +5,10 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import javax.persistence.Query; @@ -18,6 +20,7 @@ import org.lamsfoundation.lams.dao.hibernate.LAMSBaseDAO; import org.lamsfoundation.lams.learningdesign.ToolActivity; import org.lamsfoundation.lams.qb.dao.IQbDAO; +import org.lamsfoundation.lams.qb.dto.QbAnswersForOptionDTO; import org.lamsfoundation.lams.qb.model.QbCollection; import org.lamsfoundation.lams.qb.model.QbQuestion; import org.lamsfoundation.lams.qb.model.QbToolQuestion; @@ -50,7 +53,7 @@ + "LEFT JOIN tl_laasse10_option_answer AS aa ON a.answer_uid = aa.question_result_uid AND aa.answer_boolean = 1 " + "WHERE tq.qb_question_uid = :qbQuestionUid GROUP BY opt HAVING opt IS NOT NULL"; - private static final String FIND_ANSWERS_BY_ACTIVITY = "SELECT COALESCE(mcu.que_usr_id, su.user_id, au.user_id) AS user_id, " + private static final String FIND_ANSWERS_BY_ACTIVITY_AND_QUESTION = "SELECT COALESCE(mcu.que_usr_id, su.user_id, au.user_id) AS user_id, " + "IF(su.user_id IS NULL, COALESCE(a.qb_option_uid, aa.question_option_uid), IF(COUNT(a.qb_option_uid) > 1, -1, a.qb_option_uid)) AS opt " + "FROM lams_learning_activity AS act JOIN lams_qb_tool_question AS tq USING (tool_content_id) " + "JOIN lams_qb_tool_answer AS a USING (tool_question_uid) " @@ -66,6 +69,19 @@ + "WHERE act.activity_id = :activityId AND tq.qb_question_uid = :qbQuestionUid GROUP BY user_id " + "HAVING opt IS NOT NULL AND user_id IS NOT NULL"; + private static final String FIND_OPTION_ANSWER_COUNT_BY_QUESTION = "SELECT tq.qb_question_uid, o.uid AS qb_option_uid, o.max_mark = 1 AS is_correct, " + + "SUM(IF(aa.uid IS NULL AND sa.uid IS NULL, 0, 1)) AS chosen_count " + + "FROM lams_qb_tool_question AS tq JOIN lams_qb_option AS o USING (qb_question_uid) " + + "LEFT JOIN lams_qb_tool_answer AS a ON a.tool_question_uid = tq.tool_question_uid AND (a.qb_option_uid IS NULL OR a.qb_option_uid = o.uid) " + + "LEFT JOIN tl_laasse10_question_result AS aq ON a.answer_uid = aq.uid " + + "LEFT JOIN tl_laasse10_option_answer AS aa ON aa.question_result_uid = aq.uid AND aa.question_option_uid = o.uid AND aa.answer_boolean = 1 " + + "LEFT JOIN tl_laasse10_assessment_result AS ar ON aq.result_uid = ar.uid AND ar.latest = 1 " + + "LEFT JOIN tl_lascrt11_answer_log AS sa ON a.answer_uid = sa.uid AND a.answer_uid = " + + " (SELECT sa2.uid FROM tl_lascrt11_answer_log AS sa2 JOIN lams_qb_tool_answer AS a2 " + + " ON a2.answer_uid = sa2.uid AND a2.tool_question_uid = a.tool_question_uid AND sa2.session_id = sa.session_id " + + " ORDER BY sa2.access_date, sa2.uid LIMIT 1) " + + "WHERE tq.tool_content_id = :toolContentId GROUP BY o.uid ORDER BY tq.display_order, o.display_order"; + private static final String FIND_BURNING_QUESTIONS = "SELECT b.question, COUNT(bl.uid) FROM ScratchieBurningQuestion b LEFT OUTER JOIN " + "BurningQuestionLike AS bl ON bl.burningQuestion = b WHERE b.scratchieItem.qbQuestion.uid = :qbQuestionUid " + "GROUP BY b.question ORDER BY COUNT(bl.uid) DESC"; @@ -279,8 +295,8 @@ " JOIN lams_qb_collection_question collection ON question.question_id = collection.qb_question_id"); } if (learningDesignId != null) { - queryBuilder.append( - " JOIN lams_qb_tool_question tool_question ON question.uid = tool_question.qb_question_uid") + queryBuilder + .append(" JOIN lams_qb_tool_question tool_question ON question.uid = tool_question.qb_question_uid") .append(" JOIN lams_learning_activity activity USING (tool_content_id)"); } @@ -394,8 +410,8 @@ @Override @SuppressWarnings("unchecked") - public Map getAnswersForActivity(long activityId, long qbQuestionUid) { - List result = this.getSession().createSQLQuery(FIND_ANSWERS_BY_ACTIVITY) + public Map getAnswersForActivityAndQuestion(long activityId, long qbQuestionUid) { + List result = this.getSession().createSQLQuery(FIND_ANSWERS_BY_ACTIVITY_AND_QUESTION) .setParameter("activityId", activityId).setParameter("qbQuestionUid", qbQuestionUid).list(); Map map = new HashMap<>(result.size()); for (Object[] answerStat : result) { @@ -406,6 +422,57 @@ @Override @SuppressWarnings("unchecked") + public List getAnswerCountForOptions(long toolContentId) { + List result = this.getSession().createSQLQuery(FIND_OPTION_ANSWER_COUNT_BY_QUESTION) + .setParameter("toolContentId", toolContentId).list(); + List dtos = new LinkedList<>(); + Map> questionsToOptionsMap = new LinkedHashMap<>(); + // it contains all UID of correct answers + Set correctOptionUids = new HashSet<>(); + + // build a map of question ID -> option ID -> answer count + for (Object[] answerEntry : result) { + Long qbQuestionUid = ((Number) answerEntry[0]).longValue(); + Map answersForOptions = questionsToOptionsMap.get(qbQuestionUid); + if (answersForOptions == null) { + answersForOptions = new LinkedHashMap<>(); + questionsToOptionsMap.put(qbQuestionUid, answersForOptions); + } + Long qbOptionUid = ((Number) answerEntry[1]).longValue(); + answersForOptions.put(qbOptionUid, ((Number) answerEntry[3]).intValue()); + + boolean isCorrect = ((Number) answerEntry[2]).intValue() == 1; + if (isCorrect) { + correctOptionUids.add(qbOptionUid); + } + } + + // calculate answer percentage for question and all options + int displayOrder = 1; + for (Entry> questionToOptionEntry : questionsToOptionsMap.entrySet()) { + double totalAnswers = questionToOptionEntry.getValue().values().stream().mapToInt(Integer::intValue).sum(); + QbAnswersForOptionDTO dto = new QbAnswersForOptionDTO(questionToOptionEntry.getKey(), displayOrder++); + dtos.add(dto); + + for (Entry answersForOptionEntry : questionToOptionEntry.getValue().entrySet()) { + Long qbOptionUid = answersForOptionEntry.getKey(); + if (totalAnswers == 0) { + dto.getOptionAnswerPercent().put(qbOptionUid, -1); + continue; + } + + Long answerPercent = Math.round(answersForOptionEntry.getValue() / totalAnswers * 100); + dto.getOptionAnswerPercent().put(qbOptionUid, answerPercent.intValue()); + if (correctOptionUids.contains(qbOptionUid)) { + dto.setCorrectAnswerPercent(answerPercent.intValue()); + } + } + } + return dtos; + } + + @Override + @SuppressWarnings("unchecked") public Map getBurningQuestions(long qbQuestionUid) { List result = this.getSession().createQuery(FIND_BURNING_QUESTIONS) .setParameter("qbQuestionUid", qbQuestionUid).list(); Index: lams_common/src/java/org/lamsfoundation/lams/qb/dto/QbAnswersForOptionDTO.java =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/qb/dto/QbAnswersForOptionDTO.java (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/qb/dto/QbAnswersForOptionDTO.java (revision eaf86cc891c4a0dbcc71fa7bd87f0657a250c730) @@ -0,0 +1,39 @@ +package org.lamsfoundation.lams.qb.dto; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class QbAnswersForOptionDTO { + private long qbQuestionUid; + private int displayOrder; + + private Integer correctAnswerPercent; + private Map optionAnswerPercent; + + public QbAnswersForOptionDTO(long qbQuestionUid, int displayOrder) { + this.qbQuestionUid = qbQuestionUid; + this.displayOrder = displayOrder; + this.correctAnswerPercent = -1; + this.optionAnswerPercent = new LinkedHashMap<>(); + } + + public int getCorrectAnswerPercent() { + return correctAnswerPercent; + } + + public void setCorrectAnswerPercent(int correctAnswerPercent) { + this.correctAnswerPercent = correctAnswerPercent; + } + + public long getQbQuestionUid() { + return qbQuestionUid; + } + + public int getDisplayOrder() { + return displayOrder; + } + + public Map getOptionAnswerPercent() { + return optionAnswerPercent; + } +} \ No newline at end of file Index: lams_common/src/java/org/lamsfoundation/lams/qb/service/IQbService.java =================================================================== diff -u -rd578cfde655760533b6ede422e7cf675a8c4ca6d -reaf86cc891c4a0dbcc71fa7bd87f0657a250c730 --- lams_common/src/java/org/lamsfoundation/lams/qb/service/IQbService.java (.../IQbService.java) (revision d578cfde655760533b6ede422e7cf675a8c4ca6d) +++ lams_common/src/java/org/lamsfoundation/lams/qb/service/IQbService.java (.../IQbService.java) (revision eaf86cc891c4a0dbcc71fa7bd87f0657a250c730) @@ -3,11 +3,13 @@ import java.math.BigInteger; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.TreeSet; import java.util.UUID; import javax.servlet.http.HttpServletRequest; +import org.lamsfoundation.lams.qb.dto.QbAnswersForOptionDTO; import org.lamsfoundation.lams.qb.dto.QbStatsActivityDTO; import org.lamsfoundation.lams.qb.dto.QbStatsDTO; import org.lamsfoundation.lams.qb.form.QbQuestionForm; @@ -77,6 +79,8 @@ QbStatsActivityDTO getActivityStats(Long activityId, Long qbQuestionUid, Collection correctOptionUids); + List getAnswerCountForOptions(long toolContentId); + List getPagedQuestions(String questionTypes, String collectionUids, Long onlyInSameLearningDesignAsToolContentID, int page, int size, String sortBy, String sortOrder, String searchString); Index: lams_common/src/java/org/lamsfoundation/lams/qb/service/QbService.java =================================================================== diff -u -rbba82a2ff7eefb96488e0e095b8d1a153699266b -reaf86cc891c4a0dbcc71fa7bd87f0657a250c730 --- lams_common/src/java/org/lamsfoundation/lams/qb/service/QbService.java (.../QbService.java) (revision bba82a2ff7eefb96488e0e095b8d1a153699266b) +++ lams_common/src/java/org/lamsfoundation/lams/qb/service/QbService.java (.../QbService.java) (revision eaf86cc891c4a0dbcc71fa7bd87f0657a250c730) @@ -40,6 +40,7 @@ import org.lamsfoundation.lams.qb.QbConstants; import org.lamsfoundation.lams.qb.QbUtils; import org.lamsfoundation.lams.qb.dao.IQbDAO; +import org.lamsfoundation.lams.qb.dto.QbAnswersForOptionDTO; import org.lamsfoundation.lams.qb.dto.QbStatsActivityDTO; import org.lamsfoundation.lams.qb.dto.QbStatsDTO; import org.lamsfoundation.lams.qb.form.QbQuestionForm; @@ -274,7 +275,8 @@ // if there is only 1 participant, there is no point in calculating question indexes if (participantCount >= Configuration.getAsInt(ConfigurationKeys.QB_STATS_MIN_PARTICIPANTS)) { // mapping of user ID -> option UID - Map activityAnswers = qbDAO.getAnswersForActivity(activity.getActivityId(), qbQuestionUid); + Map activityAnswers = qbDAO.getAnswersForActivityAndQuestion(activity.getActivityId(), + qbQuestionUid); // take only learners who finished (not only submitted) this activity userLessonGrades = userLessonGrades.stream() .filter(g -> activityAnswers.containsKey(g.getLearner().getUserId())).collect(Collectors.toList()); @@ -348,6 +350,11 @@ } @Override + public List getAnswerCountForOptions(long toolContentId) { + return qbDAO.getAnswerCountForOptions(toolContentId); + } + + @Override public QbCollection getCollectionByUid(Long collectionUid) { return qbDAO.find(QbCollection.class, collectionUid); } Index: lams_monitoring/conf/language/lams/ApplicationResources.properties =================================================================== diff -u -re65e473d6582caae620fe3cc3115fbc59fc48dc8 -reaf86cc891c4a0dbcc71fa7bd87f0657a250c730 --- lams_monitoring/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision e65e473d6582caae620fe3cc3115fbc59fc48dc8) +++ lams_monitoring/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision eaf86cc891c4a0dbcc71fa7bd87f0657a250c730) @@ -530,3 +530,5 @@ label.monitoring.edit.lesson.settings = Edit lesson settings label.monitoring.edit = Edit label.monitoring.burning.questions = Burning Questions +label.monitoring.teams.question.results = Question results +label.monitoring.teams.question.results.question = Question \ No newline at end of file Index: lams_monitoring/src/java/org/lamsfoundation/lams/monitoring/web/TblMonitoringController.java =================================================================== diff -u -r3ef9ae27663556230437c96a76b34599ca104a18 -reaf86cc891c4a0dbcc71fa7bd87f0657a250c730 --- lams_monitoring/src/java/org/lamsfoundation/lams/monitoring/web/TblMonitoringController.java (.../TblMonitoringController.java) (revision 3ef9ae27663556230437c96a76b34599ca104a18) +++ lams_monitoring/src/java/org/lamsfoundation/lams/monitoring/web/TblMonitoringController.java (.../TblMonitoringController.java) (revision eaf86cc891c4a0dbcc71fa7bd87f0657a250c730) @@ -1,7 +1,11 @@ package org.lamsfoundation.lams.monitoring.web; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; @@ -10,6 +14,7 @@ import javax.servlet.http.HttpServletRequest; +import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.lamsfoundation.lams.learningdesign.Activity; import org.lamsfoundation.lams.learningdesign.ActivityOrderComparator; @@ -32,6 +37,9 @@ import org.lamsfoundation.lams.monitoring.dto.TblGroupDTO; import org.lamsfoundation.lams.monitoring.dto.TblUserDTO; import org.lamsfoundation.lams.monitoring.service.IMonitoringFullService; +import org.lamsfoundation.lams.qb.dto.QbAnswersForOptionDTO; +import org.lamsfoundation.lams.qb.model.QbQuestion; +import org.lamsfoundation.lams.qb.service.IQbService; import org.lamsfoundation.lams.tool.ToolSession; import org.lamsfoundation.lams.tool.service.ICommonAssessmentService; import org.lamsfoundation.lams.tool.service.ICommonScratchieService; @@ -46,7 +54,9 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; @@ -66,6 +76,8 @@ @Autowired private IMonitoringFullService monitoringService; @Autowired + private IQbService qbService; + @Autowired private ILamsToolService lamsToolService; @Autowired private ILamsCoreToolService lamsCoreToolService; @@ -263,6 +275,140 @@ return "tblmonitor/teams"; } + @GetMapping("/questionResults") + public String getQuestionResults(@RequestParam(required = false) Long iraToolContentId, + @RequestParam(required = false) Long traToolContentId, @RequestParam(required = false) String order, + Model model) { + List iraAnswers = null; + if (iraToolContentId != null) { + // get answer percentage for each iRAT question and its options + iraAnswers = qbService.getAnswerCountForOptions(iraToolContentId); + model.addAttribute("iraAnswerCountForOptions", iraAnswers); + } + + List traAnswers = null; + List averageAnswers = null; + if (traToolContentId != null) { + traAnswers = qbService.getAnswerCountForOptions(traToolContentId); + model.addAttribute("traAnswerCountForOptions", traAnswers); + + // if both iRAT and tRAT are present, count average + if (iraAnswers != null) { + averageAnswers = new LinkedList<>(); + model.addAttribute("averageAnswerCountForOptions", averageAnswers); + + for (QbAnswersForOptionDTO iraDto : iraAnswers) { + QbAnswersForOptionDTO averageDto = new QbAnswersForOptionDTO(iraDto.getQbQuestionUid(), + iraDto.getDisplayOrder()); + averageAnswers.add(averageDto); + + // there can be no answers for iRAT yet + boolean iraHasAnswers = iraDto.getCorrectAnswerPercent() >= 0; + + for (QbAnswersForOptionDTO traDto : traAnswers) { + if (traDto.getQbQuestionUid() == iraDto.getQbQuestionUid()) { + boolean traHasAnswers = traDto.getCorrectAnswerPercent() >= 0; + if (iraHasAnswers) { + if (traHasAnswers) { + // both RATs have answers present, count average + averageDto.setCorrectAnswerPercent( + (iraDto.getCorrectAnswerPercent() + traDto.getCorrectAnswerPercent()) / 2); + } else { + // just iRAT has answers + averageDto.setCorrectAnswerPercent(iraDto.getCorrectAnswerPercent()); + } + } else if (traHasAnswers) { + // just tRAT has answers + averageDto.setCorrectAnswerPercent(traDto.getCorrectAnswerPercent()); + } + + // count average for each option + for (Long qbOptionUid : iraDto.getOptionAnswerPercent().keySet()) { + int iraOptionAnswers = iraDto.getOptionAnswerPercent().get(qbOptionUid); + int traOptionAnswers = traDto.getOptionAnswerPercent().get(qbOptionUid); + + if (iraHasAnswers) { + if (traHasAnswers) { + averageDto.getOptionAnswerPercent().put(qbOptionUid, + (iraOptionAnswers + traOptionAnswers) / 2); + } else { + averageDto.getOptionAnswerPercent().put(qbOptionUid, iraOptionAnswers); + } + } else if (traHasAnswers) { + averageDto.getOptionAnswerPercent().put(qbOptionUid, traOptionAnswers); + } else { + averageDto.getOptionAnswerPercent().put(qbOptionUid, -1); + } + } + } + } + } + } + } + + List sortSource = iraAnswers == null ? iraAnswers : traAnswers; + List> sortTargets = new LinkedList<>(); + + if (StringUtils.isBlank(order)) { + order = "default"; + } else { + order = order.toLowerCase(); + } + + // find out by which column to sort + if (iraAnswers != null && order.startsWith("ira")) { + iraAnswers.sort(Comparator.comparing(QbAnswersForOptionDTO::getCorrectAnswerPercent)); + sortSource = iraAnswers; + // other collections need to have same sorting as well + if (traAnswers != null) { + sortTargets.add(traAnswers); + sortTargets.add(averageAnswers); + } + } else if (traAnswers != null && order.startsWith("tra")) { + traAnswers.sort(Comparator.comparing(QbAnswersForOptionDTO::getCorrectAnswerPercent)); + + sortSource = traAnswers; + if (iraAnswers != null) { + sortTargets.add(iraAnswers); + sortTargets.add(averageAnswers); + } + } else if (averageAnswers != null && order.startsWith("average")) { + averageAnswers.sort(Comparator.comparing(QbAnswersForOptionDTO::getCorrectAnswerPercent)); + sortSource = averageAnswers; + sortTargets.add(traAnswers); + sortTargets.add(iraAnswers); + } + + if (order.endsWith("reversed")) { + Collections.reverse(sortSource); + } + + // fetch questions according to sort order + Map questions = new LinkedHashMap<>(); + for (QbAnswersForOptionDTO dto : sortSource) { + QbQuestion question = qbService.getQuestionByUid(dto.getQbQuestionUid()); + questions.put(dto.getDisplayOrder(), question); + } + + // sort remaining collections according to sor order + for (List dtos : sortTargets) { + List sortedTarget = new LinkedList<>(); + for (QbAnswersForOptionDTO sourceDto : sortSource) { + for (QbAnswersForOptionDTO targetDto : dtos) { + if (sourceDto.getQbQuestionUid() == targetDto.getQbQuestionUid()) { + sortedTarget.add(targetDto); + break; + } + } + } + dtos.clear(); + dtos.addAll(sortedTarget); + } + + model.addAttribute("questions", questions); + return "tblmonitor/teamsQuestionResults"; + } + /** * Shows Gates page */ Index: lams_monitoring/web/tblmonitor/teams.jsp =================================================================== diff -u -rdb0095c7ccb1a6c3bdbce14f3ca019ac106bc1e5 -reaf86cc891c4a0dbcc71fa7bd87f0657a250c730 --- lams_monitoring/web/tblmonitor/teams.jsp (.../teams.jsp) (revision db0095c7ccb1a6c3bdbce14f3ca019ac106bc1e5) +++ lams_monitoring/web/tblmonitor/teams.jsp (.../teams.jsp) (revision eaf86cc891c4a0dbcc71fa7bd87f0657a250c730) @@ -1,9 +1,14 @@ <%@ include file="/taglibs.jsp"%> + + + @@ -428,6 +457,8 @@ + +
@@ -592,7 +623,7 @@
- +