From 3bab442a5dfc9837ae7bd5c24b8fcbc529a20c08 Mon Sep 17 00:00:00 2001
From: srosse <stephane.rosse@frentix.com>
Date: Fri, 16 Oct 2020 10:21:17 +0200
Subject: [PATCH] OO-4934: handle compensation disadvantage in manual
 assessment modes

---
 .../fullWebApp/BaseFullWebappController.java  |  28 ++-
 .../core/util/openxml/OpenXMLDocument.java    |   2 +-
 .../course/assessment/AssessmentMode.java     |  16 ++
 .../AssessmentModeCoordinationService.java    |   6 +-
 .../assessment/AssessmentModeManager.java     |  34 ++--
 ...AssessmentModeCoordinationServiceImpl.java | 157 ++++++++++++++---
 .../assessment/manager/AssessmentModeDAO.java |  25 +--
 .../manager/AssessmentModeManagerImpl.java    |  21 ++-
 .../assessment/model/AssessmentModeImpl.java  |  48 +++++-
 .../assessment/model/EnhancedStatus.java      |  10 +-
 .../model/TransientAssessmentMode.java        |  17 +-
 .../mode/AssessmentModeGuardController.java   | 163 ++++++++++--------
 .../ui/mode/AssessmentModeListModel.java      | 101 +++++------
 .../ui/mode/ModeStatusCellRenderer.java       |  11 +-
 .../AssessmentModeOverviewListController.java |   5 +-
 .../AssessmentModeOverviewListTableModel.java |   3 +-
 .../ui/tool/AssessmentToolController.java     |  13 +-
 .../ui/tool/_content/confirm_stop.html        |   8 +
 .../ui/tool/_i18n/LocalStrings_de.properties  |   3 +
 .../ui/tool/_i18n/LocalStrings_en.properties  |   3 +
 .../java/org/olat/ims/qti21/QTI21Service.java |   2 +
 .../manager/AssessmentTestSessionDAO.java     |  38 ++++
 .../ims/qti21/manager/QTI21ServiceImpl.java   |   5 +
 ...TracingReportGeneratorStep2Controller.java |   1 -
 .../DisadvantageCompensationService.java      |   2 +
 .../manager/DisadvantageCompensationDAO.java  |  24 +++
 .../DisadvantageCompensationServiceImpl.java  |   5 +
 .../database/mysql/alter_15_2_x_to_15_3_0.sql |   1 +
 .../database/mysql/setupDatabase.sql          |   8 +-
 .../oracle/alter_15_2_x_to_15_3_0.sql         |   1 +
 .../database/oracle/setupDatabase.sql         |   1 +
 .../postgresql/alter_15_2_x_to_15_3_0.sql     |   1 +
 .../database/postgresql/setupDatabase.sql     |   1 +
 .../manager/AssessmentTestSessionDAOTest.java |  23 +++
 .../DisadvantageCompensationDAOTest.java      |  26 ++-
 35 files changed, 608 insertions(+), 205 deletions(-)

diff --git a/src/main/java/org/olat/core/commons/fullWebApp/BaseFullWebappController.java b/src/main/java/org/olat/core/commons/fullWebApp/BaseFullWebappController.java
index 6a7335953a3..25d82d054c6 100644
--- a/src/main/java/org/olat/core/commons/fullWebApp/BaseFullWebappController.java
+++ b/src/main/java/org/olat/core/commons/fullWebApp/BaseFullWebappController.java
@@ -108,6 +108,7 @@ import org.olat.core.util.i18n.I18nManager;
 import org.olat.core.util.i18n.I18nModule;
 import org.olat.core.util.prefs.Preferences;
 import org.olat.core.util.resource.OresHelper;
+import org.olat.course.assessment.AssessmentMode.EndStatus;
 import org.olat.course.assessment.AssessmentMode.Status;
 import org.olat.course.assessment.AssessmentModeNotificationEvent;
 import org.olat.course.assessment.model.TransientAssessmentMode;
@@ -115,7 +116,9 @@ import org.olat.course.assessment.ui.mode.AssessmentModeGuardController;
 import org.olat.course.assessment.ui.mode.ChooseAssessmentModeEvent;
 import org.olat.gui.control.UserToolsMenuController;
 import org.olat.home.HomeSite;
+import org.olat.modules.dcompensation.DisadvantageCompensationService;
 import org.olat.modules.edusharing.EdusharingModule;
+import org.olat.repository.model.RepositoryEntryRefImpl;
 import org.olat.user.UserManager;
 import org.olat.user.propertyhandlers.UserPropertyHandler;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -1399,7 +1402,7 @@ public class BaseFullWebappController extends BasicController implements DTabs,
 		if(getIdentity() == null || !event.isModeOf(lockMode, getIdentity())) {
 			return;
 		}
-		
+
 		String cmd = event.getCommand();
 		switch(cmd) {
 			case AssessmentModeNotificationEvent.STOP_WARNING:
@@ -1416,15 +1419,19 @@ public class BaseFullWebappController extends BasicController implements DTabs,
 				}
 				break;
 			case AssessmentModeNotificationEvent.START_ASSESSMENT:
-				asyncLockResource(event.getAssessementMode());
+				if(event.getAssessedIdentityKeys().contains(getIdentity().getKey())) {
+					asyncLockResource(event.getAssessementMode());
+				}
 				break;
 			case AssessmentModeNotificationEvent.STOP_ASSESSMENT:
