Index: lams_tool_scratchie/conf/hibernate/mappings/org/lamsfoundation/lams/tool/scratchie/model/ScratchieSession.hbm.xml =================================================================== diff -u -r65efb0abb529cafc1977e284e2c9a4ed33722334 -rbeb150b1b726c122ccc5af1ba95083105f9948b8 --- lams_tool_scratchie/conf/hibernate/mappings/org/lamsfoundation/lams/tool/scratchie/model/ScratchieSession.hbm.xml (.../ScratchieSession.hbm.xml) (revision 65efb0abb529cafc1977e284e2c9a4ed33722334) +++ lams_tool_scratchie/conf/hibernate/mappings/org/lamsfoundation/lams/tool/scratchie/model/ScratchieSession.hbm.xml (.../ScratchieSession.hbm.xml) (revision beb150b1b726c122ccc5af1ba95083105f9948b8) @@ -33,6 +33,14 @@ insert="true" column="session_end_date" /> + + + @@ -137,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_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/service/IScratchieService.java =================================================================== diff -u -rae63df3e06f618712e1eab064a05015eb45c2f17 -rbeb150b1b726c122ccc5af1ba95083105f9948b8 --- lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/service/IScratchieService.java (.../IScratchieService.java) (revision ae63df3e06f618712e1eab064a05015eb45c2f17) +++ lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/service/IScratchieService.java (.../IScratchieService.java) (revision beb150b1b726c122ccc5af1ba95083105f9948b8) @@ -43,6 +43,7 @@ import org.lamsfoundation.lams.tool.scratchie.model.ScratchieSession; import org.lamsfoundation.lams.tool.scratchie.model.ScratchieUser; import org.lamsfoundation.lams.util.ExcelCell; +import org.quartz.SchedulerException; /** * Interface that defines the contract that all ShareScratchie service provider must follow. @@ -66,6 +67,14 @@ * @param toolSessionId */ ScratchieUser checkLeaderSelectToolForSessionLeader(ScratchieUser user, Long toolSessionId); + + /** + * Stores date when user has started activity with time limit. + * + * @param sessionId + * @throws SchedulerException + */ + void launchTimeLimit(Long sessionId) throws SchedulerException; List getBurningQuestionsBySession(Long sessionId); Index: lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/service/ScratchieServiceImpl.java =================================================================== diff -u -rae63df3e06f618712e1eab064a05015eb45c2f17 -rbeb150b1b726c122ccc5af1ba95083105f9948b8 --- lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/service/ScratchieServiceImpl.java (.../ScratchieServiceImpl.java) (revision ae63df3e06f618712e1eab064a05015eb45c2f17) +++ lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/service/ScratchieServiceImpl.java (.../ScratchieServiceImpl.java) (revision beb150b1b726c122ccc5af1ba95083105f9948b8) @@ -28,8 +28,10 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; import java.util.Collection; import java.util.Date; +import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; @@ -39,6 +41,7 @@ import java.util.Map; import java.util.Set; import java.util.SortedMap; +import java.util.TimeZone; import java.util.TreeSet; import org.apache.commons.lang.StringEscapeUtils; @@ -87,6 +90,7 @@ import org.lamsfoundation.lams.tool.scratchie.model.ScratchieItem; import org.lamsfoundation.lams.tool.scratchie.model.ScratchieSession; import org.lamsfoundation.lams.tool.scratchie.model.ScratchieUser; +import org.lamsfoundation.lams.tool.scratchie.util.FinishScratchingJob; import org.lamsfoundation.lams.tool.scratchie.util.ScratchieAnswerComparator; import org.lamsfoundation.lams.tool.scratchie.util.ScratchieItemComparator; import org.lamsfoundation.lams.tool.scratchie.util.ScratchieToolContentHandler; @@ -99,6 +103,12 @@ import org.lamsfoundation.lams.util.JsonUtil; import org.lamsfoundation.lams.util.MessageService; import org.lamsfoundation.lams.util.audit.IAuditService; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; /** * @author Andrey Balan @@ -149,6 +159,8 @@ private IEventNotificationService eventNotificationService; private ScratchieOutputFactory scratchieOutputFactory; + + private Scheduler scheduler; // ******************************************************************************* // Service method @@ -256,6 +268,39 @@ } return leader; } + + @Override + public void launchTimeLimit(Long sessionId) throws SchedulerException { + ScratchieSession session = getScratchieSessionBySessionId(sessionId); + int timeLimit = session.getScratchie().getTimeLimit(); + if (timeLimit == 0) { + return; + } + + //store timeLimitLaunchedDate into DB + Date timeLimitLaunchedDate = new Date(); + session.setTimeLimitLaunchedDate(timeLimitLaunchedDate); + scratchieSessionDao.saveObject(session); + + //calculate timeLimit finish date + Calendar timeLimitFinishDate = new GregorianCalendar(TimeZone.getDefault()); + timeLimitFinishDate.setTime(timeLimitLaunchedDate); + timeLimitFinishDate.add(Calendar.MINUTE, timeLimit); + //adding 5 extra seconds to let leader auto-submit results and store them in DB + timeLimitFinishDate.add(Calendar.SECOND, 5); + + //start quartz job to notify non-leaders time is over + JobDetail finishScratchingJob = JobBuilder.newJob(FinishScratchingJob.class) + .withIdentity("finishScratching:" + sessionId) + .withDescription("Group name: " + session.getSessionName()) + .usingJobData("toolSessionId", sessionId).build(); + + Trigger fnishScratchingTrigger = TriggerBuilder.newTrigger() + .withIdentity("fnishScratchingTrigger:" + sessionId) + .startAt(timeLimitFinishDate.getTime()).build(); + + scheduler.scheduleJob(finishScratchingJob, fnishScratchingTrigger); + } @Override public void changeUserMark(Long userId, Long sessionId, Integer newMark) { @@ -2044,6 +2089,14 @@ public void setScratchieOutputFactory(ScratchieOutputFactory scratchieOutputFactory) { this.scratchieOutputFactory = scratchieOutputFactory; } + + /** + * @param scheduler + * The scheduler to set. + */ + public void setScheduler(Scheduler scheduler) { + this.scheduler = scheduler; + } // ****************** REST methods ************************* Index: lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/util/FinishScratchingJob.java =================================================================== diff -u --- lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/util/FinishScratchingJob.java (revision 0) +++ lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/util/FinishScratchingJob.java (revision beb150b1b726c122ccc5af1ba95083105f9948b8) @@ -0,0 +1,60 @@ +package org.lamsfoundation.lams.tool.scratchie.util; + +import java.io.IOException; +import java.util.Map; + +import org.apache.log4j.Logger; +import org.apache.tomcat.util.json.JSONException; +import org.lamsfoundation.lams.tool.scratchie.model.ScratchieSession; +import org.lamsfoundation.lams.tool.scratchie.service.IScratchieService; +import org.lamsfoundation.lams.tool.scratchie.web.action.LearningWebsocketServer; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.SchedulerContext; +import org.quartz.SchedulerException; +import org.springframework.context.ApplicationContext; +import org.springframework.scheduling.quartz.QuartzJobBean; + +/** + * The Quartz sheduling job that finishes scratching for the given ToolSession (which will automatically lead to showing + * Finish button all non-leaders and thus let then finish the activity). + * + * @author Andrey Balan + */ +public class FinishScratchingJob extends QuartzJobBean { + + private static Logger log = Logger.getLogger(FinishScratchingJob.class); + + @Override + protected void executeInternal(JobExecutionContext context) throws JobExecutionException { + IScratchieService scratchieService = getScratchieService(context); + + //getting toolSessionId set from scheduler + Map properties = context.getJobDetail().getJobDataMap(); + Long toolSessionId = (Long) properties.get("toolSessionId"); + ScratchieSession toolSession = scratchieService.getScratchieSessionBySessionId(toolSessionId); + + // if leader hasn't finished scratching yet - let all non-leaders see Next Activity button + if (!toolSession.isScratchingFinished()) { + try { + scratchieService.setScratchingFinished(toolSessionId); + } catch (JSONException | IOException e) { + throw new RuntimeException(e); + } + } + + if (log.isDebugEnabled()) { + log.debug("Scratching is finished for toolSessionId: " + toolSessionId.longValue()); + } + } + + protected IScratchieService getScratchieService(JobExecutionContext context) throws JobExecutionException { + try { + SchedulerContext sc = context.getScheduler().getContext(); + ApplicationContext cxt = (ApplicationContext) sc.get("context.central"); + return (IScratchieService) cxt.getBean("scratchieService"); + } catch (SchedulerException e) { + throw new JobExecutionException("Failed look up the scratchieService" + e.toString()); + } + } +} Index: lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/web/action/LearningAction.java =================================================================== diff -u -rae63df3e06f618712e1eab064a05015eb45c2f17 -rbeb150b1b726c122ccc5af1ba95083105f9948b8 --- lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/web/action/LearningAction.java (.../LearningAction.java) (revision ae63df3e06f618712e1eab064a05015eb45c2f17) +++ lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/web/action/LearningAction.java (.../LearningAction.java) (revision beb150b1b726c122ccc5af1ba95083105f9948b8) @@ -72,6 +72,7 @@ import org.lamsfoundation.lams.web.session.SessionManager; import org.lamsfoundation.lams.web.util.AttributeNames; import org.lamsfoundation.lams.web.util.SessionMap; +import org.quartz.SchedulerException; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; @@ -87,7 +88,7 @@ @Override public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) - throws IOException, ServletException, JSONException, ScratchieApplicationException { + throws IOException, ServletException, JSONException, ScratchieApplicationException, SchedulerException { String param = mapping.getParameter(); // -----------------------Scratchie Learner function --------------------------- @@ -97,6 +98,9 @@ if (param.equals("recordItemScratched")) { return recordItemScratched(mapping, form, request, response); } + if (param.equals("launchTimeLimit")) { + return launchTimeLimit(mapping, form, request, response); + } if (param.equals("finish")) { return finish(mapping, form, request, response); } @@ -176,6 +180,30 @@ return mapping.findForward("waitforleader"); } + // forwards to the waitForLeader time limit pages + if (!mode.isTeacher()) { + boolean isNonLeader = !user.getUserId().equals(groupLeader.getUserId()); + if (isNonLeader && scratchie.getTimeLimit() != 0 && !user.isSessionFinished()) { + + //show waitForLeaderLaunchTimeLimit page if the leader hasn't started activity or hasn't pressed OK button to launch time limit + if (toolSession.getTimeLimitLaunchedDate() == null) { + request.setAttribute(ScratchieConstants.ATTR_WAITING_MESSAGE_KEY, "label.waiting.for.leader.launch.time.limit"); + return mapping.findForward("waitForLeaderTimeLimit"); + } + + // check if the time limit is exceeded + boolean isTimeLimitExceeded = toolSession.getTimeLimitLaunchedDate().getTime() + + scratchie.getTimeLimit() * 60000 < System.currentTimeMillis(); + + // if the time is up and leader hasn't submitted response (as there will be a little delay between time + // is up and scratching is finished) - show waitForLeaderFinish page + if (isTimeLimitExceeded && !toolSession.isScratchingFinished()) { + request.setAttribute(ScratchieConstants.ATTR_WAITING_MESSAGE_KEY, "label.waiting.for.leader.finish"); + return mapping.findForward("waitForLeaderTimeLimit"); + } + } + } + // initial Session Map SessionMap sessionMap = new SessionMap(); request.getSession().setAttribute(sessionMap.getSessionID(), sessionMap); @@ -306,24 +334,40 @@ redirect.addParameter(ScratchieConstants.ATTR_SESSION_MAP_ID, sessionMap.getSessionID()); return redirect; - // show leader notebook page + // show leader notebook page } else if (isUserLeader && isScratchingFinished && isWaitingForLeaderToSubmitNotebook) { ActionRedirect redirect = new ActionRedirect(mapping.findForwardConfig("newReflection")); redirect.addParameter(ScratchieConstants.ATTR_SESSION_MAP_ID, sessionMap.getSessionID()); redirect.addParameter(AttributeNames.ATTR_MODE, mode); return redirect; - // show results page + // show results page } else if (isShowResults) { ActionRedirect redirect = new ActionRedirect(mapping.findForwardConfig("showResults")); redirect.addParameter(ScratchieConstants.ATTR_SESSION_MAP_ID, sessionMap.getSessionID()); redirect.addParameter(AttributeNames.ATTR_MODE, mode); return redirect; - // show learning.jsp page + // show learning.jsp page } else { + // time limit feature + boolean isTimeLimitEnabled = isUserLeader && !isScratchingFinished && scratchie.getTimeLimit() != 0; + boolean isTimeLimitNotLaunched = toolSession.getTimeLimitLaunchedDate() == null; + long secondsLeft = 1; + if (isTimeLimitEnabled) { + // if user has pressed OK button already - calculate remaining time, and full time otherwise + secondsLeft = isTimeLimitNotLaunched ? scratchie.getTimeLimit() * 60 + : scratchie.getTimeLimit() * 60 + - (System.currentTimeMillis() - toolSession.getTimeLimitLaunchedDate().getTime()) / 1000; + // change negative number or zero to 1 so it can autosubmit results + secondsLeft = Math.max(1, secondsLeft); + } + request.setAttribute(ScratchieConstants.ATTR_IS_TIME_LIMIT_ENABLED, isTimeLimitEnabled); + request.setAttribute(ScratchieConstants.ATTR_IS_TIME_LIMIT_NOT_LAUNCHED, isTimeLimitNotLaunched); + request.setAttribute(ScratchieConstants.ATTR_SECONDS_LEFT, secondsLeft); + // make non leaders also wait for burning questions submit isWaitingForLeaderToSubmitNotebook |= isWaitingForLeaderToSubmitBurningQuestions; @@ -351,7 +395,7 @@ ScratchieSession toolSession = service.getScratchieSessionBySessionId(toolSessionId); - ScratchieUser leader = this.getCurrentUser(toolSessionId); + ScratchieUser leader = getCurrentUser(toolSessionId); // only leader is allowed to scratch answers if (!toolSession.isUserGroupLeader(leader.getUid())) { return null; @@ -386,7 +430,32 @@ return null; } + + /** + * Stores date when user has started activity with time limit + * @throws ScratchieApplicationException + * @throws SchedulerException + */ + private ActionForward launchTimeLimit(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws ScratchieApplicationException, SchedulerException { + initializeScratchieService(); + String sessionMapID = WebUtil.readStrParam(request, ScratchieConstants.ATTR_SESSION_MAP_ID); + SessionMap sessionMap = (SessionMap) request.getSession() + .getAttribute(sessionMapID); + final Long toolSessionId = (Long) sessionMap.get(AttributeNames.PARAM_TOOL_SESSION_ID); + ScratchieSession toolSession = service.getScratchieSessionBySessionId(toolSessionId); + + ScratchieUser leader = getCurrentUser(toolSessionId); + // only leader is allowed to launch time limit + if (!toolSession.isUserGroupLeader(leader.getUid())) { + return null; + } + + service.launchTimeLimit(toolSessionId); + return null; + } + /** * Displays results page. When leader gets to this page, scratchingFinished column is set to true for all users. * Index: lams_tool_scratchie/web/WEB-INF/struts-config.xml =================================================================== diff -u -r480406c3b8253f063ad53045227a09a79e98f1d5 -rbeb150b1b726c122ccc5af1ba95083105f9948b8 --- lams_tool_scratchie/web/WEB-INF/struts-config.xml (.../struts-config.xml) (revision 480406c3b8253f063ad53045227a09a79e98f1d5) +++ lams_tool_scratchie/web/WEB-INF/struts-config.xml (.../struts-config.xml) (revision beb150b1b726c122ccc5af1ba95083105f9948b8) @@ -205,6 +205,7 @@ parameter="start" > + @@ -215,6 +216,10 @@ type="org.lamsfoundation.lams.tool.scratchie.web.action.LearningAction" parameter="recordItemScratched" > + + Index: lams_tool_scratchie/web/pages/learning/learning.jsp =================================================================== diff -u -r5f05f0ea0765aae9eaec741f5419fa9f7762f150 -rbeb150b1b726c122ccc5af1ba95083105f9948b8 --- lams_tool_scratchie/web/pages/learning/learning.jsp (.../learning.jsp) (revision 5f05f0ea0765aae9eaec741f5419fa9f7762f150) +++ lams_tool_scratchie/web/pages/learning/learning.jsp (.../learning.jsp) (revision beb150b1b726c122ccc5af1ba95083105f9948b8) @@ -69,20 +69,38 @@ } //time limit feature - + $(document).ready(function(){ - //show confirmation dialog - $.blockUI({ - message: $('#timelimit-start-dialog'), - css: { width: '325px', height: '120px'}, - overlayCSS: { opacity: '.98'} - }); + + //show timelimit-start-dialog in order to start countdown + if (${isTimeLimitNotLaunched}) { - //once OK button pressed start countdown - $('#timelimit-start-ok').click(function() { - $.unblockUI(); - displayCountdown(); - }); + //show confirmation dialog + $.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(); + }); + + } else { + displayCountdown(); + } + }); function displayCountdown(){ @@ -103,14 +121,14 @@ }); $('#countdown').countdown({ - until: '+${scratchie.timeLimit * 60}S', + until: '+${secondsLeft}S', format: 'hMS', compact: true, onTick: function(periods) { //check for 30 seconds if ((periods[4] == 0) && (periods[5] == 0) && (periods[6] <= 30)) { $('#countdown').css('color', '#FF3333'); - } + } }, onExpiry: function(periods) { $.blockUI({ message: '

' }); Index: lams_tool_scratchie/web/pages/learning/waitForLeaderTimeLimit.jsp =================================================================== diff -u --- lams_tool_scratchie/web/pages/learning/waitForLeaderTimeLimit.jsp (revision 0) +++ lams_tool_scratchie/web/pages/learning/waitForLeaderTimeLimit.jsp (revision beb150b1b726c122ccc5af1ba95083105f9948b8) @@ -0,0 +1,38 @@ + + +<%@ include file="/common/taglibs.jsp"%> + + + + <fmt:message key="label.learning.title" /> + <%@ include file="/common/header.jsp"%> + + + + + + + + + + + +
+ + + +
+ + + +
+ +
+