SEBSERV-137 implementation added texts and auto publish selection
This commit is contained in:
parent
921e1959ce
commit
ee0960602c
7 changed files with 127 additions and 82 deletions
|
@ -14,16 +14,21 @@ import java.util.function.Function;
|
|||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.swt.layout.GridData;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.eclipse.swt.widgets.Control;
|
||||
import org.eclipse.swt.widgets.Label;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||
import ch.ethz.seb.sebserver.gbl.api.APIMessageError;
|
||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.Domain;
|
||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||
|
@ -50,7 +55,9 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCallError;
|
|||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetExamConfigNodeNames;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.ImportExamConfigOnExistingConfig;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.ImportNewExamConfig;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.SaveExamConfigHistory;
|
||||
import ch.ethz.seb.sebserver.gui.widget.FileUploadSelection;
|
||||
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
|
||||
|
||||
@Lazy
|
||||
@Component
|
||||
|
@ -59,8 +66,18 @@ public class SEBExamConfigImportPopup {
|
|||
|
||||
private static final Logger log = LoggerFactory.getLogger(SEBExamConfigImportPopup.class);
|
||||
|
||||
private static final LocTextKey EXAM_CONFIG_IMPORT_TEXT =
|
||||
new LocTextKey("sebserver.examconfig.action.import.config.text");
|
||||
private static final LocTextKey SEB_SETTINGS_IMPORT_TEXT =
|
||||
new LocTextKey("sebserver.examconfig.action.import.settings.text");
|
||||
private static final LocTextKey SEB_SETTINGS_IMPORT_AUTO_PUBLISH =
|
||||
new LocTextKey("sebserver.examconfig.action.import.auto-publish");
|
||||
private final static PageMessageException MISSING_PASSWORD = new PageMessageException(
|
||||
new LocTextKey("sebserver.examconfig.action.import.missing-password"));
|
||||
private static final LocTextKey MESSAGE_SAVE_INTEGRITY_VIOLATION =
|
||||
new LocTextKey("sebserver.examconfig.action.saveToHistory.integrity-violation");
|
||||
|
||||
private final static String AUTO_PUBLISH_FLAG = "AUTO_PUBLISH_FLAG";
|
||||
|
||||
private final PageService pageService;
|
||||
|
||||
|
@ -152,13 +169,20 @@ public class SEBExamConfigImportPopup {
|
|||
restCall.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId);
|
||||
}
|
||||
|
||||
final Result<Configuration> configuration = restCall
|
||||
.call();
|
||||
|
||||
if (!configuration.hasError()) {
|
||||
final Result<Configuration> importResult = restCall.call();
|
||||
if (!importResult.hasError()) {
|
||||
context.publishInfo(SEBExamConfigForm.FORM_IMPORT_CONFIRM_TEXT_KEY);
|
||||
|
||||
// Auto publish?
|
||||
if (!newConfig && BooleanUtils.toBoolean(form.getFieldValue(AUTO_PUBLISH_FLAG))) {
|
||||
this.pageService.getRestService()
|
||||
.getBuilder(SaveExamConfigHistory.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, importResult.get().getModelId())
|
||||
.call()
|
||||
.onError(error -> notifyErrorOnSave(error, context));
|
||||
}
|
||||
} else {
|
||||
handleImportError(formHandle, configuration);
|
||||
handleImportError(formHandle, importResult);
|
||||
}
|
||||
|
||||
reloadPage(newConfig, context);
|
||||
|
@ -295,8 +319,17 @@ public class SEBExamConfigImportPopup {
|
|||
@Override
|
||||
public Supplier<FormHandle<ConfigurationNode>> compose(final Composite parent) {
|
||||
|
||||
final Composite grid = this.pageService.getWidgetFactory()
|
||||
.createPopupScrollComposite(parent);
|
||||
final WidgetFactory widgetFactory = this.pageService.getWidgetFactory();
|
||||
final Composite grid = widgetFactory.createPopupScrollComposite(parent);
|
||||
|
||||
final Label info = widgetFactory.labelLocalized(
|
||||
grid,
|
||||
(this.newConfig) ? EXAM_CONFIG_IMPORT_TEXT : SEB_SETTINGS_IMPORT_TEXT,
|
||||
true);
|
||||
final GridData gridData = new GridData(0, 0, this.newConfig, this.newConfig);
|
||||
gridData.horizontalIndent = 10;
|
||||
gridData.verticalIndent = 10;
|
||||
info.setLayoutData(gridData);
|
||||
|
||||
final ResourceService resourceService = this.pageService.getResourceService();
|
||||
final List<Tuple<String>> examConfigTemplateResources = resourceService.getExamConfigTemplateResources();
|
||||
|
@ -309,6 +342,13 @@ public class SEBExamConfigImportPopup {
|
|||
null,
|
||||
API.SEB_FILE_EXTENSION))
|
||||
|
||||
.addFieldIf(
|
||||
() -> !this.newConfig,
|
||||
() -> FormBuilder.checkbox(
|
||||
AUTO_PUBLISH_FLAG,
|
||||
SEB_SETTINGS_IMPORT_AUTO_PUBLISH,
|
||||
Constants.FALSE_STRING))
|
||||
|
||||
.addFieldIf(
|
||||
() -> this.newConfig,
|
||||
() -> FormBuilder.text(
|
||||
|
@ -348,4 +388,22 @@ public class SEBExamConfigImportPopup {
|
|||
}
|
||||
}
|
||||
|
||||
private static void notifyErrorOnSave(final Exception error, final PageContext context) {
|
||||
if (error instanceof APIMessageError) {
|
||||
try {
|
||||
final List<APIMessage> errorMessages = ((APIMessageError) error).getErrorMessages();
|
||||
final APIMessage apiMessage = errorMessages.get(0);
|
||||
if (APIMessage.ErrorMessage.INTEGRITY_VALIDATION.isOf(apiMessage)) {
|
||||
context.publishPageMessage(new PageMessageException(MESSAGE_SAVE_INTEGRITY_VIOLATION));
|
||||
} else {
|
||||
context.notifyUnexpectedError(error);
|
||||
}
|
||||
} catch (final PageMessageException e) {
|
||||
throw e;
|
||||
} catch (final Exception e) {
|
||||
throw new RuntimeException(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
|||
import ch.ethz.seb.sebserver.gbl.api.APIMessageError;
|
||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
|
||||
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;
|
||||
|
@ -51,7 +50,6 @@ import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
|
|||
import ch.ethz.seb.sebserver.gui.service.remote.download.DownloadService;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.download.SEBExamConfigPlaintextDownload;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamConfigMappingNames;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetConfigurations;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetExamConfigNode;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetSettingsPublished;
|
||||
|
@ -122,13 +120,6 @@ public class SEBSettingsForm implements TemplateComposer {
|
|||
.getOrThrow();
|
||||
|
||||
final boolean readonly = pageContext.isReadonly() || configNode.status == ConfigurationStatus.IN_USE;
|
||||
final boolean isAttachedToExam = !readonly && this.restService
|
||||
.getBuilder(GetExamConfigMappingNames.class)
|
||||
.withQueryParam(ExamConfigurationMap.FILTER_ATTR_CONFIG_ID, configNode.getModelId())
|
||||
.call()
|
||||
.map(names -> names != null && !names.isEmpty())
|
||||
.getOr(Boolean.FALSE);
|
||||
|
||||
final Composite warningPanelAnchor = new Composite(pageContext.getParent(), SWT.NONE);
|
||||
final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false);
|
||||
warningPanelAnchor.setLayoutData(gridData);
|
||||
|
@ -251,7 +242,7 @@ public class SEBSettingsForm implements TemplateComposer {
|
|||
.withExec(this.sebExamConfigImportPopup.importFunction(
|
||||
() -> String.valueOf(tabFolder.getSelectionIndex())))
|
||||
.noEventPropagation()
|
||||
.publishIf(() -> examConfigGrant.iw() && !readonly && !isAttachedToExam)
|
||||
.publishIf(() -> examConfigGrant.iw() && !readonly)
|
||||
|
||||
.newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP)
|
||||
.withEntityKey(entityKey)
|
||||
|
@ -298,7 +289,8 @@ public class SEBSettingsForm implements TemplateComposer {
|
|||
return;
|
||||
}
|
||||
|
||||
final boolean settingsPublished = this.pageService.getRestService()
|
||||
final boolean settingsPublished = this.pageService
|
||||
.getRestService()
|
||||
.getBuilder(GetSettingsPublished.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, this.nodeId)
|
||||
.call()
|
||||
|
@ -322,34 +314,7 @@ public class SEBSettingsForm implements TemplateComposer {
|
|||
}
|
||||
}
|
||||
|
||||
// private Runnable publishedMessagePanelViewCallback(
|
||||
// final Composite parent,
|
||||
// final String nodeId) {
|
||||
// return () -> {
|
||||
// final boolean settingsPublished = this.restService.getBuilder(GetSettingsPublished.class)
|
||||
// .withURIVariable(API.PARAM_MODEL_ID, nodeId)
|
||||
// .call()
|
||||
// .onError(error -> log.warn("Failed to verify published settings. Cause: ", error.getMessage()))
|
||||
// .map(result -> result.settingsPublished)
|
||||
// .getOr(false);
|
||||
//
|
||||
// if (!settingsPublished) {
|
||||
// if (parent.getChildren() != null && parent.getChildren().length == 0) {
|
||||
// final WidgetFactory widgetFactory = this.pageService.getWidgetFactory();
|
||||
// final Composite warningPanel = widgetFactory.createWarningPanel(parent);
|
||||
// widgetFactory.labelLocalized(
|
||||
// warningPanel,
|
||||
// CustomVariant.MESSAGE,
|
||||
// UNPUBLISHED_MESSAGE_KEY);
|
||||
// }
|
||||
// } else if (parent.getChildren() != null && parent.getChildren().length > 0) {
|
||||
// parent.getChildren()[0].dispose();
|
||||
// }
|
||||
// parent.getParent().layout();
|
||||
// };
|
||||
// }
|
||||
|
||||
private void notifyErrorOnSave(final Exception error, final PageContext context) {
|
||||
public void notifyErrorOnSave(final Exception error, final PageContext context) {
|
||||
if (error instanceof APIMessageError) {
|
||||
try {
|
||||
final List<APIMessage> errorMessages = ((APIMessageError) error).getErrorMessages();
|
||||
|
|
|
@ -231,6 +231,17 @@ public final class ViewContext {
|
|||
inputField);
|
||||
}
|
||||
|
||||
public String getValue(final String name) {
|
||||
try {
|
||||
final ConfigurationAttribute attributeByName = getAttributeByName(name);
|
||||
final InputField inputField = this.inputFieldMapping.get(attributeByName.id);
|
||||
return inputField.getValue();
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to get attribute value: {}, cause {}", name, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setValue(final String name, final String value) {
|
||||
try {
|
||||
final ConfigurationAttribute attributeByName = getAttributeByName(name);
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.eclipse.swt.SWT;
|
|||
import org.eclipse.swt.layout.GridData;
|
||||
import org.eclipse.swt.layout.GridLayout;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.eclipse.swt.widgets.Event;
|
||||
import org.eclipse.swt.widgets.Label;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -66,7 +67,7 @@ public class FileUploadSelection extends Composite {
|
|||
|
||||
super(parent, SWT.NONE);
|
||||
final GridLayout gridLayout = new GridLayout(2, false);
|
||||
gridLayout.horizontalSpacing = 0;
|
||||
gridLayout.horizontalSpacing = 5;
|
||||
gridLayout.marginHeight = 0;
|
||||
gridLayout.marginWidth = 0;
|
||||
gridLayout.verticalSpacing = 0;
|
||||
|
@ -78,14 +79,15 @@ public class FileUploadSelection extends Composite {
|
|||
if (readonly) {
|
||||
this.fileName = new Label(this, SWT.NONE);
|
||||
this.fileName.setText(i18nSupport.getText(PLEASE_SELECT_TEXT));
|
||||
this.fileName.setLayoutData(new GridData());
|
||||
this.fileName.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, true));
|
||||
this.fileUpload = null;
|
||||
this.uploadHandler = null;
|
||||
this.inputReceiver = null;
|
||||
} else {
|
||||
this.fileUpload = new FileUpload(this, SWT.NONE);
|
||||
this.fileUpload.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, true));
|
||||
this.fileUpload.setImage(WidgetFactory.ImageIcon.IMPORT.getImage(parent.getDisplay()));
|
||||
this.fileUpload.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false));
|
||||
|
||||
this.fileUpload.setToolTipText(Utils.formatLineBreaks(this.i18nSupport.getText(PLEASE_SELECT_TEXT)));
|
||||
this.inputReceiver = new InputReceiver();
|
||||
this.uploadHandler = new FileUploadHandler(this.inputReceiver);
|
||||
|
@ -96,29 +98,31 @@ public class FileUploadSelection extends Composite {
|
|||
|
||||
this.fileName = new Label(this, SWT.NONE);
|
||||
this.fileName.setText(i18nSupport.getText(PLEASE_SELECT_TEXT));
|
||||
this.fileName.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
|
||||
|
||||
this.fileUpload.addListener(SWT.Selection, event -> {
|
||||
this.selection = true;
|
||||
final String fileName = FileUploadSelection.this.fileUpload.getFileName();
|
||||
if (fileName == null || !fileSupported(fileName)) {
|
||||
if (FileUploadSelection.this.errorHandler != null) {
|
||||
final String text = i18nSupport.getText(new LocTextKey(
|
||||
"sebserver.overall.upload.unsupported.file",
|
||||
this.supportedFileExtensions.toString()),
|
||||
"Unsupported image file type selected");
|
||||
FileUploadSelection.this.errorHandler.accept(text);
|
||||
}
|
||||
return;
|
||||
}
|
||||
FileUploadSelection.this.fileUpload.submit(this.uploadHandler.getUploadUrl());
|
||||
FileUploadSelection.this.fileName.setText(fileName);
|
||||
FileUploadSelection.this.errorHandler.accept(null);
|
||||
});
|
||||
this.fileName.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, true));
|
||||
|
||||
this.fileUpload.addListener(SWT.Selection, this::selectFile);
|
||||
this.fileName.addListener(SWT.Selection, this::selectFile);
|
||||
}
|
||||
}
|
||||
|
||||
private void selectFile(final Event event) {
|
||||
this.selection = true;
|
||||
final String fileName = FileUploadSelection.this.fileUpload.getFileName();
|
||||
if (fileName == null || !fileSupported(fileName)) {
|
||||
if (FileUploadSelection.this.errorHandler != null) {
|
||||
final String text = this.i18nSupport.getText(new LocTextKey(
|
||||
"sebserver.overall.upload.unsupported.file",
|
||||
this.supportedFileExtensions.toString()),
|
||||
"Unsupported image file type selected");
|
||||
FileUploadSelection.this.errorHandler.accept(text);
|
||||
}
|
||||
return;
|
||||
}
|
||||
FileUploadSelection.this.fileUpload.submit(this.uploadHandler.getUploadUrl());
|
||||
FileUploadSelection.this.fileName.setText(fileName);
|
||||
FileUploadSelection.this.errorHandler.accept(null);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (this.inputReceiver != null) {
|
||||
this.inputReceiver.close();
|
||||
|
|
|
@ -105,8 +105,8 @@ public class ExamSessionCacheService {
|
|||
key = "#exam.id")
|
||||
public Exam evict(final Exam exam) {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Conditional eviction of running Exam from cache: {}", isRunning(exam));
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Conditional eviction of running Exam from cache: {}", isRunning(exam));
|
||||
}
|
||||
|
||||
return exam;
|
||||
|
@ -117,8 +117,8 @@ public class ExamSessionCacheService {
|
|||
key = "#examId")
|
||||
public Long evict(final Long examId) {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Conditional eviction of running Exam from cache: {}", examId);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Conditional eviction of running Exam from cache: {}", examId);
|
||||
}
|
||||
|
||||
return examId;
|
||||
|
@ -167,8 +167,8 @@ public class ExamSessionCacheService {
|
|||
cacheNames = CACHE_NAME_ACTIVE_CLIENT_CONNECTION,
|
||||
key = "#connectionToken")
|
||||
public void evictClientConnection(final String connectionToken) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Eviction of ClientConnectionData from cache: {}", connectionToken);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Eviction of ClientConnectionData from cache: {}", connectionToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -197,8 +197,8 @@ public class ExamSessionCacheService {
|
|||
cacheNames = CACHE_NAME_SEB_CONFIG_EXAM,
|
||||
key = "#examId")
|
||||
public void evictDefaultSEBConfig(final Long examId) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Eviction of default SEB Configuration from cache for exam: {}", examId);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Eviction of default SEB Configuration from cache for exam: {}", examId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -208,6 +208,7 @@ public class ExamSessionCacheService {
|
|||
unless = "#result == null")
|
||||
@Transactional
|
||||
public ClientEventRecord getPingRecord(final String connectionToken) {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Verify ClientConnection for ping record to cache by connectionToken: {}", connectionToken);
|
||||
}
|
||||
|
@ -240,8 +241,8 @@ public class ExamSessionCacheService {
|
|||
cacheNames = CACHE_NAME_PING_RECORD,
|
||||
key = "#connectionToken")
|
||||
public void evictPingRecord(final String connectionToken) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Eviction of ReusableClientEventRecord from cache for connection token: {}", connectionToken);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Eviction of ReusableClientEventRecord from cache for connection token: {}", connectionToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -324,17 +324,19 @@ public class ConfigurationNodeController extends EntityController<ConfigurationN
|
|||
this.entityDAO.byPK(modelId)
|
||||
.flatMap(this.authorization::checkModify);
|
||||
|
||||
// final Configuration newConfig = this.configurationDAO
|
||||
// .saveToHistory(modelId)
|
||||
// .flatMap(this.configurationDAO::restoreToDefaultValues)
|
||||
// .getOrThrow();
|
||||
|
||||
final Configuration newConfig = this.configurationDAO
|
||||
.saveToHistory(modelId)
|
||||
.flatMap(this.configurationDAO::restoreToDefaultValues)
|
||||
.getFollowupConfiguration(modelId)
|
||||
.getOrThrow();
|
||||
|
||||
final Result<Configuration> doImport = doImport(password, request, newConfig);
|
||||
if (doImport.hasError()) {
|
||||
|
||||
// rollback of the existing values
|
||||
this.configurationDAO.undo(newConfig.configurationNodeId);
|
||||
|
||||
}
|
||||
|
||||
return doImport
|
||||
|
|
|
@ -803,6 +803,10 @@ sebserver.examconfig.action.import-file-select=Import From File
|
|||
sebserver.examconfig.action.import-file-password=Password
|
||||
sebserver.examconfig.action.import-config.confirm=Exam Configuration successfully imported
|
||||
sebserver.examconfig.action.import.missing-password=Missing Password: The chosen exam configuration is password-protected.<br/><br/>Please choose it again and provide the correct password within the password field.
|
||||
sebserver.examconfig.action.import.config.text=To import a valid SEB settings configuration file (.seb) as whole new exam configuration for SEB Server,<br/>please select the file from the local directory and provide a password if the file is protected.<br/>Please also give a name for the new exam configuration.
|
||||
sebserver.examconfig.action.import.settings.text=To import a valid SEB settings configuration file (.seb) into an existing exam configuration,<br/>please select the file from the local directory and provide a password if the file is protected.<br/>Please note that this import will override the existing SEB settings of this exam configuration<br/>and also try to publish the changes if selected.
|
||||
sebserver.examconfig.action.import.auto-publish=Publish
|
||||
sebserver.examconfig.action.import.auto-publish.tooltip=Try to automatically publish the imported changes
|
||||
sebserver.examconfig.action.state-change.confirm=This configuration is already attached to an exam.<br/>Please note that changing an attached configuration will take effect on the exam when the configuration changes are saved<br/><br/>Are you sure to change this configuration to an editable state?
|
||||
sebserver.examconfig.message.error.file=Please select a valid SEB Exam Configuration File
|
||||
|
||||
|
|
Loading…
Reference in a new issue