SEBSERV-308 added archived state
This commit is contained in:
parent
c342dcdbdd
commit
960864e58f
19 changed files with 327 additions and 159 deletions
|
@ -145,6 +145,7 @@ public final class API {
|
||||||
|
|
||||||
public static final String EXAM_ADMINISTRATION_ENDPOINT = "/exam";
|
public static final String EXAM_ADMINISTRATION_ENDPOINT = "/exam";
|
||||||
//public static final String EXAM_ADMINISTRATION_DOWNLOAD_CONFIG_PATH_SEGMENT = "/download-config";
|
//public static final String EXAM_ADMINISTRATION_DOWNLOAD_CONFIG_PATH_SEGMENT = "/download-config";
|
||||||
|
public static final String EXAM_ADMINISTRATION_ARCHIVE_PATH_SEGMENT = "/archive";
|
||||||
public static final String EXAM_ADMINISTRATION_CONSISTENCY_CHECK_PATH_SEGMENT = "/check-consistency";
|
public static final String EXAM_ADMINISTRATION_CONSISTENCY_CHECK_PATH_SEGMENT = "/check-consistency";
|
||||||
public static final String EXAM_ADMINISTRATION_CONSISTENCY_CHECK_INCLUDE_RESTRICTION = "include-restriction";
|
public static final String EXAM_ADMINISTRATION_CONSISTENCY_CHECK_INCLUDE_RESTRICTION = "include-restriction";
|
||||||
public static final String EXAM_ADMINISTRATION_SEB_RESTRICTION_PATH_SEGMENT = "/seb-restriction";
|
public static final String EXAM_ADMINISTRATION_SEB_RESTRICTION_PATH_SEGMENT = "/seb-restriction";
|
||||||
|
|
|
@ -291,6 +291,11 @@ public enum ActionDefinition {
|
||||||
ImageIcon.DELETE,
|
ImageIcon.DELETE,
|
||||||
PageStateDefinitionImpl.EXAM_VIEW,
|
PageStateDefinitionImpl.EXAM_VIEW,
|
||||||
ActionCategory.FORM),
|
ActionCategory.FORM),
|
||||||
|
EXAM_ARCHIVE(
|
||||||
|
new LocTextKey("sebserver.exam.action.archive"),
|
||||||
|
ImageIcon.ARCHIVE,
|
||||||
|
PageStateDefinitionImpl.EXAM_VIEW,
|
||||||
|
ActionCategory.FORM),
|
||||||
|
|
||||||
EXAM_MODIFY_SEB_RESTRICTION_DETAILS(
|
EXAM_MODIFY_SEB_RESTRICTION_DETAILS(
|
||||||
new LocTextKey("sebserver.exam.action.sebrestriction.details"),
|
new LocTextKey("sebserver.exam.action.sebrestriction.details"),
|
||||||
|
|
|
@ -61,6 +61,7 @@ import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.download.DownloadService;
|
import ch.ethz.seb.sebserver.gui.service.remote.download.DownloadService;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCallError;
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCallError;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
|
||||||
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.ArchiveExam;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckExamConsistency;
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckExamConsistency;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckSEBRestriction;
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckSEBRestriction;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetDefaultExamTemplate;
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetDefaultExamTemplate;
|
||||||
|
@ -118,6 +119,8 @@ public class ExamForm implements TemplateComposer {
|
||||||
new LocTextKey("sebserver.exam.form.examTemplate");
|
new LocTextKey("sebserver.exam.form.examTemplate");
|
||||||
private static final LocTextKey FORM_EXAM_TEMPLATE_ERROR =
|
private static final LocTextKey FORM_EXAM_TEMPLATE_ERROR =
|
||||||
new LocTextKey("sebserver.exam.form.examTemplate.error");
|
new LocTextKey("sebserver.exam.form.examTemplate.error");
|
||||||
|
private static final LocTextKey EXAM_ARCHIVE_CONFIRM =
|
||||||
|
new LocTextKey("sebserver.exam.action.archive.confirm");
|
||||||
|
|
||||||
private final static LocTextKey CONSISTENCY_MESSAGE_TITLE =
|
private final static LocTextKey CONSISTENCY_MESSAGE_TITLE =
|
||||||
new LocTextKey("sebserver.exam.consistency.title");
|
new LocTextKey("sebserver.exam.consistency.title");
|
||||||
|
@ -412,6 +415,12 @@ public class ExamForm implements TemplateComposer {
|
||||||
.withExec(this.examDeletePopup.deleteWizardFunction(pageContext))
|
.withExec(this.examDeletePopup.deleteWizardFunction(pageContext))
|
||||||
.publishIf(() -> writeGrant && readonly)
|
.publishIf(() -> writeGrant && readonly)
|
||||||
|
|
||||||
|
.newAction(ActionDefinition.EXAM_ARCHIVE)
|
||||||
|
.withEntityKey(entityKey)
|
||||||
|
.withConfirm(() -> EXAM_ARCHIVE_CONFIRM)
|
||||||
|
.withExec(this::archiveExam)
|
||||||
|
.publishIf(() -> writeGrant && readonly && examStatus == ExamStatus.FINISHED)
|
||||||
|
|
||||||
.newAction(ActionDefinition.EXAM_SAVE)
|
.newAction(ActionDefinition.EXAM_SAVE)
|
||||||
.withExec(action -> (importFromQuizData)
|
.withExec(action -> (importFromQuizData)
|
||||||
? importExam(action, formHandle, sebRestrictionAvailable && exam.status == ExamStatus.RUNNING)
|
? importExam(action, formHandle, sebRestrictionAvailable && exam.status == ExamStatus.RUNNING)
|
||||||
|
@ -485,6 +494,16 @@ public class ExamForm implements TemplateComposer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PageAction archiveExam(final PageAction action) {
|
||||||
|
|
||||||
|
this.restService.getBuilder(ArchiveExam.class)
|
||||||
|
.withURIVariable(API.PARAM_MODEL_ID, action.getEntityKey().modelId)
|
||||||
|
.call()
|
||||||
|
.onError(error -> action.pageContext().notifyUnexpectedError(error));
|
||||||
|
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
private String getDefaultExamTemplateId() {
|
private String getDefaultExamTemplateId() {
|
||||||
return this.restService.getBuilder(GetDefaultExamTemplate.class)
|
return this.restService.getBuilder(GetDefaultExamTemplate.class)
|
||||||
.call()
|
.call()
|
||||||
|
|
|
@ -77,6 +77,8 @@ public class ExamList implements TemplateComposer {
|
||||||
new LocTextKey("sebserver.exam.list.column.lmssetup");
|
new LocTextKey("sebserver.exam.list.column.lmssetup");
|
||||||
public final static LocTextKey COLUMN_TITLE_NAME_KEY =
|
public final static LocTextKey COLUMN_TITLE_NAME_KEY =
|
||||||
new LocTextKey("sebserver.exam.list.column.name");
|
new LocTextKey("sebserver.exam.list.column.name");
|
||||||
|
public final static LocTextKey COLUMN_TITLE_STATE_KEY =
|
||||||
|
new LocTextKey("sebserver.exam.list.column.state");
|
||||||
public final static LocTextKey COLUMN_TITLE_TYPE_KEY =
|
public final static LocTextKey COLUMN_TITLE_TYPE_KEY =
|
||||||
new LocTextKey("sebserver.exam.list.column.type");
|
new LocTextKey("sebserver.exam.list.column.type");
|
||||||
public final static LocTextKey NO_MODIFY_OF_OUT_DATED_EXAMS =
|
public final static LocTextKey NO_MODIFY_OF_OUT_DATED_EXAMS =
|
||||||
|
@ -88,6 +90,7 @@ public class ExamList implements TemplateComposer {
|
||||||
private final TableFilterAttribute lmsFilter;
|
private final TableFilterAttribute lmsFilter;
|
||||||
private final TableFilterAttribute nameFilter =
|
private final TableFilterAttribute nameFilter =
|
||||||
new TableFilterAttribute(CriteriaType.TEXT, QuizData.FILTER_ATTR_NAME);
|
new TableFilterAttribute(CriteriaType.TEXT, QuizData.FILTER_ATTR_NAME);
|
||||||
|
private final TableFilterAttribute stateFilter;
|
||||||
private final TableFilterAttribute typeFilter;
|
private final TableFilterAttribute typeFilter;
|
||||||
|
|
||||||
private final PageService pageService;
|
private final PageService pageService;
|
||||||
|
@ -112,6 +115,11 @@ public class ExamList implements TemplateComposer {
|
||||||
LmsSetup.FILTER_ATTR_LMS_SETUP,
|
LmsSetup.FILTER_ATTR_LMS_SETUP,
|
||||||
this.resourceService::lmsSetupResource);
|
this.resourceService::lmsSetupResource);
|
||||||
|
|
||||||
|
this.stateFilter = new TableFilterAttribute(
|
||||||
|
CriteriaType.SINGLE_SELECTION,
|
||||||
|
Exam.FILTER_ATTR_STATUS,
|
||||||
|
this.resourceService::localizedExamStatusSelection);
|
||||||
|
|
||||||
this.typeFilter = new TableFilterAttribute(
|
this.typeFilter = new TableFilterAttribute(
|
||||||
CriteriaType.SINGLE_SELECTION,
|
CriteriaType.SINGLE_SELECTION,
|
||||||
Exam.FILTER_ATTR_TYPE,
|
Exam.FILTER_ATTR_TYPE,
|
||||||
|
@ -189,6 +197,13 @@ public class ExamList implements TemplateComposer {
|
||||||
.toString()))
|
.toString()))
|
||||||
.sortable())
|
.sortable())
|
||||||
|
|
||||||
|
.withColumn(new ColumnDefinition<>(
|
||||||
|
Domain.EXAM.ATTR_STATUS,
|
||||||
|
COLUMN_TITLE_STATE_KEY,
|
||||||
|
this.resourceService::localizedExamStatusName)
|
||||||
|
.withFilter(this.stateFilter)
|
||||||
|
.sortable())
|
||||||
|
|
||||||
.withColumn(new ColumnDefinition<Exam>(
|
.withColumn(new ColumnDefinition<Exam>(
|
||||||
Domain.EXAM.ATTR_TYPE,
|
Domain.EXAM.ATTR_TYPE,
|
||||||
COLUMN_TITLE_TYPE_KEY,
|
COLUMN_TITLE_TYPE_KEY,
|
||||||
|
|
|
@ -48,6 +48,8 @@ public class FinishedExamList implements TemplateComposer {
|
||||||
new LocTextKey("sebserver.finished.exam.info.pleaseSelect");
|
new LocTextKey("sebserver.finished.exam.info.pleaseSelect");
|
||||||
private final static LocTextKey COLUMN_TITLE_NAME_KEY =
|
private final static LocTextKey COLUMN_TITLE_NAME_KEY =
|
||||||
new LocTextKey("sebserver.finished.exam.list.column.name");
|
new LocTextKey("sebserver.finished.exam.list.column.name");
|
||||||
|
private final static LocTextKey COLUMN_TITLE_STATE_KEY =
|
||||||
|
new LocTextKey("sebserver.finished.exam.list.column.state");
|
||||||
private final static LocTextKey COLUMN_TITLE_TYPE_KEY =
|
private final static LocTextKey COLUMN_TITLE_TYPE_KEY =
|
||||||
new LocTextKey("sebserver.finished.exam.list.column.type");
|
new LocTextKey("sebserver.finished.exam.list.column.type");
|
||||||
private final static LocTextKey EMPTY_LIST_TEXT_KEY =
|
private final static LocTextKey EMPTY_LIST_TEXT_KEY =
|
||||||
|
@ -56,6 +58,7 @@ public class FinishedExamList implements TemplateComposer {
|
||||||
private final TableFilterAttribute nameFilter =
|
private final TableFilterAttribute nameFilter =
|
||||||
new TableFilterAttribute(CriteriaType.TEXT, QuizData.FILTER_ATTR_NAME);
|
new TableFilterAttribute(CriteriaType.TEXT, QuizData.FILTER_ATTR_NAME);
|
||||||
private final TableFilterAttribute typeFilter;
|
private final TableFilterAttribute typeFilter;
|
||||||
|
private final TableFilterAttribute stateFilter;
|
||||||
|
|
||||||
private final PageService pageService;
|
private final PageService pageService;
|
||||||
private final ResourceService resourceService;
|
private final ResourceService resourceService;
|
||||||
|
@ -73,6 +76,11 @@ public class FinishedExamList implements TemplateComposer {
|
||||||
CriteriaType.SINGLE_SELECTION,
|
CriteriaType.SINGLE_SELECTION,
|
||||||
Exam.FILTER_ATTR_TYPE,
|
Exam.FILTER_ATTR_TYPE,
|
||||||
this.resourceService::examTypeResources);
|
this.resourceService::examTypeResources);
|
||||||
|
|
||||||
|
this.stateFilter = new TableFilterAttribute(
|
||||||
|
CriteriaType.SINGLE_SELECTION,
|
||||||
|
Exam.FILTER_ATTR_STATUS,
|
||||||
|
this.resourceService::localizedFinishedExamStatusSelection);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -105,6 +113,13 @@ public class FinishedExamList implements TemplateComposer {
|
||||||
.withFilter(this.nameFilter)
|
.withFilter(this.nameFilter)
|
||||||
.sortable())
|
.sortable())
|
||||||
|
|
||||||
|
.withColumn(new ColumnDefinition<>(
|
||||||
|
Domain.EXAM.ATTR_STATUS,
|
||||||
|
COLUMN_TITLE_STATE_KEY,
|
||||||
|
this.resourceService::localizedExamStatusName)
|
||||||
|
.withFilter(this.stateFilter)
|
||||||
|
.sortable())
|
||||||
|
|
||||||
.withColumn(new ColumnDefinition<Exam>(
|
.withColumn(new ColumnDefinition<Exam>(
|
||||||
Domain.EXAM.ATTR_TYPE,
|
Domain.EXAM.ATTR_TYPE,
|
||||||
COLUMN_TITLE_TYPE_KEY,
|
COLUMN_TITLE_TYPE_KEY,
|
||||||
|
|
|
@ -118,6 +118,7 @@ public class ResourceService {
|
||||||
public static final EnumSet<EventType> CLIENT_EVENT_TYPE_EXCLUDE_MAP = EnumSet.of(
|
public static final EnumSet<EventType> CLIENT_EVENT_TYPE_EXCLUDE_MAP = EnumSet.of(
|
||||||
EventType.UNKNOWN);
|
EventType.UNKNOWN);
|
||||||
|
|
||||||
|
public static final String EXAM_STATUS_PREFIX = "sebserver.exam.status.";
|
||||||
public static final String EXAMCONFIG_STATUS_PREFIX = "sebserver.examconfig.status.";
|
public static final String EXAMCONFIG_STATUS_PREFIX = "sebserver.examconfig.status.";
|
||||||
public static final String EXAM_TYPE_PREFIX = "sebserver.exam.type.";
|
public static final String EXAM_TYPE_PREFIX = "sebserver.exam.type.";
|
||||||
public static final String USERACCOUNT_ROLE_PREFIX = "sebserver.useraccount.role.";
|
public static final String USERACCOUNT_ROLE_PREFIX = "sebserver.useraccount.role.";
|
||||||
|
@ -548,6 +549,32 @@ public class ResourceService {
|
||||||
.apply(String.valueOf(config.institutionId));
|
.apply(String.valueOf(config.institutionId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Tuple<String>> localizedExamStatusSelection() {
|
||||||
|
return Arrays.stream(ExamStatus.values())
|
||||||
|
.map(type -> new Tuple<>(type.name(),
|
||||||
|
this.i18nSupport.getText(EXAM_STATUS_PREFIX + type.name())))
|
||||||
|
.sorted(RESOURCE_COMPARATOR)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Tuple<String>> localizedFinishedExamStatusSelection() {
|
||||||
|
return Arrays.stream(ExamStatus.values())
|
||||||
|
.filter(st -> st == ExamStatus.ARCHIVED || st == ExamStatus.FINISHED)
|
||||||
|
.map(type -> new Tuple<>(type.name(),
|
||||||
|
this.i18nSupport.getText(EXAM_STATUS_PREFIX + type.name())))
|
||||||
|
.sorted(RESOURCE_COMPARATOR)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String localizedExamStatusName(final Exam exam) {
|
||||||
|
if (exam.status == null) {
|
||||||
|
return Constants.EMPTY_NOTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.i18nSupport
|
||||||
|
.getText(ResourceService.EXAM_STATUS_PREFIX + exam.status.name());
|
||||||
|
}
|
||||||
|
|
||||||
public String localizedExamConfigStatusName(final ConfigurationNode config) {
|
public String localizedExamConfigStatusName(final ConfigurationNode config) {
|
||||||
if (config.status == null) {
|
if (config.status == null) {
|
||||||
return Constants.EMPTY_NOTE;
|
return Constants.EMPTY_NOTE;
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||||
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
@Component
|
||||||
|
@GuiProfile
|
||||||
|
public class ArchiveExam extends RestCall<Exam> {
|
||||||
|
|
||||||
|
public ArchiveExam() {
|
||||||
|
super(new TypeKey<>(
|
||||||
|
CallType.SAVE,
|
||||||
|
EntityType.EXAM,
|
||||||
|
new TypeReference<Exam>() {
|
||||||
|
}),
|
||||||
|
HttpMethod.PATCH,
|
||||||
|
MediaType.APPLICATION_JSON,
|
||||||
|
API.EXAM_ADMINISTRATION_ENDPOINT
|
||||||
|
+ API.MODEL_ID_VAR_PATH_SEGMENT
|
||||||
|
+ API.EXAM_ADMINISTRATION_ARCHIVE_PATH_SEGMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -122,6 +122,7 @@ public class WidgetFactory {
|
||||||
SECURE("secure.png"),
|
SECURE("secure.png"),
|
||||||
NEW("new.png"),
|
NEW("new.png"),
|
||||||
DELETE("delete.png"),
|
DELETE("delete.png"),
|
||||||
|
ARCHIVE("archive.png"),
|
||||||
SEARCH("lens.png"),
|
SEARCH("lens.png"),
|
||||||
UNDO("undo.png"),
|
UNDO("undo.png"),
|
||||||
COLOR("color.png"),
|
COLOR("color.png"),
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
|
package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
import org.springframework.cache.annotation.CacheEvict;
|
import org.springframework.cache.annotation.CacheEvict;
|
||||||
|
|
||||||
|
@ -80,10 +81,13 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
|
||||||
|
|
||||||
/** Use this to get identifiers of all exams in a specified state for a specified institution.
|
/** Use this to get identifiers of all exams in a specified state for a specified institution.
|
||||||
*
|
*
|
||||||
* @param institutionId the institution identifier. May be null for all institutions
|
* @param filterMap FilterMap with other filter criteria
|
||||||
* @param status the ExamStatus
|
* @param status the list of ExamStatus
|
||||||
* @return Result refer to collection of exam identifiers or to an error if happened */
|
* @return Result refer to collection of exam identifiers or to an error if happened */
|
||||||
Result<Collection<Long>> getExamIdsForStatus(Long institutionId, ExamStatus status);
|
Result<Collection<Exam>> getExamIdsForStatus(
|
||||||
|
final FilterMap filterMap,
|
||||||
|
final Predicate<Exam> predicate,
|
||||||
|
final ExamStatus... status);
|
||||||
|
|
||||||
/** Gets all for active and none archived exams within the system, independently form institution and LMSSetup.
|
/** Gets all for active and none archived exams within the system, independently form institution and LMSSetup.
|
||||||
*
|
*
|
||||||
|
|
|
@ -34,6 +34,8 @@ import org.springframework.transaction.annotation.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
|
||||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.EntityDependency;
|
import ch.ethz.seb.sebserver.gbl.model.EntityDependency;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||||
|
@ -118,6 +120,18 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
|
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
|
|
||||||
|
final Predicate<Exam> examDataFilter = createPredicate(filterMap);
|
||||||
|
return this.examRecordDAO
|
||||||
|
.allMatching(filterMap, null)
|
||||||
|
.flatMap(this::toDomainModel)
|
||||||
|
.getOrThrow()
|
||||||
|
.stream()
|
||||||
|
.filter(examDataFilter.and(predicate))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Predicate<Exam> createPredicate(final FilterMap filterMap) {
|
||||||
final String name = filterMap.getQuizName();
|
final String name = filterMap.getQuizName();
|
||||||
final DateTime from = filterMap.getExamFromTime();
|
final DateTime from = filterMap.getExamFromTime();
|
||||||
final Predicate<Exam> quizDataFilter = exam -> {
|
final Predicate<Exam> quizDataFilter = exam -> {
|
||||||
|
@ -139,15 +153,7 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
return quizDataFilter;
|
||||||
return this.examRecordDAO
|
|
||||||
.allMatching(filterMap)
|
|
||||||
.flatMap(this::toDomainModel)
|
|
||||||
.getOrThrow()
|
|
||||||
.stream()
|
|
||||||
.filter(quizDataFilter.and(predicate))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -159,9 +165,9 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<Exam> save(final Exam exam) {
|
public Result<Exam> save(final Exam exam) {
|
||||||
return this.examRecordDAO
|
return this.checkStateEdit(exam)
|
||||||
.save(exam)
|
.flatMap(this.examRecordDAO::save)
|
||||||
.map(rec -> saveAdditionalAttributes(exam, rec))
|
.flatMap(rec -> saveAdditionalAttributes(exam, rec))
|
||||||
.flatMap(this::toDomainModel);
|
.flatMap(this::toDomainModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,7 +207,7 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
public Result<Exam> createNew(final Exam exam) {
|
public Result<Exam> createNew(final Exam exam) {
|
||||||
return this.examRecordDAO
|
return this.examRecordDAO
|
||||||
.createNew(exam)
|
.createNew(exam)
|
||||||
.map(rec -> saveAdditionalAttributes(exam, rec))
|
.flatMap(rec -> saveAdditionalAttributes(exam, rec))
|
||||||
.flatMap(this::toDomainModel);
|
.flatMap(this::toDomainModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,22 +254,26 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Result<Collection<Long>> getExamIdsForStatus(final Long institutionId, final ExamStatus status) {
|
public Result<Collection<Exam>> getExamIdsForStatus(
|
||||||
return Result.tryCatch(() -> this.examRecordMapper.selectIdsByExample()
|
final FilterMap filterMap,
|
||||||
.where(
|
final Predicate<Exam> predicate,
|
||||||
ExamRecordDynamicSqlSupport.active,
|
final ExamStatus... status) {
|
||||||
isEqualTo(BooleanUtils.toInteger(true)))
|
|
||||||
.and(
|
return Result.tryCatch(() -> {
|
||||||
ExamRecordDynamicSqlSupport.institutionId,
|
|
||||||
isEqualToWhenPresent(institutionId))
|
final List<String> stateNames = (status != null && status.length > 0)
|
||||||
.and(
|
? Arrays.asList(status)
|
||||||
ExamRecordDynamicSqlSupport.status,
|
.stream().map(s -> s.name())
|
||||||
isEqualTo(status.name()))
|
.collect(Collectors.toList())
|
||||||
.and(
|
: null;
|
||||||
ExamRecordDynamicSqlSupport.updating,
|
final Predicate<Exam> examDataFilter = createPredicate(filterMap);
|
||||||
isEqualTo(BooleanUtils.toInteger(false)))
|
return this.examRecordDAO.allMatching(filterMap, stateNames)
|
||||||
.build()
|
.flatMap(this::toDomainModel)
|
||||||
.execute());
|
.getOrThrow()
|
||||||
|
.stream()
|
||||||
|
.filter(examDataFilter.and(predicate))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -697,81 +707,6 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// private QuizData getQuizData(
|
|
||||||
// final Map<String, QuizData> quizzes,
|
|
||||||
// final String externalId,
|
|
||||||
// final ExamRecord record) {
|
|
||||||
//
|
|
||||||
// if (quizzes.containsKey(externalId)) {
|
|
||||||
// return quizzes.get(externalId);
|
|
||||||
// } else {
|
|
||||||
// // If this is a Moodle quiz, try to recover from eventually restore of the quiz on the LMS side
|
|
||||||
// // NOTE: This is a workaround for Moodle quizzes that had have a recovery within the sandbox tool
|
|
||||||
// // Where potentially quiz identifiers get changed during such a recovery and the SEB Server
|
|
||||||
// // internal mapping is not working properly anymore. In this case we try to recover from such
|
|
||||||
// // a case by using the short name of the quiz and search for the quiz within the course with this
|
|
||||||
// // short name. If one quiz has been found that matches all criteria, we adapt the internal id
|
|
||||||
// // mapping to this quiz.
|
|
||||||
// // If recovering fails, this returns null and the calling side must handle the lack of quiz data
|
|
||||||
// try {
|
|
||||||
// final LmsSetup lmsSetup = this.lmsAPIService
|
|
||||||
// .getLmsSetup(record.getLmsSetupId())
|
|
||||||
// .getOrThrow();
|
|
||||||
// if (lmsSetup.lmsType == LmsType.MOODLE) {
|
|
||||||
//
|
|
||||||
// log.info("Try to recover quiz data for Moodle quiz with internal identifier: {}", externalId);
|
|
||||||
//
|
|
||||||
// // get former quiz name attribute
|
|
||||||
// final String formerName = getFormerName(record.getId());
|
|
||||||
// if (formerName != null) {
|
|
||||||
//
|
|
||||||
// log.debug("Found formerName quiz name: {}", formerName);
|
|
||||||
//
|
|
||||||
// // get the course name identifier
|
|
||||||
// final String shortname = MoodleCourseAccess.getShortname(externalId);
|
|
||||||
// if (StringUtils.isNotBlank(shortname)) {
|
|
||||||
//
|
|
||||||
// log.debug("Using short-name: {} for recovering", shortname);
|
|
||||||
//
|
|
||||||
// final QuizData recoveredQuizData = this.lmsAPIService
|
|
||||||
// .getLmsAPITemplate(lmsSetup.id)
|
|
||||||
// .map(template -> template.getQuizzes(new FilterMap())
|
|
||||||
// .getOrThrow()
|
|
||||||
// .stream()
|
|
||||||
// .filter(quiz -> {
|
|
||||||
// final String qShortName = MoodleCourseAccess.getShortname(quiz.id);
|
|
||||||
// return qShortName != null && qShortName.equals(shortname);
|
|
||||||
// })
|
|
||||||
// .filter(quiz -> formerName.equals(quiz.name))
|
|
||||||
// .findAny()
|
|
||||||
// .get())
|
|
||||||
// .getOrThrow();
|
|
||||||
// if (recoveredQuizData != null) {
|
|
||||||
//
|
|
||||||
// log.debug("Found quiz data for recovering: {}", recoveredQuizData);
|
|
||||||
//
|
|
||||||
// // save exam with new external id
|
|
||||||
// this.examRecordMapper.updateByPrimaryKeySelective(new ExamRecord(
|
|
||||||
// record.getId(),
|
|
||||||
// null, null,
|
|
||||||
// recoveredQuizData.id,
|
|
||||||
// null, null, null, null, null, null, null, null, null, null, null,
|
|
||||||
// Utils.getMillisecondsNow()));
|
|
||||||
//
|
|
||||||
// log.debug("Successfully recovered exam quiz data to new externalId {}",
|
|
||||||
// recoveredQuizData.id);
|
|
||||||
// }
|
|
||||||
// return recoveredQuizData;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } catch (final Exception e) {
|
|
||||||
// log.debug("Failed to try to recover from Moodle quiz restore: {}", e.getMessage());
|
|
||||||
// }
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
private Result<Exam> toDomainModel(final ExamRecord record) {
|
private Result<Exam> toDomainModel(final ExamRecord record) {
|
||||||
|
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
|
@ -817,7 +752,8 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExamRecord saveAdditionalAttributes(final Exam exam, final ExamRecord rec) {
|
private Result<ExamRecord> saveAdditionalAttributes(final Exam exam, final ExamRecord rec) {
|
||||||
|
return Result.tryCatch(() -> {
|
||||||
if (exam.additionalAttributesIncluded()) {
|
if (exam.additionalAttributesIncluded()) {
|
||||||
this.additionalAttributesDAO.saveAdditionalAttributes(
|
this.additionalAttributesDAO.saveAdditionalAttributes(
|
||||||
EntityType.EXAM,
|
EntityType.EXAM,
|
||||||
|
@ -827,6 +763,8 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
}
|
}
|
||||||
|
|
||||||
return rec;
|
return rec;
|
||||||
|
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private QuizData saveAdditionalQuizAttributes(final Long examId, final QuizData quizData) {
|
private QuizData saveAdditionalQuizAttributes(final Long examId, final QuizData quizData) {
|
||||||
|
@ -843,4 +781,14 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
return quizData;
|
return quizData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Result<Exam> checkStateEdit(final Exam exam) {
|
||||||
|
return Result.tryCatch(() -> {
|
||||||
|
|
||||||
|
if (exam.status == ExamStatus.ARCHIVED) {
|
||||||
|
throw new APIMessageException(APIMessage.ErrorMessage.INTEGRITY_VALIDATION.of("Exam is archived"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return exam;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,8 @@ import org.apache.commons.lang3.StringUtils;
|
||||||
import org.mybatis.dynamic.sql.SqlBuilder;
|
import org.mybatis.dynamic.sql.SqlBuilder;
|
||||||
import org.mybatis.dynamic.sql.select.MyBatis3SelectModelAdapter;
|
import org.mybatis.dynamic.sql.select.MyBatis3SelectModelAdapter;
|
||||||
import org.mybatis.dynamic.sql.select.QueryExpressionDSL;
|
import org.mybatis.dynamic.sql.select.QueryExpressionDSL;
|
||||||
|
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 org.springframework.transaction.annotation.Transactional;
|
||||||
|
@ -52,6 +54,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
|
||||||
@WebServiceProfile
|
@WebServiceProfile
|
||||||
public class ExamRecordDAO {
|
public class ExamRecordDAO {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ExamRecordDAO.class);
|
||||||
|
|
||||||
private final ExamRecordMapper examRecordMapper;
|
private final ExamRecordMapper examRecordMapper;
|
||||||
private final ClientConnectionRecordMapper clientConnectionRecordMapper;
|
private final ClientConnectionRecordMapper clientConnectionRecordMapper;
|
||||||
|
|
||||||
|
@ -133,13 +137,13 @@ public class ExamRecordDAO {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Result<Collection<ExamRecord>> allMatching(final FilterMap filterMap) {
|
public Result<Collection<ExamRecord>> allMatching(final FilterMap filterMap, final List<String> stateNames) {
|
||||||
|
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
|
|
||||||
// If we have a sort on institution name, join the institution table
|
// If we have a sort on institution name, join the institution table
|
||||||
// If we have a sort on lms setup name, join lms setup table
|
// If we have a sort on lms setup name, join lms setup table
|
||||||
final QueryExpressionDSL<MyBatis3SelectModelAdapter<List<ExamRecord>>>.QueryExpressionWhereBuilder whereClause =
|
QueryExpressionDSL<MyBatis3SelectModelAdapter<List<ExamRecord>>>.QueryExpressionWhereBuilder whereClause =
|
||||||
(filterMap.getBoolean(FilterMap.ATTR_ADD_INSITUTION_JOIN))
|
(filterMap.getBoolean(FilterMap.ATTR_ADD_INSITUTION_JOIN))
|
||||||
? this.examRecordMapper
|
? this.examRecordMapper
|
||||||
.selectByExample()
|
.selectByExample()
|
||||||
|
@ -165,7 +169,9 @@ public class ExamRecordDAO {
|
||||||
ExamRecordDynamicSqlSupport.active,
|
ExamRecordDynamicSqlSupport.active,
|
||||||
isEqualToWhenPresent(filterMap.getActiveAsInt()));
|
isEqualToWhenPresent(filterMap.getActiveAsInt()));
|
||||||
|
|
||||||
final List<ExamRecord> records = whereClause
|
//
|
||||||
|
|
||||||
|
whereClause = whereClause
|
||||||
.and(
|
.and(
|
||||||
ExamRecordDynamicSqlSupport.institutionId,
|
ExamRecordDynamicSqlSupport.institutionId,
|
||||||
isEqualToWhenPresent(filterMap.getInstitutionId()))
|
isEqualToWhenPresent(filterMap.getInstitutionId()))
|
||||||
|
@ -174,10 +180,23 @@ public class ExamRecordDAO {
|
||||||
isEqualToWhenPresent(filterMap.getLmsSetupId()))
|
isEqualToWhenPresent(filterMap.getLmsSetupId()))
|
||||||
.and(
|
.and(
|
||||||
ExamRecordDynamicSqlSupport.type,
|
ExamRecordDynamicSqlSupport.type,
|
||||||
isEqualToWhenPresent(filterMap.getExamType()))
|
isEqualToWhenPresent(filterMap.getExamType()));
|
||||||
|
|
||||||
|
final String examStatus = filterMap.getExamStatus();
|
||||||
|
if (StringUtils.isNotBlank(examStatus)) {
|
||||||
|
whereClause = whereClause
|
||||||
.and(
|
.and(
|
||||||
ExamRecordDynamicSqlSupport.status,
|
ExamRecordDynamicSqlSupport.status,
|
||||||
isEqualToWhenPresent(filterMap.getExamStatus()))
|
isEqualToWhenPresent(examStatus));
|
||||||
|
} else if (stateNames != null && !stateNames.isEmpty()) {
|
||||||
|
whereClause = whereClause
|
||||||
|
.and(
|
||||||
|
ExamRecordDynamicSqlSupport.status,
|
||||||
|
isInWhenPresent(stateNames));
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<ExamRecord> records = whereClause
|
||||||
|
|
||||||
.and(
|
.and(
|
||||||
ExamRecordDynamicSqlSupport.quizName,
|
ExamRecordDynamicSqlSupport.quizName,
|
||||||
isLikeWhenPresent(filterMap.getSQLWildcard(EXAM.ATTR_QUIZ_NAME)))
|
isLikeWhenPresent(filterMap.getSQLWildcard(EXAM.ATTR_QUIZ_NAME)))
|
||||||
|
@ -192,7 +211,9 @@ public class ExamRecordDAO {
|
||||||
public Result<ExamRecord> updateState(final Long examId, final ExamStatus status, final String updateId) {
|
public Result<ExamRecord> updateState(final Long examId, final ExamStatus status, final String updateId) {
|
||||||
return recordById(examId)
|
return recordById(examId)
|
||||||
.map(examRecord -> {
|
.map(examRecord -> {
|
||||||
if (BooleanUtils.isTrue(BooleanUtils.toBooleanObject(examRecord.getUpdating()))) {
|
if (updateId != null &&
|
||||||
|
BooleanUtils.isTrue(BooleanUtils.toBooleanObject(examRecord.getUpdating()))) {
|
||||||
|
|
||||||
if (!updateId.equals(examRecord.getLastupdate())) {
|
if (!updateId.equals(examRecord.getLastupdate())) {
|
||||||
throw new IllegalStateException("Exam is currently locked: " + examRecord.getExternalId());
|
throw new IllegalStateException("Exam is currently locked: " + examRecord.getExternalId());
|
||||||
}
|
}
|
||||||
|
@ -221,6 +242,13 @@ public class ExamRecordDAO {
|
||||||
throw new IllegalStateException("Exam is currently locked: " + exam.externalId);
|
throw new IllegalStateException("Exam is currently locked: " + exam.externalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (exam.status != null && !exam.status.name().equals(oldRecord.getStatus())) {
|
||||||
|
log.warn("Exam state change on save. Exam. {}, Old state: {}, new state: {}",
|
||||||
|
exam.externalId,
|
||||||
|
oldRecord.getStatus(),
|
||||||
|
exam.status);
|
||||||
|
}
|
||||||
|
|
||||||
final ExamRecord examRecord = new ExamRecord(
|
final ExamRecord examRecord = new ExamRecord(
|
||||||
exam.id,
|
exam.id,
|
||||||
null, null, null, null,
|
null, null, null, null,
|
||||||
|
@ -232,9 +260,7 @@ public class ExamRecordDAO {
|
||||||
: null,
|
: null,
|
||||||
null,
|
null,
|
||||||
exam.browserExamKeys,
|
exam.browserExamKeys,
|
||||||
(exam.status != null)
|
null,
|
||||||
? exam.status.name()
|
|
||||||
: null,
|
|
||||||
1, // seb restriction (deprecated)
|
1, // seb restriction (deprecated)
|
||||||
null, // updating
|
null, // updating
|
||||||
null, // lastUpdate
|
null, // lastUpdate
|
||||||
|
@ -412,6 +438,9 @@ public class ExamRecordDAO {
|
||||||
.and(
|
.and(
|
||||||
ExamRecordDynamicSqlSupport.status,
|
ExamRecordDynamicSqlSupport.status,
|
||||||
isNotEqualTo(ExamStatus.RUNNING.name()))
|
isNotEqualTo(ExamStatus.RUNNING.name()))
|
||||||
|
.and(
|
||||||
|
ExamRecordDynamicSqlSupport.status,
|
||||||
|
isNotEqualTo(ExamStatus.ARCHIVED.name()))
|
||||||
.and(
|
.and(
|
||||||
ExamRecordDynamicSqlSupport.updating,
|
ExamRecordDynamicSqlSupport.updating,
|
||||||
isEqualTo(BooleanUtils.toInteger(false)))
|
isEqualTo(BooleanUtils.toInteger(false)))
|
||||||
|
@ -430,6 +459,9 @@ public class ExamRecordDAO {
|
||||||
.and(
|
.and(
|
||||||
ExamRecordDynamicSqlSupport.status,
|
ExamRecordDynamicSqlSupport.status,
|
||||||
isEqualTo(ExamStatus.RUNNING.name()))
|
isEqualTo(ExamStatus.RUNNING.name()))
|
||||||
|
.and(
|
||||||
|
ExamRecordDynamicSqlSupport.status,
|
||||||
|
isNotEqualTo(ExamStatus.ARCHIVED.name()))
|
||||||
.and(
|
.and(
|
||||||
ExamRecordDynamicSqlSupport.updating,
|
ExamRecordDynamicSqlSupport.updating,
|
||||||
isEqualTo(BooleanUtils.toInteger(false)))
|
isEqualTo(BooleanUtils.toInteger(false)))
|
||||||
|
|
|
@ -266,8 +266,11 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
||||||
final FilterMap filterMap,
|
final FilterMap filterMap,
|
||||||
final Predicate<Exam> predicate) {
|
final Predicate<Exam> predicate) {
|
||||||
|
|
||||||
filterMap.putIfAbsent(Exam.FILTER_ATTR_STATUS, ExamStatus.FINISHED.name());
|
return this.examDAO.getExamIdsForStatus(
|
||||||
return this.examDAO.allMatching(filterMap, predicate);
|
filterMap,
|
||||||
|
predicate,
|
||||||
|
ExamStatus.FINISHED,
|
||||||
|
ExamStatus.ARCHIVED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -149,7 +149,11 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
}
|
}
|
||||||
|
|
||||||
if (examId != null) {
|
if (examId != null) {
|
||||||
checkExamIntegrity(examId, institutionId);
|
checkExamIntegrity(
|
||||||
|
examId,
|
||||||
|
institutionId,
|
||||||
|
(principal != null) ? principal.getName() : "--",
|
||||||
|
clientAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create ClientConnection in status CONNECTION_REQUESTED for further processing
|
// Create ClientConnection in status CONNECTION_REQUESTED for further processing
|
||||||
|
@ -233,7 +237,11 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
}
|
}
|
||||||
|
|
||||||
if (examId != null) {
|
if (examId != null) {
|
||||||
checkExamIntegrity(examId, institutionId);
|
checkExamIntegrity(
|
||||||
|
examId,
|
||||||
|
institutionId,
|
||||||
|
StringUtils.isNoneBlank(userSessionId) ? userSessionId : clientConnection.userSessionId,
|
||||||
|
clientConnection.clientAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log.isDebugEnabled()) {
|
if (log.isDebugEnabled()) {
|
||||||
|
@ -419,7 +427,11 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
.getOrThrow();
|
.getOrThrow();
|
||||||
|
|
||||||
// check exam integrity for established connection
|
// check exam integrity for established connection
|
||||||
checkExamIntegrity(establishedClientConnection.examId, institutionId);
|
checkExamIntegrity(
|
||||||
|
establishedClientConnection.examId,
|
||||||
|
institutionId,
|
||||||
|
establishedClientConnection.userSessionId,
|
||||||
|
establishedClientConnection.clientAddress);
|
||||||
|
|
||||||
// initialize distributed indicator value caches if possible and needed
|
// initialize distributed indicator value caches if possible and needed
|
||||||
if (examId != null && this.isDistributedSetup) {
|
if (examId != null && this.isDistributedSetup) {
|
||||||
|
@ -733,9 +745,9 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
this.clientIndicatorFactory.getIndicatorValues(clientConnection)));
|
this.clientIndicatorFactory.getIndicatorValues(clientConnection)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkExamRunning(final Long examId) {
|
private void checkExamRunning(final Long examId, final String user, final String address) {
|
||||||
if (examId != null && !this.examSessionService.isExamRunning(examId)) {
|
if (examId != null && !this.examSessionService.isExamRunning(examId)) {
|
||||||
examNotRunningException(examId);
|
examNotRunningException(examId, user, address);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -761,9 +773,10 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
return UUID.randomUUID().toString();
|
return UUID.randomUUID().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void examNotRunningException(final Long examId) {
|
private void examNotRunningException(final Long examId, final String user, final String address) {
|
||||||
log.error("The exam {} is not running", examId);
|
log.warn("The exam {} is not running. Called by: {} | on: {}", examId, user, address);
|
||||||
throw new IllegalStateException("The exam " + examId + " is not running");
|
throw new APIConstraintViolationException(
|
||||||
|
"The exam " + examId + " is not running");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkExamIntegrity(final Long examId, final ClientConnection clientConnection) {
|
private void checkExamIntegrity(final Long examId, final ClientConnection clientConnection) {
|
||||||
|
@ -776,7 +789,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Exam integrity violation: another examId is already set for the connection");
|
"Exam integrity violation: another examId is already set for the connection");
|
||||||
}
|
}
|
||||||
checkExamRunning(examId);
|
checkExamRunning(examId, clientConnection.userSessionId, clientConnection.clientAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ClientConnection updateUserSessionId(
|
private ClientConnection updateUserSessionId(
|
||||||
|
@ -835,7 +848,12 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
return clientConnection;
|
return clientConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkExamIntegrity(final Long examId, final Long institutionId) {
|
private void checkExamIntegrity(
|
||||||
|
final Long examId,
|
||||||
|
final Long institutionId,
|
||||||
|
final String user,
|
||||||
|
final String address) {
|
||||||
|
|
||||||
if (this.isDistributedSetup) {
|
if (this.isDistributedSetup) {
|
||||||
// if the cached Exam is not up to date anymore, we have to update the cache first
|
// if the cached Exam is not up to date anymore, we have to update the cache first
|
||||||
final Result<Exam> updateExamCache = this.examSessionService.updateExamCache(examId);
|
final Result<Exam> updateExamCache = this.examSessionService.updateExamCache(examId);
|
||||||
|
@ -845,7 +863,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
||||||
}
|
}
|
||||||
|
|
||||||
// check Exam is running and not locked
|
// check Exam is running and not locked
|
||||||
checkExamRunning(examId);
|
checkExamRunning(examId, user, address);
|
||||||
if (this.examSessionService.isExamLocked(examId)) {
|
if (this.examSessionService.isExamLocked(examId)) {
|
||||||
throw new APIConstraintViolationException(
|
throw new APIConstraintViolationException(
|
||||||
"Exam is currently on update and locked for new SEB Client connections");
|
"Exam is currently on update and locked for new SEB Client connections");
|
||||||
|
|
|
@ -192,7 +192,8 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler {
|
||||||
final APIConstraintViolationException ex,
|
final APIConstraintViolationException ex,
|
||||||
final WebRequest request) {
|
final WebRequest request) {
|
||||||
|
|
||||||
log.warn("Illegal API Argument Exception: ", ex);
|
log.warn("Illegal API Argument Exception: {}", ex.getMessage());
|
||||||
|
|
||||||
return APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT
|
return APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT
|
||||||
.createErrorResponse(ex.getMessage());
|
.createErrorResponse(ex.getMessage());
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ import ch.ethz.seb.sebserver.gbl.model.Page;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.PageSortOrder;
|
import ch.ethz.seb.sebserver.gbl.model.PageSortOrder;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
|
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
|
||||||
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.exam.Exam.ExamStatus;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
|
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
|
||||||
|
@ -213,6 +214,27 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequestMapping(
|
||||||
|
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||||
|
+ API.EXAM_ADMINISTRATION_ARCHIVE_PATH_SEGMENT,
|
||||||
|
method = RequestMethod.PATCH,
|
||||||
|
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public Exam archive(
|
||||||
|
@PathVariable final Long modelId,
|
||||||
|
@RequestParam(
|
||||||
|
name = API.PARAM_INSTITUTION_ID,
|
||||||
|
required = true,
|
||||||
|
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) {
|
||||||
|
|
||||||
|
checkWritePrivilege(institutionId);
|
||||||
|
return this.examDAO.byPK(modelId)
|
||||||
|
.flatMap(this::checkWriteAccess)
|
||||||
|
.flatMap(this::checkArchive)
|
||||||
|
.flatMap(exam -> this.examDAO.updateState(exam.id, ExamStatus.ARCHIVED, null))
|
||||||
|
.flatMap(this::logModify)
|
||||||
|
.getOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
// ****************************************************************************
|
// ****************************************************************************
|
||||||
// **** SEB Restriction
|
// **** SEB Restriction
|
||||||
|
|
||||||
|
@ -557,6 +579,15 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Result<Exam> checkArchive(final Exam exam) {
|
||||||
|
if (exam.status != ExamStatus.FINISHED) {
|
||||||
|
throw new APIMessageException(
|
||||||
|
APIMessage.ErrorMessage.INTEGRITY_VALIDATION.of("Exam is in wrong status to archive."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.of(exam);
|
||||||
|
}
|
||||||
|
|
||||||
static Function<Collection<Exam>, List<Exam>> pageSort(final String sort) {
|
static Function<Collection<Exam>, List<Exam>> pageSort(final String sort) {
|
||||||
|
|
||||||
final String sortBy = PageSortOrder.decode(sort);
|
final String sortBy = PageSortOrder.decode(sort);
|
||||||
|
|
|
@ -466,6 +466,8 @@ sebserver.exam.list.column.starttime=Start Time {0}
|
||||||
sebserver.exam.list.column.starttime.tooltip=The start time of the exam<br/><br/>Use the filter above to set a specific from date<br/>{0}
|
sebserver.exam.list.column.starttime.tooltip=The start time of the exam<br/><br/>Use the filter above to set a specific from date<br/>{0}
|
||||||
sebserver.exam.list.column.type=Type
|
sebserver.exam.list.column.type=Type
|
||||||
sebserver.exam.list.column.type.tooltip=The type of the exam<br/><br/>Use the filter above to set a specific exam type<br/>{0}
|
sebserver.exam.list.column.type.tooltip=The type of the exam<br/><br/>Use the filter above to set a specific exam type<br/>{0}
|
||||||
|
sebserver.exam.list.column.state=Status
|
||||||
|
sebserver.exam.list.column.state.tooltip=The current status of the exam<br/><br/>Use the filter above to set a specific exam status<br/>{0}
|
||||||
|
|
||||||
sebserver.exam.list.empty=No Exam can be found. Please adapt the filter or import one from LMS
|
sebserver.exam.list.empty=No Exam can be found. Please adapt the filter or import one from LMS
|
||||||
sebserver.exam.list.modify.out.dated=Finished exams cannot be modified.
|
sebserver.exam.list.modify.out.dated=Finished exams cannot be modified.
|
||||||
|
@ -492,6 +494,8 @@ sebserver.exam.action.activate=Activate Exam
|
||||||
sebserver.exam.action.deactivate=Deactivate Exam
|
sebserver.exam.action.deactivate=Deactivate Exam
|
||||||
sebserver.exam.action.delete=Delete Exam
|
sebserver.exam.action.delete=Delete Exam
|
||||||
sebserver.exam.action.delete.consistency.error=Deletion Failed.<br/>Please make sure there are no active SEB clients connected to the exam before deletion.
|
sebserver.exam.action.delete.consistency.error=Deletion Failed.<br/>Please make sure there are no active SEB clients connected to the exam before deletion.
|
||||||
|
sebserver.exam.action.archive=Archive
|
||||||
|
sebserver.exam.action.archive.confirm=An archived exam cannot be rerun and will remain in read-only view.<br/><br/>Are you sure to archive the exam?
|
||||||
sebserver.exam.action.sebrestriction.enable=Apply SEB Lock
|
sebserver.exam.action.sebrestriction.enable=Apply SEB Lock
|
||||||
sebserver.exam.action.sebrestriction.disable=Release SEB Lock
|
sebserver.exam.action.sebrestriction.disable=Release SEB Lock
|
||||||
sebserver.exam.action.sebrestriction.details=SEB Restriction Details
|
sebserver.exam.action.sebrestriction.details=SEB Restriction Details
|
||||||
|
@ -584,6 +588,7 @@ sebserver.exam.type.VDI.tooltip=Exam type specified for Virtual Desktop Infrastr
|
||||||
sebserver.exam.status.UP_COMING=Up Coming
|
sebserver.exam.status.UP_COMING=Up Coming
|
||||||
sebserver.exam.status.RUNNING=Running
|
sebserver.exam.status.RUNNING=Running
|
||||||
sebserver.exam.status.FINISHED=Finished
|
sebserver.exam.status.FINISHED=Finished
|
||||||
|
sebserver.exam.status.ARCHIVED=Archived
|
||||||
sebserver.exam.status.CORRUPT_NO_LMS_CONNECTION=Corrupt (No LMS Connection)
|
sebserver.exam.status.CORRUPT_NO_LMS_CONNECTION=Corrupt (No LMS Connection)
|
||||||
sebserver.exam.status.CORRUPT_INVALID_ID=Corrupt (Invalid Identifier)
|
sebserver.exam.status.CORRUPT_INVALID_ID=Corrupt (Invalid Identifier)
|
||||||
|
|
||||||
|
@ -1937,6 +1942,8 @@ sebserver.finished.exam.list.empty=There are currently no finished exams
|
||||||
|
|
||||||
sebserver.finished.exam.list.column.name=Name
|
sebserver.finished.exam.list.column.name=Name
|
||||||
sebserver.finished.exam.list.column.name.tooltip=The name of the exam<br/><br/>Use the filter above to narrow down to a specific exam name<br/>{0}
|
sebserver.finished.exam.list.column.name.tooltip=The name of the exam<br/><br/>Use the filter above to narrow down to a specific exam name<br/>{0}
|
||||||
|
sebserver.finished.exam.list.column.state=State
|
||||||
|
sebserver.finished.exam.list.column.state.tooltip=The state of the finished exam<br/><br/>Use the filter above to see only archived or finished exams
|
||||||
sebserver.finished.exam.list.column.type=Type
|
sebserver.finished.exam.list.column.type=Type
|
||||||
sebserver.finished.exam.list.column.type.tooltip=The type of the exam<br/><br/>Use the filter above to set a specific exam type<br/>{0}
|
sebserver.finished.exam.list.column.type.tooltip=The type of the exam<br/><br/>Use the filter above to set a specific exam type<br/>{0}
|
||||||
sebserver.finished.exam.list.column.startTime=Start Time {0}
|
sebserver.finished.exam.list.column.startTime=Start Time {0}
|
||||||
|
|
BIN
src/main/resources/static/images/archive.png
Normal file
BIN
src/main/resources/static/images/archive.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 139 B |
|
@ -62,7 +62,6 @@ import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport.ErrorEntry;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.Page;
|
import ch.ethz.seb.sebserver.gbl.model.Page;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
|
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
|
||||||
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.exam.Exam.ExamStatus;
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType;
|
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ExamTemplate;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ExamTemplate;
|
||||||
|
@ -891,7 +890,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
|
||||||
ExamType.MANAGED,
|
ExamType.MANAGED,
|
||||||
null,
|
null,
|
||||||
Utils.immutableCollectionOf(userId),
|
Utils.immutableCollectionOf(userId),
|
||||||
ExamStatus.RUNNING,
|
null,
|
||||||
false,
|
false,
|
||||||
null,
|
null,
|
||||||
true,
|
true,
|
||||||
|
|
|
@ -170,7 +170,7 @@ public class SebConnectionTest extends ExamAPIIntegrationTester {
|
||||||
new TypeReference<Collection<APIMessage>>() {
|
new TypeReference<Collection<APIMessage>>() {
|
||||||
});
|
});
|
||||||
final APIMessage error = errorMessage.iterator().next();
|
final APIMessage error = errorMessage.iterator().next();
|
||||||
assertEquals(ErrorMessage.UNEXPECTED.messageCode, error.messageCode);
|
assertEquals(ErrorMessage.ILLEGAL_API_ARGUMENT.messageCode, error.messageCode);
|
||||||
assertEquals("The exam 1 is not running", error.details);
|
assertEquals("The exam 1 is not running", error.details);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue