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_SAVE_TO_HISTORY_PATH_SEGMENT = "/save_to_history";
public static final String CONFIGURATION_UNDO_PATH_SEGMENT = "/undo";
public static final String CONFIGURATION_COPY_PATH_SEGMENT = "/copy";
public static final String CONFIGURATION_RESTORE_FROM_HISTORY_PATH_SEGMENT = "/restore";
public static final String CONFIGURATION_VALUE_ENDPOINT = "/configuration_value";
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 static final Long DEFAULT_TEMPLATE_ID = 0L;
public static final String ATTR_COPY_WITH_HISTORY = "with-history";
public static final String FILTER_ATTR_TEMPLATE_ID = "templateId";
public static final String FILTER_ATTR_DESCRIPTION = "description";

View file

@ -176,7 +176,7 @@ public class ConfigTemplateAttributeForm implements TemplateComposer {
configuration,
new View(-1L, "template", 10, 0, templateId),
attributeMapping,
1);
1, false);
final InputFieldBuilder inputFieldBuilder = this.examConfigurationService.getInputFieldBuilder(
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.Configuration;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationType;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
@ -74,6 +75,8 @@ public class SebExamConfigPropForm implements TemplateComposer {
new LocTextKey("sebserver.examconfig.form.name");
private static final LocTextKey FORM_DESCRIPTION_TEXT_KEY =
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 =
new LocTextKey("sebserver.examconfig.form.status");
private static final LocTextKey FORM_IMPORT_TEXT_KEY =
@ -113,6 +116,7 @@ public class SebExamConfigPropForm implements TemplateComposer {
final UserInfo user = this.currentUser.get();
final EntityKey entityKey = pageContext.getEntityKey();
final EntityKey parentEntityKey = pageContext.getParentEntityKey();
final boolean isNew = entityKey == null;
// get data or create new. Handle error if happen
@ -169,6 +173,14 @@ public class SebExamConfigPropForm implements TemplateComposer {
FORM_DESCRIPTION_TEXT_KEY,
examConfig.description)
.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(
Domain.CONFIGURATION_NODE.ATTR_STATUS,
FORM_STATUS_TEXT_KEY,
@ -178,6 +190,7 @@ public class SebExamConfigPropForm implements TemplateComposer {
? this.restService.getRestCall(NewExamConfig.class)
: this.restService.getRestCall(SaveExamConfig.class));
final boolean settingsReadonly = examConfig.status == ConfigurationStatus.IN_USE;
final UrlLauncher urlLauncher = RWT.getClient().getService(UrlLauncher.class);
this.pageService.pageActionBuilder(formContext.clearEntityKeys())
@ -190,7 +203,11 @@ public class SebExamConfigPropForm implements TemplateComposer {
.newAction(ActionDefinition.SEB_EXAM_CONFIG_MODIFY)
.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)
.withEntityKey(entityKey)
@ -216,13 +233,13 @@ public class SebExamConfigPropForm implements TemplateComposer {
.noEventPropagation()
.publishIf(() -> modifyGrant && isReadonly)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_SAVE)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_PROP_SAVE)
.withEntityKey(entityKey)
.withExec(formHandle::processFormSave)
.ignoreMoveAwayFromEdit()
.publishIf(() -> !isReadonly)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_CANCEL_MODIFY)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_PROP_CANCEL_MODIFY)
.withEntityKey(entityKey)
.withExec(this.pageService.backToCurrentFunction())
.publishIf(() -> !isReadonly);

View file

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

View file

@ -351,12 +351,12 @@ public enum ActionDefinition {
SEB_EXAM_CONFIG_VIEW_PROP_FROM_LIST(
new LocTextKey("sebserver.examconfig.action.list.view"),
ImageIcon.SHOW,
PageStateDefinitionImpl.SEB_EXAM_CONFIG_VIEW,
PageStateDefinitionImpl.SEB_EXAM_CONFIG_PROP_VIEW,
ActionCategory.SEB_EXAM_CONFIG_LIST),
SEB_EXAM_CONFIG_VIEW_PROP(
new LocTextKey("sebserver.examconfig.action.view"),
ImageIcon.SHOW,
PageStateDefinitionImpl.SEB_EXAM_CONFIG_VIEW,
PageStateDefinitionImpl.SEB_EXAM_CONFIG_PROP_VIEW,
ActionCategory.FORM),
SEB_EXAM_CONFIG_MODIFY_PROP_FROM_LIST(
@ -374,15 +374,20 @@ public enum ActionDefinition {
ImageIcon.EDIT_SETTINGS,
PageStateDefinitionImpl.SEB_EXAM_CONFIG_EDIT,
ActionCategory.FORM),
SEB_EXAM_CONFIG_CANCEL_MODIFY(
new LocTextKey("sebserver.overall.action.modify.cancel"),
ImageIcon.CANCEL,
SEB_EXAM_CONFIG_VIEW(
new LocTextKey("sebserver.examconfig.action.view"),
ImageIcon.EDIT_SETTINGS,
PageStateDefinitionImpl.SEB_EXAM_CONFIG_VIEW,
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"),
ImageIcon.SAVE,
PageStateDefinitionImpl.SEB_EXAM_CONFIG_VIEW,
PageStateDefinitionImpl.SEB_EXAM_CONFIG_PROP_VIEW,
ActionCategory.FORM),
SEB_EXAM_CONFIG_EXPORT_PLAIN_XML(
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_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_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_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.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
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);
if (this.isArea) {
gridData.minimumHeight = 35;
gridData.minimumHeight = WidgetFactory.TEXT_AREA_INPUT_MIN_HEIGHT;
}
textInput.setLayoutData(gridData);
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.institution.GetInstitutionNames;
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.useraccount.GetUserAccountNames;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
@ -551,6 +552,19 @@ public class ResourceService {
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() {
return this.restService.getBuilder(GetExamConfigMappingNames.class)
.withQueryParam(

View file

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

View file

@ -104,6 +104,7 @@ public abstract class AbstractTableFieldBuilder implements InputFieldBuilder {
final TableContext tableContext) {
try {
final boolean readonly = tableContext.getViewContext().readonly;
final int currentTableWidth = table.getClientArea().width - TABLE_WIDTH_SPACE;
final TableColumn[] columns = table.getColumns();
final List<Orientation> orientations = tableContext
@ -116,7 +117,7 @@ public abstract class AbstractTableFieldBuilder implements InputFieldBuilder {
.map(o -> o.width)
.reduce(0, (acc, val) -> acc + val);
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);
}
} catch (final Exception e) {

View file

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

View file

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

View file

@ -201,16 +201,27 @@ public class CompositeTableFieldBuilder extends AbstractTableFieldBuilder {
rowValues,
row);
new ModalInputDialog<Map<Long, TableValue>>(
final ModalInputDialog<Map<Long, TableValue>> dialog = new ModalInputDialog<Map<Long, TableValue>>(
this.control.getShell(),
this.tableContext.getWidgetFactory())
.setDialogWidth(500)
.open(
new LocTextKey(ExamConfigurationService.ATTRIBUTE_LABEL_LOC_TEXT_PREFIX + row),
rowVals -> applyFormValues(this.values, rowVals, selectionIndex),
() -> this.tableContext.getValueChangeListener()
.tableChanged(extractTableValue(this.values)),
builder);
.setDialogWidth(500);
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),
rowVals -> applyFormValues(this.values, rowVals, selectionIndex),
() -> this.tableContext.getValueChangeListener()
.tableChanged(extractTableValue(this.values)),
builder);
}
}
@Override

View file

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

View file

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

View file

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

View file

@ -13,6 +13,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.apache.commons.codec.binary.Hex;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
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.InputFieldBuilder;
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
@Component
@ -67,7 +70,8 @@ public class PassworFieldBuilder implements InputFieldBuilder {
.createInnerGrid(parent, attribute, orientation);
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 GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false);
gridData.verticalIndent = 14;
@ -80,37 +84,46 @@ public class PassworFieldBuilder implements InputFieldBuilder {
confirmInput,
Form.createErrorLabel(innerGrid));
final Listener valueChangeEventListener = event -> {
passwordInputField.clearError();
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 -> {
passwordInputField.clearError();
final String pwd = passwordInput.getText();
final String confirm = confirmInput.getText();
final String pwd = passwordInput.getText();
final String confirm = confirmInput.getText();
if (passwordInputField.initValue != null && passwordInputField.initValue.equals(pwd)) {
return;
}
if (passwordInputField.initValue != null && passwordInputField.initValue.equals(pwd)) {
return;
}
if (!pwd.equals(confirm)) {
passwordInputField.showError(viewContext
.getI18nSupport()
.getText(VAL_CONFIRM_PWD_TEXT_KEY));
return;
}
if (!pwd.equals(confirm)) {
passwordInputField.showError(viewContext
.getI18nSupport()
.getText(VAL_CONFIRM_PWD_TEXT_KEY));
return;
}
final String hashedPWD = passwordInputField.getValue();
if (hashedPWD != null) {
viewContext.getValueChangeListener().valueChanged(
viewContext,
attribute,
hashedPWD,
passwordInputField.listIndex);
}
};
final String hashedPWD = passwordInputField.getValue();
if (hashedPWD != null) {
viewContext.getValueChangeListener().valueChanged(
viewContext,
attribute,
hashedPWD,
passwordInputField.listIndex);
}
};
passwordInput.addListener(SWT.FocusOut, valueChangeEventListener);
passwordInput.addListener(SWT.Traverse, valueChangeEventListener);
confirmInput.addListener(SWT.FocusOut, valueChangeEventListener);
confirmInput.addListener(SWT.Traverse, valueChangeEventListener);
passwordInput.addListener(SWT.FocusOut, valueChangeEventListener);
passwordInput.addListener(SWT.Traverse, valueChangeEventListener);
confirmInput.addListener(SWT.FocusOut, valueChangeEventListener);
confirmInput.addListener(SWT.Traverse, valueChangeEventListener);
}
return passwordInputField;
}

View file

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

View file

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

View file

@ -73,17 +73,21 @@ public class SliderFieldBuilder implements InputFieldBuilder {
orientation,
slider);
final Listener valueChangeEventListener = event -> {
inputField.clearError();
viewContext.getValueChangeListener().valueChanged(
viewContext,
attribute,
inputField.getValue(),
inputField.listIndex);
};
if (viewContext.readonly) {
slider.setEnabled(false);
} else {
final Listener valueChangeEventListener = event -> {
inputField.clearError();
viewContext.getValueChangeListener().valueChanged(
viewContext,
attribute,
inputField.getValue(),
inputField.listIndex);
};
slider.addListener(SWT.FocusOut, valueChangeEventListener);
slider.addListener(SWT.Traverse, valueChangeEventListener);
slider.addListener(SWT.FocusOut, valueChangeEventListener);
slider.addListener(SWT.Traverse, valueChangeEventListener);
}
return inputField;
}

View file

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

View file

@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.gui.service.examconfig.impl;
import java.util.function.Consumer;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
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.LocTextKey;
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
@Component
@ -74,7 +77,7 @@ public class TextFieldBuilder implements InputFieldBuilder {
}
case TEXT_AREA: {
text = new Text(innerGrid, SWT.LEFT | SWT.BORDER | SWT.MULTI);
gridData.heightHint = 50;
gridData.minimumHeight = WidgetFactory.TEXT_AREA_INPUT_MIN_HEIGHT;
break;
}
default: {
@ -101,17 +104,25 @@ public class TextFieldBuilder implements InputFieldBuilder {
text,
Form.createErrorLabel(innerGrid));
final Listener valueChangeEventListener = event -> {
textInputField.clearError();
viewContext.getValueChangeListener().valueChanged(
viewContext,
attribute,
textInputField.getValue(),
textInputField.listIndex);
};
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 -> {
textInputField.clearError();
viewContext.getValueChangeListener().valueChanged(
viewContext,
attribute,
textInputField.getValue(),
textInputField.listIndex);
};
text.addListener(SWT.FocusOut, valueChangeEventListener);
text.addListener(SWT.Traverse, valueChangeEventListener);
text.addListener(SWT.FocusOut, valueChangeEventListener);
text.addListener(SWT.Traverse, valueChangeEventListener);
}
return textInputField;
}

View file

@ -37,6 +37,7 @@ public final class ViewContext {
final Map<Long, InputField> inputFieldMapping;
final ValueChangeListener valueChangeListener;
final I18nSupport i18nSupport;
final boolean readonly;
ViewContext(
final Configuration configuration,
@ -44,7 +45,8 @@ public final class ViewContext {
final int rows,
final AttributeMapping attributeContext,
final ValueChangeListener valueChangeListener,
final I18nSupport i18nSupport) {
final I18nSupport i18nSupport,
final boolean readonly) {
Objects.requireNonNull(configuration);
Objects.requireNonNull(view);
@ -59,6 +61,7 @@ public final class ViewContext {
this.inputFieldMapping = new HashMap<>();
this.valueChangeListener = valueChangeListener;
this.i18nSupport = i18nSupport;
this.readonly = readonly;
}
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);
this.widgetFactory = widgetFactory;
}
public ModalInputDialog<T> setDialogWidth(final int dialogWidth) {
@ -161,7 +160,6 @@ public class ModalInputDialog<T> extends Dialog {
shell.pack();
final Rectangle bounds = shell.getBounds();
final Rectangle bounds2 = super.getParent().getDisplay().getBounds();
//bounds.width = bounds.width;
bounds.x = (bounds2.width - bounds.width) / 2;
bounds.y = (bounds2.height - bounds.height) / 2;
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.StringUtils;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
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.gui.service.i18n.LocTextKey;
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;
public class GridTable extends Composite {
@ -394,6 +396,7 @@ public class GridTable extends Composite {
TextField(final Composite parent, final ColumnDef columnDef, final Listener listener) {
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.columnDef = columnDef;
this._textField.addListener(SWT.FocusOut, listener);

View file

@ -58,6 +58,9 @@ public class WidgetFactory {
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 {
MAXIMIZE("maximize.png"),
MINIMIZE("minimize.png"),
@ -127,7 +130,8 @@ public class WidgetFactory {
TITLE_LABEL("head"),
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) {
return readonly
? new Text(content, SWT.LEFT | SWT.BORDER)
? new Text(content, SWT.LEFT)
: new Text(content, (password)
? SWT.LEFT | SWT.BORDER | SWT.PASSWORD
: SWT.LEFT | SWT.BORDER);

View file

@ -43,7 +43,7 @@ public interface ConfigurationDAO extends EntityDAO<Configuration, Configuration
Result<Configuration> saveToHistory(Long configurationNodeId);
/** Can be used to reset the current follow-up configuration back to the last saved version in the history
*
*
* @param configurationNodeId ConfigurationNode identifier to apply the undo on
* @return the current and reseted follow-up version */
Result<Configuration> undo(Long configurationNodeId);
@ -62,4 +62,10 @@ public interface ConfigurationDAO extends EntityDAO<Configuration, Configuration
* @return the current follow-up configuration */
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;
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;
public interface ConfigurationNodeDAO extends
EntityDAO<ConfigurationNode, 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 */
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.
*
* @param configurationId
* @return */
* @return Result referencing the List of exam identifiers (PK) for a given configuration identifier */
Result<Collection<Long>> getExamIdsForConfigId(Long configurationId);
}

View file

@ -66,6 +66,8 @@ class ConfigurationDAOBatchService {
private static final Logger log = LoggerFactory.getLogger(ConfigurationDAOBatchService.class);
public static final String INITIAL_VERSION_NAME = "v0";
private final ConfigurationNodeRecordMapper batchConfigurationNodeRecordMapper;
private final ConfigurationValueRecordMapper batchConfigurationValueRecordMapper;
private final ConfigurationAttributeRecordMapper batchConfigurationAttributeRecordMapper;
@ -187,23 +189,13 @@ class ConfigurationDAOBatchService {
.execute();
// 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
final ConfigurationRecord configUpdate = new ConfigurationRecord(
followupConfig.getId(),
null,
null,
"v" + versions,
generateVersionName(configurationNodeId),
DateTime.now(DateTimeZone.UTC),
BooleanUtils.toInteger(false));
this.batchConfigurationRecordMapper.updateByPrimaryKeySelective(configUpdate);
@ -240,6 +232,20 @@ class ConfigurationDAOBatchService {
.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) {
return Result.tryCatch(() -> {
// get all configurations of the node
@ -311,6 +317,69 @@ class ConfigurationDAOBatchService {
.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) {
return this.batchConfigurationRecordMapper
.selectByExample()
@ -454,7 +523,7 @@ class ConfigurationDAOBatchService {
null,
config.institutionId,
config.id,
"v0", // TODO?
INITIAL_VERSION_NAME,
DateTime.now(DateTimeZone.UTC),
BooleanUtils.toInteger(false));
@ -547,7 +616,7 @@ class ConfigurationDAOBatchService {
}
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)))
.build()
.execute()
@ -555,14 +624,15 @@ class ConfigurationDAOBatchService {
.collect(Utils.toSingleton())
.getId();
return this.batchConfigurationValueRecordMapper.selectByExample()
final List<ConfigurationValueRecord> values = this.batchConfigurationValueRecordMapper.selectByExample()
.where(ConfigurationValueRecordDynamicSqlSupport.configurationId, isEqualTo(configurationId))
.build()
.execute()
.stream()
.execute();
return values.stream()
.collect(Collectors.toMap(
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.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
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
@Transactional
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.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;
@ -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.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;
@ -56,18 +62,24 @@ 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) {
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.configurationNodeRecordMapper = configurationNodeRecordMapper;
this.configurationValueRecordMapper = configurationValueRecordMapper;
this.configurationDAOBatchService = ConfigurationDAOBatchService;
this.copyNamePrefix = copyNamePrefix;
this.copyNameSuffix = copyNameSuffix;
}
@Override
@ -191,6 +203,84 @@ public class ConfigurationNodeDAOImpl implements ConfigurationNodeDAO {
.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
@Transactional
public Result<Collection<EntityKey>> delete(final Set<EntityKey> all) {

View file

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

View file

@ -111,7 +111,7 @@ public class ExamConfigIO {
// get follow-up configurationId for given configurationNodeId
final Long configurationId = this.configurationDAO
.getFollowupConfiguration(configurationNodeId)
.getConfigurationLastStableVersion(configurationNodeId)
.getOrThrow().id;
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.RestController;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
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.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.servicelayer.PaginationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.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.sebconfig.ConfigurationChangedEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
@WebServiceProfile
@ -40,7 +46,9 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe
public class ConfigurationController extends ReadonlyEntityController<Configuration, Configuration> {
private final ConfigurationDAO configurationDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final ApplicationEventPublisher applicationEventPublisher;
private final ExamSessionService examSessionService;
protected ConfigurationController(
final AuthorizationService authorization,
@ -49,7 +57,9 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
final UserActivityLogDAO userActivityLogDAO,
final PaginationService paginationService,
final BeanValidationService beanValidationService,
final ApplicationEventPublisher applicationEventPublisher) {
final ApplicationEventPublisher applicationEventPublisher,
final ExamSessionService examSessionService,
final ExamConfigurationMapDAO examConfigurationMapDAO) {
super(authorization,
bulkActionService,
@ -60,6 +70,8 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
this.configurationDAO = entityDAO;
this.applicationEventPublisher = applicationEventPublisher;
this.examSessionService = examSessionService;
this.examConfigurationMapDAO = examConfigurationMapDAO;
}
@RequestMapping(
@ -71,6 +83,7 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
return this.entityDAO.byModelId(modelId)
.flatMap(this.authorization::checkModify)
.flatMap(this::checkRunningExamIntegrity)
.flatMap(config -> this.configurationDAO.saveToHistory(config.configurationNodeId))
.flatMap(this.userActivityLogDAO::logSaveToHistory)
.flatMap(this::publishConfigChanged)
@ -118,9 +131,53 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
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) {
this.applicationEventPublisher.publishEvent(new ConfigurationChangedEvent(config.id));
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 org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.mybatis.dynamic.sql.SqlTable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -71,11 +72,11 @@ public class ConfigurationNodeController extends EntityController<ConfigurationN
private static final Logger log = LoggerFactory.getLogger(ConfigurationNodeController.class);
private final ConfigurationNodeDAO configurationNodeDAO;
private final ConfigurationDAO configurationDAO;
private final ViewDAO viewDAO;
private final OrientationDAO orientationDAO;
private final SebExamConfigService sebExamConfigService;
private final SebExamConfigTemplateService sebExamConfigTemplateService;
protected ConfigurationNodeController(
@ -99,6 +100,7 @@ public class ConfigurationNodeController extends EntityController<ConfigurationN
beanValidationService);
this.configurationDAO = configurationDAO;
this.configurationNodeDAO = entityDAO;
this.viewDAO = viewDAO;
this.orientationDAO = orientationDAO;
this.sebExamConfigService = sebExamConfigService;
@ -135,6 +137,35 @@ public class ConfigurationNodeController extends EntityController<ConfigurationN
.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(
path = API.MODEL_ID_VAR_PATH_SEGMENT + API.CONFIGURATION_CONFIG_KEY_PATH_SEGMENT,
method = RequestMethod.GET,

View file

@ -13,7 +13,6 @@ import java.util.Objects;
import javax.validation.Valid;
import org.mybatis.dynamic.sql.SqlTable;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestBody;
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.ConfigurationValueDAO;
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.validation.BeanValidationService;
@ -48,7 +46,6 @@ public class ConfigurationValueController extends EntityController<Configuration
private final ConfigurationDAO configurationDAO;
private final ConfigurationValueDAO configurationValueDAO;
private final SebExamConfigService sebExamConfigService;
private final ApplicationEventPublisher applicationEventPublisher;
protected ConfigurationValueController(
final AuthorizationService authorization,
@ -58,8 +55,7 @@ public class ConfigurationValueController extends EntityController<Configuration
final PaginationService paginationService,
final BeanValidationService beanValidationService,
final ConfigurationDAO configurationDAO,
final SebExamConfigService sebExamConfigService,
final ApplicationEventPublisher applicationEventPublisher) {
final SebExamConfigService sebExamConfigService) {
super(authorization,
bulkActionService,
@ -71,19 +67,6 @@ public class ConfigurationValueController extends EntityController<Configuration
this.configurationDAO = configurationDAO;
this.configurationValueDAO = entityDAO;
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
@ -141,11 +124,6 @@ public class ConfigurationValueController extends EntityController<Configuration
return this.configurationDAO.byPK(tableValue.configurationId)
.flatMap(this.authorization::checkModify)
.flatMap(config -> this.configurationValueDAO.saveTableValues(tableValue))
.map(config -> {
this.applicationEventPublisher.publishEvent(
new ConfigurationChangedEvent(config.configurationId));
return config;
})
.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.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

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.view=View Configuration
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.view=View Configuration
sebserver.examconfig.action.modify=Edit Settings
sebserver.examconfig.action.modify.properties=Edit Configuration
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.undo=Undo
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.name=Name
sebserver.examconfig.form.description=Description
sebserver.examconfig.form.template=From Template
sebserver.examconfig.form.status=Status
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.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.view=View Template
sebserver.configtemplate.action.list.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.name=Name
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 {
border: 1px solid #4f7cb1;
border-radius: 0;
box-shadow: none;
}
Text:disabled,
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;
}
Text:disabled,
Text:read-only,
Text[BORDER]:disabled,
Text[BORDER]:read-only,
Text[MULTI]:disabled,
Text[MULTI]:read-only,
Text[MULTI][BORDER]:disabled,
Text[MULTI][BORDER]:read-only {
Text[MULTI]:disabled,
Text[MULTI]:read-only {
box-shadow: none;
background-color: #ffffff;
border: none;
@ -460,6 +475,14 @@ Button[PUSH]:hover.header {
cursor: pointer;
}
Button[CHECK]:disabled {
color: #4a4a4a;
}
Button[RADIO]:disabled {
color: #4a4a4a;
}
FileUpload,
FileUpload:default,
FileUpload:hover,

View file

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