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