Index: lams_central/conf/language/lams/ApplicationResources.properties =================================================================== diff -u -r48340d4d314a3487c1ac43f91415cf57955b555e -rf9cae5b21c66ce71a10d3e930fe8e40e265d2536 --- lams_central/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 48340d4d314a3487c1ac43f91415cf57955b555e) +++ lams_central/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision f9cae5b21c66ce71a10d3e930fe8e40e265d2536) @@ -1120,3 +1120,7 @@ label.qb.collection.remove.questions.tooltip = Remove all selected questions from the collection. If they are present only in this collection, they will be permanently removed. label.qb.collection.remove.questions.fail = Questions can only be removed/deleted from the collection if they have not been used in an assessment activity. If the question is already in used, it will not be deleted. label.authoring.short.answer.hint = In each option box type answers in separate lines. +label.vsa.allocate.button = Allocate VSAs +label.vsa.allocate.description = Allocate students' answers by dragging and dropping them to correct options +label.vsa.deallocate.button.tip = Click to move answer back to queue +label.vsa.deallocate.confirm = Are you sure you want to mark this answer as not correct? Students' scores will be recalculated. Index: lams_common/src/java/org/lamsfoundation/lams/qb/dao/hibernate/QbDAO.java =================================================================== diff -u -r6f0f844e82f555e8dc0df6916226fac35a2a7c36 -rf9cae5b21c66ce71a10d3e930fe8e40e265d2536 --- lams_common/src/java/org/lamsfoundation/lams/qb/dao/hibernate/QbDAO.java (.../QbDAO.java) (revision 6f0f844e82f555e8dc0df6916226fac35a2a7c36) +++ lams_common/src/java/org/lamsfoundation/lams/qb/dao/hibernate/QbDAO.java (.../QbDAO.java) (revision f9cae5b21c66ce71a10d3e930fe8e40e265d2536) @@ -260,66 +260,59 @@ private List getPagedQuestions(String questionTypes, String collectionUids, int page, int size, String sortBy, String sortOrder, String searchString, boolean onlyUidsRequested) { - String RETURN_VALUE = onlyUidsRequested ? "question.uid" : "question.*"; - //we sort of strip out HTML tags from the search by using REGEXP_REPLACE which skips all the content between < > - final String SELECT_QUESTIONS = "SELECT DISTINCT " + RETURN_VALUE + " FROM lams_qb_question question " - + " LEFT OUTER JOIN lams_qb_option qboption ON qboption.qb_question_uid = question.uid " - + " LEFT OUTER JOIN lams_qb_collection_question collection ON question.question_id = collection.qb_question_id " - + " LEFT JOIN ("//help finding questions with the max available version - + " SELECT biggerQuestion.* FROM lams_qb_question biggerQuestion " - + " LEFT OUTER JOIN lams_qb_collection_question collection ON biggerQuestion.question_id = collection.qb_question_id " - + " LEFT OUTER JOIN lams_qb_option qboption1 " - + " ON qboption1.qb_question_uid = biggerQuestion.uid WHERE " - + (questionTypes == null ? "" : " biggerQuestion.type in (:questionTypes) AND ") - + (collectionUids == null ? "" : " collection.collection_uid in (:collectionUids) AND ") - + " (REGEXP_REPLACE(biggerQuestion.description, '<[^>]*>+', '') LIKE CONCAT('%', :searchString, '%')" - + " OR biggerQuestion.name LIKE CONCAT('%', :searchString, '%') " - + " OR REGEXP_REPLACE(qboption1.name, '<[^>]*>+', '') LIKE CONCAT('%', :searchString, '%')) " - + ") AS biggerQuestion ON question.question_id = biggerQuestion.question_id AND question.version < biggerQuestion.version " - + " WHERE biggerQuestion.version is NULL " - + (questionTypes == null ? "" : " AND question.type in (:questionTypes) ") - + (collectionUids == null ? "" : " AND collection.collection_uid in (:collectionUids) ") - + " AND (REGEXP_REPLACE(question.description, '<[^>]*>+', '') LIKE CONCAT('%', :searchString, '%')" - + " OR question.name LIKE CONCAT('%', :searchString, '%') " - + " OR REGEXP_REPLACE(qboption.name, '<[^>]*>+', '') LIKE CONCAT('%', :searchString, '%')) "; - final String ORDER_BY_NAME = "ORDER BY question.name, question.description "; - final String ORDER_BY_SMTH_ELSE = "ORDER BY question.question_id "; + StringBuilder queryBuilder = new StringBuilder("SELECT DISTINCT ").append(onlyUidsRequested ? "q.uid" : "q.*") + .append(" FROM (SELECT question.* FROM lams_qb_question question"); + if (searchString != null) { + queryBuilder.append(" LEFT JOIN lams_qb_option qboption ON qboption.qb_question_uid = question.uid"); + } + if (collectionUids != null) { + queryBuilder.append( + " LEFT JOIN lams_qb_collection_question collection ON question.question_id = collection.qb_question_id"); + } - //TODO check the following query with real data. and see maybe it's better than the current (it's unlikely though) [https://stackoverflow.com/a/28090544/10331386 and https://stackoverflow.com/a/612268/10331386] -// SELECT t1.* -// FROM lams_qb_question t1 -// INNER JOIN -// ( -// SELECT `question_id`, MAX(version) AS max_version -// FROM lams_qb_question as t3 -// LEFT OUTER JOIN lams_qb_option qboption -// ON qboption.qb_question_uid = t3.uid -// WHERE REGEXP_REPLACE(qboption.name, '<[^>]*>+', '') LIKE CONCAT('%', :searchString, '%') -// GROUP BY `question_id` -// ) t2 -// ON t1.`question_id` = t2.`question_id` AND t1.version = t2.max_version; + queryBuilder.append(" WHERE"); + if (questionTypes != null) { + queryBuilder.append(" question.type in (:questionTypes) AND"); + } + if (collectionUids != null) { + queryBuilder.append(" collection.collection_uid in (:collectionUids) AND"); + } + if (searchString == null) { + // there has to be something after AND or even after just WHERE + queryBuilder.append(" TRUE"); + } else { + // we sort of strip out HTML tags from the search by using REGEXP_REPLACE which skips all the content between < > + queryBuilder.append( + " (REGEXP_REPLACE(question.description, '<[^>]*>+', '') LIKE CONCAT('%', :searchString, '%')") + .append(" OR question.name LIKE CONCAT('%', :searchString, '%')") + .append(" OR REGEXP_REPLACE(qboption.name, '<[^>]*>+', '') LIKE CONCAT('%', :searchString, '%'))"); + } - StringBuilder bldr = new StringBuilder(SELECT_QUESTIONS); + queryBuilder.append(" ORDER BY question.version DESC) AS q GROUP BY q.question_id"); + if ("smth_else".equalsIgnoreCase(sortBy)) { - bldr.append(ORDER_BY_SMTH_ELSE); + queryBuilder.append(" ORDER BY q.question_id "); } else { - bldr.append(ORDER_BY_NAME); + queryBuilder.append(" ORDER BY q.name, q.description "); } LAMSBaseDAO.sanitiseQueryPart(sortOrder); - bldr.append(sortOrder); + queryBuilder.append(sortOrder); - NativeQuery query = getSession().createNativeQuery(bldr.toString()); + NativeQuery query = getSession().createNativeQuery(queryBuilder.toString()); if (questionTypes != null) { query.setParameterList("questionTypes", questionTypes.split(",")); } if (collectionUids != null) { query.setParameterList("collectionUids", collectionUids.split(",")); } - // support for custom search from a toolbar - searchString = searchString == null ? "" : searchString; - query.setParameter("searchString", searchString); + if (searchString != null) { + // support for custom search from the toolbar + searchString = searchString == null ? "" : searchString; + query.setParameter("searchString", searchString); + } + query.setFirstResult(page * size); query.setMaxResults(size); if (!onlyUidsRequested) { @@ -332,29 +325,37 @@ @Override public int getCountQuestions(String questionTypes, String collectionUids, String searchString) { - final String SELECT_QUESTIONS = "SELECT COUNT(DISTINCT question.uid) count " - + " FROM lams_qb_question question " - + " LEFT OUTER JOIN lams_qb_collection_question collection ON question.question_id = collection.qb_question_id " - + " LEFT OUTER JOIN lams_qb_option qboption " + " ON qboption.qb_question_uid = question.uid " - + " LEFT JOIN ("//help finding questions with the max available version - + " SELECT biggerQuestion.* FROM lams_qb_question biggerQuestion " - + " LEFT OUTER JOIN lams_qb_collection_question collection ON biggerQuestion.question_id = collection.qb_question_id " - + " LEFT OUTER JOIN lams_qb_option qboption1 " - + " ON qboption1.qb_question_uid = biggerQuestion.uid " - + (questionTypes == null ? "" : " WHERE biggerQuestion.type in (:questionTypes) ") - + (collectionUids == null ? "" : " AND collection.collection_uid in (:collectionUids) ") - + " AND (REGEXP_REPLACE(biggerQuestion.description, '<[^>]*>+', '') LIKE CONCAT('%', :searchString, '%')" - + " OR biggerQuestion.name LIKE CONCAT('%', :searchString, '%') " - + " OR REGEXP_REPLACE(qboption1.name, '<[^>]*>+', '') LIKE CONCAT('%', :searchString, '%')) " - + ") AS biggerQuestion ON question.question_id = biggerQuestion.question_id AND question.version < biggerQuestion.version " - + " WHERE biggerQuestion.version is NULL " - + (questionTypes == null ? "" : " AND question.type in (:questionTypes) ") - + (collectionUids == null ? "" : " AND collection.collection_uid in (:collectionUids) ") - + " AND (REGEXP_REPLACE(question.description, '<[^>]*>+', '') LIKE CONCAT('%', :searchString, '%')" - + " OR question.name LIKE CONCAT('%', :searchString, '%') " - + " OR REGEXP_REPLACE(qboption.name, '<[^>]*>+', '') LIKE CONCAT('%', :searchString, '%')) "; + StringBuilder queryBuilder = new StringBuilder("SELECT COUNT(DISTINCT c.uid) AS cnt FROM (SELECT q.uid FROM ") + .append(" (SELECT question.uid, question.question_id FROM lams_qb_question question"); + if (searchString != null) { + queryBuilder.append(" LEFT JOIN lams_qb_option qboption ON qboption.qb_question_uid = question.uid"); + } + if (collectionUids != null) { + queryBuilder.append( + " LEFT JOIN lams_qb_collection_question collection ON question.question_id = collection.qb_question_id"); + } - NativeQuery query = getSession().createNativeQuery(SELECT_QUESTIONS).addScalar("count", + queryBuilder.append(" WHERE"); + if (questionTypes != null) { + queryBuilder.append(" question.type in (:questionTypes) AND"); + } + if (collectionUids != null) { + queryBuilder.append(" collection.collection_uid in (:collectionUids) AND"); + } + if (searchString == null) { + // there has to be something after AND or even after just WHERE + queryBuilder.append(" TRUE"); + } else { + // we sort of strip out HTML tags from the search by using REGEXP_REPLACE which skips all the content between < > + queryBuilder.append( + " (REGEXP_REPLACE(question.description, '<[^>]*>+', '') LIKE CONCAT('%', :searchString, '%')") + .append(" OR question.name LIKE CONCAT('%', :searchString, '%')") + .append(" OR REGEXP_REPLACE(qboption.name, '<[^>]*>+', '') LIKE CONCAT('%', :searchString, '%'))"); + } + + queryBuilder.append(") AS q GROUP BY q.question_id) AS c"); + + NativeQuery query = getSession().createNativeQuery(queryBuilder.toString()).addScalar("cnt", IntegerType.INSTANCE); if (questionTypes != null) { query.setParameterList("questionTypes", questionTypes.split(",")); @@ -363,8 +364,11 @@ query.setParameterList("collectionUids", collectionUids.split(",")); } // support for custom search from a toolbar - searchString = searchString == null ? "" : searchString; - query.setParameter("searchString", searchString); + if (searchString != null) { + // support for custom search from a toolbar + searchString = searchString == null ? "" : searchString; + query.setParameter("searchString", searchString); + } int result = (int) query.getSingleResult(); return result; } Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/MonitoringController.java =================================================================== diff -u -r53b1f5c6dcfb9e0d74e56c9647da69f07b889a55 -rf9cae5b21c66ce71a10d3e930fe8e40e265d2536 --- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/MonitoringController.java (.../MonitoringController.java) (revision 53b1f5c6dcfb9e0d74e56c9647da69f07b889a55) +++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/MonitoringController.java (.../MonitoringController.java) (revision f9cae5b21c66ce71a10d3e930fe8e40e265d2536) @@ -310,65 +310,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); Index: lams_tool_assessment/web/pages/monitoring/parts/questionsummary.jsp =================================================================== diff -u -re581d656fb9eae279adb11f28b8419fd49cd05c2 -rf9cae5b21c66ce71a10d3e930fe8e40e265d2536 --- lams_tool_assessment/web/pages/monitoring/parts/questionsummary.jsp (.../questionsummary.jsp) (revision e581d656fb9eae279adb11f28b8419fd49cd05c2) +++ lams_tool_assessment/web/pages/monitoring/parts/questionsummary.jsp (.../questionsummary.jsp) (revision f9cae5b21c66ce71a10d3e930fe8e40e265d2536) @@ -318,13 +318,15 @@ -
- -
-
-
-
-
+ +
+ +
+
+
+
+
+
Index: lams_tool_assessment/web/pages/monitoring/summary.jsp =================================================================== diff -u -ra30927f25a782dce1955df338c5fb59e455b00ac -rf9cae5b21c66ce71a10d3e930fe8e40e265d2536 --- lams_tool_assessment/web/pages/monitoring/summary.jsp (.../summary.jsp) (revision a30927f25a782dce1955df338c5fb59e455b00ac) +++ lams_tool_assessment/web/pages/monitoring/summary.jsp (.../summary.jsp) (revision f9cae5b21c66ce71a10d3e930fe8e40e265d2536) @@ -3,14 +3,20 @@ + +<%@ include file="parts/discloseAnswers.jsp"%> +