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);