SEBSERV-73 Exam Config changes and test fixes

This commit is contained in:
anhefti 2019-10-24 12:00:56 +02:00
parent 83985cdbf7
commit fb13c62eeb
19 changed files with 256 additions and 121 deletions

View file

@ -9,6 +9,7 @@
package ch.ethz.seb.sebserver.gui.content; package ch.ethz.seb.sebserver.gui.content;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
@ -19,6 +20,7 @@ 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.ConfigCopyInfo;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.form.FormBuilder; import ch.ethz.seb.sebserver.gui.form.FormBuilder;
import ch.ethz.seb.sebserver.gui.form.FormHandle; 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.ModalInputDialogComposer;
@ -30,7 +32,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.Co
public final class SebExamConfigCopy { public final class SebExamConfigCopy {
static Function<PageAction, PageAction> importConfigFunction( static Function<PageAction, PageAction> copyConfigFunction(
final PageService pageService, final PageService pageService,
final PageContext pageContext) { final PageContext pageContext) {
@ -46,12 +48,14 @@ public final class SebExamConfigCopy {
pageService, pageService,
action.pageContext()); action.pageContext());
final Predicate<FormHandle<ConfigCopyInfo>> doCopy = formHandle -> doCopy(
pageService,
pageContext,
formHandle);
dialog.open( dialog.open(
SebExamConfigPropForm.FORM_COPY_TEXT_KEY, SebExamConfigPropForm.FORM_COPY_TEXT_KEY,
formHandle -> doCopy( doCopy,
pageService,
pageContext,
formHandle),
Utils.EMPTY_EXECUTION, Utils.EMPTY_EXECUTION,
formContext); formContext);
@ -59,7 +63,7 @@ public final class SebExamConfigCopy {
}; };
} }
private static final void doCopy( private static final boolean doCopy(
final PageService pageService, final PageService pageService,
final PageContext pageContext, final PageContext pageContext,
final FormHandle<ConfigCopyInfo> formHandle) { final FormHandle<ConfigCopyInfo> formHandle) {
@ -67,13 +71,21 @@ public final class SebExamConfigCopy {
final ConfigurationNode newConfig = pageService.getRestService().getBuilder(CopyConfiguration.class) final ConfigurationNode newConfig = pageService.getRestService().getBuilder(CopyConfiguration.class)
.withFormBinding(formHandle.getFormBinding()) .withFormBinding(formHandle.getFormBinding())
.call() .call()
.getOrThrow(); .onError(formHandle::handleError)
.getOr(null);
final PageAction viewNewConfig = pageService.pageActionBuilder(pageContext.copy().clearAttributes()) if (newConfig == null) {
return false;
}
final PageAction viewNewConfig = pageService.pageActionBuilder(pageContext)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP)
.withEntityKey(new EntityKey(newConfig.id, EntityType.CONFIGURATION_NODE)) .withEntityKey(new EntityKey(newConfig.id, EntityType.CONFIGURATION_NODE))
.create(); .create();
pageService.executePageAction(viewNewConfig); pageService.executePageAction(viewNewConfig);
return true;
} }
private static final class CopyFormContext implements ModalInputDialogComposer<FormHandle<ConfigCopyInfo>> { private static final class CopyFormContext implements ModalInputDialogComposer<FormHandle<ConfigCopyInfo>> {
@ -103,6 +115,9 @@ public final class SebExamConfigCopy {
Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION, Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION,
SebExamConfigPropForm.FORM_DESCRIPTION_TEXT_KEY) SebExamConfigPropForm.FORM_DESCRIPTION_TEXT_KEY)
.asArea()) .asArea())
.addField(FormBuilder.checkbox(
ConfigCopyInfo.ATTR_COPY_WITH_HISTORY,
SebExamConfigPropForm.FORM_HISTORY_TEXT_KEY))
.build(); .build();
return () -> formHandle; return () -> formHandle;

View file

