diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/configs/SEBSettingsForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/configs/SEBSettingsForm.java index b95ace8f..c389f14a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/configs/SEBSettingsForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/configs/SEBSettingsForm.java @@ -171,7 +171,7 @@ public class SEBSettingsForm implements TemplateComposer { view, viewContextSupplier, attributes, - 20, + 30, readonly, publishedMessagePanelViewCallback); viewContexts.add(viewContext); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/FieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/FieldBuilder.java index b52546d7..e196febb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/FieldBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/FieldBuilder.java @@ -216,7 +216,7 @@ public abstract class FieldBuilder { public static Label createErrorLabel(final Composite innerGrid) { final Label errorLabel = new Label(innerGrid, SWT.NONE); - final GridData gridData = new GridData(SWT.FILL, SWT.CENTER, true, true); + final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true); errorLabel.setLayoutData(gridData); errorLabel.setVisible(false); errorLabel.setData(RWT.CUSTOM_VARIANT, CustomVariant.ERROR.key); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/TextFieldListBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/TextFieldListBuilder.java new file mode 100644 index 00000000..b90f238f --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/TextFieldListBuilder.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2023 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.examconfig.impl; + +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; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gbl.model.sebconfig.AttributeType; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.Orientation; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.form.FieldBuilder; +import ch.ethz.seb.sebserver.gui.service.examconfig.ExamConfigurationService; +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.service.remote.webservice.api.RestService; +import ch.ethz.seb.sebserver.gui.widget.TextListInput; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant; + +@Lazy +@Component +@GuiProfile +public class TextFieldListBuilder extends AbstractTableFieldBuilder { + + protected TextFieldListBuilder( + final RestService restService, + final WidgetFactory widgetFactory) { + + super(restService, widgetFactory); + } + + @Override + public boolean builderFor(final ConfigurationAttribute attribute, final Orientation orientation) { + return AttributeType.TEXT_FIELD_LIST == attribute.type; + } + + @Override + public InputField createInputField( + final Composite parent, + final ConfigurationAttribute attribute, + final ViewContext viewContext) { + + final Orientation orientation = viewContext + .getOrientation(attribute.id); + final Composite innerGrid = InputFieldBuilder + .createInnerGrid(parent, attribute, orientation); + +// final Composite scroll = PageService.createManagedVScrolledComposite( +// innerGrid, +// scrolledComposite -> { +// final Composite result = new Composite(scrolledComposite, SWT.NONE); +// final GridLayout gridLayout1 = new GridLayout(); +// result.setLayout(gridLayout1); +// final GridData gridData1 = new GridData(SWT.FILL, SWT.FILL, true, true); +// result.setLayoutData(gridData1); +// return result; +// }, +// false, +// false, true); + + final String attributeNameKey = ExamConfigurationService.attributeNameKey(attribute); + final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false); + gridData.minimumHeight = WidgetFactory.TEXT_AREA_INPUT_MIN_HEIGHT; + final TextListInput textListInput = new TextListInput( + innerGrid, + new LocTextKey(attributeNameKey), + 3, + this.widgetFactory); + WidgetFactory.setTestId(textListInput, attributeNameKey); + textListInput.setLayoutData(gridData); + + final TextListInputField textListInputField = new TextListInputField( + attribute, + orientation, + textListInput, + FieldBuilder.createErrorLabel(innerGrid)); + + if (viewContext.readonly) { + textListInput.setEditable(false); + + } else { + final Listener valueChangeEventListener = event -> { + textListInputField.clearError(); + viewContext.getValueChangeListener().valueChanged( + viewContext, + attribute, + textListInputField.getValue(), + textListInputField.listIndex); + }; + + textListInput.addListener(valueChangeEventListener); + } + + return textListInputField; + } + + static final class TextListInputField extends AbstractInputField { + + TextListInputField( + final ConfigurationAttribute attribute, + final Orientation orientation, + final TextListInput control, + final Label errorLabel) { + + super(attribute, orientation, control, errorLabel); + } + + @Override + protected void setValueToControl(final String value) { + if (value == null) { + this.control.setValue(StringUtils.EMPTY); + return; + } + + this.control.setValue(value); + } + + @Override + public void enable(final boolean group) { + this.control.setData(RWT.CUSTOM_VARIANT, null); + this.control.setEditable(true); + } + + @Override + public void disable(final boolean group) { + this.control.setData(RWT.CUSTOM_VARIANT, CustomVariant.CONFIG_INPUT_READONLY.key); + this.control.setEditable(false); + final GridData gridData = (GridData) this.control.getLayoutData(); + gridData.heightHint = (this.attribute.type == AttributeType.TEXT_AREA) + ? WidgetFactory.TEXT_AREA_INPUT_MIN_HEIGHT + : WidgetFactory.TEXT_INPUT_MIN_HEIGHT; + } + + @Override + public String getValue() { + return this.control.getValue(); + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/ControlAdapter.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/ControlAdapter.java new file mode 100644 index 00000000..7b0a484e --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/ControlAdapter.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 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.widget; + +import ch.ethz.seb.sebserver.gui.widget.GridTable.ColumnDef; + +interface ControlAdapter { + String getValue(); + + void setValue(String value); + + void dispose(); + + ColumnDef columnDef(); +} \ No newline at end of file diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/GridTable.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/GridTable.java index 3d86aaa0..2a169f10 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/widget/GridTable.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/GridTable.java @@ -305,16 +305,6 @@ public class GridTable extends Composite { } } - interface ControlAdapter { - String getValue(); - - void setValue(String value); - - void dispose(); - - ColumnDef columnDef(); - } - private static class Dummy implements ControlAdapter { private final Label label; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/TextListInput.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/TextListInput.java new file mode 100644 index 00000000..4d0343e6 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/TextListInput.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2023 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.widget; + +import java.util.ArrayList; +import java.util.List; + +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; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Text; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.ImageIcon; + +public class TextListInput extends Composite { + + private static final long serialVersionUID = 1361754245260627626L; + + private static final int ACTION_COLUMN_WIDTH = 20; + + private final LocTextKey nameKey; + private final int initialSize; + private final WidgetFactory widgetFactory; + private final Button addAction; + private final Composite content; + private final List list = new ArrayList<>(); + + private Listener valueChangeEventListener = null; + + public TextListInput( + final Composite parent, + final LocTextKey nameKey, + final int initialSize, + final WidgetFactory widgetFactory) { + + super(parent, SWT.NONE); + this.nameKey = nameKey; + this.initialSize = initialSize; + this.widgetFactory = widgetFactory; + + // main grid layout + GridLayout gridLayout = new GridLayout(1, false); + gridLayout.verticalSpacing = 1; + gridLayout.marginLeft = 0; + gridLayout.marginHeight = 5; + gridLayout.marginWidth = 0; + gridLayout.horizontalSpacing = 0; + this.setLayout(gridLayout); + final GridData gridData2 = new GridData(SWT.FILL, SWT.FILL, true, true); + this.setLayoutData(gridData2); + + // build header + final Composite header = new Composite(this, SWT.NONE); + gridLayout = new GridLayout(2, false); + header.setLayout(gridLayout); + GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true); + header.setLayoutData(gridData); + + final Label label = widgetFactory.labelLocalized(header, this.nameKey); + gridData = new GridData(SWT.FILL, SWT.FILL, true, true); + label.setLayoutData(gridData); + + final LocTextKey toolTipKey = new LocTextKey(nameKey.name + ".tooltip"); + if (widgetFactory.getI18nSupport().hasText(toolTipKey)) { + label.setToolTipText(Utils.formatLineBreaks( + widgetFactory.getI18nSupport().getText(toolTipKey))); + } + + this.addAction = widgetFactory.imageButton( + ImageIcon.ADD_BOX, + header, + new LocTextKey(this.nameKey.name + ".addAction"), + this::addRow); + gridData = new GridData(SWT.RIGHT, SWT.CENTER, false, true); + gridData.widthHint = ACTION_COLUMN_WIDTH; + this.addAction.setLayoutData(gridData); + + this.content = header; + for (int i = 0; i < initialSize; i++) { + addRow(null); + } + } + + public void addListener(final Listener valueChangeEventListener) { + this.valueChangeEventListener = valueChangeEventListener; + this.list.stream().forEach(row -> row.addListener()); + } + + void addRow(final Event event) { + this.list.add(new Row(this.list.size())); + this.content.getParent().getParent().layout(true, true); + } + + public String getValue() { + return this.list.stream() + .map(row -> row.textInput.getText()) + .reduce("", (acc, val) -> { + if (StringUtils.isNotBlank(val)) { + if (StringUtils.isNotBlank(acc)) { + acc += Constants.LIST_SEPARATOR; + } + acc += val; + } + return acc; + }); + } + + public void setValue(final String value) { + System.out.println("************ value: " + value); + if (StringUtils.isBlank(value)) { + // clear rows + new ArrayList<>(this.list).stream().forEach(row -> row.deleteRow()); + this.list.clear(); + // and fill with default empty + for (int i = 0; i < this.initialSize; i++) { + addRow(null); + } + return; + } + + final String[] split = StringUtils.split(value, Constants.LIST_SEPARATOR); + int gap = this.list.size() - split.length; + while (gap < 0) { + addRow(null); + gap++; + } + + for (int i = 0; i < split.length; i++) { + this.list.get(i).textInput.setText(split[i]); + } + } + + public void setEditable(final boolean b) { + this.addAction.setEnabled(b); + this.list.stream().forEach(row -> row.setEditable(b)); + } + + private final class Row { + + public final Text textInput; + public final Button deleteButton; + + public Row(final int index) { + this.textInput = TextListInput.this.widgetFactory.textInput( + TextListInput.this.content, + TextListInput.this.nameKey); + GridData gridData = new GridData(SWT.FILL, SWT.CENTER, true, true); + this.textInput.setLayoutData(gridData); + this.addListener(); + + this.deleteButton = TextListInput.this.widgetFactory.imageButton( + ImageIcon.REMOVE_BOX, + TextListInput.this.content, + new LocTextKey(TextListInput.this.nameKey.name + ".removeAction"), + deleteEvent -> deleteRow()); + gridData = new GridData(SWT.RIGHT, SWT.CENTER, false, true); + gridData.widthHint = ACTION_COLUMN_WIDTH; + this.deleteButton.setLayoutData(gridData); + } + + private void addListener() { + if (TextListInput.this.valueChangeEventListener != null) { + this.textInput.addListener(SWT.FocusOut, TextListInput.this.valueChangeEventListener); + this.textInput.addListener(SWT.Traverse, TextListInput.this.valueChangeEventListener); + } + } + + public void deleteRow() { + TextListInput.this.list.remove(this); + this.textInput.dispose(); + this.deleteButton.dispose(); + TextListInput.this.content.getParent().getParent().layout(true, true); + if (TextListInput.this.valueChangeEventListener != null) { + TextListInput.this.valueChangeEventListener.handleEvent(null); + } + } + + public void setEditable(final boolean e) { + this.textInput.setEditable(e); + this.textInput.setData( + RWT.CUSTOM_VARIANT, + e ? null : CustomVariant.CONFIG_INPUT_READONLY.key); + this.deleteButton.setEnabled(e); + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/validation/SEBVersionValidator.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/validation/SEBVersionValidator.java new file mode 100644 index 00000000..2ef4e816 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/validation/SEBVersionValidator.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023 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.webservice.servicelayer.sebconfig.impl.validation; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationValueDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationValueValidator; + +@Lazy +@Component +@WebServiceProfile +public class SEBVersionValidator implements ConfigurationValueValidator { + + public static final String NAME = "SEBVersionValidator"; + + private final ConfigurationValueDAO configurationValueDAO; + + public SEBVersionValidator(final ConfigurationValueDAO configurationValueDAO) { + this.configurationValueDAO = configurationValueDAO; + } + + @Override + public String name() { + return NAME; + } + + @Override + public boolean validate(final ConfigurationValue value, final ConfigurationAttribute attribute) { + // if validator is not specified --> skip + if (!name().equals(attribute.validator)) { + return true; + } + + if (StringUtils.isBlank(value.value)) { + return true; + } + + final String[] split = StringUtils.split(value.value, Constants.LIST_SEPARATOR); + for (int i = 0; i < split.length; i++) { + if (!isValidSEBVersionMarker(split[i])) { + return false; + } + } + return true; + } + + private boolean isValidSEBVersionMarker(final String versionMarker) { + // TODO Auto-generated method stub + return false; + } + + @Override + public String createErrorMessage(final ConfigurationValue value, final ConfigurationAttribute attribute) { + if (StringUtils.isBlank(value.value)) { + return ConfigurationValueValidator.super.createErrorMessage(value, attribute); + } + + final String[] split = StringUtils.split(value.value, Constants.LIST_SEPARATOR); + final List newValues = new ArrayList<>(); + for (int i = 0; i < split.length; i++) { + if (isValidSEBVersionMarker(split[i])) { + newValues.add(split[i]); + } + } + + // save with the removed invalid values + if (!newValues.isEmpty()) { + this.configurationValueDAO.save(new ConfigurationValue( + value.id, + value.institutionId, + value.configurationId, + value.attributeId, + value.listIndex, + StringUtils.join(newValues, Constants.LIST_SEPARATOR))); + } + + return ConfigurationValueValidator.super.createErrorMessage(value, attribute); + } + +} diff --git a/src/main/resources/config/sql/base/V21__sebClientVersionSettings_v1_5.sql b/src/main/resources/config/sql/base/V21__sebClientVersionSettings_v1_5.sql new file mode 100644 index 00000000..12e77a11 --- /dev/null +++ b/src/main/resources/config/sql/base/V21__sebClientVersionSettings_v1_5.sql @@ -0,0 +1,23 @@ +-- ----------------------------------------------------------------- +-- SEBSERV-376 Add SEBVersionValidator to setting +-- ----------------------------------------------------------------- + +UPDATE configuration_attribute SET validator='SEBVersionValidator' WHERE id=1578; + +-- ----------------------------------------------------------------- +-- SEBSERV-376 Reorder security / logging section for default template +-- ----------------------------------------------------------------- + +UPDATE orientation SET width=6 WHERE config_attribute_id=305 AND template_id=0; +UPDATE orientation SET width=4 WHERE config_attribute_id=306 AND template_id=0; +UPDATE orientation SET width=4 WHERE config_attribute_id=307 AND template_id=0; +UPDATE orientation SET width=4 WHERE config_attribute_id=317 AND template_id=0; +UPDATE orientation SET width=4 WHERE config_attribute_id=319 AND template_id=0; +UPDATE orientation SET width=4 WHERE config_attribute_id=320 AND template_id=0; + +-- ----------------------------------------------------------------- +-- SEBSERV-376 add new orientation for default template +-- ----------------------------------------------------------------- + +INSERT IGNORE INTO orientation (config_attribute_id, template_id, view_id, group_id, x_position, y_position, width, height, title) VALUES + (1578, 0, 9, null, 7, 14, 4, 12, 'NONE'); \ No newline at end of file diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index fba48880..a5355639 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -1841,7 +1841,9 @@ sebserver.examconfig.props.label.prohibitedProcesses.ignoreInAAC=Ignore in AAC sebserver.examconfig.props.label.prohibitedProcesses.ignoreInAAC.tooltip=When using the AAC kiosk mode (which prevents network and screen access for other processes), ignore this prohibited process sebserver.examconfig.props.label.sebAllowedVersions=Allowed SEB Versions sebserver.examconfig.props.label.sebAllowedVersions.tooltip=List of text inputs which represent either a specific or a minimal SEB version which is allowed to access the (exam) session using current settings.
The version string has the following format: [OS.mayor.minor.patch(.minimal)], OS and mayor version are mandatory,
"minimal" indicates if version shall be interpreted as minimal version this and all above are valid. - +sebserver.examconfig.props.label.sebAllowedVersions.addAction=Add new allowed SEB version +sebserver.examconfig.props.label.sebAllowedVersions.deleteAction=Delete allowed SEB version +sebserver.examconfig.props.validation.SEBVersionValidator=At least one SEB Version has wrong format ################################ # SEB Exam Configuration Template