diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigCopy.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigCopy.java index 9c2f5b9f..d617f872 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigCopy.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigCopy.java @@ -9,6 +9,7 @@ package ch.ethz.seb.sebserver.gui.content; import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; 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.ConfigurationNode; 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.FormHandle; 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 { - static Function importConfigFunction( + static Function copyConfigFunction( final PageService pageService, final PageContext pageContext) { @@ -46,12 +48,14 @@ public final class SebExamConfigCopy { pageService, action.pageContext()); + final Predicate> doCopy = formHandle -> doCopy( + pageService, + pageContext, + formHandle); + dialog.open( SebExamConfigPropForm.FORM_COPY_TEXT_KEY, - formHandle -> doCopy( - pageService, - pageContext, - formHandle), + doCopy, Utils.EMPTY_EXECUTION, formContext); @@ -59,7 +63,7 @@ public final class SebExamConfigCopy { }; } - private static final void doCopy( + private static final boolean doCopy( final PageService pageService, final PageContext pageContext, final FormHandle formHandle) { @@ -67,13 +71,21 @@ public final class SebExamConfigCopy { final ConfigurationNode newConfig = pageService.getRestService().getBuilder(CopyConfiguration.class) .withFormBinding(formHandle.getFormBinding()) .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)) .create(); pageService.executePageAction(viewNewConfig); + + return true; } private static final class CopyFormContext implements ModalInputDialogComposer> { @@ -103,6 +115,9 @@ public final class SebExamConfigCopy { Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION, SebExamConfigPropForm.FORM_DESCRIPTION_TEXT_KEY) .asArea()) + .addField(FormBuilder.checkbox( + ConfigCopyInfo.ATTR_COPY_WITH_HISTORY, + SebExamConfigPropForm.FORM_HISTORY_TEXT_KEY)) .build(); return () -> formHandle; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigPropForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigPropForm.java index fc8ee4e0..4f1bf1a0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigPropForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigPropForm.java @@ -69,6 +69,8 @@ public class SebExamConfigPropForm implements TemplateComposer { new LocTextKey("sebserver.examconfig.form.name"); static final LocTextKey FORM_DESCRIPTION_TEXT_KEY = 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 = new LocTextKey("sebserver.examconfig.form.template"); 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 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) .publishIf(() -> writeGrant && isReadonly) @@ -237,6 +240,12 @@ public class SebExamConfigPropForm implements TemplateComposer { .noEventPropagation() .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) .withEntityKey(entityKey) .withExec(formHandle::processFormSave) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigSettingsForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigSettingsForm.java index 62b21c8f..2342e013 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigSettingsForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigSettingsForm.java @@ -155,6 +155,7 @@ public class SebExamConfigSettingsForm implements TemplateComposer { return action; }) .withSuccess(KEY_SAVE_TO_HISTORY_SUCCESS) + .ignoreMoveAwayFromEdit() .publishIf(() -> examConfigGrant.iw() && !readonly) .newAction(ActionDefinition.SEB_EXAM_CONFIG_UNDO) @@ -168,6 +169,7 @@ public class SebExamConfigSettingsForm implements TemplateComposer { return action; }) .withSuccess(KEY_UNDO_SUCCESS) + .ignoreMoveAwayFromEdit() .publishIf(() -> examConfigGrant.iw() && !readonly) .newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java index b2af4285..935f65e1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java @@ -392,7 +392,6 @@ public enum ActionDefinition { SEB_EXAM_CONFIG_EXPORT_PLAIN_XML( new LocTextKey("sebserver.examconfig.action.export.plainxml"), ImageIcon.EXPORT, - PageStateDefinitionImpl.SEB_EXAM_CONFIG_VIEW, ActionCategory.FORM), SEB_EXAM_CONFIG_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"), ImageIcon.IMPORT, ActionCategory.FORM), - - // TODO copy config action - // TODO SEB_EXAM_CONFIG_COPY_CONFIG( - new LocTextKey("sebserver.examconfig.action.copy-config"), - ImageIcon.IMPORT, + new LocTextKey("sebserver.examconfig.action.copy"), + ImageIcon.COPY, ActionCategory.FORM), SEB_EXAM_CONFIG_MODIFY_FROM_LIST( diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/CheckboxFieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/CheckboxFieldBuilder.java new file mode 100644 index 00000000..25c35506 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/CheckboxFieldBuilder.java @@ -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 { + + 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); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java index 5641a600..500295c8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java @@ -18,12 +18,14 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Predicate; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.rap.rwt.RWT; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; @@ -130,6 +132,11 @@ public final class Form implements FormBinding { 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) { 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);} }; } + 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) { switch (selection.type()) { case MULTI: diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/FormBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/FormBuilder.java index 3ae7c926..e0afbffe 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/FormBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/FormBuilder.java @@ -194,6 +194,14 @@ public class FormBuilder { 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) { return new TextFieldBuilder(name, label, null); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ModalInputDialog.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ModalInputDialog.java index 5379607f..2691eefa 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ModalInputDialog.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ModalInputDialog.java @@ -9,6 +9,7 @@ package ch.ethz.seb.sebserver.gui.service.page.impl; import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.function.Supplier; import org.eclipse.rap.rwt.RWT; @@ -72,6 +73,20 @@ public class ModalInputDialog extends Dialog { final Runnable cancelCallback, final ModalInputDialogComposer contentComposer) { + final Predicate predicate = result -> { + callback.accept(result); + return true; + }; + + open(title, predicate, cancelCallback, contentComposer); + } + + public void open( + final LocTextKey title, + final Predicate callback, + final Runnable cancelCallback, + final ModalInputDialogComposer contentComposer) { + // Create the selection dialog window final Shell shell = new Shell(getParent(), getStyle()); shell.setText(getText()); @@ -98,8 +113,9 @@ public class ModalInputDialog extends Dialog { ok.addListener(SWT.Selection, event -> { if (valueSuppier != null) { final T result = valueSuppier.get(); - callback.accept(result); - shell.close(); + if (callback.test(result)) { + shell.close(); + } } else { shell.close(); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java index 6f2b519b..a02ed53b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java @@ -71,6 +71,7 @@ public class WidgetFactory { EDIT("edit.png"), EDIT_SETTINGS("settings.png"), TEST("test.png"), + COPY("copy.png"), IMPORT("import.png"), CANCEL("cancel.png"), CANCEL_EDIT("cancelEdit.png"), diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationDAOBatchService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationDAOBatchService.java index f380db1c..ed9c0644 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationDAOBatchService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationDAOBatchService.java @@ -18,6 +18,7 @@ import java.util.function.Function; 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; @@ -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.EntityType; 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.ConfigurationAttribute; 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.ConfigurationValueRecord; 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 * intensive write operation on Configuration domain. */ @@ -317,7 +320,99 @@ class ConfigurationDAOBatchService { .flatMap(ConfigurationDAOImpl::toDomainModel); } - Result copyConfiguration( + Result 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 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 copyConfiguration( final Long institutionId, final Long fromConfigurationId, final Long toConfigurationNodeId) { @@ -338,6 +433,7 @@ class ConfigurationDAOBatchService { fromRecord.getVersionDate(), fromRecord.getFollowup()); this.batchConfigurationRecordMapper.insert(configurationRecord); + this.batchSqlSessionTemplate.flushStatements(); return configurationRecord; }) .flatMap(ConfigurationDAOImpl::toDomainModel) @@ -347,14 +443,10 @@ class ConfigurationDAOBatchService { fromConfigurationId, newConfig.getId()); return newConfig; - }) - .map(config -> { - this.batchSqlSessionTemplate.flushStatements(); - return config; }); } - void copyValues( + private void copyValues( final Long institutionId, final Long fromConfigId, final Long toConfigId) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationNodeDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationNodeDAOImpl.java index 0e757089..0957a1b8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationNodeDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationNodeDAOImpl.java @@ -19,12 +19,7 @@ import java.util.function.Function; import java.util.function.Predicate; 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.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; 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.ConfigurationValueRecordMapper; 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.dao.ConfigurationNodeDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport; @@ -63,24 +57,18 @@ public class ConfigurationNodeDAOImpl implements ConfigurationNodeDAO { private final ConfigurationNodeRecordMapper configurationNodeRecordMapper; private final ConfigurationValueRecordMapper configurationValueRecordMapper; private final ConfigurationDAOBatchService configurationDAOBatchService; - private final String copyNamePrefix; - private final String copyNameSuffix; protected ConfigurationNodeDAOImpl( final ConfigurationRecordMapper configurationRecordMapper, final ConfigurationNodeRecordMapper configurationNodeRecordMapper, final ConfigurationValueRecordMapper configurationValueRecordMapper, final ConfigurationAttributeRecordMapper configurationAttributeRecordMapper, - 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) { + final ConfigurationDAOBatchService ConfigurationDAOBatchService) { this.configurationRecordMapper = configurationRecordMapper; this.configurationNodeRecordMapper = configurationNodeRecordMapper; this.configurationValueRecordMapper = configurationValueRecordMapper; this.configurationDAOBatchService = ConfigurationDAOBatchService; - this.copyNamePrefix = copyNamePrefix; - this.copyNameSuffix = copyNameSuffix; } @Override @@ -211,12 +199,7 @@ public class ConfigurationNodeDAOImpl implements ConfigurationNodeDAO { final String newOwner, final ConfigCopyInfo copyInfo) { - 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, copyInfo)) - .flatMap(ConfigurationNodeDAOImpl::toDomainModel); + return this.configurationDAOBatchService.createCopy(institutionId, newOwner, copyInfo); } @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 configs = this.configurationRecordMapper - .selectByExample() - .where( - ConfigurationRecordDynamicSqlSupport.configurationNodeId, - isEqualTo(nodeRec.getId())) - .build() - .execute(); - - if (BooleanUtils.toBoolean(copyInfo.withHistory)) { - configs - .stream() - .forEach(configRec -> this.configurationDAOBatchService.copyConfiguration( - configRec.getInstitutionId(), - configRec.getId(), - newNodeRec.getId())); - } else { - configs - .stream() - .filter(configRec -> configRec.getVersionDate() == null) - .findFirst() - .map(configRec -> { - // No history means to create a first version and a follow-up with the copied values - final ConfigurationRecord newFirstVersion = new ConfigurationRecord( - null, - configRec.getInstitutionId(), - configRec.getConfigurationNodeId(), - ConfigurationDAOBatchService.INITIAL_VERSION_NAME, - DateTime.now(DateTimeZone.UTC), - BooleanUtils.toInteger(false)); - this.configurationRecordMapper.insert(newFirstVersion); - this.configurationDAOBatchService.copyValues( - configRec.getInstitutionId(), - configRec.getId(), - newFirstVersion.getId()); - // and copy the follow-up - this.configurationDAOBatchService.copyConfiguration( - configRec.getInstitutionId(), - configRec.getId(), - newNodeRec.getId()); - return configRec; - }); - } - - return newNodeRec; - } - static Result toDomainModel(final ConfigurationNodeRecord record) { return Result.tryCatch(() -> new ConfigurationNode( record.getId(), diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java index d357c49f..75798518 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java @@ -257,6 +257,12 @@ public class ExamAdministrationController extends ActivatableEntityController validForCreate(final Exam entity) { + return super.validForCreate(entity) + .map(this::checkExamSupporterRole); + } + @Override protected Result validForSave(final Exam entity) { return super.validForSave(entity) diff --git a/src/main/resources/config/application-dev-ws.properties b/src/main/resources/config/application-dev-ws.properties index 11aaeeb6..19223ba0 100644 --- a/src/main/resources/config/application-dev-ws.properties +++ b/src/main/resources/config/application-dev-ws.properties @@ -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.enable-indicator-cache=true 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 sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token sebserver.webservice.lms.address.alias=lms.mockup.com=lms.address.alias diff --git a/src/main/resources/data-demo.sql b/src/main/resources/data-demo.sql index f3e1b8dd..5a38c15c 100644 --- a/src/main/resources/data-demo.sql +++ b/src/main/resources/data-demo.sql @@ -495,14 +495,9 @@ INSERT IGNORE INTO orientation VALUES (520, 520, 0, 11, 'functionKeys', 3, 12, 3, 1, 'NONE') ; - - - - - - + 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 diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 6187b9e0..15f9fad0 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -450,6 +450,7 @@ sebserver.examconfig.action.saveToHistory=Save / Publish sebserver.examconfig.action.saveToHistory.success=Successfully saved in history sebserver.examconfig.action.undo=Undo 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.get-config-key=Export Config-Key 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.name=Name sebserver.examconfig.form.description=Description +sebserver.examconfig.form.with-history=With History sebserver.examconfig.form.template=From Template sebserver.examconfig.form.status=Status sebserver.examconfig.form.config-key.title=Config Key diff --git a/src/main/resources/static/images/copy.png b/src/main/resources/static/images/copy.png new file mode 100644 index 00000000..1650262e Binary files /dev/null and b/src/main/resources/static/images/copy.png differ diff --git a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java index 361e559d..cc465dd5 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java @@ -679,6 +679,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { .getBuilder(ImportAsExam.class) .withFormParam(QuizData.QUIZ_ATTR_LMS_SETUP_ID, String.valueOf(quizData.lmsSetupId)) .withFormParam(QuizData.QUIZ_ATTR_ID, quizData.id) + .withFormParam(Domain.EXAM.ATTR_SUPPORTER, userId) .call(); assertNotNull(newExamResult); @@ -687,7 +688,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { assertEquals("Demo Quiz 1", newExam.name); assertEquals(ExamType.UNDEFINED, newExam.type); - assertTrue(newExam.supporter.isEmpty()); + assertFalse(newExam.supporter.isEmpty()); // create Exam with type and supporter examSupport2 final Exam examForSave = new Exam( diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamAPITest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamAPITest.java index e654df39..b2d406f1 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamAPITest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamAPITest.java @@ -37,12 +37,13 @@ public class ExamAPITest extends AdministrationAPIIntegrationTester { sebAdminAccess, "LmsSetupMock", "quiz2", - ExamType.MANAGED); + ExamType.MANAGED, + "user5"); assertNotNull(exam); assertEquals("quiz2", exam.getExternalId()); assertEquals(ExamType.MANAGED, exam.getType()); - assertTrue(exam.getSupporter().isEmpty()); + assertFalse(exam.getSupporter().isEmpty()); // add ExamSupporter final Exam newExam = new RestAPITestHelper() diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamImportTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamImportTest.java index b1459e40..bbbcf485 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamImportTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamImportTest.java @@ -43,6 +43,7 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester { .withMethod(HttpMethod.POST) .withAttribute(QuizData.QUIZ_ATTR_LMS_SETUP_ID, lmsSetup1.getModelId()) .withAttribute(QuizData.QUIZ_ATTR_ID, "quiz1") + .withAttribute(Domain.EXAM.ATTR_SUPPORTER, "user1") .withExpectedStatus(HttpStatus.OK) .getAsObject(new TypeReference() { }); @@ -58,7 +59,8 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester { getSebAdminAccess(), "LmsSetupMock", "quiz2", - ExamType.MANAGED); + ExamType.MANAGED, + "user5"); assertNotNull(exam2); assertEquals("quiz2", exam2.getExternalId()); @@ -76,7 +78,8 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester { getAdminInstitution2Access(), "LmsSetupMock", "quiz2", - ExamType.MANAGED); + ExamType.MANAGED, + "user7"); fail("AssertionError expected here"); } catch (final AssertionError ae) { 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 "LmsSetupMock2", "quiz2", - ExamType.MANAGED); + ExamType.MANAGED, + "user7"); assertNotNull(exam2); assertEquals("quiz2", exam2.getExternalId()); @@ -106,7 +110,8 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester { getExamAdmin1(), // this exam administrator is on Institution 2 "LmsSetupMock", "quiz2", - ExamType.MANAGED); + ExamType.MANAGED, + "user7"); fail("AssertionError expected here"); } catch (final AssertionError ae) { assertEquals("Response status expected:<200> but was:<403>", ae.getMessage()); @@ -119,7 +124,8 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester { final String tokenForExamImport, final String lmsSetupName, final String importQuizName, - final ExamType examType) throws Exception { + final ExamType examType, + final String supporter) throws Exception { // create new active LmsSetup Mock with seb-admin final LmsSetup lmsSetup1 = QuizDataTest.createLmsSetupMock( @@ -135,6 +141,7 @@ public class ExamImportTest extends AdministrationAPIIntegrationTester { .withMethod(HttpMethod.POST) .withAttribute(QuizData.QUIZ_ATTR_LMS_SETUP_ID, lmsSetup1.getModelId()) .withAttribute(QuizData.QUIZ_ATTR_ID, importQuizName) + .withAttribute(Domain.EXAM.ATTR_SUPPORTER, supporter) .withAttribute(Domain.EXAM.ATTR_TYPE, examType.name()) .withExpectedStatus(HttpStatus.OK) .getAsObject(new TypeReference() {