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,
formContext,
multiSelection,
false)
false, false)
.build();
applySelectionList(formContext, multiSelection);
@ -181,7 +181,7 @@ public abstract class AbstractBatchActionWizard {
this.pageService,
formContext,
multiSelection,
true)
true, true)
.build();
this.serverPushService.runServerPush(
@ -234,7 +234,8 @@ public abstract class AbstractBatchActionWizard {
final PageService pageService,
final PageContext formContext,
final Set<EntityKey> multiSelection,
final boolean readonly) {
final boolean readonly,
final boolean resultPage) {
final FormBuilder formBuilder = pageService
.formBuilder(formContext)
@ -245,7 +246,11 @@ public abstract class AbstractBatchActionWizard {
.readonly(true));
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) {

View file

@ -13,8 +13,11 @@ import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
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.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.webservice.servicelayer.authorization.AuthorizationService;
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.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.session.ExamSessionService;
@ -36,18 +44,32 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
@WebServiceProfile
public class DeleteExamAction implements BatchActionExec {
private static final Logger log = LoggerFactory.getLogger(DeleteExamAction.class);
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 UserActivityLogDAO userActivityLogDAO;
private final ExamSessionService examSessionService;
public DeleteExamAction(
final ExamDAO examDAO,
final ClientConnectionDAO clientConnectionDAO,
final ExamConfigurationMapDAO examConfigurationMapDAO,
final ClientGroupDAO clientGroupDAO,
final IndicatorDAO indicatorDAO,
final AuthorizationService authorization,
final UserActivityLogDAO userActivityLogDAO,
final ExamSessionService examSessionService) {
this.examDAO = examDAO;
this.clientConnectionDAO = clientConnectionDAO;
this.examConfigurationMapDAO = examConfigurationMapDAO;
this.clientGroupDAO = clientGroupDAO;
this.indicatorDAO = indicatorDAO;
this.authorization = authorization;
this.userActivityLogDAO = userActivityLogDAO;
this.examSessionService = examSessionService;
@ -65,13 +87,40 @@ public class DeleteExamAction implements BatchActionExec {
}
@Override
@Transactional
public Result<EntityKey> doSingleAction(final String modelId, final BatchAction batchAction) {
return this.examDAO.byModelId(modelId)
.flatMap(this::checkWriteAccess)
.flatMap(this::checkNoActiveSEBClientConnections)
.flatMap(this::deleteExamDependencies)
.flatMap(this::deleteExamWithRefs)
.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) {

View file

@ -16,6 +16,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
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.session.ClientConnection;
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 */
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.Cacheable;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroup;
import ch.ethz.seb.sebserver.gbl.util.Result;
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
}
/** 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 ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO;
@ -76,4 +77,10 @@ public interface ExamConfigurationMapDAO extends
* if happened */
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 ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.util.Result;
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 */
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);
// delete all related client indicators
this.clientIndicatorRecordMapper.deleteByExample()
.where(
ClientIndicatorRecordDynamicSqlSupport.clientConnectionId,
SqlBuilder.isIn(ids))
.build()
.execute();
deleteAllRelations(ids);
// 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()
.where(
ClientConnectionRecordDynamicSqlSupport.id,
@ -882,6 +838,36 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
.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) {
return Result.tryCatch(() -> {
@ -1025,4 +1011,51 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
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);
}
@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) {
return Result.tryCatch(() -> this.clientGroupRecordMapper
.selectByExample()

View file

@ -394,6 +394,44 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
.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) {
try {
final boolean active = this.examRecordMapper.countByExample()

View file

@ -270,6 +270,40 @@ public class IndicatorDAOImpl implements IndicatorDAO {
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) {
return Result.tryCatch(() -> this.indicatorRecordMapper.selectByExample()
.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.webservice.servicelayer.dao.AdditionalAttributesDAO;
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.SEBRestrictionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigService;
@ -232,6 +233,26 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
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
public Result<Exam> applySEBClientRestriction(final Exam exam) {
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.batch.archive.title=Archive all Exams in Batch Action
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.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.consistency.title=Note: This exam is already running but has some missing settings