SEBSERV-353 fixed delete with references

This commit is contained in:
anhefti 2023-02-28 10:56:37 +01:00
parent 3026edeb4c
commit b213b0a876
12 changed files with 289 additions and 52 deletions

View file

@ -138,7 +138,7 @@ public abstract class AbstractBatchActionWizard {
pageService, pageService,
formContext, formContext,
multiSelection, multiSelection,
false) false, false)
.build(); .build();
applySelectionList(formContext, multiSelection); applySelectionList(formContext, multiSelection);
@ -181,7 +181,7 @@ public abstract class AbstractBatchActionWizard {
this.pageService, this.pageService,
formContext, formContext,
multiSelection, multiSelection,
true) true, true)
.build(); .build();
this.serverPushService.runServerPush( this.serverPushService.runServerPush(
@ -234,7 +234,8 @@ public abstract class AbstractBatchActionWizard {
final PageService pageService, final PageService pageService,
final PageContext formContext, final PageContext formContext,
final Set<EntityKey> multiSelection, final Set<EntityKey> multiSelection,
final boolean readonly) { final boolean readonly,
final boolean resultPage) {
final FormBuilder formBuilder = pageService final FormBuilder formBuilder = pageService
.formBuilder(formContext) .formBuilder(formContext)
@ -245,7 +246,11 @@ public abstract class AbstractBatchActionWizard {
.readonly(true)); .readonly(true));
buildSpecificFormFields(formContext, formBuilder, readonly); buildSpecificFormFields(formContext, formBuilder, readonly);
return buildProgressFields(formBuilder, readonly); if (resultPage) {
return buildProgressFields(formBuilder, readonly);
} else {
return formBuilder;
}
} }
protected FormBuilder buildProgressFields(final FormBuilder formHead, final boolean readonly) { protected FormBuilder buildProgressFields(final FormBuilder formHead, final boolean readonly) {

View file

@ -13,8 +13,11 @@ import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.api.API.BatchActionType; import ch.ethz.seb.sebserver.gbl.api.API.BatchActionType;
import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage;
@ -27,7 +30,12 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BatchActionExec; import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BatchActionExec;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientGroupDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.IndicatorDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
@ -36,18 +44,32 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
@WebServiceProfile @WebServiceProfile
public class DeleteExamAction implements BatchActionExec { public class DeleteExamAction implements BatchActionExec {
private static final Logger log = LoggerFactory.getLogger(DeleteExamAction.class);
private final ExamDAO examDAO; private final ExamDAO examDAO;
private final ClientConnectionDAO clientConnectionDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final ClientGroupDAO clientGroupDAO;
private final IndicatorDAO indicatorDAO;
private final AuthorizationService authorization; private final AuthorizationService authorization;
private final UserActivityLogDAO userActivityLogDAO; private final UserActivityLogDAO userActivityLogDAO;
private final ExamSessionService examSessionService; private final ExamSessionService examSessionService;
public DeleteExamAction( public DeleteExamAction(
final ExamDAO examDAO, final ExamDAO examDAO,
final ClientConnectionDAO clientConnectionDAO,
final ExamConfigurationMapDAO examConfigurationMapDAO,
final ClientGroupDAO clientGroupDAO,
final IndicatorDAO indicatorDAO,
final AuthorizationService authorization, final AuthorizationService authorization,
final UserActivityLogDAO userActivityLogDAO, final UserActivityLogDAO userActivityLogDAO,
final ExamSessionService examSessionService) { final ExamSessionService examSessionService) {
this.examDAO = examDAO; this.examDAO = examDAO;
this.clientConnectionDAO = clientConnectionDAO;
this.examConfigurationMapDAO = examConfigurationMapDAO;
this.clientGroupDAO = clientGroupDAO;
this.indicatorDAO = indicatorDAO;
this.authorization = authorization; this.authorization = authorization;
this.userActivityLogDAO = userActivityLogDAO; this.userActivityLogDAO = userActivityLogDAO;
this.examSessionService = examSessionService; this.examSessionService = examSessionService;
@ -65,13 +87,40 @@ public class DeleteExamAction implements BatchActionExec {
} }
@Override @Override
@Transactional
public Result<EntityKey> doSingleAction(final String modelId, final BatchAction batchAction) { public Result<EntityKey> doSingleAction(final String modelId, final BatchAction batchAction) {
return this.examDAO.byModelId(modelId) return this.examDAO.byModelId(modelId)
.flatMap(this::checkWriteAccess) .flatMap(this::checkWriteAccess)
.flatMap(this::checkNoActiveSEBClientConnections) .flatMap(this::checkNoActiveSEBClientConnections)
.flatMap(this::deleteExamDependencies)
.flatMap(this::deleteExamWithRefs) .flatMap(this::deleteExamWithRefs)
.flatMap(exam -> logDeleted(exam, batchAction)) .flatMap(exam -> logDeleted(exam, batchAction))
.map(Exam::getEntityKey); .map(Exam::getEntityKey)
.onError(TransactionHandler::rollback);
}
private Result<Exam> deleteExamDependencies(final Exam entity) {
return this.clientConnectionDAO.deleteAllForExam(entity.id)
.map(this::logDelete)
.flatMap(res -> this.examConfigurationMapDAO.deleteAllForExam(entity.id))
.map(this::logDelete)
.flatMap(res -> this.clientGroupDAO.deleteAllForExam(entity.id))
.map(this::logDelete)
.flatMap(res -> this.indicatorDAO.deleteAllForExam(entity.id))
.map(this::logDelete)
.map(res -> entity);
}
private Collection<EntityKey> logDelete(final Collection<EntityKey> deletedKeys) {
try {
if (log.isDebugEnabled()) {
log.debug("Exam deletion, deleted references: {}", deletedKeys);
}
} catch (final Exception e) {
log.error("Failed to log deletion for: {}", deletedKeys, e);
}
return deletedKeys;
} }
private Result<Exam> deleteExamWithRefs(final Exam entity) { private Result<Exam> deleteExamWithRefs(final Exam entity) {

View file

@ -16,6 +16,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
@ -212,4 +213,10 @@ public interface ClientConnectionDAO extends
* @return Result refer to the given check status or to an error when happened */ * @return Result refer to the given check status or to an error when happened */
Result<Boolean> saveSEBClientVersionCheckStatus(Long connectionId, Boolean checkStatus); Result<Boolean> saveSEBClientVersionCheckStatus(Long connectionId, Boolean checkStatus);
/** Delete all client connections for a particular exam.
*
* @param examId the exam identifier
* @return Result refer to the list of deleted client connections or to an error when happened */
Result<Collection<EntityKey>> deleteAllForExam(Long examId);
} }

View file

@ -13,6 +13,7 @@ import java.util.Collection;
import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroup; import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroup;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO;
@ -40,4 +41,10 @@ public interface ClientGroupDAO extends EntityDAO<ClientGroup, ClientGroup>, Bul
// just evict the cache // just evict the cache
} }
/** Delete all client groups for a particular exam.
*
* @param examId the exam identifier
* @return Result refer to the list of deleted client groups or to an error when happened */
Result<Collection<EntityKey>> deleteAllForExam(Long examId);
} }

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
import java.util.Collection; import java.util.Collection;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap; import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO;
@ -76,4 +77,10 @@ public interface ExamConfigurationMapDAO extends
* if happened */ * if happened */
Result<Boolean> checkNoActiveExamReferences(Long configurationNodeId); Result<Boolean> checkNoActiveExamReferences(Long configurationNodeId);
/** Delete all configuration mappings for a particular exam.
*
* @param examId the exam identifier
* @return Result refer to the list of deleted configuration mappings or to an error when happened */
Result<Collection<EntityKey>> deleteAllForExam(Long examId);
} }

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
import java.util.Collection; import java.util.Collection;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO;
@ -23,4 +24,10 @@ public interface IndicatorDAO extends EntityDAO<Indicator, Indicator>, BulkActio
* @return Result referring to the collection of Indicators of an Exam or to an error if happened */ * @return Result referring to the collection of Indicators of an Exam or to an error if happened */
Result<Collection<Indicator>> allForExam(Long examId); Result<Collection<Indicator>> allForExam(Long examId);
/** Delete all indicators for a particular exam.
*
* @param examId the exam identifier
* @return Result refer to the list of deleted indicators or to an error when happened */
Result<Collection<EntityKey>> deleteAllForExam(Long examId);
} }

