SEBSERV-73 made changed to Exam end Exam Config handling

This commit is contained in:
anhefti 2019-10-23 16:56:42 +02:00
parent 6eaef577d2
commit 83985cdbf7
30 changed files with 844 additions and 259 deletions

View file

@ -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";

View file

@ -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;

View file

@ -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<String> supporter;
/** Indicates whether this Exam is active or not */

View file

@ -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();
}
}

View file

@ -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";

View file

@ -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<Exam> 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<APIMessage> 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<ExamConfigurationMap> 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<PageAction, PageAction> cancelModifyFunction() {
final Function<PageAction, PageAction> 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);
};
}
}

View file

@ -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<Exam>(
@ -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<Exam, String> examLmsSetupNameFunction(final ResourceService resourceService) {
return exam -> resourceService.getLmsSetupNameFunction()
.apply(String.valueOf(exam.lmsSetupId));
}
}

View file

@ -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<PageAction, PageAction> importConfigFunction(
final PageService pageService,
final PageContext pageContext) {
return action -> {
final ModalInputDialog<FormHandle<ConfigCopyInfo>> dialog =
new ModalInputDialog<FormHandle<ConfigCopyInfo>>(
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<ConfigCopyInfo> 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<FormHandle<ConfigCopyInfo>> {
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<FormHandle<ConfigCopyInfo>> compose(final Composite parent) {
final EntityKey entityKey = this.pageContext.getEntityKey();
final FormHandle<ConfigCopyInfo> 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;
}
}
}

View file

@ -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<PageAction, PageAction> importConfigFunction(final PageService pageService) {
return action -> {
final ModalInputDialog<FormHandle<ConfigurationNode>> dialog =
new ModalInputDialog<FormHandle<ConfigurationNode>>(
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<ConfigurationNode> 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<FormHandle<ConfigurationNode>> {
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<FormHandle<ConfigurationNode>> compose(final Composite parent) {
final FormHandle<ConfigurationNode> 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();
}
}
}
}
}

View file

@ -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<ConfigurationNode> 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<ConfigurationNode> 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<PageAction, PageAction> getConfigKeyFunction(final PageService pageService) {
final RestService restService = pageService.getResourceService().getRestService();
return action -> {
@ -281,108 +306,4 @@ public class SebExamConfigPropForm implements TemplateComposer {
};
}
private static Function<PageAction, PageAction> importConfigFunction(final PageService pageService) {
return action -> {
final ModalInputDialog<FormHandle<ConfigurationNode>> dialog =
new ModalInputDialog<FormHandle<ConfigurationNode>>(
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<ConfigurationNode> 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<FormHandle<ConfigurationNode>> {
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<FormHandle<ConfigurationNode>> compose(final Composite parent) {
final FormHandle<ConfigurationNode> 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();
}
}
}
}
}

View file

@ -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<View> 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)

View file

@ -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),

View file

@ -365,8 +365,13 @@ public class ResourceService {
}
public List<Tuple<String>> examConfigStatusResources() {
return examConfigStatusResources(false);
}
public List<Tuple<String>> 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())))

View file

@ -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<Collection<APIMessage>> {
public CheckExamConsistency() {
super(new TypeKey<>(
CallType.UNDEFINED,
EntityType.EXAM,
new TypeReference<Collection<APIMessage>>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_ADMINISTRATION_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_CONSISTENCY_CHECK_PATH_SEGMENT);
}
}

View file

@ -32,10 +32,9 @@ public class CopyConfiguration extends RestCall<ConfigurationNode> {
EntityType.CONFIGURATION_NODE,
new TypeReference<ConfigurationNode>() {
}),
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);
}

View file

