Index: lams_common/src/java/org/lamsfoundation/lams/dbupdates/patch20190110.sql =================================================================== diff -u --- lams_common/src/java/org/lamsfoundation/lams/dbupdates/patch20190110.sql (revision 0) +++ lams_common/src/java/org/lamsfoundation/lams/dbupdates/patch20190110.sql (revision cd39ade0293bfdcdb89176284da7138266107689) @@ -0,0 +1,41 @@ +-- Turn off autocommit, so nothing is committed if there is an error +SET AUTOCOMMIT = 0; +SET FOREIGN_KEY_CHECKS=0; +----------------------Put all sql statements below here------------------------- + +-- LDEV-4746 Create a Question Bank question structure +CREATE TABLE lams_qb_question (`uid` BIGINT AUTO_INCREMENT, + `local` TINYINT(1) NOT NULL DEFAULT 1, + `type` TINYINT NOT NULL, + `question_id` INT NOT NULL, + `version` SMALLINT NOT NULL DEFAULT 1, + `name` TEXT, + `mark` INT, + `feedback` TEXT, + PRIMARY KEY (uid), + CONSTRAINT UQ_question_version UNIQUE KEY (question_id, version)); + +-- Convert MCQ question into Question Bank question +-- This part of patch should be in MCQ, but it must be run after lams_qb_question is created and it must not run in parallel with other tools' patches, +-- so the safest solution is to place it here + +-- fill Question Bank table with unique questions, with manually incremented question ID +SET @question_id = (SELECT IF(MAX(question_id) IS NULL, 0, MAX(question_id)) FROM lams_qb_question); +SET @start_question_id = @question_id; +INSERT INTO lams_qb_question SELECT NULL, 1, 1, @question_id:=@question_id + 1, 1, question, mark, feedback FROM (SELECT DISTINCT TRIM(question) AS question, mark, IF(TRIM(feedback) = '', NULL, TRIM(feedback)) AS feedback FROM tl_lamc11_que_content) AS mcq; + +-- prepare MCQ's question table for referencing Question Bank's question +ALTER TABLE tl_lamc11_que_content ADD COLUMN qb_question_uid BIGINT AFTER uid, ADD CONSTRAINT FK_qb_question FOREIGN KEY (qb_question_uid) REFERENCES lams_qb_question (uid) ON UPDATE CASCADE; + +-- find matching questions in Question Bank and set up references +UPDATE tl_lamc11_que_content AS mcq, lams_qb_question AS qb SET mcq.qb_question_uid = qb.uid WHERE TRIM(mcq.question) = qb.name AND mcq.mark = qb.mark AND (TRIM(mcq.feedback) = qb.feedback OR (IF(TRIM(mcq.feedback) = '', NULL, TRIM(mcq.feedback)) IS NULL AND qb.feedback IS NULL)) AND qb.question_id > @start_question_id; + +-- remove columns from MCQ which are duplicated in Question Bank +ALTER TABLE tl_lamc11_que_content DROP COLUMN question, DROP COLUMN mark, DROP COLUMN feedback; + +----------------------Put all sql statements above here------------------------- + +-- If there were no errors, commit and restore autocommit to on +COMMIT; +SET AUTOCOMMIT = 1; +SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file Index: lams_common/src/java/org/lamsfoundation/lams/qb/QbQuestion.java =================================================================== diff -u -rd9b3433f41c2b25011809144d241d33237d9b9d8 -rcd39ade0293bfdcdb89176284da7138266107689 --- lams_common/src/java/org/lamsfoundation/lams/qb/QbQuestion.java (.../QbQuestion.java) (revision d9b3433f41c2b25011809144d241d33237d9b9d8) +++ lams_common/src/java/org/lamsfoundation/lams/qb/QbQuestion.java (.../QbQuestion.java) (revision cd39ade0293bfdcdb89176284da7138266107689) @@ -141,7 +141,7 @@ } public void setName(String name) { - this.name = name == null ? null : name.trim(); + this.name = StringUtils.isBlank(name) ? null : name.trim(); } public Integer getMark() { @@ -157,6 +157,6 @@ } public void setFeedback(String feedback) { - this.feedback = feedback == null ? null : feedback.trim(); + this.feedback = StringUtils.isBlank(feedback) ? null : feedback.trim(); } } \ No newline at end of file Index: lams_tool_lamc/src/java/org/lamsfoundation/lams/tool/mc/service/McService.java =================================================================== diff -u -r3bb7e0141ae1cc15ccd737c95d90b5762a34ad61 -rcd39ade0293bfdcdb89176284da7138266107689 --- lams_tool_lamc/src/java/org/lamsfoundation/lams/tool/mc/service/McService.java (.../McService.java) (revision 3bb7e0141ae1cc15ccd737c95d90b5762a34ad61) +++ lams_tool_lamc/src/java/org/lamsfoundation/lams/tool/mc/service/McService.java (.../McService.java) (revision cd39ade0293bfdcdb89176284da7138266107689) @@ -60,6 +60,7 @@ import org.lamsfoundation.lams.notebook.model.NotebookEntry; import org.lamsfoundation.lams.notebook.service.CoreNotebookConstants; import org.lamsfoundation.lams.notebook.service.ICoreNotebookService; +import org.lamsfoundation.lams.qb.QbQuestion; import org.lamsfoundation.lams.rest.RestTags; import org.lamsfoundation.lams.rest.ToolRestManager; import org.lamsfoundation.lams.tool.Tool; @@ -248,6 +249,8 @@ String newHash = mcQueContent.getQuestion() == null ? null : HashUtil.sha1(mcQueContent.getQuestion()); mcQueContent.setQuestionHash(newHash); + mcQueContentDAO.insertOrUpdate(mcQueContent.getQbQuestion()); + mcQueContentDAO.saveOrUpdateMcQueContent(mcQueContent); } @@ -273,29 +276,49 @@ McQueContent question = getQuestionByUid(questionDTO.getUid()); + // see if question already exists in QB + QbQuestion qbQuestion = (QbQuestion) mcQueContentDAO.find(QbQuestion.class, questionDTO.getQbQuestionUid()); + if (qbQuestion == null) { + // if it does not exist, create a new one + qbQuestion = new QbQuestion(); + qbQuestion.setType(QbQuestion.TYPE_MULTIPLE_CHOICE_SINGLE_ANSWER); + } else { + // if it exists, detach it so we do not change data on the existing entity, + // otherwise it gets saved on transaction end with modified data + mcQueContentDAO.releaseFromCache(qbQuestion); + } + // set question's data to current values + qbQuestion.setName(currentQuestionText); + qbQuestion.setMark(Integer.valueOf(currentMark)); + qbQuestion.setFeedback(currentFeedback); + // if it is a new question, it is always considered modified + // if it is an existing question, it gets checked whether data changed + if (qbQuestion.isModified()) { + // the cloned entity will be attached to session, unlike the previously detached qbQuestion + qbQuestion = qbQuestion.clone(); + // if vital data changed, create a new question instead of modifying existing one + qbQuestion.setQuestionId(mcQueContentDAO.getMaxQbQuestionId()); + qbQuestion.setVersion(1); + } + // in case question doesn't exist if (question == null) { - question = new McQueContent(currentQuestionText, null, new Integer(displayOrder), - new Integer(currentMark), currentFeedback, content, null); + question = new McQueContent(qbQuestion, null, new Integer(displayOrder), content, null); // adding a new question to content content.getMcQueContents().add(question); question.setMcContent(content); // in case question exists already } else { - - question.setQuestion(currentQuestionText); - question.setFeedback(currentFeedback); question.setDisplayOrder(new Integer(displayOrder)); - question.setMark(new Integer(currentMark)); + question.setQbQuestion(qbQuestion); } // persist candidate answers List optionDTOs = questionDTO.getOptionDtos(); Set oldOptions = question.getMcOptionsContents(); - Set newOptions = new HashSet(); - Set wantedUids = new HashSet(); + Set newOptions = new HashSet<>(); int displayOrderOption = 1; for (McOptionDTO optionDTO : optionDTOs) { @@ -315,12 +338,12 @@ option.setCorrectOption(isCorrectOption); option.setMcQueOptionText(optionText); option.setMcQueContent(question); - + newOptions.add(option); displayOrderOption++; } question.setMcOptionsContents(newOptions); - + // updating the existing question content saveOrUpdateMcQueContent(question); @@ -330,7 +353,7 @@ @Override public void releaseQuestionsFromCache(McContent content) { - for (McQueContent question : (Set) content.getMcQueContents()) { + for (McQueContent question : content.getMcQueContents()) { mcQueContentDAO.releaseQuestionFromCache(question); } } @@ -468,19 +491,19 @@ @Override public List getAnswersFromDatabase(McContent mcContent, McQueUsr user) { - List answerDtos = new LinkedList(); + List answerDtos = new LinkedList<>(); List questions = this.getQuestionsByContentUid(mcContent.getUid()); for (McQueContent question : questions) { AnswerDTO answerDto = new AnswerDTO(); Set optionSet = question.getMcOptionsContents(); - List optionList = new LinkedList(optionSet); + List optionList = new LinkedList<>(optionSet); boolean randomize = mcContent.isRandomize(); if (randomize) { - ArrayList shuffledList = new ArrayList(optionList); + ArrayList shuffledList = new ArrayList<>(optionList); Collections.shuffle(shuffledList); - optionList = new LinkedList(shuffledList); + optionList = new LinkedList<>(shuffledList); } answerDto.setQuestion(question.getQuestion()); @@ -519,8 +542,8 @@ @Override public List buildGroupsMarkData(McContent mcContent, boolean isFullAttemptDetailsRequired) { - List listMonitoredMarksContainerDTO = new LinkedList(); - Set sessions = new TreeSet(new McSessionComparator()); + List listMonitoredMarksContainerDTO = new LinkedList<>(); + Set sessions = new TreeSet<>(new McSessionComparator()); sessions.addAll(mcContent.getMcSessions()); int numQuestions = mcContent.getMcQueContents().size(); @@ -583,8 +606,7 @@ // find out the answered option's sequential letter - A,B,C... String answeredOptionLetter = ""; int optionCount = 1; - for (McOptsContent option : (Set) attempt.getMcQueContent() - .getMcOptionsContents()) { + for (McOptsContent option : attempt.getMcQueContent().getMcOptionsContents()) { if (attempt.getMcOptionsContent().getUid().equals(option.getUid())) { answeredOptionLetter = String.valueOf((char) ((optionCount + 'A') - 1)); break; @@ -739,7 +761,7 @@ int oldTotalMark = mcUsrAttemptDAO.getUserTotalMark(userUid); int totalMark = (oldMark == null) ? oldTotalMark + newMark : (oldTotalMark - oldMark) + newMark; - List userAttempts = new ArrayList(); + List userAttempts = new ArrayList<>(); McQueUsr groupLeader = mcSession.getGroupLeader(); if (groupLeader != null) { if (groupLeader.equals(selectedUser)) { @@ -755,7 +777,7 @@ logger.warn(error); } } else { - userAttempts = new ArrayList(); + userAttempts = new ArrayList<>(); userAttempts.add(userAttemptToUpdate); } @@ -789,9 +811,9 @@ List questionDTOs, List deletedQuestions) { // create list of modified questions - List modifiedQuestions = new ArrayList(); + List modifiedQuestions = new ArrayList<>(); // create list of modified question marks - List modifiedQuestionsMarksOnly = new ArrayList(); + List modifiedQuestionsMarksOnly = new ArrayList<>(); for (McQueContent oldQuestion : oldQuestions) { for (McQuestionDTO questionDTO : questionDTOs) { if (oldQuestion.getUid().equals(questionDTO.getUid())) { @@ -928,7 +950,7 @@ } int totalNumberOfUsers = 0; - for (McSession session : (Set) mcContent.getMcSessions()) { + for (McSession session : mcContent.getMcSessions()) { totalNumberOfUsers += session.getMcQueUsers().size(); } @@ -974,7 +996,7 @@ rowCount++; int totalPercentage = 0; - for (McOptsContent option : (Set) question.getMcOptionsContents()) { + for (McOptsContent option : question.getMcOptionsContents()) { int optionAttemptCount = mcUsrAttemptDAO.getAttemptsCountPerOption(option.getUid()); cell = row.createCell(count++); int percentage = (optionAttemptCount * 100) / totalNumberOfUsers; @@ -1020,15 +1042,15 @@ row = sheet.createRow(rowCount++); count = 1; - ArrayList correctAnswers = new ArrayList(); + ArrayList correctAnswers = new ArrayList<>(); cell = row.createCell(count++); cell.setCellValue(messageService.getMessage("label.correct.answer")); for (McQueContent question : questions) { // find out the correct answer's sequential letter - A,B,C... String correctAnswerLetter = ""; int answerCount = 1; - for (McOptsContent option : (Set) question.getMcOptionsContents()) { + for (McOptsContent option : question.getMcOptionsContents()) { if (option.isCorrectOption()) { correctAnswerLetter = String.valueOf((char) ((answerCount + 'A') - 1)); break; @@ -1047,7 +1069,7 @@ cell = row.createCell(count++); cell.setCellValue(messageService.getMessage("label.learner")); - ArrayList totalPercentList = new ArrayList(); + ArrayList totalPercentList = new ArrayList<>(); int[] numberOfCorrectAnswersPerQuestion = new int[questions.size()]; for (McSessionMarkDTO sessionMarkDTO : sessionMarkDTOs) { Map usersMarksMap = sessionMarkDTO.getUserMarks(); @@ -1257,7 +1279,7 @@ return; } - for (McSession session : (Set) mcContent.getMcSessions()) { + for (McSession session : mcContent.getMcSessions()) { List entries = coreNotebookService.getEntry(session.getMcSessionId(), CoreNotebookConstants.NOTEBOOK_TOOL, McAppConstants.TOOL_SIGNATURE); for (NotebookEntry entry : entries) { @@ -1288,7 +1310,7 @@ McContent content = mcContentDAO.findMcContentById(toolContentId); if (content != null) { - for (McSession session : (Set) content.getMcSessions()) { + for (McSession session : content.getMcSessions()) { McQueUsr user = mcUserDAO.getMcUserBySession(userId.longValue(), session.getUid()); if (user != null) { mcUsrAttemptDAO.removeAllUserAttempts(user.getUid()); @@ -1379,7 +1401,7 @@ @Override public boolean isReadOnly(Long toolContentId) { McContent content = mcContentDAO.findMcContentById(toolContentId); - for (McSession session : (Set) content.getMcSessions()) { + for (McSession session : content.getMcSessions()) { if (!session.getMcQueUsers().isEmpty()) { return true; } @@ -1521,7 +1543,7 @@ @Override public List getConfidenceLevels(Long toolSessionId) { - List confidenceLevelDtos = new ArrayList(); + List confidenceLevelDtos = new ArrayList<>(); if (toolSessionId == null) { return confidenceLevelDtos; } @@ -1614,7 +1636,7 @@ public void auditLogStartEditingActivityInMonitor(long toolContentID) { toolService.auditLogStartEditingActivityInMonitor(toolContentID); } - + @Override public boolean isLastActivity(Long toolSessionId) { return toolService.isLastActivity(toolSessionId); @@ -1704,9 +1726,9 @@ @Override public List getReflectionList(McContent mcContent, Long userID) { - List reflectionsContainerDTO = new LinkedList(); - for (McSession mcSession : (Set) mcContent.getMcSessions()) { - for (McQueUsr user : (Set) mcSession.getMcQueUsers()) { + List reflectionsContainerDTO = new LinkedList<>(); + for (McSession mcSession : mcContent.getMcSessions()) { + for (McQueUsr user : mcSession.getMcQueUsers()) { // if all users mode or single user mode and found right user if (userID == null || user.getQueUsrId().equals(userID)) { NotebookEntry notebookEntry = this.getEntry(mcSession.getMcSessionId(), @@ -1847,11 +1869,11 @@ @SuppressWarnings("unchecked") @Override public List getSessionDtos(Long contentId, boolean includeStatistics) { - List sessionDtos = new ArrayList(); + List sessionDtos = new ArrayList<>(); McContent mcContent = getMcContent(contentId); if (mcContent != null) { - Set sessions = new TreeSet(new McSessionComparator()); + Set sessions = new TreeSet<>(new McSessionComparator()); sessions.addAll(mcContent.getMcSessions()); for (McSession session : sessions) { SessionDTO sessionDto = new SessionDTO(); @@ -1939,8 +1961,12 @@ // Questions ArrayNode questions = JsonUtil.optArray(toolContentJSON, RestTags.QUESTIONS); for (JsonNode questionData : questions) { - McQueContent question = new McQueContent(JsonUtil.optString(questionData, RestTags.QUESTION_TEXT), null, - JsonUtil.optInt(questionData, RestTags.DISPLAY_ORDER), 1, "", mcq, new HashSet()); + QbQuestion qbQuestion = new QbQuestion(); + qbQuestion.setType(QbQuestion.TYPE_MULTIPLE_CHOICE_SINGLE_ANSWER); + qbQuestion.setName(JsonUtil.optString(questionData, RestTags.QUESTION_TEXT)); + qbQuestion.setMark(1); + McQueContent question = new McQueContent(qbQuestion, null, + JsonUtil.optInt(questionData, RestTags.DISPLAY_ORDER), mcq, new HashSet()); ArrayNode optionsData = JsonUtil.optArray(questionData, RestTags.ANSWERS); for (JsonNode optionData : optionsData) {