SEBSERV-435 implementation and testing for Exam. TODO Monitoring

This commit is contained in:
anhefti 2023-09-28 15:49:26 +02:00
parent fb74fb1804
commit 60bdb38c7b
34 changed files with 1404 additions and 509 deletions

View file

@ -22,4 +22,6 @@ public interface FeatureService {
boolean isEnabled(CollectingStrategy collectingRoomStrategy);
boolean isScreenProcteringEnabled();
}

View file

@ -59,4 +59,12 @@ public class FeatureServiceImpl implements FeatureService {
return key.replaceAll("_", "-");
}
@Override
public boolean isScreenProcteringEnabled() {
return this.environment.getProperty(toConfigName(
FEATURE_SETTINGS_PREFIX + "seb.screenProctoring"),
Boolean.class,
Boolean.FALSE);
}
}

View file

@ -90,6 +90,10 @@ public class ClientCredentialServiceImpl implements ClientCredentialService {
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~`!@#$%^*()-_=+[{]}?"
.toCharArray();
private final static char[] possibleLess =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
.toCharArray();
public static CharSequence generateClientId() {
return RandomStringUtils.random(
16, 0, possibleCharacters.length - 1, false, false,
@ -102,6 +106,12 @@ public class ClientCredentialServiceImpl implements ClientCredentialService {
possibleCharacters, new SecureRandom());
}
public static CharSequence generateClientSecretLess() {
return RandomStringUtils.random(
16, 0, possibleLess.length - 1, false, false,
possibleLess, new SecureRandom());
}
public static void clearChars(final CharSequence sequence) {
if (sequence == null) {
return;

View file

@ -343,6 +343,16 @@ public enum ActionDefinition {
ImageIcon.VISIBILITY_OFF,
PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.EXAM_SECURITY),
SCREEN_PROCTORING_ON(
new LocTextKey("sebserver.exam.sps.actions.open"),
ImageIcon.SCREEN_PROC_ON,
PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.EXAM_SECURITY),
SCREEN_PROCTORING_OFF(
new LocTextKey("sebserver.exam.sps.actions.open"),
ImageIcon.SCREEN_PROC_OFF,
PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.EXAM_SECURITY),
EXAM_CONFIGURATION_NEW(
new LocTextKey("sebserver.exam.configuration.action.list.new"),

View file

@ -27,6 +27,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.FeatureService;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
@ -38,6 +39,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamTemplate;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult.ErrorType;
@ -66,6 +68,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckExamCon
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckSEBRestriction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetScreenProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetDefaultExamTemplate;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetExamTemplate;
@ -149,6 +152,7 @@ public class ExamForm implements TemplateComposer {
private final ResourceService resourceService;
private final ExamSEBRestrictionSettings examSEBRestrictionSettings;
private final ProctoringSettingsPopup proctoringSettingsPopup;
private final ScreenProctoringSettingsPopup screenProctoringSettingsPopup;
private final WidgetFactory widgetFactory;
private final RestService restService;
private final ExamDeletePopup examDeletePopup;
@ -156,22 +160,26 @@ public class ExamForm implements TemplateComposer {
private final ExamIndicatorsList examIndicatorsList;
private final ExamClientGroupList examClientGroupList;
private final ExamCreateClientConfigPopup examCreateClientConfigPopup;
private final FeatureService featureService;
protected ExamForm(
final PageService pageService,
final ExamSEBRestrictionSettings examSEBRestrictionSettings,
final ProctoringSettingsPopup proctoringSettingsPopup,
final ScreenProctoringSettingsPopup screenProctoringSettingsPopup,
final ExamToConfigBindingForm examToConfigBindingForm,
final DownloadService downloadService,
final ExamDeletePopup examDeletePopup,
final ExamFormConfigs examFormConfigs,
final ExamIndicatorsList examIndicatorsList,
final ExamClientGroupList examClientGroupList,
final ExamCreateClientConfigPopup examCreateClientConfigPopup) {
final ExamCreateClientConfigPopup examCreateClientConfigPopup,
final FeatureService featureService) {
this.pageService = pageService;
this.resourceService = pageService.getResourceService();
this.examSEBRestrictionSettings = examSEBRestrictionSettings;
this.screenProctoringSettingsPopup = screenProctoringSettingsPopup;
this.proctoringSettingsPopup = proctoringSettingsPopup;
this.widgetFactory = pageService.getWidgetFactory();
this.restService = this.resourceService.getRestService();
@ -180,6 +188,7 @@ public class ExamForm implements TemplateComposer {
this.examIndicatorsList = examIndicatorsList;
this.examClientGroupList = examClientGroupList;
this.examCreateClientConfigPopup = examCreateClientConfigPopup;
this.featureService = featureService;
this.consistencyMessageMapping = new HashMap<>();
this.consistencyMessageMapping.put(
@ -410,6 +419,13 @@ public class ExamForm implements TemplateComposer {
.map(ProctoringServiceSettings::getEnableProctoring)
.getOr(false);
final boolean screenProctoringEnabled = importFromQuizData ? false : this.restService
.getBuilder(GetScreenProctoringSettings.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.map(ScreenProctoringSettings::getEnableScreenProctoring)
.getOr(false);
final PageActionBuilder actionBuilder = this.pageService.pageActionBuilder(formContext
.clearEntityKeys()
.removeAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA));
@ -491,7 +507,24 @@ public class ExamForm implements TemplateComposer {
.withEntityKey(entityKey)
.withExec(this.proctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable))
.noEventPropagation()
.publishIf(() -> !proctoringEnabled && readonly);
.publishIf(() -> !proctoringEnabled && readonly)
.newAction(ActionDefinition.SCREEN_PROCTORING_ON)
.withEntityKey(entityKey)
.withExec(
this.screenProctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable))
.noEventPropagation()
.publishIf(() -> this.featureService.isScreenProcteringEnabled() && screenProctoringEnabled && readonly)
.newAction(ActionDefinition.SCREEN_PROCTORING_OFF)
.withEntityKey(entityKey)
.withExec(
this.screenProctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable))
.noEventPropagation()
.publishIf(
() -> this.featureService.isScreenProcteringEnabled() && !screenProctoringEnabled && readonly)
;
// additional data in read-only view
if (readonly && !importFromQuizData) {

View file

@ -486,8 +486,11 @@ public class ProctoringSettingsPopup {
}
private FormBuilder buildHeader(final ProctoringServiceSettings proctoringSettings,
final FormHandleAnchor formHandleAnchor, final boolean isReadonly) {
private FormBuilder buildHeader(
final ProctoringServiceSettings proctoringSettings,
final FormHandleAnchor formHandleAnchor,
final boolean isReadonly) {
final FormBuilder formBuilder = this.pageService.formBuilder(
formHandleAnchor.formContext)
.withDefaultSpanInput(5)

View file

@ -8,19 +8,297 @@
package ch.ethz.seb.sebserver.gui.content.exam;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.RowData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
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.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.CollectingStrategy;
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.form.Form;
import ch.ethz.seb.sebserver.gui.form.FormBuilder;
import ch.ethz.seb.sebserver.gui.form.FormHandle;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.ModalInputDialogComposer;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.event.ActionEvent;
import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetScreenProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveScreenProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetExamTemplateScreenProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.SaveExamTemplateScreenProctoringSettings;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
@Lazy
@Component
@GuiProfile
public class ScreenProctoringSettingsPopup {
private final static LocTextKey SEB_PROCTORING_FORM_APPKEY_SPS =
new LocTextKey("sebserver.exam.proctoring.form.appkey.sps");
private final static LocTextKey SEB_PROCTORING_FORM_SECRET_SPS =
new LocTextKey("sebserver.exam.proctoring.form.appsecret.sps");
private final static LocTextKey SEB_PROCTORING_FORM_ACCOUNT_ID_SPS =
new LocTextKey("sebserver.exam.proctoring.form.accountId.sps");
private final static LocTextKey SEB_PROCTORING_FORM_ACCOUNT_SECRET_SPS =
new LocTextKey("sebserver.exam.proctoring.form.accountSecret.sps");
private final static LocTextKey SEB_PROCTORING_FORM_COLLECT_STRATEGY =
new LocTextKey("sebserver.exam.proctoring.form.collect.strategy");
private static final Logger log = LoggerFactory.getLogger(ScreenProctoringSettingsPopup.class);
private final static LocTextKey FORM_TITLE =
new LocTextKey("sebserver.exam.sps.form.title");
private final static LocTextKey FORM_INFO_TITLE =
new LocTextKey("sebserver.exam.sps.form.info.title");
private final static LocTextKey FORM_INFO =
new LocTextKey("sebserver.exam.sps.form.info");
private final static LocTextKey FORM_ENABLE =
new LocTextKey("sebserver.exam.sps.form.enable");
private final static LocTextKey FORM_URL =
new LocTextKey("sebserver.exam.sps.form.url");
private final static LocTextKey FORM_APPKEY_SPS =
new LocTextKey("sebserver.exam.sps.form.appkey");
private final static LocTextKey FORM_APPSECRET_SPS =
new LocTextKey("sebserver.exam.sps.form.appsecret");
private final static LocTextKey FORM_ACCOUNT_ID_SPS =
new LocTextKey("sebserver.exam.sps.form.accountId");
private final static LocTextKey FORM_ACCOUNT_SECRET_SPS =
new LocTextKey("sebserver.exam.sps.form.accountSecret");
private final static LocTextKey FORM_COLLECT_STRATEGY =
new LocTextKey("sebserver.exam.sps.form.collect.strategy");
private final static LocTextKey SAVE_TEXT_KEY =
new LocTextKey("sebserver.exam.sps.form.saveSettings");
Function<PageAction, PageAction> settingsFunction(final PageService pageService, final boolean modifyGrant) {
return action -> {
final PageContext pageContext = action.pageContext()
.withAttribute(
PageContext.AttributeKeys.FORCE_READ_ONLY,
(modifyGrant) ? Constants.FALSE_STRING : Constants.TRUE_STRING);
final ModalInputDialog<FormHandle<?>> dialog =
new ModalInputDialog<FormHandle<?>>(
action.pageContext().getParent().getShell(),
pageService.getWidgetFactory())
.setDialogWidth(860);
if (modifyGrant) {
final BiConsumer<Composite, Supplier<FormHandle<?>>> actionComposer = (composite, handle) -> {
final WidgetFactory widgetFactory = pageService.getWidgetFactory();
final Button save = widgetFactory.buttonLocalized(composite, SAVE_TEXT_KEY);
save.setLayoutData(new RowData());
save.addListener(SWT.Selection, event -> {
if (doSaveSettings(pageService, pageContext, handle.get())) {
dialog.close();
}
});
};
final ScreenProctoringPropertiesForm bindFormContext = new ScreenProctoringPropertiesForm(
pageService,
pageContext);
dialog.openWithActions(
FORM_TITLE,
actionComposer,
Utils.EMPTY_EXECUTION,
bindFormContext);
} else {
dialog.open(
FORM_TITLE,
pageContext,
pc -> new ScreenProctoringPropertiesForm(
pageService,
pageContext)
.compose(pc.getParent()));
}
return action;
};
}
private boolean doSaveSettings(
final PageService pageService,
final PageContext pageContext,
final FormHandle<?> formHandle) {
final boolean isReadonly = BooleanUtils.toBoolean(
pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY));
if (isReadonly) {
return true;
}
final EntityKey entityKey = pageContext.getEntityKey();
ScreenProctoringSettings settings = null;
try {
final Form form = formHandle.getForm();
form.clearErrors();
final boolean enabled = BooleanUtils.toBoolean(
form.getFieldValue(ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING));
final String groupSizeString = form.getFieldValue(ScreenProctoringSettings.ATTR_COLLECTING_GROUP_SIZE);
final int groupSize = StringUtils.isNotBlank(groupSizeString) ? Integer.parseInt(groupSizeString) : 0;
settings = new ScreenProctoringSettings(
Long.parseLong(entityKey.modelId),
enabled,
form.getFieldValue(ScreenProctoringSettings.ATTR_SPS_SERVICE_URL),
form.getFieldValue(ScreenProctoringSettings.ATTR_SPS_API_KEY),
form.getFieldValue(ScreenProctoringSettings.ATTR_SPS_API_SECRET),
form.getFieldValue(ScreenProctoringSettings.ATTR_SPS_ACCOUNT_ID),
form.getFieldValue(ScreenProctoringSettings.ATTR_SPS_ACCOUNT_PASSWORD),
CollectingStrategy.EXAM,
groupSize);
} catch (final Exception e) {
log.error("Unexpected error while trying to get settings from form: ", e);
}
if (settings == null) {
return false;
}
final Result<ScreenProctoringSettings> saveRequest = pageService
.getRestService()
.getBuilder(
entityKey.entityType == EntityType.EXAM
? SaveScreenProctoringSettings.class
: SaveExamTemplateScreenProctoringSettings.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.withBody(settings)
.call();
final boolean saveOk = !saveRequest
.onError(formHandle::handleError)
.hasError();
if (saveOk) {
final PageAction action = pageService.pageActionBuilder(pageContext)
.newAction(
entityKey.entityType == EntityType.EXAM
? ActionDefinition.EXAM_VIEW_FROM_LIST
: ActionDefinition.EXAM_TEMPLATE_VIEW_FROM_LIST)
.create();
pageService.firePageEvent(
new ActionEvent(action),
action.pageContext());
return true;
}
return false;
}
private final class ScreenProctoringPropertiesForm
implements ModalInputDialogComposer<FormHandle<?>> {
private final PageService pageService;
private final PageContext pageContext;
protected ScreenProctoringPropertiesForm(
final PageService pageService,
final PageContext pageContext) {
this.pageService = pageService;
this.pageContext = pageContext;
}
@Override
public Supplier<FormHandle<?>> compose(final Composite parent) {
final RestService restService = this.pageService.getRestService();
final EntityKey entityKey = this.pageContext.getEntityKey();
final Composite content = this.pageService
.getWidgetFactory()
.createPopupScrollComposite(parent);
final PageContext formContext = this.pageContext
.copyOf(content)
.clearEntityKeys();
final ScreenProctoringSettings settings = restService
.getBuilder(
entityKey.entityType == EntityType.EXAM
? GetScreenProctoringSettings.class
: GetExamTemplateScreenProctoringSettings.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.getOrThrow();
final boolean isReadonly = BooleanUtils.toBoolean(
this.pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY));
final FormHandle<Entity> form = this.pageService.formBuilder(formContext)
.withDefaultSpanInput(5)
.withEmptyCellSeparation(true)
.withDefaultSpanEmptyCell(1)
.readonly(isReadonly)
.addField(FormBuilder.text(
"Info",
FORM_INFO_TITLE,
this.pageService.getI18nSupport().getText(FORM_INFO))
.asArea(80)
.asHTML()
.readonly(true))
.addField(FormBuilder.checkbox(
ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING,
FORM_ENABLE,
String.valueOf(settings.enableScreenProctoring)))
.addField(FormBuilder.text(
ScreenProctoringSettings.ATTR_SPS_SERVICE_URL,
FORM_URL,
settings.spsServiceURL)
.mandatory())
.addField(FormBuilder.text(
ScreenProctoringSettings.ATTR_SPS_API_KEY,
FORM_APPKEY_SPS,
settings.spsAPIKey))
.withEmptyCellSeparation(false)
.addField(FormBuilder.password(
ScreenProctoringSettings.ATTR_SPS_API_SECRET,
FORM_APPSECRET_SPS,
(settings.spsAPISecret != null)
? String.valueOf(settings.spsAPISecret)
: null))
.addField(FormBuilder.text(
ScreenProctoringSettings.ATTR_SPS_ACCOUNT_ID,
FORM_ACCOUNT_ID_SPS,
settings.spsAccountId))
.withEmptyCellSeparation(false)
.addField(FormBuilder.password(
ScreenProctoringSettings.ATTR_SPS_ACCOUNT_PASSWORD,
FORM_ACCOUNT_SECRET_SPS,
(settings.spsAccountPassword != null)
? String.valueOf(settings.spsAccountPassword)
: null))
.build();
return () -> form;
}
}
}

View file

@ -0,0 +1,42 @@
/*
* 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.remote.webservice.api.exam;
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.exam.ScreenProctoringSettings;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class GetScreenProctoringSettings extends RestCall<ScreenProctoringSettings> {
public GetScreenProctoringSettings() {
super(new TypeKey<>(
CallType.GET_SINGLE,
EntityType.EXAM_PROCTOR_DATA,
new TypeReference<ScreenProctoringSettings>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_JSON,
API.EXAM_ADMINISTRATION_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_SCREEN_PROCTORING_PATH_SEGMENT);
}
}

View file

@ -0,0 +1,42 @@
/*
* 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.remote.webservice.api.exam;
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.exam.ScreenProctoringSettings;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class SaveScreenProctoringSettings extends RestCall<ScreenProctoringSettings> {
public SaveScreenProctoringSettings() {
super(new TypeKey<>(
CallType.SAVE,
EntityType.EXAM_PROCTOR_DATA,
new TypeReference<ScreenProctoringSettings>() {
}),
HttpMethod.POST,
MediaType.APPLICATION_JSON,
API.EXAM_ADMINISTRATION_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_SCREEN_PROCTORING_PATH_SEGMENT);
}
}

View file

@ -0,0 +1,42 @@
/*
* 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.remote.webservice.api.exam.template;
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.exam.ScreenProctoringSettings;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class GetExamTemplateScreenProctoringSettings extends RestCall<ScreenProctoringSettings> {
public GetExamTemplateScreenProctoringSettings() {
super(new TypeKey<>(
CallType.GET_SINGLE,
EntityType.EXAM_PROCTOR_DATA,
new TypeReference<ScreenProctoringSettings>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_JSON,
API.EXAM_TEMPLATE_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_SCREEN_PROCTORING_PATH_SEGMENT);
}
}

View file

@ -0,0 +1,42 @@
/*
* 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.remote.webservice.api.exam.template;
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.exam.ScreenProctoringSettings;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class SaveExamTemplateScreenProctoringSettings extends RestCall<ScreenProctoringSettings> {
public SaveExamTemplateScreenProctoringSettings() {
super(new TypeKey<>(
CallType.SAVE,
EntityType.EXAM_PROCTOR_DATA,
new TypeReference<ScreenProctoringSettings>() {
}),
HttpMethod.POST,
MediaType.APPLICATION_JSON,
API.EXAM_TEMPLATE_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_SCREEN_PROCTORING_PATH_SEGMENT);
}
}

View file

@ -148,7 +148,9 @@ public class WidgetFactory {
VERIFY("verify.png"),
SHIELD("shield.png"),
NO_SHIELD("no_shield.png"),
BACK("back.png");
BACK("back.png"),
SCREEN_PROC_ON("screen_proc_on.png"),
SCREEN_PROC_OFF("screen_proc_off.png");
public String fileName;
private ImageData image = null;

View file

@ -211,6 +211,16 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
key = "#examId")
Result<QuizData> updateQuizData(Long examId, QuizData quizData, String updateId);
/** Marks the given exam by setting the last-update-time to current time.
* This provokes a cache update on distributed setup.
* Additionally this flushes the local cache immediately.
*
* @param examId the Exam identifier */
@CacheEvict(
cacheNames = ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM,
key = "#examId")
void markUpdate(Long examId);
/** This is used by the internal update process to mark exams for which the LMS related data availability
*
* @param externalQuizId The exams external UUID or quiz id of the exam to mark

View file

@ -27,4 +27,8 @@ public interface ProctoringSettingsDAO {
final EntityKey entityKey,
final ScreenProctoringSettings screenProctoringSettings);
void disableScreenProctoring(Long examId);
boolean isScreenProctoringEnabled(Long examId);
}

View file

@ -238,7 +238,7 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
.build()
.execute();
final List<ClientConnectionRecord> execute = this.clientConnectionRecordMapper
this.clientConnectionRecordMapper
.selectByExample()
.build()
.execute();
@ -340,8 +340,6 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
final long millisecondsNow = Utils.getMillisecondsNow();
// NOTE: we use nanoseconds here to get a better precision to better avoid
// same value of real concurrent calls on distributed systems
// TODO: Better solution for the future would be to count this value and
// isolation is done via DB transaction
final long nanosecondsNow = System.nanoTime();
final ClientConnectionRecord newRecord = new ClientConnectionRecord(
null,

View file

@ -159,6 +159,28 @@ public class ExamDAOImpl implements ExamDAO {
.map(rec -> saveAdditionalQuizAttributes(examId, quizData));
}
@Override
@Transactional
public void markUpdate(final Long examId) {
try {
final long millisecondsNow = Utils.getMillisecondsNow();
UpdateDSL.updateWithMapper(
this.examRecordMapper::update,
ExamRecordDynamicSqlSupport.examRecord)
.set(ExamRecordDynamicSqlSupport.lastModified)
.equalTo(millisecondsNow)
.where(ExamRecordDynamicSqlSupport.id, isEqualTo(examId))
.build()
.execute();
} catch (final Exception e) {
log.error("Failed to mark exam for update on distributed setup. exam: {}", examId, e);
}
}
@Override
public void markLMSAvailability(final String externalQuizId, final boolean available, final String updateId) {

View file

@ -235,6 +235,30 @@ public class ProctoringSettingsDAOImpl implements ProctoringSettingsDAO {
});
}
@Override
@Transactional
public void disableScreenProctoring(final Long examId) {
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.EXAM,
examId,
ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING,
Constants.FALSE_STRING)
.onError(error -> log.warn(
"Failed to disable screen proctoring for exam: {} error: {}",
examId,
error.getMessage()));
}
@Override
@Transactional(readOnly = true)
public boolean isScreenProctoringEnabled(final Long examId) {
return this.additionalAttributesDAO.getAdditionalAttribute(EntityType.EXAM,
examId,
ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING)
.map(attrRec -> BooleanUtils.toBoolean(attrRec.getValue()))
.getOr(false);
}
private Boolean getEnabled(final Map<String, AdditionalAttributeRecord> mapping) {
if (mapping.containsKey(ProctoringServiceSettings.ATTR_ENABLE_PROCTORING)) {
return BooleanUtils.toBoolean(mapping.get(ProctoringServiceSettings.ATTR_ENABLE_PROCTORING).getValue());

View file

@ -136,6 +136,11 @@ public interface ExamAdminService {
* @return Result refer to the archived exam or to an error when happened */
Result<Exam> archiveExam(Exam exam);
/** Gets invoked after an exam has been changed and saved.
*
* @param exam the exam that has been changed and saved */
void notifyExamSaved(Exam exam);
/** Used to check threshold consistency for a given list of thresholds.
* Checks if all values are present (none null value)
* Checks if there are duplicates

View file

@ -20,6 +20,7 @@ import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
@ -70,6 +71,11 @@ public interface ProctoringAdminService {
* @return ExamProctoringService instance */
Result<RemoteProctoringService> getExamProctoringService(final ProctoringServerType type);
/** Gets invoked after an exam has been changed and saved.
*
* @param exam the exam that has been changed and saved */
void notifyExamSaved(Exam exam);
/** Use this to test the proctoring service settings against the remote proctoring server.
*
* @param proctoringSettings the settings to test

View file

@ -299,6 +299,12 @@ public class ExamAdminServiceImpl implements ExamAdminService {
.map(settings -> exam);
}
@Override
public void notifyExamSaved(final Exam exam) {
updateAdditionalExamConfigAttributes(exam.id);
this.proctoringAdminService.notifyExamSaved(exam);
}
private Result<Exam> initAdditionalAttributesForMoodleExams(final Exam exam) {
return Result.tryCatch(() -> {
final LmsAPITemplate lmsTemplate = this.lmsAPIService

View file

@ -13,6 +13,7 @@ import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
@ -92,8 +93,6 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
checkType(parentEntityKey);
final boolean notifyChangesToService = notifyChangesToService(parentEntityKey, screenProctoringSettings);
this.screenProctoringService
.testSettings(screenProctoringSettings)
.flatMap(settings -> this.proctoringSettingsDAO.storeScreenProctoringSettings(
@ -101,9 +100,12 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
screenProctoringSettings))
.getOrThrow();
if (notifyChangesToService) {
if (parentEntityKey.entityType == EntityType.EXAM) {
this.screenProctoringService
.applyScreenProctoingForExam(screenProctoringSettings)
.applyScreenProctoingForExam(screenProctoringSettings.examId)
.onError(error -> this.proctoringSettingsDAO
.disableScreenProctoring(screenProctoringSettings.examId))
.getOrThrow();
}
@ -111,26 +113,17 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
});
}
private boolean notifyChangesToService(
final EntityKey entityKey,
final ScreenProctoringSettings newSettings) {
if (entityKey.entityType != EntityType.EXAM) {
return false;
}
return this.proctoringSettingsDAO
.getScreenProctoringSettings(entityKey)
.map(oldSettings -> oldSettings.enableScreenProctoring != newSettings.enableScreenProctoring)
.getOr(true);
}
@Override
public Result<RemoteProctoringService> getExamProctoringService(final ProctoringServerType type) {
return this.remoteProctoringServiceFactory
.getExamProctoringService(type);
}
@Override
public void notifyExamSaved(final Exam exam) {
this.screenProctoringService.notifyExamSaved(exam);
}
private void checkType(final EntityKey parentEntityKey) {
if (!SUPPORTED_PARENT_ENTITES.contains(parentEntityKey.entityType)) {
throw new UnsupportedOperationException(

View file

@ -40,6 +40,11 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
public class MockCourseAccessAPI implements CourseAccessAPI {
private static final String startTime10 = DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS)
.toString(Constants.DEFAULT_DATE_TIME_FORMAT);
private static final String endTime10 = DateTime.now(DateTimeZone.UTC).plus(6 * Constants.MINUTE_IN_MILLIS)
.toString(Constants.DEFAULT_DATE_TIME_FORMAT);
private final Collection<QuizData> mockups;
private final WebserviceInfo webserviceInfo;
private final APITemplateDataSupplier apiTemplateDataSupplier;
@ -85,10 +90,8 @@ public class MockCourseAccessAPI implements CourseAccessAPI {
this.mockups.add(new QuizData(
"quiz10", institutionId, lmsSetupId, lmsType, "Demo Quiz 10 (MOCKUP)",
"Starts in a minute and ends after five minutes",
DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS)
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
DateTime.now(DateTimeZone.UTC).plus(6 * Constants.MINUTE_IN_MILLIS)
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
MockCourseAccessAPI.startTime10,
MockCourseAccessAPI.endTime10,
"http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz11", institutionId, lmsSetupId, lmsType, "Demo Quiz 11 (MOCKUP)",

View file

@ -34,17 +34,26 @@ public interface ScreenProctoringService extends SessionUpdateTask {
* @return Result refer to the settings or to an error when happened */
Result<ScreenProctoringSettings> testSettings(ScreenProctoringSettings settings);
//Result<ScreenProctoringSettings> saveSettingsForExam(ScreenProctoringSettings settings);
/** This applies the screen proctoring for the given exam.
/** This applies the stored screen proctoring for the given exam.
* If screen proctoring for the exam is enabled, this initializes or re-activate all
* needed things for screen proctoring for the given exam.
* If screen proctoring is set to disable, this disables the whole service for the
* given exam also on SPS side.
*
* @param settings the actual ScreenProctoringSettings of the exam
* @return Result refer to the given ScreenProctoringSettings or to an error when happened */
Result<ScreenProctoringSettings> applyScreenProctoingForExam(ScreenProctoringSettings settings);
* @param examId use the screen proctoring settings of the exam with the given exam id
* @return Result refer to the given Exam or to an error when happened */
Result<Exam> applyScreenProctoingForExam(Long examId);
/** Gets invoked after an exam has been changed and saved.
*
* @param exam the exam that has been changed and saved */
void notifyExamSaved(Exam exam);
@EventListener(ExamStartedEvent.class)
void notifyExamStarted(ExamStartedEvent event);
@EventListener(ExamFinishedEvent.class)
void notifyExamFinished(ExamFinishedEvent event);
/** This is been called just before an Exam gets deleted on the permanent storage.
* This deactivates and dispose or deletes all exam relevant domain entities on the SPS service side.

View file

@ -18,7 +18,6 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
@ -52,7 +51,6 @@ public class ExamSessionCacheService {
private final ClientConnectionDAO clientConnectionDAO;
private final InternalClientConnectionDataFactory internalClientConnectionDataFactory;
private final ExamConfigService sebExamConfigService;
private final ExamUpdateHandler examUpdateHandler;
protected ExamSessionCacheService(
final ExamDAO examDAO,
@ -60,7 +58,6 @@ public class ExamSessionCacheService {
final ClientConnectionDAO clientConnectionDAO,
final InternalClientConnectionDataFactory internalClientConnectionDataFactory,
final ExamConfigService sebExamConfigService,
final ExamUpdateHandler examUpdateHandler,
final RemoteProctoringRoomDAO remoteProctoringRoomDAO) {
this.examDAO = examDAO;
@ -68,7 +65,6 @@ public class ExamSessionCacheService {
this.clientConnectionDAO = clientConnectionDAO;
this.internalClientConnectionDataFactory = internalClientConnectionDataFactory;
this.sebExamConfigService = sebExamConfigService;
this.examUpdateHandler = examUpdateHandler;
}
@Cacheable(
@ -132,9 +128,11 @@ public class ExamSessionCacheService {
}
case UP_COMING:
case FINISHED: {
return this.examUpdateHandler.updateRunning(exam.id)
.map(e -> e.status == ExamStatus.RUNNING)
.getOr(false);
return false;
// TODO do we really need to double-check here?
// return this.examUpdateHandler.updateRunning(exam.id)
// .map(e -> e.status == ExamStatus.RUNNING)
// .getOr(false);
}
default: {
return false;

View file

@ -532,6 +532,9 @@ public class ExamSessionServiceImpl implements ExamSessionService {
@Override
public Result<Exam> updateExamCache(final Long examId) {
// TODO check how often this is called in distributed environments
System.out.println("************** performance check: updateExamCache");
try {
final Cache cache = this.cacheManager.getCache(ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM);
final ValueWrapper valueWrapper = cache.get(examId);
@ -547,7 +550,8 @@ public class ExamSessionServiceImpl implements ExamSessionService {
return Result.ofEmpty();
}
final Boolean isUpToDate = this.examDAO.upToDate(exam)
final Boolean isUpToDate = this.examDAO
.upToDate(exam)
.onError(t -> log.error("Failed to verify if cached exam is up to date: {}", exam, t))
.getOr(false);

View file

@ -0,0 +1,24 @@
/*
* 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.session.impl;
import org.springframework.context.ApplicationEvent;
public class ExamUpdateEvent extends ApplicationEvent {
private static final long serialVersionUID = -367779249954953708L;
public final Long examId;
public ExamUpdateEvent(final Long examId) {
super(examId);
this.examId = examId;
}
}

View file

@ -22,6 +22,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
@ -216,18 +217,16 @@ class ExamUpdateHandler implements ExamUpdateTask {
});
}
Result<Exam> updateRunning(final Long examId) {
return this.examDAO.byPK(examId)
.map(exam -> {
final DateTime now = DateTime.now(DateTimeZone.UTC);
if (exam.getStatus() == ExamStatus.UP_COMING
&& exam.endTime.plus(this.examTimeSuffix).isBefore(now)) {
return setRunning(exam, this.createUpdateId())
.getOr(exam);
} else {
return exam;
}
});
@EventListener(ExamUpdateEvent.class)
void updateRunning(final ExamUpdateEvent event) {
this.examDAO
.byPK(event.examId)
.onSuccess(exam -> updateState(
exam,
DateTime.now(DateTimeZone.UTC),
this.examTimePrefix,
this.examTimeSuffix,
this.createUpdateId()));
}
void updateState(

View file

@ -24,12 +24,9 @@ import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.CollectingStrategy;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
@ -42,13 +39,17 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRe
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ProctoringSettingsDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ScreenProctoringGroupDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ExamDeletionEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ProctoringSettingsDAOImpl;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ScreenProctoringGroupDAOImpl.AllGroupsFullException;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ExamSessionCacheService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.ScreenProctoringAPIBinding.SPSData;
@Lazy
@Service
@ -58,16 +59,18 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
private static final Logger log = LoggerFactory.getLogger(ScreenProctoringServiceImpl.class);
private final Cryptor cryptor;
private final JSONMapper jsonMapper;
private final ScreenProctoringAPIBinding screenProctoringAPIBinding;
private final ScreenProctoringGroupDAO screenProctoringGroupDAO;
private final ProctoringSettingsDAO proctoringSettingsDAO;
private final ClientConnectionDAO clientConnectionDAO;
private final ExamDAO examDAO;
private final ProctoringSettingsDAOImpl proctoringSettingsSupplier;
private final SEBClientInstructionService sebInstructionService;
private final ExamSessionCacheService examSessionCacheService;
public ScreenProctoringServiceImpl(
final Cryptor cryptor,
final ProctoringSettingsDAOImpl proctoringSettingsSupplier,
final ProctoringSettingsDAO proctoringSettingsDAO,
final AsyncService asyncService,
final JSONMapper jsonMapper,
final UserDAO userDAO,
@ -75,21 +78,24 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
final ClientConnectionDAO clientConnectionDAO,
final AdditionalAttributesDAO additionalAttributesDAO,
final ScreenProctoringGroupDAO screenProctoringGroupDAO,
final SEBClientInstructionService sebInstructionService) {
final SEBClientInstructionService sebInstructionService,
final ExamSessionCacheService examSessionCacheService) {
this.cryptor = cryptor;
this.jsonMapper = jsonMapper;
this.examDAO = examDAO;
this.proctoringSettingsSupplier = proctoringSettingsSupplier;
this.screenProctoringGroupDAO = screenProctoringGroupDAO;
this.clientConnectionDAO = clientConnectionDAO;
this.sebInstructionService = sebInstructionService;
this.examSessionCacheService = examSessionCacheService;
this.proctoringSettingsDAO = proctoringSettingsDAO;
this.screenProctoringAPIBinding = new ScreenProctoringAPIBinding(
userDAO,
cryptor,
asyncService,
jsonMapper,
proctoringSettingsSupplier,
proctoringSettingsDAO,
additionalAttributesDAO);
}
@ -97,14 +103,19 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
public Result<ScreenProctoringSettings> testSettings(final ScreenProctoringSettings screenProctoringSettings) {
return Result.tryCatch(() -> {
final Collection<APIMessage> fieldChecks = new ArrayList<>();
if (StringUtils.isBlank(screenProctoringSettings.spsServiceURL)) {
fieldChecks.add(APIMessage.fieldValidationError(
"appKey",
"screenProctoringSettings:spsServiceURL:notNull"));
}
if (screenProctoringSettings.spsServiceURL != null
&& screenProctoringSettings.spsServiceURL.contains("?")) {
throw new FieldValidationException(
"serverURL",
"screenProctoringSettings:spsServiceURL:invalidURL");
fieldChecks.add(APIMessage.fieldValidationError(
"appKey",
"screenProctoringSettings:spsServiceURL:invalidURL"));
}
final Collection<APIMessage> fieldChecks = new ArrayList<>();
if (StringUtils.isBlank(screenProctoringSettings.spsAPIKey)) {
fieldChecks.add(APIMessage.fieldValidationError(
"appKey",
@ -150,27 +161,41 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
}
@Override
public Result<ScreenProctoringSettings> applyScreenProctoingForExam(final ScreenProctoringSettings settings) {
public Result<Exam> applyScreenProctoingForExam(final Long examId) {
return this.examDAO
.byPK(settings.examId)
.byPK(examId)
.map(exam -> {
if (BooleanUtils.toBoolean(settings.enableScreenProctoring)) {
final boolean isSPSActive = this.screenProctoringAPIBinding.isSPSActive(exam);
final boolean isEnabling = this.proctoringSettingsDAO.isScreenProctoringEnabled(examId);
if (isEnabling && !isSPSActive) {
this.screenProctoringAPIBinding
.startScreenProctoring(exam)
.onError(error -> log.error(
"Failed to apply screen proctoring for exam: {}",
exam,
error))
.getOrThrow()
.stream()
.forEach(newGroup -> createNewLocalGroup(exam, newGroup));
} else {
this.examDAO.markUpdate(exam.id);
} else if (!isEnabling && isSPSActive) {
this.screenProctoringAPIBinding
.dispsoseScreenProctoring(exam)
.onError(error -> log.error("Failed to dispose screen proctoring for exam: {}",
exam,
error))
.getOrThrow();
}
return settings;
this.examDAO.markUpdate(exam.id);
}
return exam;
});
}
@ -204,20 +229,51 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
public void updateClientConnections() {
try {
final Map<Long, ExamInfo> examInfoCache = new HashMap<>();
this.examDAO
.allIdsOfRunningWithScreenProctoringEnabled()
.flatMap(this.clientConnectionDAO::getAllForScreenProctoringUpdate)
.getOrThrow()
.stream()
.forEach(cc -> applyScreenProctoringSession(cc, examInfoCache));
.forEach(cc -> applyScreenProctoringSession(cc));
} catch (final Exception e) {
log.error("Failed to update active SEB connections for screen proctoring");
}
}
@Override
public void notifyExamSaved(final Exam exam) {
final String enabeld = exam.additionalAttributes
.get(ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING);
if (!BooleanUtils.toBoolean(enabeld)) {
return;
}
this.screenProctoringAPIBinding.synchronizeUserAccounts(exam);
this.screenProctoringAPIBinding.createExamReadPrivileges(exam);
}
@Override
public void notifyExamStarted(final ExamStartedEvent event) {
final Exam exam = event.exam;
if (!BooleanUtils.toBoolean(exam.additionalAttributes.get(SPSData.ATTR_SPS_ACTIVE))) {
return;
}
this.screenProctoringAPIBinding.activateSEBAccessOnSPS(exam, true);
}
@Override
public void notifyExamFinished(final ExamFinishedEvent event) {
final Exam exam = event.exam;
if (!BooleanUtils.toBoolean(exam.additionalAttributes.get(SPSData.ATTR_SPS_ACTIVE))) {
return;
}
this.screenProctoringAPIBinding.activateSEBAccessOnSPS(exam, false);
}
@Override
public void notifyExamDeletion(final ExamDeletionEvent event) {
event.ids
@ -234,86 +290,42 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
});
}
private void applyScreenProctoringSession(
final ClientConnectionRecord ccRecord,
final Map<Long, ExamInfo> examInfoCache) {
private void applyScreenProctoringSession(final ClientConnectionRecord ccRecord) {
try {
final Long examId = ccRecord.getExamId();
// process based caching...
if (!examInfoCache.containsKey(examId)) {
final ScreenProctoringSettings settings = this.proctoringSettingsSupplier
.getScreenProctoringSettings(new EntityKey(examId, EntityType.EXAM))
.getOrThrow();
final ClientCredentials sebClientCredential = this.screenProctoringAPIBinding
.getSEBClientCredentials(examId)
.getOrThrow();
examInfoCache.put(examId, new ExamInfo(settings, sebClientCredential));
}
final ExamInfo examInfo = examInfoCache.get(examId);
final Exam runningExam = this.examSessionCacheService.getRunningExam(examId);
// apply SEB connection to screen proctoring group
final ScreenProctoringGroup group = applySEBConnectionToGroup(
ccRecord,
examInfo.screenProctoringSettings);
runningExam);
// create screen proctoring session for SEB connection on SPS service
final String spsSessionToken = this.screenProctoringAPIBinding
.createSEBSession(examId, group, ccRecord);
// create instruction for SEB and add it to instruction queue for SEB connection
registerJoinInstruction(ccRecord, spsSessionToken, group, examInfo);
registerJoinInstruction(ccRecord, spsSessionToken, group, runningExam);
} catch (final Exception e) {
log.error("Failed to apply screen proctoring session to SEB with connection: ", ccRecord, e);
}
}
private void registerJoinInstruction(
final ClientConnectionRecord ccRecord,
final String spsSessionToken,
final ScreenProctoringGroup group,
final ExamInfo examInfo) {
if (log.isDebugEnabled()) {
log.debug("Register JOIN instruction for client ");
}
final Long examId = examInfo.screenProctoringSettings.examId;
final Map<String, String> attributes = new HashMap<>();
attributes.put(SERVICE_TYPE, SERVICE_TYPE_NAME);
attributes.put(METHOD, ClientInstruction.ProctoringInstructionMethod.JOIN.name());
attributes.put(URL, examInfo.screenProctoringSettings.getSpsServiceURL());
attributes.put(CLIENT_ID, examInfo.sebClientCredential.clientIdAsString());
attributes.put(CLIENT_SECRET, this.cryptor.decrypt(examInfo.sebClientCredential.secret).toString());
attributes.put(GROUP_ID, group.uuid);
attributes.put(SESSION_ID, spsSessionToken);
this.sebInstructionService
.registerInstruction(
examId,
InstructionType.SEB_PROCTORING,
attributes,
ccRecord.getConnectionToken(),
true)
.onError(error -> log.error(
"Failed to register screen proctoring join instruction for SEB connection: {}",
ccRecord,
error));
}
private ScreenProctoringGroup applySEBConnectionToGroup(
final ClientConnectionRecord ccRecord,
final ScreenProctoringSettings settings) {
final Exam exam) {
final Long examId = ccRecord.getExamId();
switch (settings.collectingStrategy) {
if (!exam.additionalAttributes.containsKey(ScreenProctoringSettings.ATTR_COLLECTING_STRATEGY)) {
log.warn("Can't verify collecting strategy for exam: {} use default group assignment.", exam.id);
return applyToDefaultGroup(ccRecord, exam);
}
final CollectingStrategy strategy = CollectingStrategy.valueOf(exam.additionalAttributes
.get(ScreenProctoringSettings.ATTR_COLLECTING_STRATEGY));
switch (strategy) {
case SEB_GROUP: {
// TODO
throw new UnsupportedOperationException("SEB_GROUP based group collection is not supported yet");
@ -321,30 +333,43 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
case EXAM:
case FIX_SIZE:
default: {
final ScreenProctoringGroup screenProctoringGroup = getProctoringGroup(settings);
this.clientConnectionDAO.assignToScreenProctoringGroup(
examId,
ccRecord.getConnectionToken(),
screenProctoringGroup.id)
.getOrThrow();
return screenProctoringGroup;
return applyToDefaultGroup(ccRecord, exam);
}
}
}
private ScreenProctoringGroup getProctoringGroup(final ScreenProctoringSettings settings) {
private ScreenProctoringGroup applyToDefaultGroup(
final ClientConnectionRecord ccRecord,
final Exam exam) {
final ScreenProctoringGroup screenProctoringGroup = getProctoringGroup(exam);
this.clientConnectionDAO.assignToScreenProctoringGroup(
exam.id,
ccRecord.getConnectionToken(),
screenProctoringGroup.id)
.getOrThrow();
return screenProctoringGroup;
}
private ScreenProctoringGroup getProctoringGroup(final Exam exam) {
int collectingGroupSize = 0;
if (exam.additionalAttributes.containsKey(ScreenProctoringSettings.ATTR_COLLECTING_GROUP_SIZE)) {
collectingGroupSize = Integer.parseInt(exam.additionalAttributes
.get(ScreenProctoringSettings.ATTR_COLLECTING_GROUP_SIZE));
}
final Result<ScreenProctoringGroup> reserve = this.screenProctoringGroupDAO
.reservePlaceInCollectingGroup(
settings.examId,
settings.collectingGroupSize != null ? settings.collectingGroupSize : 0);
exam.id,
collectingGroupSize);
ScreenProctoringGroup screenProctoringGroup = null;
if (reserve.hasError()) {
if (reserve.getError() instanceof AllGroupsFullException) {
screenProctoringGroup = applyNewGroup(settings.examId, settings.collectingGroupSize);
screenProctoringGroup = applyNewGroup(exam, collectingGroupSize);
} else {
throw new RuntimeException(
"Failed to create new screen proctoring group: ",
@ -356,11 +381,9 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
return screenProctoringGroup;
}
private ScreenProctoringGroup applyNewGroup(final Long examId, final Integer groupSize) {
private ScreenProctoringGroup applyNewGroup(final Exam exam, final Integer groupSize) {
final Exam exam = this.examDAO.byPK(examId).getOrThrow();
final String spsExamUUID = exam.getAdditionalAttribute(
ScreenProctoringAPIBinding.SPS_API.SEB_SERVER_EXAM_SETTINGS.ATTR_SPS_EXAM_UUID);
final String spsExamUUID = this.getSPSData(exam).spsExamUUID;
return this.screenProctoringGroupDAO
.getCollectingGroups(exam.id)
@ -368,14 +391,16 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
.createGroup(spsExamUUID, count.size() + 1, "Created by SEB Server", exam))
.flatMap(this.screenProctoringGroupDAO::createNewGroup)
.flatMap(group -> this.screenProctoringGroupDAO
.reservePlaceInCollectingGroup(examId, groupSize != null ? groupSize : 0))
.reservePlaceInCollectingGroup(exam.id, groupSize != null ? groupSize : 0))
.getOrThrow();
}
private Result<Exam> deleteForExam(final Long examId) {
return this.examDAO.byPK(examId)
return this.examDAO
.byPK(examId)
.flatMap(this.screenProctoringAPIBinding::deleteScreenProctoring)
.map(this::cleanupAllLocalGroups);
.map(this::cleanupAllLocalGroups)
.onError(error -> log.error("Failed to delete SPS integration for exam: {}", examId, error));
}
private Exam cleanupAllLocalGroups(final Exam exam) {
@ -401,26 +426,54 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
newGroup, error));
}
// private Exam saveSettings(final Exam exam, final ScreenProctoringSettings settings) {
//
// this.proctoringAdminService
// .saveScreenProctoringSettings(exam.getEntityKey(), settings)
// .getOrThrow();
//
// return exam;
// }
private void registerJoinInstruction(
final ClientConnectionRecord ccRecord,
final String spsSessionToken,
final ScreenProctoringGroup group,
final Exam exam) {
private static final class ExamInfo {
if (log.isDebugEnabled()) {
log.debug("Register JOIN instruction for client ");
}
final ScreenProctoringSettings screenProctoringSettings;
final ClientCredentials sebClientCredential;
final SPSData spsData = getSPSData(exam);
final String url = exam.additionalAttributes.get(ScreenProctoringSettings.ATTR_SPS_SERVICE_URL);
final Map<String, String> attributes = new HashMap<>();
public ExamInfo(
final ScreenProctoringSettings screenProctoringSettings,
final ClientCredentials sebClientCredential) {
attributes.put(SERVICE_TYPE, SERVICE_TYPE_NAME);
attributes.put(METHOD, ClientInstruction.ProctoringInstructionMethod.JOIN.name());
attributes.put(URL, url);
attributes.put(CLIENT_ID, spsData.spsSEBAccessName);
attributes.put(CLIENT_SECRET, spsData.spsSEBAccessPWD);
attributes.put(GROUP_ID, group.uuid);
attributes.put(SESSION_ID, spsSessionToken);
this.screenProctoringSettings = screenProctoringSettings;
this.sebClientCredential = sebClientCredential;
this.sebInstructionService
.registerInstruction(
exam.id,
InstructionType.SEB_PROCTORING,
attributes,
ccRecord.getConnectionToken(),
true)
.onError(error -> log.error(
"Failed to register screen proctoring join instruction for SEB connection: {}",
ccRecord,
error));
}
// TODO make this with caching if performance is not good
private SPSData getSPSData(final Exam exam) {
try {
final String dataEncrypted = exam.additionalAttributes.get(SPSData.ATTR_SPS_ACCESS_DATA);
return this.jsonMapper.readValue(
this.cryptor.decrypt(dataEncrypted).getOrThrow().toString(),
SPSData.class);
} catch (final Exception e) {
log.error("Failed to get local SPSData for exam: {}", exam);
return null;
}
}

View file

@ -85,6 +85,8 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
private static final Logger log = LoggerFactory.getLogger(ExamAdministrationController.class);
// TODO reduce dependencies here.
// Move SecurityKeyService, SEBRestrictionService RemoteProctoringRoomService into ExamAdminService
private final ExamDAO examDAO;
private final UserDAO userDAO;
private final ExamAdminService examAdminService;
@ -658,7 +660,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
@Override
protected Result<Exam> notifySaved(final Exam entity) {
return Result.tryCatch(() -> {
this.examAdminService.updateAdditionalExamConfigAttributes(entity.id);
this.examAdminService.notifyExamSaved(entity);
this.examSessionService.flushCache(entity);
return entity;
});

View file

@ -12,10 +12,10 @@ logging.level.ROOT=INFO
logging.level.ch=INFO
logging.level.ch.ethz.seb.sebserver.webservice.datalayer=INFO
logging.level.org.springframework.cache=DEBUG
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=DEBUG
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=INFO
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=DEBUG
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=INFO
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator=DEBUG
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=DEBUG
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator=INFO
logging.level.ch.ethz.seb.sebserver.webservice.weblayer.oauth=DEBUG
#logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl=DEBUG
#logging.level.ch.ethz.seb.sebserver.webservice.datalayer.batis=DEBUG

View file

@ -806,8 +806,7 @@ sebserver.exam.delete.report.list.empty=No dependencies will be deleted.
sebserver.exam.proctoring.actions.open=Proctoring Settings
sebserver.exam.proctoring.form.title=Exam Proctoring Settings
sebserver.exam.proctoring.form.info.title=Remote Proctoring
sebserver.exam.proctoring.form.info=This allows to integrate a supported external proctoring service.<br/>To integrate Jitsi Meet, JWT based authentication must be enabled.<br/>To integrate Zoom a Zoom higher-plan account is needed and also JWT based authentication.
sebserver.exam.proctoring.form.enabled=Proctoring enabled
sebserver.exam.proctoring.form.sebserver.exam.proctoring.form.enabled=Proctoring enabled
sebserver.exam.proctoring.form.enabled.tooltip=Indicates whether the exam proctoring feature is enabled for this exam or not.
sebserver.exam.proctoring.form.type=Type
sebserver.exam.proctoring.form.type.tooltip=The type and server type of the external proctoring service.
@ -828,17 +827,6 @@ sebserver.exam.proctoring.form.appkey.zoom.tooltip=Since Zoom no longer supports
sebserver.exam.proctoring.form.secret.zoom=App Secret (Deprecated)
sebserver.exam.proctoring.form.secret.zoom.tooltip=Since Zoom no longer supports JWT App, this is deprecated and the below Account ID, Client ID and Client Secret must be used
sebserver.exam.proctoring.form.appkey.sps=App Key
sebserver.exam.proctoring.form.appkey.sps.tooltip=The application key for witch SEB Server can connect to the proctoring service. This is defined by the screen proctoring service.
sebserver.exam.proctoring.form.appsecret.sps=App Secret
sebserver.exam.proctoring.form.appsecret.sps.tooltip=The secret used to access the proctoring service by SEB Server.
sebserver.exam.proctoring.form.accountId.sps=API Account Id
sebserver.exam.proctoring.form.accountId.sps.tooltip=This is the API account id on the proctoring service that SEB Server shall use to connect and manage the proctoring service
sebserver.exam.proctoring.form.accountSecret.sps=API Account Secret
sebserver.exam.proctoring.form.accountSecret.sps.tooltip=This is the secret for the above API account id
sebserver.exam.proctoring.form.accountId=Account ID
sebserver.exam.proctoring.form.accountId.tooltip=This is the Account ID from the Zoom Server-to-Server OAuth application and needed by SEB server to automatically create meetings
sebserver.exam.proctoring.form.clientId=Client ID
@ -887,6 +875,25 @@ sebserver.exam.proctoring.one.close.error=Failed to close the one-to-one room pr
sebserver.exam.proctoring.collecting.open.error=Failed to open the collecting room.
sebserver.exam.proctoring.collecting.close.error=Failed to close the collecting room properly.
sebserver.exam.sps.actions.open=Screen Recording Settings
sebserver.exam.sps.form.title=Screen Recording Settings
sebserver.exam.sps.form.info.title=Info
sebserver.exam.sps.form.info=This allows to integrate a SEB screen proctoring service for this exam to record screens and interaction-data for all SEBs applying the exam.<br/>The SEB screen proctoring service must be available within the given URL and accessible as administrator with the given account credentials.<br/>Also the service API credentials are needed to connect to the service securely.
sebserver.exam.sps.form.enable=Enable Screen Recording
sebserver.exam.sps.form.enable.tooltip=To enable screen recording with SEB and SEB Server you need to integrate an available SEB screen proctoring service
sebserver.exam.sps.form.url=Service URL
sebserver.exam.sps.form.url.tooltip=The root URL of the SEB screen proctoring service for integration
sebserver.exam.sps.form.appkey=API Key
sebserver.exam.sps.form.appkey.tooltip=The application key for witch SEB Server can connect to the proctoring service. This is defined by the screen proctoring service.
sebserver.exam.sps.form.appsecret=API Secret
sebserver.exam.sps.form.accountId=Account Name
sebserver.exam.sps.form.accountId.tooltip=This is the API account id on the proctoring service that SEB Server shall use to connect and manage the proctoring service
sebserver.exam.sps.form.accountSecret=Account Password
sebserver.exam.sps.form.accountSecret.tooltip=This is the secret/password for the above API account id
sebserver.exam.sps.form.collect.strategy=Grouping Strategy
sebserver.exam.sps.form.saveSettings=Save Settings
sebserver.exam.signaturekey.action.edit=App Signature Key
sebserver.exam.signaturekey.action.save=Save Settings
sebserver.exam.signaturekey.action.cancel=Cancel and back to Exam
@ -1927,6 +1934,8 @@ sebserver.examconfig.props.label.screenProctoringMetadataURLEnabled=Enable URL R
sebserver.examconfig.props.label.screenProctoringMetadataURLEnabled.tooltip=Enable the recording of browser URL input during a screen proctoring session
sebserver.examconfig.props.label.screenProctoringMetadataWindowTitleEnabled=Enable Window Title Recording
sebserver.examconfig.props.label.screenProctoringMetadataWindowTitleEnabled.tooltip=Enable the recording of the title of the active window during a screen proctoring session
sebserver.examconfig.props.label.screenProctoringMetadataActiveAppEnabled=Enable Active Application Recording
sebserver.examconfig.props.label.screenProctoringMetadataActiveAppEnabled.tooltip=Enable the recording of the name of the active application. The active application is the one with the actual user focus
sebserver.examconfig.props.group.ScreenProctoring=SEB Screen Proctoring Settings
sebserver.examconfig.props.group.screenshot=Screenshot Settings

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B