SEBSERV-73 adapt new config handling with states in front- and back-end

This commit is contained in:
anhefti 2019-10-22 16:42:33 +02:00
parent 555d9a34e0
commit 6eaef577d2
44 changed files with 713 additions and 212 deletions

View file

@ -119,6 +119,7 @@ public final class API {
public static final String CONFIGURATION_ENDPOINT = "/configuration"; 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_UNDO_PATH_SEGMENT = "/undo";
public static final String CONFIGURATION_COPY_PATH_SEGMENT = "/copy";
public static final String CONFIGURATION_RESTORE_FROM_HISTORY_PATH_SEGMENT = "/restore"; public static final String CONFIGURATION_RESTORE_FROM_HISTORY_PATH_SEGMENT = "/restore";
public static final String CONFIGURATION_VALUE_ENDPOINT = "/configuration_value"; public static final String CONFIGURATION_VALUE_ENDPOINT = "/configuration_value";
public static final String CONFIGURATION_TABLE_VALUE_PATH_SEGMENT = "/table"; public static final String CONFIGURATION_TABLE_VALUE_PATH_SEGMENT = "/table";

View file

@ -25,6 +25,7 @@ import ch.ethz.seb.sebserver.gbl.model.GrantEntity;
public final class ConfigurationNode implements GrantEntity { public final class ConfigurationNode implements GrantEntity {
public static final Long DEFAULT_TEMPLATE_ID = 0L; 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_TEMPLATE_ID = "templateId";
public static final String FILTER_ATTR_DESCRIPTION = "description"; public static final String FILTER_ATTR_DESCRIPTION = "description";

View file

@ -176,7 +176,7 @@ public class ConfigTemplateAttributeForm implements TemplateComposer {
configuration, configuration,
new View(-1L, "template", 10, 0, templateId), new View(-1L, "template", 10, 0, templateId),
attributeMapping, attributeMapping,
1); 1, false);
final InputFieldBuilder inputFieldBuilder = this.examConfigurationService.getInputFieldBuilder( final InputFieldBuilder inputFieldBuilder = this.examConfigurationService.getInputFieldBuilder(
attribute.getConfigAttribute(), attribute.getConfigAttribute(),

View file

@ -30,6 +30,7 @@ import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigKey; 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.Configuration;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; 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.sebconfig.ConfigurationNode.ConfigurationType;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
@ -74,6 +75,8 @@ public class SebExamConfigPropForm implements TemplateComposer {
new LocTextKey("sebserver.examconfig.form.name"); new LocTextKey("sebserver.examconfig.form.name");
private static final LocTextKey FORM_DESCRIPTION_TEXT_KEY = private static final LocTextKey FORM_DESCRIPTION_TEXT_KEY =
new LocTextKey("sebserver.examconfig.form.description"); new LocTextKey("sebserver.examconfig.form.description");
private static final LocTextKey FORM_TEMPLATE_TEXT_KEY =
new LocTextKey("sebserver.examconfig.form.template");
private static final LocTextKey FORM_STATUS_TEXT_KEY = private static final LocTextKey FORM_STATUS_TEXT_KEY =
new LocTextKey("sebserver.examconfig.form.status"); new LocTextKey("sebserver.examconfig.form.status");
private static final LocTextKey FORM_IMPORT_TEXT_KEY = private static final LocTextKey FORM_IMPORT_TEXT_KEY =
@ -113,6 +116,7 @@ public class SebExamConfigPropForm implements TemplateComposer {
final UserInfo user = this.currentUser.get(); final UserInfo user = this.currentUser.get();
final EntityKey entityKey = pageContext.getEntityKey(); final EntityKey entityKey = pageContext.getEntityKey();
final EntityKey parentEntityKey = pageContext.getParentEntityKey();
final boolean isNew = entityKey == null; final boolean isNew = entityKey == null;
// get data or create new. Handle error if happen // get data or create new. Handle error if happen
@ -169,6 +173,14 @@ public class SebExamConfigPropForm implements TemplateComposer {
FORM_DESCRIPTION_TEXT_KEY, FORM_DESCRIPTION_TEXT_KEY,
examConfig.description) examConfig.description)
.asArea()) .asArea())
.addField(FormBuilder.singleSelection(
Domain.CONFIGURATION_NODE.ATTR_TEMPLATE_ID,
FORM_TEMPLATE_TEXT_KEY,
(parentEntityKey != null)
? parentEntityKey.modelId
: String.valueOf(examConfig.templateId),
resourceService::getExamConfigTemplateResources)
.readonly(!isNew))
.addField(FormBuilder.singleSelection( .addField(FormBuilder.singleSelection(
Domain.CONFIGURATION_NODE.ATTR_STATUS, Domain.CONFIGURATION_NODE.ATTR_STATUS,
FORM_STATUS_TEXT_KEY, FORM_STATUS_TEXT_KEY,
@ -178,6 +190,7 @@ public class SebExamConfigPropForm implements TemplateComposer {
? this.restService.getRestCall(NewExamConfig.class) ? this.restService.getRestCall(NewExamConfig.class)
: this.restService.getRestCall(SaveExamConfig.class)); : this.restService.getRestCall(SaveExamConfig.class));
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()) this.pageService.pageActionBuilder(formContext.clearEntityKeys())
@ -190,7 +203,11 @@ public class SebExamConfigPropForm implements TemplateComposer {
.newAction(ActionDefinition.SEB_EXAM_CONFIG_MODIFY) .newAction(ActionDefinition.SEB_EXAM_CONFIG_MODIFY)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.publishIf(() -> modifyGrant && isReadonly) .publishIf(() -> modifyGrant && isReadonly && !settingsReadonly)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW)
.withEntityKey(entityKey)
.publishIf(() -> modifyGrant && isReadonly && settingsReadonly)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_EXPORT_PLAIN_XML) .newAction(ActionDefinition.SEB_EXAM_CONFIG_EXPORT_PLAIN_XML)
.withEntityKey(entityKey) .withEntityKey(entityKey)
@ -216,13 +233,13 @@ public class SebExamConfigPropForm implements TemplateComposer {
.noEventPropagation() .noEventPropagation()
.publishIf(() -> modifyGrant && isReadonly) .publishIf(() -> modifyGrant && isReadonly)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_SAVE) .newAction(ActionDefinition.SEB_EXAM_CONFIG_PROP_SAVE)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.withExec(formHandle::processFormSave) .withExec(formHandle::processFormSave)
.ignoreMoveAwayFromEdit() .ignoreMoveAwayFromEdit()
.publishIf(() -> !isReadonly) .publishIf(() -> !isReadonly)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_CANCEL_MODIFY) .newAction(ActionDefinition.SEB_EXAM_CONFIG_PROP_CANCEL_MODIFY)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.withExec(this.pageService.backToCurrentFunction()) .withExec(this.pageService.backToCurrentFunction())
.publishIf(() -> !isReadonly); .publishIf(() -> !isReadonly);

View file

@ -113,7 +113,6 @@ public class SebExamConfigSettingsForm implements TemplateComposer {
.getOrThrow(); .getOrThrow();
final List<View> views = this.examConfigurationService.getViews(attributes); final List<View> views = this.examConfigurationService.getViews(attributes);
final TabFolder tabFolder = widgetFactory.tabFolderLocalized(content); final TabFolder tabFolder = widgetFactory.tabFolderLocalized(content);
tabFolder.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); tabFolder.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
@ -124,7 +123,8 @@ public class SebExamConfigSettingsForm implements TemplateComposer {
configuration, configuration,
view, view,
attributes, attributes,
20); 20,
pageContext.isReadonly());
viewContexts.add(viewContext); viewContexts.add(viewContext);
final Composite viewGrid = this.examConfigurationService.createViewGrid( final Composite viewGrid = this.examConfigurationService.createViewGrid(
@ -141,6 +141,7 @@ public class SebExamConfigSettingsForm implements TemplateComposer {
final GrantCheck examConfigGrant = this.currentUser.grantCheck(EntityType.CONFIGURATION_NODE); final GrantCheck examConfigGrant = this.currentUser.grantCheck(EntityType.CONFIGURATION_NODE);
this.pageService.pageActionBuilder(pageContext.clearEntityKeys()) this.pageService.pageActionBuilder(pageContext.clearEntityKeys())
.newAction(ActionDefinition.SEB_EXAM_CONFIG_SAVE_TO_HISTORY) .newAction(ActionDefinition.SEB_EXAM_CONFIG_SAVE_TO_HISTORY)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.withExec(action -> { .withExec(action -> {
@ -169,6 +170,7 @@ public class SebExamConfigSettingsForm implements TemplateComposer {
.newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP) .newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.ignoreMoveAwayFromEdit()
.publish() .publish()
; ;

View file

@ -351,12 +351,12 @@ public enum ActionDefinition {
SEB_EXAM_CONFIG_VIEW_PROP_FROM_LIST( SEB_EXAM_CONFIG_VIEW_PROP_FROM_LIST(
new LocTextKey("sebserver.examconfig.action.list.view"), new LocTextKey("sebserver.examconfig.action.list.view"),
ImageIcon.SHOW, ImageIcon.SHOW,
PageStateDefinitionImpl.SEB_EXAM_CONFIG_VIEW, PageStateDefinitionImpl.SEB_EXAM_CONFIG_PROP_VIEW,
ActionCategory.SEB_EXAM_CONFIG_LIST), ActionCategory.SEB_EXAM_CONFIG_LIST),
SEB_EXAM_CONFIG_VIEW_PROP( SEB_EXAM_CONFIG_VIEW_PROP(
new LocTextKey("sebserver.examconfig.action.view"), new LocTextKey("sebserver.examconfig.action.view"),
ImageIcon.SHOW, ImageIcon.SHOW,
PageStateDefinitionImpl.SEB_EXAM_CONFIG_VIEW, PageStateDefinitionImpl.SEB_EXAM_CONFIG_PROP_VIEW,
ActionCategory.FORM), ActionCategory.FORM),
SEB_EXAM_CONFIG_MODIFY_PROP_FROM_LIST( SEB_EXAM_CONFIG_MODIFY_PROP_FROM_LIST(
@ -374,15 +374,20 @@ public enum ActionDefinition {
ImageIcon.EDIT_SETTINGS, ImageIcon.EDIT_SETTINGS,
PageStateDefinitionImpl.SEB_EXAM_CONFIG_EDIT, PageStateDefinitionImpl.SEB_EXAM_CONFIG_EDIT,
ActionCategory.FORM), ActionCategory.FORM),
SEB_EXAM_CONFIG_CANCEL_MODIFY( SEB_EXAM_CONFIG_VIEW(
new LocTextKey("sebserver.overall.action.modify.cancel"), new LocTextKey("sebserver.examconfig.action.view"),
ImageIcon.CANCEL, ImageIcon.EDIT_SETTINGS,
PageStateDefinitionImpl.SEB_EXAM_CONFIG_VIEW, PageStateDefinitionImpl.SEB_EXAM_CONFIG_VIEW,
ActionCategory.FORM), ActionCategory.FORM),
SEB_EXAM_CONFIG_SAVE( SEB_EXAM_CONFIG_PROP_CANCEL_MODIFY(
new LocTextKey("sebserver.overall.action.modify.cancel"),
ImageIcon.CANCEL,
PageStateDefinitionImpl.SEB_EXAM_CONFIG_PROP_VIEW,
ActionCategory.FORM),
SEB_EXAM_CONFIG_PROP_SAVE(
new LocTextKey("sebserver.examconfig.action.save"), new LocTextKey("sebserver.examconfig.action.save"),
ImageIcon.SAVE, ImageIcon.SAVE,
PageStateDefinitionImpl.SEB_EXAM_CONFIG_VIEW, PageStateDefinitionImpl.SEB_EXAM_CONFIG_PROP_VIEW,
ActionCategory.FORM), ActionCategory.FORM),
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"),

View file

@ -65,9 +65,10 @@ public enum PageStateDefinitionImpl implements PageStateDefinition {
SEB_CLIENT_CONFIG_EDIT(Type.FORM_EDIT, SebClientConfigForm.class, ActivityDefinition.SEB_CLIENT_CONFIG), SEB_CLIENT_CONFIG_EDIT(Type.FORM_EDIT, SebClientConfigForm.class, ActivityDefinition.SEB_CLIENT_CONFIG),
SEB_EXAM_CONFIG_LIST(Type.LIST_VIEW, SebExamConfigList.class, ActivityDefinition.SEB_EXAM_CONFIG), SEB_EXAM_CONFIG_LIST(Type.LIST_VIEW, SebExamConfigList.class, ActivityDefinition.SEB_EXAM_CONFIG),
SEB_EXAM_CONFIG_VIEW(Type.FORM_VIEW, SebExamConfigPropForm.class, ActivityDefinition.SEB_EXAM_CONFIG), SEB_EXAM_CONFIG_PROP_VIEW(Type.FORM_VIEW, SebExamConfigPropForm.class, ActivityDefinition.SEB_EXAM_CONFIG),
SEB_EXAM_CONFIG_PROP_EDIT(Type.FORM_EDIT, SebExamConfigPropForm.class, ActivityDefinition.SEB_EXAM_CONFIG), SEB_EXAM_CONFIG_PROP_EDIT(Type.FORM_EDIT, SebExamConfigPropForm.class, ActivityDefinition.SEB_EXAM_CONFIG),
SEB_EXAM_CONFIG_EDIT(Type.FORM_VIEW, SebExamConfigSettingsForm.class, ActivityDefinition.SEB_EXAM_CONFIG), SEB_EXAM_CONFIG_EDIT(Type.FORM_EDIT, SebExamConfigSettingsForm.class, ActivityDefinition.SEB_EXAM_CONFIG),
SEB_EXAM_CONFIG_VIEW(Type.FORM_VIEW, SebExamConfigSettingsForm.class, ActivityDefinition.SEB_EXAM_CONFIG),
SEB_EXAM_CONFIG_TEMPLATE_VIEW(Type.FORM_VIEW, ConfigTemplateForm.class, ActivityDefinition.SEB_EXAM_CONFIG), SEB_EXAM_CONFIG_TEMPLATE_VIEW(Type.FORM_VIEW, ConfigTemplateForm.class, ActivityDefinition.SEB_EXAM_CONFIG),
SEB_EXAM_CONFIG_TEMPLATE_EDIT(Type.FORM_EDIT, ConfigTemplateForm.class, ActivityDefinition.SEB_EXAM_CONFIG), SEB_EXAM_CONFIG_TEMPLATE_EDIT(Type.FORM_EDIT, ConfigTemplateForm.class, ActivityDefinition.SEB_EXAM_CONFIG),

View file

@ -17,6 +17,7 @@ import org.eclipse.swt.widgets.Text;
import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
public final class TextFieldBuilder extends FieldBuilder<String> { public final class TextFieldBuilder extends FieldBuilder<String> {
@ -61,7 +62,7 @@ public final class TextFieldBuilder extends FieldBuilder<String> {
final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true); final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true);
if (this.isArea) { if (this.isArea) {
gridData.minimumHeight = 35; gridData.minimumHeight = WidgetFactory.TEXT_AREA_INPUT_MIN_HEIGHT;
} }
textInput.setLayoutData(gridData); textInput.setLayoutData(gridData);
if (StringUtils.isNoneBlank(this.value)) { if (StringUtils.isNoneBlank(this.value)) {

View file

@ -62,6 +62,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamNames
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExams; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExams;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.institution.GetInstitutionNames; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.institution.GetInstitutionNames;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetupNames; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetupNames;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetExamConfigNodes;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetViews; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetViews;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.useraccount.GetUserAccountNames; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.useraccount.GetUserAccountNames;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
@ -551,6 +552,19 @@ public class ResourceService {
k -> k.name)); k -> k.name));
} }
public List<Tuple<String>> getExamConfigTemplateResources() {
final UserInfo userInfo = this.currentUser.get();
return this.restService.getBuilder(GetExamConfigNodes.class)
.withQueryParam(Entity.FILTER_ATTR_INSTITUTION, String.valueOf(userInfo.getInstitutionId()))
.withQueryParam(ConfigurationNode.FILTER_ATTR_TYPE, ConfigurationType.TEMPLATE.name())
.call()
.getOr(Collections.emptyList())
.stream()
.map(node -> new Tuple<>(node.getModelId(), node.name))
.sorted(RESOURCE_COMPARATOR)
.collect(Collectors.toList());
}
private Result<List<EntityName>> getExamConfigurationSelection() { private Result<List<EntityName>> getExamConfigurationSelection() {
return this.restService.getBuilder(GetExamConfigMappingNames.class) return this.restService.getBuilder(GetExamConfigMappingNames.class)
.withQueryParam( .withQueryParam(

View file

@ -54,7 +54,8 @@ public interface ExamConfigurationService {
Configuration configuration, Configuration configuration,
View view, View view,
AttributeMapping attributeMapping, AttributeMapping attributeMapping,
int rows); int rows,
boolean readonly);
Composite createViewGrid( Composite createViewGrid(
Composite parent, Composite parent,

View file

@ -104,6 +104,7 @@ public abstract class AbstractTableFieldBuilder implements InputFieldBuilder {
final TableContext tableContext) { final TableContext tableContext) {
try { try {
final boolean readonly = tableContext.getViewContext().readonly;
final int currentTableWidth = table.getClientArea().width - TABLE_WIDTH_SPACE; final int currentTableWidth = table.getClientArea().width - TABLE_WIDTH_SPACE;
final TableColumn[] columns = table.getColumns(); final TableColumn[] columns = table.getColumns();
final List<Orientation> orientations = tableContext final List<Orientation> orientations = tableContext
@ -116,7 +117,7 @@ public abstract class AbstractTableFieldBuilder implements InputFieldBuilder {
.map(o -> o.width) .map(o -> o.width)
.reduce(0, (acc, val) -> acc + val); .reduce(0, (acc, val) -> acc + val);
final int widthUnit = currentTableWidth / div; final int widthUnit = currentTableWidth / div;
for (int i = 0; i < columns.length - 2; i++) { for (int i = 0; i < columns.length - ((readonly) ? 0 : 2); i++) {
columns[i].setWidth(widthUnit * orientations.get(i).width); columns[i].setWidth(widthUnit * orientations.get(i).width);
} }
} catch (final Exception e) { } catch (final Exception e) {

View file

@ -154,7 +154,6 @@ interface CellFieldBuilderAdapter {
label.setAlignment(SWT.LEFT); label.setAlignment(SWT.LEFT);
gridData.verticalIndent = 20; gridData.verticalIndent = 20;
label.setLayoutData(gridData); label.setLayoutData(gridData);
//label.setData(RWT.CUSTOM_VARIANT, CustomVariant.TITLE_LABEL.key);
} }
@Override @Override

View file

@ -85,6 +85,9 @@ public class CheckBoxBuilder implements InputFieldBuilder {
viewContext.getOrientation(attribute.id), viewContext.getOrientation(attribute.id),
checkbox); checkbox);
if (viewContext.readonly) {
checkbox.setEnabled(false);
} else {
checkbox.addListener( checkbox.addListener(
SWT.Selection, SWT.Selection,
event -> viewContext.getValueChangeListener().valueChanged( event -> viewContext.getValueChangeListener().valueChanged(
@ -92,6 +95,7 @@ public class CheckBoxBuilder implements InputFieldBuilder {
attribute, attribute,
checkboxField.getValue(), checkboxField.getValue(),
checkboxField.listIndex)); checkboxField.listIndex));
}
return checkboxField; return checkboxField;
} }

View file

@ -201,17 +201,28 @@ public class CompositeTableFieldBuilder extends AbstractTableFieldBuilder {
rowValues, rowValues,
row); row);
new ModalInputDialog<Map<Long, TableValue>>( final ModalInputDialog<Map<Long, TableValue>> dialog = new ModalInputDialog<Map<Long, TableValue>>(
this.control.getShell(), this.control.getShell(),
this.tableContext.getWidgetFactory()) this.tableContext.getWidgetFactory())
.setDialogWidth(500) .setDialogWidth(500);
.open(
if (this.tableContext.getViewContext().readonly) {
dialog.open(
new LocTextKey(ExamConfigurationService.ATTRIBUTE_LABEL_LOC_TEXT_PREFIX + row),
rowVals -> {
},
() -> {
},
builder);
} else {
dialog.open(
new LocTextKey(ExamConfigurationService.ATTRIBUTE_LABEL_LOC_TEXT_PREFIX + row), new LocTextKey(ExamConfigurationService.ATTRIBUTE_LABEL_LOC_TEXT_PREFIX + row),
rowVals -> applyFormValues(this.values, rowVals, selectionIndex), rowVals -> applyFormValues(this.values, rowVals, selectionIndex),
() -> this.tableContext.getValueChangeListener() () -> this.tableContext.getValueChangeListener()
.tableChanged(extractTableValue(this.values)), .tableChanged(extractTableValue(this.values)),
builder); builder);
} }
}
@Override @Override
protected Map<Integer, Map<Long, TableValue>> createRowIndexMap(final List<TableValue> tableValues) { protected Map<Integer, Map<Long, TableValue>> createRowIndexMap(final List<TableValue> tableValues) {

View file

@ -177,7 +177,8 @@ public class ExamConfigurationServiceImpl implements ExamConfigurationService {
final Configuration configuration, final Configuration configuration,
final View view, final View view,
final AttributeMapping attributeMapping, final AttributeMapping attributeMapping,
final int rows) { final int rows,
final boolean readonly) {
return new ViewContext( return new ViewContext(
configuration, configuration,
@ -189,7 +190,8 @@ public class ExamConfigurationServiceImpl implements ExamConfigurationService {
this.restService, this.restService,
this.jsonMapper, this.jsonMapper,
this.valueChangeRules), this.valueChangeRules),
this.widgetFactory.getI18nSupport()); this.widgetFactory.getI18nSupport(),
readonly);
} }

View file

@ -92,11 +92,17 @@ public class InlineTableFieldBuilder implements InputFieldBuilder {
viewContext.getOrientation(attribute.id), viewContext.getOrientation(attribute.id),
gridTable); gridTable);
if (viewContext.readonly) {
gridTable.setEnabled(false);
gridTable.setListener(event -> {
});
} else {
gridTable.setListener(event -> viewContext.getValueChangeListener().valueChanged( gridTable.setListener(event -> viewContext.getValueChangeListener().valueChanged(
viewContext, viewContext,
attribute, attribute,
inlineTableInputField.getValue(), inlineTableInputField.getValue(),
inlineTableInputField.listIndex)); inlineTableInputField.listIndex));
}
return inlineTableInputField; return inlineTableInputField;

View file

@ -68,6 +68,9 @@ public class MultiCheckboxSelection extends SelectionFieldBuilder implements Inp
orientation, orientation,
selection); selection);
if (viewContext.readonly) {
selection.setEnabled(false);
} else {
selection.setSelectionListener(event -> { selection.setSelectionListener(event -> {
multiSelectionCheckboxInputField.clearError(); multiSelectionCheckboxInputField.clearError();
viewContext.getValueChangeListener().valueChanged( viewContext.getValueChangeListener().valueChanged(
@ -76,6 +79,7 @@ public class MultiCheckboxSelection extends SelectionFieldBuilder implements Inp
multiSelectionCheckboxInputField.getValue(), multiSelectionCheckboxInputField.getValue(),
multiSelectionCheckboxInputField.listIndex); multiSelectionCheckboxInputField.listIndex);
}); });
}
return multiSelectionCheckboxInputField; return multiSelectionCheckboxInputField;
} }

View file

@ -13,6 +13,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.binary.Hex;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.SWT; import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
@ -32,6 +33,8 @@ import ch.ethz.seb.sebserver.gui.form.Form;
import ch.ethz.seb.sebserver.gui.service.examconfig.InputField; import ch.ethz.seb.sebserver.gui.service.examconfig.InputField;
import ch.ethz.seb.sebserver.gui.service.examconfig.InputFieldBuilder; import ch.ethz.seb.sebserver.gui.service.examconfig.InputFieldBuilder;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant;
@Lazy @Lazy
@Component @Component
@ -67,7 +70,8 @@ public class PassworFieldBuilder implements InputFieldBuilder {
.createInnerGrid(parent, attribute, orientation); .createInnerGrid(parent, attribute, orientation);
final Text passwordInput = new Text(innerGrid, SWT.LEFT | SWT.BORDER | SWT.PASSWORD); final Text passwordInput = new Text(innerGrid, SWT.LEFT | SWT.BORDER | SWT.PASSWORD);
passwordInput.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); final GridData passwordInputLD = new GridData(SWT.FILL, SWT.FILL, true, false);
passwordInput.setLayoutData(passwordInputLD);
final Text confirmInput = new Text(innerGrid, SWT.LEFT | SWT.BORDER | SWT.PASSWORD); final Text confirmInput = new Text(innerGrid, SWT.LEFT | SWT.BORDER | SWT.PASSWORD);
final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false); final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false);
gridData.verticalIndent = 14; gridData.verticalIndent = 14;
@ -80,6 +84,14 @@ public class PassworFieldBuilder implements InputFieldBuilder {
confirmInput, confirmInput,
Form.createErrorLabel(innerGrid)); Form.createErrorLabel(innerGrid));
if (viewContext.readonly) {
passwordInput.setEditable(false);
passwordInput.setData(RWT.CUSTOM_VARIANT, CustomVariant.CONFIG_INPUT_READONLY.key);
passwordInputLD.heightHint = WidgetFactory.TEXT_INPUT_MIN_HEIGHT;
confirmInput.setEditable(false);
confirmInput.setData(RWT.CUSTOM_VARIANT, CustomVariant.CONFIG_INPUT_READONLY.key);
gridData.heightHint = WidgetFactory.TEXT_INPUT_MIN_HEIGHT;
} else {
final Listener valueChangeEventListener = event -> { final Listener valueChangeEventListener = event -> {
passwordInputField.clearError(); passwordInputField.clearError();
@ -111,6 +123,7 @@ public class PassworFieldBuilder implements InputFieldBuilder {
passwordInput.addListener(SWT.Traverse, valueChangeEventListener); passwordInput.addListener(SWT.Traverse, valueChangeEventListener);
confirmInput.addListener(SWT.FocusOut, valueChangeEventListener); confirmInput.addListener(SWT.FocusOut, valueChangeEventListener);
confirmInput.addListener(SWT.Traverse, valueChangeEventListener); confirmInput.addListener(SWT.Traverse, valueChangeEventListener);
}
return passwordInputField; return passwordInputField;
} }

View file

@ -71,6 +71,9 @@ public class RadioSelectionFieldBuilder extends SelectionFieldBuilder implements
selection, selection,
null); null);
if (viewContext.readonly) {
selection.setEnabled(false);
} else {
selection.setSelectionListener(event -> { selection.setSelectionListener(event -> {
radioSelectionInputField.clearError(); radioSelectionInputField.clearError();
viewContext.getValueChangeListener().valueChanged( viewContext.getValueChangeListener().valueChanged(
@ -79,6 +82,7 @@ public class RadioSelectionFieldBuilder extends SelectionFieldBuilder implements
radioSelectionInputField.getValue(), radioSelectionInputField.getValue(),
radioSelectionInputField.listIndex); radioSelectionInputField.listIndex);
}); });
}
return radioSelectionInputField; return radioSelectionInputField;

View file

@ -76,6 +76,9 @@ public class SingleSelectionFieldBuilder extends SelectionFieldBuilder implement
selection, selection,
Form.createErrorLabel(innerGrid)); Form.createErrorLabel(innerGrid));
if (viewContext.readonly) {
selection.setEnabled(false);
} else {
selection.setSelectionListener(event -> { selection.setSelectionListener(event -> {
singleSelectionInputField.clearError(); singleSelectionInputField.clearError();
viewContext.getValueChangeListener().valueChanged( viewContext.getValueChangeListener().valueChanged(
@ -84,6 +87,7 @@ public class SingleSelectionFieldBuilder extends SelectionFieldBuilder implement
singleSelectionInputField.getValue(), singleSelectionInputField.getValue(),
singleSelectionInputField.listIndex); singleSelectionInputField.listIndex);
}); });
}
return singleSelectionInputField; return singleSelectionInputField;
} }