@ -69,6 +69,8 @@ public class SebExamConfigPropForm implements TemplateComposer {
new LocTextKey("sebserver.examconfig.form.name"); new LocTextKey("sebserver.examconfig.form.name");
static final LocTextKey FORM_DESCRIPTION_TEXT_KEY = static final LocTextKey FORM_DESCRIPTION_TEXT_KEY =
new LocTextKey("sebserver.examconfig.form.description"); new LocTextKey("sebserver.examconfig.form.description");
static final LocTextKey FORM_HISTORY_TEXT_KEY =
new LocTextKey("sebserver.examconfig.form.with-history");
static final LocTextKey FORM_TEMPLATE_TEXT_KEY = static final LocTextKey FORM_TEMPLATE_TEXT_KEY =
new LocTextKey("sebserver.examconfig.form.template"); new LocTextKey("sebserver.examconfig.form.template");
static final LocTextKey FORM_STATUS_TEXT_KEY = static final LocTextKey FORM_STATUS_TEXT_KEY =
@ -196,7 +198,8 @@ public class SebExamConfigPropForm implements TemplateComposer {
final boolean settingsReadonly = examConfig.status == ConfigurationStatus.IN_USE; final boolean settingsReadonly = examConfig.status == ConfigurationStatus.IN_USE;
final UrlLauncher urlLauncher = RWT.getClient().getService(UrlLauncher.class); final UrlLauncher urlLauncher = RWT.getClient().getService(UrlLauncher.class);
this.pageService.pageActionBuilder(formContext.clearEntityKeys()) final PageContext actionContext = formContext.clearEntityKeys();
this.pageService.pageActionBuilder(actionContext)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_NEW) .newAction(ActionDefinition.SEB_EXAM_CONFIG_NEW)
.publishIf(() -> writeGrant && isReadonly) .publishIf(() -> writeGrant && isReadonly)
@ -237,6 +240,12 @@ public class SebExamConfigPropForm implements TemplateComposer {
.noEventPropagation() .noEventPropagation()
.publishIf(() -> modifyGrant && isReadonly) .publishIf(() -> modifyGrant && isReadonly)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_COPY_CONFIG)
.withEntityKey(entityKey)
.withExec(SebExamConfigCopy.copyConfigFunction(this.pageService, actionContext))
.noEventPropagation()
.publishIf(() -> modifyGrant && isReadonly)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_PROP_SAVE) .newAction(ActionDefinition.SEB_EXAM_CONFIG_PROP_SAVE)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.withExec(formHandle::processFormSave) .withExec(formHandle::processFormSave)

View file

@ -155,6 +155,7 @@ public class SebExamConfigSettingsForm implements TemplateComposer {
return action; return action;
}) })
.withSuccess(KEY_SAVE_TO_HISTORY_SUCCESS) .withSuccess(KEY_SAVE_TO_HISTORY_SUCCESS)
.ignoreMoveAwayFromEdit()
.publishIf(() -> examConfigGrant.iw() && !readonly) .publishIf(() -> examConfigGrant.iw() && !readonly)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_UNDO) .newAction(ActionDefinition.SEB_EXAM_CONFIG_UNDO)
@ -168,6 +169,7 @@ public class SebExamConfigSettingsForm implements TemplateComposer {
return action; return action;
}) })
.withSuccess(KEY_UNDO_SUCCESS) .withSuccess(KEY_UNDO_SUCCESS)
.ignoreMoveAwayFromEdit()
.publishIf(() -> examConfigGrant.iw() && !readonly) .publishIf(() -> examConfigGrant.iw() && !readonly)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP) .newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP)

View file

