diff --git a/src/main/java/org/olat/course/assessment/AssessmentModeCoordinationService.java b/src/main/java/org/olat/course/assessment/AssessmentModeCoordinationService.java index 038994c6fe44f469526bf1987750cab894446ee2..dadd9e7771ac8ec1c6d1ee8a91307b6ed813ee34 100644 --- a/src/main/java/org/olat/course/assessment/AssessmentModeCoordinationService.java +++ b/src/main/java/org/olat/course/assessment/AssessmentModeCoordinationService.java @@ -22,6 +22,7 @@ package org.olat.course.assessment; import java.util.Date; import org.olat.basesecurity.IdentityRef; +import org.olat.core.id.Identity; import org.olat.course.assessment.AssessmentMode.Status; import org.olat.course.assessment.model.AssessmentModeStatistics; import org.olat.course.assessment.model.TransientAssessmentMode; @@ -59,7 +60,7 @@ public interface AssessmentModeCoordinationService { public AssessmentMode startAssessment(AssessmentMode assessmentMode); - public AssessmentMode stopAssessment(AssessmentMode assessmentMode, boolean pullTestSessions, boolean withDisadvantaged); + public AssessmentMode stopAssessment(AssessmentMode assessmentMode, boolean pullTestSessions, boolean withDisadvantaged, Identity doer); public AssessmentModeStatistics getStatistics(AssessmentMode assessmentMode); diff --git a/src/main/java/org/olat/course/assessment/manager/AssessmentModeCoordinationServiceImpl.java b/src/main/java/org/olat/course/assessment/manager/AssessmentModeCoordinationServiceImpl.java index 4588cee3928183f70177709570101fb1aea3c728..a910e6f4dff91b8418b81c3a1af95ce8e82fb5d0 100644 --- a/src/main/java/org/olat/course/assessment/manager/AssessmentModeCoordinationServiceImpl.java +++ b/src/main/java/org/olat/course/assessment/manager/AssessmentModeCoordinationServiceImpl.java @@ -32,7 +32,9 @@ import org.apache.logging.log4j.Logger; import org.olat.basesecurity.IdentityRef; import org.olat.basesecurity.model.IdentityRefImpl; import org.olat.core.commons.persistence.DB; +import org.olat.core.commons.services.taskexecutor.TaskExecutorManager; import org.olat.core.gui.control.Event; +import org.olat.core.id.Identity; import org.olat.core.logging.Tracing; import org.olat.core.util.cache.CacheWrapper; import org.olat.core.util.coordinate.CoordinatorManager; @@ -51,6 +53,8 @@ import org.olat.course.assessment.model.AssessmentModeStatistics; import org.olat.course.assessment.model.CoordinatedAssessmentMode; import org.olat.course.assessment.model.TransientAssessmentMode; import org.olat.group.ui.edit.BusinessGroupModifiedEvent; +import org.olat.ims.qti21.AssessmentTestSession; +import org.olat.ims.qti21.manager.AssessmentTestSessionDAO; import org.olat.modules.dcompensation.manager.DisadvantageCompensationDAO; import org.olat.repository.RepositoryEntry; import org.olat.repository.RepositoryEntryStatusEnum; @@ -74,6 +78,8 @@ public class AssessmentModeCoordinationServiceImpl implements AssessmentModeCoor @Autowired private DB dbInstance; @Autowired + private TaskExecutorManager taskExecutor; + @Autowired private AssessmentModule assessmentModule; @Autowired private RepositoryEntryDAO repositoryEntryDao; @@ -84,6 +90,8 @@ public class AssessmentModeCoordinationServiceImpl implements AssessmentModeCoor @Autowired private AssessmentModeManagerImpl assessmentModeManager; @Autowired + private AssessmentTestSessionDAO assessmentTestSessionDao; + @Autowired private DisadvantageCompensationDAO disadvantageCompensationDao; private Map<Long,CoordinatedAssessmentMode> coordinatedModes = new ConcurrentHashMap<>(); @@ -460,7 +468,7 @@ public class AssessmentModeCoordinationServiceImpl implements AssessmentModeCoor } @Override - public AssessmentMode stopAssessment(AssessmentMode mode, boolean pullTestSessions, boolean withDisadvantaged) { + public AssessmentMode stopAssessment(AssessmentMode mode, boolean pullTestSessions, boolean withDisadvantaged, Identity doer) { mode = assessmentModeManager.getAssessmentModeById(mode.getKey()); EndStatus endStatus; @@ -482,16 +490,33 @@ public class AssessmentModeCoordinationServiceImpl implements AssessmentModeCoor } } - if(assessedIdentityKeys.contains(252744713l)) { - System.out.println(); - } - endStatus = partial ? EndStatus.withoutDisadvantage : EndStatus.all; } + if(pullTestSessions) { + pullSessions(mode, assessedIdentityKeys, doer); + } + return stopAssessment(mode, assessedIdentityKeys, endStatus); } + private void pullSessions(AssessmentMode mode, Set<Long> assessedIdentityKeys, Identity doer) { + List<IdentityRef> identityRefs = assessedIdentityKeys.stream() + .map(IdentityRefImpl::new) + .collect(Collectors.toList()); + List<AssessmentTestSession> testSessions = assessmentTestSessionDao + .getRunningTestSessions(mode.getRepositoryEntry(), mode.getElementAsList(), identityRefs); + if(!testSessions.isEmpty()) { + List<Long> testSessionKeys = testSessions.stream() + .map(AssessmentTestSession::getKey) + .collect(Collectors.toList()); + + Long doerKey = doer == null ? null : doer.getKey(); + PullTestSessionsTask task = new PullTestSessionsTask(mode.getRepositoryEntry().getKey(), testSessionKeys, doerKey); + taskExecutor.schedule(task, 30000); + } + } + private AssessmentMode stopAssessment(AssessmentMode mode, Set<Long> assessedIdentityKeys, EndStatus endStatus) { String cmd; if(mode.getFollowupTime() > 0) { diff --git a/src/main/java/org/olat/course/assessment/manager/PullTestSessionsTask.java b/src/main/java/org/olat/course/assessment/manager/PullTestSessionsTask.java new file mode 100644 index 0000000000000000000000000000000000000000..7bfaaeab1fe41468ab1d043292905da552886e0e --- /dev/null +++ b/src/main/java/org/olat/course/assessment/manager/PullTestSessionsTask.java @@ -0,0 +1,118 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.course.assessment.manager; + +import java.io.Serializable; +import java.util.List; +import java.util.Locale; +import java.util.TimerTask; + +import org.apache.logging.log4j.Logger; +import org.olat.core.CoreSpringFactory; +import org.olat.core.commons.persistence.DB; +import org.olat.core.id.Identity; +import org.olat.core.logging.Tracing; +import org.olat.core.util.i18n.I18nManager; +import org.olat.course.CourseFactory; +import org.olat.course.ICourse; +import org.olat.course.assessment.AssessmentHelper; +import org.olat.course.nodes.CourseNode; +import org.olat.course.nodes.IQTESTCourseNode; +import org.olat.course.run.environment.CourseEnvironment; +import org.olat.course.run.userview.UserCourseEnvironment; +import org.olat.ims.qti21.AssessmentTestSession; +import org.olat.ims.qti21.QTI21Service; +import org.olat.ims.qti21.model.DigitalSignatureOptions; +import org.olat.modules.assessment.Role; +import org.olat.repository.RepositoryEntry; +import org.olat.repository.RepositoryService; + +/** + * + * Initial date: 16 oct. 2020<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class PullTestSessionsTask extends TimerTask implements Serializable { + + private static final Logger log = Tracing.createLoggerFor(PullTestSessionsTask.class); + + private static final long serialVersionUID = 3863367666724686544L; + + private Long coachkKey; + private Long courseEntryKey; + private List<Long> testSessionKeys; + + public PullTestSessionsTask(Long courseEntryKey, List<Long> testSessionKeys, Long coachkKey) { + this.courseEntryKey = courseEntryKey; + this.testSessionKeys = testSessionKeys; + this.coachkKey = coachkKey; + } + + @Override + public void run() { + RepositoryEntry courseEntry = CoreSpringFactory.getImpl(RepositoryService.class) + .loadByKey(courseEntryKey); + ICourse course = CourseFactory.loadCourse(courseEntry); + + DB dbInstance = CoreSpringFactory.getImpl(DB.class); + for(Long testSessionKey:testSessionKeys) { + try { + pullSession(course, testSessionKey); + } catch (Exception e) { + log.error("", e); + } finally { + dbInstance.commitAndCloseSession(); + } + } + } + + private void pullSession(ICourse course, Long testSessionKey) { + QTI21Service qtiService = CoreSpringFactory.getImpl(QTI21Service.class); + AssessmentTestSession session = qtiService.getAssessmentTestSession(testSessionKey); + if(session == null || session.isCancelled() || session.isExploded() + || session.getFinishTime() != null || session.getTerminationTime() != null) { + return; + } + + Identity identity = session.getIdentity(); + CourseNode node = course.getRunStructure().getNode(session.getSubIdent()); + if(node instanceof IQTESTCourseNode) { + Object identifier = identity == null ? session.getAnonymousIdentifier() : identity.getKey(); + log.info(Tracing.M_AUDIT, "Retrieve test session async: {} (assessed identity={}) retrieved by coach {}", + session.getKey(), identifier, coachkKey); + + IQTESTCourseNode courseNode = (IQTESTCourseNode)node; + String language = null; + if(identity != null) { + language = identity.getUser().getPreferences().getLanguage(); + } + Locale locale = CoreSpringFactory.getImpl(I18nManager.class).getLocaleOrDefault(language); + DigitalSignatureOptions signatureOptions = courseNode.getSignatureOptions(session, locale); + session = qtiService.pullSession(session, signatureOptions, identity); + + RepositoryEntry courseEntry = session.getRepositoryEntry(); + CourseEnvironment courseEnv = CourseFactory.loadCourse(courseEntry).getCourseEnvironment(); + UserCourseEnvironment assessedUserCourseEnv = AssessmentHelper + .createAndInitUserCourseEnvironment(session.getIdentity(), courseEnv); + courseNode.pullAssessmentTestSession(session, assessedUserCourseEnv, identity, Role.coach); + } + } +} diff --git a/src/main/java/org/olat/course/assessment/ui/tool/ConfirmStopAssessmentModeController.java b/src/main/java/org/olat/course/assessment/ui/tool/ConfirmStopAssessmentModeController.java index 3e91dd4991661903785c2a7f8d01309ead96d47f..0b9b69b3656e974a7dbada9fc8a10febaf2e3f6c 100644 --- a/src/main/java/org/olat/course/assessment/ui/tool/ConfirmStopAssessmentModeController.java +++ b/src/main/java/org/olat/course/assessment/ui/tool/ConfirmStopAssessmentModeController.java @@ -89,7 +89,7 @@ public class ConfirmStopAssessmentModeController extends FormBasicController { initForm(formLayout, nodeList, assessedIdentityKeys); } - if(runningSessions && false) {//TODO assessment mode + if(runningSessions) { KeyValues keyValues = new KeyValues(); keyValues.add(KeyValues.entry("with", translate("confirm.stop.pull.running.sessions"))); pullRunningSessionsEl = uifactory.addCheckboxesHorizontal("runningSessions", "confirm.stop.pull.running.sessions", formLayout, @@ -160,7 +160,7 @@ public class ConfirmStopAssessmentModeController extends FormBasicController { AssessmentMode reloadedMode = assessmentModeManager.getAssessmentModeById(mode.getKey()); boolean pullTests = pullRunningSessionsEl != null && pullRunningSessionsEl.isAtLeastSelected(1); boolean withDisadvantaged = withDisadvantagesEl == null || withDisadvantagesEl.isAtLeastSelected(1); - assessmentModeCoordinationService.stopAssessment(reloadedMode, pullTests, withDisadvantaged); + assessmentModeCoordinationService.stopAssessment(reloadedMode, pullTests, withDisadvantaged, getIdentity()); dbInstance.commit(); fireEvent(ureq, Event.DONE_EVENT); } diff --git a/src/main/java/org/olat/course/nodes/IQTESTCourseNode.java b/src/main/java/org/olat/course/nodes/IQTESTCourseNode.java index 64ff12f0289499e2b17183e3b86bf9b56c3d3da3..f73cfd288b136dcff003920c580ec045fd1a639a 100644 --- a/src/main/java/org/olat/course/nodes/IQTESTCourseNode.java +++ b/src/main/java/org/olat/course/nodes/IQTESTCourseNode.java @@ -54,6 +54,7 @@ import org.olat.core.util.Util; import org.olat.core.util.coordinate.CoordinatorManager; import org.olat.core.util.nodes.INode; import org.olat.core.util.resource.OresHelper; +import org.olat.course.CourseFactory; import org.olat.course.ICourse; import org.olat.course.archiver.ScoreAccountingHelper; import org.olat.course.assessment.AssessmentManager; @@ -107,6 +108,7 @@ import org.olat.ims.qti21.QTI21Module; import org.olat.ims.qti21.QTI21Service; import org.olat.ims.qti21.manager.AssessmentTestSessionDAO; import org.olat.ims.qti21.manager.archive.QTI21ArchiveFormat; +import org.olat.ims.qti21.model.DigitalSignatureOptions; import org.olat.ims.qti21.model.QTI21StatisticSearchParams; import org.olat.ims.qti21.model.xml.QtiNodesExtractor; import org.olat.ims.qti21.resultexport.QTI21ResultsExportMediaResource; @@ -271,6 +273,26 @@ public class IQTESTCourseNode extends AbstractAccessableCourseNode implements QT } return timeLimit; } + + public DigitalSignatureOptions getSignatureOptions(AssessmentTestSession session, Locale locale) { + RepositoryEntry testEntry = session.getTestEntry(); + RepositoryEntry courseEntry = session.getRepositoryEntry(); + QTI21DeliveryOptions deliveryOptions = CoreSpringFactory.getImpl(QTI21Service.class) + .getDeliveryOptions(testEntry); + + ModuleConfiguration config = getModuleConfiguration(); + boolean digitalSignature = config.getBooleanSafe(IQEditController.CONFIG_DIGITAL_SIGNATURE, + deliveryOptions.isDigitalSignature()); + boolean sendMail = config.getBooleanSafe(IQEditController.CONFIG_DIGITAL_SIGNATURE_SEND_MAIL, + deliveryOptions.isDigitalSignatureMail()); + + DigitalSignatureOptions options = new DigitalSignatureOptions(digitalSignature, sendMail, courseEntry, testEntry); + if(digitalSignature) { + CourseEnvironment courseEnv = CourseFactory.loadCourse(courseEntry).getCourseEnvironment(); + QTI21AssessmentRunController.decorateCourseConfirmation(session, options, courseEnv, this, testEntry, null, locale); + } + return options; + } public boolean isScoreVisibleAfterCorrection() { String defVisibility = CoreSpringFactory.getImpl(QTI21Module.class).isResultsVisibleAfterCorrectionWorkflow() diff --git a/src/main/java/org/olat/ims/qti21/QTI21Service.java b/src/main/java/org/olat/ims/qti21/QTI21Service.java index 81835ca50d5b6daa20b6f79797e329a2d5504a9e..90ce5b5ad01d7370bc9921cc3642a1fced5a7183 100644 --- a/src/main/java/org/olat/ims/qti21/QTI21Service.java +++ b/src/main/java/org/olat/ims/qti21/QTI21Service.java @@ -314,7 +314,7 @@ public interface QTI21Service { */ public AssessmentTestSession reopenAssessmentTestSession(AssessmentTestSession session, Identity actor); - public List<AssessmentTestSession> getRunningAssessmentTestSession(RepositoryEntry entry, String subIdent, RepositoryEntry testEntry); + public List<AssessmentTestSession> getRunningAssessmentTestSession(RepositoryEntryRef entry, String subIdent, RepositoryEntry testEntry); public TestSessionState loadTestSessionState(AssessmentTestSession session); diff --git a/src/main/java/org/olat/ims/qti21/manager/AssessmentTestSessionDAO.java b/src/main/java/org/olat/ims/qti21/manager/AssessmentTestSessionDAO.java index ff74f2f7bb498b8a896723a1155dcc40c892c695..6dc6cf4f41dec7d84556f2092f9bf90b850e6d70 100644 --- a/src/main/java/org/olat/ims/qti21/manager/AssessmentTestSessionDAO.java +++ b/src/main/java/org/olat/ims/qti21/manager/AssessmentTestSessionDAO.java @@ -514,6 +514,37 @@ public class AssessmentTestSessionDAO { return query.getResultList(); } + public List<AssessmentTestSession> getRunningTestSessions(RepositoryEntryRef entry, List<String> courseSubIdents, List<? extends IdentityRef> identities) { + StringBuilder sb = new StringBuilder(); + sb.append("select session from qtiassessmenttestsession session") + .append(" left join session.testEntry testEntry") + .append(" left join testEntry.olatResource testResource") + .append(" inner join fetch session.identity assessedIdentity") + .append(" inner join fetch assessedIdentity.user assessedUser") + .append(" where session.repositoryEntry.key=:repositoryEntryKey") + .append(" and session.finishTime is null and session.terminationTime is null") + .append(" and session.exploded=false and session.cancelled=false") + .append(" and session.identity.key in (:identityKeys)"); + if(courseSubIdents != null && !courseSubIdents.isEmpty()) { + sb.append(" and session.subIdent in (:subIdents)"); + } + + List<Long> identityKeys = identities.stream() + .map(IdentityRef::getKey) + .collect(Collectors.toList()); + + TypedQuery<AssessmentTestSession> query = dbInstance.getCurrentEntityManager() + .createQuery(sb.toString(), AssessmentTestSession.class) + .setFirstResult(0) + .setMaxResults(1) + .setParameter("repositoryEntryKey", entry.getKey()) + .setParameter("identityKeys", identityKeys); + if(courseSubIdents != null && !courseSubIdents.isEmpty()) { + query.setParameter("subIdents", courseSubIdents); + } + return query.getResultList(); + } + /** * * @param entry The repository entry (typically the course, or the test if not in a course) (mandatory) diff --git a/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java b/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java index 93508b8d62e40c8b7e4c92c6293b7127b82112fa..2079d993ebc3fa96e0eba08b80e9aafedd893a36 100644 --- a/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java +++ b/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java @@ -351,9 +351,8 @@ public class QTI21ServiceImpl implements QTI21Service, UserDataDeletable, Initia assessmentTestsCache.replace(resourceFile, resolvedAssessmentTest); return resolvedAssessmentTest; } - return assessmentTestsCache.computeIfAbsent(resourceFile, file -> { - return internalLoadAndResolveAssessmentTest(resourceDirectory, assessmentObjectSystemId); - }); + return assessmentTestsCache.computeIfAbsent(resourceFile, file -> + internalLoadAndResolveAssessmentTest(resourceDirectory, assessmentObjectSystemId)); } private ResolvedAssessmentTest internalLoadAndResolveAssessmentTest(File resourceDirectory, URI assessmentObjectSystemId) { @@ -368,9 +367,8 @@ public class QTI21ServiceImpl implements QTI21Service, UserDataDeletable, Initia @Override public ResolvedAssessmentItem loadAndResolveAssessmentItem(URI assessmentObjectSystemId, File resourceDirectory) { File resourceFile = new File(assessmentObjectSystemId); - return assessmentItemsCache.computeIfAbsent(resourceFile, (file) -> { - return loadAndResolveAssessmentItemForCopy(assessmentObjectSystemId, resourceDirectory); - }); + return assessmentItemsCache.computeIfAbsent(resourceFile, file -> + loadAndResolveAssessmentItemForCopy(assessmentObjectSystemId, resourceDirectory)); } @Override @@ -667,7 +665,7 @@ public class QTI21ServiceImpl implements QTI21Service, UserDataDeletable, Initia } @Override - public List<AssessmentTestSession> getRunningAssessmentTestSession(RepositoryEntry entry, String subIdent, RepositoryEntry testEntry) { + public List<AssessmentTestSession> getRunningAssessmentTestSession(RepositoryEntryRef entry, String subIdent, RepositoryEntry testEntry) { return testSessionDao.getRunningTestSessions(entry, subIdent, testEntry); } @@ -909,7 +907,7 @@ public class QTI21ServiceImpl implements QTI21Service, UserDataDeletable, Initia return new DigitalSignatureValidation(DigitalSignatureValidation.Message.sessionNotFound, false); } String testSessionKey = uri.substring(start + 1, end); - AssessmentTestSession testSession = getAssessmentTestSession(new Long(testSessionKey)); + AssessmentTestSession testSession = getAssessmentTestSession(Long.valueOf(testSessionKey)); if(testSession == null) { return new DigitalSignatureValidation(DigitalSignatureValidation.Message.sessionNotFound, false); } @@ -1245,7 +1243,7 @@ public class QTI21ServiceImpl implements QTI21Service, UserDataDeletable, Initia try { Value computedValue = outcomeVariable.getComputedValue(); if (QtiConstants.VARIABLE_DURATION_IDENTIFIER.equals(identifier)) { - log.info(Tracing.M_AUDIT, candidateSession.getKey() + " :: " + outcomeVariable.getIdentifier() + " - " + stringifyQtiValue(computedValue)); + log.info(Tracing.M_AUDIT, "{} :: {} - {}", candidateSession.getKey(), outcomeVariable.getIdentifier(), stringifyQtiValue(computedValue)); } else if (QTI21Constants.SCORE_IDENTIFIER.equals(identifier)) { if (computedValue instanceof NumberValue) { double score = ((NumberValue) computedValue).doubleValue();