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