View file

@ -568,52 +568,8 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
ids.stream().forEach(this::clearConnecionTokenCache); ids.stream().forEach(this::clearConnecionTokenCache);
// delete all related client indicators deleteAllRelations(ids);
this.clientIndicatorRecordMapper.deleteByExample()
.where(
ClientIndicatorRecordDynamicSqlSupport.clientConnectionId,
SqlBuilder.isIn(ids))
.build()
.execute();
// delete all related client events
this.clientEventRecordMapper.deleteByExample()
.where(
ClientEventRecordDynamicSqlSupport.clientConnectionId,
SqlBuilder.isIn(ids))
.build()
.execute();
// delete all related client notifications
this.clientNotificationRecordMapper.deleteByExample()
.where(
ClientNotificationRecordDynamicSqlSupport.clientConnectionId,
SqlBuilder.isIn(ids))
.build()
.execute();
// then delete all related client instructions
final List<String> connectionTokens = this.clientConnectionRecordMapper.selectByExample()
.where(
ClientConnectionRecordDynamicSqlSupport.id,
SqlBuilder.isIn(ids))
.build()
.execute()
.stream()
.map(r -> r.getConnectionToken())
.collect(Collectors.toList());
if (connectionTokens != null && !connectionTokens.isEmpty()) {
this.clientInstructionRecordMapper.deleteByExample()
.where(
ClientInstructionRecordDynamicSqlSupport.connectionToken,
SqlBuilder.isIn(connectionTokens))
.build()
.execute();
}
// then delete all requested client-connections
this.clientConnectionRecordMapper.deleteByExample() this.clientConnectionRecordMapper.deleteByExample()
.where( .where(
ClientConnectionRecordDynamicSqlSupport.id, ClientConnectionRecordDynamicSqlSupport.id,
@ -882,6 +838,36 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
.execute()); .execute());
} }
@Override
@Transactional
public Result<Collection<EntityKey>> deleteAllForExam(final Long examId) {
return Result.<Collection<EntityKey>> tryCatch(() -> {
final List<Long> ids = this.clientConnectionRecordMapper.selectIdsByExample()
.where(ClientConnectionRecordDynamicSqlSupport.examId, isEqualTo(examId))
.build()
.execute();
if (ids == null || ids.isEmpty()) {
return Collections.emptyList();
}
deleteAllRelations(ids);
this.clientConnectionRecordMapper.deleteByExample()
.where(
ClientConnectionRecordDynamicSqlSupport.id,
SqlBuilder.isIn(ids))
.build()
.execute();
return ids.stream()
.map(id -> new EntityKey(id, EntityType.CLIENT_CONNECTION))
.collect(Collectors.toList());
})
.onError(TransactionHandler::rollback);
}
private Result<ClientConnectionRecord> recordById(final Long id) { private Result<ClientConnectionRecord> recordById(final Long id) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
@ -1025,4 +1011,51 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
return id; return id;
} }
private void deleteAllRelations(final List<Long> ids) {
// delete all related client indicators
this.clientIndicatorRecordMapper.deleteByExample()
.where(
ClientIndicatorRecordDynamicSqlSupport.clientConnectionId,
SqlBuilder.isIn(ids))
.build()
.execute();
// delete all related client events
this.clientEventRecordMapper.deleteByExample()
.where(
ClientEventRecordDynamicSqlSupport.clientConnectionId,
SqlBuilder.isIn(ids))
.build()
.execute();
// delete all related client notifications
this.clientNotificationRecordMapper.deleteByExample()
.where(
ClientNotificationRecordDynamicSqlSupport.clientConnectionId,
SqlBuilder.isIn(ids))
.build()
.execute();
// then delete all related client instructions
final List<String> connectionTokens = this.clientConnectionRecordMapper.selectByExample()
.where(
ClientConnectionRecordDynamicSqlSupport.id,
SqlBuilder.isIn(ids))
.build()
.execute()
.stream()
.map(r -> r.getConnectionToken())
.collect(Collectors.toList());
if (connectionTokens != null && !connectionTokens.isEmpty()) {
this.clientInstructionRecordMapper.deleteByExample()
.where(
ClientInstructionRecordDynamicSqlSupport.connectionToken,
SqlBuilder.isIn(connectionTokens))
.build()
.execute();
}
}
} }

