Index: lams_tool_assessment/.classpath
===================================================================
diff -u -r4148b35337096058f50c22fa950f64aa77294a4f -r998ba383ec2a06647d309f910ebefe0a33fa30a4
--- lams_tool_assessment/.classpath (.../.classpath) (revision 4148b35337096058f50c22fa950f64aa77294a4f)
+++ lams_tool_assessment/.classpath (.../.classpath) (revision 998ba383ec2a06647d309f910ebefe0a33fa30a4)
@@ -17,7 +17,8 @@
-
+
+
Index: lams_tool_assessment/build.xml
===================================================================
diff -u -r7475d08afc280b5e2e5ddf04e8bf35e3166aaf80 -r998ba383ec2a06647d309f910ebefe0a33fa30a4
--- lams_tool_assessment/build.xml (.../build.xml) (revision 7475d08afc280b5e2e5ddf04e8bf35e3166aaf80)
+++ lams_tool_assessment/build.xml (.../build.xml) (revision 998ba383ec2a06647d309f910ebefe0a33fa30a4)
@@ -3,5 +3,23 @@
-
+
+
+
+
+
+ ${ant.project.name}: Copying additional Java classes to WAR
+
+
\ No newline at end of file
Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/AssessmentConstants.java
===================================================================
diff -u -rb9300513239d652c59e3bfd190d0973295844f37 -r998ba383ec2a06647d309f910ebefe0a33fa30a4
--- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/AssessmentConstants.java (.../AssessmentConstants.java) (revision b9300513239d652c59e3bfd190d0973295844f37)
+++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/AssessmentConstants.java (.../AssessmentConstants.java) (revision 998ba383ec2a06647d309f910ebefe0a33fa30a4)
@@ -109,8 +109,6 @@
public static final String ATTR_IS_TIME_LIMIT_NOT_LAUNCHED = "isTimeLimitNotLaunched";
- public static final String ATTR_SECONDS_LEFT = "secondsLeft";
-
public static final String ATTR_OVERALL_FEEDBACK_LIST = "overallFeedbackList";
public static final String ATTR_OVERALL_FEEDBACK_COUNT = "overallFeedbackCount";
Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/dao/AssessmentUserDAO.java
===================================================================
diff -u -r7475d08afc280b5e2e5ddf04e8bf35e3166aaf80 -r998ba383ec2a06647d309f910ebefe0a33fa30a4
--- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/dao/AssessmentUserDAO.java (.../AssessmentUserDAO.java) (revision 7475d08afc280b5e2e5ddf04e8bf35e3166aaf80)
+++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/dao/AssessmentUserDAO.java (.../AssessmentUserDAO.java) (revision 998ba383ec2a06647d309f910ebefe0a33fa30a4)
@@ -34,26 +34,28 @@
AssessmentUser getUserByUserIDAndSessionID(Long userID, Long sessionId);
AssessmentUser getUserCreatedAssessment(Long userId, Long contentId);
-
+
AssessmentUser getUserByIdAndContent(Long userId, Long contentId);
+ AssessmentUser getUserByLoginAndContent(String login, Long contentId);
+
List getBySessionID(Long sessionId);
List getPagedUsersBySession(Long sessionId, int page, int size, String sortBy, String sortOrder,
String searchString, IUserManagementService userManagementService);
int getCountUsersBySession(Long sessionId, String searchString);
-
+
int getCountUsersByContentId(Long contentId);
List getPagedUsersBySessionAndQuestion(Long sessionId, Long questionUid, int page, int size,
String sortBy, String sortOrder, String searchString, IUserManagementService userManagementService);
-
+
List getRawUserMarksBySession(Long sessionId);
-
+
Object[] getStatsMarksBySession(Long sessionId);
-
+
List getRawLeaderMarksByToolContentId(Long toolContentId);
-
+
Object[] getStatsMarksForLeaders(Long toolContentId);
}
Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/dao/hibernate/AssessmentUserDAOHibernate.java
===================================================================
diff -u -r0e7d403e91b0916fd3842d8d3098b1c466d28ece -r998ba383ec2a06647d309f910ebefe0a33fa30a4
--- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/dao/hibernate/AssessmentUserDAOHibernate.java (.../AssessmentUserDAOHibernate.java) (revision 0e7d403e91b0916fd3842d8d3098b1c466d28ece)
+++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/dao/hibernate/AssessmentUserDAOHibernate.java (.../AssessmentUserDAOHibernate.java) (revision 998ba383ec2a06647d309f910ebefe0a33fa30a4)
@@ -24,7 +24,9 @@
package org.lamsfoundation.lams.tool.assessment.dao.hibernate;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import org.hibernate.query.NativeQuery;
import org.hibernate.query.Query;
@@ -96,6 +98,15 @@
}
@Override
+ public AssessmentUser getUserByLoginAndContent(String login, Long contentId) {
+ Map properties = new HashMap<>();
+ properties.put("loginName", login);
+ properties.put("session.assessment.contentId", contentId);
+ List users = findByProperties(AssessmentUser.class, properties);
+ return users.isEmpty() ? null : users.get(0);
+ }
+
+ @Override
@SuppressWarnings("unchecked")
public List getBySessionID(Long sessionId) {
return this.doFind(FIND_BY_SESSION_ID, sessionId);
Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/AssessmentServiceImpl.java
===================================================================
diff -u -rf280ea4699aa04587b63c0fef3e2a02b7d847c0d -r998ba383ec2a06647d309f910ebefe0a33fa30a4
--- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/AssessmentServiceImpl.java (.../AssessmentServiceImpl.java) (revision f280ea4699aa04587b63c0fef3e2a02b7d847c0d)
+++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/AssessmentServiceImpl.java (.../AssessmentServiceImpl.java) (revision 998ba383ec2a06647d309f910ebefe0a33fa30a4)
@@ -26,7 +26,6 @@
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.sql.Timestamp;
-import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
@@ -110,6 +109,7 @@
import org.lamsfoundation.lams.tool.assessment.util.AssessmentEscapeUtils;
import org.lamsfoundation.lams.tool.assessment.util.AssessmentSessionComparator;
import org.lamsfoundation.lams.tool.assessment.util.SequencableComparator;
+import org.lamsfoundation.lams.tool.assessment.web.controller.LearningWebsocketServer;
import org.lamsfoundation.lams.tool.exception.DataMissingException;
import org.lamsfoundation.lams.tool.exception.ToolException;
import org.lamsfoundation.lams.tool.service.ICommonAssessmentService;
@@ -306,47 +306,21 @@
}
@Override
- public void launchTimeLimit(Long assessmentUid, Long userId) {
+ public LocalDateTime launchTimeLimit(Long assessmentUid, Long userId) {
AssessmentResult lastResult = getLastAssessmentResult(assessmentUid, userId);
- lastResult.setTimeLimitLaunchedDate(LocalDateTime.now());
+ LocalDateTime launchedDate = LocalDateTime.now();
+ lastResult.setTimeLimitLaunchedDate(launchedDate);
assessmentResultDao.saveObject(lastResult);
+ return launchedDate;
}
@Override
- public long getSecondsLeft(Assessment assessment, AssessmentUser user) {
- AssessmentResult lastResult = getLastAssessmentResult(assessment.getUid(), user.getUserId());
-
- long secondsLeft = 1;
- if (assessment.getTimeLimit() != 0) {
- // if user has pressed OK button already - calculate remaining time, and full time otherwise
- boolean isTimeLimitNotLaunched = (lastResult == null) || (lastResult.getTimeLimitLaunchedDate() == null);
-
- secondsLeft = isTimeLimitNotLaunched ? assessment.getTimeLimit() * 60
- : assessment.getTimeLimit() * 60
- - Duration.between(lastResult.getTimeLimitLaunchedDate(), LocalDateTime.now()).toSeconds();
-
- // change negative or zero number to 1
- secondsLeft = Math.max(1, secondsLeft);
- }
-
- return secondsLeft;
+ public boolean checkTimeLimitExceeded(long assessmentUid, long userUid) {
+ Long secondsLeft = LearningWebsocketServer.getSecondsLeft(assessmentUid, userUid);
+ return secondsLeft != null && secondsLeft.equals(0);
}
@Override
- public boolean checkTimeLimitExceeded(Assessment assessment, AssessmentUser groupLeader) {
- int timeLimit = assessment.getTimeLimit();
- if (timeLimit == 0) {
- return false;
- }
-
- AssessmentResult lastLeaderResult = getLastAssessmentResult(assessment.getUid(), groupLeader.getUserId());
-
- //check if the time limit is exceeded
- return (lastLeaderResult != null) && (lastLeaderResult.getTimeLimitLaunchedDate() != null)
- && lastLeaderResult.getTimeLimitLaunchedDate().plusSeconds(timeLimit).isBefore(LocalDateTime.now());
- }
-
- @Override
public List getUsersBySession(Long toolSessionID) {
return assessmentUserDao.getBySessionID(toolSessionID);
}
@@ -429,6 +403,11 @@
}
@Override
+ public AssessmentUser getUserByLoginAndContent(String login, Long contentId) {
+ return assessmentUserDao.getUserByLoginAndContent(login, contentId);
+ }
+
+ @Override
public AssessmentUser getUserByIDAndSession(Long userId, Long sessionId) {
return assessmentUserDao.getUserByUserIDAndSessionID(userId, sessionId);
}
Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/IAssessmentService.java
===================================================================
diff -u -r36e9121497b2c963250d22ec0f660fd66934182e -r998ba383ec2a06647d309f910ebefe0a33fa30a4
--- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/IAssessmentService.java (.../IAssessmentService.java) (revision 36e9121497b2c963250d22ec0f660fd66934182e)
+++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/IAssessmentService.java (.../IAssessmentService.java) (revision 998ba383ec2a06647d309f910ebefe0a33fa30a4)
@@ -24,6 +24,7 @@
package org.lamsfoundation.lams.tool.assessment.service;
import java.lang.reflect.InvocationTargetException;
+import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -91,24 +92,14 @@
* @param assessmentUid
* @param userId
*/
- void launchTimeLimit(Long assessmentUid, Long userId);
+ LocalDateTime launchTimeLimit(Long assessmentUid, Long userId);
/**
- * Calculates how many seconds left till the time limit will expire. If it's expired already - returns 1 in order to
- * show learning.jsp and autosubmit results.
- *
* @param assessment
- * @param user
- * @return
- */
- long getSecondsLeft(Assessment assessment, AssessmentUser user);
-
- /**
- * @param assessment
* @param groupLeader
* @return whether the time limit is exceeded already
*/
- boolean checkTimeLimitExceeded(Assessment assessment, AssessmentUser groupLeader);
+ boolean checkTimeLimitExceeded(long assessmentUid, long userUid);
/**
* Get users by given toolSessionID.
@@ -168,6 +159,8 @@
*/
AssessmentUser getUserByIdAndContent(Long userID, Long contentId);
+ AssessmentUser getUserByLoginAndContent(String login, Long contentId);
+
/**
* Get user by sessionID and UserID
*
Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/LearningController.java
===================================================================
diff -u -rf88d199227dcdea28740a0783adcf0c726eed463 -r998ba383ec2a06647d309f910ebefe0a33fa30a4
--- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/LearningController.java (.../LearningController.java) (revision f88d199227dcdea28740a0783adcf0c726eed463)
+++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/LearningController.java (.../LearningController.java) (revision 998ba383ec2a06647d309f910ebefe0a33fa30a4)
@@ -188,7 +188,7 @@
}
//if the time is up and leader hasn't submitted response - show waitForLeaderFinish page
- boolean isTimeLimitExceeded = service.checkTimeLimitExceeded(assessment, groupLeader);
+ boolean isTimeLimitExceeded = service.checkTimeLimitExceeded(assessment.getUid(), groupLeader.getUid());
if (isTimeLimitExceeded) {
request.setAttribute(AssessmentConstants.PARAM_WAITING_MESSAGE_KEY,
"label.waiting.for.leader.finish");
@@ -296,9 +296,6 @@
sessionMap.put(AssessmentConstants.ATTR_REFLECTION_ENTRY, entryText);
//time limit
- boolean isTimeLimitEnabled = hasEditRight && !showResults && assessment.getTimeLimit() != 0;
- long secondsLeft = isTimeLimitEnabled ? service.getSecondsLeft(assessment, user) : 0;
- sessionMap.put(AssessmentConstants.ATTR_SECONDS_LEFT, secondsLeft);
boolean isTimeLimitNotLaunched = (lastResult == null) || (lastResult.getTimeLimitLaunchedDate() == null);
sessionMap.put(AssessmentConstants.ATTR_IS_TIME_LIMIT_NOT_LAUNCHED, isTimeLimitNotLaunched);
@@ -433,7 +430,7 @@
AssessmentUser leader = session.getGroupLeader();
//in case of time limit - prevent user from seeing questions page longer than time limit allows
- boolean isTimeLimitExceeded = service.checkTimeLimitExceeded(session.getAssessment(), leader);
+ boolean isTimeLimitExceeded = service.checkTimeLimitExceeded(session.getAssessment().getUid(), leader.getUid());
boolean isLeaderResponseFinalized = service.isLastAttemptFinishedByUser(leader);
ObjectNode responseJSON = JsonNodeFactory.instance.objectNode();
@@ -469,8 +466,6 @@
throws ServletException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
String sessionMapID = WebUtil.readStrParam(request, AssessmentConstants.ATTR_SESSION_MAP_ID);
SessionMap sessionMap = getSessionMap(request);
- Assessment assessment = (Assessment) sessionMap.get(AssessmentConstants.ATTR_ASSESSMENT);
- AssessmentUser user = (AssessmentUser) sessionMap.get(AssessmentConstants.ATTR_USER);
int oldPageNumber = (Integer) sessionMap.get(AssessmentConstants.ATTR_PAGE_NUMBER);
//if AnswersValidationFailed - get pageNumber as request parameter and as method parameter otherwise
@@ -502,9 +497,6 @@
// store results from sessionMap into DB
storeUserAnswersIntoDatabase(sessionMap, true);
- long secondsLeft = service.getSecondsLeft(assessment, user);
- sessionMap.put(AssessmentConstants.ATTR_SECONDS_LEFT, secondsLeft);
-
// use redirect to prevent form resubmission
String redirectURL = "redirect:/pages/learning/learning.jsp";
redirectURL = WebUtil.appendParameterToURL(redirectURL, AssessmentConstants.ATTR_SESSION_MAP_ID,
@@ -516,29 +508,6 @@
}
/**
- * Ajax call to get the remaining seconds. Needed when the page is reloaded in the browser to check with the server
- * what the current values should be! Otherwise the learner can keep hitting reload after a page change or submit
- * all (when questions are spread across pages) and increase their time!
- *
- * @return
- * @throws JSONException
- * @throws IOException
- */
- @RequestMapping("/getSecondsLeft")
- @ResponseBody
- public String getSecondsLeft(HttpServletRequest request, HttpServletResponse response) throws ServletException,
- IllegalAccessException, InvocationTargetException, NoSuchMethodException, IOException {
- SessionMap sessionMap = getSessionMap(request);
- Assessment assessment = (Assessment) sessionMap.get(AssessmentConstants.ATTR_ASSESSMENT);
- AssessmentUser user = (AssessmentUser) sessionMap.get(AssessmentConstants.ATTR_USER);
- long secondsLeft = service.getSecondsLeft(assessment, user);
- ObjectNode responseJSON = JsonNodeFactory.instance.objectNode();
- responseJSON.put(AssessmentConstants.ATTR_SECONDS_LEFT, secondsLeft);
- response.setContentType("application/json;charset=utf-8");
- return responseJSON.toString();
- }
-
- /**
* Handling submittion of MarkHedging type of Questions (in case of leader aware tool)
*/
@SuppressWarnings("unchecked")
@@ -674,7 +643,6 @@
// time limit feature
sessionMap.put(AssessmentConstants.ATTR_IS_TIME_LIMIT_NOT_LAUNCHED, true);
- sessionMap.put(AssessmentConstants.ATTR_SECONDS_LEFT, assessment.getTimeLimit() * 60);
return "pages/learning/learning";
}
Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/LearningWebsocketServer.java
===================================================================
diff -u
--- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/LearningWebsocketServer.java (revision 0)
+++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/LearningWebsocketServer.java (revision 998ba383ec2a06647d309f910ebefe0a33fa30a4)
@@ -0,0 +1,238 @@
+package org.lamsfoundation.lams.tool.assessment.web.controller;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import javax.websocket.CloseReason;
+import javax.websocket.CloseReason.CloseCodes;
+import javax.websocket.OnClose;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.apache.log4j.Logger;
+import org.lamsfoundation.lams.tool.assessment.AssessmentConstants;
+import org.lamsfoundation.lams.tool.assessment.model.Assessment;
+import org.lamsfoundation.lams.tool.assessment.model.AssessmentResult;
+import org.lamsfoundation.lams.tool.assessment.model.AssessmentUser;
+import org.lamsfoundation.lams.tool.assessment.service.IAssessmentService;
+import org.lamsfoundation.lams.util.hibernate.HibernateSessionManager;
+import org.lamsfoundation.lams.web.session.SessionManager;
+import org.lamsfoundation.lams.web.util.AttributeNames;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.context.support.WebApplicationContextUtils;
+
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+/**
+ * Controls Assessment time limits
+ *
+ * @author Marcin Cieslak
+ */
+@ServerEndpoint("/learningWebsocket")
+public class LearningWebsocketServer {
+
+ private static class TimeCache {
+ private int relativeTimeLimit;
+ private final Map timeLimitLaunchedDate = new ConcurrentHashMap<>();
+ }
+
+ /**
+ * A singleton which updates Learners with their time limit
+ */
+ private static final Runnable sendWorker = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // websocket communication bypasses standard HTTP filters, so Hibernate session needs to be initialised manually
+ HibernateSessionManager.openSession();
+
+ Iterator>> entryIterator = LearningWebsocketServer.websockets.entrySet()
+ .iterator();
+ // go through activities and update registered learners with time if needed
+ while (entryIterator.hasNext()) {
+ Entry> entry = entryIterator.next();
+ Long toolContentId = entry.getKey();
+ // if all learners left the activity, remove the obsolete mapping
+ Set websockets = entry.getValue();
+ if (websockets.isEmpty()) {
+ entryIterator.remove();
+ timeCaches.remove(toolContentId);
+ continue;
+ }
+
+ Assessment assessment = LearningWebsocketServer.getAssessmentService()
+ .getAssessmentByContentId(toolContentId);
+ long assessmentUid = assessment.getUid();
+ TimeCache timeCache = timeCaches.get(toolContentId);
+ if (timeCache == null) {
+ timeCache = new TimeCache();
+ timeCaches.put(toolContentId, timeCache);
+ }
+
+ boolean updateAllUsers = false;
+ int existingRelativeTimeLimit = assessment.getTimeLimit() * 60;
+ if (timeCache.relativeTimeLimit != existingRelativeTimeLimit) {
+ timeCache.relativeTimeLimit = existingRelativeTimeLimit;
+ updateAllUsers = true;
+ }
+
+ for (Session websocket : entry.getValue()) {
+ String login = websocket.getUserPrincipal().getName();
+ AssessmentUser user = LearningWebsocketServer.getAssessmentService()
+ .getUserByLoginAndContent(login, toolContentId);
+ long userId = user.getUserId();
+ boolean updateUser = updateAllUsers;
+
+ if (timeCache.relativeTimeLimit > 0) {
+ AssessmentResult result = LearningWebsocketServer.getAssessmentService()
+ .getLastAssessmentResult(assessmentUid, userId);
+ LocalDateTime existingLaunchDate = result == null ? null
+ : result.getTimeLimitLaunchedDate();
+ if (existingLaunchDate == null) {
+ continue;
+ }
+
+ LocalDateTime launchedDate = timeCache.timeLimitLaunchedDate.get(userId);
+ if (launchedDate == null || !launchedDate.equals(existingLaunchDate)) {
+ updateUser = true;
+ timeCache.timeLimitLaunchedDate.put(userId, existingLaunchDate);
+ }
+ }
+
+ if (updateUser) {
+ Long secondsLeft = LearningWebsocketServer.getSecondsLeft(timeCache, userId);
+ LearningWebsocketServer.sendUpdate(websocket, secondsLeft);
+ }
+ }
+ }
+ } catch (IllegalStateException e) {
+ // do nothing as server is probably shutting down and we could not obtain Hibernate session
+ } catch (Exception e) {
+ // error caught, but carry on
+ LearningWebsocketServer.log.error("Error in Assessment worker thread", e);
+ } finally {
+ HibernateSessionManager.closeSession();
+ }
+ }
+ };
+
+ // how ofter the thread runs in seconds
+ private static final long CHECK_INTERVAL = 3;
+
+ private static final Logger log = Logger.getLogger(LearningWebsocketServer.class);
+
+ private static final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
+ private static final Map> websockets = new ConcurrentHashMap<>();
+ private static final Map timeCaches = new ConcurrentHashMap<>();
+
+ private static IAssessmentService assessmentService;
+
+ static {
+ // run the singleton thread
+ executor.scheduleAtFixedRate(sendWorker, 0, CHECK_INTERVAL, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Registers the Learner for processing.
+ */
+ @OnOpen
+ public void registerUser(Session websocket) throws IOException {
+ Long toolContentID = Long
+ .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_TOOL_CONTENT_ID).get(0));
+ String login = websocket.getUserPrincipal().getName();
+ AssessmentUser user = LearningWebsocketServer.getAssessmentService().getUserByLoginAndContent(login,
+ toolContentID);
+ if (user == null) {
+ throw new SecurityException("User \"" + login
+ + "\" is not a participant in Assessment activity with tool content ID " + toolContentID);
+ }
+
+ Set toolContentWebsockets = websockets.get(toolContentID);
+ if (toolContentWebsockets == null) {
+ toolContentWebsockets = ConcurrentHashMap.newKeySet();
+ websockets.put(toolContentID, toolContentWebsockets);
+ }
+ toolContentWebsockets.add(websocket);
+
+ TimeCache timeCache = timeCaches.get(toolContentID);
+ if (timeCache != null) {
+ timeCache.timeLimitLaunchedDate.remove(user.getUserId());
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("User " + login + " entered Assessment with toolContentId: " + toolContentID);
+ }
+ }
+
+ /**
+ * When user leaves the activity.
+ */
+ @OnClose
+ public void unregisterUser(Session websocket, CloseReason reason) {
+ Long toolContentID = Long
+ .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_TOOL_CONTENT_ID).get(0));
+ websockets.get(toolContentID).remove(websocket);
+
+ if (log.isDebugEnabled()) {
+ // If there was something wrong with the connection, put it into logs.
+ log.debug("User " + websocket.getUserPrincipal().getName() + " left Assessment with Tool Content ID: "
+ + toolContentID
+ + (!(reason.getCloseCode().equals(CloseCodes.GOING_AWAY)
+ || reason.getCloseCode().equals(CloseCodes.NORMAL_CLOSURE))
+ ? ". Abnormal close. Code: " + reason.getCloseCode() + ". Reason: "
+ + reason.getReasonPhrase()
+ : ""));
+ }
+ }
+
+ public static Long getSecondsLeft(long assessmentUid, long userUid) {
+ TimeCache timeCache = timeCaches.get(assessmentUid);
+ return timeCache == null ? null : LearningWebsocketServer.getSecondsLeft(timeCache, userUid);
+ }
+
+ private static Long getSecondsLeft(TimeCache timeCache, long userUid) {
+ if (timeCache.relativeTimeLimit == 0) {
+ return null;
+ }
+
+ LocalDateTime now = LocalDateTime.now();
+ LocalDateTime finish = timeCache.timeLimitLaunchedDate.get(userUid).plusSeconds(timeCache.relativeTimeLimit);
+ long secondsLeft = Duration.between(now, finish).toSeconds();
+
+ return Math.max(0, secondsLeft);
+ }
+
+ private static void sendUpdate(Session websocket, Long secondsLeft) throws IOException {
+ ObjectNode responseJSON = JsonNodeFactory.instance.objectNode();
+ if (secondsLeft == null) {
+ responseJSON.put("clearTimer", true);
+ } else {
+ responseJSON.put("secondsLeft", secondsLeft);
+ }
+ String response = responseJSON.toString();
+
+ if (websocket.isOpen()) {
+ websocket.getBasicRemote().sendText(response);
+ }
+ }
+
+ private static IAssessmentService getAssessmentService() {
+ if (assessmentService == null) {
+ WebApplicationContext wac = WebApplicationContextUtils
+ .getRequiredWebApplicationContext(SessionManager.getServletContext());
+ assessmentService = (IAssessmentService) wac.getBean(AssessmentConstants.ASSESSMENT_SERVICE);
+ }
+ return assessmentService;
+ }
+}
\ No newline at end of file
Index: lams_tool_assessment/web/pages/learning/learning.jsp
===================================================================
diff -u -rb9300513239d652c59e3bfd190d0973295844f37 -r998ba383ec2a06647d309f910ebefe0a33fa30a4
--- lams_tool_assessment/web/pages/learning/learning.jsp (.../learning.jsp) (revision b9300513239d652c59e3bfd190d0973295844f37)
+++ lams_tool_assessment/web/pages/learning/learning.jsp (.../learning.jsp) (revision 998ba383ec2a06647d309f910ebefe0a33fa30a4)
@@ -125,39 +125,81 @@
//timelimit feature
- $(document).ready(function(){
- //show timelimit-start-dialog in order to start countdown
- if (${sessionMap.isTimeLimitNotLaunched}) {
-
- $.blockUI({
- message: $('#timelimit-start-dialog'),
- css: { width: '325px', height: '120px'},
- overlayCSS: { opacity: '.98'}
- });
-
- //once OK button pressed start countdown
- $('#timelimit-start-ok').click(function() {
-
- //store date when user has started activity with time limit
- $.ajax({
- async: true,
- url: '',
- data: 'sessionMapID=${sessionMapID}',
- type: 'post'
- });
-
- $.unblockUI();
- displayCountdown();
- isWaitingForConfirmation = false;
- });
-
- } else {
- displayCountdown();
+ // websocket needs pinging and reconnection feature in case it fails
+ // it works pretty much the same as command websocket in Page.tag
+ var assessmentTimeLimitWebsocketInitTime = null,
+ assessmentTimeLimitWebsocket = null,
+ assessmentTimeLimitWebsocketPingTimeout = null,
+ assessmentTimeLimitWebsocketPingFunc = null,
+ assessmentTimeLimitWebsocketReconnectAttempts = 0,
+ counterInitialised = false;
+
+ assessmentTimeLimitWebsocketPingFunc = function(skipPing){
+ if (assessmentTimeLimitWebsocket.readyState == assessmentTimeLimitWebsocket.CLOSING
+ || assessmentTimeLimitWebsocket.readyState == assessmentTimeLimitWebsocket.CLOSED){
+ return;
}
- });
+
+ // check and ping every 3 minutes
+ assessmentTimeLimitWebsocketPingTimeout = setTimeout(assessmentTimeLimitWebsocketPingFunc, 3*60*1000);
+ // initial set up does not send ping
+ if (!skipPing) {
+ assessmentTimeLimitWebsocket.send("ping");
+ }
+ };
+
+ function initAssessmentTimeLimitWebsocket(){
+ assessmentTimeLimitWebsocketInitTime = Date.now();
+ assessmentTimeLimitWebsocket = new WebSocket(''.replace('http', 'ws')
+ + 'learningWebsocket?toolContentID=' + ${sessionMap.assessment.contentId});
+
+ assessmentTimeLimitWebsocket.onclose = function(e){
+ // check reason and whether the close did not happen immediately after websocket creation
+ // (possible access denied, user logged out?)
+ if (e.code === 1006 &&
+ Date.now() - assessmentTimeLimitWebsocketInitTime > 1000 &&
+ assessmentTimeLimitWebsocketReconnectAttempts < 20) {
+ assessmentTimeLimitWebsocketReconnectAttempts++;
+ // maybe iPad went into sleep mode?
+ // we need this websocket working, so init it again after delay
+ setTimeout(initAssessmentTimeLimitWebsocket, 3000);
+ }
+ };
+
+ // set up timer for the first time
+ assessmentTimeLimitWebsocketPingFunc(true);
+
+ // when the server pushes new inputs
+ assessmentTimeLimitWebsocket.onmessage = function(e){
+ // read JSON object
+ var input = JSON.parse(e.data);
+
+ if (input.clearTimer == true) {
+ // teacher has stopped the timer, destroy it
+ $('#countdown').countdown('destroy').remove();
+ counterInitialised = false;
+ } else {
+ // teacher has updated the timer
+ var secondsLeft = +input.secondsLeft;
+ if (counterInitialised) {
+ // just set the new time
+ $('#countdown').countdown('option', 'until', secondsLeft + 'S');
+ } else {
+ // initialise the timer
+ displayCountdown(secondsLeft);
+ }
+ }
+
+ // reset ping timer
+ clearTimeout(assessmentTimeLimitWebsocketPingTimeout);
+ assessmentTimeLimitWebsocketPingFunc(true);
+ };
+ }
- function displayCountdown(){
- var countdown = ''
+ function displayCountdown(secondsLeft){
+ counterIntialised = true;
+ var countdown = '';
+
$.blockUI({
message: countdown,
showOverlay: false,
@@ -174,9 +216,10 @@
});
$('#countdown').countdown({
- until: '+${secondsLeft}S',
+ until: '+' + secondsLeft +'S',
format: 'hMS',
compact: true,
+ alwaysExpire : true,
onTick: function(periods) {
//check for 30 seconds
if ((periods[4] == 0) && (periods[5] == 0) && (periods[6] <= 30)) {
@@ -192,18 +235,39 @@
},
description: "
"
});
+ }
- <%-- double check if we have the correct number of seconds left in case user has clicked refresh --%>
- $.ajax({
- url: '',
- data: 'sessionMapID=${sessionMapID}',
- dataType: 'json',
- type: 'post',
- success: function (json) {
- $('#countdown').countdown('option', 'until', json.secondsLeft+'S');
- }
- });
- }
+
+ $(document).ready(function(){
+ //show timelimit-start-dialog in order to start countdown
+ if (${sessionMap.isTimeLimitNotLaunched}) {
+
+ $.blockUI({
+ message: $('#timelimit-start-dialog'),
+ css: { width: '325px', height: '120px'},
+ overlayCSS: { opacity: '.98'}
+ });
+
+ //once OK button pressed start countdown
+ $('#timelimit-start-ok').click(function() {
+
+ //store date when user has started activity with time limit
+ $.ajax({
+ async: true,
+ url: '',
+ data: 'sessionMapID=${sessionMapID}',
+ type: 'post'
+ });
+
+ $.unblockUI();
+ initAssessmentTimeLimitWebsocket();
+ isWaitingForConfirmation = false;
+ });
+
+ } else {
+ initAssessmentTimeLimitWebsocket();
+ }
+ });
//autosave feature