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…
	
	Add table
		
		Reference in a new issue
	
	 anhefti
						anhefti