View file

@ -233,6 +233,35 @@ public class ClientGroupDAOImpl implements ClientGroupDAO {
return getDependencies(bulkAction, selectionFunction); return getDependencies(bulkAction, selectionFunction);
} }
@Override
@Transactional
public Result<Collection<EntityKey>> deleteAllForExam(final Long examId) {
return Result.<Collection<EntityKey>> tryCatch(() -> {
final List<Long> ids = this.clientGroupRecordMapper.selectIdsByExample()
.where(ClientGroupRecordDynamicSqlSupport.examId, isEqualTo(examId))
.build()
.execute();
if (ids == null || ids.isEmpty()) {
return Collections.emptyList();
}
// delete all client groups
this.clientGroupRecordMapper
.deleteByExample()
.where(ClientGroupRecordDynamicSqlSupport.id, isIn(ids))
.build()
.execute();
return ids.stream()
.map(id -> new EntityKey(id, EntityType.CLIENT_GROUP))
.collect(Collectors.toList());
})
.onError(TransactionHandler::rollback);
}
private Result<Collection<EntityDependency>> allIdsOfInstitution(final EntityKey institutionKey) { private Result<Collection<EntityDependency>> allIdsOfInstitution(final EntityKey institutionKey) {
return Result.tryCatch(() -> this.clientGroupRecordMapper return Result.tryCatch(() -> this.clientGroupRecordMapper
.selectByExample() .selectByExample()

View file

@ -394,6 +394,44 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
.isPresent()); .isPresent());
} }
@Override
@Transactional
public Result<Collection<EntityKey>> deleteAllForExam(final Long examId) {
return Result.<Collection<EntityKey>> tryCatch(() -> {
final List<Long> ids = this.examConfigurationMapRecordMapper.selectIdsByExample()
.where(ExamConfigurationMapRecordDynamicSqlSupport.examId, isEqualTo(examId))
.build()
.execute();
if (ids == null || ids.isEmpty()) {
return Collections.emptyList();
}
// get all involved configurations
final List<Long> configIds = this.examConfigurationMapRecordMapper.selectByExample()
.where(ExamConfigurationMapRecordDynamicSqlSupport.id, isIn(ids))
.build()
.execute()
.stream()
.map(rec -> rec.getConfigurationNodeId())
.collect(Collectors.toList());
this.examConfigurationMapRecordMapper.deleteByExample()
.where(ExamConfigurationMapRecordDynamicSqlSupport.id, isIn(ids))
.build()
.execute();
updateConfigurationStates(configIds)
.onError(error -> log.error("Unexpected error while update exam configuration state: ", error));
return ids.stream()
.map(id -> new EntityKey(id, EntityType.EXAM_CONFIGURATION_MAP))
.collect(Collectors.toList());
})
.onError(TransactionHandler::rollback);
}
private boolean isExamActive(final Long examId) { private boolean isExamActive(final Long examId) {
try { try {
final boolean active = this.examRecordMapper.countByExample() final boolean active = this.examRecordMapper.countByExample()

View file

@ -270,6 +270,40 @@ public class IndicatorDAOImpl implements IndicatorDAO {
return getDependencies(bulkAction, selectionFunction); return getDependencies(bulkAction, selectionFunction);
} }
@Override
@Transactional
public Result<Collection<EntityKey>> deleteAllForExam(final Long examId) {
return Result.<Collection<EntityKey>> tryCatch(() -> {
final List<Long> ids = this.indicatorRecordMapper.selectIdsByExample()
.where(IndicatorRecordDynamicSqlSupport.examId, isEqualTo(examId))
.build()
.execute();
if (ids == null || ids.isEmpty()) {
return Collections.emptyList();
}
// first delete all thresholds of indicators
this.thresholdRecordMapper.deleteByExample()
.where(ThresholdRecordDynamicSqlSupport.indicatorId, isIn(ids))
.build()
.execute();
// then delete all indicators
this.indicatorRecordMapper.deleteByExample()
.where(IndicatorRecordDynamicSqlSupport.id, isIn(ids))
.build()
.execute();
return ids.stream()
.map(id -> new EntityKey(id, EntityType.INDICATOR))
.collect(Collectors.toList());
})
.onError(TransactionHandler::rollback);
}
private Result<Collection<EntityDependency>> allIdsOfInstitution(final EntityKey institutionKey) { private Result<Collection<EntityDependency>> allIdsOfInstitution(final EntityKey institutionKey) {
return Result.tryCatch(() -> this.indicatorRecordMapper.selectByExample() return Result.tryCatch(() -> this.indicatorRecordMapper.selectByExample()
.leftJoin(ExamRecordDynamicSqlSupport.examRecord) .leftJoin(ExamRecordDynamicSqlSupport.examRecord)

View file

@ -38,6 +38,7 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ExamDeletionEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigService; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigService;
@ -232,6 +233,26 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
error)); error));
} }
@EventListener(ExamDeletionEvent.class)
public void notifyExamDeletion(final ExamDeletionEvent event) {
if (log.isDebugEnabled()) {
log.debug("ExamDeletionEvent received, process releaseSEBClientRestriction...");
}
event.ids.stream().forEach(examId -> {
this.examDAO
.byPK(examId)
.onSuccess(exam -> {
releaseSEBClientRestriction(exam)
.onError(error -> log.error(
"Failed to release SEB restrictions for finished exam: {}",
exam,
error));
});
});
}
@Override @Override
public Result<Exam> applySEBClientRestriction(final Exam exam) { public Result<Exam> applySEBClientRestriction(final Exam exam) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {

View file

@ -519,9 +519,9 @@ sebserver.exam.list.action.archive=Archive Selected Exams
sebserver.exam.list.action.delete=Delete Selected Exams sebserver.exam.list.action.delete=Delete Selected Exams
sebserver.exam.list.batch.archive.title=Archive all Exams in Batch Action sebserver.exam.list.batch.archive.title=Archive all Exams in Batch Action
sebserver.exam.list.batch.action.archive=Archive All sebserver.exam.list.batch.action.archive=Archive All
sebserver.exam.list.batch.action.archive.info=This batch action archives all selected exams below. To archive an exam it must be finished (Finished state)<br/>If a particular exam is in a invalid state for archiving it will be ignored. Archive is an irreversible action and once an exam is archived it cannot be reactivated anymore,<br/>please make sure all exams below shall be archived before applying the batch action. sebserver.exam.list.batch.action.archive.info=This batch action archives all selected exams below. To archive an exam it must be finished (Finished state)<br/>If a particular exam is in a invalid state for archiving it will be ignored.<br/>Archive is an irreversible action and once an exam is archived it cannot be reactivated anymore,<br/>please make sure all exams below shall be archived before applying the batch action.
sebserver.exam.list.batch.delete.title=Delete all Exams in Batch Action sebserver.exam.list.batch.delete.title=Delete all Exams in Batch Action
sebserver.exam.list.batch.action.delete=Delete Selected Exams sebserver.exam.list.batch.action.delete=Delete All
sebserver.exam.list.batch.action.delete.info=This batch action deletes all selected exams below.<br/>If a particular exam is in a invalid state for deleting it will be ignored.<br/>Delete is an action is an irreversible action and all deleted exams will be lost.<br/>Please make sure that all selected exams below shall be deleted and use Cancel to abort the action if not so. sebserver.exam.list.batch.action.delete.info=This batch action deletes all selected exams below.<br/>If a particular exam is in a invalid state for deleting it will be ignored.<br/>Delete is an action is an irreversible action and all deleted exams will be lost.<br/>Please make sure that all selected exams below shall be deleted and use Cancel to abort the action if not so.
sebserver.exam.consistency.title=Note: This exam is already running but has some missing settings sebserver.exam.consistency.title=Note: This exam is already running but has some missing settings