@ -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<ROW extends Entity> {
@ -80,6 +82,7 @@ public class EntityTable<ROW extends Entity> {
private final Table table;
private final TableNavigator navigator;
private final MultiValueMap<String, String> staticQueryParams;
private final BiConsumer<TableItem, ROW> rowDecorator;
int pageNumber = 1;
int pageSize;
@ -99,7 +102,8 @@ public class EntityTable<ROW extends Entity> {
final LocTextKey emptyMessage,
final Function<EntityTable<ROW>, PageAction> defaultActionFunction,
final boolean hideNavigation,
final MultiValueMap<String, String> staticQueryParams) {
final MultiValueMap<String, String> staticQueryParams,
final BiConsumer<TableItem, ROW> rowDecorator) {
this.composite = new Composite(pageContext.getParent(), type);
this.pageService = pageService;
@ -120,6 +124,7 @@ public class EntityTable<ROW extends Entity> {
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<ROW extends Entity> {
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<ROW> column : this.columns) {

View file

@ -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<ROW extends Entity> {
private int type = SWT.NONE;
private boolean hideNavigation = false;
private Function<RestCall<Page<ROW>>.RestCallBuilder, RestCall<Page<ROW>>.RestCallBuilder> restCallAdapter;
private BiConsumer<TableItem, ROW> rowDecorator;
public TableBuilder(
final PageService pageService,
@ -126,6 +129,11 @@ public class TableBuilder<ROW extends Entity> {
return this;
}
public TableBuilder<ROW> withRowDecorator(final BiConsumer<TableItem, ROW> rowDecorator) {
this.rowDecorator = rowDecorator;
return this;
}
public EntityTable<ROW> compose(final PageContext pageContext) {
return new EntityTable<>(
this.type,
@ -138,7 +146,8 @@ public class TableBuilder<ROW extends Entity> {
this.emptyMessage,
this.defaultActionFunction,
this.hideNavigation,
this.staticQueryParams);
this.staticQueryParams,
this.rowDecorator);
}
}

View file

@ -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));

View file

@ -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<ConfigurationNode> createCopy(
Long institutionId,
String newOwner,
Long configurationNodeId,
boolean withHistory);
ConfigCopyInfo copyInfo);
}

View file

@ -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<ConfigurationNode> 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<ConfigurationRecord> 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<Collection<EntityKey>> delete(final Set<EntityKey> 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<ConfigurationRecord> 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<ConfigurationNode> toDomainModel(final ConfigurationNodeRecord record) {
return Result.tryCatch(() -> new ConfigurationNode(
record.getId(),

View file

@ -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<Collection<APIMessage>> checkRunningExamConsystency(Long examId);
/** Indicates whether an Exam is currently running or not.
*
* @param examId the PK of the Exam to test

View file

@ -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<Collection<APIMessage>> checkRunningExamConsystency(final Long examId) {
return Result.tryCatch(() -> {
final Collection<APIMessage> 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();

View file

@ -62,7 +62,7 @@ public abstract class ActivatableEntityController<T extends GrantEntity, M exten
path = API.ACTIVE_PATH_SEGMENT,
method = RequestMethod.GET,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Page<T> allActive(
@RequestParam(
name = Entity.FILTER_ATTR_INSTITUTION,
@ -90,7 +90,7 @@ public abstract class ActivatableEntityController<T extends GrantEntity, M exten
path = API.INACTIVE_PATH_SEGMENT,
method = RequestMethod.GET,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Page<T> allInactive(
@RequestParam(
name = Entity.FILTER_ATTR_INSTITUTION,
@ -118,7 +118,7 @@ public abstract class ActivatableEntityController<T extends GrantEntity, M exten
path = API.PATH_VAR_ACTIVE,
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public EntityProcessingReport activate(@PathVariable final String modelId) {
return setActive(modelId, true)
.getOrThrow();
@ -128,7 +128,7 @@ public abstract class ActivatableEntityController<T extends GrantEntity, M exten
value = API.PATH_VAR_INACTIVE,
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public EntityProcessingReport deactivate(@PathVariable final String modelId) {
return setActive(modelId, false)
.getOrThrow();

View file

@ -22,6 +22,8 @@ import org.springframework.web.bind.annotation.RestController;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
@ -157,7 +159,9 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
.count();
if (activeConnections > 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);
}

View file

@ -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<ConfigurationN
}
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT + API.CONFIGURATION_COPY_PATH_SEGMENT,
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
path = API.CONFIGURATION_COPY_PATH_SEGMENT,
method = RequestMethod.PUT,
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ConfigurationNode copyConfiguration(
@PathVariable final Long modelId,
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
@RequestParam(name = ConfigurationNode.ATTR_COPY_WITH_HISTORY,
required = false) final Boolean withHistory) {
@Valid @RequestBody final ConfigCopyInfo copyInfo) {
this.entityDAO.byPK(modelId)
this.entityDAO.byPK(copyInfo.configurationNodeId)
.flatMap(this.authorization::checkWrite);
final SEBServerUser currentUser = this.authorization
@ -161,8 +161,7 @@ public class ConfigurationNodeController extends EntityController<ConfigurationN
return this.configurationNodeDAO.createCopy(
institutionId,
currentUser.getUserInfo().uuid,
modelId,
BooleanUtils.toBoolean(withHistory))
copyInfo)
.getOrThrow();
}

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
@ -59,6 +60,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebExamConfigService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
@WebServiceProfile
@ -72,6 +74,7 @@ public class ExamAdministrationController extends ActivatableEntityController<Ex
private final UserDAO userDAO;
private final LmsAPIService lmsAPIService;
private final SebExamConfigService sebExamConfigService;
private final ExamSessionService examSessionService;
public ExamAdministrationController(
final AuthorizationService authorization,
@ -82,7 +85,8 @@ public class ExamAdministrationController extends ActivatableEntityController<Ex
final BeanValidationService beanValidationService,
final LmsAPIService lmsAPIService,
final UserDAO userDAO,
final SebExamConfigService sebExamConfigService) {
final SebExamConfigService sebExamConfigService,
final ExamSessionService examSessionService) {
super(authorization,
bulkActionService,
@ -95,6 +99,7 @@ public class ExamAdministrationController extends ActivatableEntityController<Ex
this.userDAO = userDAO;
this.lmsAPIService = lmsAPIService;
this.sebExamConfigService = sebExamConfigService;
this.examSessionService = examSessionService;
}
@Override
@ -105,7 +110,7 @@ public class ExamAdministrationController extends ActivatableEntityController<Ex
@RequestMapping(
method = RequestMethod.GET,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@Override
public Page<Exam> getPage(
@RequestParam(
@ -187,6 +192,17 @@ public class ExamAdministrationController extends ActivatableEntityController<Ex
}
}
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_CONSISTENCY_CHECK_PATH_SEGMENT,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Collection<APIMessage> checkExamConsistency(@PathVariable final Long modelId) {
return this.examSessionService
.checkRunningExamConsystency(modelId)
.getOrThrow();
}
public static Page<Exam> buildSortedExamPage(
final Integer pageNumber,
final Integer pageSize,

View file

@ -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<ExamConfigurationMap, ExamConfigurationMap> {
private final ExamDAO examDao;
private final ConfigurationNodeDAO configurationNodeDAO;
protected ExamConfigurationMappingController(
final AuthorizationService authorization,
@ -47,7 +52,8 @@ public class ExamConfigurationMappingController extends EntityController<ExamCon
final UserActivityLogDAO userActivityLogDAO,
final PaginationService paginationService,
final BeanValidationService beanValidationService,
final ExamDAO examDao) {
final ExamDAO examDao,
final ConfigurationNodeDAO configurationNodeDAO) {
super(
authorization,
@ -58,6 +64,7 @@ public class ExamConfigurationMappingController extends EntityController<ExamCon
beanValidationService);
this.examDao = examDao;
this.configurationNodeDAO = configurationNodeDAO;
}
@Override
@ -97,6 +104,7 @@ public class ExamConfigurationMappingController extends EntityController<ExamCon
@Override
protected Result<ExamConfigurationMap> validForCreate(final ExamConfigurationMap entity) {
return super.validForCreate(entity)
.map(this::checkConfigurationState)
.map(this::checkPasswordMatch);
}
@ -106,6 +114,21 @@ public class ExamConfigurationMappingController extends EntityController<ExamCon
.map(this::checkPasswordMatch);
}
@Override
protected Result<ExamConfigurationMap> 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 EntityController<ExamCon
return entity;
}
private ExamConfigurationMap checkConfigurationState(final ExamConfigurationMap entity) {
final ConfigurationStatus status;
if (entity.getConfigStatus() != null) {
status = entity.getConfigStatus();
} else {
status = this.configurationNodeDAO.byPK(entity.configurationNodeId)
.getOrThrow()
.getStatus();
}
if (status != ConfigurationStatus.READY_TO_USE) {
throw new APIMessageException(ErrorMessage.INTEGRITY_VALIDATION.of(
"Illegal SEB Exam Configuration state"));
}
return entity;
}
}

View file

@ -130,8 +130,8 @@ public class UserAccountController extends ActivatableEntityController<UserInfo,
@RequestMapping(
path = API.PASSWORD_PATH_SEGMENT,
method = RequestMethod.PUT,
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public UserInfo changePassword(@Valid @RequestBody final PasswordChange passwordChange) {
final String modelId = passwordChange.getModelId();

View file

@ -288,6 +288,11 @@ sebserver.exam.list.empty=No Exam has been found. Please adapt the filter or imp
sebserver.exam.list.modify.out.dated=Finished exams cannot be modified.
sebserver.exam.list.action.no.modify.privilege=No Access: An Exam from other institution cannot be modified.
sebserver.exam.consistency.title=Note: This exam is already running but has some missing configurations
sebserver.exam.consistency.missing-supporter= - There currently are no Exam-Supporter defined for this exam. Edit the exam to add an Exam-Supporter
sebserver.exam.consistency.missing-config= - There is currently no exam-configuration defined for this exam. Use 'Add Configuration' to attach one
sebserver.exam.confirm.remove-config=This exam is current running. The remove of the attached configuration will led to an invalid state<br/>where connecting SEB clients cannot download the configuration for the exam.<br/><br/>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.<br/>Please note that changing an attached configuration will take effect on the exam when the configuration changes are saved<br/><br/>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

View file

@ -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;