View file

@ -73,6 +73,9 @@ public class SliderFieldBuilder implements InputFieldBuilder {
orientation, orientation,
slider); slider);
if (viewContext.readonly) {
slider.setEnabled(false);
} else {
final Listener valueChangeEventListener = event -> { final Listener valueChangeEventListener = event -> {
inputField.clearError(); inputField.clearError();
viewContext.getValueChangeListener().valueChanged( viewContext.getValueChangeListener().valueChanged(
@ -84,6 +87,7 @@ public class SliderFieldBuilder implements InputFieldBuilder {
slider.addListener(SWT.FocusOut, valueChangeEventListener); slider.addListener(SWT.FocusOut, valueChangeEventListener);
slider.addListener(SWT.Traverse, valueChangeEventListener); slider.addListener(SWT.Traverse, valueChangeEventListener);
}
return inputField; return inputField;
} }

View file

@ -85,6 +85,7 @@ public class TableFieldBuilder extends AbstractTableFieldBuilder {
tableContext, tableContext,
table); table);
if (!viewContext.readonly) {
TableColumn column = new TableColumn(table, SWT.NONE); TableColumn column = new TableColumn(table, SWT.NONE);
column.setImage(ImageIcon.ADD_BOX.getImage(parent.getDisplay())); column.setImage(ImageIcon.ADD_BOX.getImage(parent.getDisplay()));
@ -109,6 +110,7 @@ public class TableFieldBuilder extends AbstractTableFieldBuilder {
tableField.deleteRow(selectionIndex); tableField.deleteRow(selectionIndex);
} }
}); });
}
setSelectionListener(table, tableField); setSelectionListener(table, tableField);
return tableField; return tableField;
@ -163,8 +165,6 @@ public class TableFieldBuilder extends AbstractTableFieldBuilder {
protected void addTableRow(final int index, final Map<Long, TableValue> rowValues) { protected void addTableRow(final int index, final Map<Long, TableValue> rowValues) {
new TableItem(this.control, SWT.NONE); new TableItem(this.control, SWT.NONE);
applyTableRowValues(index); applyTableRowValues(index);
// TODO try to add delete button within table row?
} }
@Override @Override

View file

@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.gui.service.examconfig.impl;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.SWT; import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
@ -31,6 +32,8 @@ import ch.ethz.seb.sebserver.gui.service.examconfig.InputFieldBuilder;
import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService; import ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant;
@Lazy @Lazy
@Component @Component
@ -74,7 +77,7 @@ public class TextFieldBuilder implements InputFieldBuilder {
} }
case TEXT_AREA: { case TEXT_AREA: {
text = new Text(innerGrid, SWT.LEFT | SWT.BORDER | SWT.MULTI); text = new Text(innerGrid, SWT.LEFT | SWT.BORDER | SWT.MULTI);
gridData.heightHint = 50; gridData.minimumHeight = WidgetFactory.TEXT_AREA_INPUT_MIN_HEIGHT;
break; break;
} }
default: { default: {
@ -101,6 +104,13 @@ public class TextFieldBuilder implements InputFieldBuilder {
text, text,
Form.createErrorLabel(innerGrid)); Form.createErrorLabel(innerGrid));
if (viewContext.readonly) {
text.setEditable(false);
text.setData(RWT.CUSTOM_VARIANT, CustomVariant.CONFIG_INPUT_READONLY.key);
gridData.heightHint = (attribute.type == AttributeType.TEXT_AREA)
? WidgetFactory.TEXT_AREA_INPUT_MIN_HEIGHT
: WidgetFactory.TEXT_INPUT_MIN_HEIGHT;
} else {
final Listener valueChangeEventListener = event -> { final Listener valueChangeEventListener = event -> {
textInputField.clearError(); textInputField.clearError();
viewContext.getValueChangeListener().valueChanged( viewContext.getValueChangeListener().valueChanged(
@ -112,6 +122,7 @@ public class TextFieldBuilder implements InputFieldBuilder {
text.addListener(SWT.FocusOut, valueChangeEventListener); text.addListener(SWT.FocusOut, valueChangeEventListener);
text.addListener(SWT.Traverse, valueChangeEventListener); text.addListener(SWT.Traverse, valueChangeEventListener);
}
return textInputField; return textInputField;
} }

View file

@ -37,6 +37,7 @@ public final class ViewContext {
final Map<Long, InputField> inputFieldMapping; final Map<Long, InputField> inputFieldMapping;
final ValueChangeListener valueChangeListener; final ValueChangeListener valueChangeListener;
final I18nSupport i18nSupport; final I18nSupport i18nSupport;
final boolean readonly;
ViewContext( ViewContext(
final Configuration configuration, final Configuration configuration,
@ -44,7 +45,8 @@ public final class ViewContext {
final int rows, final int rows,
final AttributeMapping attributeContext, final AttributeMapping attributeContext,
final ValueChangeListener valueChangeListener, final ValueChangeListener valueChangeListener,
final I18nSupport i18nSupport) { final I18nSupport i18nSupport,
final boolean readonly) {
Objects.requireNonNull(configuration); Objects.requireNonNull(configuration);
Objects.requireNonNull(view); Objects.requireNonNull(view);
@ -59,6 +61,7 @@ public final class ViewContext {
this.inputFieldMapping = new HashMap<>(); this.inputFieldMapping = new HashMap<>();
this.valueChangeListener = valueChangeListener; this.valueChangeListener = valueChangeListener;
this.i18nSupport = i18nSupport; this.i18nSupport = i18nSupport;
this.readonly = readonly;
} }
public I18nSupport getI18nSupport() { public I18nSupport getI18nSupport() {

View file

@ -49,7 +49,6 @@ public class ModalInputDialog<T> extends Dialog {
super(parent, SWT.BORDER | SWT.TITLE | SWT.APPLICATION_MODAL | SWT.CLOSE); super(parent, SWT.BORDER | SWT.TITLE | SWT.APPLICATION_MODAL | SWT.CLOSE);
this.widgetFactory = widgetFactory; this.widgetFactory = widgetFactory;
} }
public ModalInputDialog<T> setDialogWidth(final int dialogWidth) { public ModalInputDialog<T> setDialogWidth(final int dialogWidth) {
@ -161,7 +160,6 @@ public class ModalInputDialog<T> extends Dialog {
shell.pack(); shell.pack();
final Rectangle bounds = shell.getBounds(); final Rectangle bounds = shell.getBounds();
final Rectangle bounds2 = super.getParent().getDisplay().getBounds(); final Rectangle bounds2 = super.getParent().getDisplay().getBounds();
//bounds.width = bounds.width;
bounds.x = (bounds2.width - bounds.width) / 2; bounds.x = (bounds2.width - bounds.width) / 2;
bounds.y = (bounds2.height - bounds.height) / 2; bounds.y = (bounds2.height - bounds.height) / 2;
shell.setBounds(bounds); shell.setBounds(bounds);

View file

@ -0,0 +1,42 @@
/*
* 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.seb.examconfig;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class CopyConfiguration extends RestCall<ConfigurationNode> {
public CopyConfiguration() {
super(new TypeKey<>(
CallType.SAVE,
EntityType.CONFIGURATION_NODE,
new TypeReference<ConfigurationNode>() {
}),
HttpMethod.POST,
MediaType.APPLICATION_FORM_URLENCODED,
API.CONFIGURATION_NODE_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.CONFIGURATION_COPY_PATH_SEGMENT);
}
}

View file

@ -0,0 +1,38 @@
/*
* 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.seb.examconfig;
import java.util.List;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.PageToListCallAdapter;
@Lazy
@Component
@GuiProfile
public class GetExamConfigNodes extends PageToListCallAdapter<ConfigurationNode> {
public GetExamConfigNodes() {
super(
GetExamConfigNodePage.class,
EntityType.CONFIGURATION_NODE,
new TypeReference<List<ConfigurationNode>>() {
},
API.CONFIGURATION_NODE_ENDPOINT);
}
}

View file

@ -18,6 +18,7 @@ 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.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.SWT; import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.layout.GridLayout;
@ -34,6 +35,7 @@ import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.AttributeType; import ch.ethz.seb.sebserver.gbl.model.sebconfig.AttributeType;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.PageService; import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.ImageIcon; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.ImageIcon;
public class GridTable extends Composite { public class GridTable extends Composite {
@ -394,6 +396,7 @@ public class GridTable extends Composite {
TextField(final Composite parent, final ColumnDef columnDef, final Listener listener) { TextField(final Composite parent, final ColumnDef columnDef, final Listener listener) {
this._textField = new Text(parent, SWT.LEFT | SWT.BORDER); this._textField = new Text(parent, SWT.LEFT | SWT.BORDER);
this._textField.setData(RWT.CUSTOM_VARIANT, CustomVariant.CONFIG_INPUT_READONLY.key);
this._textField.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); this._textField.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
this.columnDef = columnDef; this.columnDef = columnDef;
this._textField.addListener(SWT.FocusOut, listener); this._textField.addListener(SWT.FocusOut, listener);

View file

@ -58,6 +58,9 @@ public class WidgetFactory {
private static final Logger log = LoggerFactory.getLogger(WidgetFactory.class); private static final Logger log = LoggerFactory.getLogger(WidgetFactory.class);
public static final int TEXT_AREA_INPUT_MIN_HEIGHT = 50;
public static final int TEXT_INPUT_MIN_HEIGHT = 24;
public enum ImageIcon { public enum ImageIcon {
MAXIMIZE("maximize.png"), MAXIMIZE("maximize.png"),
MINIMIZE("minimize.png"), MINIMIZE("minimize.png"),
@ -127,7 +130,8 @@ public class WidgetFactory {
TITLE_LABEL("head"), TITLE_LABEL("head"),
MESSAGE("message"), MESSAGE("message"),
ERROR("error") ERROR("error"),
CONFIG_INPUT_READONLY("inputreadonly")
; ;
@ -327,7 +331,7 @@ public class WidgetFactory {
public Text textInput(final Composite content, final boolean password, final boolean readonly) { public Text textInput(final Composite content, final boolean password, final boolean readonly) {
return readonly return readonly
? new Text(content, SWT.LEFT | SWT.BORDER) ? new Text(content, SWT.LEFT)
: new Text(content, (password) : new Text(content, (password)
? SWT.LEFT | SWT.BORDER | SWT.PASSWORD ? SWT.LEFT | SWT.BORDER | SWT.PASSWORD
: SWT.LEFT | SWT.BORDER); : SWT.LEFT | SWT.BORDER);

View file

@ -62,4 +62,10 @@ public interface ConfigurationDAO extends EntityDAO<Configuration, Configuration
* @return the current follow-up configuration */ * @return the current follow-up configuration */
Result<Configuration> getFollowupConfiguration(Long configNodeId); Result<Configuration> getFollowupConfiguration(Long configNodeId);
/** Use this to get the last version of a configuration that is not the follow-up.
*
* @param configNodeId ConfigurationNode identifier to get the last version of configuration from
* @return the last version of configuration */
Result<Configuration> getConfigurationLastStableVersion(Long configNodeId);
} }

View file

@ -9,10 +9,17 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.dao; package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
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.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO;
public interface ConfigurationNodeDAO extends public interface ConfigurationNodeDAO extends
EntityDAO<ConfigurationNode, ConfigurationNode>, EntityDAO<ConfigurationNode, ConfigurationNode>,
BulkActionSupportDAO<ConfigurationNode> { BulkActionSupportDAO<ConfigurationNode> {
Result<ConfigurationNode> createCopy(
Long institutionId,
String newOwner,
Long configurationNodeId,
boolean withHistory);
} }

View file

@ -49,10 +49,16 @@ public interface ExamConfigurationMapDAO extends
* the Exam for a specified user identifier */ * the Exam for a specified user identifier */
Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId); Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId);
/** Get all id of Exams that has a relation to the given configuration id.
*
* @param configurationNodeId the configuration node identifier (PK)
* @return Result referencing the List of exam identifiers (PK) for a given configuration node identifier */
Result<Collection<Long>> getExamIdsForConfigNodeId(Long configurationNodeId);
/** Get all id of Exams that has a relation to the given configuration id. /** Get all id of Exams that has a relation to the given configuration id.
* *
* @param configurationId * @param configurationId
* @return */ * @return Result referencing the List of exam identifiers (PK) for a given configuration identifier */
Result<Collection<Long>> getExamIdsForConfigId(Long configurationId); Result<Collection<Long>> getExamIdsForConfigId(Long configurationId);
} }

View file

@ -66,6 +66,8 @@ class ConfigurationDAOBatchService {
private static final Logger log = LoggerFactory.getLogger(ConfigurationDAOBatchService.class); private static final Logger log = LoggerFactory.getLogger(ConfigurationDAOBatchService.class);
public static final String INITIAL_VERSION_NAME = "v0";
private final ConfigurationNodeRecordMapper batchConfigurationNodeRecordMapper; private final ConfigurationNodeRecordMapper batchConfigurationNodeRecordMapper;
private final ConfigurationValueRecordMapper batchConfigurationValueRecordMapper; private final ConfigurationValueRecordMapper batchConfigurationValueRecordMapper;
private final ConfigurationAttributeRecordMapper batchConfigurationAttributeRecordMapper; private final ConfigurationAttributeRecordMapper batchConfigurationAttributeRecordMapper;
@ -187,23 +189,13 @@ class ConfigurationDAOBatchService {
.execute(); .execute();
// get current versions count // get current versions count
final Long versions = this.batchConfigurationRecordMapper
.countByExample()
.where(
ConfigurationRecordDynamicSqlSupport.configurationNodeId,
isEqualTo(configurationNodeId))
.and(
ConfigurationRecordDynamicSqlSupport.followup,
isNotEqualTo(BooleanUtils.toInteger(true)))
.build()
.execute();
// close follow-up configuration to save in history // close follow-up configuration to save in history
final ConfigurationRecord configUpdate = new ConfigurationRecord( final ConfigurationRecord configUpdate = new ConfigurationRecord(
followupConfig.getId(), followupConfig.getId(),
null, null,
null, null,
"v" + versions, generateVersionName(configurationNodeId),
DateTime.now(DateTimeZone.UTC), DateTime.now(DateTimeZone.UTC),
BooleanUtils.toInteger(false)); BooleanUtils.toInteger(false));
this.batchConfigurationRecordMapper.updateByPrimaryKeySelective(configUpdate); this.batchConfigurationRecordMapper.updateByPrimaryKeySelective(configUpdate);
@ -240,6 +232,20 @@ class ConfigurationDAOBatchService {
.flatMap(ConfigurationDAOImpl::toDomainModel); .flatMap(ConfigurationDAOImpl::toDomainModel);
} }
private String generateVersionName(final Long configurationNodeId) {
final Long versions = this.batchConfigurationRecordMapper
.countByExample()
.where(
ConfigurationRecordDynamicSqlSupport.configurationNodeId,
isEqualTo(configurationNodeId))
.and(
ConfigurationRecordDynamicSqlSupport.followup,
isNotEqualTo(BooleanUtils.toInteger(true)))
.build()
.execute();
return "v" + versions;
}
Result<Configuration> undo(final Long configurationNodeId) { Result<Configuration> undo(final Long configurationNodeId) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
// get all configurations of the node // get all configurations of the node
@ -311,6 +317,69 @@ class ConfigurationDAOBatchService {
.flatMap(ConfigurationDAOImpl::toDomainModel); .flatMap(ConfigurationDAOImpl::toDomainModel);
} }
Result<Configuration> copyConfiguration(
final Long institutionId,
final Long fromConfigurationId,
final Long toConfigurationNodeId) {
return Result.tryCatch(() -> {
final ConfigurationRecord fromRecord = this.batchConfigurationRecordMapper
.selectByPrimaryKey(fromConfigurationId);
if (!fromRecord.getInstitutionId().equals(institutionId)) {
throw new IllegalArgumentException("Institution integrity violation");
}
final ConfigurationRecord configurationRecord = new ConfigurationRecord(
null,
fromRecord.getInstitutionId(),
toConfigurationNodeId,
fromRecord.getVersion(),
fromRecord.getVersionDate(),
fromRecord.getFollowup());
this.batchConfigurationRecordMapper.insert(configurationRecord);
return configurationRecord;
})
.flatMap(ConfigurationDAOImpl::toDomainModel)
.map(newConfig -> {
this.copyValues(
institutionId,
fromConfigurationId,
newConfig.getId());
return newConfig;
})
.map(config -> {
this.batchSqlSessionTemplate.flushStatements();
return config;
});
}
void copyValues(
final Long institutionId,
final Long fromConfigId,
final Long toConfigId) {
this.batchConfigurationValueRecordMapper
.selectByExample()
.where(
ConfigurationValueRecordDynamicSqlSupport.institutionId,
isEqualTo(institutionId))
.and(
ConfigurationValueRecordDynamicSqlSupport.configurationId,
isEqualTo(fromConfigId))
.build()
.execute()
.stream()
.map(fromRec -> new ConfigurationValueRecord(
null,
fromRec.getInstitutionId(),
toConfigId,
fromRec.getConfigurationAttributeId(),
fromRec.getListIndex(),
fromRec.getValue()))
.forEach(this.batchConfigurationValueRecordMapper::insert);
}
private ConfigurationRecord getFollowupConfigurationRecord(final Long configurationNodeId) { private ConfigurationRecord getFollowupConfigurationRecord(final Long configurationNodeId) {
return this.batchConfigurationRecordMapper return this.batchConfigurationRecordMapper
.selectByExample() .selectByExample()
@ -454,7 +523,7 @@ class ConfigurationDAOBatchService {
null, null,
config.institutionId, config.institutionId,
config.id, config.id,
"v0", // TODO? INITIAL_VERSION_NAME,
DateTime.now(DateTimeZone.UTC), DateTime.now(DateTimeZone.UTC),
BooleanUtils.toInteger(false)); BooleanUtils.toInteger(false));
@ -547,7 +616,7 @@ class ConfigurationDAOBatchService {
} }
final Long configurationId = this.batchConfigurationRecordMapper.selectByExample() final Long configurationId = this.batchConfigurationRecordMapper.selectByExample()
.where(ConfigurationRecordDynamicSqlSupport.configurationNodeId, isEqualTo(configNode.id)) .where(ConfigurationRecordDynamicSqlSupport.configurationNodeId, isEqualTo(configNode.templateId))
.and(ConfigurationRecordDynamicSqlSupport.followup, isEqualTo(BooleanUtils.toIntegerObject(true))) .and(ConfigurationRecordDynamicSqlSupport.followup, isEqualTo(BooleanUtils.toIntegerObject(true)))
.build() .build()
.execute() .execute()
@ -555,14 +624,15 @@ class ConfigurationDAOBatchService {
.collect(Utils.toSingleton()) .collect(Utils.toSingleton())
.getId(); .getId();
return this.batchConfigurationValueRecordMapper.selectByExample() final List<ConfigurationValueRecord> values = this.batchConfigurationValueRecordMapper.selectByExample()
.where(ConfigurationValueRecordDynamicSqlSupport.configurationId, isEqualTo(configurationId)) .where(ConfigurationValueRecordDynamicSqlSupport.configurationId, isEqualTo(configurationId))
.build() .build()
.execute() .execute();
.stream()
return values.stream()
.collect(Collectors.toMap( .collect(Collectors.toMap(
valRec -> valRec.getConfigurationAttributeId(), valRec -> valRec.getConfigurationAttributeId(),
valRec -> valRec.getValue())); valRec -> (valRec.getValue() != null) ? valRec.getValue() : ""));
} }
} }

View file

@ -12,6 +12,8 @@ import static org.mybatis.dynamic.sql.SqlBuilder.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -131,6 +133,27 @@ public class ConfigurationDAOImpl implements ConfigurationDAO {
} }
@Override
@Transactional(readOnly = true)
public Result<Configuration> getConfigurationLastStableVersion(final Long configNodeId) {
return Result.tryCatch(() -> {
final List<ConfigurationRecord> configs = this.configurationRecordMapper.selectByExample()
.where(
ConfigurationRecordDynamicSqlSupport.configurationNodeId,
isEqualTo(configNodeId))
.and(
ConfigurationRecordDynamicSqlSupport.followup,
isEqualTo(BooleanUtils.toInteger(false)))
.build()
.execute();
Collections.sort(
configs,
(c1, c2) -> c1.getVersionDate().compareTo(c2.getVersionDate()));
final ConfigurationRecord configurationRecord = configs.get(0);
return configurationRecord;
}).flatMap(ConfigurationDAOImpl::toDomainModel);
}
@Override @Override
@Transactional @Transactional
public Result<Configuration> save(final Configuration data) { public Result<Configuration> save(final Configuration data) {

View file

@ -19,7 +19,12 @@ 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;
@ -40,6 +45,7 @@ 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;
@ -56,18 +62,24 @@ 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
@ -191,6 +203,84 @@ public class ConfigurationNodeDAOImpl implements ConfigurationNodeDAO {
.onError(TransactionHandler::rollback); .onError(TransactionHandler::rollback);
} }
@Override
@Transactional
public Result<ConfigurationNode> createCopy(
final Long institutionId,
final String newOwner,
final Long configurationNodeId,
final boolean withHistory) {
return this.recordById(configurationNodeId)
.flatMap(nodeRec -> (nodeRec.getInstitutionId().equals(institutionId)
? Result.of(nodeRec)
: Result.ofError(new IllegalArgumentException("Institution integrity violation"))))
.map(nodeRec -> this.copyNodeRecord(nodeRec, newOwner, withHistory))
.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 @Override
@Transactional @Transactional
public Result<Collection<EntityKey>> delete(final Set<EntityKey> all) { public Result<Collection<EntityKey>> delete(final Set<EntityKey> all) {

View file

@ -288,11 +288,27 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
return getDependencies(bulkAction, selectionFunction); return getDependencies(bulkAction, selectionFunction);
} }
@Override
@Transactional(readOnly = true)
public Result<Collection<Long>> getExamIdsForConfigNodeId(final Long configurationNodeId) {
return Result.tryCatch(() -> {
return this.examConfigurationMapRecordMapper.selectByExample()
.where(
ExamConfigurationMapRecordDynamicSqlSupport.configurationNodeId,
isEqualTo(configurationNodeId))
.build()
.execute()
.stream()
.map(record -> record.getExamId())
.collect(Collectors.toList());
});
}
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Collection<Long>> getExamIdsForConfigId(final Long configurationId) { public Result<Collection<Long>> getExamIdsForConfigId(final Long configurationId) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final Long configNodeId = this.configurationNodeRecordMapper.selectIdsByExample() return this.configurationNodeRecordMapper.selectIdsByExample()
.leftJoin(ConfigurationRecordDynamicSqlSupport.configurationRecord) .leftJoin(ConfigurationRecordDynamicSqlSupport.configurationRecord)
.on( .on(
ConfigurationRecordDynamicSqlSupport.configurationNodeId, ConfigurationRecordDynamicSqlSupport.configurationNodeId,
@ -304,17 +320,8 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
.execute() .execute()
.stream() .stream()
.collect(Utils.toSingleton()); .collect(Utils.toSingleton());
})
return this.examConfigurationMapRecordMapper.selectByExample() .flatMap(this::getExamIdsForConfigNodeId);
.where(
ExamConfigurationMapRecordDynamicSqlSupport.configurationNodeId,
isEqualTo(configNodeId))
.build()
.execute()
.stream()
.map(record -> record.getExamId())
.collect(Collectors.toList());
});
} }
private Result<ExamConfigurationMapRecord> recordById(final Long id) { private Result<ExamConfigurationMapRecord> recordById(final Long id) {

View file

@ -111,7 +111,7 @@ public class ExamConfigIO {
// get follow-up configurationId for given configurationNodeId // get follow-up configurationId for given configurationNodeId
final Long configurationId = this.configurationDAO final Long configurationId = this.configurationDAO
.getFollowupConfiguration(configurationNodeId) .getConfigurationLastStableVersion(configurationNodeId)
.getOrThrow().id; .getOrThrow().id;
final Function<ConfigurationAttribute, ConfigurationValue> configurationValueSupplier = final Function<ConfigurationAttribute, ConfigurationValue> configurationValueSupplier =

View file

@ -19,19 +19,25 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; 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;
import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType; import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; 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.Configuration;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; 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.bulkaction.BulkActionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationChangedEvent; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationChangedEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService; import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
@WebServiceProfile @WebServiceProfile
@ -40,7 +46,9 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe
public class ConfigurationController extends ReadonlyEntityController<Configuration, Configuration> { public class ConfigurationController extends ReadonlyEntityController<Configuration, Configuration> {
private final ConfigurationDAO configurationDAO; private final ConfigurationDAO configurationDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final ApplicationEventPublisher applicationEventPublisher; private final ApplicationEventPublisher applicationEventPublisher;
private final ExamSessionService examSessionService;
protected ConfigurationController( protected ConfigurationController(
final AuthorizationService authorization, final AuthorizationService authorization,
@ -49,7 +57,9 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
final UserActivityLogDAO userActivityLogDAO, final UserActivityLogDAO userActivityLogDAO,
final PaginationService paginationService, final PaginationService paginationService,
final BeanValidationService beanValidationService, final BeanValidationService beanValidationService,
final ApplicationEventPublisher applicationEventPublisher) { final ApplicationEventPublisher applicationEventPublisher,
final ExamSessionService examSessionService,
final ExamConfigurationMapDAO examConfigurationMapDAO) {
super(authorization, super(authorization,
bulkActionService, bulkActionService,
@ -60,6 +70,8 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
this.configurationDAO = entityDAO; this.configurationDAO = entityDAO;
this.applicationEventPublisher = applicationEventPublisher; this.applicationEventPublisher = applicationEventPublisher;
this.examSessionService = examSessionService;
this.examConfigurationMapDAO = examConfigurationMapDAO;
} }
@RequestMapping( @RequestMapping(
@ -71,6 +83,7 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
return this.entityDAO.byModelId(modelId) return this.entityDAO.byModelId(modelId)
.flatMap(this.authorization::checkModify) .flatMap(this.authorization::checkModify)
.flatMap(this::checkRunningExamIntegrity)
.flatMap(config -> this.configurationDAO.saveToHistory(config.configurationNodeId)) .flatMap(config -> this.configurationDAO.saveToHistory(config.configurationNodeId))
.flatMap(this.userActivityLogDAO::logSaveToHistory) .flatMap(this.userActivityLogDAO::logSaveToHistory)
.flatMap(this::publishConfigChanged) .flatMap(this::publishConfigChanged)
@ -118,9 +131,53 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
return ConfigurationRecordDynamicSqlSupport.configurationRecord; return ConfigurationRecordDynamicSqlSupport.configurationRecord;
} }
// NOTE: This will not properly work within a distributed setup since other instances
// are not notified about the configuration change
// TODO: find a way to manage the notification of a changed configuration that works
// also on distributed environments. For example use the database to store configuration
// changed information and check before getting a configuration from cache if it is still valid
private Result<Configuration> publishConfigChanged(final Configuration config) { private Result<Configuration> publishConfigChanged(final Configuration config) {
this.applicationEventPublisher.publishEvent(new ConfigurationChangedEvent(config.id)); this.applicationEventPublisher.publishEvent(new ConfigurationChangedEvent(config.id));
return Result.of(config); return Result.of(config);
} }
private Result<Configuration> checkRunningExamIntegrity(final Configuration config) {
// check if the configuration is attached to an exam
final long activeConnections = this.examConfigurationMapDAO
.getExamIdsForConfigNodeId(config.configurationNodeId)
.getOrThrow()
.stream()
.flatMap(examId -> {
return this.examSessionService
.getConnectionData(examId)
.getOrThrow()
.stream();
})
.filter(this::isActiveConnection)
.count();
if (activeConnections > 0) {
throw new IllegalStateException("Integrity violation: There are currently active SEB Client connection.");
} else {
return Result.of(config);
}
}
private boolean isActiveConnection(final ClientConnectionData connection) {
if (connection.clientConnection.status == ConnectionStatus.ESTABLISHED
|| connection.clientConnection.status == ConnectionStatus.AUTHENTICATED) {
return true;
}
if (connection.clientConnection.status == ConnectionStatus.CONNECTION_REQUESTED) {
final Long creationTime = connection.clientConnection.getCreationTime();
final long millisecondsNow = Utils.getMillisecondsNow();
if (millisecondsNow - creationTime < 30 * Constants.SECOND_IN_MILLIS) {
return true;
}
}
return false;
}
} }

View file

@ -19,6 +19,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.mybatis.dynamic.sql.SqlTable; import org.mybatis.dynamic.sql.SqlTable;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -71,11 +72,11 @@ public class ConfigurationNodeController extends EntityController<ConfigurationN
private static final Logger log = LoggerFactory.getLogger(ConfigurationNodeController.class); private static final Logger log = LoggerFactory.getLogger(ConfigurationNodeController.class);
private final ConfigurationNodeDAO configurationNodeDAO;
private final ConfigurationDAO configurationDAO; private final ConfigurationDAO configurationDAO;
private final ViewDAO viewDAO; private final ViewDAO viewDAO;
private final OrientationDAO orientationDAO; private final OrientationDAO orientationDAO;
private final SebExamConfigService sebExamConfigService; private final SebExamConfigService sebExamConfigService;
private final SebExamConfigTemplateService sebExamConfigTemplateService; private final SebExamConfigTemplateService sebExamConfigTemplateService;
protected ConfigurationNodeController( protected ConfigurationNodeController(
@ -99,6 +100,7 @@ public class ConfigurationNodeController extends EntityController<ConfigurationN
beanValidationService); beanValidationService);
this.configurationDAO = configurationDAO; this.configurationDAO = configurationDAO;
this.configurationNodeDAO = entityDAO;
this.viewDAO = viewDAO; this.viewDAO = viewDAO;
this.orientationDAO = orientationDAO; this.orientationDAO = orientationDAO;
this.sebExamConfigService = sebExamConfigService; this.sebExamConfigService = sebExamConfigService;
@ -135,6 +137,35 @@ public class ConfigurationNodeController extends EntityController<ConfigurationN
.getOrThrow(); .getOrThrow();
} }
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT + API.CONFIGURATION_COPY_PATH_SEGMENT,
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_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) {
this.entityDAO.byPK(modelId)
.flatMap(this.authorization::checkWrite);
final SEBServerUser currentUser = this.authorization
.getUserService()
.getCurrentUser();
return this.configurationNodeDAO.createCopy(
institutionId,
currentUser.getUserInfo().uuid,
modelId,
BooleanUtils.toBoolean(withHistory))
.getOrThrow();
}
@RequestMapping( @RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT + API.CONFIGURATION_CONFIG_KEY_PATH_SEGMENT, path = API.MODEL_ID_VAR_PATH_SEGMENT + API.CONFIGURATION_CONFIG_KEY_PATH_SEGMENT,
method = RequestMethod.GET, method = RequestMethod.GET,

View file

@ -13,7 +13,6 @@ import java.util.Objects;
import javax.validation.Valid; import javax.validation.Valid;
import org.mybatis.dynamic.sql.SqlTable; import org.mybatis.dynamic.sql.SqlTable;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@ -36,7 +35,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionServic
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationValueDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationValueDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationChangedEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebExamConfigService; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebExamConfigService;
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService; import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
@ -48,7 +46,6 @@ public class ConfigurationValueController extends EntityController<Configuration
private final ConfigurationDAO configurationDAO; private final ConfigurationDAO configurationDAO;
private final ConfigurationValueDAO configurationValueDAO; private final ConfigurationValueDAO configurationValueDAO;
private final SebExamConfigService sebExamConfigService; private final SebExamConfigService sebExamConfigService;
private final ApplicationEventPublisher applicationEventPublisher;
protected ConfigurationValueController( protected ConfigurationValueController(
final AuthorizationService authorization, final AuthorizationService authorization,
@ -58,8 +55,7 @@ public class ConfigurationValueController extends EntityController<Configuration
final PaginationService paginationService, final PaginationService paginationService,
final BeanValidationService beanValidationService, final BeanValidationService beanValidationService,
final ConfigurationDAO configurationDAO, final ConfigurationDAO configurationDAO,
final SebExamConfigService sebExamConfigService, final SebExamConfigService sebExamConfigService) {
final ApplicationEventPublisher applicationEventPublisher) {
super(authorization, super(authorization,
bulkActionService, bulkActionService,
@ -71,19 +67,6 @@ public class ConfigurationValueController extends EntityController<Configuration
this.configurationDAO = configurationDAO; this.configurationDAO = configurationDAO;
this.configurationValueDAO = entityDAO; this.configurationValueDAO = entityDAO;
this.sebExamConfigService = sebExamConfigService; this.sebExamConfigService = sebExamConfigService;
this.applicationEventPublisher = applicationEventPublisher;
}
@Override
protected Result<ConfigurationValue> notifySaved(final ConfigurationValue entity) {
if (entity == null) {
return super.notifySaved(entity);
}
this.applicationEventPublisher.publishEvent(
new ConfigurationChangedEvent(entity.configurationId));
return super.notifySaved(entity);
} }
@Override @Override
@ -141,11 +124,6 @@ public class ConfigurationValueController extends EntityController<Configuration
return this.configurationDAO.byPK(tableValue.configurationId) return this.configurationDAO.byPK(tableValue.configurationId)
.flatMap(this.authorization::checkModify) .flatMap(this.authorization::checkModify)
.flatMap(config -> this.configurationValueDAO.saveTableValues(tableValue)) .flatMap(config -> this.configurationValueDAO.saveTableValues(tableValue))
.map(config -> {
this.applicationEventPublisher.publishEvent(
new ConfigurationChangedEvent(config.configurationId));
return config;
})
.getOrThrow(); .getOrThrow();
} }

View file

@ -34,6 +34,8 @@ 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

@ -435,12 +435,13 @@ sebserver.examconfig.list.action.no.modify.privilege=No Access: An Exam Configur
sebserver.examconfig.action.list.new=Add Exam Configuration sebserver.examconfig.action.list.new=Add Exam Configuration
sebserver.examconfig.action.list.view=View Configuration sebserver.examconfig.action.list.view=View Configuration
sebserver.examconfig.action.list.modify=Edit Settings sebserver.examconfig.action.list.modify=Edit Settings
sebserver.examconfig.action.list.modify=View Settings
sebserver.examconfig.action.list.modify.properties=Edit Configuration sebserver.examconfig.action.list.modify.properties=Edit Configuration
sebserver.examconfig.action.view=View Configuration sebserver.examconfig.action.view=View Configuration
sebserver.examconfig.action.modify=Edit Settings sebserver.examconfig.action.modify=Edit Settings
sebserver.examconfig.action.modify.properties=Edit Configuration sebserver.examconfig.action.modify.properties=Edit Configuration
sebserver.examconfig.action.save=Save sebserver.examconfig.action.save=Save
sebserver.examconfig.action.saveToHistory=Save In History 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
@ -455,6 +456,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.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
@ -935,13 +937,13 @@ sebserver.configtemplate.list.actions=Selected Template
sebserver.configtemplate.info.pleaseSelect=Please select an exam configuration template first sebserver.configtemplate.info.pleaseSelect=Please select an exam configuration template first
sebserver.exam.configtemplate.action.list.new=New Template sebserver.exam.configtemplate.action.list.new=Add Template
sebserver.configtemplate.action.list.view=View Template sebserver.configtemplate.action.list.view=View Template
sebserver.configtemplate.action.view=View Template sebserver.configtemplate.action.view=View Template
sebserver.configtemplate.action.list.modify=Edit Template sebserver.configtemplate.action.list.modify=Edit Template
sebserver.configtemplate.action.modify=Edit Template sebserver.configtemplate.action.modify=Edit Template
sebserver.configtemplate.form.title.new=New Template sebserver.configtemplate.form.title.new=Add Template
sebserver.configtemplate.form.title=Configuration Template sebserver.configtemplate.form.title=Configuration Template
sebserver.configtemplate.form.name=Name sebserver.configtemplate.form.name=Name
sebserver.configtemplate.form.description=Description sebserver.configtemplate.form.description=Description

View file

@ -244,17 +244,32 @@ Text[BORDER].error, Text[MULTI][BORDER].error {
Text[BORDER]:focused, Text[MULTI][BORDER]:focused { Text[BORDER]:focused, Text[MULTI][BORDER]:focused {
border: 1px solid #4f7cb1; border: 1px solid #4f7cb1;
border-radius: 0;
box-shadow: none;
}
Text[BORDER]:disabled.inputreadonly,
Text[BORDER]:read-only.inputreadonly,
Text[MULTI][BORDER]:disabled.inputreadonly,
Text[MULTI][BORDER]:read-only.inputreadonly {
font: 12px Arial, Helvetica, sans-serif;
border: 1px solid #aaaaaa;
border-radius: 0;
padding: 3px 10px 3px 10px;
color: #aaaaaa;
background-repeat: repeat;
background-position: left top;
background-color: #ffffff;
background-image: none;
text-shadow: none;
box-shadow: none; box-shadow: none;
} }
Text:disabled, Text:disabled,
Text:read-only, Text:read-only,
Text[BORDER]:disabled,
Text[BORDER]:read-only,
Text[MULTI]:disabled, Text[MULTI]:disabled,
Text[MULTI]:read-only, Text[MULTI]:read-only {
Text[MULTI][BORDER]:disabled,
Text[MULTI][BORDER]:read-only {
box-shadow: none; box-shadow: none;
background-color: #ffffff; background-color: #ffffff;
border: none; border: none;
@ -460,6 +475,14 @@ Button[PUSH]:hover.header {
cursor: pointer; cursor: pointer;
} }
Button[CHECK]:disabled {
color: #4a4a4a;
}
Button[RADIO]:disabled {
color: #4a4a4a;
}
FileUpload, FileUpload,
FileUpload:default, FileUpload:default,
FileUpload:hover, FileUpload:hover,

View file

@ -247,13 +247,18 @@ Text[BORDER]:focused, Text[MULTI][BORDER]:focused {
box-shadow: none; box-shadow: none;
} }
Text[BORDER]:disabled,
Text[MULTI][BORDER]:disabled {
border: 1px solid #4a4a4a;
border-radius: 0;
box-shadow: none;
}
Text:disabled, Text:disabled,
Text:read-only, Text:read-only,
Text[BORDER]:disabled,
Text[BORDER]:read-only,
Text[MULTI]:disabled, Text[MULTI]:disabled,
Text[BORDER]:read-only,
Text[MULTI]:read-only, Text[MULTI]:read-only,
Text[MULTI][BORDER]:disabled,
Text[MULTI][BORDER]:read-only { Text[MULTI][BORDER]:read-only {
box-shadow: none; box-shadow: none;
background-color: #ffffff; background-color: #ffffff;