-				if(asyncLockResource(event.getAssessementMode())) {
+				if(event.getAssessedIdentityKeys().contains(getIdentity().getKey())
+						&& asyncLockResource(event.getAssessementMode())) {
 					stickyMessageCmp.setDelegateComponent(null);
 				}
 				break;
 			case AssessmentModeNotificationEvent.END:
-				if(asyncUnlockResource(event.getAssessementMode())) {
+				if(event.getAssessedIdentityKeys().contains(getIdentity().getKey())
+						&& asyncUnlockResource(event.getAssessementMode())) {
 					stickyMessageCmp.setDelegateComponent(null);
 				}
 				break;	
@@ -1434,7 +1441,7 @@ public class BaseFullWebappController extends BasicController implements DTabs,
 	@Override
 	public boolean hasStaticSite(Class<? extends SiteInstance> type) {
 		boolean hasSite = false;
-		if(sites != null && sites.size() > 0) {
+		if(sites != null && !sites.isEmpty()) {
 			for(SiteInstance site:sites) {
 				if(site.getClass().equals(type)) {
 					hasSite = true;
@@ -1523,8 +1530,10 @@ public class BaseFullWebappController extends BasicController implements DTabs,
 			lock = true;
 			lockMode = mode;
 			lockStatus = LockStatus.need;
-		} else if(lockResource != null && lockResource.getResourceableId().equals(mode.getResource().getResourceableId())) {
-			if(mode.getStatus() == Status.leadtime || mode.getStatus() == Status.followup) {
+		} else if(lockResource.getResourceableId().equals(mode.getResource().getResourceableId())) {
+			if(mode.getStatus() == Status.leadtime
+					|| (mode.getStatus() == Status.followup && mode.getEndStatus() == EndStatus.all)
+					|| (mode.getStatus() == Status.followup && mode.getEndStatus() == EndStatus.withoutDisadvantage && !hasDisadvantageCompensation(mode))) {
 				if(assessmentGuardCtrl == null) {
 					lockStatus = LockStatus.need;
 				}
@@ -1537,6 +1546,11 @@ public class BaseFullWebappController extends BasicController implements DTabs,
 		return lock;
 	}
 	
+	private boolean hasDisadvantageCompensation(TransientAssessmentMode mode) {
+		return CoreSpringFactory.getImpl(DisadvantageCompensationService.class)
+			.isActiveDisadvantageCompensation(getIdentity(), new RepositoryEntryRefImpl(mode.getRepositoryEntryKey()), mode.getElementList());
+	}
+	
 	private boolean asyncUnlockResource(TransientAssessmentMode mode) {
 		boolean unlock;
 		if(lockResource != null && lockResource.getResourceableId().equals(mode.getResource().getResourceableId())) {
diff --git a/src/main/java/org/olat/core/util/openxml/OpenXMLDocument.java b/src/main/java/org/olat/core/util/openxml/OpenXMLDocument.java
index 12f4621f1b8..85fa8353c64 100644
--- a/src/main/java/org/olat/core/util/openxml/OpenXMLDocument.java
+++ b/src/main/java/org/olat/core/util/openxml/OpenXMLDocument.java
@@ -78,7 +78,7 @@ public class OpenXMLDocument {
 	
 	private static final Logger log = Tracing.createLoggerFor(OpenXMLDocument.class);
 	
-	private final int DPI = 72;
+	private static final int DPI = 72;
 	
 	private final Document document;
 	private final Element rootElement;
diff --git a/src/main/java/org/olat/course/assessment/AssessmentMode.java b/src/main/java/org/olat/course/assessment/AssessmentMode.java
index 6c23ed804cf..07ff41c9d8f 100644
--- a/src/main/java/org/olat/course/assessment/AssessmentMode.java
+++ b/src/main/java/org/olat/course/assessment/AssessmentMode.java
@@ -20,6 +20,7 @@
 package org.olat.course.assessment;
 
 import java.util.Date;
+import java.util.List;
 import java.util.Set;
 
 import org.olat.core.id.CreateInfo;
@@ -53,6 +54,10 @@ public interface AssessmentMode extends ModifiedInfo, CreateInfo {
 
 	public void setStatus(Status status);
 	
+	public EndStatus getEndStatus();
+	
+	public void setEndStatus(EndStatus status);
+	
 	public boolean isManualBeginEnd();
 	
 	public void setManualBeginEnd(boolean manualBeginEnd);
@@ -97,6 +102,12 @@ public interface AssessmentMode extends ModifiedInfo, CreateInfo {
 
 	public void setElementList(String elementList);
 	
+	/**
+	 * 
+	 * @return A list of course elements identifiers or null if not defined
+	 */
+	public List<String> getElementAsList();
+	
 	public String getStartElement();
 
 	public void setStartElement(String startElement);
@@ -150,4 +161,9 @@ public interface AssessmentMode extends ModifiedInfo, CreateInfo {
 			return cssClass;
 		}
 	}
+	
+	public enum EndStatus {
+		all,
+		withoutDisadvantage
+	}
 }
diff --git a/src/main/java/org/olat/course/assessment/AssessmentModeCoordinationService.java b/src/main/java/org/olat/course/assessment/AssessmentModeCoordinationService.java
index 4c8e318f59e..038994c6fe4 100644
--- a/src/main/java/org/olat/course/assessment/AssessmentModeCoordinationService.java
+++ b/src/main/java/org/olat/course/assessment/AssessmentModeCoordinationService.java
@@ -50,6 +50,8 @@ public interface AssessmentModeCoordinationService {
 	 */
 	public boolean canStop(AssessmentMode assessmentMode);
 	
+	public boolean isDisadvantageCompensationExtensionTime(AssessmentMode assessmentMode);
+	
 	
 	public void processRepositoryEntryChangedStatus(RepositoryEntry entry);
 	
@@ -57,9 +59,7 @@ public interface AssessmentModeCoordinationService {
 	
 	public AssessmentMode startAssessment(AssessmentMode assessmentMode);
 	
-	public AssessmentMode stopAssessment(AssessmentMode assessmentMode);
-	
-	
+	public AssessmentMode stopAssessment(AssessmentMode assessmentMode, boolean pullTestSessions, boolean withDisadvantaged);
 	
 	public AssessmentModeStatistics getStatistics(AssessmentMode assessmentMode);
 	
diff --git a/src/main/java/org/olat/course/assessment/AssessmentModeManager.java b/src/main/java/org/olat/course/assessment/AssessmentModeManager.java
index e6f3736c2a5..dcd4f2aefc1 100644
--- a/src/main/java/org/olat/course/assessment/AssessmentModeManager.java
+++ b/src/main/java/org/olat/course/assessment/AssessmentModeManager.java
@@ -113,7 +113,7 @@ public interface AssessmentModeManager {
 	/**
 	 * Delete a specific assessment mode.
 	 * 
-	 * @param assessmentMode
+	 * @param assessmentMode The assessment mode to delete
 	 */
 	public void delete(AssessmentMode assessmentMode);
 	
@@ -126,7 +126,7 @@ public interface AssessmentModeManager {
 	/**
 	 * Search the whole assessment modes on the system.
 	 * 
-	 * @param params
+	 * @param params The search parameters
 	 * @return A list of assessment modes
 	 */
 	public List<AssessmentMode> findAssessmentMode(SearchAssessmentModeParams params);
@@ -149,15 +149,19 @@ public interface AssessmentModeManager {
 	public List<AssessmentMode> getPlannedAssessmentMode(RepositoryEntryRef entry, Date from, Date to);
 	
 	/**
-	 * Load the assessment mode for a specific user now.
+	 * Load the assessment mode for a specific user now. The status
+	 * of the assessment mode is checked and if the user has some
+	 * disadvantage compensations.
 	 * 
-	 * @param identity
-	 * @return
+	 * @param identity The identity
+	 * @return The active assessment mode for the specified user
 	 */
 	public List<AssessmentMode> getAssessmentModeFor(IdentityRef identity);
 	
 	/**
 	 * This return all modes between the begin date minus lead time and end time.
+	 * Or if the assessment modes are stopped but some users with disadvantage
+	 * compensations are still at work.
 	 * 
 	 * @return The list of modes
 	 */
@@ -165,6 +169,8 @@ public interface AssessmentModeManager {
 	
 	/**
 	 * Return true if the course is in assessment mode at the specified time.
+	 * Disadvantage compensations are taken in account.
+	 * 
 	 * @param entry The course
 	 * @param date The date
 	 * @return true if the course is in assessment mode
@@ -185,27 +191,27 @@ public interface AssessmentModeManager {
 	
 	/**
 	 * Return the list of assessed users specified in the configuration.
-	 * @param assessmentMode
-	 * @return
+	 * 
+	 * @param assessmentMode The assessment mode
+	 * @return A list of identity keys
 	 */
 	public Set<Long> getAssessedIdentityKeys(AssessmentMode assessmentMode);
 	
 	public boolean isNodeInUse(RepositoryEntryRef entry, CourseNode node);
 	
-	
 	/**
 	 * 
-	 * @param ipList
-	 * @param address
-	 * @return
+	 * @param ipList A list of IPs as string
+	 * @param address An address, IP or domain
+	 * @return true if the specified address match the ips list
 	 */
 	public boolean isIpAllowed(String ipList, String address);
 	
 	/**
 	 * 
-	 * @param request
-	 * @param safeExamBrowserKey
-	 * @return
+	 * @param request The HTTP request
+	 * @param safeExamBrowserKey The key
+	 * @return true if the request is allowed based on the specified key
 	 */
 	public boolean isSafelyAllowed(HttpServletRequest request, String safeExamBrowserKeys);
 
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 60e08330013..4588cee3928 100644
--- a/src/main/java/org/olat/course/assessment/manager/AssessmentModeCoordinationServiceImpl.java
+++ b/src/main/java/org/olat/course/assessment/manager/AssessmentModeCoordinationServiceImpl.java
@@ -26,6 +26,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
 
 import org.apache.logging.log4j.Logger;
 import org.olat.basesecurity.IdentityRef;
@@ -40,6 +41,7 @@ import org.olat.core.util.resource.OresHelper;
 import org.olat.core.util.session.UserSessionManager;
 import org.olat.course.CourseFactory;
 import org.olat.course.assessment.AssessmentMode;
+import org.olat.course.assessment.AssessmentMode.EndStatus;
 import org.olat.course.assessment.AssessmentMode.Status;
 import org.olat.course.assessment.AssessmentModeCoordinationService;
 import org.olat.course.assessment.AssessmentModeNotificationEvent;
@@ -49,6 +51,7 @@ 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.modules.dcompensation.manager.DisadvantageCompensationDAO;
 import org.olat.repository.RepositoryEntry;
 import org.olat.repository.RepositoryEntryStatusEnum;
 import org.olat.repository.manager.RepositoryEntryDAO;
@@ -80,6 +83,8 @@ public class AssessmentModeCoordinationServiceImpl implements AssessmentModeCoor
 	private UserSessionManager userSessionManager;
 	@Autowired
 	private AssessmentModeManagerImpl assessmentModeManager;
+	@Autowired
+	private DisadvantageCompensationDAO disadvantageCompensationDao;
 	
 	private Map<Long,CoordinatedAssessmentMode> coordinatedModes = new ConcurrentHashMap<>();
 	private CacheWrapper<Long,AssessmentModeStatistics> coordinatedModeStatistics;
@@ -275,17 +280,7 @@ public class AssessmentModeCoordinationServiceImpl implements AssessmentModeCoor
 						assessmentModeManager.getAssessedIdentityKeys(mode));
 			}
 		} else if(mode.isManualBeginEnd() && !forceStatus) {
-			//what to do in manual mode
-			if(mode.getStatus() == Status.followup) {
-				if(mode.getEndWithFollowupTime().compareTo(now) < 0) {
-					mode = ensureStatusOfMode(mode, Status.end);
-					sendEvent(AssessmentModeNotificationEvent.END, mode,
-							assessmentModeManager.getAssessedIdentityKeys(mode));
-				}
-			} else if(mode.getStatus() == Status.assessment) {
-				sendEvent(AssessmentModeNotificationEvent.START_ASSESSMENT, mode,
-						assessmentModeManager.getAssessedIdentityKeys(mode));
-			}
+			sendEventForManualMode(mode, now);
 		} else {
 			if(mode.getBegin().compareTo(now) <= 0 && mode.getEnd().compareTo(now) > 0) {
 				mode = ensureStatusOfMode(mode, Status.assessment);
@@ -307,12 +302,12 @@ public class AssessmentModeCoordinationServiceImpl implements AssessmentModeCoor
 					sendEvent(AssessmentModeNotificationEvent.STOP_ASSESSMENT, mode,
 							assessmentModeManager.getAssessedIdentityKeys(mode));
 				} else {
-					mode = ensureStatusOfMode(mode, Status.end);
+					mode = ensureBothStatusOfMode(mode, Status.end, EndStatus.all);
 					sendEvent(AssessmentModeNotificationEvent.END, mode,
 							assessmentModeManager.getAssessedIdentityKeys(mode));
 				}
 			} else if(mode.getEndWithFollowupTime().compareTo(now) <= 0) {
-				mode = ensureStatusOfMode(mode, Status.end);
+				mode = ensureBothStatusOfMode(mode, Status.end, EndStatus.all);
 				sendEvent(AssessmentModeNotificationEvent.END, mode,
 						assessmentModeManager.getAssessedIdentityKeys(mode));
 			}
@@ -321,6 +316,38 @@ public class AssessmentModeCoordinationServiceImpl implements AssessmentModeCoor
 		return mode;
 	}
 	
+	private void sendEventForManualMode(AssessmentMode mode, Date now) {
+		//what to do in manual mode
+		if(mode.getStatus() == Status.assessment) {
+			sendEvent(AssessmentModeNotificationEvent.START_ASSESSMENT, mode,
+					assessmentModeManager.getAssessedIdentityKeys(mode));
+		} else if(mode.getStatus() == Status.followup) {
+			// remind user with compensation for disadvantage
+			if(mode.getEndStatus() == EndStatus.withoutDisadvantage) {
+				Set<Long> keys = getAssessedIdentitiesWithDisadvantageCompensations(mode);
+				sendEvent(AssessmentModeNotificationEvent.START_ASSESSMENT, mode, keys);
+			}
+			
+			// follow-up is ended
+			if(mode.getEndWithFollowupTime().compareTo(now) < 0) {
+				Set<Long> keys = getAssessedIdentitiesWithDisadvantageCompensations(mode);
+				if(keys.isEmpty() || mode.getEndStatus() == EndStatus.all) {
+					mode = ensureBothStatusOfMode(mode, Status.end, EndStatus.all);
+					sendEvent(AssessmentModeNotificationEvent.END, mode,
+						assessmentModeManager.getAssessedIdentityKeys(mode));
+				} else {
+					mode = ensureBothStatusOfMode(mode, Status.end, EndStatus.withoutDisadvantage);
+					Set<Long> identitiesToNotify = assessmentModeManager.getAssessedIdentityKeys(mode);
+					identitiesToNotify.removeAll(keys);
+					sendEvent(AssessmentModeNotificationEvent.END, mode, identitiesToNotify);
+				}
+			}
+		} else if(mode.getStatus() == Status.end && mode.getEndStatus() == EndStatus.withoutDisadvantage) {
+			Set<Long> keys = getAssessedIdentitiesWithDisadvantageCompensations(mode);
+			sendEvent(AssessmentModeNotificationEvent.START_ASSESSMENT, mode, keys);
+		}
+	}
+	
 	private AssessmentMode ensureStatusOfMode(AssessmentMode mode, Status status) {
 		Status currentStatus = mode.getStatus();
 		if(currentStatus == null || currentStatus != status) {
@@ -337,6 +364,24 @@ public class AssessmentModeCoordinationServiceImpl implements AssessmentModeCoor
 		return mode;
 	}
 	
+	private AssessmentMode ensureBothStatusOfMode(AssessmentMode mode, Status status, EndStatus endStatus) {
+		Status currentStatus = mode.getStatus();
+		EndStatus currentEndStatus = mode.getEndStatus();
+		if(currentStatus == null || currentStatus != status || currentEndStatus != endStatus) {
+			mode.setStatus(status);
+			mode.setEndStatus(endStatus);
+			if(status == Status.assessment && mode.isManualBeginEnd()) {
+				syncBeginEndDate(mode);
+			}
+			mode = dbInstance.getCurrentEntityManager().merge(mode);
+			if(status == Status.leadtime || status == Status.assessment) {
+				warmUpAssessment(mode);
+			}
+			dbInstance.commit();
+		}
+		return mode;
+	}
+	
 	private void syncBeginEndDate(AssessmentMode mode) {
 		Date programmedBegin = mode.getBegin();
 		
@@ -388,7 +433,9 @@ public class AssessmentModeCoordinationServiceImpl implements AssessmentModeCoor
 	public boolean canStop(AssessmentMode assessmentMode) {
 		boolean canStop;
 		Status status = assessmentMode.getStatus();
-		if(status == Status.leadtime || status == Status.assessment) {
+		EndStatus endStatus = assessmentMode.getEndStatus();
+		if(status == Status.leadtime || status == Status.assessment
+				|| (endStatus == EndStatus.withoutDisadvantage && (status == Status.followup || status == Status.end))) {
 			canStop = true;
 		} else {
 			canStop = false;
@@ -396,6 +443,13 @@ public class AssessmentModeCoordinationServiceImpl implements AssessmentModeCoor
 		return canStop;
 	}
 
+	@Override
+	public boolean isDisadvantageCompensationExtensionTime(AssessmentMode assessmentMode) {
+		Status status = assessmentMode.getStatus();
+		EndStatus endStatus = assessmentMode.getEndStatus();
+		return endStatus == EndStatus.withoutDisadvantage && (status == Status.followup || status == Status.end);
+	}
+
 	@Override
 	public AssessmentMode startAssessment(AssessmentMode mode) {
 		mode = assessmentModeManager.getAssessmentModeById(mode.getKey());
@@ -406,32 +460,75 @@ public class AssessmentModeCoordinationServiceImpl implements AssessmentModeCoor
 	}
 
 	@Override
-	public AssessmentMode stopAssessment(AssessmentMode mode) {
+	public AssessmentMode stopAssessment(AssessmentMode mode, boolean pullTestSessions, boolean withDisadvantaged) {
 		mode = assessmentModeManager.getAssessmentModeById(mode.getKey());
-		Set<Long> assessedIdentityKeys = assessmentModeManager.getAssessedIdentityKeys(mode);
+		
+		EndStatus endStatus;
+		Set<Long> assessedIdentityKeys;
+		if(isDisadvantageCompensationExtensionTime(mode)) {
+			endStatus = withDisadvantaged ?  EndStatus.all : EndStatus.withoutDisadvantage;
+			assessedIdentityKeys = getAssessedIdentitiesWithDisadvantageCompensations(mode);
+		} else {
+			assessedIdentityKeys = assessmentModeManager.getAssessedIdentityKeys(mode);
+		
+			boolean partial = false;
+			if(!withDisadvantaged) {
+				List<IdentityRef> disadvantagedIdentities = disadvantageCompensationDao
+						.getActiveDisadvantagedUsers(mode.getRepositoryEntry(), mode.getElementAsList());
+				for(IdentityRef disadvantagedIdentity:disadvantagedIdentities) {
+					if(assessedIdentityKeys.remove(disadvantagedIdentity.getKey())) {
+						partial |= true;
+					}
+				}
+			}
+			
+			if(assessedIdentityKeys.contains(252744713l)) {
+				System.out.println();
+			}
+			
+			endStatus = partial ? EndStatus.withoutDisadvantage : EndStatus.all;
+		}
+		
+		return stopAssessment(mode, assessedIdentityKeys, endStatus);
+	}
+	
+	private AssessmentMode stopAssessment(AssessmentMode mode, Set<Long> assessedIdentityKeys, EndStatus endStatus) {
+		String cmd;
 		if(mode.getFollowupTime() > 0) {
-			Date now = new Date();
-			Date followupTime = assessmentModeManager.evaluateFollowupTime(now, mode.getFollowupTime());
-			((AssessmentModeImpl)mode).setEnd(now);
-			((AssessmentModeImpl)mode).setEndWithFollowupTime(followupTime);
-			mode.setStatus(Status.followup);
+			if(mode.getStatus() != Status.followup || mode.getStatus() != Status.end) {
+				Date now = new Date();
+				Date followupTime = assessmentModeManager.evaluateFollowupTime(now, mode.getFollowupTime());
+				((AssessmentModeImpl)mode).setEnd(now);
+				((AssessmentModeImpl)mode).setEndWithFollowupTime(followupTime);
+				mode.setStatus(Status.followup);
+			}
+			mode.setEndStatus(endStatus);
 			mode = dbInstance.getCurrentEntityManager().merge(mode);
-			dbInstance.commit();
-			sendEvent(AssessmentModeNotificationEvent.STOP_ASSESSMENT, mode, assessedIdentityKeys);
+			cmd = AssessmentModeNotificationEvent.STOP_ASSESSMENT;
 		} else {
-			mode = ensureStatusOfMode(mode, Status.end);
-			sendEvent(AssessmentModeNotificationEvent.END, mode, assessedIdentityKeys);
+			mode = ensureBothStatusOfMode(mode, Status.end, endStatus);
+			cmd = AssessmentModeNotificationEvent.END;
 		}
+		dbInstance.commit();
+		sendEvent(cmd, mode, assessedIdentityKeys);
 		return mode;
 	}
 	
+	/**
+	 * This force the end of an assessment for all users.
+	 * 
+	 * @param mode The assessment to end
+	 * @return The merged assessment mode
+	 */
 	private AssessmentMode endAssessment(AssessmentMode mode) {
 		mode = assessmentModeManager.getAssessmentModeById(mode.getKey());
 		Set<Long> assessedIdentityKeys = assessmentModeManager.getAssessedIdentityKeys(mode);
-		mode = ensureStatusOfMode(mode, Status.end);
+		mode = ensureBothStatusOfMode(mode, Status.end, EndStatus.all);
 		Date now = new Date();
 		((AssessmentModeImpl)mode).setEnd(now);
 		((AssessmentModeImpl)mode).setEndWithFollowupTime(now);
+		mode = dbInstance.getCurrentEntityManager().merge(mode);
+		dbInstance.commit();
 		sendEvent(AssessmentModeNotificationEvent.END, mode, assessedIdentityKeys);
 		return mode;
 	}
@@ -440,6 +537,14 @@ public class AssessmentModeCoordinationServiceImpl implements AssessmentModeCoor
 		RepositoryEntry entry = repositoryEntryDao.loadByKey(mode.getRepositoryEntry().getKey());
 		CourseFactory.loadCourse(entry);
 	}
+	
+	private Set<Long> getAssessedIdentitiesWithDisadvantageCompensations(AssessmentMode assessmentMode) {
+		List<IdentityRef> identities = disadvantageCompensationDao
+				.getActiveDisadvantagedUsers(assessmentMode.getRepositoryEntry(), assessmentMode.getElementAsList());
+		return identities.stream()
+				.map(IdentityRef::getKey)
+				.collect(Collectors.toSet());
+	}
 
 	@Override
 	public AssessmentModeStatistics getStatistics(AssessmentMode assessmentMode) {
diff --git a/src/main/java/org/olat/course/assessment/manager/AssessmentModeDAO.java b/src/main/java/org/olat/course/assessment/manager/AssessmentModeDAO.java
index f91316796a8..c11fc58c526 100644
--- a/src/main/java/org/olat/course/assessment/manager/AssessmentModeDAO.java
+++ b/src/main/java/org/olat/course/assessment/manager/AssessmentModeDAO.java
@@ -35,8 +35,10 @@ import org.olat.basesecurity.GroupRoles;
 import org.olat.basesecurity.IdentityRef;
 import org.olat.core.commons.persistence.DB;
 import org.olat.core.commons.persistence.PersistenceHelper;
+import org.olat.core.commons.persistence.QueryBuilder;
 import org.olat.core.util.StringHelper;
 import org.olat.course.assessment.AssessmentMode;
+import org.olat.course.assessment.AssessmentMode.EndStatus;
 import org.olat.course.assessment.AssessmentMode.Status;
 import org.olat.course.assessment.model.AssessmentModeImpl;
 import org.olat.course.assessment.model.SearchAssessmentModeParams;
@@ -181,12 +183,12 @@ public class AssessmentModeDAO {
 		cal.set(Calendar.MILLISECOND, 0);
 		cal.set(Calendar.SECOND, 0);
 		
-		StringBuilder sb = new StringBuilder();
+		QueryBuilder sb = new QueryBuilder();
 		sb.append("select mode from courseassessmentmode mode where ")
 		  .append(" (mode.beginWithLeadTime<=:now and mode.endWithFollowupTime>=:now")
 		  .append("   and (mode.manualBeginEnd=false or (mode.manualBeginEnd=true and mode.leadTime>0)))")
-		  .append(" or mode.statusString in ('").append(Status.leadtime.name()).append("','")
-		  .append(Status.assessment.name()).append("','").append(Status.followup.name()).append("')");
+		  .append(" or mode.statusString ").in(Status.leadtime, Status.assessment, Status.followup)
+		  .append(" or (mode.statusString ").in(Status.end.name()).append(" and mode.endStatusString ").in(EndStatus.withoutDisadvantage).append(")");
 
 		return dbInstance.getCurrentEntityManager()
 				.createQuery(sb.toString(), AssessmentMode.class)
@@ -200,13 +202,13 @@ public class AssessmentModeDAO {
 		cal.set(Calendar.MILLISECOND, 0);
 		cal.set(Calendar.SECOND, 0);
 		
-		StringBuilder sb = new StringBuilder();
+		QueryBuilder sb = new QueryBuilder();
 		sb.append("select count(mode) from courseassessmentmode mode where ")
 		  .append(" mode.repositoryEntry.key=:repoKey and (")
 		  .append(" (mode.beginWithLeadTime<=:now and mode.endWithFollowupTime>=:now ")
 		  .append("   and (mode.manualBeginEnd=false or (mode.manualBeginEnd=true and mode.leadTime>0)))")
-		  .append(" or mode.statusString in ('").append(Status.leadtime.name()).append("','")
-		  .append(Status.assessment.name()).append("','").append(Status.followup.name()).append("'))");
+		  .append(" or mode.statusString ").in(Status.leadtime, Status.assessment, Status.followup)
+		  .append(" or (mode.statusString ").in(Status.end.name()).append(" and mode.endStatusString ").in(EndStatus.withoutDisadvantage).append("))");
 
 		List<Number> count = dbInstance.getCurrentEntityManager()
 				.createQuery(sb.toString(), Number.class)
@@ -222,14 +224,14 @@ public class AssessmentModeDAO {
 		cal.set(Calendar.MILLISECOND, 0);
 		cal.set(Calendar.SECOND, 0);
 		
-		StringBuilder sb = new StringBuilder(512);
+		QueryBuilder sb = new QueryBuilder();
 		sb.append("select mode from courseassessmentmode mode")
 		  .append(" left join fetch mode.lectureBlock block")
 		  .append(" where mode.repositoryEntry.key=:repoKey and (")
 		  .append(" (mode.beginWithLeadTime<=:now and mode.endWithFollowupTime>=:now")
 		  .append("   and (mode.manualBeginEnd=false or (mode.manualBeginEnd=true and mode.leadTime>0)))")
-		  .append(" or mode.statusString in ('").append(Status.leadtime.name()).append("','")
-		  .append(Status.assessment.name()).append("','").append(Status.followup.name()).append("'))");
+		  .append(" or mode.statusString ").in(Status.leadtime, Status.assessment, Status.followup)
+		  .append(" or (mode.statusString ").in(Status.end.name()).append(" and mode.endStatusString ").in(EndStatus.withoutDisadvantage).append("))");
 
 		return dbInstance.getCurrentEntityManager()
 				.createQuery(sb.toString(), AssessmentMode.class)
@@ -285,12 +287,13 @@ public class AssessmentModeDAO {
 	public boolean isNodeInUse(RepositoryEntryRef entry, CourseNode node) {
 		if(entry == null || node == null) return false;
 		
-		StringBuilder sb = new StringBuilder();
+		QueryBuilder sb = new QueryBuilder();
 		sb.append("select count(mode) from courseassessmentmode mode where ")
 		  .append(" mode.repositoryEntry.key=:repoKey ")
 		  .append(" and (mode.startElement=:startIdent or mode.elementList like :nodeIdent)")
 		  .append(" and (mode.beginWithLeadTime>=:now")
-		  .append(" or mode.statusString in ('").append(Status.none.name()).append("','").append(Status.leadtime.name()).append("','").append(Status.assessment.name()).append("','").append(Status.followup.name()).append("'))");
+		  .append(" or mode.statusString ").in(Status.none, Status.leadtime, Status.assessment, Status.followup)
+		  .append(" or mod.endStatusString ").in(EndStatus.withoutDisadvantage).append(")");
 
 		Calendar cal = Calendar.getInstance();
 		cal.set(Calendar.MILLISECOND, 0);
diff --git a/src/main/java/org/olat/course/assessment/manager/AssessmentModeManagerImpl.java b/src/main/java/org/olat/course/assessment/manager/AssessmentModeManagerImpl.java
index c4c6fdeb72b..d87b2221f5b 100644
--- a/src/main/java/org/olat/course/assessment/manager/AssessmentModeManagerImpl.java
+++ b/src/main/java/org/olat/course/assessment/manager/AssessmentModeManagerImpl.java
@@ -63,6 +63,7 @@ import org.olat.group.model.SearchBusinessGroupParams;
 import org.olat.modules.curriculum.CurriculumElement;
 import org.olat.modules.curriculum.CurriculumElementRef;
 import org.olat.modules.curriculum.manager.CurriculumElementDAO;
+import org.olat.modules.dcompensation.manager.DisadvantageCompensationDAO;
 import org.olat.modules.lecture.LectureBlock;
 import org.olat.modules.lecture.manager.LectureBlockToGroupDAO;
 import org.olat.repository.RepositoryEntry;
@@ -100,6 +101,8 @@ public class AssessmentModeManagerImpl implements AssessmentModeManager {
 	@Autowired
 	private LectureBlockToGroupDAO lectureBlockToGroupDao;
 	@Autowired
+	private DisadvantageCompensationDAO disadvantageCompensationDao;
+	@Autowired
 	private AssessmentModeCoordinationServiceImpl assessmentModeCoordinationService;
 
 	@Override
@@ -362,10 +365,26 @@ public class AssessmentModeManagerImpl implements AssessmentModeManager {
 		List<AssessmentMode> myModes = null;
 		if(!currentModes.isEmpty()) {
 			//check permissions, groups, areas, course
-			myModes = assessmentModeDao.loadAssessmentModeFor(identity, currentModes);
+			List<AssessmentMode> allMyModes = assessmentModeDao.loadAssessmentModeFor(identity, currentModes);
+
+			myModes = new ArrayList<>(allMyModes.size());
+			for(AssessmentMode mode:allMyModes) {
+				if(assessmentModeCoordinationService.isDisadvantageCompensationExtensionTime(mode)) {
+					if(isDisadvantagedUser(mode, identity)) {
+						myModes.add(mode);
+					}
+				} else {
+					myModes.add(mode);
+				}
+			}
 		}
 		return myModes == null ? Collections.<AssessmentMode>emptyList() : myModes;
 	}
+	
+	private boolean isDisadvantagedUser(AssessmentMode mode, IdentityRef identity) {
+		return disadvantageCompensationDao
+				.isActiveDisadvantagedUser(identity, mode.getRepositoryEntry(), mode.getElementAsList());
+	}
 
 	@Override
 	public Set<Long> getAssessedIdentityKeys(AssessmentMode assessmentMode) {
diff --git a/src/main/java/org/olat/course/assessment/model/AssessmentModeImpl.java b/src/main/java/org/olat/course/assessment/model/AssessmentModeImpl.java
index 3484b356496..8fdc80147da 100644
--- a/src/main/java/org/olat/course/assessment/model/AssessmentModeImpl.java
+++ b/src/main/java/org/olat/course/assessment/model/AssessmentModeImpl.java
@@ -19,8 +19,10 @@
  */
 package org.olat.course.assessment.model;
 
+import java.util.Arrays;
 import java.util.Date;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 import javax.persistence.CascadeType;
@@ -36,6 +38,7 @@ import javax.persistence.OneToMany;
 import javax.persistence.Table;
 import javax.persistence.Temporal;
 import javax.persistence.TemporalType;
+import javax.persistence.Transient;
 
 import org.hibernate.annotations.GenericGenerator;
 import org.hibernate.annotations.Parameter;
@@ -90,6 +93,8 @@ public class AssessmentModeImpl implements Persistable, AssessmentMode {
 
 	@Column(name="a_status", nullable=true, insertable=true, updatable=true)
 	private String statusString;
+	@Column(name="a_end_status", nullable=true, insertable=true, updatable=true)
+	private String endStatusString;
 
 	@Temporal(TemporalType.TIMESTAMP)
 	@Column(name="a_begin", nullable=false, insertable=true, updatable=true)
@@ -207,18 +212,48 @@ public class AssessmentModeImpl implements Persistable, AssessmentMode {
 
 	@Override
 	public Status getStatus() {
-		return StringHelper.containsNonWhitespace(statusString) ? Status.valueOf(statusString) : Status.none;
+		return StringHelper.containsNonWhitespace(getStatusString()) ? Status.valueOf(getStatusString()) : Status.none;
 	}
 
 	@Override
 	public void setStatus(Status status) {
 		if(status == null) {
-			statusString = Status.none.name();
+			setStatusString(Status.none.name());
 		} else {
-			statusString = status.name();
+			setStatusString(status.name());
 		}
 	}
 
+	public String getStatusString() {
+		return statusString;
+	}
+
+	public void setStatusString(String statusString) {
+		this.statusString = statusString;
+	}
+
+	@Override
+	public EndStatus getEndStatus() {
+		return StringHelper.containsNonWhitespace(getEndStatusString()) ? EndStatus.valueOf(getEndStatusString()) : null;
+	}
+
+	@Override
+	public void setEndStatus(EndStatus status) {
+		if(status == null) {
+			setEndStatusString(null);
+		} else {
+			setEndStatusString(status.name());	
+		}
+	}
+
+	public String getEndStatusString() {
+		return endStatusString;
+	}
+
+	public void setEndStatusString(String endStatusString) {
+		this.endStatusString = endStatusString;
+	}
+
 	@Override
 	public boolean isManualBeginEnd() {
 		return manualBeginEnd;
@@ -357,6 +392,13 @@ public class AssessmentModeImpl implements Persistable, AssessmentMode {
 	public void setElementList(String elementList) {
 		this.elementList = elementList;
 	}
+	
+	@Override
+	@Transient
+	public List<String> getElementAsList() {
+		String nodes = getElementList();
+		return StringHelper.containsNonWhitespace(nodes) ? Arrays.asList(nodes.split("[,]")) : null;
+	}
 
 	@Override
 	public String getStartElement() {
diff --git a/src/main/java/org/olat/course/assessment/model/EnhancedStatus.java b/src/main/java/org/olat/course/assessment/model/EnhancedStatus.java
index 583e046ebc0..9f454eaaba6 100644
--- a/src/main/java/org/olat/course/assessment/model/EnhancedStatus.java
+++ b/src/main/java/org/olat/course/assessment/model/EnhancedStatus.java
@@ -21,6 +21,7 @@ package org.olat.course.assessment.model;
 
 import java.util.List;
 
+import org.olat.course.assessment.AssessmentMode.EndStatus;
 import org.olat.course.assessment.AssessmentMode.Status;
 
 /**
@@ -32,11 +33,13 @@ import org.olat.course.assessment.AssessmentMode.Status;
 public class EnhancedStatus {
 	
 	private final Status status;
+	private final EndStatus endStatus;
 	private final List<String> warnings;
 	
-	public EnhancedStatus(Status status, List<String> warnings) {
+	public EnhancedStatus(Status status, EndStatus endStatus, List<String> warnings) {
 		this.status = status;
 		this.warnings = warnings;
+		this.endStatus = endStatus;
 	}
 
 	public List<String> getWarnings() {
@@ -46,5 +49,8 @@ public class EnhancedStatus {
 	public Status getStatus() {
 		return status;
 	}
-
+	
+	public EndStatus getEndStatus() {
+		return endStatus;
+	}
 }
diff --git a/src/main/java/org/olat/course/assessment/model/TransientAssessmentMode.java b/src/main/java/org/olat/course/assessment/model/TransientAssessmentMode.java
index 27b6217fe9d..a306a8a5551 100644
--- a/src/main/java/org/olat/course/assessment/model/TransientAssessmentMode.java
+++ b/src/main/java/org/olat/course/assessment/model/TransientAssessmentMode.java
@@ -27,6 +27,7 @@ import java.util.List;
 import org.olat.core.id.OLATResourceable;
 import org.olat.core.util.resource.OresHelper;
 import org.olat.course.assessment.AssessmentMode;
+import org.olat.course.assessment.AssessmentMode.EndStatus;
 import org.olat.course.assessment.AssessmentMode.Status;
 
 /**
@@ -52,10 +53,12 @@ public class TransientAssessmentMode implements Serializable {
 	private int leadTime;
 	private int followupTime;
 	private String startElementKey;
+	private List<String> elementList;
 	
-	private boolean manual;
+	private final boolean manual;
 	
-	private Status status;
+	private final Status status;
+	private final EndStatus endStatus;
 	
 	private String ipList;
 	private String safeExamBrowserKey;
@@ -77,10 +80,12 @@ public class TransientAssessmentMode implements Serializable {
 		leadTime = mode.getLeadTime();
 		followupTime = mode.getFollowupTime();
 		startElementKey = mode.getStartElement();
+		elementList = mode.getElementAsList();
 		
 		manual = mode.isManualBeginEnd();
 		
 		status = mode.getStatus();
+		endStatus = mode.getEndStatus();
 
 		if(mode.isRestrictAccessIps()) {
 			ipList = mode.getIpList();
@@ -139,6 +144,10 @@ public class TransientAssessmentMode implements Serializable {
 	public Status getStatus() {
 		return status;
 	}
+	
+	public EndStatus getEndStatus() {
+		return endStatus;
+	}
 
 	public Date getBegin() {
 		return begin;
@@ -180,6 +189,10 @@ public class TransientAssessmentMode implements Serializable {
 		return startElementKey;
 	}
 	
+	public List<String> getElementList() {
+		return elementList;
+	}
+	
 	public String getIpList() {
 		return ipList;
 	}
diff --git a/src/main/java/org/olat/course/assessment/ui/mode/AssessmentModeGuardController.java b/src/main/java/org/olat/course/assessment/ui/mode/AssessmentModeGuardController.java
index c1daecb11d3..35f64069143 100644
--- a/src/main/java/org/olat/course/assessment/ui/mode/AssessmentModeGuardController.java
+++ b/src/main/java/org/olat/course/assessment/ui/mode/AssessmentModeGuardController.java
@@ -45,11 +45,14 @@ import org.olat.core.util.Formatter;
 import org.olat.core.util.StringHelper;
 import org.olat.core.util.coordinate.CoordinatorManager;
 import org.olat.core.util.event.GenericEventListener;
+import org.olat.course.assessment.AssessmentMode.EndStatus;
 import org.olat.course.assessment.AssessmentMode.Status;
 import org.olat.course.assessment.AssessmentModeCoordinationService;
 import org.olat.course.assessment.AssessmentModeManager;
 import org.olat.course.assessment.AssessmentModeNotificationEvent;
 import org.olat.course.assessment.model.TransientAssessmentMode;
+import org.olat.modules.dcompensation.DisadvantageCompensationService;
+import org.olat.repository.model.RepositoryEntryRefImpl;
 import org.springframework.beans.factory.annotation.Autowired;
 
 /**
@@ -74,6 +77,8 @@ public class AssessmentModeGuardController extends BasicController implements Ge
 	@Autowired
 	private AssessmentModeManager assessmentModeMgr;
 	@Autowired
+	private DisadvantageCompensationService disadvantageCompensationService;
+	@Autowired
 	private AssessmentModeCoordinationService assessmentModeCoordinationService;
 	
 	/**
@@ -159,7 +164,7 @@ public class AssessmentModeGuardController extends BasicController implements Ge
 		Date beginWithLeadTime = mode.getBeginWithLeadTime();
 		Date endWithFollowupTime = mode.getEndWithFollowupTime();
 		//check if the mode must not be guarded anymore
-		if(mode.isManual() && (Status.end.equals(mode.getStatus()) || Status.none.equals(mode.getStatus()))) {
+		if(mode.isManual() && ((Status.end.equals(mode.getStatus()) && EndStatus.all.equals(mode.getEndStatus())) || Status.none.equals(mode.getStatus()))) {
 			return null;
 		} else if(!mode.isManual() && (beginWithLeadTime.after(now) || now.after(endWithFollowupTime))) {
 			return null;
@@ -214,79 +219,97 @@ public class AssessmentModeGuardController extends BasicController implements Ge
 	private String updateButtons(TransientAssessmentMode mode, Date now, Link go, Link cont) {
 		String state;
 		if(mode.isManual()) {
-			if(Status.leadtime == mode.getStatus()) {
-				state = Status.leadtime.name();
-				go.setEnabled(false);
-				go.setVisible(true);
-				cont.setEnabled(false);
-				cont.setVisible(false);
-			} else if(Status.assessment == mode.getStatus()) {
-				state = Status.assessment.name();
-				go.setEnabled(true);
-				go.setVisible(true);
-				cont.setEnabled(false);
-				cont.setVisible(false);
-			} else if(Status.followup == mode.getStatus()) {
-				state = Status.followup.name();
-				go.setEnabled(false);
-				go.setVisible(false);
-				cont.setEnabled(false);
-				cont.setVisible(false);
-			} else if(Status.end == mode.getStatus()) {
-				state = Status.end.name();
-				go.setEnabled(false);
-				go.setVisible(false);
-				cont.setEnabled(true);
-				cont.setVisible(true);
-			} else {
-				state = "error";
-				go.setEnabled(false);
-				go.setVisible(false);
-				cont.setEnabled(false);
-				cont.setVisible(false);
-			}
+			state = updateButtonsManual(mode, go, cont);
 		} else {
-
-			Date begin = mode.getBegin();
-			Date beginWithLeadTime = mode.getBeginWithLeadTime();
-			Date end = mode.getEnd();
-			Date endWithLeadTime = mode.getEndWithFollowupTime();
-			
-			if(beginWithLeadTime.compareTo(now) <= 0 && begin.compareTo(now) > 0) {
-				state = Status.leadtime.name();
-				go.setEnabled(false);
-				go.setVisible(true);
-				cont.setEnabled(false);
-				cont.setVisible(false);
-			} else if(begin.compareTo(now) <= 0 && end.compareTo(now) > 0) {
-				state = Status.assessment.name();
-				go.setEnabled(true);
-				go.setVisible(true);
-				cont.setEnabled(false);
-				cont.setVisible(false);
-			} else if(end.compareTo(now) <= 0 && endWithLeadTime.compareTo(now) > 0) {
-				state = Status.followup.name();
-				go.setEnabled(false);
-				go.setVisible(false);
-				cont.setEnabled(false);
-				cont.setVisible(false);
-			} else if(endWithLeadTime.compareTo(now) <= 0 || Status.end == mode.getStatus()) {
-				state = Status.end.name();
-				go.setEnabled(false);
-				go.setVisible(false);
-				cont.setEnabled(true);
-				cont.setVisible(true);
-			} else {
-				state = "error";
-				go.setEnabled(false);
-				go.setVisible(false);
-				cont.setEnabled(false);
-				cont.setVisible(false);
-			}
+			state = updateButtonsAuto(mode, now, go, cont);
+		}
+		return state;
+	}
+	
+	private String updateButtonsManual(TransientAssessmentMode mode, Link go, Link cont) {
+		String state;
+		if(Status.leadtime == mode.getStatus()) {
+			state = Status.leadtime.name();
+			go.setEnabled(false);
+			go.setVisible(true);
+			cont.setEnabled(false);
+			cont.setVisible(false);
+		} else if(Status.assessment == mode.getStatus() || isDisadvantageCompensationExtension(mode)) {
+			state = Status.assessment.name();
+			go.setEnabled(true);
+			go.setVisible(true);
+			cont.setEnabled(false);
+			cont.setVisible(false);
+		} else if(Status.followup == mode.getStatus()) {
+			state = Status.followup.name();
+			go.setEnabled(false);
+			go.setVisible(false);
+			cont.setEnabled(false);
+			cont.setVisible(false);
+		} else if(Status.end == mode.getStatus()) {
+			state = Status.end.name();
+			go.setEnabled(false);
+			go.setVisible(false);
+			cont.setEnabled(true);
+			cont.setVisible(true);
+		} else {
+			state = "error";
+			go.setEnabled(false);
+			go.setVisible(false);
+			cont.setEnabled(false);
+			cont.setVisible(false);
 		}
-		
 		return state;
+	}
+	
+	private boolean isDisadvantageCompensationExtension(TransientAssessmentMode mode) {
+		if(mode.getEndStatus() == EndStatus.withoutDisadvantage
+				&& (mode.getStatus() == Status.followup || mode.getStatus() == Status.end)) {
+			return disadvantageCompensationService.isActiveDisadvantageCompensation(getIdentity(),
+					new RepositoryEntryRefImpl(mode.getRepositoryEntryKey()), mode.getElementList());
+		}
+		return false;
+	}
+	
+	private String updateButtonsAuto(TransientAssessmentMode mode, Date now, Link go, Link cont) {
+		Date begin = mode.getBegin();
+		Date beginWithLeadTime = mode.getBeginWithLeadTime();
+		Date end = mode.getEnd();
+		Date endWithLeadTime = mode.getEndWithFollowupTime();
 		
+		String state;
+		if(beginWithLeadTime.compareTo(now) <= 0 && begin.compareTo(now) > 0) {
+			state = Status.leadtime.name();
+			go.setEnabled(false);
+			go.setVisible(true);
+			cont.setEnabled(false);
+			cont.setVisible(false);
+		} else if(begin.compareTo(now) <= 0 && end.compareTo(now) > 0) {
+			state = Status.assessment.name();
+			go.setEnabled(true);
+			go.setVisible(true);
+			cont.setEnabled(false);
+			cont.setVisible(false);
+		} else if(end.compareTo(now) <= 0 && endWithLeadTime.compareTo(now) > 0) {
+			state = Status.followup.name();
+			go.setEnabled(false);
+			go.setVisible(false);
+			cont.setEnabled(false);
+			cont.setVisible(false);
+		} else if(endWithLeadTime.compareTo(now) <= 0 || Status.end == mode.getStatus()) {
+			state = Status.end.name();
+			go.setEnabled(false);
+			go.setVisible(false);
+			cont.setEnabled(true);
+			cont.setVisible(true);
+		} else {
+			state = "error";
+			go.setEnabled(false);
+			go.setVisible(false);
+			cont.setEnabled(false);
+			cont.setVisible(false);
+		}
+		return state;
 	}
 	
 	private ResourceGuard createGuard(TransientAssessmentMode mode) {
diff --git a/src/main/java/org/olat/course/assessment/ui/mode/AssessmentModeListModel.java b/src/main/java/org/olat/course/assessment/ui/mode/AssessmentModeListModel.java
index b861bfbfd6a..09471208704 100644
--- a/src/main/java/org/olat/course/assessment/ui/mode/AssessmentModeListModel.java
+++ b/src/main/java/org/olat/course/assessment/ui/mode/AssessmentModeListModel.java
@@ -35,7 +35,6 @@ import org.olat.course.CorruptedCourseException;
 import org.olat.course.CourseFactory;
 import org.olat.course.ICourse;
 import org.olat.course.assessment.AssessmentMode;
-import org.olat.course.assessment.AssessmentMode.Status;
 import org.olat.course.assessment.AssessmentModeCoordinationService;
 import org.olat.course.assessment.model.EnhancedStatus;
 import org.olat.course.assessment.model.TransientAssessmentMode;
@@ -50,6 +49,7 @@ import org.olat.course.nodes.CourseNode;
 public class AssessmentModeListModel extends DefaultFlexiTableDataModel<AssessmentMode> implements SortableFlexiTableDataModel<AssessmentMode> {
 	
 	private static final Logger log = Tracing.createLoggerFor(AssessmentModeListModel.class);
+	private static final Cols[] COLS = Cols.values();
 	
 	private final Translator translator;
 	private final AssessmentModeCoordinationService coordinationService;
@@ -74,42 +74,8 @@ public class AssessmentModeListModel extends DefaultFlexiTableDataModel<Assessme
 		
 	@Override
 	public Object getValueAt(AssessmentMode mode, int col) {
-		switch(Cols.values()[col]) {
-			case status: {
-				List<String> warnings = null;
-				Status status = mode.getStatus();
-				try {
-					if(StringHelper.containsNonWhitespace(mode.getStartElement())) {
-						ICourse course = CourseFactory.loadCourse(mode.getRepositoryEntry());
-						CourseNode node = course.getRunStructure().getNode(mode.getStartElement());
-						if(node == null) {
-							warnings = new ArrayList<>(2);
-							warnings.add(translator.translate("warning.missing.start.element"));
-						}
-					}
-					if(StringHelper.containsNonWhitespace(mode.getElementList())) {
-						ICourse course = CourseFactory.loadCourse(mode.getRepositoryEntry());
-						String elements = mode.getElementList();
-						for(String element:elements.split(",")) {
-							CourseNode node = course.getRunStructure().getNode(element);
-							if(node == null) {
-								if(warnings == null) {
-									warnings = new ArrayList<>(2);
-								}
-								warnings.add(translator.translate("warning.missing.element"));
-								break;
-							}
-						}
-					}
-				} catch (CorruptedCourseException e) {
-					log.error("", e);
-					if(warnings == null) {
-						warnings = new ArrayList<>(2);
-					}
-					warnings.add(translator.translate("cif.error.corrupted"));
-				}
-				return new EnhancedStatus(status, warnings);
-			}
+		switch(COLS[col]) {
+			case status: return getStatus(mode);
 			case course: return mode.getRepositoryEntry().getDisplayname();
 			case externalId: return mode.getRepositoryEntry().getExternalId();
 			case externalRef: return mode.getRepositoryEntry().getExternalRef();
@@ -119,22 +85,61 @@ public class AssessmentModeListModel extends DefaultFlexiTableDataModel<Assessme
 			case leadTime: return mode.getLeadTime();
 			case followupTime: return mode.getFollowupTime();
 			case target: return mode.getTargetAudience();
-			case start: {
-				boolean canStart = mode.isManualBeginEnd();
-				if(canStart) {
-					canStart = coordinationService.canStart(mode);
+			case start: return canStart(mode);
+			case stop: return canStop(mode);
+			default: return "ERROR";
+		}
+	}
+	
+	private boolean canStart(AssessmentMode mode) {
+		boolean canStart = mode.isManualBeginEnd();
+		if(canStart) {
+			canStart = coordinationService.canStart(mode);
+		}
+		return canStart;
+	}
+	
+	private boolean canStop(AssessmentMode mode) {
+		boolean canStop = mode.isManualBeginEnd();
+		if(canStop) {
+			canStop = coordinationService.canStop(mode);
+		}
+		return canStop;
+	}
+	
+	private EnhancedStatus getStatus(AssessmentMode mode) {
+		List<String> warnings = null;
+		try {
+			if(StringHelper.containsNonWhitespace(mode.getStartElement())) {
+				ICourse course = CourseFactory.loadCourse(mode.getRepositoryEntry());
+				CourseNode node = course.getRunStructure().getNode(mode.getStartElement());
+				if(node == null) {
+					warnings = new ArrayList<>(2);
+					warnings.add(translator.translate("warning.missing.start.element"));
 				}
-				return canStart;
 			}
-			case stop: {
-				boolean canStop = mode.isManualBeginEnd();
-				if(canStop) {
-					canStop = coordinationService.canStop(mode);
+			if(StringHelper.containsNonWhitespace(mode.getElementList())) {
+				ICourse course = CourseFactory.loadCourse(mode.getRepositoryEntry());
+				String elements = mode.getElementList();
+				for(String element:elements.split(",")) {
+					CourseNode node = course.getRunStructure().getNode(element);
+					if(node == null) {
+						if(warnings == null) {
+							warnings = new ArrayList<>(2);
+						}
+						warnings.add(translator.translate("warning.missing.element"));
+						break;
+					}
 				}
-				return canStop;
 			}
+		} catch (CorruptedCourseException e) {
+			log.error("", e);
+			if(warnings == null) {
+				warnings = new ArrayList<>(2);
+			}
+			warnings.add(translator.translate("cif.error.corrupted"));
 		}
-		return null;
+		return new EnhancedStatus(mode.getStatus(), mode.getEndStatus(), warnings);
 	}
 	
 	@Override
diff --git a/src/main/java/org/olat/course/assessment/ui/mode/ModeStatusCellRenderer.java b/src/main/java/org/olat/course/assessment/ui/mode/ModeStatusCellRenderer.java
index f19b45cce62..8995f01c85f 100644
--- a/src/main/java/org/olat/course/assessment/ui/mode/ModeStatusCellRenderer.java
+++ b/src/main/java/org/olat/course/assessment/ui/mode/ModeStatusCellRenderer.java
@@ -27,6 +27,7 @@ import org.olat.core.gui.render.Renderer;
 import org.olat.core.gui.render.StringOutput;
 import org.olat.core.gui.render.URLBuilder;
 import org.olat.core.gui.translator.Translator;
+import org.olat.course.assessment.AssessmentMode.EndStatus;
 import org.olat.course.assessment.AssessmentMode.Status;
 import org.olat.course.assessment.model.EnhancedStatus;
 
@@ -50,11 +51,11 @@ public class ModeStatusCellRenderer implements FlexiCellRenderer {
 
 		if(cellValue instanceof Status) {
 			Status status = (Status)cellValue;
-			render(status, sb);
+			render(status, null, sb);
 		} else if(cellValue instanceof EnhancedStatus) {
 			EnhancedStatus enStatus = (EnhancedStatus)cellValue;
 			renderWarning(enStatus.getWarnings(), sb);
-			render(enStatus.getStatus(), sb);
+			render(enStatus.getStatus(), enStatus.getEndStatus(), sb);
 		}
 	}
 	
@@ -68,8 +69,10 @@ public class ModeStatusCellRenderer implements FlexiCellRenderer {
 		}
 	}
 
-	private void render(Status status, StringOutput sb) {
+	private void render(Status status, EndStatus endStatus, StringOutput sb) {
 		String title = helper.getStatusLabel(status);
-		sb.append("<span title='").append(title).append("'><i class='o_icon ").append(status.cssClass()).append("'> </i></span>");
+		sb.append("<span title='").append(title).append("'><i class='o_icon ").append(status.cssClass()).append("'> </i>")
+		  .append(" <i class='o_icon o_icon_disadvantage_compensation'> </i>", EndStatus.withoutDisadvantage == endStatus)
+		  .append("</span>");
 	}
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/assessment/ui/tool/AssessmentModeOverviewListController.java b/src/main/java/org/olat/course/assessment/ui/tool/AssessmentModeOverviewListController.java
index 3278988d0f7..3fc9931a415 100644
--- a/src/main/java/org/olat/course/assessment/ui/tool/AssessmentModeOverviewListController.java
+++ b/src/main/java/org/olat/course/assessment/ui/tool/AssessmentModeOverviewListController.java
@@ -200,8 +200,6 @@ public class AssessmentModeOverviewListController extends FormBasicController im
 		boolean endSoon = (endInMillseconds < (5l * 60l * 1000l))
 				&& (mode.getStatus() == Status.assessment || mode.getStatus() == Status.followup);
 		
-		
-		
 		AssessmentModeOverviewRow row = new AssessmentModeOverviewRow(mode, isToday, endSoon, endInMillseconds);
 		
 		LectureBlock block = mode.getLectureBlock();
@@ -223,6 +221,9 @@ public class AssessmentModeOverviewListController extends FormBasicController im
 				FormLink endButton = uifactory.addFormLink(id, "end", "end", null, flc, Link.BUTTON_SMALL);
 				endButton.setDomReplacementWrapperRequired(false);
 				endButton.setIconLeftCSS("o_icon o_icon-fw o_as_mode_stop");
+				if(assessmentModeCoordinationService.isDisadvantageCompensationExtensionTime(mode)) {
+					endButton.setIconRightCSS("o_icon o_icon-fw o_icon_disadvantage_compensation");
+				}
 				endButton.setUserObject(row);
 				flc.add(id, endButton);
 				forgeStatistics(mode, row);
diff --git a/src/main/java/org/olat/course/assessment/ui/tool/AssessmentModeOverviewListTableModel.java b/src/main/java/org/olat/course/assessment/ui/tool/AssessmentModeOverviewListTableModel.java
index b6e7b8d4edb..f1a195b1bbc 100644
--- a/src/main/java/org/olat/course/assessment/ui/tool/AssessmentModeOverviewListTableModel.java
+++ b/src/main/java/org/olat/course/assessment/ui/tool/AssessmentModeOverviewListTableModel.java
@@ -42,7 +42,8 @@ public class AssessmentModeOverviewListTableModel extends DefaultFlexiTableDataM
 	public Object getValueAt(int row, int col) {
 		AssessmentModeOverviewRow mode = getObject(row);
 		switch(ModeCols.values()[col]) {
-			case status: return new EnhancedStatus(mode.getAssessmentMode().getStatus(), Collections.emptyList());
+			case status: return new EnhancedStatus(mode.getAssessmentMode().getStatus(),
+					mode.getAssessmentMode().getEndStatus(), Collections.emptyList());
 			case name: return mode.getAssessmentMode().getName();
 			case begin: return mode.getAssessmentMode().getBegin();
 			case end: return mode.getAssessmentMode().getEnd();
diff --git a/src/main/java/org/olat/course/assessment/ui/tool/AssessmentToolController.java b/src/main/java/org/olat/course/assessment/ui/tool/AssessmentToolController.java
index 1c0e72e89a5..755d06ff5bc 100644
--- a/src/main/java/org/olat/course/assessment/ui/tool/AssessmentToolController.java
+++ b/src/main/java/org/olat/course/assessment/ui/tool/AssessmentToolController.java
@@ -51,6 +51,7 @@ import org.olat.core.util.resource.OresHelper;
 import org.olat.course.CourseFactory;
 import org.olat.course.ICourse;
 import org.olat.course.assessment.AssessmentMode;
+import org.olat.course.assessment.AssessmentModeCoordinationService;
 import org.olat.course.assessment.AssessmentModeManager;
 import org.olat.course.assessment.AssessmentModule;
 import org.olat.course.assessment.CourseAssessmentService;
@@ -104,11 +105,13 @@ public class AssessmentToolController extends MainLayoutBasicController implemen
 	@Autowired
 	private CourseAssessmentService courseAssessmentService;
 	@Autowired
+	private NodeAccessService nodeAccessService;
+	@Autowired
 	private AssessmentService assessmentService;
 	@Autowired
 	private AssessmentModeManager assessmentModeManager;
 	@Autowired
-	private NodeAccessService nodeAccessService;
+	private AssessmentModeCoordinationService assessmentModeCoordinationService;
 	
 	public AssessmentToolController(UserRequest ureq, WindowControl wControl, TooledStackedPanel stackPanel,
 			RepositoryEntry courseEntry, UserCourseEnvironment coachUserEnv, AssessmentToolSecurityCallback assessmentCallback) {
@@ -141,6 +144,9 @@ public class AssessmentToolController extends MainLayoutBasicController implemen
 					String label = translate("assessment.tool.stop", new String[] { StringHelper.escapeHtml(modeName) });
 					stopAssessmentMode = LinkFactory.createCustomLink("assessment.stop", "stop", label, Link.BUTTON_SMALL | Link.NONTRANSLATED, warn, this);
 					stopAssessmentMode.setIconLeftCSS("o_icon o_icon-fw o_as_mode_stop");
+					if(assessmentModeCoordinationService.isDisadvantageCompensationExtensionTime(mode)) {
+						stopAssessmentMode.setIconRightCSS("o_icon o_icon-fw o_icon_disadvantage_compensation");
+					}
 					stopAssessmentMode.setUserObject(mode);
 				}
 			} else {
@@ -193,12 +199,11 @@ public class AssessmentToolController extends MainLayoutBasicController implemen
 	
 	private boolean canStopAssessmentMode(AssessmentMode mode) {
 		if(assessmentCallback.canStartStopAllAssessments()) {
-			return mode.getStatus() == AssessmentMode.Status.assessment || mode.getStatus() == AssessmentMode.Status.leadtime;
+			return assessmentModeCoordinationService.canStop(mode);
 		} else if(mode.getLectureBlock() != null) {
 			List<Identity> teachers = lectureService.getTeachers(mode.getLectureBlock());
 			return teachers.contains(getIdentity())
-					&& (mode.getStatus() == AssessmentMode.Status.assessment
-						|| mode.getStatus() == AssessmentMode.Status.leadtime);
+					&& assessmentModeCoordinationService.canStop(mode);
 		}
 		return false;
 	}
diff --git a/src/main/java/org/olat/course/assessment/ui/tool/_content/confirm_stop.html b/src/main/java/org/olat/course/assessment/ui/tool/_content/confirm_stop.html
index e94e80be276..9b93db40ae1 100644
--- a/src/main/java/org/olat/course/assessment/ui/tool/_content/confirm_stop.html
+++ b/src/main/java/org/olat/course/assessment/ui/tool/_content/confirm_stop.html
@@ -3,6 +3,14 @@
 	<p>$compensationMsg</p>
 #end
 </div>
+<div>
+#if($r.available("disadvantages"))
+	$r.render("disadvantages")
+#end
+#if($r.available("runningSessions"))
+	$r.render("runningSessions")
+#end
+</div>
 <div class="o_button_group">
 	$r.render("cancel")
 	$r.render("stop")
diff --git a/src/main/java/org/olat/course/assessment/ui/tool/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/assessment/ui/tool/_i18n/LocalStrings_de.properties
index a9f160eaa29..ae749b47d79 100644
--- a/src/main/java/org/olat/course/assessment/ui/tool/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/course/assessment/ui/tool/_i18n/LocalStrings_de.properties
@@ -33,10 +33,13 @@ command.previous.node=Zur\u00FCck zum letzten Kursbaustein
 confirm.certificate.deleted=$org.olat.course.certificate.ui\:confirm.certificate.deleted
 confirm.delete.certificate.text=$org.olat.course.certificate.ui\:confirm.delete.certificate.text
 confirm.delete.certificate.title=$org.olat.course.certificate.ui\:confirm.delete.certificate.title
+confirm.stop.pull.running.sessions=Den Tests automatisch ziehen.
 confirm.stop.title=Pr\u00FCfung beenden
 confirm.stop.text.compensations=Es gibt <strong>ein Person</strong> mit Nachteilausgleich.
 confirm.stop.text.compensations.plural=Es gibt <strong>{0} Personen</strong> mit Nachteilausgleich.
 confirm.stop.text.details=Wollen Sie die Pr\u00FCfung "{0}" jetzt beenden?
+confirm.stop.final.text.details=Wollen Sie die Pr\u00FCfung "{0}" jetzt beenden? Es betrifft noch die Personen  mit Nachteilausgleich.
+confirm.stop.with.disadvantages=Pr\u00FCfung f\u00FCr Personen mit Nachteilausgleich auch beenden.
 elements.to.review=<i class\="o_icon o_icon_warning"> </i> {0} unerledigt
 end=Beenden
 filter=$org.olat.modules.assessment.ui\:filter
diff --git a/src/main/java/org/olat/course/assessment/ui/tool/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/course/assessment/ui/tool/_i18n/LocalStrings_en.properties
index 3cb4bf42d63..d4749674d7e 100644
--- a/src/main/java/org/olat/course/assessment/ui/tool/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/course/assessment/ui/tool/_i18n/LocalStrings_en.properties
@@ -33,10 +33,13 @@ command.previous.node=Back to previous course element
 confirm.certificate.deleted=$org.olat.course.certificate.ui\:confirm.certificate.deleted
 confirm.delete.certificate.text=$org.olat.course.certificate.ui\:confirm.delete.certificate.text
 confirm.delete.certificate.title=$org.olat.course.certificate.ui\:confirm.delete.certificate.title
+confirm.stop.pull.running.sessions=Pull the tests automatically.
 confirm.stop.text.compensations=There is <strong>a person</strong> with a compensation for disadvantages.
 confirm.stop.text.compensations.plural=There is <strong>{0} persons</strong> with compensations for disadvantages.
 confirm.stop.text.details=Do you want to end the exam "{0}" now?
+confirm.stop.final.text.details=Do you want to end the exam "{0}" for the persons with disadvantage compensations now? 
 confirm.stop.title=End the exam
+confirm.stop.with.disadvantages=End the exam for the persons with a compensation for disadvantages too.
 elements.to.review=<i class\="o_icon o_icon_warning"> </i> {0} pending
 end=End
 filter=$org.olat.modules.assessment.ui\:filter
diff --git a/src/main/java/org/olat/ims/qti21/QTI21Service.java b/src/main/java/org/olat/ims/qti21/QTI21Service.java
index c838a2e6b35..81835ca50d5 100644
--- a/src/main/java/org/olat/ims/qti21/QTI21Service.java
+++ b/src/main/java/org/olat/ims/qti21/QTI21Service.java
@@ -286,6 +286,8 @@ public interface QTI21Service {
 
 	public boolean isRunningAssessmentTestSession(RepositoryEntry entry, String subIdent, RepositoryEntry testEntry, List<? extends IdentityRef> identities);
 	
+	public boolean isRunningAssessmentTestSession(RepositoryEntry entry, List<String> subIdents, List<? extends IdentityRef> identities);
+	
 	/**
 	 * Add some extra time to an assessment test 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 923dcabd0f3..ff74f2f7bb4 100644
--- a/src/main/java/org/olat/ims/qti21/manager/AssessmentTestSessionDAO.java
+++ b/src/main/java/org/olat/ims/qti21/manager/AssessmentTestSessionDAO.java
@@ -556,6 +556,44 @@ public class AssessmentTestSessionDAO {
 		return found != null && !found.isEmpty() && found.get(0) != null && found.get(0) >= 0;
 	}
 	
+	/**
+	 * 
+	 * @param entry The repository entry (typically the course, or the test if not in a course) (mandatory)
+	 * @param courseSubIdent An optional sub-identifier
+	 * @param testEntry The test repository entry
+	 * @param identities The list of assessed identities
+	 * @return true if at least one of the identities has a running test session
+	 */
+	public boolean hasRunningTestSessions(RepositoryEntryRef entry, List<String> courseSubIdents, List<? extends IdentityRef> identities) {
+		StringBuilder sb = new StringBuilder();
+		sb.append("select session.key from qtiassessmenttestsession session")
+		  .append(" left join session.testEntry testEntry")
+		  .append(" left join testEntry.olatResource testResource")
+		  .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<Long> query = dbInstance.getCurrentEntityManager()
+				.createQuery(sb.toString(), Long.class)
+				.setFirstResult(0)
+				.setMaxResults(1)
+				.setParameter("repositoryEntryKey", entry.getKey())
+				.setParameter("identityKeys", identityKeys);		
+		if(courseSubIdents != null && !courseSubIdents.isEmpty()) {
+			query.setParameter("subIdents", courseSubIdents);
+		}
+		List<Long> found = query.getResultList();
+		return found != null && !found.isEmpty() && found.get(0) != null && found.get(0) >= 0;
+	}
+	
 	public int deleteTestSession(AssessmentTestSession testSession) {
 		StringBuilder responseSb  = new StringBuilder();
 		responseSb.append("delete from qtiassessmentresponse response where response.assessmentTestSession.key=:sessionKey");
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 1631bb85cea..93508b8d62e 100644
--- a/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java
+++ b/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java
@@ -660,6 +660,11 @@ public class QTI21ServiceImpl implements QTI21Service, UserDataDeletable, Initia
 	public boolean isRunningAssessmentTestSession(RepositoryEntry entry, String subIdent, RepositoryEntry testEntry, List<? extends IdentityRef> identities) {
 		return testSessionDao.hasRunningTestSessions(entry, subIdent, testEntry, identities);
 	}
+	
+	@Override
+	public boolean isRunningAssessmentTestSession(RepositoryEntry entry, List<String> subIdents, List<? extends IdentityRef> identities) {
+		return testSessionDao.hasRunningTestSessions(entry, subIdents, identities);
+	}
 
 	@Override
 	public List<AssessmentTestSession> getRunningAssessmentTestSession(RepositoryEntry entry, String subIdent, RepositoryEntry testEntry) {
diff --git a/src/main/java/org/olat/modules/contacttracing/ui/ContactTracingReportGeneratorStep2Controller.java b/src/main/java/org/olat/modules/contacttracing/ui/ContactTracingReportGeneratorStep2Controller.java
index 4a301b24379..4a70b2a424d 100644
--- a/src/main/java/org/olat/modules/contacttracing/ui/ContactTracingReportGeneratorStep2Controller.java
+++ b/src/main/java/org/olat/modules/contacttracing/ui/ContactTracingReportGeneratorStep2Controller.java
@@ -31,7 +31,6 @@ import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.generic.wizard.StepFormBasicController;
 import org.olat.core.gui.control.generic.wizard.StepsEvent;
 import org.olat.core.gui.control.generic.wizard.StepsRunContext;
-import org.olat.course.editor.overview.YesNoCellRenderer;
 import org.olat.modules.contacttracing.ContactTracingManager;
 import org.olat.modules.contacttracing.ui.ContactTracingLocationTableModel.ContactTracingLocationCols;
 import org.springframework.beans.factory.annotation.Autowired;
diff --git a/src/main/java/org/olat/modules/dcompensation/DisadvantageCompensationService.java b/src/main/java/org/olat/modules/dcompensation/DisadvantageCompensationService.java
index 744f7ff55ea..d9d88542c52 100644
--- a/src/main/java/org/olat/modules/dcompensation/DisadvantageCompensationService.java
+++ b/src/main/java/org/olat/modules/dcompensation/DisadvantageCompensationService.java
@@ -57,6 +57,8 @@ public interface DisadvantageCompensationService {
 	 */
 	public DisadvantageCompensation getActiveDisadvantageCompensation(IdentityRef identity, RepositoryEntryRef entry, String subIdent);
 	
+	public boolean isActiveDisadvantageCompensation(IdentityRef identity, RepositoryEntryRef entry, List<String> subIdents);
+	
 	/**
 	 * @param entry The course
 	 * @param subIdents A list of course elements (optional, null means all)
diff --git a/src/main/java/org/olat/modules/dcompensation/manager/DisadvantageCompensationDAO.java b/src/main/java/org/olat/modules/dcompensation/manager/DisadvantageCompensationDAO.java
index 76f36f6d227..c0376828eac 100644
--- a/src/main/java/org/olat/modules/dcompensation/manager/DisadvantageCompensationDAO.java
+++ b/src/main/java/org/olat/modules/dcompensation/manager/DisadvantageCompensationDAO.java
@@ -155,4 +155,28 @@ public class DisadvantageCompensationDAO {
 				.map(IdentityRefImpl::new)
 				.collect(Collectors.toList());
 	}
+	
+	public boolean isActiveDisadvantagedUser(IdentityRef identity, RepositoryEntryRef entry, List<String> subIdents) {
+		QueryBuilder sb = new QueryBuilder();
+		sb.append("select compensation.key from dcompensation as compensation")
+		  .append(" where compensation.identity.key=:identityKey")
+		  .append(" and compensation.entry.key=:entryKey and compensation.status=:status");
+		if(subIdents != null && !subIdents.isEmpty()) {
+			sb.append(" and compensation.subIdent in (:subIdent)");
+		}
+		
+		TypedQuery<Long> query = dbInstance.getCurrentEntityManager()
+				.createQuery(sb.toString(), Long.class)
+				.setFirstResult(0)
+				.setMaxResults(1)
+				.setParameter("status", DisadvantageCompensationStatusEnum.active.name())
+				.setParameter("identityKey", identity.getKey())
+				.setParameter("entryKey", entry.getKey());
+		if(subIdents != null && !subIdents.isEmpty()) {
+			query.setParameter("subIdent", subIdents);
+		}
+		
+		List<Long> keys = query.getResultList();
+		return keys != null && !keys.isEmpty() && keys.get(0) != null && keys.get(0).longValue() > 0;
+	}
 }
diff --git a/src/main/java/org/olat/modules/dcompensation/manager/DisadvantageCompensationServiceImpl.java b/src/main/java/org/olat/modules/dcompensation/manager/DisadvantageCompensationServiceImpl.java
index 8200178ef55..191fd425969 100644
--- a/src/main/java/org/olat/modules/dcompensation/manager/DisadvantageCompensationServiceImpl.java
+++ b/src/main/java/org/olat/modules/dcompensation/manager/DisadvantageCompensationServiceImpl.java
@@ -81,6 +81,11 @@ public class DisadvantageCompensationServiceImpl implements DisadvantageCompensa
 		return disadvantageCompensationDao.getActiveDisadvantageCompensation(identity, entry, subIdent);
 	}
 
+	@Override
+	public boolean isActiveDisadvantageCompensation(IdentityRef identity, RepositoryEntryRef entry, List<String> subIdents) {
+		return disadvantageCompensationDao.isActiveDisadvantagedUser(identity, entry, subIdents);
+	}
+
 	@Override
 	public List<IdentityRef> getActiveDisadvantagedUsers(RepositoryEntryRef entry, List<String> subIdents) {
 		return disadvantageCompensationDao.getActiveDisadvantagedUsers(entry, subIdents);
diff --git a/src/main/resources/database/mysql/alter_15_2_x_to_15_3_0.sql b/src/main/resources/database/mysql/alter_15_2_x_to_15_3_0.sql
index cca46347424..e4e3513fbc4 100644
--- a/src/main/resources/database/mysql/alter_15_2_x_to_15_3_0.sql
+++ b/src/main/resources/database/mysql/alter_15_2_x_to_15_3_0.sql
@@ -30,6 +30,7 @@ create unique index idx_de_userinfo_ident_idx on o_de_user_info(fk_identity);
 -- Assessment
 alter table o_as_entry add column a_current_run_start datetime;
 
+alter table o_as_mode_course add column a_end_status varchar(32);
 
 -- Disadvantage compensation
 alter table o_qti_assessmenttest_session add column q_compensation_extra_time bigint;
diff --git a/src/main/resources/database/mysql/setupDatabase.sql b/src/main/resources/database/mysql/setupDatabase.sql
index 121a1d3a8f3..60c1ced1b61 100644
--- a/src/main/resources/database/mysql/setupDatabase.sql
+++ b/src/main/resources/database/mysql/setupDatabase.sql
@@ -1387,6 +1387,7 @@ create table o_as_mode_course (
    a_name varchar(255),
    a_description longtext,
    a_status varchar(16),
+   a_end_status varchar(32),
    a_manual_beginend bit not null default 0,
    a_begin datetime not null,
    a_leadtime bigint not null default 0,
@@ -4290,9 +4291,10 @@ alter table o_ap_appointment add constraint ap_appointment_meeting_idx foreign k
 alter table o_ap_participation add constraint ap_part_appointment_idx foreign key (fk_appointment_id) references o_ap_appointment (id);
 alter table o_ap_participation add constraint ap_part_identity_idx foreign key (fk_identity_id) references o_bs_identity (id);
 
-insert into hibernate_unique_key values ( 0 );
-SET FOREIGN_KEY_CHECKS = 1;
-
 -- Organiation role rights
 alter table o_org_role_to_right add constraint org_role_to_right_to_organisation_idx foreign key (fk_organisation) references o_org_organisation (id);
 create index idx_org_role_to_right_to_organisation_idx on o_org_role_to_right (fk_organisation);
+
+insert into hibernate_unique_key values ( 0 );
+SET FOREIGN_KEY_CHECKS = 1;
+
diff --git a/src/main/resources/database/oracle/alter_15_2_x_to_15_3_0.sql b/src/main/resources/database/oracle/alter_15_2_x_to_15_3_0.sql
index 3d26a1b9f1f..b8857782f37 100644
--- a/src/main/resources/database/oracle/alter_15_2_x_to_15_3_0.sql
+++ b/src/main/resources/database/oracle/alter_15_2_x_to_15_3_0.sql
@@ -27,6 +27,7 @@ create unique index idx_de_userinfo_ident_idx on o_de_user_info(fk_identity);
 -- Assessment
 alter table o_as_entry add a_current_run_start timestamp;
 
+alter table o_as_mode_course add a_end_status varchar(32);
 
 -- Disadvantage compensation
 alter table o_qti_assessmenttest_session add q_compensation_extra_time number(20);
diff --git a/src/main/resources/database/oracle/setupDatabase.sql b/src/main/resources/database/oracle/setupDatabase.sql
index de0f8880725..5a62656d4f1 100644
--- a/src/main/resources/database/oracle/setupDatabase.sql
+++ b/src/main/resources/database/oracle/setupDatabase.sql
@@ -1449,6 +1449,7 @@ create table o_as_mode_course (
    a_name varchar2(255 char),
    a_description clob,
    a_status varchar2(16 char),
+   a_end_status varchar(32),
    a_manual_beginend number default 0 not null,
    a_begin date not null,
    a_leadtime number(20) default 0 not null,
diff --git a/src/main/resources/database/postgresql/alter_15_2_x_to_15_3_0.sql b/src/main/resources/database/postgresql/alter_15_2_x_to_15_3_0.sql
index d3f69ebd400..77ae7f7be77 100644
--- a/src/main/resources/database/postgresql/alter_15_2_x_to_15_3_0.sql
+++ b/src/main/resources/database/postgresql/alter_15_2_x_to_15_3_0.sql
@@ -28,6 +28,7 @@ create unique index idx_de_userinfo_ident_idx on o_de_user_info(fk_identity);
 -- Assessment
 alter table o_as_entry add column a_current_run_start timestamp;
 
+alter table o_as_mode_course add column a_end_status varchar(32);
 
 -- Disadvantage compensation
 alter table o_qti_assessmenttest_session add column q_compensation_extra_time int8;
diff --git a/src/main/resources/database/postgresql/setupDatabase.sql b/src/main/resources/database/postgresql/setupDatabase.sql
index 2d3f7821c51..85f847b3ec7 100644
--- a/src/main/resources/database/postgresql/setupDatabase.sql
+++ b/src/main/resources/database/postgresql/setupDatabase.sql
@@ -1409,6 +1409,7 @@ create table o_as_mode_course (
    a_name varchar(255),
    a_description text,
    a_status varchar(16),
+   a_end_status varchar(32),
    a_manual_beginend bool not null default false,
    a_begin timestamp not null,
    a_leadtime int8 not null default 0,
diff --git a/src/test/java/org/olat/ims/qti21/manager/AssessmentTestSessionDAOTest.java b/src/test/java/org/olat/ims/qti21/manager/AssessmentTestSessionDAOTest.java
index 446185a7e91..48b57dd4f25 100644
--- a/src/test/java/org/olat/ims/qti21/manager/AssessmentTestSessionDAOTest.java
+++ b/src/test/java/org/olat/ims/qti21/manager/AssessmentTestSessionDAOTest.java
@@ -450,6 +450,29 @@ public class AssessmentTestSessionDAOTest extends OlatTestCase {
 		Assert.assertTrue(hasAtLeastOneRunningTestSession);
 	}
 	
+	@Test
+	public void hasRunningTestSessions_subIdentList() {
+		// prepare a test and a user
+		RepositoryEntry testEntry = JunitTestHelper.createAndPersistRepositoryEntry();
+		Identity assessedIdentity = JunitTestHelper.createAndPersistIdentityAsRndUser("session-30");
+		Identity otherIdentity = JunitTestHelper.createAndPersistIdentityAsRndUser("session-31");
+		String subIdent = "OO-1234";
+		AssessmentEntry assessmentEntry = assessmentService.getOrCreateAssessmentEntry(assessedIdentity, null, testEntry, subIdent, Boolean.FALSE, testEntry);
+		dbInstance.commit();
+		
+		//create an assessment test session
+		AssessmentTestSession testSession = testSessionDao.createAndPersistTestSession(testEntry, testEntry, subIdent, assessmentEntry, assessedIdentity, null, null, false);
+		Assert.assertNotNull(testSession);
+		dbInstance.commitAndCloseSession();
+		
+		//check
+		boolean hasRunningTestSessions = testSessionDao.hasRunningTestSessions(testEntry, List.of(subIdent), List.of(assessedIdentity));
+		Assert.assertTrue(hasRunningTestSessions);
+		
+		boolean hasNotRunningTestSessions = testSessionDao.hasRunningTestSessions(testEntry, List.of(subIdent), List.of(otherIdentity));
+		Assert.assertFalse(hasNotRunningTestSessions);
+	}
+	
 	@Test
 	public void getAllUserTestSessions() {
 		// prepare a test and a user
diff --git a/src/test/java/org/olat/modules/dcompensation/manager/DisadvantageCompensationDAOTest.java b/src/test/java/org/olat/modules/dcompensation/manager/DisadvantageCompensationDAOTest.java
index a1cec066069..4d70f26ba7b 100644
--- a/src/test/java/org/olat/modules/dcompensation/manager/DisadvantageCompensationDAOTest.java
+++ b/src/test/java/org/olat/modules/dcompensation/manager/DisadvantageCompensationDAOTest.java
@@ -153,13 +153,35 @@ public class DisadvantageCompensationDAOTest extends OlatTestCase {
 		List<String> subIdents = List.of(subIdent1,  subIdent2);
 		List<IdentityRef> disadvantegdIdentities = disadvantageCompensationDao
 				.getActiveDisadvantagedUsers(entry, subIdents);
-		Assert.assertNotNull(disadvantegdIdentities);
-		Assert.assertEquals(2, disadvantegdIdentities.size());
 		assertThat(disadvantegdIdentities)
+			.isNotNull()
+			.hasSize(2)
 			.extracting(IdentityRef::getKey)
 			.containsExactlyInAnyOrder(identity1.getKey(), identity2.getKey());
 	}
 	
+	@Test
+	public void isActiveDisadvantagedUser() {
+		Identity identity = JunitTestHelper.createAndPersistIdentityAsRndUser("dcompensation-16");
+		Identity creator = JunitTestHelper.createAndPersistIdentityAsRndUser("dcompensation-18");
+		RepositoryEntry entry = JunitTestHelper.createAndPersistRepositoryEntry();
+		String subIdent = UUID.randomUUID().toString();
+		Date approval = DateUtils.addDays(new Date(), -21);
+		
+		DisadvantageCompensation compensation = disadvantageCompensationDao
+				.createDisadvantageCompensation(identity, 15, "Not responsible", approval, creator, entry, subIdent, "Element-1");
+		dbInstance.commitAndCloseSession();
+		Assert.assertNotNull(compensation);
+		
+		boolean disadvantegdCompensations = disadvantageCompensationDao
+				.isActiveDisadvantagedUser(identity, entry, List.of(subIdent));
+		Assert.assertTrue(disadvantegdCompensations);
+		
+		boolean creatorCompensations = disadvantageCompensationDao
+				.isActiveDisadvantagedUser(creator, entry, List.of(subIdent));
+		Assert.assertFalse(creatorCompensations);
+	}
+	
 	@Test
 	public void getDisadvantageCompensationsByIdentityAndEntry() {
 		Identity identity = JunitTestHelper.createAndPersistIdentityAsRndUser("dcompensation-9");
-- 
GitLab