diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java index e0ff73c4..7394aeeb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java @@ -91,7 +91,7 @@ public final class API { public static final String INSTITUTION_ENDPOINT = "/institution"; - public static final String LMS_SETUP_ENDPOINT = "/lms_setup"; + public static final String LMS_SETUP_ENDPOINT = "/lms-setup"; public static final String LMS_SETUP_TEST_PATH_SEGMENT = "/test"; public static final String LMS_SETUP_TEST_ENDPOINT = LMS_SETUP_ENDPOINT + LMS_SETUP_TEST_PATH_SEGMENT @@ -106,18 +106,19 @@ public final class API { public static final String QUIZ_DISCOVERY_ENDPOINT = "/quiz"; public static final String EXAM_ADMINISTRATION_ENDPOINT = "/exam"; - public static final String EXAM_ADMINISTRATION_DOWNLOAD_CONFIG_PATH_SEGMENT = "/downloadConfig"; + public static final String EXAM_ADMINISTRATION_DOWNLOAD_CONFIG_PATH_SEGMENT = "/download-config"; + public static final String EXAM_ADMINISTRATION_CONSISTENCY_CHECK_PATH_SEGMENT = "/check-consistency"; public static final String EXAM_INDICATOR_ENDPOINT = "/indicator"; public static final String SEB_CLIENT_CONFIG_ENDPOINT = "/client_configuration"; public static final String SEB_CLIENT_CONFIG_DOWNLOAD_PATH_SEGMENT = "/download"; - public static final String CONFIGURATION_NODE_ENDPOINT = "/configuration_node"; + public static final String CONFIGURATION_NODE_ENDPOINT = "/configuration-node"; public static final String CONFIGURATION_FOLLOWUP_PATH_SEGMENT = "/followup"; public static final String CONFIGURATION_CONFIG_KEY_PATH_SEGMENT = "/configkey"; public static final String CONFIGURATION_ENDPOINT = "/configuration"; - public static final String CONFIGURATION_SAVE_TO_HISTORY_PATH_SEGMENT = "/save_to_history"; + public static final String CONFIGURATION_SAVE_TO_HISTORY_PATH_SEGMENT = "/save-to-history"; public static final String CONFIGURATION_UNDO_PATH_SEGMENT = "/undo"; public static final String CONFIGURATION_COPY_PATH_SEGMENT = "/copy"; public static final String CONFIGURATION_RESTORE_FROM_HISTORY_PATH_SEGMENT = "/restore"; @@ -137,7 +138,7 @@ public final class API { public static final String ORIENTATION_ENDPOINT = "/orientation"; public static final String VIEW_ENDPOINT = ORIENTATION_ENDPOINT + "/view"; - public static final String EXAM_CONFIGURATION_MAP_ENDPOINT = "/exam_configuration_map"; + public static final String EXAM_CONFIGURATION_MAP_ENDPOINT = "/exam-configuration-map"; public static final String USER_ACTIVITY_LOG_ENDPOINT = "/useractivity"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/APIMessage.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/APIMessage.java index 44e378cc..5ebe8abb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/APIMessage.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/APIMessage.java @@ -43,9 +43,12 @@ public class APIMessage implements Serializable { ILLEGAL_API_ARGUMENT("1010", HttpStatus.BAD_REQUEST, "Illegal API request argument"), UNEXPECTED("1100", HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected intenral server-side error"), FIELD_VALIDATION("1200", HttpStatus.BAD_REQUEST, "Field validation error"), - PASSWORD_MISMATCH("1300", HttpStatus.BAD_REQUEST, "new password do not match confirmed password") + INTEGRITY_VALIDATION("1201", HttpStatus.BAD_REQUEST, "Action would lied to an integrity violation"), + PASSWORD_MISMATCH("1300", HttpStatus.BAD_REQUEST, "new password do not match confirmed password"), - ; + EXAM_CONSISTANCY_VALIDATION_SUPPORTER("1400", HttpStatus.OK, "No Exam Supporter defined for the Exam"), + EXAM_CONSISTANCY_VALIDATION_CONFIG("1401", HttpStatus.OK, "No SEB Exam Configuration defined for the Exam"), + EXAM_CONSISTANCY_VALIDATION_INDICATOR("1402", HttpStatus.OK, "No Indicator defined for the Exam"); public final String messageCode; public final HttpStatus httpStatus; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java index 8b6f0e49..f50b6011 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import org.apache.commons.lang3.StringUtils; @@ -115,6 +116,7 @@ public final class Exam implements GrantEntity, Activatable { public final String owner; @JsonProperty(EXAM.ATTR_SUPPORTER) + @NotEmpty(message = "exam:supporter:notNull") public final Collection supporter; /** Indicates whether this Exam is active or not */ diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/sebconfig/ConfigCopyInfo.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/sebconfig/ConfigCopyInfo.java new file mode 100644 index 00000000..6c5e6942 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/sebconfig/ConfigCopyInfo.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2019 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.gbl.model.sebconfig; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.Domain.CONFIGURATION_NODE; +import ch.ethz.seb.sebserver.gbl.model.Entity; + +public final class ConfigCopyInfo implements Entity { + + public static final String ATTR_COPY_WITH_HISTORY = "with-history"; + + @NotNull + @JsonProperty(CONFIGURATION_NODE.ATTR_ID) + public final Long configurationNodeId; + + @NotNull(message = "configurationNode:name:notNull") + @Size(min = 3, max = 255, message = "configurationNode:name:size:{min}:{max}:${validatedValue}") + @JsonProperty(CONFIGURATION_NODE.ATTR_NAME) + public final String name; + + @Size(max = 4000, message = "configurationNode:description:size:{min}:{max}:${validatedValue}") + @JsonProperty(CONFIGURATION_NODE.ATTR_DESCRIPTION) + public final String description; + + @JsonProperty(ATTR_COPY_WITH_HISTORY) + public final Boolean withHistory; + + protected ConfigCopyInfo( + @JsonProperty(CONFIGURATION_NODE.ATTR_ID) final Long configurationNodeId, + @JsonProperty(CONFIGURATION_NODE.ATTR_NAME) final String name, + @JsonProperty(CONFIGURATION_NODE.ATTR_DESCRIPTION) final String description, + @JsonProperty(ATTR_COPY_WITH_HISTORY) final Boolean withHistory) { + + this.configurationNodeId = configurationNodeId; + this.name = name; + this.description = description; + this.withHistory = withHistory; + } + + public Long getConfigurationNodeId() { + return this.configurationNodeId; + } + + @Override + public String getName() { + return this.name; + } + + public String getDescription() { + return this.description; + } + + public Boolean getWithHistory() { + return this.withHistory; + } + + @Override + public String getModelId() { + return (this.configurationNodeId != null) + ? String.valueOf(this.configurationNodeId) + : null; + } + + @Override + public EntityType entityType() { + return EntityType.CONFIGURATION_NODE; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("ConfigCopyInfo [configurationNodeId="); + builder.append(this.configurationNodeId); + builder.append(", name="); + builder.append(this.name); + builder.append(", description="); + builder.append(this.description); + builder.append(", withHistory="); + builder.append(this.withHistory); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/sebconfig/ConfigurationNode.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/sebconfig/ConfigurationNode.java index d7f59eb3..f2640c23 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/sebconfig/ConfigurationNode.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/sebconfig/ConfigurationNode.java @@ -25,7 +25,6 @@ import ch.ethz.seb.sebserver.gbl.model.GrantEntity; public final class ConfigurationNode implements GrantEntity { public static final Long DEFAULT_TEMPLATE_ID = 0L; - public static final String ATTR_COPY_WITH_HISTORY = "with-history"; public static final String FILTER_ATTR_TEMPLATE_ID = "templateId"; public static final String FILTER_ATTR_DESCRIPTION = "description"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java index 756cdd78..22d6e690 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java @@ -9,10 +9,12 @@ package ch.ethz.seb.sebserver.gui.content; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.function.BooleanSupplier; +import java.util.function.Function; import java.util.function.Supplier; import org.apache.commons.lang3.BooleanUtils; @@ -28,6 +30,8 @@ import org.springframework.stereotype.Component; import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.APIMessage; +import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage; import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.EntityKey; @@ -53,10 +57,10 @@ import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder; import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; import ch.ethz.seb.sebserver.gui.service.page.event.ActionEvent; import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; -import ch.ethz.seb.sebserver.gui.service.page.impl.PageState; import ch.ethz.seb.sebserver.gui.service.remote.download.DownloadService; import ch.ethz.seb.sebserver.gui.service.remote.download.SebExamConfigDownload; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckExamConsistency; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeleteExamConfigMapping; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeleteIndicator; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam; @@ -126,10 +130,21 @@ public class ExamForm implements TemplateComposer { private final static LocTextKey INDICATOR_EMPTY_SELECTION_TEXT_KEY = new LocTextKey("sebserver.exam.indicator.list.pleaseSelect"); + private final static LocTextKey CONSISTENCY_MESSAGE_TITLE = + new LocTextKey("sebserver.exam.consistency.title"); + private final static LocTextKey CONSISTENCY_MESSAGE_MISSING_SUPPORTER = + new LocTextKey("sebserver.exam.consistency.missing-supporter"); + private final static LocTextKey CONSISTENCY_MESSAGE_MISSING_CONFIG = + new LocTextKey("sebserver.exam.consistency.missing-config"); + + private final static LocTextKey CONFIRM_MESSAGE_REMOVE_CONFIG = + new LocTextKey("sebserver.exam.confirm.remove-config"); + private final PageService pageService; private final ResourceService resourceService; private final DownloadService downloadService; private final String downloadFileName; + private final WidgetFactory widgetFactory; protected ExamForm( final PageService pageService, @@ -141,15 +156,15 @@ public class ExamForm implements TemplateComposer { this.resourceService = resourceService; this.downloadService = downloadService; this.downloadFileName = downloadFileName; + this.widgetFactory = pageService.getWidgetFactory(); } @Override public void compose(final PageContext pageContext) { final CurrentUser currentUser = this.resourceService.getCurrentUser(); final RestService restService = this.resourceService.getRestService(); - final WidgetFactory widgetFactory = this.pageService.getWidgetFactory(); - final I18nSupport i18nSupport = this.resourceService.getI18nSupport(); + final I18nSupport i18nSupport = this.resourceService.getI18nSupport(); final EntityKey entityKey = pageContext.getEntityKey(); final EntityKey parentEntityKey = pageContext.getParentEntityKey(); final boolean readonly = pageContext.isReadonly(); @@ -173,12 +188,20 @@ public class ExamForm implements TemplateComposer { // new PageContext with actual EntityKey final PageContext formContext = pageContext.withEntityKey(exam.getEntityKey()); + // check exam consistency and inform the user if needed + if (readonly) { + restService.getBuilder(CheckExamConsistency.class) + .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) + .call() + .ifPresent(result -> showConsistencyChecks(result, formContext.getParent())); + } + // the default page layout with title final LocTextKey titleKey = new LocTextKey( importFromQuizData ? "sebserver.exam.form.title.import" : "sebserver.exam.form.title"); - final Composite content = widgetFactory.defaultPageLayout( + final Composite content = this.widgetFactory.defaultPageLayout( formContext.getParent(), titleKey); @@ -187,9 +210,10 @@ public class ExamForm implements TemplateComposer { final EntityGrantCheck userGrantCheck = currentUser.entityGrantCheck(exam); final boolean modifyGrant = userGrantCheck.m(); final ExamStatus examStatus = exam.getStatus(); - final boolean editable = examStatus == ExamStatus.UP_COMING || - examStatus == ExamStatus.RUNNING && - currentUser.get().hasRole(UserRole.EXAM_ADMIN); + final boolean isExamRunning = examStatus == ExamStatus.RUNNING; + final boolean editable = examStatus == ExamStatus.UP_COMING + || examStatus == ExamStatus.RUNNING + && currentUser.get().hasRole(UserRole.EXAM_ADMIN); // The Exam form final FormHandle formHandle = this.pageService.formBuilder( @@ -283,14 +307,14 @@ public class ExamForm implements TemplateComposer { .newAction(ActionDefinition.EXAM_CANCEL_MODIFY) .withEntityKey(entityKey) .withAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA, String.valueOf(importFromQuizData)) - .withExec(this::cancelModify) + .withExec(this.cancelModifyFunction()) .publishIf(() -> !readonly); // additional data in read-only view if (readonly && !importFromQuizData) { // List of SEB Configuration - widgetFactory.labelLocalized( + this.widgetFactory.labelLocalized( content, CustomVariant.TEXT_H3, CONFIG_LIST_TITLE_KEY); @@ -354,6 +378,12 @@ public class ExamForm implements TemplateComposer { getConfigMappingSelection(configurationTable), this::deleteExamConfigMapping, CONFIG_EMPTY_SELECTION_TEXT_KEY) + .withConfirm(() -> { + if (isExamRunning) { + return CONFIRM_MESSAGE_REMOVE_CONFIG; + } + return null; + }) .publishIf(() -> modifyGrant && configurationTable.hasAnyContent() && editable) .newAction(ActionDefinition.EXAM_CONFIGURATION_EXPORT) @@ -374,7 +404,7 @@ public class ExamForm implements TemplateComposer { .publishIf(() -> userGrantCheck.r() && configurationTable.hasAnyContent()); // List of Indicators - widgetFactory.labelLocalized( + this.widgetFactory.labelLocalized( content, CustomVariant.TEXT_H3, INDICATOR_LIST_TITLE_KEY); @@ -432,6 +462,35 @@ public class ExamForm implements TemplateComposer { } } + private void showConsistencyChecks(final Collection result, final Composite parent) { + if (result == null || result.isEmpty()) { + return; + } + + final Composite warningPanel = this.widgetFactory.createWarningPanel(parent); + this.widgetFactory.labelLocalized( + warningPanel, + CustomVariant.TITLE_LABEL, + CONSISTENCY_MESSAGE_TITLE); + + result + .stream() + .map(message -> { + if (message.messageCode.equals(ErrorMessage.EXAM_CONSISTANCY_VALIDATION_SUPPORTER.messageCode)) { + return CONSISTENCY_MESSAGE_MISSING_SUPPORTER; + } else if (message.messageCode + .equals(ErrorMessage.EXAM_CONSISTANCY_VALIDATION_CONFIG.messageCode)) { + return CONSISTENCY_MESSAGE_MISSING_CONFIG; + } + return null; + }) + .filter(message -> message != null) + .forEach(message -> this.widgetFactory.labelLocalized( + warningPanel, + CustomVariant.MESSAGE, + message)); + } + private PageAction viewExamConfigPageAction(final EntityTable table) { final PageActionBuilder actionBuilder = this.pageService.pageActionBuilder(table.getPageContext() @@ -549,6 +608,7 @@ public class ExamForm implements TemplateComposer { .getText(ResourceService.EXAM_INDICATOR_TYPE_PREFIX + indicator.type.name()); } + // TODO find a better way to show a threshold value as text private static String thresholdsValue(final Indicator indicator) { if (indicator.thresholds.isEmpty()) { return Constants.EMPTY_NOTE; @@ -558,25 +618,31 @@ public class ExamForm implements TemplateComposer { .stream() .reduce( new StringBuilder(), - (sb, threshold) -> sb.append(threshold.value).append(":").append(threshold.color).append("|"), + (sb, threshold) -> sb.append(threshold.value) + .append(":") + .append(threshold.color) + .append("|"), (sb1, sb2) -> sb1.append(sb2)) .toString(); } - private PageAction cancelModify(final PageAction action) { - final boolean importFromQuizData = BooleanUtils.toBoolean( - action.pageContext().getAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA)); - if (importFromQuizData) { - final PageActionBuilder actionBuilder = this.pageService.pageActionBuilder(action.pageContext()); - final PageAction activityHomeAction = actionBuilder - .newAction(ActionDefinition.QUIZ_DISCOVERY_VIEW_LIST) - .create(); - this.pageService.firePageEvent(new ActionEvent(activityHomeAction), action.pageContext()); - return activityHomeAction; - } + private Function cancelModifyFunction() { + final Function backToCurrentFunction = this.pageService.backToCurrentFunction(); + return action -> { + final boolean importFromQuizData = BooleanUtils.toBoolean( - final PageState lastState = this.pageService.getCurrentState(); - return lastState.gotoAction; + action.pageContext().getAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA)); + if (importFromQuizData) { + final PageActionBuilder actionBuilder = this.pageService.pageActionBuilder(action.pageContext()); + final PageAction activityHomeAction = actionBuilder + .newAction(ActionDefinition.QUIZ_DISCOVERY_VIEW_LIST) + .create(); + this.pageService.firePageEvent(new ActionEvent(activityHomeAction), action.pageContext()); + return activityHomeAction; + } + + return backToCurrentFunction.apply(action); + }; } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamList.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamList.java index d4784d21..3ecfd8d3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamList.java @@ -11,18 +11,22 @@ package ch.ethz.seb.sebserver.gui.content; import java.util.function.BooleanSupplier; import java.util.function.Function; +import org.eclipse.rap.rwt.RWT; import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.TableItem; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; +import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.Entity; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; +import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; @@ -38,6 +42,7 @@ import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder; import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckExamConsistency; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamPage; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.GrantCheck; @@ -46,6 +51,7 @@ import ch.ethz.seb.sebserver.gui.table.ColumnDefinition.TableFilterAttribute; import ch.ethz.seb.sebserver.gui.table.EntityTable; import ch.ethz.seb.sebserver.gui.table.TableFilter.CriteriaType; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant; @Lazy @Component @@ -133,7 +139,8 @@ public class ExamList implements TemplateComposer { this.pageService.entityTableBuilder(restService.getRestCall(GetExamPage.class)) .withEmptyMessage(EMPTY_LIST_TEXT_KEY) .withPaging(this.pageSize) - + .withRowDecorator(this::decorateOnExamConsistency) + .withColumnIf( isSebAdmin, () -> new ColumnDefinition( @@ -220,10 +227,27 @@ public class ExamList implements TemplateComposer { return action.withEntityKey(action.getSingleSelection()); } + + private void decorateOnExamConsistency(TableItem item, Exam exam) { + if (exam.getStatus() != ExamStatus.RUNNING) { + return; + } + + this.pageService.getRestService().getBuilder(CheckExamConsistency.class) + .withURIVariable(API.PARAM_MODEL_ID, exam.getModelId()) + .call() + .ifPresent(warnings -> { + if (warnings != null && !warnings.isEmpty()) { + item.setData(RWT.CUSTOM_VARIANT, CustomVariant.WARNING.key); + } + }); + } private static Function examLmsSetupNameFunction(final ResourceService resourceService) { return exam -> resourceService.getLmsSetupNameFunction() .apply(String.valueOf(exam.lmsSetupId)); } + + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigCopy.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigCopy.java new file mode 100644 index 00000000..9c2f5b9f --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigCopy.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2019 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.content; + +import java.util.function.Function; +import java.util.function.Supplier; + +import org.eclipse.swt.widgets.Composite; + +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.Domain; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigCopyInfo; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.gui.form.FormBuilder; +import ch.ethz.seb.sebserver.gui.form.FormHandle; +import ch.ethz.seb.sebserver.gui.service.page.ModalInputDialogComposer; +import ch.ethz.seb.sebserver.gui.service.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.PageService; +import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog; +import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.CopyConfiguration; + +public final class SebExamConfigCopy { + + static Function importConfigFunction( + final PageService pageService, + final PageContext pageContext) { + + return action -> { + + final ModalInputDialog> dialog = + new ModalInputDialog>( + action.pageContext().getParent().getShell(), + pageService.getWidgetFactory()) + .setDialogWidth(600); + + final CopyFormContext formContext = new CopyFormContext( + pageService, + action.pageContext()); + + dialog.open( + SebExamConfigPropForm.FORM_COPY_TEXT_KEY, + formHandle -> doCopy( + pageService, + pageContext, + formHandle), + Utils.EMPTY_EXECUTION, + formContext); + + return action; + }; + } + + private static final void doCopy( + final PageService pageService, + final PageContext pageContext, + final FormHandle formHandle) { + + final ConfigurationNode newConfig = pageService.getRestService().getBuilder(CopyConfiguration.class) + .withFormBinding(formHandle.getFormBinding()) + .call() + .getOrThrow(); + + final PageAction viewNewConfig = pageService.pageActionBuilder(pageContext.copy().clearAttributes()) + .withEntityKey(new EntityKey(newConfig.id, EntityType.CONFIGURATION_NODE)) + .create(); + + pageService.executePageAction(viewNewConfig); + } + + private static final class CopyFormContext implements ModalInputDialogComposer> { + + private final PageService pageService; + private final PageContext pageContext; + + protected CopyFormContext(final PageService pageService, final PageContext pageContext) { + this.pageService = pageService; + this.pageContext = pageContext; + } + + @Override + public Supplier> compose(final Composite parent) { + + final EntityKey entityKey = this.pageContext.getEntityKey(); + final FormHandle formHandle = this.pageService.formBuilder( + this.pageContext.copyOf(parent), 4) + .readonly(false) + .putStaticValue( + Domain.CONFIGURATION_NODE.ATTR_ID, + entityKey.getModelId()) + .addField(FormBuilder.text( + Domain.CONFIGURATION_NODE.ATTR_NAME, + SebExamConfigPropForm.FORM_NAME_TEXT_KEY)) + .addField(FormBuilder.text( + Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION, + SebExamConfigPropForm.FORM_DESCRIPTION_TEXT_KEY) + .asArea()) + .build(); + + return () -> formHandle; + } + + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigImport.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigImport.java new file mode 100644 index 00000000..75fdfbc6 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigImport.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2019 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.content; + +import java.io.InputStream; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; +import ch.ethz.seb.sebserver.gui.form.Form; +import ch.ethz.seb.sebserver.gui.form.FormBuilder; +import ch.ethz.seb.sebserver.gui.form.FormHandle; +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.service.page.ModalInputDialogComposer; +import ch.ethz.seb.sebserver.gui.service.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.PageService; +import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog; +import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.ImportExamConfig; +import ch.ethz.seb.sebserver.gui.widget.FileUploadSelection; + +public final class SebExamConfigImport { + + static Function importConfigFunction(final PageService pageService) { + return action -> { + + final ModalInputDialog> dialog = + new ModalInputDialog>( + action.pageContext().getParent().getShell(), + pageService.getWidgetFactory()) + .setDialogWidth(600); + + final ImportFormContext importFormContext = new ImportFormContext( + pageService, + action.pageContext()); + + dialog.open( + SebExamConfigPropForm.FORM_IMPORT_TEXT_KEY, + formHandle -> doImport( + pageService, + formHandle), + importFormContext::cancelUpload, + importFormContext); + + return action; + }; + } + + private static final void doImport( + final PageService pageService, + final FormHandle formHandle) { + + final Form form = formHandle.getForm(); + final EntityKey entityKey = formHandle.getContext().getEntityKey(); + final Control fieldControl = form.getFieldControl(API.IMPORT_FILE_ATTR_NAME); + final PageContext context = formHandle.getContext(); + if (fieldControl != null && fieldControl instanceof FileUploadSelection) { + final FileUploadSelection fileUpload = (FileUploadSelection) fieldControl; + final InputStream inputStream = fileUpload.getInputStream(); + if (inputStream != null) { + final Configuration configuration = pageService.getRestService() + .getBuilder(ImportExamConfig.class) + .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) + .withHeader( + API.IMPORT_PASSWORD_ATTR_NAME, + form.getFieldValue(API.IMPORT_PASSWORD_ATTR_NAME)) + .withBody(inputStream) + .call() + .get(e -> { + fileUpload.close(); + return context.notifyError(e); + }); + + if (configuration != null) { + context.publishInfo(SebExamConfigPropForm.FORM_IMPORT_CONFIRM_TEXT_KEY); + } + } else { + formHandle.getContext().publishPageMessage( + new LocTextKey("sebserver.error.unexpected"), + new LocTextKey("Please selecte a valid SEB Exam Configuration File")); + } + } + } + + private static final class ImportFormContext implements ModalInputDialogComposer> { + + private final PageService pageService; + private final PageContext pageContext; + + private Form form = null; + + protected ImportFormContext(final PageService pageService, final PageContext pageContext) { + this.pageService = pageService; + this.pageContext = pageContext; + } + + @Override + public Supplier> compose(final Composite parent) { + + final FormHandle formHandle = this.pageService.formBuilder( + this.pageContext.copyOf(parent), 4) + .readonly(false) + .addField(FormBuilder.fileUpload( + API.IMPORT_FILE_ATTR_NAME, + SebExamConfigPropForm.FORM_IMPORT_SELECT_TEXT_KEY, + null, + API.SEB_FILE_EXTENSION)) + .addField(FormBuilder.text( + API.IMPORT_PASSWORD_ATTR_NAME, + SebExamConfigPropForm.FORM_IMPORT_PASSWORD_TEXT_KEY, + "").asPasswordField()) + .build(); + + this.form = formHandle.getForm(); + return () -> formHandle; + } + + void cancelUpload() { + if (this.form != null) { + final Control fieldControl = this.form.getFieldControl(API.IMPORT_FILE_ATTR_NAME); + if (fieldControl != null && fieldControl instanceof FileUploadSelection) { + ((FileUploadSelection) fieldControl).close(); + } + } + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigPropForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigPropForm.java index 68f13bcf..fc8ee4e0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigPropForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigPropForm.java @@ -8,15 +8,12 @@ package ch.ethz.seb.sebserver.gui.content; -import java.io.InputStream; import java.util.function.Function; -import java.util.function.Supplier; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.client.service.UrlLauncher; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Text; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,20 +24,18 @@ import org.springframework.stereotype.Component; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigKey; -import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationType; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; -import ch.ethz.seb.sebserver.gui.form.Form; import ch.ethz.seb.sebserver.gui.form.FormBuilder; import ch.ethz.seb.sebserver.gui.form.FormHandle; import ch.ethz.seb.sebserver.gui.service.ResourceService; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; -import ch.ethz.seb.sebserver.gui.service.page.ModalInputDialogComposer; import ch.ethz.seb.sebserver.gui.service.page.PageContext; import ch.ethz.seb.sebserver.gui.service.page.PageService; import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; @@ -49,14 +44,13 @@ 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.SebExamConfigPlaintextDownload; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamConfigMappingNames; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.ExportConfigKey; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetExamConfigNode; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.ImportExamConfig; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.NewExamConfig; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.SaveExamConfig; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.EntityGrantCheck; -import ch.ethz.seb.sebserver.gui.widget.FileUploadSelection; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant; @@ -67,29 +61,35 @@ public class SebExamConfigPropForm implements TemplateComposer { private static final Logger log = LoggerFactory.getLogger(SebExamConfigPropForm.class); - private static final LocTextKey FORM_TITLE_NEW = + static final LocTextKey FORM_TITLE_NEW = new LocTextKey("sebserver.examconfig.form.title.new"); - private static final LocTextKey FORM_TITLE = + static final LocTextKey FORM_TITLE = new LocTextKey("sebserver.examconfig.form.title"); - private static final LocTextKey FORM_NAME_TEXT_KEY = + static final LocTextKey FORM_NAME_TEXT_KEY = new LocTextKey("sebserver.examconfig.form.name"); - private static final LocTextKey FORM_DESCRIPTION_TEXT_KEY = + static final LocTextKey FORM_DESCRIPTION_TEXT_KEY = new LocTextKey("sebserver.examconfig.form.description"); - private static final LocTextKey FORM_TEMPLATE_TEXT_KEY = + static final LocTextKey FORM_TEMPLATE_TEXT_KEY = new LocTextKey("sebserver.examconfig.form.template"); - private static final LocTextKey FORM_STATUS_TEXT_KEY = + static final LocTextKey FORM_STATUS_TEXT_KEY = new LocTextKey("sebserver.examconfig.form.status"); - private static final LocTextKey FORM_IMPORT_TEXT_KEY = + static final LocTextKey FORM_IMPORT_TEXT_KEY = new LocTextKey("sebserver.examconfig.action.import-config"); - private static final LocTextKey FORM_IMPORT_SELECT_TEXT_KEY = + static final LocTextKey FORM_IMPORT_SELECT_TEXT_KEY = new LocTextKey("sebserver.examconfig.action.import-file-select"); - private static final LocTextKey FORM_IMPORT_PASSWORD_TEXT_KEY = + static final LocTextKey FORM_IMPORT_PASSWORD_TEXT_KEY = new LocTextKey("sebserver.examconfig.action.import-file-password"); - private static final LocTextKey CONFIG_KEY_TITLE_TEXT_KEY = + static final LocTextKey CONFIG_KEY_TITLE_TEXT_KEY = new LocTextKey("sebserver.examconfig.form.config-key.title"); - private static final LocTextKey FORM_IMPORT_CONFIRM_TEXT_KEY = + static final LocTextKey FORM_IMPORT_CONFIRM_TEXT_KEY = new LocTextKey("sebserver.examconfig.action.import-config.confirm"); + static final LocTextKey FORM_COPY_TEXT_KEY = + new LocTextKey("sebserver.examconfig.action.copy"); + + static final LocTextKey SAVE_CONFIRM_STATE_CHANGE_WHILE_ATTACHED = + new LocTextKey("sebserver.examconfig.action.state-change.confirm"); + private final PageService pageService; private final RestService restService; private final CurrentUser currentUser; @@ -127,7 +127,6 @@ public class SebExamConfigPropForm implements TemplateComposer { .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) .call() .get(pageContext::notifyError); - if (examConfig == null) { log.error("Failed to get ConfigurationNode. " + "Error was notified to the User. " @@ -139,6 +138,12 @@ public class SebExamConfigPropForm implements TemplateComposer { final boolean writeGrant = entityGrant.w(); final boolean modifyGrant = entityGrant.m(); final boolean isReadonly = pageContext.isReadonly(); + final boolean isAttachedToExam = this.restService + .getBuilder(GetExamConfigMappingNames.class) + .withQueryParam(ExamConfigurationMap.FILTER_ATTR_CONFIG_ID, examConfig.getModelId()) + .call() + .map(names -> names != null && !names.isEmpty()) + .getOr(Boolean.FALSE); // new PageContext with actual EntityKey final PageContext formContext = pageContext.withEntityKey(examConfig.getEntityKey()); @@ -151,7 +156,6 @@ public class SebExamConfigPropForm implements TemplateComposer { formContext.getParent(), titleKey); - // The SebClientConfig form final FormHandle formHandle = this.pageService.formBuilder( formContext.copyOf(content), 4) .readonly(isReadonly) @@ -185,7 +189,7 @@ public class SebExamConfigPropForm implements TemplateComposer { Domain.CONFIGURATION_NODE.ATTR_STATUS, FORM_STATUS_TEXT_KEY, examConfig.status.name(), - resourceService::examConfigStatusResources)) + () -> resourceService.examConfigStatusResources(isAttachedToExam))) .buildFor((isNew) ? this.restService.getRestCall(NewExamConfig.class) : this.restService.getRestCall(SaveExamConfig.class)); @@ -229,7 +233,7 @@ public class SebExamConfigPropForm implements TemplateComposer { .newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_CONFIG) .withEntityKey(entityKey) - .withExec(SebExamConfigPropForm.importConfigFunction(this.pageService)) + .withExec(SebExamConfigImport.importConfigFunction(this.pageService)) .noEventPropagation() .publishIf(() -> modifyGrant && isReadonly) @@ -237,6 +241,7 @@ public class SebExamConfigPropForm implements TemplateComposer { .withEntityKey(entityKey) .withExec(formHandle::processFormSave) .ignoreMoveAwayFromEdit() + .withConfirm(() -> stateChangeConfirm(isAttachedToExam, formHandle)) .publishIf(() -> !isReadonly) .newAction(ActionDefinition.SEB_EXAM_CONFIG_PROP_CANCEL_MODIFY) @@ -246,6 +251,26 @@ public class SebExamConfigPropForm implements TemplateComposer { } + private LocTextKey stateChangeConfirm( + final boolean isAttachedToExam, + final FormHandle formHandle) { + + if (isAttachedToExam) { + final String fieldValue = formHandle + .getForm() + .getFieldValue(Domain.CONFIGURATION_NODE.ATTR_STATUS); + + if (fieldValue != null) { + final ConfigurationStatus state = ConfigurationStatus.valueOf(fieldValue); + if (state != ConfigurationStatus.IN_USE) { + return SAVE_CONFIRM_STATE_CHANGE_WHILE_ATTACHED; + } + } + } + + return null; + } + public static Function getConfigKeyFunction(final PageService pageService) { final RestService restService = pageService.getResourceService().getRestService(); return action -> { @@ -281,108 +306,4 @@ public class SebExamConfigPropForm implements TemplateComposer { }; } - private static Function importConfigFunction(final PageService pageService) { - return action -> { - - final ModalInputDialog> dialog = - new ModalInputDialog>( - action.pageContext().getParent().getShell(), - pageService.getWidgetFactory()) - .setDialogWidth(600); - - final ImportFormContext importFormContext = new ImportFormContext( - pageService, - action.pageContext()); - - dialog.open( - FORM_IMPORT_TEXT_KEY, - formHandle -> SebExamConfigPropForm.doImport( - pageService, - formHandle), - importFormContext::cancelUpload, - importFormContext); - - return action; - }; - } - - private static final void doImport( - final PageService pageService, - final FormHandle formHandle) { - - final Form form = formHandle.getForm(); - final EntityKey entityKey = formHandle.getContext().getEntityKey(); - final Control fieldControl = form.getFieldControl(API.IMPORT_FILE_ATTR_NAME); - final PageContext context = formHandle.getContext(); - if (fieldControl != null && fieldControl instanceof FileUploadSelection) { - final FileUploadSelection fileUpload = (FileUploadSelection) fieldControl; - final InputStream inputStream = fileUpload.getInputStream(); - if (inputStream != null) { - final Configuration configuration = pageService.getRestService() - .getBuilder(ImportExamConfig.class) - .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) - .withHeader( - API.IMPORT_PASSWORD_ATTR_NAME, - form.getFieldValue(API.IMPORT_PASSWORD_ATTR_NAME)) - .withBody(inputStream) - .call() - .get(e -> { - fileUpload.close(); - return context.notifyError(e); - }); - - if (configuration != null) { - context.publishInfo(FORM_IMPORT_CONFIRM_TEXT_KEY); - } - } else { - formHandle.getContext().publishPageMessage( - new LocTextKey("sebserver.error.unexpected"), - new LocTextKey("Please selecte a valid SEB Exam Configuration File")); - } - } - } - - private static final class ImportFormContext implements ModalInputDialogComposer> { - - private final PageService pageService; - private final PageContext pageContext; - - private Form form = null; - - protected ImportFormContext(final PageService pageService, final PageContext pageContext) { - this.pageService = pageService; - this.pageContext = pageContext; - } - - @Override - public Supplier> compose(final Composite parent) { - - final FormHandle formHandle = this.pageService.formBuilder( - this.pageContext.copyOf(parent), 4) - .readonly(false) - .addField(FormBuilder.fileUpload( - API.IMPORT_FILE_ATTR_NAME, - FORM_IMPORT_SELECT_TEXT_KEY, - null, - API.SEB_FILE_EXTENSION)) - .addField(FormBuilder.text( - API.IMPORT_PASSWORD_ATTR_NAME, - FORM_IMPORT_PASSWORD_TEXT_KEY, - "").asPasswordField()) - .build(); - - this.form = formHandle.getForm(); - return () -> formHandle; - } - - void cancelUpload() { - if (this.form != null) { - final Control fieldControl = this.form.getFieldControl(API.IMPORT_FILE_ATTR_NAME); - if (fieldControl != null && fieldControl instanceof FileUploadSelection) { - ((FileUploadSelection) fieldControl).close(); - } - } - } - } - } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigSettingsForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigSettingsForm.java index 73b7d3c7..62b21c8f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigSettingsForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigSettingsForm.java @@ -27,6 +27,7 @@ import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus; import ch.ethz.seb.sebserver.gbl.model.sebconfig.View; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.util.Utils; @@ -112,6 +113,7 @@ public class SebExamConfigSettingsForm implements TemplateComposer { .onError(pageContext::notifyError) .getOrThrow(); + final boolean readonly = pageContext.isReadonly() || configNode.status == ConfigurationStatus.IN_USE; final List views = this.examConfigurationService.getViews(attributes); final TabFolder tabFolder = widgetFactory.tabFolderLocalized(content); tabFolder.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); @@ -124,7 +126,7 @@ public class SebExamConfigSettingsForm implements TemplateComposer { view, attributes, 20, - pageContext.isReadonly()); + readonly); viewContexts.add(viewContext); final Composite viewGrid = this.examConfigurationService.createViewGrid( @@ -153,7 +155,7 @@ public class SebExamConfigSettingsForm implements TemplateComposer { return action; }) .withSuccess(KEY_SAVE_TO_HISTORY_SUCCESS) - .publishIf(() -> examConfigGrant.iw()) + .publishIf(() -> examConfigGrant.iw() && !readonly) .newAction(ActionDefinition.SEB_EXAM_CONFIG_UNDO) .withEntityKey(entityKey) @@ -166,7 +168,7 @@ public class SebExamConfigSettingsForm implements TemplateComposer { return action; }) .withSuccess(KEY_UNDO_SUCCESS) - .publishIf(() -> examConfigGrant.iw()) + .publishIf(() -> examConfigGrant.iw() && !readonly) .newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP) .withEntityKey(entityKey) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java index f1ca1651..b2af4285 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java @@ -403,6 +403,13 @@ public enum ActionDefinition { ImageIcon.IMPORT, ActionCategory.FORM), + // TODO copy config action + // TODO + SEB_EXAM_CONFIG_COPY_CONFIG( + new LocTextKey("sebserver.examconfig.action.copy-config"), + ImageIcon.IMPORT, + ActionCategory.FORM), + SEB_EXAM_CONFIG_MODIFY_FROM_LIST( new LocTextKey("sebserver.examconfig.action.list.modify"), ImageIcon.EDIT_SETTINGS, @@ -421,7 +428,7 @@ public enum ActionDefinition { ActionCategory.FORM), SEB_EXAM_CONFIG_TEMPLATE_NEW( - new LocTextKey("sebserver.exam.configtemplate.action.list.new"), + new LocTextKey("sebserver.configtemplate.action.list.new"), ImageIcon.NEW, PageStateDefinitionImpl.SEB_EXAM_CONFIG_TEMPLATE_EDIT, ActionCategory.VARIA), diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java index 7082596c..e23adb37 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java @@ -365,8 +365,13 @@ public class ResourceService { } public List> examConfigStatusResources() { + return examConfigStatusResources(false); + } + + public List> examConfigStatusResources(final boolean isAttachedToExam) { return Arrays.asList(ConfigurationStatus.values()) .stream() + .filter(status -> !isAttachedToExam || status != ConfigurationStatus.READY_TO_USE) .map(type -> new Tuple<>( type.name(), this.i18nSupport.getText(EXAMCONFIG_STATUS_PREFIX + type.name()))) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/CheckExamConsistency.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/CheckExamConsistency.java new file mode 100644 index 00000000..bb15a02c --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/CheckExamConsistency.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 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 java.util.Collection; + +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.APIMessage; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class CheckExamConsistency extends RestCall> { + + public CheckExamConsistency() { + super(new TypeKey<>( + CallType.UNDEFINED, + EntityType.EXAM, + new TypeReference>() { + }), + HttpMethod.GET, + MediaType.APPLICATION_FORM_URLENCODED, + API.EXAM_ADMINISTRATION_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_ADMINISTRATION_CONSISTENCY_CHECK_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/seb/examconfig/CopyConfiguration.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/seb/examconfig/CopyConfiguration.java index b40077f5..58c4e176 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/seb/examconfig/CopyConfiguration.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/seb/examconfig/CopyConfiguration.java @@ -32,10 +32,9 @@ public class CopyConfiguration extends RestCall { EntityType.CONFIGURATION_NODE, new TypeReference() { }), - HttpMethod.POST, - MediaType.APPLICATION_FORM_URLENCODED, + HttpMethod.PUT, + MediaType.APPLICATION_JSON_UTF8, API.CONFIGURATION_NODE_ENDPOINT - + API.MODEL_ID_VAR_PATH_SEGMENT + API.CONFIGURATION_COPY_PATH_SEGMENT); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java b/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java index ed2032b8..cb434640 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java @@ -15,6 +15,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -54,6 +55,7 @@ import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.ImageIcon; public class EntityTable { @@ -80,6 +82,7 @@ public class EntityTable { private final Table table; private final TableNavigator navigator; private final MultiValueMap staticQueryParams; + private final BiConsumer rowDecorator; int pageNumber = 1; int pageSize; @@ -99,7 +102,8 @@ public class EntityTable { final LocTextKey emptyMessage, final Function, PageAction> defaultActionFunction, final boolean hideNavigation, - final MultiValueMap staticQueryParams) { + final MultiValueMap staticQueryParams, + final BiConsumer rowDecorator) { this.composite = new Composite(pageContext.getParent(), type); this.pageService = pageService; @@ -120,6 +124,7 @@ public class EntityTable { GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, false); this.composite.setLayoutData(gridData); this.staticQueryParams = staticQueryParams; + this.rowDecorator = rowDecorator; // TODO just for debugging, remove when tested // this.composite.setBackground(new Color(parent.getDisplay(), new RGB(0, 200, 0))); @@ -404,6 +409,9 @@ public class EntityTable { final TableItem item = new TableItem(this.table, SWT.NONE); item.setData(RWT.MARKUP_ENABLED, Boolean.TRUE); item.setData(TABLE_ROW_DATA, row); + if (this.rowDecorator != null) { + this.rowDecorator.accept(item, row); + } int index = 0; for (final ColumnDefinition column : this.columns) { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/table/TableBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/table/TableBuilder.java index 8b67be02..2779af2d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/table/TableBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/table/TableBuilder.java @@ -10,11 +10,13 @@ package ch.ethz.seb.sebserver.gui.table; import java.util.ArrayList; import java.util.List; +import java.util.function.BiConsumer; import java.util.function.BooleanSupplier; import java.util.function.Function; import java.util.function.Supplier; import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.TableItem; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -38,6 +40,7 @@ public class TableBuilder { private int type = SWT.NONE; private boolean hideNavigation = false; private Function>.RestCallBuilder, RestCall>.RestCallBuilder> restCallAdapter; + private BiConsumer rowDecorator; public TableBuilder( final PageService pageService, @@ -126,6 +129,11 @@ public class TableBuilder { return this; } + public TableBuilder withRowDecorator(final BiConsumer rowDecorator) { + this.rowDecorator = rowDecorator; + return this; + } + public EntityTable compose(final PageContext pageContext) { return new EntityTable<>( this.type, @@ -138,7 +146,8 @@ public class TableBuilder { this.emptyMessage, this.defaultActionFunction, this.hideNavigation, - this.staticQueryParams); + this.staticQueryParams, + this.rowDecorator); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java index f3b86a22..6f2b519b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java @@ -131,6 +131,7 @@ public class WidgetFactory { MESSAGE("message"), ERROR("error"), + WARNING("warning"), CONFIG_INPUT_READONLY("inputreadonly") ; @@ -218,6 +219,17 @@ public class WidgetFactory { return grid; } + public Composite createWarningPanel(final Composite parent) { + final Composite composite = new Composite(parent, SWT.NONE); + composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); + final GridLayout gridLayout = new GridLayout(1, true); + gridLayout.marginWidth = 20; + gridLayout.marginHeight = 20; + composite.setLayout(gridLayout); + composite.setData(RWT.CUSTOM_VARIANT, CustomVariant.WARNING.key); + return composite; + } + public Button buttonLocalized(final Composite parent, final String locTextKey) { final Button button = new Button(parent, SWT.NONE); this.polyglotPageService.injectI18n(button, new LocTextKey(locTextKey)); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ConfigurationNodeDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ConfigurationNodeDAO.java index c52cd3db..d4eb4aff 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ConfigurationNodeDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ConfigurationNodeDAO.java @@ -8,6 +8,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigCopyInfo; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO; @@ -19,7 +20,6 @@ public interface ConfigurationNodeDAO extends Result createCopy( Long institutionId, String newOwner, - Long configurationNodeId, - boolean withHistory); + ConfigCopyInfo copyInfo); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationNodeDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationNodeDAOImpl.java index 6069ceec..0e757089 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationNodeDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationNodeDAOImpl.java @@ -32,6 +32,7 @@ import org.springframework.transaction.annotation.Transactional; import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException; import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigCopyInfo; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationType; @@ -208,79 +209,16 @@ public class ConfigurationNodeDAOImpl implements ConfigurationNodeDAO { public Result createCopy( final Long institutionId, final String newOwner, - final Long configurationNodeId, - final boolean withHistory) { + final ConfigCopyInfo copyInfo) { - return this.recordById(configurationNodeId) + return this.recordById(copyInfo.configurationNodeId) .flatMap(nodeRec -> (nodeRec.getInstitutionId().equals(institutionId) ? Result.of(nodeRec) : Result.ofError(new IllegalArgumentException("Institution integrity violation")))) - .map(nodeRec -> this.copyNodeRecord(nodeRec, newOwner, withHistory)) + .map(nodeRec -> this.copyNodeRecord(nodeRec, newOwner, copyInfo)) .flatMap(ConfigurationNodeDAOImpl::toDomainModel); } - private ConfigurationNodeRecord copyNodeRecord( - final ConfigurationNodeRecord nodeRec, - final String newOwner, - final boolean withHistory) { - - final ConfigurationNodeRecord newNodeRec = new ConfigurationNodeRecord( - null, - nodeRec.getInstitutionId(), - nodeRec.getTemplateId(), - StringUtils.isNotBlank(newOwner) ? newOwner : nodeRec.getOwner(), - this.copyNamePrefix + nodeRec.getName() + this.copyNameSuffix, - nodeRec.getDescription(), - nodeRec.getType(), - ConfigurationStatus.CONSTRUCTION.name()); - this.configurationNodeRecordMapper.insert(newNodeRec); - - final List configs = this.configurationRecordMapper - .selectByExample() - .where( - ConfigurationRecordDynamicSqlSupport.configurationNodeId, - isEqualTo(nodeRec.getId())) - .build() - .execute(); - - if (withHistory) { - configs - .stream() - .forEach(configRec -> this.configurationDAOBatchService.copyConfiguration( - configRec.getInstitutionId(), - configRec.getId(), - newNodeRec.getId())); - } else { - configs - .stream() - .filter(configRec -> configRec.getVersionDate() == null) - .findFirst() - .map(configRec -> { - // No history means to create a first version and a follow-up with the copied values - final ConfigurationRecord newFirstVersion = new ConfigurationRecord( - null, - configRec.getInstitutionId(), - configRec.getConfigurationNodeId(), - ConfigurationDAOBatchService.INITIAL_VERSION_NAME, - DateTime.now(DateTimeZone.UTC), - BooleanUtils.toInteger(false)); - this.configurationRecordMapper.insert(newFirstVersion); - this.configurationDAOBatchService.copyValues( - configRec.getInstitutionId(), - configRec.getId(), - newFirstVersion.getId()); - // and copy the follow-up - this.configurationDAOBatchService.copyConfiguration( - configRec.getInstitutionId(), - configRec.getId(), - newNodeRec.getId()); - return configRec; - }); - } - - return newNodeRec; - } - @Override @Transactional public Result> delete(final Set all) { @@ -345,6 +283,68 @@ public class ConfigurationNodeDAOImpl implements ConfigurationNodeDAO { }); } + private ConfigurationNodeRecord copyNodeRecord( + final ConfigurationNodeRecord nodeRec, + final String newOwner, + final ConfigCopyInfo copyInfo) { + + final ConfigurationNodeRecord newNodeRec = new ConfigurationNodeRecord( + null, + nodeRec.getInstitutionId(), + nodeRec.getTemplateId(), + StringUtils.isNotBlank(newOwner) ? newOwner : nodeRec.getOwner(), + this.copyNamePrefix + nodeRec.getName() + this.copyNameSuffix, + nodeRec.getDescription(), + nodeRec.getType(), + ConfigurationStatus.CONSTRUCTION.name()); + this.configurationNodeRecordMapper.insert(newNodeRec); + + final List configs = this.configurationRecordMapper + .selectByExample() + .where( + ConfigurationRecordDynamicSqlSupport.configurationNodeId, + isEqualTo(nodeRec.getId())) + .build() + .execute(); + + if (BooleanUtils.toBoolean(copyInfo.withHistory)) { + configs + .stream() + .forEach(configRec -> this.configurationDAOBatchService.copyConfiguration( + configRec.getInstitutionId(), + configRec.getId(), + newNodeRec.getId())); + } else { + configs + .stream() + .filter(configRec -> configRec.getVersionDate() == null) + .findFirst() + .map(configRec -> { + // No history means to create a first version and a follow-up with the copied values + final ConfigurationRecord newFirstVersion = new ConfigurationRecord( + null, + configRec.getInstitutionId(), + configRec.getConfigurationNodeId(), + ConfigurationDAOBatchService.INITIAL_VERSION_NAME, + DateTime.now(DateTimeZone.UTC), + BooleanUtils.toInteger(false)); + this.configurationRecordMapper.insert(newFirstVersion); + this.configurationDAOBatchService.copyValues( + configRec.getInstitutionId(), + configRec.getId(), + newFirstVersion.getId()); + // and copy the follow-up + this.configurationDAOBatchService.copyConfiguration( + configRec.getInstitutionId(), + configRec.getId(), + newNodeRec.getId()); + return configRec; + }); + } + + return newNodeRec; + } + static Result toDomainModel(final ConfigurationNodeRecord record) { return Result.tryCatch(() -> new ConfigurationNode( record.getId(), diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java index a9227e7e..2a190244 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java @@ -14,6 +14,7 @@ import java.util.function.Predicate; import org.springframework.context.event.EventListener; +import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.util.Result; @@ -29,6 +30,17 @@ public interface ExamSessionService { * @return the underling ExamDAO service. */ ExamDAO getExamDAO(); + /** Use this to check the consistency of a running Exam. + * Current consistency checks are: + * - Check if there is at least one Exam supporter attached to the Exam + * - Check if there is one default SEB Exam Configuration attached to the Exam + * - Check if there is at least one Indicator defined for the monitoring of the Exam + * + * @param examId the identifier of the Exam to check + * @return Result of one APIMessage per consistency check if the check failed. An empty Collection of everything is + * okay. */ + Result> checkRunningExamConsystency(Long examId); + /** Indicates whether an Exam is currently running or not. * * @param examId the PK of the Exam to test diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java index 332c11ee..cec94439 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java @@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl; import java.io.IOException; import java.io.OutputStream; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.NoSuchElementException; @@ -25,6 +26,8 @@ import org.springframework.context.event.EventListener; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; +import ch.ethz.seb.sebserver.gbl.api.APIMessage; +import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; @@ -34,6 +37,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.IndicatorDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationChangedEvent; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; @@ -45,6 +49,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { private static final Logger log = LoggerFactory.getLogger(ExamSessionServiceImpl.class); private final ClientConnectionDAO clientConnectionDAO; + private final IndicatorDAO indicatorDAO; private final ExamSessionCacheService examSessionCacheService; private final ExamDAO examDAO; private final ExamConfigurationMapDAO examConfigurationMapDAO; @@ -55,6 +60,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { final ExamDAO examDAO, final ExamConfigurationMapDAO examConfigurationMapDAO, final ClientConnectionDAO clientConnectionDAO, + final IndicatorDAO indicatorDAO, final CacheManager cacheManager) { this.examSessionCacheService = examSessionCacheService; @@ -62,6 +68,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { this.examConfigurationMapDAO = examConfigurationMapDAO; this.clientConnectionDAO = clientConnectionDAO; this.cacheManager = cacheManager; + this.indicatorDAO = indicatorDAO; } @Override @@ -69,6 +76,40 @@ public class ExamSessionServiceImpl implements ExamSessionService { return this.examDAO; } + @Override + public Result> checkRunningExamConsystency(final Long examId) { + return Result.tryCatch(() -> { + final Collection result = new ArrayList<>(); + + if (isExamRunning(examId)) { + final Exam exam = getRunningExam(examId) + .getOrThrow(); + + // check exam supporter + if (exam.getSupporter().isEmpty()) { + result.add(ErrorMessage.EXAM_CONSISTANCY_VALIDATION_SUPPORTER.of(exam.getModelId())); + } + + // check SEB configuration + this.examConfigurationMapDAO.getDefaultConfigurationForExam(examId) + .get(t -> { + result.add(ErrorMessage.EXAM_CONSISTANCY_VALIDATION_CONFIG.of(exam.getModelId())); + return null; + }); + + // check indicator exists + if (this.indicatorDAO.allForExam(examId) + .getOrThrow() + .isEmpty()) { + + result.add(ErrorMessage.EXAM_CONSISTANCY_VALIDATION_INDICATOR.of(exam.getModelId())); + } + } + + return result; + }); + } + @Override public boolean isExamRunning(final Long examId) { return !getRunningExam(examId).hasError(); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ActivatableEntityController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ActivatableEntityController.java index fe384ec6..cefb46cb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ActivatableEntityController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ActivatableEntityController.java @@ -62,7 +62,7 @@ public abstract class ActivatableEntityController allActive( @RequestParam( name = Entity.FILTER_ATTR_INSTITUTION, @@ -90,7 +90,7 @@ public abstract class ActivatableEntityController allInactive( @RequestParam( name = Entity.FILTER_ATTR_INSTITUTION, @@ -118,7 +118,7 @@ public abstract class ActivatableEntityController 0) { - throw new IllegalStateException("Integrity violation: There are currently active SEB Client connection."); + throw new APIMessage.APIMessageException( + ErrorMessage.INTEGRITY_VALIDATION, + "Integrity violation: There are currently active SEB Client connection."); } else { return Result.of(config); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java index 27568c86..341f0d04 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java @@ -17,9 +17,9 @@ import java.util.List; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.BooleanUtils; import org.mybatis.dynamic.sql.SqlTable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,6 +28,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -40,6 +41,7 @@ import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType; import ch.ethz.seb.sebserver.gbl.model.Domain.EXAM; import ch.ethz.seb.sebserver.gbl.model.Page; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigCopyInfo; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigKey; import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; @@ -138,20 +140,18 @@ public class ConfigurationNodeController extends EntityController getPage( @RequestParam( @@ -187,6 +192,17 @@ public class ExamAdministrationController extends ActivatableEntityController checkExamConsistency(@PathVariable final Long modelId) { + return this.examSessionService + .checkRunningExamConsystency(modelId) + .getOrThrow(); + } + public static Page buildSortedExamPage( final Integer pageNumber, final Integer pageSize, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamConfigurationMappingController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamConfigurationMappingController.java index 9981f48a..4013e81b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamConfigurationMappingController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamConfigurationMappingController.java @@ -16,11 +16,14 @@ import org.springframework.web.bind.annotation.RestController; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException; +import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage; import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.GrantEntity; import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus; import ch.ethz.seb.sebserver.gbl.model.user.PasswordChange; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; @@ -28,6 +31,7 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamConfiguration import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationNodeDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.EntityDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; @@ -39,6 +43,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe public class ExamConfigurationMappingController extends EntityController { private final ExamDAO examDao; + private final ConfigurationNodeDAO configurationNodeDAO; protected ExamConfigurationMappingController( final AuthorizationService authorization, @@ -47,7 +52,8 @@ public class ExamConfigurationMappingController extends EntityController validForCreate(final ExamConfigurationMap entity) { return super.validForCreate(entity) + .map(this::checkConfigurationState) .map(this::checkPasswordMatch); } @@ -106,6 +114,21 @@ public class ExamConfigurationMappingController extends EntityController notifyCreated(final ExamConfigurationMap entity) { + // update the attached configurations state to "In Use" + return this.configurationNodeDAO.save(new ConfigurationNode( + entity.configurationNodeId, + entity.institutionId, + null, + null, + null, + null, + null, + ConfigurationStatus.IN_USE)) + .map(config -> entity); + } + private ExamConfigurationMap checkPasswordMatch(final ExamConfigurationMap entity) { if (entity.hasEncryptionSecret() && !entity.encryptSecret.equals(entity.confirmEncryptSecret)) { throw new APIMessageException(APIMessage.fieldValidationError( @@ -118,4 +141,22 @@ public class ExamConfigurationMappingController extends EntityControllerwhere connecting SEB clients cannot download the configuration for the exam.

Are you sure to remove the Configuration? + sebserver.exam.action.list=Exam sebserver.exam.action.list.view=View Exam sebserver.exam.action.list.modify=Edit Exam @@ -451,6 +456,7 @@ sebserver.examconfig.action.import-config=Import Configuration sebserver.examconfig.action.import-file-select=Import From File sebserver.examconfig.action.import-file-password=Password sebserver.examconfig.action.import-config.confirm=Configuration successfully imported +sebserver.examconfig.action.state-change.confirm=This configuration is already attached to an exam.
Please note that changing an attached configuration will take effect on the exam when the configuration changes are saved

Are you sure to change this configuration to an editable state? sebserver.examconfig.form.title.new=Add Exam Configuration sebserver.examconfig.form.title=Exam Configuration @@ -937,7 +943,7 @@ sebserver.configtemplate.list.actions=Selected Template sebserver.configtemplate.info.pleaseSelect=Please select an exam configuration template first -sebserver.exam.configtemplate.action.list.new=Add Template +sebserver.configtemplate.action.list.new=Add Template sebserver.configtemplate.action.list.view=View Template sebserver.configtemplate.action.view=View Template sebserver.configtemplate.action.list.modify=Edit Template diff --git a/src/main/resources/static/css/sebserver.css b/src/main/resources/static/css/sebserver.css index 5998f173..c8657fa7 100644 --- a/src/main/resources/static/css/sebserver.css +++ b/src/main/resources/static/css/sebserver.css @@ -170,6 +170,14 @@ Composite.error { border-radius: 1px; } +Composite.warning { + background-gradient-color: rgba( 168, 50, 45, 0.5 ); + background-image: gradient( linear, left top, left bottom, from(rgba( 168, 50, 45, 0.5 ) ), to( rgba( 168, 50, 45, 0.5 ) ) ); + background-repeat: repeat; + background-position: left top; + opacity: 1; +} + *.header { font: bold 12px Arial, Helvetica, sans-serif; color: #FFFFFF; @@ -740,7 +748,7 @@ TableColumn:hover { background-image: gradient( linear, left top, left bottom, from( #595959 ), to( #595959 ) ); } -TableItem, TableItem:linesvisible:even:rowtemplate { +TableItem { background-color: transparent; color: inherit; text-decoration: none; @@ -748,6 +756,12 @@ TableItem, TableItem:linesvisible:even:rowtemplate { background-image: none; } +Table-RowOverlay.warning { + background-color: rgba( 168, 50, 45, 0.5 ); + background-gradient-color: rgba( 168, 50, 45, 0.5 ); + background-image: gradient( linear, left top, left bottom, from(rgba( 168, 50, 45, 0.5 ) ), to( rgba( 168, 50, 45, 0.5 ) ) ); +} + TableItem:linesvisible:even { background-color: #ffffff; color: inherit; @@ -759,6 +773,7 @@ Table-RowOverlay { background-image: none; } + Table-RowOverlay:hover { color: #4a4a4a; background-color: #b5b5b5;