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_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_INCLUDE_RESTRICTION = "include-restriction";
|
||||
public static final String EXAM_ADMINISTRATION_SEB_RESTRICTION_PATH_SEGMENT = "/seb-restriction";
|
||||
|
|
|
@ -291,6 +291,11 @@ public enum ActionDefinition {
|
|||
ImageIcon.DELETE,
|
||||
PageStateDefinitionImpl.EXAM_VIEW,
|
||||
ActionCategory.FORM),
|
||||
EXAM_ARCHIVE(
|
||||
new LocTextKey("sebserver.exam.action.archive"),
|
||||
ImageIcon.ARCHIVE,
|
||||
PageStateDefinitionImpl.EXAM_VIEW,
|
||||
ActionCategory.FORM),
|
||||
|
||||
EXAM_MODIFY_SEB_RESTRICTION_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.webservice.api.RestCallError;
|
||||
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.CheckSEBRestriction;
|
||||
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");
|
||||
private static final LocTextKey FORM_EXAM_TEMPLATE_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 =
|
||||
new LocTextKey("sebserver.exam.consistency.title");
|
||||
|
@ -412,6 +415,12 @@ public class ExamForm implements TemplateComposer {
|
|||
.withExec(this.examDeletePopup.deleteWizardFunction(pageContext))
|
||||
.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)
|
||||
.withExec(action -> (importFromQuizData)
|
||||
? 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() {
|
||||
return this.restService.getBuilder(GetDefaultExamTemplate.class)
|
||||
.call()
|
||||
|
|
|
@ -77,6 +77,8 @@ public class ExamList implements TemplateComposer {
|
|||
new LocTextKey("sebserver.exam.list.column.lmssetup");
|
||||
public final static LocTextKey COLUMN_TITLE_NAME_KEY =
|
||||
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 =
|
||||
new LocTextKey("sebserver.exam.list.column.type");
|
||||
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 nameFilter =
|
||||
new TableFilterAttribute(CriteriaType.TEXT, QuizData.FILTER_ATTR_NAME);
|
||||
private final TableFilterAttribute stateFilter;
|
||||
private final TableFilterAttribute typeFilter;
|
||||
|
||||
private final PageService pageService;
|
||||
|
@ -112,6 +115,11 @@ public class ExamList implements TemplateComposer {
|
|||
LmsSetup.FILTER_ATTR_LMS_SETUP,
|
||||
this.resourceService::lmsSetupResource);
|
||||
|
||||
this.stateFilter = new TableFilterAttribute(
|
||||
CriteriaType.SINGLE_SELECTION,
|
||||
Exam.FILTER_ATTR_STATUS,
|
||||
this.resourceService::localizedExamStatusSelection);
|
||||
|
||||
this.typeFilter = new TableFilterAttribute(
|
||||
CriteriaType.SINGLE_SELECTION,
|
||||
Exam.FILTER_ATTR_TYPE,
|
||||
|
@ -189,6 +197,13 @@ public class ExamList implements TemplateComposer {
|
|||
.toString()))
|
||||
.sortable())
|
||||
|
||||
.withColumn(new ColumnDefinition<>(
|
||||
Domain.EXAM.ATTR_STATUS,
|
||||
COLUMN_TITLE_STATE_KEY,
|
||||
this.resourceService::localizedExamStatusName)
|
||||
.withFilter(this.stateFilter)
|
||||
.sortable())
|
||||
|
||||
.withColumn(new ColumnDefinition<Exam>(
|
||||
Domain.EXAM.ATTR_TYPE,
|
||||
COLUMN_TITLE_TYPE_KEY,
|
||||
|
|
|
@ -48,6 +48,8 @@ public class FinishedExamList implements TemplateComposer {
|
|||
new LocTextKey("sebserver.finished.exam.info.pleaseSelect");
|
||||
private final static LocTextKey COLUMN_TITLE_NAME_KEY =
|
||||
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 =
|
||||
new LocTextKey("sebserver.finished.exam.list.column.type");
|
||||
private final static LocTextKey EMPTY_LIST_TEXT_KEY =
|
||||
|
@ -56,6 +58,7 @@ public class FinishedExamList implements TemplateComposer {
|
|||
private final TableFilterAttribute nameFilter =
|
||||
new TableFilterAttribute(CriteriaType.TEXT, QuizData.FILTER_ATTR_NAME);
|
||||
private final TableFilterAttribute typeFilter;
|
||||
private final TableFilterAttribute stateFilter;
|
||||
|
||||
private final PageService pageService;
|
||||
private final ResourceService resourceService;
|
||||
|
@ -73,6 +76,11 @@ public class FinishedExamList implements TemplateComposer {
|
|||
CriteriaType.SINGLE_SELECTION,
|
||||
Exam.FILTER_ATTR_TYPE,
|
||||
this.resourceService::examTypeResources);
|
||||
|
||||
this.stateFilter = new TableFilterAttribute(
|
||||
CriteriaType.SINGLE_SELECTION,
|
||||
Exam.FILTER_ATTR_STATUS,
|
||||
this.resourceService::localizedFinishedExamStatusSelection);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -105,6 +113,13 @@ public class FinishedExamList implements TemplateComposer {
|
|||
.withFilter(this.nameFilter)
|
||||
.sortable())
|
||||
|
||||
.withColumn(new ColumnDefinition<>(
|
||||
Domain.EXAM.ATTR_STATUS,
|
||||
COLUMN_TITLE_STATE_KEY,
|
||||
this.resourceService::localizedExamStatusName)
|
||||
.withFilter(this.stateFilter)
|
||||
.sortable())
|
||||
|
||||
.withColumn(new ColumnDefinition<Exam>(
|
||||
Domain.EXAM.ATTR_TYPE,
|
||||
COLUMN_TITLE_TYPE_KEY,
|
||||
|
|
|
@ -118,6 +118,7 @@ public class ResourceService {
|
|||
public static final EnumSet<EventType> CLIENT_EVENT_TYPE_EXCLUDE_MAP = EnumSet.of(
|
||||
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 EXAM_TYPE_PREFIX = "sebserver.exam.type.";
|
||||
public static final String USERACCOUNT_ROLE_PREFIX = "sebserver.useraccount.role.";
|
||||
|
@ -548,6 +549,32 @@ public class ResourceService {
|
|||
.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) {
|
||||
if (config.status == null) {
|
||||
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"),
|
||||
NEW("new.png"),
|
||||
DELETE("delete.png"),
|
||||
ARCHIVE("archive.png"),
|
||||
SEARCH("lens.png"),
|
||||
UNDO("undo.png"),
|
||||
COLOR("color.png"),
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
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.
|
||||
*
|
||||
* @param institutionId the institution identifier. May be null for all institutions
|
||||
* @param status the ExamStatus
|
||||
* @param filterMap FilterMap with other filter criteria
|
||||
* @param status the list of ExamStatus
|
||||
* @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.
|
||||
*
|
||||
|
|
|
@ -34,6 +34,8 @@ import org.springframework.transaction.annotation.Propagation;
|
|||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
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.model.EntityDependency;
|
||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||
|
@ -118,38 +120,42 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
final String name = filterMap.getQuizName();
|
||||
final DateTime from = filterMap.getExamFromTime();
|
||||
final Predicate<Exam> quizDataFilter = exam -> {
|
||||
if (StringUtils.isNotBlank(name)) {
|
||||
if (!exam.name.contains(name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (from != null && exam.startTime != null) {
|
||||
// always show exams that has not ended yet
|
||||
if (exam.endTime == null || exam.endTime.isAfter(from)) {
|
||||
return true;
|
||||
}
|
||||
if (exam.startTime.isBefore(from)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
final Predicate<Exam> examDataFilter = createPredicate(filterMap);
|
||||
return this.examRecordDAO
|
||||
.allMatching(filterMap)
|
||||
.allMatching(filterMap, null)
|
||||
.flatMap(this::toDomainModel)
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
.filter(quizDataFilter.and(predicate))
|
||||
.filter(examDataFilter.and(predicate))
|
||||
.collect(Collectors.toList());
|
||||
});
|
||||
}
|
||||
|
||||
private Predicate<Exam> createPredicate(final FilterMap filterMap) {
|
||||
final String name = filterMap.getQuizName();
|
||||
final DateTime from = filterMap.getExamFromTime();
|
||||
final Predicate<Exam> quizDataFilter = exam -> {
|
||||
if (StringUtils.isNotBlank(name)) {
|
||||
if (!exam.name.contains(name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (from != null && exam.startTime != null) {
|
||||
// always show exams that has not ended yet
|
||||
if (exam.endTime == null || exam.endTime.isAfter(from)) {
|
||||
return true;
|
||||
}
|
||||
if (exam.startTime.isBefore(from)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
return quizDataFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Exam> updateState(final Long examId, final ExamStatus status, final String updateId) {
|
||||
return this.examRecordDAO
|
||||
|
@ -159,9 +165,9 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
|
||||
@Override
|
||||
public Result<Exam> save(final Exam exam) {
|
||||
return this.examRecordDAO
|
||||
.save(exam)
|
||||
.map(rec -> saveAdditionalAttributes(exam, rec))
|
||||
return this.checkStateEdit(exam)
|
||||
.flatMap(this.examRecordDAO::save)
|
||||
.flatMap(rec -> saveAdditionalAttributes(exam, rec))
|
||||
.flatMap(this::toDomainModel);
|
||||
}
|
||||
|
||||
|
@ -201,7 +207,7 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
public Result<Exam> createNew(final Exam exam) {
|
||||
return this.examRecordDAO
|
||||
.createNew(exam)
|
||||
.map(rec -> saveAdditionalAttributes(exam, rec))
|
||||
.flatMap(rec -> saveAdditionalAttributes(exam, rec))
|
||||
.flatMap(this::toDomainModel);
|
||||
}
|
||||
|
||||
|
@ -248,22 +254,26 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Result<Collection<Long>> getExamIdsForStatus(final Long institutionId, final ExamStatus status) {
|
||||
return Result.tryCatch(() -> this.examRecordMapper.selectIdsByExample()
|
||||
.where(
|
||||
ExamRecordDynamicSqlSupport.active,
|
||||
isEqualTo(BooleanUtils.toInteger(true)))
|
||||
.and(
|
||||
ExamRecordDynamicSqlSupport.institutionId,
|
||||
isEqualToWhenPresent(institutionId))
|
||||
.and(
|
||||
ExamRecordDynamicSqlSupport.status,
|
||||
isEqualTo(status.name()))
|
||||
.and(
|
||||
ExamRecordDynamicSqlSupport.updating,
|
||||
isEqualTo(BooleanUtils.toInteger(false)))
|
||||
.build()
|
||||
.execute());
|
||||
public Result<Collection<Exam>> getExamIdsForStatus(
|
||||
final FilterMap filterMap,
|
||||
final Predicate<Exam> predicate,
|
||||
final ExamStatus... status) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
final List<String> stateNames = (status != null && status.length > 0)
|
||||
? Arrays.asList(status)
|
||||
.stream().map(s -> s.name())
|
||||
.collect(Collectors.toList())
|
||||
: null;
|
||||
final Predicate<Exam> examDataFilter = createPredicate(filterMap);
|
||||
return this.examRecordDAO.allMatching(filterMap, stateNames)
|
||||
.flatMap(this::toDomainModel)
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
.filter(examDataFilter.and(predicate))
|
||||
.collect(Collectors.toList());
|
||||
});
|
||||
}
|
||||
|
||||
@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) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
|
@ -817,16 +752,19 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
});
|
||||
}
|
||||
|
||||
private ExamRecord saveAdditionalAttributes(final Exam exam, final ExamRecord rec) {
|
||||
if (exam.additionalAttributesIncluded()) {
|
||||
this.additionalAttributesDAO.saveAdditionalAttributes(
|
||||
EntityType.EXAM,
|
||||
rec.getId(),
|
||||
exam.additionalAttributes)
|
||||
.getOrThrow();
|
||||
}
|
||||
private Result<ExamRecord> saveAdditionalAttributes(final Exam exam, final ExamRecord rec) {
|
||||
return Result.tryCatch(() -> {
|
||||
if (exam.additionalAttributesIncluded()) {
|
||||
this.additionalAttributesDAO.saveAdditionalAttributes(
|
||||
EntityType.EXAM,
|
||||
rec.getId(),
|
||||
exam.additionalAttributes)
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
return rec;
|
||||
return rec;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private QuizData saveAdditionalQuizAttributes(final Long examId, final QuizData quizData) {
|
||||
|
@ -843,4 +781,14 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
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.select.MyBatis3SelectModelAdapter;
|
||||
import org.mybatis.dynamic.sql.select.QueryExpressionDSL;
|
||||
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;
|
||||
|
@ -52,6 +54,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
|
|||
@WebServiceProfile
|
||||
public class ExamRecordDAO {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ExamRecordDAO.class);
|
||||
|
||||
private final ExamRecordMapper examRecordMapper;
|
||||
private final ClientConnectionRecordMapper clientConnectionRecordMapper;
|
||||
|
||||
|
@ -133,13 +137,13 @@ public class ExamRecordDAO {
|
|||
}
|
||||
|
||||
@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(() -> {
|
||||
|
||||
// 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
|
||||
final QueryExpressionDSL<MyBatis3SelectModelAdapter<List<ExamRecord>>>.QueryExpressionWhereBuilder whereClause =
|
||||
QueryExpressionDSL<MyBatis3SelectModelAdapter<List<ExamRecord>>>.QueryExpressionWhereBuilder whereClause =
|
||||
(filterMap.getBoolean(FilterMap.ATTR_ADD_INSITUTION_JOIN))
|
||||
? this.examRecordMapper
|
||||
.selectByExample()
|
||||
|
@ -165,7 +169,9 @@ public class ExamRecordDAO {
|
|||
ExamRecordDynamicSqlSupport.active,
|
||||
isEqualToWhenPresent(filterMap.getActiveAsInt()));
|
||||
|
||||
final List<ExamRecord> records = whereClause
|
||||
//
|
||||
|
||||
whereClause = whereClause
|
||||
.and(
|
||||
ExamRecordDynamicSqlSupport.institutionId,
|
||||
isEqualToWhenPresent(filterMap.getInstitutionId()))
|
||||
|
@ -174,10 +180,23 @@ public class ExamRecordDAO {
|
|||
isEqualToWhenPresent(filterMap.getLmsSetupId()))
|
||||
.and(
|
||||
ExamRecordDynamicSqlSupport.type,
|
||||
isEqualToWhenPresent(filterMap.getExamType()))
|
||||
.and(
|
||||
ExamRecordDynamicSqlSupport.status,
|
||||
isEqualToWhenPresent(filterMap.getExamStatus()))
|
||||
isEqualToWhenPresent(filterMap.getExamType()));
|
||||
|
||||
final String examStatus = filterMap.getExamStatus();
|
||||
if (StringUtils.isNotBlank(examStatus)) {
|
||||
whereClause = whereClause
|
||||
.and(
|
||||
ExamRecordDynamicSqlSupport.status,
|
||||
isEqualToWhenPresent(examStatus));
|
||||
} else if (stateNames != null && !stateNames.isEmpty()) {
|
||||
whereClause = whereClause
|
||||
.and(
|
||||
ExamRecordDynamicSqlSupport.status,
|
||||
isInWhenPresent(stateNames));
|
||||
}
|
||||
|
||||
final List<ExamRecord> records = whereClause
|
||||
|
||||
.and(
|
||||
ExamRecordDynamicSqlSupport.quizName,
|
||||
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) {
|
||||
return recordById(examId)
|
||||
.map(examRecord -> {
|
||||
if (BooleanUtils.isTrue(BooleanUtils.toBooleanObject(examRecord.getUpdating()))) {
|
||||
if (updateId != null &&
|
||||
BooleanUtils.isTrue(BooleanUtils.toBooleanObject(examRecord.getUpdating()))) {
|
||||
|
||||
if (!updateId.equals(examRecord.getLastupdate())) {
|
||||
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);
|
||||
}
|
||||
|
||||
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(
|
||||
exam.id,
|
||||
null, null, null, null,
|
||||
|
@ -232,9 +260,7 @@ public class ExamRecordDAO {
|
|||
: null,
|
||||
null,
|
||||
exam.browserExamKeys,
|
||||
(exam.status != null)
|
||||
? exam.status.name()
|
||||
: null,
|
||||
null,
|
||||
1, // seb restriction (deprecated)
|
||||
null, // updating
|
||||
null, // lastUpdate
|
||||
|
@ -412,6 +438,9 @@ public class ExamRecordDAO {
|
|||
.and(
|
||||
ExamRecordDynamicSqlSupport.status,
|
||||
isNotEqualTo(ExamStatus.RUNNING.name()))
|
||||
.and(
|
||||
ExamRecordDynamicSqlSupport.status,
|
||||
isNotEqualTo(ExamStatus.ARCHIVED.name()))
|
||||
.and(
|
||||
ExamRecordDynamicSqlSupport.updating,
|
||||
isEqualTo(BooleanUtils.toInteger(false)))
|
||||
|
@ -430,6 +459,9 @@ public class ExamRecordDAO {
|
|||
.and(
|
||||
ExamRecordDynamicSqlSupport.status,
|
||||
isEqualTo(ExamStatus.RUNNING.name()))
|
||||
.and(
|
||||
ExamRecordDynamicSqlSupport.status,
|
||||
isNotEqualTo(ExamStatus.ARCHIVED.name()))
|
||||
.and(
|
||||
ExamRecordDynamicSqlSupport.updating,
|
||||
isEqualTo(BooleanUtils.toInteger(false)))
|
||||
|
|
|
@ -266,8 +266,11 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
final FilterMap filterMap,
|
||||
final Predicate<Exam> predicate) {
|
||||
|
||||
filterMap.putIfAbsent(Exam.FILTER_ATTR_STATUS, ExamStatus.FINISHED.name());
|
||||
return this.examDAO.allMatching(filterMap, predicate);
|
||||
return this.examDAO.getExamIdsForStatus(
|
||||
filterMap,
|
||||
predicate,
|
||||
ExamStatus.FINISHED,
|
||||
ExamStatus.ARCHIVED);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -149,7 +149,11 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
}
|
||||
|
||||
if (examId != null) {
|
||||
checkExamIntegrity(examId, institutionId);
|
||||
checkExamIntegrity(
|
||||
examId,
|
||||
institutionId,
|
||||
(principal != null) ? principal.getName() : "--",
|
||||
clientAddress);
|
||||
}
|
||||
|
||||
// Create ClientConnection in status CONNECTION_REQUESTED for further processing
|
||||
|
@ -233,7 +237,11 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
}
|
||||
|
||||
if (examId != null) {
|
||||
checkExamIntegrity(examId, institutionId);
|
||||
checkExamIntegrity(
|
||||
examId,
|
||||
institutionId,
|
||||
StringUtils.isNoneBlank(userSessionId) ? userSessionId : clientConnection.userSessionId,
|
||||
clientConnection.clientAddress);
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
|
@ -419,7 +427,11 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
.getOrThrow();
|
||||
|
||||
// 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
|
||||
if (examId != null && this.isDistributedSetup) {
|
||||
|
@ -733,9 +745,9 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
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)) {
|
||||
examNotRunningException(examId);
|
||||
examNotRunningException(examId, user, address);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -761,9 +773,10 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
private void examNotRunningException(final Long examId) {
|
||||
log.error("The exam {} is not running", examId);
|
||||
throw new IllegalStateException("The exam " + examId + " is not running");
|
||||
private void examNotRunningException(final Long examId, final String user, final String address) {
|
||||
log.warn("The exam {} is not running. Called by: {} | on: {}", examId, user, address);
|
||||
throw new APIConstraintViolationException(
|
||||
"The exam " + examId + " is not running");
|
||||
}
|
||||
|
||||
private void checkExamIntegrity(final Long examId, final ClientConnection clientConnection) {
|
||||
|
@ -776,7 +789,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
throw new IllegalArgumentException(
|
||||
"Exam integrity violation: another examId is already set for the connection");
|
||||
}
|
||||
checkExamRunning(examId);
|
||||
checkExamRunning(examId, clientConnection.userSessionId, clientConnection.clientAddress);
|
||||
}
|
||||
|
||||
private ClientConnection updateUserSessionId(
|
||||
|
@ -835,7 +848,12 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
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 the cached Exam is not up to date anymore, we have to update the cache first
|
||||
final Result<Exam> updateExamCache = this.examSessionService.updateExamCache(examId);
|
||||
|
@ -845,7 +863,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
}
|
||||
|
||||
// check Exam is running and not locked
|
||||
checkExamRunning(examId);
|
||||
checkExamRunning(examId, user, address);
|
||||
if (this.examSessionService.isExamLocked(examId)) {
|
||||
throw new APIConstraintViolationException(
|
||||
"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 WebRequest request) {
|
||||
|
||||
log.warn("Illegal API Argument Exception: ", ex);
|
||||
log.warn("Illegal API Argument Exception: {}", ex.getMessage());
|
||||
|
||||
return APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT
|
||||
.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.exam.Chapters;
|
||||
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.QuizData;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
|
||||
|
@ -213,6 +214,27 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
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
|
||||
|
||||
|
@ -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) {
|
||||
|
||||
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.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.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.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.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.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.disable=Release SEB Lock
|
||||
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.RUNNING=Running
|
||||
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_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.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.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}
|
||||
|
|
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.exam.Chapters;
|
||||
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.ExamConfigurationMap;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ExamTemplate;
|
||||
|
@ -891,7 +890,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
|
|||
ExamType.MANAGED,
|
||||
null,
|
||||
Utils.immutableCollectionOf(userId),
|
||||
ExamStatus.RUNNING,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
true,
|
||||
|
|
|
@ -170,7 +170,7 @@ public class SebConnectionTest extends ExamAPIIntegrationTester {
|
|||
new TypeReference<Collection<APIMessage>>() {
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue