Index: 3rdParty_sources/versions.txt =================================================================== diff -u -r2d6722d97aad801e2f7db229945ae3dab6ec8576 -r5694a8e26e12cfd208ef7f26d736f02dc6749f23 --- 3rdParty_sources/versions.txt (.../versions.txt) (revision 2d6722d97aad801e2f7db229945ae3dab6ec8576) +++ 3rdParty_sources/versions.txt (.../versions.txt) (revision 5694a8e26e12cfd208ef7f26d736f02dc6749f23) @@ -39,7 +39,7 @@ jLaTexMath 1.0.6 -jsonwebtoken 0.9.0 +jsonwebtoken 0.11.2 JSP API 2.3 1.0.3 @@ -69,6 +69,8 @@ Undertow servlet 2.0.13 +UOC LTI Advantage integration 0.0.3 + xmltooling 1.4.0 XStream 1.4.11 Index: lams_build/3rdParty.userlibraries =================================================================== diff -u -rc33d4aa11d22778bd16a874f0a95237da3f2f10c -r5694a8e26e12cfd208ef7f26d736f02dc6749f23 --- lams_build/3rdParty.userlibraries (.../3rdParty.userlibraries) (revision c33d4aa11d22778bd16a874f0a95237da3f2f10c) +++ lams_build/3rdParty.userlibraries (.../3rdParty.userlibraries) (revision 5694a8e26e12cfd208ef7f26d736f02dc6749f23) @@ -12,7 +12,7 @@ - + @@ -23,7 +23,6 @@ - @@ -46,6 +45,9 @@ + + + Index: lams_build/build.xml =================================================================== diff -u -r4bcdd9565f14ec38ad5b21fb982196f4c1746528 -r5694a8e26e12cfd208ef7f26d736f02dc6749f23 --- lams_build/build.xml (.../build.xml) (revision 4bcdd9565f14ec38ad5b21fb982196f4c1746528) +++ lams_build/build.xml (.../build.xml) (revision 5694a8e26e12cfd208ef7f26d736f02dc6749f23) @@ -613,6 +613,7 @@ + + + /css/* /errorpages/* + /error.jsp /images/* /includes/javascript/* /includes/font-awesome/* /ckeditor/* + /loadVars.jsp /favicon.ico GET POST Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/AssessmentServiceImpl.java =================================================================== diff -u -r4dbd12afbfcb6eb768ceb772768779e7619dc2f8 -r5694a8e26e12cfd208ef7f26d736f02dc6749f23 --- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/AssessmentServiceImpl.java (.../AssessmentServiceImpl.java) (revision 4dbd12afbfcb6eb768ceb772768779e7619dc2f8) +++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/AssessmentServiceImpl.java (.../AssessmentServiceImpl.java) (revision 5694a8e26e12cfd208ef7f26d736f02dc6749f23) @@ -77,6 +77,7 @@ import org.lamsfoundation.lams.outcome.Outcome; import org.lamsfoundation.lams.outcome.OutcomeMapping; import org.lamsfoundation.lams.outcome.service.IOutcomeService; +import org.lamsfoundation.lams.qb.QbUtils; import org.lamsfoundation.lams.qb.model.QbCollection; import org.lamsfoundation.lams.qb.model.QbOption; import org.lamsfoundation.lams.qb.model.QbQuestion; @@ -887,27 +888,16 @@ if (questionDto.getAnswer() != null) { boolean isQuestionCaseSensitive = questionDto.isCaseSensitive(); - String normalisedQuestionAnswer = AssessmentEscapeUtils.normaliseVSAnswer(questionDto.getAnswer()); + String normalisedQuestionAnswer = QbUtils.normaliseVSAnswer(questionDto.getAnswer()); for (OptionDTO optionDto : questionDto.getOptionDtos()) { // refresh latest answers from DB QbOption qbOption = qbService.getOptionByUid(optionDto.getUid()); optionDto.setName(qbOption.getName()); + boolean isAnswerAllocated = QbUtils.isVSAnswerAllocated(qbOption.getName(), + normalisedQuestionAnswer, isQuestionCaseSensitive); - Collection optionAnswers = AssessmentEscapeUtils.normaliseVSOption(optionDto.getName()); - boolean isAnswerMatchedCurrentOption = false; - for (String optionAnswer : optionAnswers) { - String normalisedOptionAnswer = AssessmentEscapeUtils.normaliseVSAnswer(optionAnswer); - - // check is item unraveled - if (isQuestionCaseSensitive ? normalisedQuestionAnswer.equals(normalisedOptionAnswer) - : normalisedQuestionAnswer.equalsIgnoreCase(normalisedOptionAnswer)) { - isAnswerMatchedCurrentOption = true; - break; - } - } - - if (isAnswerMatchedCurrentOption) { + if (isAnswerAllocated) { mark = optionDto.getMaxMark() * maxMark; questionResult.setQbOption(qbOption); break; @@ -1458,6 +1448,25 @@ } @Override + public Map> getUnallocatedVSAnswers(long toolContentId) { + Map> result = new LinkedHashMap<>(); + + Assessment assessment = getAssessmentByContentId(toolContentId); + for (AssessmentQuestion question : assessment.getQuestions()) { + if (question.getType().equals(QbQuestion.TYPE_VERY_SHORT_ANSWERS)) { + // gets mapping answer -> user ID for all answers which were not allocation into VSA option yet + QuestionSummary questionSummary = getQuestionSummary(toolContentId, question.getUid()); + Map notAllocatedAnswerMap = questionSummary.getNotAllocatedQuestionResults().stream() + .collect(Collectors.toMap(AssessmentQuestionResult::getAnswer, + r -> r.getAssessmentResult().getUser().getUserId().intValue(), (user1, user2) -> user1, + LinkedHashMap::new)); + result.put(question, notAllocatedAnswerMap); + } + } + return result; + } + + @Override public QuestionSummary getQuestionSummary(Long contentId, Long questionUid) { AssessmentQuestion question = assessmentQuestionDao.getByUid(questionUid); QbQuestion qbQuestion = question.getQbQuestion(); @@ -1469,42 +1478,19 @@ //prepare extra data for VSA type of questions, so teachers can allocate answers into groups if (isVSA) { - boolean isQuestionCaseSensitive = question.getQbQuestion().isCaseSensitive(); //find all questionResults that are not allocated into groups yet List notAllocatedQuestionResults = new ArrayList<>(); - Set notAllocatedAnswers = new HashSet<>(); + for (AssessmentQuestionResult questionResult : allQuestionResults) { String answer = questionResult.getAnswer(); if (StringUtils.isBlank(answer)) { continue; } - boolean isAnswerAllocated = false; - - String normalisedAnswer = AssessmentEscapeUtils.normaliseVSAnswer(answer); - for (QbOption option : qbQuestion.getQbOptions()) { - Collection alternatives = AssessmentEscapeUtils.normaliseVSOption(option.getName()); - for (String alternative : alternatives) { - if (isQuestionCaseSensitive ? normalisedAnswer.equals(alternative) - : normalisedAnswer.equalsIgnoreCase(alternative)) { - isAnswerAllocated = true; - break; - } - } - if (isAnswerAllocated) { - break; - } - } - + Set notAllocatedAnswers = new HashSet<>(); + boolean isAnswerAllocated = QbUtils.isVSAnswerAllocated(qbQuestion, answer, notAllocatedAnswers); if (!isAnswerAllocated) { - if (!isQuestionCaseSensitive) { - normalisedAnswer = normalisedAnswer.toLowerCase(); - } - // do not add repetitive students' suggestions for teacher to assign to an option - if (!notAllocatedAnswers.contains(normalisedAnswer)) { - notAllocatedAnswers.add(normalisedAnswer); - notAllocatedQuestionResults.add(questionResult); - } + notAllocatedQuestionResults.add(questionResult); } } questionSummary.setNotAllocatedQuestionResults(notAllocatedQuestionResults); @@ -1517,119 +1503,16 @@ return questionSummary; } - public static boolean isAnswersEqual(AssessmentQuestion question, String answer1, String answer2) { - if (answer1 == null || answer2 == null) { - return false; - } - String normalisedAnswer1 = AssessmentEscapeUtils.normaliseVSAnswer(answer1); - String normalisedAnswer2 = AssessmentEscapeUtils.normaliseVSAnswer(answer2); - - return question.getQbQuestion().isCaseSensitive() ? normalisedAnswer1.equals(normalisedAnswer2) - : normalisedAnswer1.equalsIgnoreCase(normalisedAnswer2); - } - @Override - public Long allocateAnswerToOption(Long questionUid, Long targetOptionUid, Long previousOptionUid, String answer) { - AssessmentQuestion assessmentQuestion = assessmentQuestionDao.getByUid(questionUid); - QbQuestion qbQuestion = assessmentQuestion.getQbQuestion(); - String normalisedAnswer = AssessmentEscapeUtils.normaliseVSAnswer(answer); - - //adding - if (previousOptionUid.equals(-1L)) { - //search for duplicates and, if found, return false - QbOption targetOption = null; - for (QbOption option : qbQuestion.getQbOptions()) { - String name = option.getName(); - Collection alternatives = AssessmentEscapeUtils.normaliseVSOption(name); - if (alternatives.contains(normalisedAnswer)) { - return option.getUid(); - } - if (option.getUid().equals(targetOptionUid)) { - targetOption = option; - } - } - - String name = targetOption.getName(); - name += "\r\n" + answer; - targetOption.setName(name); - assessmentDao.saveObject(targetOption); - - if (log.isDebugEnabled()) { - log.debug("Adding answer \"" + answer + "\" to option " + targetOptionUid + " in question " - + questionUid); - } - return null; - } - - //removing - if (targetOptionUid.equals(-1L)) { - for (QbOption previousOption : qbQuestion.getQbOptions()) { - if (previousOption.getUid().equals(previousOptionUid)) { - String name = previousOption.getName(); - String[] alternatives = name.split(","); - - StringBuilder nameWithoutUserAnswer = new StringBuilder(); - for (String alternative : alternatives) { - String normalisedAlternative = AssessmentEscapeUtils.normaliseVSAnswer(alternative); - if (!normalisedAlternative.equals(normalisedAnswer)) { - nameWithoutUserAnswer.append(alternative).append("\r\n"); - } - } - if (nameWithoutUserAnswer.length() > 2) { - previousOption.setName(nameWithoutUserAnswer.substring(0, nameWithoutUserAnswer.length() - 2)); - assessmentDao.saveObject(previousOption); - } - break; - } - } - return null; - } - - //moving from one to another - for (QbOption targetOption : qbQuestion.getQbOptions()) { - if (targetOption.getUid().equals(targetOptionUid)) { - String name = targetOption.getName(); - name += "\r\n" + answer; - targetOption.setName(name); - assessmentDao.saveObject(targetOption); - break; - } - } - - for (QbOption previousOption : qbQuestion.getQbOptions()) { - if (previousOption.getUid().equals(previousOptionUid)) { - String name = previousOption.getName(); - String[] alternatives = name.split(","); - - StringBuilder nameWithoutUserAnswer = new StringBuilder(); - for (String alternative : alternatives) { - String normalisedAlternative = AssessmentEscapeUtils.normaliseVSAnswer(alternative); - if (!normalisedAlternative.equals(normalisedAnswer)) { - nameWithoutUserAnswer.append(alternative).append("\r\n"); - } - } - previousOption.setName(nameWithoutUserAnswer.length() > 2 - ? nameWithoutUserAnswer.substring(0, nameWithoutUserAnswer.length() - 2) - : ""); - assessmentDao.saveObject(previousOption); - break; - } - } - return null; - } - - @Override - public void recalculateMarksForAllocatedAnswer(Long questionUid, String answer) { - AssessmentQuestion assessmentQuestion = assessmentQuestionDao.getByUid(questionUid); - QbQuestion qbQuestion = assessmentQuestion.getQbQuestion(); - // get all finished user results + public boolean recalculateMarksForVsaQuestion(Long qbQuestionUid, String answer) { + // get all user results List assessmentResults = assessmentResultDao - .getAssessmentResultsByQbQuestionAndAnswer(qbQuestion.getUid(), answer); + .getAssessmentResultsByQbQuestionAndAnswer(qbQuestionUid, answer); //stores userId->lastFinishedAssessmentResult - Map lastFinishedAssessmentResults = new LinkedHashMap<>(); + Map assessmentResultsMap = new LinkedHashMap<>(); for (AssessmentResult assessmentResult : assessmentResults) { Long userId = assessmentResult.getUser().getUserId(); - lastFinishedAssessmentResults.put(userId, assessmentResult); + assessmentResultsMap.put(userId, assessmentResult); } for (AssessmentResult assessmentResult : assessmentResults) { @@ -1638,13 +1521,13 @@ int assessmentMaxMark = assessmentResult.getMaximumGrade(); for (AssessmentQuestionResult questionResult : assessmentResult.getQuestionResults()) { - if (questionResult.getQbQuestion().getUid().equals(qbQuestion.getUid())) { + if (questionResult.getQbQuestion().getUid().equals(qbQuestionUid)) { Float oldQuestionAnswerMark = questionResult.getMark(); int oldResultMaxMark = questionResult.getMaxMark() == null ? 0 : questionResult.getMaxMark().intValue(); //actually recalculate marks - QuestionDTO questionDto = new QuestionDTO(assessmentQuestion); + QuestionDTO questionDto = new QuestionDTO(questionResult.getQbToolQuestion()); questionDto.setMaxMark(oldResultMaxMark); loadupQuestionResultIntoQuestionDto(questionDto, questionResult); calculateAnswerMark(assessmentResult.getAssessment().getUid(), user.getUserId(), questionResult, @@ -1658,13 +1541,12 @@ } // store new mark and maxMark if they were changed - AssessmentResult lastFinishedAssessmentResult = lastFinishedAssessmentResults.get(user.getUserId()); + AssessmentResult lastFinishedAssessmentResult = assessmentResultsMap.get(user.getUserId()); storeAssessmentResultMarkAndMaxMark(assessmentResult, lastFinishedAssessmentResult, assessmentMark, assessmentMaxMark, user); } - //recalculate marks in all Scratchie activities, that use modified QbQuestion - toolService.recalculateScratchieMarksForVsaQuestion(qbQuestion.getUid(), answer); + return !assessmentResults.isEmpty(); } @Override @@ -2955,7 +2837,99 @@ return result; } + /** + * Updates updates this iRAT activity with tRAT questions. + */ @Override + public boolean syncRatQuestions(long toolContentId, List newQuestionUids) { + Assessment assessment = getAssessmentByContentId(toolContentId); + + List existingReferences = new ArrayList<>(assessment.getQuestionReferences()); + List newReferences = new ArrayList<>(); + Set referencesToRemove = new HashSet<>(existingReferences); + List newAssessmentQuestions = new ArrayList<>(); + + int displayOrder = 0; + boolean syncNeeded = false; + for (Long newQuestionUid : newQuestionUids) { + QbQuestion newQuestion = qbService.getQuestionByUid(newQuestionUid); + displayOrder++; + + QuestionReference matchingReference = null; + for (QuestionReference existingReference : existingReferences) { + // try to find exactly same question + if (newQuestion.getUid().equals(existingReference.getQuestion().getQbQuestion().getUid())) { + matchingReference = existingReference; + syncNeeded |= displayOrder != existingReference.getSequenceId(); + break; + } + } + + if (matchingReference == null) { + syncNeeded = true; + for (QuestionReference existingReference : existingReferences) { + // try to find same question with another version + if (newQuestion.getQuestionId() + .equals(existingReference.getQuestion().getQbQuestion().getQuestionId())) { + existingReference.getQuestion().setQbQuestion(newQuestion); + matchingReference = existingReference; + break; + } + } + } + if (matchingReference == null) { + // build question reference from scratch + AssessmentQuestion assessmentQuestion = new AssessmentQuestion(); + assessmentQuestion.setDisplayOrder(displayOrder); + assessmentQuestion.setAnswerRequired(true); + assessmentQuestion.setQbQuestion(newQuestion); + assessmentQuestion.setToolContentId(toolContentId); + assessmentQuestionDao.insert(assessmentQuestion); + + matchingReference = new QuestionReference(); + matchingReference.setQuestion(assessmentQuestion); + matchingReference.setSequenceId(displayOrder); + matchingReference.setMaxMark(1); + assessmentQuestionDao.insert(matchingReference); + } else { + matchingReference.setSequenceId(displayOrder); + matchingReference.getQuestion().setDisplayOrder(displayOrder); + referencesToRemove.remove(matchingReference); + } + + newReferences.add(matchingReference); + newAssessmentQuestions.add(matchingReference.getQuestion()); + } + + // all this collections clearing is for Hibernate to feel safe + existingReferences.clear(); + syncNeeded |= !referencesToRemove.isEmpty(); + + if (!syncNeeded) { + return false; + } + + assessment.getQuestionReferences().clear(); + assessment.getQuestions().clear(); + + for (QuestionReference referenceToRemove : referencesToRemove) { + // remove question removed from the matching RAT activity + assessmentQuestionDao.delete(referenceToRemove.getQuestion()); + assessmentQuestionDao.delete(referenceToRemove); + } + referencesToRemove.clear(); + + assessment.getQuestions().addAll(newAssessmentQuestions); + assessment.getQuestionReferences().addAll(newReferences); + newAssessmentQuestions.clear(); + newReferences.clear(); + + assessmentDao.update(assessment); + + return true; + } + + @Override public void replaceQuestion(long toolContentId, long oldQbQuestionUid, long newQbQuestionUid) { Assessment assessment = getAssessmentByContentId(toolContentId); QbQuestion newQbQuestion = null; @@ -3382,7 +3356,7 @@ } @Override - public Collection getVsaAnswers(Long toolSessionId) { + public Collection getVSAnswers(Long toolSessionId) { if (toolSessionId == null) { return new ArrayList<>(); } Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/IAssessmentService.java =================================================================== diff -u -rc33d4aa11d22778bd16a874f0a95237da3f2f10c -r5694a8e26e12cfd208ef7f26d736f02dc6749f23 --- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/IAssessmentService.java (.../IAssessmentService.java) (revision c33d4aa11d22778bd16a874f0a95237da3f2f10c) +++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/IAssessmentService.java (.../IAssessmentService.java) (revision 5694a8e26e12cfd208ef7f26d736f02dc6749f23) @@ -417,23 +417,6 @@ QuestionSummary getQuestionSummary(Long contentId, Long questionUid); /** - * Allocate learner's answer into one of the available answer groups. - * - * @param questionUid - * @param targetOptionUid - * @param previousOptionUid - * @param questionResultUid - * @return if present, it contains optionUid of the option group containing duplicate (added there presumably by - * another teacher working in parallel) - */ - Long allocateAnswerToOption(Long questionUid, Long targetOptionUid, Long previousOptionUid, String answer); - - /** - * Recalculate learners' marks after a VSA answer was allocated as correct or incorrect. - */ - void recalculateMarksForAllocatedAnswer(Long questionUid, String answer); - - /** * For export purposes * * @param contentId Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/MonitoringController.java =================================================================== diff -u -rc33d4aa11d22778bd16a874f0a95237da3f2f10c -r5694a8e26e12cfd208ef7f26d736f02dc6749f23 --- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/MonitoringController.java (.../MonitoringController.java) (revision c33d4aa11d22778bd16a874f0a95237da3f2f10c) +++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/MonitoringController.java (.../MonitoringController.java) (revision 5694a8e26e12cfd208ef7f26d736f02dc6749f23) @@ -295,65 +295,6 @@ return "pages/monitoring/parts/questionsummary"; } - @RequestMapping("/displayVsaAllocate") - public String displayVsaAllocate(HttpServletRequest request, HttpServletResponse response) { - Long contentId = WebUtil.readLongParam(request, AssessmentConstants.ATTR_TOOL_CONTENT_ID); - Assessment assessment = service.getAssessmentByContentId(contentId); - - List questionSummaries = new ArrayList<>(); - for (AssessmentQuestion question : assessment.getQuestions()) { - if (question.getType().equals(QbQuestion.TYPE_VERY_SHORT_ANSWERS)) { - QuestionSummary questionSummary = service.getQuestionSummary(contentId, question.getUid()); - questionSummaries.add(questionSummary); - } - } - request.setAttribute("questionSummaries", questionSummaries); - - return "pages/monitoring/vsaAllocate"; - } - - @RequestMapping(path = "/allocateUserAnswer", method = RequestMethod.POST) - @ResponseBody - public String allocateUserAnswer(HttpServletRequest request, HttpServletResponse response, - @RequestParam Long questionUid, @RequestParam Long targetOptionUid, @RequestParam Long previousOptionUid, - @RequestParam Long questionResultUid) { - - Long optionUid = null; - - if (!targetOptionUid.equals(previousOptionUid)) { - AssessmentQuestionResult questionRes = service.getAssessmentQuestionResultByUid(questionResultUid); - String answer = questionRes.getAnswer(); - /* - * We need to synchronise this operation. - * When multiple requests are made to modify the same option, for example to add a VSA answer, - * we have a case of dirty reads. - * One answer gets added, but while DB is still flushing, - * another answer reads the option without the first answer, - * because it is not there yet. - * The second answer gets added, but the first one gets lost. - * - * We can not synchronise the method in service - * as the "dirty" transaction is already started before synchronisation kicks in. - * We do it here, before transaction starts. - * It will not work for distributed environment, though. - * If teachers allocate answers on different LAMS servers, - * we can still get the same problem. We will need a more sophisticated solution then. - */ - - synchronized (service) { - optionUid = service.allocateAnswerToOption(questionUid, targetOptionUid, previousOptionUid, answer); - } - //recalculate marks for all lessons in all cases except for reshuffling inside the same container - service.recalculateMarksForAllocatedAnswer(questionUid, answer); - } - - ObjectNode responseJSON = JsonNodeFactory.instance.objectNode(); - responseJSON.put("isAnswerDuplicated", optionUid != null); - responseJSON.put("optionUid", optionUid == null ? -1 : optionUid); - response.setContentType("application/json;charset=utf-8"); - return responseJSON.toString(); - } - @RequestMapping("/userSummary") public String userSummary(HttpServletRequest request, HttpServletResponse response) { SessionMap sessionMap = getSessionMap(request);