@ -392,7 +392,6 @@ public enum ActionDefinition {
SEB_EXAM_CONFIG_EXPORT_PLAIN_XML( SEB_EXAM_CONFIG_EXPORT_PLAIN_XML(
new LocTextKey("sebserver.examconfig.action.export.plainxml"), new LocTextKey("sebserver.examconfig.action.export.plainxml"),
ImageIcon.EXPORT, ImageIcon.EXPORT,
PageStateDefinitionImpl.SEB_EXAM_CONFIG_VIEW,
ActionCategory.FORM), ActionCategory.FORM),
SEB_EXAM_CONFIG_GET_CONFIG_KEY( SEB_EXAM_CONFIG_GET_CONFIG_KEY(
new LocTextKey("sebserver.examconfig.action.get-config-key"), new LocTextKey("sebserver.examconfig.action.get-config-key"),
@ -402,12 +401,9 @@ public enum ActionDefinition {
new LocTextKey("sebserver.examconfig.action.import-config"), new LocTextKey("sebserver.examconfig.action.import-config"),
ImageIcon.IMPORT, ImageIcon.IMPORT,
ActionCategory.FORM), ActionCategory.FORM),
// TODO copy config action
// TODO
SEB_EXAM_CONFIG_COPY_CONFIG( SEB_EXAM_CONFIG_COPY_CONFIG(
new LocTextKey("sebserver.examconfig.action.copy-config"), new LocTextKey("sebserver.examconfig.action.copy"),
ImageIcon.IMPORT, ImageIcon.COPY,
ActionCategory.FORM), ActionCategory.FORM),
SEB_EXAM_CONFIG_MODIFY_FROM_LIST( SEB_EXAM_CONFIG_MODIFY_FROM_LIST(

View file

@ -0,0 +1,52 @@
/*
* 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.form;
import org.apache.commons.lang3.BooleanUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
public class CheckboxFieldBuilder extends FieldBuilder<String> {
protected CheckboxFieldBuilder(final String name, final LocTextKey label, final String value) {
super(name, label, value);
}
@Override
void build(final FormBuilder builder) {
final boolean readonly = builder.readonly || this.readonly;
final Label lab = builder.labelLocalized(
builder.formParent,
this.label,
this.defaultLabel,
this.spanLabel);
final Composite fieldGrid = Form.createFieldGrid(builder.formParent, this.spanInput);
final Button checkbox = builder.widgetFactory.buttonLocalized(
fieldGrid,
SWT.CHECK,
null, null);
final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true);
checkbox.setLayoutData(gridData);
checkbox.setSelection(BooleanUtils.toBoolean(this.value));
if (readonly) {
checkbox.setEnabled(false);
}
builder.form.putField(this.name, lab, checkbox);
}
}

View file

@ -18,12 +18,14 @@ import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.SWT; import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Label;
@ -130,6 +132,11 @@ public final class Form implements FormBinding {
return this; return this;
} }
Form putField(final String name, final Label label, final Button checkbox) {
this.formFields.add(name, createAccessor(label, checkbox, null));
return this;
}
void putField(final String name, final Label label, final Selection field, final Label errorLabel) { void putField(final String name, final Label label, final Selection field, final Label errorLabel) {
this.formFields.add(name, createAccessor(label, field, errorLabel)); this.formFields.add(name, createAccessor(label, field, errorLabel));
} }
@ -266,6 +273,12 @@ public final class Form implements FormBinding {
@Override public void setStringValue(final String value) {text.setText(value);} @Override public void setStringValue(final String value) {text.setText(value);}
}; };
} }
private FormFieldAccessor createAccessor(final Label label, final Button checkbox, final Label errorLabel) {
return new FormFieldAccessor(label, checkbox, errorLabel) {
@Override public String getStringValue() {return BooleanUtils.toStringTrueFalse(checkbox.getSelection());}
@Override public void setStringValue(final String value) {checkbox.setSelection(BooleanUtils.toBoolean(value));}
};
}
private FormFieldAccessor createAccessor(final Label label, final Selection selection, final Label errorLabel) { private FormFieldAccessor createAccessor(final Label label, final Selection selection, final Label errorLabel) {
switch (selection.type()) { switch (selection.type()) {
case MULTI: case MULTI:

View file

@ -194,6 +194,14 @@ public class FormBuilder {
empty.setText(""); empty.setText("");
} }
public static CheckboxFieldBuilder checkbox(final String name, final LocTextKey label) {
return new CheckboxFieldBuilder(name, label, null);
}
public static CheckboxFieldBuilder checkbox(final String name, final LocTextKey label, final String value) {
return new CheckboxFieldBuilder(name, label, value);
}
public static TextFieldBuilder text(final String name, final LocTextKey label) { public static TextFieldBuilder text(final String name, final LocTextKey label) {
return new TextFieldBuilder(name, label, null); return new TextFieldBuilder(name, label, null);
} }

View file

@ -9,6 +9,7 @@
package ch.ethz.seb.sebserver.gui.service.page.impl; package ch.ethz.seb.sebserver.gui.service.page.impl;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.RWT;
@ -72,6 +73,20 @@ public class ModalInputDialog<T> extends Dialog {
final Runnable cancelCallback, final Runnable cancelCallback,
final ModalInputDialogComposer<T> contentComposer) { final ModalInputDialogComposer<T> contentComposer) {
final Predicate<T> predicate = result -> {
callback.accept(result);
return true;
};
open(title, predicate, cancelCallback, contentComposer);
}
public void open(
final LocTextKey title,
final Predicate<T> callback,
final Runnable cancelCallback,
final ModalInputDialogComposer<T> contentComposer) {
// Create the selection dialog window // Create the selection dialog window
final Shell shell = new Shell(getParent(), getStyle()); final Shell shell = new Shell(getParent(), getStyle());
shell.setText(getText()); shell.setText(getText());
@ -98,8 +113,9 @@ public class ModalInputDialog<T> extends Dialog {
ok.addListener(SWT.Selection, event -> { ok.addListener(SWT.Selection, event -> {
if (valueSuppier != null) { if (valueSuppier != null) {
final T result = valueSuppier.get(); final T result = valueSuppier.get();
callback.accept(result); if (callback.test(result)) {
shell.close(); shell.close();
}
} else { } else {
shell.close(); shell.close();
} }

View file

@ -71,6 +71,7 @@ public class WidgetFactory {
EDIT("edit.png"), EDIT("edit.png"),
EDIT_SETTINGS("settings.png"), EDIT_SETTINGS("settings.png"),
TEST("test.png"), TEST("test.png"),
COPY("copy.png"),
IMPORT("import.png"), IMPORT("import.png"),
CANCEL("cancel.png"), CANCEL("cancel.png"),
CANCEL_EDIT("cancelEdit.png"), CANCEL_EDIT("cancelEdit.png"),

View file

@ -18,6 +18,7 @@ import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.joda.time.DateTimeZone; import org.joda.time.DateTimeZone;
import org.mybatis.dynamic.sql.SqlBuilder; import org.mybatis.dynamic.sql.SqlBuilder;
@ -32,6 +33,7 @@ import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException; import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException;
import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.AttributeType; import ch.ethz.seb.sebserver.gbl.model.sebconfig.AttributeType;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigCopyInfo;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration; import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
@ -55,6 +57,7 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationNodeR
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationRecord; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationRecord;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationValueRecord; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationValueRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
/** This service is internally used to implement MyBatis batch functionality for the most /** This service is internally used to implement MyBatis batch functionality for the most
* intensive write operation on Configuration domain. */ * intensive write operation on Configuration domain. */
@ -317,7 +320,99 @@ class ConfigurationDAOBatchService {
.flatMap(ConfigurationDAOImpl::toDomainModel); .flatMap(ConfigurationDAOImpl::toDomainModel);
} }
Result<Configuration> copyConfiguration( Result<ConfigurationNode> createCopy(
final Long institutionId,
final String newOwner,
final ConfigCopyInfo copyInfo) {
return Result.tryCatch(() -> {
final ConfigurationNodeRecord sourceNode = this.batchConfigurationNodeRecordMapper
.selectByPrimaryKey(copyInfo.configurationNodeId);
if (!sourceNode.getInstitutionId().equals(institutionId)) {
new IllegalArgumentException("Institution integrity violation");
}
return this.copyNodeRecord(sourceNode, newOwner, copyInfo);
})
.flatMap(ConfigurationNodeDAOImpl::toDomainModel)
.onError(TransactionHandler::rollback);
}
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(),
copyInfo.getName(),
copyInfo.getDescription(),
nodeRec.getType(),
ConfigurationStatus.CONSTRUCTION.name());
this.batchConfigurationNodeRecordMapper.insert(newNodeRec);
this.batchSqlSessionTemplate.flushStatements();
final List<ConfigurationRecord> configs = this.batchConfigurationRecordMapper
.selectByExample()
.where(
ConfigurationRecordDynamicSqlSupport.configurationNodeId,
isEqualTo(nodeRec.getId()))
.build()
.execute();
if (BooleanUtils.toBoolean(copyInfo.withHistory)) {
configs
.stream()
.forEach(configRec -> this.copyConfiguration(
configRec.getInstitutionId(),
configRec.getId(),
newNodeRec.getId()));
} else {
configs
.stream()
.filter(configRec -> configRec.getVersionDate() == null)
.findFirst()
.ifPresent(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(),
newNodeRec.getId(),
ConfigurationDAOBatchService.INITIAL_VERSION_NAME,
DateTime.now(DateTimeZone.UTC),
BooleanUtils.toInteger(false));
this.batchConfigurationRecordMapper.insert(newFirstVersion);
this.batchSqlSessionTemplate.flushStatements();
this.copyValues(
configRec.getInstitutionId(),
configRec.getId(),
newFirstVersion.getId());
// and copy the follow-up
final ConfigurationRecord followup = new ConfigurationRecord(
null,
configRec.getInstitutionId(),
newNodeRec.getId(),
null,
null,
BooleanUtils.toInteger(true));
this.batchConfigurationRecordMapper.insert(followup);
this.batchSqlSessionTemplate.flushStatements();
this.copyValues(
configRec.getInstitutionId(),
configRec.getId(),
followup.getId());
});
}
this.batchSqlSessionTemplate.flushStatements();
return newNodeRec;
}
private Result<Configuration> copyConfiguration(
final Long institutionId, final Long institutionId,
final Long fromConfigurationId, final Long fromConfigurationId,
final Long toConfigurationNodeId) { final Long toConfigurationNodeId) {
@ -338,6 +433,7 @@ class ConfigurationDAOBatchService {
fromRecord.getVersionDate(), fromRecord.getVersionDate(),
fromRecord.getFollowup()); fromRecord.getFollowup());
this.batchConfigurationRecordMapper.insert(configurationRecord); this.batchConfigurationRecordMapper.insert(configurationRecord);
this.batchSqlSessionTemplate.flushStatements();
return configurationRecord; return configurationRecord;
}) })
.flatMap(ConfigurationDAOImpl::toDomainModel) .flatMap(ConfigurationDAOImpl::toDomainModel)
@ -347,14 +443,10 @@ class ConfigurationDAOBatchService {
fromConfigurationId, fromConfigurationId,
newConfig.getId()); newConfig.getId());
return newConfig; return newConfig;
})
.map(config -> {
this.batchSqlSessionTemplate.flushStatements();
return config;
}); });
} }
void copyValues( private void copyValues(
final Long institutionId, final Long institutionId,
final Long fromConfigId, final Long fromConfigId,
final Long toConfigId) { final Long toConfigId) {

View file

@ -19,12 +19,7 @@ import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.mybatis.dynamic.sql.SqlBuilder; import org.mybatis.dynamic.sql.SqlBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -46,7 +41,6 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationReco
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationValueRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationValueRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationValueRecordMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationValueRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationNodeRecord; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationNodeRecord;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ConfigurationRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkAction; import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkAction;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationNodeDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationNodeDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport;
@ -63,24 +57,18 @@ public class ConfigurationNodeDAOImpl implements ConfigurationNodeDAO {
private final ConfigurationNodeRecordMapper configurationNodeRecordMapper; private final ConfigurationNodeRecordMapper configurationNodeRecordMapper;
private final ConfigurationValueRecordMapper configurationValueRecordMapper; private final ConfigurationValueRecordMapper configurationValueRecordMapper;
private final ConfigurationDAOBatchService configurationDAOBatchService; private final ConfigurationDAOBatchService configurationDAOBatchService;
private final String copyNamePrefix;
private final String copyNameSuffix;
protected ConfigurationNodeDAOImpl( protected ConfigurationNodeDAOImpl(
final ConfigurationRecordMapper configurationRecordMapper, final ConfigurationRecordMapper configurationRecordMapper,
final ConfigurationNodeRecordMapper configurationNodeRecordMapper, final ConfigurationNodeRecordMapper configurationNodeRecordMapper,
final ConfigurationValueRecordMapper configurationValueRecordMapper, final ConfigurationValueRecordMapper configurationValueRecordMapper,
final ConfigurationAttributeRecordMapper configurationAttributeRecordMapper, final ConfigurationAttributeRecordMapper configurationAttributeRecordMapper,
final ConfigurationDAOBatchService ConfigurationDAOBatchService, final ConfigurationDAOBatchService ConfigurationDAOBatchService) {
@Value("${sebserver.webservice.api.copy-name-prefix:Copy of }") final String copyNamePrefix,
@Value("${sebserver.webservice.api.copy-name-suffix:}") final String copyNameSuffix) {
this.configurationRecordMapper = configurationRecordMapper; this.configurationRecordMapper = configurationRecordMapper;
this.configurationNodeRecordMapper = configurationNodeRecordMapper; this.configurationNodeRecordMapper = configurationNodeRecordMapper;
this.configurationValueRecordMapper = configurationValueRecordMapper; this.configurationValueRecordMapper = configurationValueRecordMapper;
this.configurationDAOBatchService = ConfigurationDAOBatchService; this.configurationDAOBatchService = ConfigurationDAOBatchService;
this.copyNamePrefix = copyNamePrefix;
this.copyNameSuffix = copyNameSuffix;
} }
@Override @Override
@ -211,12 +199,7 @@ public class ConfigurationNodeDAOImpl implements ConfigurationNodeDAO {
final String newOwner, final String newOwner,
final ConfigCopyInfo copyInfo) { final ConfigCopyInfo copyInfo) {
return this.recordById(copyInfo.configurationNodeId) return this.configurationDAOBatchService.createCopy(institutionId, newOwner, copyInfo);
.flatMap(nodeRec -> (nodeRec.getInstitutionId().equals(institutionId)
? Result.of(nodeRec)
: Result.ofError(new IllegalArgumentException("Institution integrity violation"))))
.map(nodeRec -> this.copyNodeRecord(nodeRec, newOwner, copyInfo))
.flatMap(ConfigurationNodeDAOImpl::toDomainModel);
} }
@Override @Override
@ -283,68 +266,6 @@ 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) { static Result<ConfigurationNode> toDomainModel(final ConfigurationNodeRecord record) {
return Result.tryCatch(() -> new ConfigurationNode( return Result.tryCatch(() -> new ConfigurationNode(
record.getId(), record.getId(),

View file

@ -257,6 +257,12 @@ public class ExamAdministrationController extends ActivatableEntityController<Ex
.getOrThrow(); .getOrThrow();
} }
@Override
protected Result<Exam> validForCreate(final Exam entity) {
return super.validForCreate(entity)
.map(this::checkExamSupporterRole);
}
@Override @Override
protected Result<Exam> validForSave(final Exam entity) { protected Result<Exam> validForSave(final Exam entity) {
return super.validForSave(entity) return super.validForSave(entity)

View file

@ -34,8 +34,6 @@ sebserver.webservice.api.exam.accessTokenValiditySeconds=3600
sebserver.webservice.api.exam.event-handling-strategy=ASYNC_BATCH_STORE_STRATEGY sebserver.webservice.api.exam.event-handling-strategy=ASYNC_BATCH_STORE_STRATEGY
sebserver.webservice.api.exam.enable-indicator-cache=true sebserver.webservice.api.exam.enable-indicator-cache=true
sebserver.webservice.api.pagination.maxPageSize=500 sebserver.webservice.api.pagination.maxPageSize=500
sebserver.webservice.api.copy-name-prefix=Copy of
sebserver.webservice.api.copy-name-suffix=
# comma separated list of known possible OpenEdX API access token request endpoints # comma separated list of known possible OpenEdX API access token request endpoints
sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token
sebserver.webservice.lms.address.alias=lms.mockup.com=lms.address.alias sebserver.webservice.lms.address.alias=lms.mockup.com=lms.address.alias

View file

@ -495,14 +495,9 @@ INSERT IGNORE INTO orientation VALUES
(520, 520, 0, 11, 'functionKeys', 3, 12, 3, 1, 'NONE') (520, 520, 0, 11, 'functionKeys', 3, 12, 3, 1, 'NONE')
; ;
INSERT IGNORE INTO configuration_node VALUES INSERT IGNORE INTO configuration_node VALUES
(1, 1, 0, 'super-admin', 'test', null, 'EXAM_CONFIG', 'READY_TO_USE') (1, 1, 0, 'super-admin', 'test', null, 'EXAM_CONFIG', 'IN_USE')
; ;
INSERT IGNORE INTO configuration VALUES INSERT IGNORE INTO configuration VALUES

View file

@ -450,6 +450,7 @@ sebserver.examconfig.action.saveToHistory=Save / Publish
sebserver.examconfig.action.saveToHistory.success=Successfully saved in history sebserver.examconfig.action.saveToHistory.success=Successfully saved in history
sebserver.examconfig.action.undo=Undo sebserver.examconfig.action.undo=Undo
sebserver.examconfig.action.undo.success=Successfully reverted to last saved state sebserver.examconfig.action.undo.success=Successfully reverted to last saved state
sebserver.examconfig.action.copy=Copy Configuration
sebserver.examconfig.action.export.plainxml=Export Configuration sebserver.examconfig.action.export.plainxml=Export Configuration
sebserver.examconfig.action.get-config-key=Export Config-Key sebserver.examconfig.action.get-config-key=Export Config-Key
sebserver.examconfig.action.import-config=Import Configuration sebserver.examconfig.action.import-config=Import Configuration
@ -462,6 +463,7 @@ sebserver.examconfig.form.title.new=Add Exam Configuration
sebserver.examconfig.form.title=Exam Configuration sebserver.examconfig.form.title=Exam Configuration
sebserver.examconfig.form.name=Name sebserver.examconfig.form.name=Name
sebserver.examconfig.form.description=Description sebserver.examconfig.form.description=Description
sebserver.examconfig.form.with-history=With History
sebserver.examconfig.form.template=From Template sebserver.examconfig.form.template=From Template
sebserver.examconfig.form.status=Status sebserver.examconfig.form.status=Status
sebserver.examconfig.form.config-key.title=Config Key sebserver.examconfig.form.config-key.title=Config Key

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

View file

@ -679,6 +679,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
.getBuilder(ImportAsExam.class) .getBuilder(ImportAsExam.class)
.withFormParam(QuizData.QUIZ_ATTR_LMS_SETUP_ID, String.valueOf(quizData.lmsSetupId)) .withFormParam(QuizData.QUIZ_ATTR_LMS_SETUP_ID, String.valueOf(quizData.lmsSetupId))
.withFormParam(QuizData.QUIZ_ATTR_ID, quizData.id) .withFormParam(QuizData.QUIZ_ATTR_ID, quizData.id)
.withFormParam(Domain.EXAM.ATTR_SUPPORTER, userId)
.call(); .call();
assertNotNull(newExamResult); assertNotNull(newExamResult);
@ -687,7 +688,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
assertEquals("Demo Quiz 1", newExam.name); assertEquals("Demo Quiz 1", newExam.name);
assertEquals(ExamType.UNDEFINED, newExam.type); assertEquals(ExamType.UNDEFINED, newExam.type);
assertTrue(newExam.supporter.isEmpty()); assertFalse(newExam.supporter.isEmpty());
// create Exam with type and supporter examSupport2 // create Exam with type and supporter examSupport2
final Exam examForSave = new Exam( final Exam examForSave = new Exam(

View file

@ -37,12 +37,13 @@ public class ExamAPITest extends AdministrationAPIIntegrationTester {
sebAdminAccess, sebAdminAccess,
"LmsSetupMock", "LmsSetupMock",
"quiz2", "quiz2",
ExamType.MANAGED); ExamType.MANAGED,
"user5");
assertNotNull(exam); assertNotNull(exam);
assertEquals("quiz2", exam.getExternalId()); assertEquals("quiz2", exam.getExternalId());
assertEquals(ExamType.MANAGED, exam.getType()); assertEquals(ExamType.MANAGED, exam.getType());
assertTrue(exam.getSupporter().isEmpty()); assertFalse(exam.getSupporter().isEmpty());
// add ExamSupporter // add ExamSupporter
final Exam newExam = new RestAPITestHelper() final Exam newExam = new RestAPITestHelper()

View file

@ -43,6 +43,7 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester {
.withMethod(HttpMethod.POST) .withMethod(HttpMethod.POST)
.withAttribute(QuizData.QUIZ_ATTR_LMS_SETUP_ID, lmsSetup1.getModelId()) .withAttribute(QuizData.QUIZ_ATTR_LMS_SETUP_ID, lmsSetup1.getModelId())
.withAttribute(QuizData.QUIZ_ATTR_ID, "quiz1") .withAttribute(QuizData.QUIZ_ATTR_ID, "quiz1")
.withAttribute(Domain.EXAM.ATTR_SUPPORTER, "user1")
.withExpectedStatus(HttpStatus.OK) .withExpectedStatus(HttpStatus.OK)
.getAsObject(new TypeReference<Exam>() { .getAsObject(new TypeReference<Exam>() {
}); });
@ -58,7 +59,8 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester {
getSebAdminAccess(), getSebAdminAccess(),
"LmsSetupMock", "LmsSetupMock",
"quiz2", "quiz2",
ExamType.MANAGED); ExamType.MANAGED,
"user5");
assertNotNull(exam2); assertNotNull(exam2);
assertEquals("quiz2", exam2.getExternalId()); assertEquals("quiz2", exam2.getExternalId());
@ -76,7 +78,8 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester {
getAdminInstitution2Access(), getAdminInstitution2Access(),
"LmsSetupMock", "LmsSetupMock",
"quiz2", "quiz2",
ExamType.MANAGED); ExamType.MANAGED,
"user7");
fail("AssertionError expected here"); fail("AssertionError expected here");
} catch (final AssertionError ae) { } catch (final AssertionError ae) {
assertEquals("Response status expected:<200> but was:<403>", ae.getMessage()); assertEquals("Response status expected:<200> but was:<403>", ae.getMessage());
@ -89,7 +92,8 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester {
getExamAdmin1(), // this exam administrator is on Institution 2 getExamAdmin1(), // this exam administrator is on Institution 2
"LmsSetupMock2", "LmsSetupMock2",
"quiz2", "quiz2",
ExamType.MANAGED); ExamType.MANAGED,
"user7");
assertNotNull(exam2); assertNotNull(exam2);
assertEquals("quiz2", exam2.getExternalId()); assertEquals("quiz2", exam2.getExternalId());
@ -106,7 +110,8 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester {
getExamAdmin1(), // this exam administrator is on Institution 2 getExamAdmin1(), // this exam administrator is on Institution 2
"LmsSetupMock", "LmsSetupMock",
"quiz2", "quiz2",
ExamType.MANAGED); ExamType.MANAGED,
"user7");
fail("AssertionError expected here"); fail("AssertionError expected here");
} catch (final AssertionError ae) { } catch (final AssertionError ae) {
assertEquals("Response status expected:<200> but was:<403>", ae.getMessage()); assertEquals("Response status expected:<200> but was:<403>", ae.getMessage());
@ -119,7 +124,8 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester {
final String tokenForExamImport, final String tokenForExamImport,
final String lmsSetupName, final String lmsSetupName,
final String importQuizName, final String importQuizName,
final ExamType examType) throws Exception { final ExamType examType,
final String supporter) throws Exception {
// create new active LmsSetup Mock with seb-admin // create new active LmsSetup Mock with seb-admin
final LmsSetup lmsSetup1 = QuizDataTest.createLmsSetupMock( final LmsSetup lmsSetup1 = QuizDataTest.createLmsSetupMock(
@ -135,6 +141,7 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester {
.withMethod(HttpMethod.POST) .withMethod(HttpMethod.POST)
.withAttribute(QuizData.QUIZ_ATTR_LMS_SETUP_ID, lmsSetup1.getModelId()) .withAttribute(QuizData.QUIZ_ATTR_LMS_SETUP_ID, lmsSetup1.getModelId())
.withAttribute(QuizData.QUIZ_ATTR_ID, importQuizName) .withAttribute(QuizData.QUIZ_ATTR_ID, importQuizName)
.withAttribute(Domain.EXAM.ATTR_SUPPORTER, supporter)
.withAttribute(Domain.EXAM.ATTR_TYPE, examType.name()) .withAttribute(Domain.EXAM.ATTR_TYPE, examType.name())
.withExpectedStatus(HttpStatus.OK) .withExpectedStatus(HttpStatus.OK)
.getAsObject(new TypeReference<Exam>() { .getAsObject(new TypeReference<Exam>() {