Index: lams_central/conf/language/lams/ApplicationResources_en_AU.properties =================================================================== diff -u -r2138124964f99aa8eca5d7691ae37bac02a2fa48 -r9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04 --- lams_central/conf/language/lams/ApplicationResources_en_AU.properties (.../ApplicationResources_en_AU.properties) (revision 2138124964f99aa8eca5d7691ae37bac02a2fa48) +++ lams_central/conf/language/lams/ApplicationResources_en_AU.properties (.../ApplicationResources_en_AU.properties) (revision 9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04) @@ -897,7 +897,7 @@ label.create.question =Create question label.question.type.multiple.choice =Multiple choice label.question.type.matching.pairs =Matching pairs -label.question.type.short.answer =Short answer +label.question.type.short.answer =Very short answers label.question.type.numerical =Numerical label.question.type.true.false =True/False label.question.type.essay =Essay @@ -955,6 +955,7 @@ label.authoring.basic.general.feedback =General feedback label.authoring.basic.shuffle.the.choices =Shuffle answers? label.ask.for.hedging.justification =Ask for hedging justification? +label.autocomplete.as.student =Autocomplete (as student types answer autocomplete with stemming from answers) label.authoring.basic.penalty.factor =Penalty factor error.form.validation.hundred.score =One of the answers should have a grade of 100% so it is possible to get full marks for this question. error.form.validation.positive.accepted.errors =All the accepted errors should be positive. @@ -969,7 +970,7 @@ label.authoring.true.false.true =True label.authoring.basic.type.multiple.choice =Multiple choice label.authoring.basic.type.matching.pairs =Matching pairs -label.authoring.basic.type.short.answer =Short answer +label.authoring.basic.type.short.answer =Very short answers label.authoring.basic.type.numerical =Numerical label.authoring.basic.type.true.false =True/False label.authoring.basic.type.essay =Essay @@ -979,5 +980,6 @@ label.authoring.basic.option.answer =Answer label.authoring.basic.delete =Delete label.authoring.basic.none =None +error.positive.grade.required =One of the answers should have a positive grade #======= End labels: Exported 872 labels for en AU ===== Index: lams_central/src/java/org/lamsfoundation/lams/web/qb/EditQbQuestionController.java =================================================================== diff -u -r58230e860ab6c5bf974f43ef5b683c92efff2516 -r9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04 --- lams_central/src/java/org/lamsfoundation/lams/web/qb/EditQbQuestionController.java (.../EditQbQuestionController.java) (revision 58230e860ab6c5bf974f43ef5b683c92efff2516) +++ lams_central/src/java/org/lamsfoundation/lams/web/qb/EditQbQuestionController.java (.../EditQbQuestionController.java) (revision 9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04) @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.lang.reflect.InvocationTargetException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Date; @@ -15,6 +16,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.math.NumberUtils; import org.apache.log4j.Logger; @@ -106,7 +108,7 @@ */ @RequestMapping("/editQuestion") public String editQuestion(@ModelAttribute("assessmentQuestionForm") QbQuestionForm form, - HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException, IllegalAccessException, InvocationTargetException { Long qbQuestionUid = WebUtil.readLongParam(request, "qbQuestionUid"); QbQuestion qbQuestion = qbService.getQuestionByUid(qbQuestionUid); if (qbQuestion == null) { @@ -136,10 +138,12 @@ form.setMaxWordsLimit(qbQuestion.getMaxWordsLimit()); form.setMinWordsLimit(qbQuestion.getMinWordsLimit()); form.setHedgingJustificationEnabled(qbQuestion.isHedgingJustificationEnabled()); + //TODO check autocomplete is saved and then maybe remove other property copying + BeanUtils.copyProperties(form, qbQuestion); Integer questionType = qbQuestion.getType(); if ((questionType == QbQuestion.TYPE_MULTIPLE_CHOICE) || (questionType == QbQuestion.TYPE_ORDERING) - || (questionType == QbQuestion.TYPE_MATCHING_PAIRS) || (questionType == QbQuestion.TYPE_SHORT_ANSWER) + || (questionType == QbQuestion.TYPE_MATCHING_PAIRS) || (questionType == QbQuestion.TYPE_VERY_SHORT_ANSWERS) || (questionType == QbQuestion.TYPE_NUMERICAL) || (questionType == QbQuestion.TYPE_MARK_HEDGING)) { List optionList = qbQuestion.getQbOptions(); request.setAttribute(QbConstants.ATTR_OPTION_LIST, optionList); @@ -259,8 +263,8 @@ // without eviction changes would be saved immediately into DB qbService.releaseFromCache(oldQuestion); - qbQuestion.setName(form.getTitle()); - qbQuestion.setDescription(form.getDescription()); + qbQuestion.setName(form.getTitle().strip()); + qbQuestion.setDescription(form.getDescription().strip()); if (!form.isAuthoringRestricted()) { qbQuestion.setMaxMark(form.getMaxMark()); @@ -282,27 +286,35 @@ qbQuestion.setFeedbackOnCorrect(form.getFeedbackOnCorrect()); qbQuestion.setFeedbackOnPartiallyCorrect(form.getFeedbackOnPartiallyCorrect()); qbQuestion.setFeedbackOnIncorrect(form.getFeedbackOnIncorrect()); + } else if ((type == QbQuestion.TYPE_MATCHING_PAIRS)) { qbQuestion.setPenaltyFactor(Float.parseFloat(form.getPenaltyFactor())); qbQuestion.setShuffle(form.isShuffle()); - } else if ((type == QbQuestion.TYPE_SHORT_ANSWER)) { + + } else if ((type == QbQuestion.TYPE_VERY_SHORT_ANSWERS)) { qbQuestion.setPenaltyFactor(Float.parseFloat(form.getPenaltyFactor())); qbQuestion.setCaseSensitive(form.isCaseSensitive()); + qbQuestion.setAutocompleteEnabled(form.isAutocompleteEnabled()); + } else if ((type == QbQuestion.TYPE_NUMERICAL)) { qbQuestion.setPenaltyFactor(Float.parseFloat(form.getPenaltyFactor())); + } else if ((type == QbQuestion.TYPE_TRUE_FALSE)) { qbQuestion.setPenaltyFactor(Float.parseFloat(form.getPenaltyFactor())); qbQuestion.setCorrectAnswer(form.isCorrectAnswer()); qbQuestion.setFeedbackOnCorrect(form.getFeedbackOnCorrect()); qbQuestion.setFeedbackOnIncorrect(form.getFeedbackOnIncorrect()); + } else if ((type == QbQuestion.TYPE_ESSAY)) { qbQuestion.setAllowRichEditor(form.isAllowRichEditor()); qbQuestion.setMaxWordsLimit(form.getMaxWordsLimit()); qbQuestion.setMinWordsLimit(form.getMinWordsLimit()); + } else if (type == QbQuestion.TYPE_ORDERING) { qbQuestion.setPenaltyFactor(Float.parseFloat(form.getPenaltyFactor())); qbQuestion.setFeedbackOnCorrect(form.getFeedbackOnCorrect()); qbQuestion.setFeedbackOnIncorrect(form.getFeedbackOnIncorrect()); + } else if (type == QbQuestion.TYPE_MARK_HEDGING) { qbQuestion.setShuffle(form.isShuffle()); qbQuestion.setFeedbackOnCorrect(form.getFeedbackOnCorrect()); @@ -313,7 +325,7 @@ // set options if ((type == QbQuestion.TYPE_MULTIPLE_CHOICE) || (type == QbQuestion.TYPE_ORDERING) - || (type == QbQuestion.TYPE_MATCHING_PAIRS) || (type == QbQuestion.TYPE_SHORT_ANSWER) + || (type == QbQuestion.TYPE_MATCHING_PAIRS) || (type == QbQuestion.TYPE_VERY_SHORT_ANSWERS) || (type == QbQuestion.TYPE_NUMERICAL) || (type == QbQuestion.TYPE_MARK_HEDGING)) { Set optionList = getOptionsFromRequest(request, true); List options = new ArrayList<>(); @@ -420,7 +432,7 @@ } option.setDisplayOrder(NumberUtils.toInt(displayOrder)); - if ((questionType == QbQuestion.TYPE_MULTIPLE_CHOICE) || (questionType == QbQuestion.TYPE_SHORT_ANSWER)) { + if ((questionType == QbQuestion.TYPE_MULTIPLE_CHOICE) || (questionType == QbQuestion.TYPE_VERY_SHORT_ANSWERS)) { String name = paramMap.get(QbConstants.ATTR_OPTION_NAME_PREFIX + i); if ((name == null) && isForSaving) { continue; @@ -488,6 +500,17 @@ optionList.add(option); } + +// //in case of VSA make sure it has 2 option groups, one of which having 0 maxMark +// if (questionType == QbQuestion.TYPE_VERY_SHORT_ANSWERS && optionList.size() == 1) { +// QbOption option = new QbOption(); +// option.setDisplayOrder(1); +// option.setName(""); +// option.setMaxMark(0); +// option.setFeedback(""); +// optionList.add(option); +// } + return optionList; } @@ -570,8 +593,8 @@ case QbQuestion.TYPE_MATCHING_PAIRS: forward = "qb/authoring/addmatchingpairs"; break; - case QbQuestion.TYPE_SHORT_ANSWER: - forward = "qb/authoring/addshortanswer"; + case QbQuestion.TYPE_VERY_SHORT_ANSWERS: + forward = "qb/authoring/addVsa"; break; case QbQuestion.TYPE_NUMERICAL: forward = "qb/authoring/addnumerical"; Index: lams_central/src/java/org/lamsfoundation/lams/web/qb/SearchQBController.java =================================================================== diff -u -rb30f0a3f132ca66aa6f9799d5eb560e82d204a00 -r9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04 --- lams_central/src/java/org/lamsfoundation/lams/web/qb/SearchQBController.java (.../SearchQBController.java) (revision b30f0a3f132ca66aa6f9799d5eb560e82d204a00) +++ lams_central/src/java/org/lamsfoundation/lams/web/qb/SearchQBController.java (.../SearchQBController.java) (revision 9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04) @@ -99,9 +99,13 @@ StringBuilder questionTypesAvailable = new StringBuilder(); //by default show MCQ type of questions (except for Q&A tool) int questionTypeDefault = QbQuestion.TYPE_MULTIPLE_CHOICE; - if (CommonConstants.TOOL_SIGNATURE_SCRATCHIE.equals(toolSignature) - || CommonConstants.TOOL_SIGNATURE_MCQ.equals(toolSignature)) { + if (CommonConstants.TOOL_SIGNATURE_MCQ.equals(toolSignature)) { + } else if (CommonConstants.TOOL_SIGNATURE_SCRATCHIE.equals(toolSignature)) { + questionTypesAvailable.append(QbQuestion.TYPE_MULTIPLE_CHOICE); + questionTypesAvailable.append(","); + questionTypesAvailable.append(QbQuestion.TYPE_VERY_SHORT_ANSWERS); + //CommonConstants.TOOL_SIGNATURE_SURVEY } else if ("lasurv11".equals(toolSignature)) { questionTypesAvailable.append(QbQuestion.TYPE_MULTIPLE_CHOICE); @@ -113,7 +117,7 @@ questionTypesAvailable.append(","); questionTypesAvailable.append(QbQuestion.TYPE_MATCHING_PAIRS); questionTypesAvailable.append(","); - questionTypesAvailable.append(QbQuestion.TYPE_SHORT_ANSWER); + questionTypesAvailable.append(QbQuestion.TYPE_VERY_SHORT_ANSWERS); questionTypesAvailable.append(","); questionTypesAvailable.append(QbQuestion.TYPE_NUMERICAL); questionTypesAvailable.append(","); @@ -131,6 +135,8 @@ } request.setAttribute("questionType", questionTypeDefault); request.setAttribute("questionTypesAvailable", questionTypesAvailable.toString()); + //let jsp know it's Scratchie, so we can disable VSA questions not compatible with TBL + request.setAttribute("isScratchie", CommonConstants.TOOL_SIGNATURE_SCRATCHIE.equals(toolSignature)); //prepare data for displaying collections Integer userId = getUserId(); @@ -214,6 +220,11 @@ QbQuestion qbQuestion = (QbQuestion) userManagementService.findById(QbQuestion.class, questionUid); request.setAttribute("question", qbQuestion); + boolean isVsaAndCompatibleWithTbl = qbQuestion.isVsaAndCompatibleWithTbl(); + request.setAttribute("isVsaAndCompatibleWithTbl", isVsaAndCompatibleWithTbl); + boolean isScratchie = WebUtil.readBooleanParam(request, "isScratchie", false); + request.setAttribute("isScratchie", isScratchie); + List otherVersions = qbService.getQuestionsByQuestionId(qbQuestion.getQuestionId()); request.setAttribute("otherVersions", otherVersions); Index: lams_central/web/qb/search.jsp =================================================================== diff -u -rb30f0a3f132ca66aa6f9799d5eb560e82d204a00 -r9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04 --- lams_central/web/qb/search.jsp (.../search.jsp) (revision b30f0a3f132ca66aa6f9799d5eb560e82d204a00) +++ lams_central/web/qb/search.jsp (.../search.jsp) (revision 9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04) @@ -236,7 +236,8 @@ $("#question-detail-area").show().load( '', { - questionUid: questionUid + questionUid: questionUid, + isScratchie: ${isScratchie} }, function() { } Index: lams_central/web/qb/stats.jsp =================================================================== diff -u -r23c28a3348397aefc09925cefbb98c042f747002 -r9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04 --- lams_central/web/qb/stats.jsp (.../stats.jsp) (revision 23c28a3348397aefc09925cefbb98c042f747002) +++ lams_central/web/qb/stats.jsp (.../stats.jsp) (revision 9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04) @@ -2,8 +2,9 @@ <%@ taglib uri="tags-lams" prefix="lams"%> <%@ taglib uri="tags-fmt" prefix="fmt"%> <%@ taglib uri="tags-core" prefix="c"%> - +<%@ taglib uri="tags-function" prefix="fn" %> +<% pageContext.setAttribute("newLineChar", "\r\n"); %> @@ -219,7 +220,15 @@ ${status.index + 1} - + + + ${fn:replace(option.name, newLineChar, ', ')} + + + + + + Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/assessmentApplicationContext.xml =================================================================== diff -u -r3649999936753b6c474d93ba9fa603bfd0d18a2c -r9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04 --- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/assessmentApplicationContext.xml (.../assessmentApplicationContext.xml) (revision 3649999936753b6c474d93ba9fa603bfd0d18a2c) +++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/assessmentApplicationContext.xml (.../assessmentApplicationContext.xml) (revision 9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04) @@ -138,6 +138,7 @@ PROPAGATION_REQUIRED,-java.lang.Exception PROPAGATION_REQUIRED,-java.lang.Exception PROPAGATION_REQUIRED,-java.lang.Exception + PROPAGATION_REQUIRED,-java.lang.Exception PROPAGATION_REQUIRED,-java.lang.Exception PROPAGATION_REQUIRED,-java.lang.Exception PROPAGATION_REQUIRED,-java.lang.Exception Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/AssessmentServiceImpl.java =================================================================== diff -u -r3649999936753b6c474d93ba9fa603bfd0d18a2c -r9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04 --- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/AssessmentServiceImpl.java (.../AssessmentServiceImpl.java) (revision 3649999936753b6c474d93ba9fa603bfd0d18a2c) +++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/AssessmentServiceImpl.java (.../AssessmentServiceImpl.java) (revision 9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04) @@ -49,6 +49,7 @@ import org.apache.log4j.Logger; import org.apache.poi.ss.usermodel.IndexedColors; import org.lamsfoundation.lams.confidencelevel.ConfidenceLevelDTO; +import org.lamsfoundation.lams.confidencelevel.VsaAnswerDTO; import org.lamsfoundation.lams.contentrepository.client.IToolContentHandler; import org.lamsfoundation.lams.events.IEventNotificationService; import org.lamsfoundation.lams.learning.service.ILearnerService; @@ -103,6 +104,7 @@ import org.lamsfoundation.lams.tool.assessment.util.SequencableComparator; import org.lamsfoundation.lams.tool.exception.DataMissingException; import org.lamsfoundation.lams.tool.exception.ToolException; +import org.lamsfoundation.lams.tool.service.ICommonAssessmentService; import org.lamsfoundation.lams.tool.service.ILamsToolService; import org.lamsfoundation.lams.tool.service.IQbToolService; import org.lamsfoundation.lams.usermanagement.User; @@ -121,8 +123,8 @@ /** * @author Andrey Balan */ -public class AssessmentServiceImpl - implements IAssessmentService, ToolContentManager, ToolSessionManager, ToolRestManager, IQbToolService { +public class AssessmentServiceImpl implements IAssessmentService, ICommonAssessmentService, ToolContentManager, + ToolSessionManager, ToolRestManager, IQbToolService { private static Logger log = Logger.getLogger(AssessmentServiceImpl.class.getName()); private AssessmentDAO assessmentDao; @@ -744,37 +746,42 @@ } } - } else if (questionDto.getType() == QbQuestion.TYPE_SHORT_ANSWER) { + } else if (questionDto.getType() == QbQuestion.TYPE_VERY_SHORT_ANSWERS) { //clear previous answer questionResult.setQbOption(null); for (OptionDTO optionDto : questionDto.getOptionDtos()) { + String[] optionAnswers = optionDto.getName().strip().split("\\r\\n"); + boolean isAnswerMatchedCurrentOption = false; + for (String optionAnswer : optionAnswers) { + optionAnswer = optionAnswer.strip(); - //prepare regex which takes into account only * special character - String regexWithOnlyAsteriskSymbolActive = "\\Q"; - String name = optionDto.getName().trim(); - for (int i = 0; i < name.length(); i++) { - //everything in between \\Q and \\E are taken literally no matter which characters it contains - if (name.charAt(i) == '*') { - regexWithOnlyAsteriskSymbolActive += "\\E.*\\Q"; + //prepare regex which takes into account only * special character + String regexWithOnlyAsteriskSymbolActive = "\\Q"; + for (int i = 0; i < optionAnswer.length(); i++) { + //everything in between \\Q and \\E are taken literally no matter which characters it contains + if (optionAnswer.charAt(i) == '*') { + regexWithOnlyAsteriskSymbolActive += "\\E.*\\Q"; + } else { + regexWithOnlyAsteriskSymbolActive += optionAnswer.charAt(i); + } + } + regexWithOnlyAsteriskSymbolActive += "\\E"; + + //check whether answer matches regex + Pattern pattern; + if (questionDto.isCaseSensitive()) { + pattern = Pattern.compile(regexWithOnlyAsteriskSymbolActive); } else { - regexWithOnlyAsteriskSymbolActive += name.charAt(i); + pattern = Pattern.compile(regexWithOnlyAsteriskSymbolActive, + java.util.regex.Pattern.CASE_INSENSITIVE | java.util.regex.Pattern.UNICODE_CASE); } + if (questionDto.getAnswer() != null && pattern.matcher(questionDto.getAnswer().strip()).matches()) { + isAnswerMatchedCurrentOption = true; + break; + } } - regexWithOnlyAsteriskSymbolActive += "\\E"; - //check whether answer matches regex - Pattern pattern; - if (questionDto.isCaseSensitive()) { - pattern = Pattern.compile(regexWithOnlyAsteriskSymbolActive); - } else { - pattern = Pattern.compile(regexWithOnlyAsteriskSymbolActive, - java.util.regex.Pattern.CASE_INSENSITIVE | java.util.regex.Pattern.UNICODE_CASE); - } - boolean isAnswerMatchedCurrentOption = (questionDto.getAnswer() != null) - ? pattern.matcher(questionDto.getAnswer().trim()).matches() - : false; - if (isAnswerMatchedCurrentOption) { mark = optionDto.getMaxMark() * maxMark; QbOption qbOption = qbService.getOptionByUid(optionDto.getUid()); @@ -1315,11 +1322,177 @@ @Override public QuestionSummary getQuestionSummary(Long contentId, Long questionUid) { AssessmentQuestion question = assessmentQuestionDao.getByUid(questionUid); + QbQuestion qbQuestion = question.getQbQuestion(); + List allQuestionResults = assessmentQuestionResultDao + .getQuestionResultsByQuestionUid(questionUid); - return new QuestionSummary(question); + QuestionSummary questionSummary = new QuestionSummary(question); + + //prepare extra data for VSA type of questions, so teachers can allocate answers into groups + if (question.getType() == QbQuestion.TYPE_VERY_SHORT_ANSWERS) { + //find all questionResults that are not allocated into groups yet + List notAllocatedQuestionResults = new ArrayList<>(); + for (AssessmentQuestionResult questionResult : allQuestionResults) { + String answer = questionResult.getAnswer(); + + boolean isAnswerAllocated = false; + for (QbOption option : qbQuestion.getQbOptions()) { + String[] alternatives = option.getName().split("\r\n"); + for (String alternative : alternatives) { + if (answer != null && alternative.equals(answer)) { + isAnswerAllocated = true; + break; + } + } + } + + if (!isAnswerAllocated) { + notAllocatedQuestionResults.add(questionResult); + } + } + questionSummary.setNotAllocatedQuestionResults(notAllocatedQuestionResults); + + //check is it a TBL case, i.e. only two option groups available, one has 0%, second - 100% + boolean isCompatibleWithTbl = qbQuestion.isVsaAndCompatibleWithTbl(); + questionSummary.setTbl(isCompatibleWithTbl); + } + + return questionSummary; } @Override + public void allocateAnswerToOption(Long questionUid, Long targetOptionUid, Long previousOptionUid, + Long questionResultUid) { + AssessmentQuestion assessmentQuestion = assessmentQuestionDao.getByUid(questionUid); + QbQuestion qbQuestion = assessmentQuestion.getQbQuestion(); + AssessmentQuestionResult questionRes = assessmentQuestionResultDao + .getAssessmentQuestionResultByUid(questionResultUid); + String answer = questionRes.getAnswer(); + + //adding + if (previousOptionUid.equals(-1L)) { + 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; + } + } + + } + //removing + else if (targetOptionUid.equals(-1L)) { + for (QbOption previousOption : qbQuestion.getQbOptions()) { + if (previousOption.getUid().equals(previousOptionUid)) { + String name = previousOption.getName(); + String[] alternatives = name.split("\r\n"); + + String nameWithoutUserAnswer = ""; + for (String alternative : alternatives) { + if (!alternative.equals(answer)) { + nameWithoutUserAnswer += alternative + "\r\n"; + } + } + if (nameWithoutUserAnswer.length() > 2) { + previousOption.setName(nameWithoutUserAnswer.substring(0, nameWithoutUserAnswer.length() - 2)); + assessmentDao.saveObject(previousOption); + } + break; + } + } + + } + //reshuffling inside the same container - do nothing + else if (targetOptionUid.equals(previousOptionUid)) { + + } + //moving from one to another + else { + 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("\r\n"); + + String nameWithoutUserAnswer = ""; + for (String alternative : alternatives) { + if (!alternative.equals(answer)) { + nameWithoutUserAnswer += alternative + "\r\n"; + } + } + if (nameWithoutUserAnswer.length() > 2) { + nameWithoutUserAnswer = nameWithoutUserAnswer.substring(0, nameWithoutUserAnswer.length() - 2); + } + previousOption.setName(nameWithoutUserAnswer); + assessmentDao.saveObject(previousOption); + break; + } + } + } + assessmentResultDao.flush(); + + //recalculate marks for all lessons in all cases except for reshuffling inside the same container + if (!targetOptionUid.equals(previousOptionUid)) { + + // get all finished user results + List assessmentResults = assessmentResultDao + .getAssessmentResultsByQbQuestion(qbQuestion.getUid()); + + //stores userId->lastFinishedAssessmentResult + Map lastFinishedAssessmentResults = new LinkedHashMap<>(); + for (AssessmentResult assessmentResult : assessmentResults) { + Long userId = assessmentResult.getUser().getUserId(); + lastFinishedAssessmentResults.put(userId, assessmentResult); + } + + for (AssessmentResult assessmentResult : assessmentResults) { + AssessmentUser user = assessmentResult.getUser(); + float assessmentMark = assessmentResult.getGrade(); + int assessmentMaxMark = assessmentResult.getMaximumGrade(); + + for (AssessmentQuestionResult questionResult : assessmentResult.getQuestionResults()) { + if (questionResult.getQbQuestion().getUid().equals(qbQuestion.getUid())) { + Float oldQuestionAnswerMark = questionResult.getMark(); + int oldResultMaxMark = questionResult.getMaxMark() == null ? 0 + : questionResult.getMaxMark().intValue(); + + //actually recalculate marks + QuestionDTO questionDto = new QuestionDTO(assessmentQuestion); + questionDto.setMaxMark(oldResultMaxMark); + loadupQuestionResultIntoQuestionDto(questionDto, questionResult); + calculateAnswerMark(assessmentResult.getAssessment().getUid(), user.getUserId(), questionResult, + questionDto); + assessmentQuestionResultDao.saveObject(questionResult); + + float newQuestionAnswerMark = questionResult.getMark(); + assessmentMark += newQuestionAnswerMark - oldQuestionAnswerMark; + break; + } + } + + // store new mark and maxMark if they were changed + AssessmentResult lastFinishedAssessmentResult = lastFinishedAssessmentResults.get(user.getUserId()); + storeAssessmentResultMarkAndMaxMark(assessmentResult, lastFinishedAssessmentResult, assessmentMark, + assessmentMaxMark, user); + } + + //recalculate marks in all Scratchie activities, that use modified QbQuestion + toolService.recalculateScratchieMarksForVsaQuestion(qbQuestion.getUid()); + } + } + + @Override public Map getQuestionSummaryForExport(Assessment assessment) { Map questionSummaries = new LinkedHashMap<>(); @@ -1599,7 +1772,7 @@ // set up the summary table data for the top of the question area. boolean doSummaryTable = question.getType() == QbQuestion.TYPE_MULTIPLE_CHOICE - || question.getType() == QbQuestion.TYPE_SHORT_ANSWER + || question.getType() == QbQuestion.TYPE_VERY_SHORT_ANSWERS || question.getType() == QbQuestion.TYPE_NUMERICAL || question.getType() == QbQuestion.TYPE_TRUE_FALSE; // For MC, Numeric & Short Answer Key is optionUid, Value is number of answers @@ -1959,7 +2132,8 @@ Long trueKey, Long falseKey) { ExcelCell[] summaryTable; int i = 0; - if (question.getType() == QbQuestion.TYPE_MULTIPLE_CHOICE || question.getType() == QbQuestion.TYPE_SHORT_ANSWER + if (question.getType() == QbQuestion.TYPE_MULTIPLE_CHOICE + || question.getType() == QbQuestion.TYPE_VERY_SHORT_ANSWERS || question.getType() == QbQuestion.TYPE_NUMERICAL) { List options = question.getQbQuestion().getQbOptions(); summaryTable = new ExcelCell[options.size() + 1]; @@ -2015,7 +2189,7 @@ if (!foundOption) { summaryNACount++; } - } else if (question.getType() == QbQuestion.TYPE_SHORT_ANSWER + } else if (question.getType() == QbQuestion.TYPE_VERY_SHORT_ANSWERS || question.getType() == QbQuestion.TYPE_NUMERICAL) { Long submittedOptionUid = questionResult.getQbOption() == null ? null : questionResult.getQbOption().getUid(); @@ -2057,7 +2231,8 @@ total += value; } int i = 0; - if (question.getType() == QbQuestion.TYPE_MULTIPLE_CHOICE || question.getType() == QbQuestion.TYPE_SHORT_ANSWER + if (question.getType() == QbQuestion.TYPE_MULTIPLE_CHOICE + || question.getType() == QbQuestion.TYPE_VERY_SHORT_ANSWERS || question.getType() == QbQuestion.TYPE_NUMERICAL) { for (QbOption option : question.getQbQuestion().getQbOptions()) { summaryTable[i] = new ExcelCell(valueAsPercentage(summaryOfAnswers.get(option.getUid()), total), false); @@ -2092,7 +2267,7 @@ return "Numerical"; case QbQuestion.TYPE_ORDERING: return "Ordering"; - case QbQuestion.TYPE_SHORT_ANSWER: + case QbQuestion.TYPE_VERY_SHORT_ANSWERS: return "Short Answer"; case QbQuestion.TYPE_TRUE_FALSE: return "True/False"; @@ -2196,7 +2371,7 @@ if (oldOption.getDisplayOrder() == newOption.getDisplayOrder()) { //short answer - if (((oldQuestion.getType() == QbQuestion.TYPE_SHORT_ANSWER) + if (((oldQuestion.getType() == QbQuestion.TYPE_VERY_SHORT_ANSWERS) && !StringUtils.equals(oldOption.getName(), newOption.getName())) //numbering || (oldOption.getNumericalOption() != newOption.getNumericalOption()) @@ -2271,7 +2446,7 @@ //update questionResult's qbQuestion with the new one questionResult.setQbToolQuestion(modifiedQuestion); - //update questionResult's qbOption + //update questionResult's qbOption - it seems to be redundant, as it's done in loadupQuestionResultIntoQuestionDto() // for (QbOption newOption : modifiedQuestion.getQbQuestion().getQbOptions()) { // if (questionResult.getQbOption().getDisplayOrder() == newOption.getDisplayOrder()) { // questionResult.setQbOption(newOption); @@ -2384,30 +2559,38 @@ } // store new mark and maxMark if they were changed - if ((assessmentResult.getGrade() != assessmentMark) - || (assessmentResult.getMaximumGrade() != assessmentMaxMark)) { + storeAssessmentResultMarkAndMaxMark(assessmentResult, lastFinishedAssessmentResult, assessmentMark, + assessmentMaxMark, user); + } + } + } + } - // marks can't be below zero - assessmentMark = (assessmentMark < 0) ? 0 : assessmentMark; - assessmentMaxMark = (assessmentMaxMark < 0) ? 0 : assessmentMaxMark; + /** + * Store new mark and maxMark if they were changed + */ + private void storeAssessmentResultMarkAndMaxMark(AssessmentResult assessmentResult, + AssessmentResult lastFinishedAssessmentResult, float newAssessmentMark, int newAssessmentMaxMark, + AssessmentUser user) { + // store new mark and maxMark if they were changed + if ((assessmentResult.getGrade() != newAssessmentMark) + || (assessmentResult.getMaximumGrade() != newAssessmentMaxMark)) { - assessmentResult.setGrade(assessmentMark); - assessmentResult.setMaximumGrade(assessmentMaxMark); - assessmentResultDao.saveObject(assessmentResult); + // marks can't be below zero + newAssessmentMark = (newAssessmentMark < 0) ? 0 : newAssessmentMark; + newAssessmentMaxMark = (newAssessmentMaxMark < 0) ? 0 : newAssessmentMaxMark; - // if this is the last finished assessment result - propagade total mark to Gradebook - if (lastFinishedAssessmentResult != null - && lastFinishedAssessmentResult.getUid().equals(assessmentResult.getUid())) { - toolService.updateActivityMark(Double.valueOf(assessmentMark), null, - user.getUserId().intValue(), toolSessionId, false); - } - } + assessmentResult.setGrade(newAssessmentMark); + assessmentResult.setMaximumGrade(newAssessmentMaxMark); + assessmentResultDao.saveObject(assessmentResult); - } - + // if this is the last finished assessment result - propagade total mark to Gradebook + if (lastFinishedAssessmentResult != null + && lastFinishedAssessmentResult.getUid().equals(assessmentResult.getUid())) { + toolService.updateActivityMark(Double.valueOf(newAssessmentMark), null, user.getUserId().intValue(), + user.getSession().getSessionId(), false); } } - } @Override @@ -2925,7 +3108,7 @@ } else if (qbQuestion.getType() == QbQuestion.TYPE_MATCHING_PAIRS) { - } else if (qbQuestion.getType() == QbQuestion.TYPE_SHORT_ANSWER) { + } else if (qbQuestion.getType() == QbQuestion.TYPE_VERY_SHORT_ANSWERS) { answers.add(questionResult.getAnswer()); } else if (qbQuestion.getType() == QbQuestion.TYPE_NUMERICAL) { @@ -2977,6 +3160,78 @@ } @Override + public Collection getVsaAnswers(Long toolSessionId) { + if (toolSessionId == null) { + return new ArrayList<>(); + } + + Map uid_answerToVsaAnswerDtoMap = new LinkedHashMap<>(); + + Assessment assessment = getAssessmentBySessionId(toolSessionId); + //in case Assessment is leader aware return all leaders confidences, otherwise - confidences from the users from the same group as requestor + List assessmentResultsAndPortraits = assessment.isUseSelectLeaderToolOuput() + ? assessmentResultDao.getLeadersLastFinishedAssessmentResults(assessment.getContentId()) + : assessmentResultDao.getLastFinishedAssessmentResultsBySession(toolSessionId); + + for (Object[] assessmentResultsAndPortraitIter : assessmentResultsAndPortraits) { + AssessmentResult assessmentResult = (AssessmentResult) assessmentResultsAndPortraitIter[0]; + Long portraitUuid = assessmentResultsAndPortraitIter[1] == null ? null + : ((Number) assessmentResultsAndPortraitIter[1]).longValue(); + AssessmentUser user = assessmentResult.getUser(); + + //fill in question's and user answer's hashes + for (AssessmentQuestionResult questionResult : assessmentResult.getQuestionResults()) { + QbQuestion qbQuestion = questionResult.getQbQuestion(); + + if (qbQuestion.getType() == QbQuestion.TYPE_VERY_SHORT_ANSWERS) { + //uid_answer should be unique in the final list + String uid_answer = qbQuestion.getUid() + "_" + questionResult.getAnswer(); + + //find VsaAnswerDTO in the map, or create the new one if it doesn't exist + VsaAnswerDTO vsaAnswerDTO; + if (uid_answerToVsaAnswerDtoMap.containsKey(uid_answer)) { + vsaAnswerDTO = uid_answerToVsaAnswerDtoMap.get(uid_answer); + + } else { + vsaAnswerDTO = new VsaAnswerDTO(); + vsaAnswerDTO.setQbQuestionUid(qbQuestion.getUid()); + vsaAnswerDTO.setAnswer(questionResult.getAnswer()); + vsaAnswerDTO.setCorrect(questionResult.getMark() > 0); + vsaAnswerDTO.setUserId(user.getUserId()); + uid_answerToVsaAnswerDtoMap.put(uid_answer, vsaAnswerDTO); + + //find and store optionUid + for (QbOption option : qbQuestion.getQbOptions()) { + for (AssessmentOptionAnswer optionAnswer : questionResult.getOptionAnswers()) { + if (optionAnswer.getAnswerBoolean() + && (optionAnswer.getOptionUid().equals(option.getUid()))) { + Long optionUid = option.getUid(); + vsaAnswerDTO.setQbOptionUid(optionUid); + break; + } + } + } + } + + ConfidenceLevelDTO confidenceLevelDto = new ConfidenceLevelDTO(); + confidenceLevelDto.setUserId(user.getUserId().intValue()); + String userName = StringUtils.isBlank(user.getFirstName()) + && StringUtils.isBlank(user.getLastName()) ? user.getLoginName() + : user.getFirstName() + " " + user.getLastName(); + confidenceLevelDto.setUserName(userName); + confidenceLevelDto.setPortraitUuid(portraitUuid); + confidenceLevelDto.setLevel(questionResult.getConfidenceLevel()); + + vsaAnswerDTO.getConfidenceLevels().add(confidenceLevelDto); + } + } + + } + + return uid_answerToVsaAnswerDtoMap.values(); + } + + @Override public void forceCompleteUser(Long toolSessionId, User user) { Long userId = user.getUserId().longValue(); @@ -3188,34 +3443,37 @@ for (JsonNode questionJSONData : questions) { AssessmentQuestion question = new AssessmentQuestion(); Integer type = JsonUtil.optInt(questionJSONData, "type"); - question.getQbQuestion().setType(type); - question.getQbQuestion().setName(questionJSONData.get(RestTags.QUESTION_TITLE).asText()); - question.getQbQuestion().setDescription(questionJSONData.get(RestTags.QUESTION_TEXT).asText()); + + QbQuestion qbQuestion = new QbQuestion(); + qbQuestion.setQuestionId(qbService.generateNextQuestionId()); + qbQuestion.setType(type); + qbQuestion.setName(questionJSONData.get(RestTags.QUESTION_TITLE).asText()); + qbQuestion.setDescription(questionJSONData.get(RestTags.QUESTION_TEXT).asText()); + question.setQbQuestion(qbQuestion); question.setDisplayOrder(JsonUtil.optInt(questionJSONData, RestTags.DISPLAY_ORDER)); - question.getQbQuestion().setAllowRichEditor( + qbQuestion.setAllowRichEditor( JsonUtil.optBoolean(questionJSONData, RestTags.ALLOW_RICH_TEXT_EDITOR, Boolean.FALSE)); - question.getQbQuestion() - .setAnswerRequired(JsonUtil.optBoolean(questionJSONData, "answerRequired", Boolean.FALSE)); - question.getQbQuestion() - .setCaseSensitive(JsonUtil.optBoolean(questionJSONData, "caseSensitive", Boolean.FALSE)); - question.getQbQuestion() - .setCorrectAnswer(JsonUtil.optBoolean(questionJSONData, "correctAnswer", Boolean.FALSE)); - question.getQbQuestion().setMaxMark(JsonUtil.optInt(questionJSONData, "maxMark", 1)); - question.getQbQuestion().setFeedback(JsonUtil.optString(questionJSONData, "feedback")); - question.getQbQuestion().setFeedbackOnCorrect(JsonUtil.optString(questionJSONData, "feedbackOnCorrect")); - question.getQbQuestion() - .setFeedbackOnIncorrect(JsonUtil.optString(questionJSONData, "feedbackOnIncorrect")); - question.getQbQuestion() + qbQuestion.setAnswerRequired(JsonUtil.optBoolean(questionJSONData, "answerRequired", Boolean.FALSE)); + qbQuestion.setCaseSensitive(JsonUtil.optBoolean(questionJSONData, "caseSensitive", Boolean.FALSE)); + qbQuestion.setCorrectAnswer(JsonUtil.optBoolean(questionJSONData, "correctAnswer", Boolean.FALSE)); + qbQuestion.setMaxMark(JsonUtil.optInt(questionJSONData, "maxMark", 1)); + qbQuestion.setFeedback(JsonUtil.optString(questionJSONData, "feedback")); + qbQuestion.setFeedbackOnCorrect(JsonUtil.optString(questionJSONData, "feedbackOnCorrect")); + qbQuestion.setFeedbackOnIncorrect(JsonUtil.optString(questionJSONData, "feedbackOnIncorrect")); + qbQuestion .setFeedbackOnPartiallyCorrect(JsonUtil.optString(questionJSONData, "feedbackOnPartiallyCorrect")); - question.getQbQuestion().setMaxWordsLimit(JsonUtil.optInt(questionJSONData, "maxWordsLimit", 0)); - question.getQbQuestion().setMinWordsLimit(JsonUtil.optInt(questionJSONData, "minWordsLimit", 0)); - question.getQbQuestion().setMultipleAnswersAllowed( + qbQuestion.setMaxWordsLimit(JsonUtil.optInt(questionJSONData, "maxWordsLimit", 0)); + qbQuestion.setMinWordsLimit(JsonUtil.optInt(questionJSONData, "minWordsLimit", 0)); + qbQuestion.setMultipleAnswersAllowed( JsonUtil.optBoolean(questionJSONData, "multipleAnswersAllowed", Boolean.FALSE)); - question.getQbQuestion().setIncorrectAnswerNullifiesMark( + qbQuestion.setIncorrectAnswerNullifiesMark( JsonUtil.optBoolean(questionJSONData, "incorrectAnswerNullifiesMark", Boolean.FALSE)); - question.getQbQuestion() - .setPenaltyFactor(JsonUtil.optDouble(questionJSONData, "penaltyFactor", 0.0).floatValue()); + qbQuestion.setPenaltyFactor(JsonUtil.optDouble(questionJSONData, "penaltyFactor", 0.0).floatValue()); + + assessmentDao.insert(qbQuestion); + question.setToolContentId(toolContentID); + // question.setUnits(units); Needed for numerical type question if ((type == QbQuestion.TYPE_MATCHING_PAIRS) || (type == QbQuestion.TYPE_MULTIPLE_CHOICE) @@ -3230,8 +3488,10 @@ ArrayNode optionsData = JsonUtil.optArray(questionJSONData, RestTags.ANSWERS); for (JsonNode answerData : optionsData) { QbOption option = new QbOption(); + option.setQbQuestion(qbQuestion); option.setDisplayOrder(JsonUtil.optInt(answerData, RestTags.DISPLAY_ORDER)); - option.setMaxMark(answerData.get("maxMark").floatValue()); + Double maxMark = JsonUtil.optDouble(answerData, "maxMark"); + option.setMaxMark(maxMark == null ? 1 : maxMark.floatValue()); option.setCorrect(JsonUtil.optBoolean(answerData, "correct", false)); option.setAcceptedError(JsonUtil.optDouble(answerData, "acceptedError", 0.0).floatValue()); option.setFeedback(JsonUtil.optString(answerData, "feedback")); Index: lams_tool_lamc/src/java/org/lamsfoundation/lams/tool/mc/service/McService.java =================================================================== diff -u -r3649999936753b6c474d93ba9fa603bfd0d18a2c -r9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04 --- lams_tool_lamc/src/java/org/lamsfoundation/lams/tool/mc/service/McService.java (.../McService.java) (revision 3649999936753b6c474d93ba9fa603bfd0d18a2c) +++ lams_tool_lamc/src/java/org/lamsfoundation/lams/tool/mc/service/McService.java (.../McService.java) (revision 9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04) @@ -293,6 +293,7 @@ // set clone's data to current values qbQuestionClone.setName(questionDTO.getName()); qbQuestionClone.setDescription(questionDTO.getDescription()); + qbQuestionClone.setContentFolderId(questionDTO.getContentFolderId()); qbQuestionClone.setMaxMark(Integer.valueOf(currentMark)); qbQuestionClone.setFeedback(currentFeedback); @@ -2042,9 +2043,12 @@ ArrayNode questions = JsonUtil.optArray(toolContentJSON, RestTags.QUESTIONS); for (JsonNode questionData : questions) { QbQuestion qbQuestion = new QbQuestion(); + qbQuestion.setQuestionId(qbService.generateNextQuestionId()); qbQuestion.setType(QbQuestion.TYPE_MULTIPLE_CHOICE); qbQuestion.setName(JsonUtil.optString(questionData, RestTags.QUESTION_TEXT)); qbQuestion.setMaxMark(1); + userManagementService.save(qbQuestion); + McQueContent question = new McQueContent(qbQuestion, JsonUtil.optInt(questionData, RestTags.DISPLAY_ORDER), mcq); @@ -2054,6 +2058,7 @@ qbOption.setName(JsonUtil.optString(optionData, RestTags.ANSWER_TEXT)); qbOption.setCorrect(JsonUtil.optBoolean(optionData, RestTags.CORRECT)); qbOption.setDisplayOrder(JsonUtil.optInt(optionData, RestTags.DISPLAY_ORDER)); + qbOption.setQbQuestion(qbQuestion); question.getQbQuestion().getQbOptions().add(qbOption); } saveOrUpdateMcQueContent(question); Index: lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/scratchieApplicationContext.xml =================================================================== diff -u -r3649999936753b6c474d93ba9fa603bfd0d18a2c -r9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04 --- lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/scratchieApplicationContext.xml (.../scratchieApplicationContext.xml) (revision 3649999936753b6c474d93ba9fa603bfd0d18a2c) +++ lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/scratchieApplicationContext.xml (.../scratchieApplicationContext.xml) (revision 9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04) @@ -143,6 +143,7 @@ PROPAGATION_REQUIRED,-java.lang.Exception PROPAGATION_REQUIRED,-java.lang.Exception PROPAGATION_REQUIRED,-java.lang.Exception + PROPAGATION_REQUIRED,-java.lang.Exception PROPAGATION_REQUIRED,-java.lang.Exception PROPAGATION_REQUIRED,-java.lang.Exception PROPAGATION_REQUIRED,-java.lang.Exception Index: lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/service/ScratchieServiceImpl.java =================================================================== diff -u -r3649999936753b6c474d93ba9fa603bfd0d18a2c -r9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04 --- lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/service/ScratchieServiceImpl.java (.../ScratchieServiceImpl.java) (revision 3649999936753b6c474d93ba9fa603bfd0d18a2c) +++ lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/service/ScratchieServiceImpl.java (.../ScratchieServiceImpl.java) (revision 9f4d4f19dc70ef350ebab8e6aa89cb05e1c78c04) @@ -41,12 +41,14 @@ import java.util.Set; import java.util.SortedMap; import java.util.TreeSet; +import java.util.regex.Pattern; import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.apache.poi.ss.usermodel.IndexedColors; import org.lamsfoundation.lams.confidencelevel.ConfidenceLevelDTO; +import org.lamsfoundation.lams.confidencelevel.VsaAnswerDTO; import org.lamsfoundation.lams.contentrepository.client.IToolContentHandler; import org.lamsfoundation.lams.events.IEventNotificationService; import org.lamsfoundation.lams.learningdesign.ToolActivity; @@ -84,7 +86,7 @@ import org.lamsfoundation.lams.tool.scratchie.dto.BurningQuestionItemDTO; import org.lamsfoundation.lams.tool.scratchie.dto.GroupSummary; import org.lamsfoundation.lams.tool.scratchie.dto.LeaderResultsDTO; -import org.lamsfoundation.lams.tool.scratchie.dto.QbOptionDTO; +import org.lamsfoundation.lams.tool.scratchie.dto.OptionDTO; import org.lamsfoundation.lams.tool.scratchie.dto.ReflectDTO; import org.lamsfoundation.lams.tool.scratchie.dto.ScratchieItemDTO; import org.lamsfoundation.lams.tool.scratchie.model.Scratchie; @@ -95,6 +97,7 @@ import org.lamsfoundation.lams.tool.scratchie.model.ScratchieSession; import org.lamsfoundation.lams.tool.scratchie.model.ScratchieUser; import org.lamsfoundation.lams.tool.scratchie.util.ScratchieItemComparator; +import org.lamsfoundation.lams.tool.service.ICommonScratchieService; import org.lamsfoundation.lams.tool.service.ILamsToolService; import org.lamsfoundation.lams.tool.service.IQbToolService; import org.lamsfoundation.lams.usermanagement.User; @@ -111,8 +114,8 @@ /** * @author Andrey Balan */ -public class ScratchieServiceImpl - implements IScratchieService, ToolContentManager, ToolSessionManager, ToolRestManager, IQbToolService { +public class ScratchieServiceImpl implements IScratchieService, ICommonScratchieService, ToolContentManager, + ToolSessionManager, ToolRestManager, IQbToolService { private static Logger log = Logger.getLogger(ScratchieServiceImpl.class.getName()); private static final ExcelCell[] EMPTY_ROW = new ExcelCell[4]; @@ -182,12 +185,6 @@ } @Override - public List getAuthoredItems(Long scratchieUid) { - List res = scratchieItemDao.getAuthoringItems(scratchieUid); - return res; - } - - @Override public void createUser(ScratchieUser scratchieUser) { ScratchieUser user = getUserByIDAndSession(scratchieUser.getUserId(), scratchieUser.getSession().getSessionId()); @@ -277,36 +274,32 @@ //populate Scratchie items with confidence levels for (ScratchieItem item : items) { - - //init options' confidenceLevelDtos list - for (QbOptionDTO optionDto : item.getOptionDtos()) { - LinkedList confidenceLevelDtosTemp = new LinkedList<>(); - optionDto.setConfidenceLevelDtos(confidenceLevelDtosTemp); - } - - //find according QbQuestion + //find corresponding QbQuestion for (ConfidenceLevelDTO confidenceLevelDto : confidenceLevelDtos) { if (item.getQbQuestion().getUid().equals(confidenceLevelDto.getQbQuestionUid())) { - //find according option - for (QbOptionDTO option : item.getOptionDtos()) { - if (option.getQbOption().getUid().equals(confidenceLevelDto.getQbOptionUid())) { - option.getConfidenceLevelDtos().add(confidenceLevelDto); + //find corresponding QbOption + for (OptionDTO optionDTO : item.getOptionDtos()) { + if (optionDTO.getQbOptionUid().equals(confidenceLevelDto.getQbOptionUid())) { + optionDTO.getConfidenceLevelDtos().add(confidenceLevelDto); } } - } } - } } @Override - public Set getPrecedingConfidenceLevelsActivities(Long toolContentId) { - return toolService.getPrecedingConfidenceLevelsActivities(toolContentId); + public Set getActivitiesProvidingConfidenceLevels(Long toolContentId) { + return toolService.getActivitiesProvidingConfidenceLevels(toolContentId); } @Override + public Set getActivitiesProvidingVsaAnswers(Long toolContentId) { + return toolService.getActivitiesProvidingVsaAnswers(toolContentId); + } + + @Override public ScratchieUser checkLeaderSelectToolForSessionLeader(ScratchieUser user, Long toolSessionId) { if ((user == null) || (toolSessionId == null)) { return null; @@ -423,6 +416,11 @@ } @Override + public ScratchieAnswerVisitLog getLog(Long sessionId, Long itemUid, boolean isCaseSensitive, String answer) { + return scratchieAnswerVisitDao.getLog(sessionId, itemUid, isCaseSensitive, answer); + } + + @Override public void recordItemScratched(Long sessionId, Long itemUid, Long optionUid) { QbOption option = this.getQbOptionByUid(optionUid); if (option == null) { @@ -438,25 +436,39 @@ log.setQbToolQuestion(qbToolQuestion); log.setAccessDate(new Timestamp(new Date().getTime())); scratchieAnswerVisitDao.saveObject(log); + + recalculateMarkForSession(sessionId, false); } + } - this.recalculateMarkForSession(sessionId, false); + @Override + public void recordVsaAnswer(Long sessionId, Long itemUid, boolean isCaseSensitive, String answer) { + ScratchieAnswerVisitLog log = scratchieAnswerVisitDao.getLog(sessionId, itemUid, isCaseSensitive, answer); + if (log == null) { + log = new ScratchieAnswerVisitLog(); + log.setAnswer(answer); + log.setSessionId(sessionId); + QbToolQuestion qbToolQuestion = scratchieDao.find(QbToolQuestion.class, itemUid); + log.setQbToolQuestion(qbToolQuestion); + log.setAccessDate(new Timestamp(new Date().getTime())); + scratchieAnswerVisitDao.saveObject(log); + + recalculateMarkForSession(sessionId, false); + } } @Override public void recalculateMarkForSession(Long sessionId, boolean isPropagateToGradebook) { List userLogs = scratchieAnswerVisitDao.getLogsBySession(sessionId); - ScratchieSession session = this.getScratchieSessionBySessionId(sessionId); + ScratchieSession session = getScratchieSessionBySessionId(sessionId); Scratchie scratchie = session.getScratchie(); Set items = scratchie.getScratchieItems(); String[] presetMarks = getPresetMarks(scratchie); // calculate mark int mark = 0; - if (!items.isEmpty()) { - for (ScratchieItem item : items) { - mark += getUserMarkPerItem(scratchie, item, userLogs, presetMarks); - } + for (ScratchieItem item : items) { + mark += ScratchieServiceImpl.getUserMarkPerItem(scratchie, item, userLogs, presetMarks); } // change mark for all learners in a group @@ -465,7 +477,7 @@ // propagade changes to Gradebook if (isPropagateToGradebook) { - List users = this.getUsersBySession(sessionId); + List users = getUsersBySession(sessionId); for (ScratchieUser user : users) { toolService.updateActivityMark(new Double(mark), null, user.getUserId().intValue(), user.getSession().getSessionId(), false); @@ -475,7 +487,6 @@ @Override public void recalculateUserAnswers(Scratchie scratchie, Set oldItems, Set newItems) { - // create list of modified questions List modifiedItems = new ArrayList<>(); for (ScratchieItem oldItem : oldItems) { @@ -540,7 +551,15 @@ recalculateMarkForSession(toolSessionId, true); } } + } + @Override + public void recalculateScratchieMarksForVsaQuestion(Long qbQuestionUid) { + List sessionIds = scratchieSessionDao.getSessionIdsByQbQuestion(qbQuestionUid); + // recalculate marks if it's required + for (Long sessionId : sessionIds) { + recalculateMarkForSession(sessionId, true); + } } @Override @@ -550,7 +569,6 @@ @Override public void saveBurningQuestion(Long sessionId, Long itemUid, String question) { - boolean isGeneralBurningQuestion = itemUid == null; ScratchieBurningQuestion burningQuestion = (isGeneralBurningQuestion) @@ -678,70 +696,185 @@ @Override public void getScratchesOrder(Collection items, Long sessionId) { for (ScratchieItem item : items) { + QbQuestion qbQuestion = item.getQbQuestion(); List itemLogs = scratchieAnswerVisitDao.getLogsBySessionAndItem(sessionId, item.getUid()); - for (QbOptionDTO optionDto : item.getOptionDtos()) { + for (OptionDTO optionDto : item.getOptionDtos()) { + if (QbQuestion.TYPE_MULTIPLE_CHOICE == qbQuestion.getType()) { + int attemptNumber; + ScratchieAnswerVisitLog log = scratchieAnswerVisitDao.getLog(optionDto.getQbOptionUid(), + item.getUid(), sessionId); + if (log == null) { + // -1 if there is no log + attemptNumber = -1; + } else { + // adding 1 to start from 1 + attemptNumber = itemLogs.indexOf(log) + 1; + } + optionDto.setAttemptOrder(attemptNumber); - int attemptNumber; - ScratchieAnswerVisitLog log = scratchieAnswerVisitDao.getLog(optionDto.getQbOption().getUid(), - item.getUid(), sessionId); - if (log == null) { - // -1 if there is no log - attemptNumber = -1; + //process VSA questions } else { - // adding 1 to start from 1. - attemptNumber = itemLogs.indexOf(log) + 1; + // -1 if there is no log + int attemptNumber = -1; + for (ScratchieAnswerVisitLog itemLog : itemLogs) { + if (itemLog.getQbToolQuestion().getUid().equals(item.getUid()) + && isAnswersEqual(item, itemLog.getAnswer(), optionDto.getAnswer())) { + // adding 1 to start from 1 + attemptNumber = itemLogs.indexOf(itemLog) + 1; + break; + } + } + optionDto.setAttemptOrder(attemptNumber); } - - optionDto.setAttemptOrder(attemptNumber); } } } + private boolean isAnswersEqual(ScratchieItem item, String answer1, String answer2) { + if (answer1 == null || answer2 == null) { + return false; + } + + return item.getQbQuestion().isCaseSensitive() ? answer1.equals(answer2) : answer1.equalsIgnoreCase(answer2); + } + @Override public Collection getItemsWithIndicatedScratches(Long toolSessionId) { + List userLogs = scratchieAnswerVisitDao.getLogsBySession(toolSessionId); - Scratchie scratchie = this.getScratchieBySessionId(toolSessionId); + Scratchie scratchie = getScratchieBySessionId(toolSessionId); Set items = new TreeSet<>(new ScratchieItemComparator()); items.addAll(scratchie.getScratchieItems()); - return getItemsWithIndicatedScratches(toolSessionId, items); - } + //populate Scratchie items with VSA answers + fillItemsWithVsaAnswers(items, toolSessionId, scratchie, userLogs); - @Override - public Collection getItemsWithIndicatedScratches(Long toolSessionId, - Collection items) { - List userLogs = scratchieAnswerVisitDao.getLogsBySession(toolSessionId); - + //mark scratched options for (ScratchieItem item : items) { + for (OptionDTO optionDto : item.getOptionDtos()) { - for (QbOptionDTO optionDto : item.getOptionDtos()) { - // find according log if it exists - ScratchieAnswerVisitLog log = null; - for (ScratchieAnswerVisitLog userLog : userLogs) { - if (userLog.getQbOption().getUid().equals(optionDto.getQbOption().getUid()) - && userLog.getQbToolQuestion().getUid().equals(item.getUid())) { - log = userLog; - break; + boolean isScratched = false; + if (QbQuestion.TYPE_MULTIPLE_CHOICE == item.getQbQuestion().getType()) { + for (ScratchieAnswerVisitLog userLog : userLogs) { + if (userLog.getQbToolQuestion().getUid().equals(item.getUid()) + && userLog.getQbOption().getUid().equals(optionDto.getQbOptionUid())) { + isScratched = true; + break; + } } - } - if (log == null) { - optionDto.setScratched(false); + + //process VSA question } else { - optionDto.setScratched(true); + // find according log if it exists + for (ScratchieAnswerVisitLog userLog : userLogs) { + if (userLog.getQbToolQuestion().getUid().equals(item.getUid()) + && isAnswersEqual(item, userLog.getAnswer(), optionDto.getAnswer())) { + isScratched = true; + break; + } + } } + optionDto.setScratched(isScratched); } - boolean isItemUnraveled = this.isItemUnraveled(item, userLogs); + boolean isItemUnraveled = ScratchieServiceImpl.isItemUnraveled(item, userLogs); item.setUnraveled(isItemUnraveled); } return items; } /** + * Populate Scratchie item with VSA answers (both from Assessment tool and entered by current learner) + */ + private void fillItemsWithVsaAnswers(Collection items, Long toolSessionId, Scratchie scratchie, + List userLogs) { + ScratchieUser leader = scratchieSessionDao.getSessionBySessionId(toolSessionId).getGroupLeader(); + Collection assessmentAnswers = scratchie.isAnswersFetchingEnabled() + ? toolService.getVsaAnswersFromAssessment(scratchie.getActivityUiidProvidingVsaAnswers(), + leader.getUserId().intValue(), toolSessionId) + : null; + + for (ScratchieItem item : items) { + Long itemQbQuestionUid = item.getQbQuestion().getUid(); + + //process only VSA items + if (item.getQbQuestion().getType() != QbQuestion.TYPE_VERY_SHORT_ANSWERS) { + continue; + } + + //populate Scratchie items with VSA answers, entered by learners in Assessment tool + if (scratchie.isAnswersFetchingEnabled()) { + //find corresponding QbQuestion + for (VsaAnswerDTO assessmentAnswer : assessmentAnswers) { + if (itemQbQuestionUid.equals(assessmentAnswer.getQbQuestionUid())) { + OptionDTO optionDto = new OptionDTO(); + optionDto.setAnswer(assessmentAnswer.getAnswer()); + optionDto.setCorrect(assessmentAnswer.isCorrect()); + optionDto.setUserId(assessmentAnswer.getUserId()); + optionDto.setQbQuestionUid(assessmentAnswer.getQbQuestionUid()); + if (!scratchie.isConfidenceLevelsEnabled()) { + //don't show confidence levels + for (ConfidenceLevelDTO confidenceLevel : assessmentAnswer.getConfidenceLevels()) { + confidenceLevel.setLevel(-1); + } + } + optionDto.getConfidenceLevelDtos().addAll(assessmentAnswer.getConfidenceLevels()); + + item.getOptionDtos().add(optionDto); + } + } + } + + //add answers provided by user, which didn't come from Assessment + for (ScratchieAnswerVisitLog userLog : userLogs) { + if (itemQbQuestionUid.equals(userLog.getQbToolQuestion().getQbQuestion().getUid())) { + + //try to find already existing VsaAnswerDTO for the answer + OptionDTO optionDto = null; + boolean skipAddingUserAnswerToConfidenceLevel = false; + for (OptionDTO optionDtoIter : item.getOptionDtos()) { + if (itemQbQuestionUid.equals(optionDtoIter.getQbQuestionUid()) + && isAnswersEqual(item, optionDtoIter.getAnswer(), userLog.getAnswer())) { + optionDto = optionDtoIter; + skipAddingUserAnswerToConfidenceLevel = optionDtoIter.getUserId() + .equals(leader.getUserId()); + break; + } + } + if (skipAddingUserAnswerToConfidenceLevel) { + continue; + } + + if (optionDto == null) { + optionDto = new OptionDTO(); + optionDto.setQbQuestionUid(itemQbQuestionUid); + String answer = userLog.getAnswer(); + optionDto.setAnswer(answer); + boolean isCorrect = ScratchieServiceImpl.isItemUnraveledByAnswers(item, List.of(answer)); + optionDto.setCorrect(isCorrect); + item.getOptionDtos().add(optionDto); + } + + ConfidenceLevelDTO confidenceLevelDto = new ConfidenceLevelDTO(); + confidenceLevelDto.setUserId(leader.getUserId().intValue()); + String userName = StringUtils.isBlank(leader.getFirstName()) + && StringUtils.isBlank(leader.getLastName()) ? leader.getLoginName() + : leader.getFirstName() + " " + leader.getLastName(); + confidenceLevelDto.setUserName(userName); + confidenceLevelDto.setPortraitUuid(leader.getPortraitId()); + //don't show confidence level + confidenceLevelDto.setLevel(-1); + optionDto.getConfidenceLevelDtos().add(confidenceLevelDto); + } + } + } + } + + /** * Check if the specified item was unraveled by user * * @param item @@ -750,27 +883,83 @@ * uses logs from it (The main reason to have this parameter is to reduce number of queries to DB) * @return */ - private boolean isItemUnraveled(ScratchieItem item, List userLogs) { + private static boolean isItemUnraveled(ScratchieItem item, List userLogs) { boolean isItemUnraveled = false; - for (QbOption option : item.getQbQuestion().getQbOptions()) { + if (QbQuestion.TYPE_MULTIPLE_CHOICE == item.getQbQuestion().getType()) { + for (QbOption option : item.getQbQuestion().getQbOptions()) { - ScratchieAnswerVisitLog log = null; - for (ScratchieAnswerVisitLog userLog : userLogs) { - if (userLog.getQbOption().getUid().equals(option.getUid())) { - log = userLog; - break; + ScratchieAnswerVisitLog log = null; + for (ScratchieAnswerVisitLog userLog : userLogs) { + if (userLog.getQbToolQuestion().getUid().equals(item.getUid()) + && userLog.getQbOption().getUid().equals(option.getUid())) { + log = userLog; + break; + } } + + if (log != null) { + isItemUnraveled |= option.isCorrect(); + } } - if (log != null) { - isItemUnraveled |= option.isCorrect(); + //VSA question + } else { + List userAnswers = new ArrayList<>(); + for (ScratchieAnswerVisitLog userLog : userLogs) { + if (userLog.getQbToolQuestion().getUid().equals(item.getUid()) && userLog.getAnswer() != null) { + userAnswers.add(userLog.getAnswer()); + } } + + isItemUnraveled = ScratchieServiceImpl.isItemUnraveledByAnswers(item, userAnswers); } return isItemUnraveled; } + public static boolean isItemUnraveledByAnswers(ScratchieItem item, List userAnswers) { + QbQuestion qbQuestion = item.getQbQuestion(); + + QbOption correctAnswersGroup = qbQuestion.getQbOptions().get(0).getMaxMark() == 1 + ? qbQuestion.getQbOptions().get(0) + : qbQuestion.getQbOptions().get(1); + String[] correctAnswers = correctAnswersGroup.getName().strip().split("\\r\\n"); + for (String correctAnswer : correctAnswers) { + correctAnswer = correctAnswer.strip(); + + //prepare regex which takes into account only * special character + String regexWithOnlyAsteriskSymbolActive = "\\Q"; + for (int i = 0; i < correctAnswer.length(); i++) { + //everything in between \\Q and \\E are taken literally no matter which characters it contains + if (correctAnswer.charAt(i) == '*') { + regexWithOnlyAsteriskSymbolActive += "\\E.*\\Q"; + } else { + regexWithOnlyAsteriskSymbolActive += correctAnswer.charAt(i); + } + } + regexWithOnlyAsteriskSymbolActive += "\\E"; + + //check whether answer matches regex + Pattern pattern; + if (qbQuestion.isCaseSensitive()) { + pattern = Pattern.compile(regexWithOnlyAsteriskSymbolActive); + } else { + pattern = Pattern.compile(regexWithOnlyAsteriskSymbolActive, + java.util.regex.Pattern.CASE_INSENSITIVE | java.util.regex.Pattern.UNICODE_CASE); + } + + for (String userAnswer : userAnswers) { + // check is item unraveled + if (pattern.matcher(userAnswer.strip()).matches()) { + return true; + } + } + } + + return false; + } + /** * * @param scratchie @@ -781,14 +970,13 @@ * presetMarks to reduce number of queries to DB * @return */ - private int getUserMarkPerItem(Scratchie scratchie, ScratchieItem item, List userLogs, - String[] presetMarks) { + private static int getUserMarkPerItem(Scratchie scratchie, ScratchieItem item, + List userLogs, String[] presetMarks) { int mark = 0; // add mark only if an item was unraveled - if (isItemUnraveled(item, userLogs)) { - - int itemAttempts = calculateItemAttempts(userLogs, item); + if (ScratchieServiceImpl.isItemUnraveled(item, userLogs)) { + int itemAttempts = ScratchieServiceImpl.getNumberAttemptsForItem(userLogs, item); String markStr = (itemAttempts <= presetMarks.length) ? presetMarks[itemAttempts - 1] : presetMarks[presetMarks.length - 1]; mark = Integer.parseInt(markStr); @@ -797,7 +985,6 @@ if (scratchie.isExtraPoint() && (itemAttempts == 1)) { mark++; } - } return mark; @@ -806,8 +993,7 @@ /** * Returns number of scraches user done for the specified item. */ - private int calculateItemAttempts(List userLogs, ScratchieItem item) { - + private static int getNumberAttemptsForItem(List userLogs, ScratchieItem item) { int itemAttempts = 0; for (ScratchieAnswerVisitLog userLog : userLogs) { if (userLog.getQbToolQuestion().getUid().equals(item.getUid())) { @@ -821,81 +1007,93 @@ @Override public List getQuestionSummary(Long contentId, Long itemUid) { List groupSummaryList = new ArrayList<>(); - + Scratchie scratchie = getScratchieByContentId(contentId); ScratchieItem item = scratchieItemDao.getByUid(itemUid); - List options = item.getQbQuestion().getQbOptions(); + boolean isMcqItem = item.getQbQuestion().getType() == QbQuestion.TYPE_MULTIPLE_CHOICE; + List sessionList = scratchieSessionDao.getByContentId(contentId); for (ScratchieSession session : sessionList) { Long sessionId = session.getSessionId(); - // one new summary for one session. GroupSummary groupSummary = new GroupSummary(session); - - Map optionMap = new HashMap<>(); - for (QbOption dbOption : options) { - - // clone it so it doesn't interfere with values from other sessions - QbOptionDTO optionDto = new QbOptionDTO(); - optionDto.setQbOption(dbOption); - int[] attempts = new int[options.size()]; - optionDto.setAttempts(attempts); - optionMap.put(dbOption.getUid(), optionDto); - } List sessionAttempts = scratchieAnswerVisitDao.getLogsBySessionAndItem(sessionId, itemUid); - List users = scratchieUserDao.getBySessionID(sessionId); + Map optionMap = new HashMap<>(); + if (isMcqItem) { + List options = item.getQbQuestion().getQbOptions(); + for (QbOption dbOption : options) { + // clone it so it doesn't interfere with values from other sessions + OptionDTO optionDto = new OptionDTO(dbOption); + int[] attempts = new int[options.size()]; + optionDto.setAttempts(attempts); + optionMap.put(dbOption.getUid(), optionDto); + } + } else { + item.getOptionDtos().clear(); + fillItemsWithVsaAnswers(List.of(item), sessionId, scratchie, sessionAttempts); + List optionDtos = item.getOptionDtos(); + for (OptionDTO optionDto : optionDtos) { + int[] attempts = new int[optionDtos.size()]; + optionDto.setAttempts(attempts); + optionMap.put(Long.valueOf(optionDto.getAnswerHash()), optionDto); + } + } + // calculate attempts table + List users = scratchieUserDao.getBySessionID(sessionId); for (ScratchieUser user : users) { int attemptNumber = 0; for (ScratchieAnswerVisitLog attempt : sessionAttempts) { - QbOptionDTO optionDto = optionMap.get(attempt.getQbOption().getUid()); - int[] attempts = optionDto == null ? new int[options.size()] : optionDto.getAttempts(); + Long optionUidOrAnswer = isMcqItem ? attempt.getQbOption().getUid() + : attempt.getAnswer().hashCode(); + OptionDTO optionDto = optionMap.get(optionUidOrAnswer); + int[] attempts = optionDto == null ? new int[optionMap.size()] : optionDto.getAttempts(); // +1 for corresponding choice attempts[attemptNumber++]++; } } - Collection sortedOptions = new LinkedList<>(); + Collection sortedOptions = new LinkedList<>(); sortedOptions.addAll(optionMap.values()); groupSummary.setOptionDtos(sortedOptions); groupSummaryList.add(groupSummary); } // show total groupSummary if there is more than 1 group available - if (sessionList.size() > 1) { + if (sessionList.size() > 1 && isMcqItem) { GroupSummary groupSummaryTotal = new GroupSummary(); - groupSummaryTotal.setSessionId(new Long(0)); + groupSummaryTotal.setSessionId(0L); groupSummaryTotal.setSessionName("Summary"); groupSummaryTotal.setMark(0); - Map optionMapTotal = new HashMap<>(); + Map optionMapTotal = new HashMap<>(); + List options = item.getQbQuestion().getQbOptions(); for (QbOption dbOption : options) { // clone it so it doesn't interfere with values from other sessions - QbOptionDTO optionDto = new QbOptionDTO(); - optionDto.setQbOption(dbOption); + OptionDTO optionDto = new OptionDTO(dbOption); int[] attempts = new int[options.size()]; optionDto.setAttempts(attempts); optionMapTotal.put(dbOption.getUid(), optionDto); } for (GroupSummary groupSummary : groupSummaryList) { - Collection sortedOptionDtos = groupSummary.getOptionDtos(); - for (QbOptionDTO sortedOptionDto : sortedOptionDtos) { - int[] attempts = sortedOptionDto.getAttempts(); + Collection optionDtos = groupSummary.getOptionDtos(); + for (OptionDTO optionDto : optionDtos) { + int[] attempts = optionDto.getAttempts(); - QbOptionDTO optionTotal = optionMapTotal.get(sortedOptionDto.getQbOption().getUid()); + OptionDTO optionTotal = optionMapTotal.get(optionDto.getQbOptionUid()); int[] attemptsTotal = optionTotal.getAttempts(); for (int i = 0; i < attempts.length; i++) { attemptsTotal[i] += attempts[i]; } } } - Collection sortedOptions = new TreeSet<>(); + Collection sortedOptions = new TreeSet<>(); sortedOptions.addAll(optionMapTotal.values()); groupSummaryTotal.setOptionDtos(sortedOptions); groupSummaryList.add(0, groupSummaryTotal); @@ -1148,8 +1346,8 @@ // find out the correct answer's sequential letter - A,B,C... String correctOptionLetter = ""; int optionCount = 1; - for (QbOptionDTO optionDto : item.getOptionDtos()) { - if (optionDto.getQbOption().isCorrect()) { + for (OptionDTO optionDto : item.getOptionDtos()) { + if (optionDto.isCorrect()) { correctOptionLetter = String.valueOf((char) ((optionCount + 'A') - 1)); break; } @@ -1331,6 +1529,7 @@ for (ScratchieItem item : items) { List itemSummary = getQuestionSummary(contentId, item.getUid()); + boolean isMcqItem = item.getQbQuestion().getType() == QbQuestion.TYPE_MULTIPLE_CHOICE; row = new ExcelCell[1]; row[0] = new ExcelCell( @@ -1344,30 +1543,32 @@ rowList.add(ScratchieServiceImpl.EMPTY_ROW); // show all team summary in case there is more than 1 group - if (summaryList.size() > 1) { + if (summaryList.size() > 1 && isMcqItem) { row = new ExcelCell[1]; row[0] = new ExcelCell(getMessage("label.all.teams.summary"), true); rowList.add(row); GroupSummary allTeamSummary = itemSummary.get(0); - Collection optionDtos = allTeamSummary.getOptionDtos(); + Collection optionDtos = allTeamSummary.getOptionDtos(); row = new ExcelCell[1 + optionDtos.size()]; for (int i = 0; i < optionDtos.size(); i++) { row[i + 1] = new ExcelCell((long) i + 1, IndexedColors.YELLOW); } rowList.add(row); - for (QbOptionDTO optionDto : optionDtos) { + for (OptionDTO optionDto : optionDtos) { row = new ExcelCell[1 + optionDtos.size()]; - String optionTitle = removeHtmlMarkup(optionDto.getQbOption().getName()); + String answer; IndexedColors color = null; - if (optionDto.getQbOption().isCorrect()) { - optionTitle += "(" + getMessage("label.monitoring.item.summary.correct") + ")"; + answer = removeHtmlMarkup(optionDto.getAnswer()); + if (optionDto.isCorrect()) { + answer += "(" + getMessage("label.monitoring.item.summary.correct") + ")"; color = IndexedColors.GREEN; } + columnCount = 0; - row[columnCount++] = new ExcelCell(optionTitle, color); + row[columnCount++] = new ExcelCell(answer, color); for (int numberAttempts : optionDto.getAttempts()) { row[columnCount++] = new ExcelCell(new Long(numberAttempts), false); @@ -1386,7 +1587,7 @@ continue; } - Collection optionDtos = groupSummary.getOptionDtos(); + Collection optionDtos = groupSummary.getOptionDtos(); row = new ExcelCell[1]; row[0] = new ExcelCell(groupSummary.getSessionName(), true); @@ -1398,12 +1599,14 @@ } rowList.add(row); - for (QbOptionDTO optionDto : optionDtos) { - row = new ExcelCell[1 + optionDtos.size()]; - String optionTitle = removeHtmlMarkup(optionDto.getQbOption().getName()); - if (optionDto.getQbOption().isCorrect()) { + for (OptionDTO optionDto : optionDtos) { + int rowLength = isMcqItem ? 1 + optionDtos.size() : 1 + optionDto.getAttempts().length; + row = new ExcelCell[rowLength]; + String optionTitle = removeHtmlMarkup(optionDto.getAnswer()); + if (optionDto.isCorrect()) { optionTitle += "(" + getMessage("label.monitoring.item.summary.correct") + ")"; } + columnCount = 0; row[columnCount++] = new ExcelCell(optionTitle, false); @@ -1447,6 +1650,7 @@ rowList.add(row); for (ScratchieItem item : items) { + boolean isMcqItem = item.getQbQuestion().getType() == QbQuestion.TYPE_MULTIPLE_CHOICE; row = new ExcelCell[1]; row[0] = new ExcelCell( getMessage("label.question.semicolon", new Object[] { item.getQbQuestion().getName() }), @@ -1460,7 +1664,8 @@ for (ScratchieAnswerVisitLog log : logs) { row = new ExcelCell[4]; row[0] = new ExcelCell(new Long(i++), false); - String optionDescr = removeHtmlMarkup(log.getQbOption().getName()); + String optionDescr = isMcqItem ? removeHtmlMarkup(log.getQbOption().getName()) + : log.getAnswer(); row[1] = new ExcelCell(optionDescr, false); row[3] = new ExcelCell(fullDateFormat.format(log.getAccessDate()), false); rowList.add(row); @@ -1534,10 +1739,11 @@ // correct option String correctOption = ""; - List options = itemDto.getOptionDtos(); - for (QbOptionDTO option : options) { - if (option.getQbOption().isCorrect()) { - correctOption = removeHtmlMarkup(option.getQbOption().getName()); + List options = itemDto.getOptionDtos(); + for (OptionDTO option : options) { + if (option.isCorrect()) { + correctOption = option.getAnswer(); + correctOption = removeHtmlMarkup(correctOption); } } row[columnCount++] = new ExcelCell(correctOption, false); @@ -1567,7 +1773,8 @@ } for (ScratchieAnswerVisitLog log : logs) { - String optionText = removeHtmlMarkup(log.getQbOption().getName()); + String optionText = removeHtmlMarkup( + log.getQbOption() == null ? log.getAnswer() : log.getQbOption().getName()); row[columnCount++] = new ExcelCell(optionText, false); } for (int i = logs.size(); i < itemDto.getOptionDtos().size(); i++) { @@ -1690,7 +1897,6 @@ */ private List getSummaryByTeam(Scratchie scratchie, Collection sortedItems) { List groupSummaries = new ArrayList<>(); - String[] presetMarks = getPresetMarks(scratchie); List sessionList = scratchieSessionDao.getByContentId(scratchie.getContentId()); @@ -1703,38 +1909,55 @@ ScratchieUser groupLeader = session.getGroupLeader(); List logs = scratchieAnswerVisitDao.getLogsBySession(sessionId); + //populate Scratchie items with VSA answers (both from Assessment tool and entered by current learner) + fillItemsWithVsaAnswers(sortedItems, sessionId, scratchie, logs); for (ScratchieItem item : sortedItems) { ScratchieItemDTO itemDto = new ScratchieItemDTO(); int numberOfAttempts = 0; int mark = -1; boolean isUnraveledOnFirstAttempt = false; String optionsSequence = ""; + boolean isMcqItem = item.getQbQuestion().getType() == QbQuestion.TYPE_MULTIPLE_CHOICE; // if there is no group leader don't calculate numbers - there aren't any if (groupLeader != null) { //create a list of attempts user done for the current item - List itemLogs = new ArrayList<>(); + List visitLogs = new ArrayList<>(); for (ScratchieAnswerVisitLog log : logs) { if (log.getQbToolQuestion().getUid().equals(item.getUid())) { - itemLogs.add(log); + visitLogs.add(log); } } - numberOfAttempts = itemLogs.size(); + numberOfAttempts = visitLogs.size(); // for displaying purposes if there is no attemps we assign -1 which will be shown as "-" - mark = (numberOfAttempts == 0) ? -1 : getUserMarkPerItem(scratchie, item, logs, presetMarks); + mark = (numberOfAttempts == 0) ? -1 + : ScratchieServiceImpl.getUserMarkPerItem(scratchie, item, logs, presetMarks); - isUnraveledOnFirstAttempt = (numberOfAttempts == 1) && isItemUnraveled(item, logs); + isUnraveledOnFirstAttempt = (numberOfAttempts == 1) + && ScratchieServiceImpl.isItemUnraveled(item, logs); // find out options' sequential letters - A,B,C... - for (ScratchieAnswerVisitLog itemAttempt : itemLogs) { - String sequencialLetter = ScratchieServiceImpl.getSequencialLetter(item, - itemAttempt.getQbOption()); + for (ScratchieAnswerVisitLog itemAttempt : visitLogs) { + String sequencialLetter = ""; + + int optionCount = 1; + for (OptionDTO optionDto : item.getOptionDtos()) { + boolean isOptionMet = isMcqItem + && optionDto.getQbOptionUid().equals(itemAttempt.getQbOption().getUid()) + || !isMcqItem + && isAnswersEqual(item, optionDto.getAnswer(), itemAttempt.getAnswer()); + if (isOptionMet) { + sequencialLetter = String.valueOf((char) ((optionCount + 'A') - 1)); + break; + } + optionCount++; + } + optionsSequence += optionsSequence.isEmpty() ? sequencialLetter : ", " + sequencialLetter; } - } itemDto.setUid(item.getUid()); @@ -1758,12 +1981,11 @@ /** * Return specified option's sequential letter (e.g. A,B,C) among other possible options */ - private static String getSequencialLetter(ScratchieItem item, QbOption asnwer) { + private static String getSequencialLetter(ScratchieItem item, QbOption qbOption) { String sequencialLetter = ""; - int optionCount = 1; - for (QbOptionDTO option : item.getOptionDtos()) { - if (option.getQbOption().getUid().equals(asnwer.getUid())) { + for (OptionDTO optionDto : item.getOptionDtos()) { + if (optionDto.getQbOptionUid() != null && optionDto.getQbOptionUid().equals(qbOption.getUid())) { sequencialLetter = String.valueOf((char) ((optionCount + 'A') - 1)); break; } @@ -2299,6 +2521,8 @@ scratchie.setShowScrachiesInResults(JsonUtil.optBoolean(toolContentJSON, "showScrachiesInResults", true)); scratchie.setConfidenceLevelsActivityUiid( JsonUtil.optInt(toolContentJSON, RestTags.CONFIDENCE_LEVELS_ACTIVITY_UIID)); + scratchie.setActivityUiidProvidingVsaAnswers( + JsonUtil.optInt(toolContentJSON, "activityUiidProvidingVsaAnswers")); // Scratchie Items Set newItems = new LinkedHashSet<>(); @@ -2309,9 +2533,15 @@ ScratchieItem item = new ScratchieItem(); item.setDisplayOrder(JsonUtil.optInt(questionData, RestTags.DISPLAY_ORDER)); - item.getQbQuestion().setName(JsonUtil.optString(questionData, RestTags.QUESTION_TITLE)); - item.getQbQuestion().setDescription(JsonUtil.optString(questionData, RestTags.QUESTION_TEXT)); + QbQuestion qbQuestion = new QbQuestion(); + + qbQuestion.setType(QbQuestion.TYPE_MULTIPLE_CHOICE); + qbQuestion.setQuestionId(qbService.generateNextQuestionId()); + qbQuestion.setName(JsonUtil.optString(questionData, RestTags.QUESTION_TITLE)); + qbQuestion.setDescription(JsonUtil.optString(questionData, RestTags.QUESTION_TEXT)); item.setToolContentId(scratchie.getContentId()); + scratchieDao.insert(qbQuestion); + item.setQbQuestion(qbQuestion); newItems.add(item); // set options