SEBSERV-435 implementation and testing for Exam. TODO Monitoring
This commit is contained in:
parent
fb74fb1804
commit
60bdb38c7b
34 changed files with 1404 additions and 509 deletions
|
@ -22,4 +22,6 @@ public interface FeatureService {
|
||||||
|
|
||||||
boolean isEnabled(CollectingStrategy collectingRoomStrategy);
|
boolean isEnabled(CollectingStrategy collectingRoomStrategy);
|
||||||
|
|
||||||
|
boolean isScreenProcteringEnabled();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,4 +59,12 @@ public class FeatureServiceImpl implements FeatureService {
|
||||||
return key.replaceAll("_", "-");
|
return key.replaceAll("_", "-");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isScreenProcteringEnabled() {
|
||||||
|
return this.environment.getProperty(toConfigName(
|
||||||
|
FEATURE_SETTINGS_PREFIX + "seb.screenProctoring"),
|
||||||
|
Boolean.class,
|
||||||
|
Boolean.FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,6 +90,10 @@ public class ClientCredentialServiceImpl implements ClientCredentialService {
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~`!@#$%^*()-_=+[{]}?"
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~`!@#$%^*()-_=+[{]}?"
|
||||||
.toCharArray();
|
.toCharArray();
|
||||||
|
|
||||||
|
private final static char[] possibleLess =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
.toCharArray();
|
||||||
|
|
||||||
public static CharSequence generateClientId() {
|
public static CharSequence generateClientId() {
|
||||||
return RandomStringUtils.random(
|
return RandomStringUtils.random(
|
||||||
16, 0, possibleCharacters.length - 1, false, false,
|
16, 0, possibleCharacters.length - 1, false, false,
|
||||||
|
@ -102,6 +106,12 @@ public class ClientCredentialServiceImpl implements ClientCredentialService {
|
||||||
possibleCharacters, new SecureRandom());
|
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) {
|
public static void clearChars(final CharSequence sequence) {
|
||||||
if (sequence == null) {
|
if (sequence == null) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -343,6 +343,16 @@ public enum ActionDefinition {
|
||||||
ImageIcon.VISIBILITY_OFF,
|
ImageIcon.VISIBILITY_OFF,
|
||||||
PageStateDefinitionImpl.EXAM_VIEW,
|
PageStateDefinitionImpl.EXAM_VIEW,
|
||||||
ActionCategory.EXAM_SECURITY),
|
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(
|
EXAM_CONFIGURATION_NEW(
|
||||||
new LocTextKey("sebserver.exam.configuration.action.list.new"),
|
new LocTextKey("sebserver.exam.configuration.action.list.new"),
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
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.API;
|
||||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
|
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.ExamTemplate;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
|
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.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.LmsSetup;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult.ErrorType;
|
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.CheckSEBRestriction;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
|
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.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.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.GetDefaultExamTemplate;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetExamTemplate;
|
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 ResourceService resourceService;
|
||||||
private final ExamSEBRestrictionSettings examSEBRestrictionSettings;
|
private final ExamSEBRestrictionSettings examSEBRestrictionSettings;
|
||||||
private final ProctoringSettingsPopup proctoringSettingsPopup;
|
private final ProctoringSettingsPopup proctoringSettingsPopup;
|
||||||
|
private final ScreenProctoringSettingsPopup screenProctoringSettingsPopup;
|
||||||
private final WidgetFactory widgetFactory;
|
private final WidgetFactory widgetFactory;
|
||||||
private final RestService restService;
|
private final RestService restService;
|
||||||
private final ExamDeletePopup examDeletePopup;
|
private final ExamDeletePopup examDeletePopup;
|
||||||
|
@ -156,22 +160,26 @@ public class ExamForm implements TemplateComposer {
|
||||||
private final ExamIndicatorsList examIndicatorsList;
|
private final ExamIndicatorsList examIndicatorsList;
|
||||||
private final ExamClientGroupList examClientGroupList;
|
private final ExamClientGroupList examClientGroupList;
|
||||||
private final ExamCreateClientConfigPopup examCreateClientConfigPopup;
|
private final ExamCreateClientConfigPopup examCreateClientConfigPopup;
|
||||||
|
private final FeatureService featureService;
|
||||||
|
|
||||||
protected ExamForm(
|
protected ExamForm(
|
||||||
final PageService pageService,
|
final PageService pageService,
|
||||||
final ExamSEBRestrictionSettings examSEBRestrictionSettings,
|
final ExamSEBRestrictionSettings examSEBRestrictionSettings,
|
||||||
final ProctoringSettingsPopup proctoringSettingsPopup,
|
final ProctoringSettingsPopup proctoringSettingsPopup,
|
||||||
|
final ScreenProctoringSettingsPopup screenProctoringSettingsPopup,
|
||||||
final ExamToConfigBindingForm examToConfigBindingForm,
|
final ExamToConfigBindingForm examToConfigBindingForm,
|
||||||
final DownloadService downloadService,
|
final DownloadService downloadService,
|
||||||
final ExamDeletePopup examDeletePopup,
|
final ExamDeletePopup examDeletePopup,
|
||||||
final ExamFormConfigs examFormConfigs,
|
final ExamFormConfigs examFormConfigs,
|
||||||
final ExamIndicatorsList examIndicatorsList,
|
final ExamIndicatorsList examIndicatorsList,
|
||||||
final ExamClientGroupList examClientGroupList,
|
final ExamClientGroupList examClientGroupList,
|
||||||
final ExamCreateClientConfigPopup examCreateClientConfigPopup) {
|
final ExamCreateClientConfigPopup examCreateClientConfigPopup,
|
||||||
|
final FeatureService featureService) {
|
||||||
|
|
||||||
this.pageService = pageService;
|
this.pageService = pageService;
|
||||||
this.resourceService = pageService.getResourceService();
|
this.resourceService = pageService.getResourceService();
|
||||||
this.examSEBRestrictionSettings = examSEBRestrictionSettings;
|
this.examSEBRestrictionSettings = examSEBRestrictionSettings;
|
||||||
|
this.screenProctoringSettingsPopup = screenProctoringSettingsPopup;
|
||||||
this.proctoringSettingsPopup = proctoringSettingsPopup;
|
this.proctoringSettingsPopup = proctoringSettingsPopup;
|
||||||
this.widgetFactory = pageService.getWidgetFactory();
|
this.widgetFactory = pageService.getWidgetFactory();
|
||||||
this.restService = this.resourceService.getRestService();
|
this.restService = this.resourceService.getRestService();
|
||||||
|
@ -180,6 +188,7 @@ public class ExamForm implements TemplateComposer {
|
||||||
this.examIndicatorsList = examIndicatorsList;
|
this.examIndicatorsList = examIndicatorsList;
|
||||||
this.examClientGroupList = examClientGroupList;
|
this.examClientGroupList = examClientGroupList;
|
||||||
this.examCreateClientConfigPopup = examCreateClientConfigPopup;
|
this.examCreateClientConfigPopup = examCreateClientConfigPopup;
|
||||||
|
this.featureService = featureService;
|
||||||
|
|
||||||
this.consistencyMessageMapping = new HashMap<>();
|
this.consistencyMessageMapping = new HashMap<>();
|
||||||
this.consistencyMessageMapping.put(
|
this.consistencyMessageMapping.put(
|
||||||
|
@ -410,6 +419,13 @@ public class ExamForm implements TemplateComposer {
|
||||||
.map(ProctoringServiceSettings::getEnableProctoring)
|
.map(ProctoringServiceSettings::getEnableProctoring)
|
||||||
.getOr(false);
|
.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
|
final PageActionBuilder actionBuilder = this.pageService.pageActionBuilder(formContext
|
||||||
.clearEntityKeys()
|
.clearEntityKeys()
|
||||||
.removeAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA));
|
.removeAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA));
|
||||||
|
@ -491,7 +507,24 @@ public class ExamForm implements TemplateComposer {
|
||||||
.withEntityKey(entityKey)
|
.withEntityKey(entityKey)
|
||||||
.withExec(this.proctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable))
|
.withExec(this.proctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable))
|
||||||
.noEventPropagation()
|
.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
|
// additional data in read-only view
|
||||||
if (readonly && !importFromQuizData) {
|
if (readonly && !importFromQuizData) {
|
||||||
|
|
|
@ -486,8 +486,11 @@ public class ProctoringSettingsPopup {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private FormBuilder buildHeader(final ProctoringServiceSettings proctoringSettings,
|
private FormBuilder buildHeader(
|
||||||
final FormHandleAnchor formHandleAnchor, final boolean isReadonly) {
|
final ProctoringServiceSettings proctoringSettings,
|
||||||
|
final FormHandleAnchor formHandleAnchor,
|
||||||
|
final boolean isReadonly) {
|
||||||
|
|
||||||
final FormBuilder formBuilder = this.pageService.formBuilder(
|
final FormBuilder formBuilder = this.pageService.formBuilder(
|
||||||
formHandleAnchor.formContext)
|
formHandleAnchor.formContext)
|
||||||
.withDefaultSpanInput(5)
|
.withDefaultSpanInput(5)
|
||||||
|
|
|
@ -8,19 +8,297 @@
|
||||||
|
|
||||||
package ch.ethz.seb.sebserver.gui.content.exam;
|
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 {
|
public class ScreenProctoringSettingsPopup {
|
||||||
|
|
||||||
private final static LocTextKey SEB_PROCTORING_FORM_APPKEY_SPS =
|
private static final Logger log = LoggerFactory.getLogger(ScreenProctoringSettingsPopup.class);
|
||||||
new LocTextKey("sebserver.exam.proctoring.form.appkey.sps");
|
|
||||||
private final static LocTextKey SEB_PROCTORING_FORM_SECRET_SPS =
|
private final static LocTextKey FORM_TITLE =
|
||||||
new LocTextKey("sebserver.exam.proctoring.form.appsecret.sps");
|
new LocTextKey("sebserver.exam.sps.form.title");
|
||||||
private final static LocTextKey SEB_PROCTORING_FORM_ACCOUNT_ID_SPS =
|
private final static LocTextKey FORM_INFO_TITLE =
|
||||||
new LocTextKey("sebserver.exam.proctoring.form.accountId.sps");
|
new LocTextKey("sebserver.exam.sps.form.info.title");
|
||||||
private final static LocTextKey SEB_PROCTORING_FORM_ACCOUNT_SECRET_SPS =
|
private final static LocTextKey FORM_INFO =
|
||||||
new LocTextKey("sebserver.exam.proctoring.form.accountSecret.sps");
|
new LocTextKey("sebserver.exam.sps.form.info");
|
||||||
private final static LocTextKey SEB_PROCTORING_FORM_COLLECT_STRATEGY =
|
private final static LocTextKey FORM_ENABLE =
|
||||||
new LocTextKey("sebserver.exam.proctoring.form.collect.strategy");
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -148,7 +148,9 @@ public class WidgetFactory {
|
||||||
VERIFY("verify.png"),
|
VERIFY("verify.png"),
|
||||||
SHIELD("shield.png"),
|
SHIELD("shield.png"),
|
||||||
NO_SHIELD("no_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;
|
public String fileName;
|
||||||
private ImageData image = null;
|
private ImageData image = null;
|
||||||
|
|
|
@ -211,6 +211,16 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
|
||||||
key = "#examId")
|
key = "#examId")
|
||||||
Result<QuizData> updateQuizData(Long examId, QuizData quizData, String updateId);
|
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
|
/** 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
|
* @param externalQuizId The exams external UUID or quiz id of the exam to mark
|
||||||
|
|
|
@ -27,4 +27,8 @@ public interface ProctoringSettingsDAO {
|
||||||
final EntityKey entityKey,
|
final EntityKey entityKey,
|
||||||
final ScreenProctoringSettings screenProctoringSettings);
|
final ScreenProctoringSettings screenProctoringSettings);
|
||||||
|
|
||||||
|
void disableScreenProctoring(Long examId);
|
||||||
|
|
||||||
|
boolean isScreenProctoringEnabled(Long examId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -238,7 +238,7 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
|
||||||
.build()
|
.build()
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
final List<ClientConnectionRecord> execute = this.clientConnectionRecordMapper
|
this.clientConnectionRecordMapper
|
||||||
.selectByExample()
|
.selectByExample()
|
||||||
.build()
|
.build()
|
||||||
.execute();
|
.execute();
|
||||||
|
@ -340,8 +340,6 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
|
||||||
final long millisecondsNow = Utils.getMillisecondsNow();
|
final long millisecondsNow = Utils.getMillisecondsNow();
|
||||||
// NOTE: we use nanoseconds here to get a better precision to better avoid
|
// NOTE: we use nanoseconds here to get a better precision to better avoid
|
||||||
// same value of real concurrent calls on distributed systems
|
// 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 long nanosecondsNow = System.nanoTime();
|
||||||
final ClientConnectionRecord newRecord = new ClientConnectionRecord(
|
final ClientConnectionRecord newRecord = new ClientConnectionRecord(
|
||||||
null,
|
null,
|
||||||
|
|
|
@ -159,6 +159,28 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
.map(rec -> saveAdditionalQuizAttributes(examId, quizData));
|
.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
|
@Override
|
||||||
public void markLMSAvailability(final String externalQuizId, final boolean available, final String updateId) {
|
public void markLMSAvailability(final String externalQuizId, final boolean available, final String updateId) {
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
private Boolean getEnabled(final Map<String, AdditionalAttributeRecord> mapping) {
|
||||||
if (mapping.containsKey(ProctoringServiceSettings.ATTR_ENABLE_PROCTORING)) {
|
if (mapping.containsKey(ProctoringServiceSettings.ATTR_ENABLE_PROCTORING)) {
|
||||||
return BooleanUtils.toBoolean(mapping.get(ProctoringServiceSettings.ATTR_ENABLE_PROCTORING).getValue());
|
return BooleanUtils.toBoolean(mapping.get(ProctoringServiceSettings.ATTR_ENABLE_PROCTORING).getValue());
|
||||||
|
|
|
@ -136,6 +136,11 @@ public interface ExamAdminService {
|
||||||
* @return Result refer to the archived exam or to an error when happened */
|
* @return Result refer to the archived exam or to an error when happened */
|
||||||
Result<Exam> archiveExam(Exam exam);
|
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.
|
/** Used to check threshold consistency for a given list of thresholds.
|
||||||
* Checks if all values are present (none null value)
|
* Checks if all values are present (none null value)
|
||||||
* Checks if there are duplicates
|
* Checks if there are duplicates
|
||||||
|
|
|
@ -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.APIMessage.APIMessageException;
|
||||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
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;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
|
||||||
|
@ -70,6 +71,11 @@ public interface ProctoringAdminService {
|
||||||
* @return ExamProctoringService instance */
|
* @return ExamProctoringService instance */
|
||||||
Result<RemoteProctoringService> getExamProctoringService(final ProctoringServerType type);
|
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.
|
/** Use this to test the proctoring service settings against the remote proctoring server.
|
||||||
*
|
*
|
||||||
* @param proctoringSettings the settings to test
|
* @param proctoringSettings the settings to test
|
||||||
|
|
|
@ -299,6 +299,12 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
||||||
.map(settings -> exam);
|
.map(settings -> exam);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyExamSaved(final Exam exam) {
|
||||||
|
updateAdditionalExamConfigAttributes(exam.id);
|
||||||
|
this.proctoringAdminService.notifyExamSaved(exam);
|
||||||
|
}
|
||||||
|
|
||||||
private Result<Exam> initAdditionalAttributesForMoodleExams(final Exam exam) {
|
private Result<Exam> initAdditionalAttributesForMoodleExams(final Exam exam) {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
final LmsAPITemplate lmsTemplate = this.lmsAPIService
|
final LmsAPITemplate lmsTemplate = this.lmsAPIService
|
||||||
|
|
|
@ -13,6 +13,7 @@ import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
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;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
|
||||||
|
@ -92,8 +93,6 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
|
||||||
|
|
||||||
checkType(parentEntityKey);
|
checkType(parentEntityKey);
|
||||||
|
|
||||||
final boolean notifyChangesToService = notifyChangesToService(parentEntityKey, screenProctoringSettings);
|
|
||||||
|
|
||||||
this.screenProctoringService
|
this.screenProctoringService
|
||||||
.testSettings(screenProctoringSettings)
|
.testSettings(screenProctoringSettings)
|
||||||
.flatMap(settings -> this.proctoringSettingsDAO.storeScreenProctoringSettings(
|
.flatMap(settings -> this.proctoringSettingsDAO.storeScreenProctoringSettings(
|
||||||
|
@ -101,9 +100,12 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
|
||||||
screenProctoringSettings))
|
screenProctoringSettings))
|
||||||
.getOrThrow();
|
.getOrThrow();
|
||||||
|
|
||||||
if (notifyChangesToService) {
|
if (parentEntityKey.entityType == EntityType.EXAM) {
|
||||||
|
|
||||||
this.screenProctoringService
|
this.screenProctoringService
|
||||||
.applyScreenProctoingForExam(screenProctoringSettings)
|
.applyScreenProctoingForExam(screenProctoringSettings.examId)
|
||||||
|
.onError(error -> this.proctoringSettingsDAO
|
||||||
|
.disableScreenProctoring(screenProctoringSettings.examId))
|
||||||
.getOrThrow();
|
.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
|
@Override
|
||||||
public Result<RemoteProctoringService> getExamProctoringService(final ProctoringServerType type) {
|
public Result<RemoteProctoringService> getExamProctoringService(final ProctoringServerType type) {
|
||||||
return this.remoteProctoringServiceFactory
|
return this.remoteProctoringServiceFactory
|
||||||
.getExamProctoringService(type);
|
.getExamProctoringService(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyExamSaved(final Exam exam) {
|
||||||
|
this.screenProctoringService.notifyExamSaved(exam);
|
||||||
|
}
|
||||||
|
|
||||||
private void checkType(final EntityKey parentEntityKey) {
|
private void checkType(final EntityKey parentEntityKey) {
|
||||||
if (!SUPPORTED_PARENT_ENTITES.contains(parentEntityKey.entityType)) {
|
if (!SUPPORTED_PARENT_ENTITES.contains(parentEntityKey.entityType)) {
|
||||||
throw new UnsupportedOperationException(
|
throw new UnsupportedOperationException(
|
||||||
|
|
|
@ -40,6 +40,11 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
||||||
|
|
||||||
public class MockCourseAccessAPI implements CourseAccessAPI {
|
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 Collection<QuizData> mockups;
|
||||||
private final WebserviceInfo webserviceInfo;
|
private final WebserviceInfo webserviceInfo;
|
||||||
private final APITemplateDataSupplier apiTemplateDataSupplier;
|
private final APITemplateDataSupplier apiTemplateDataSupplier;
|
||||||
|
@ -85,10 +90,8 @@ public class MockCourseAccessAPI implements CourseAccessAPI {
|
||||||
this.mockups.add(new QuizData(
|
this.mockups.add(new QuizData(
|
||||||
"quiz10", institutionId, lmsSetupId, lmsType, "Demo Quiz 10 (MOCKUP)",
|
"quiz10", institutionId, lmsSetupId, lmsType, "Demo Quiz 10 (MOCKUP)",
|
||||||
"Starts in a minute and ends after five minutes",
|
"Starts in a minute and ends after five minutes",
|
||||||
DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS)
|
MockCourseAccessAPI.startTime10,
|
||||||
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
|
MockCourseAccessAPI.endTime10,
|
||||||
DateTime.now(DateTimeZone.UTC).plus(6 * Constants.MINUTE_IN_MILLIS)
|
|
||||||
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
|
|
||||||
"http://lms.mockup.com/api/"));
|
"http://lms.mockup.com/api/"));
|
||||||
this.mockups.add(new QuizData(
|
this.mockups.add(new QuizData(
|
||||||
"quiz11", institutionId, lmsSetupId, lmsType, "Demo Quiz 11 (MOCKUP)",
|
"quiz11", institutionId, lmsSetupId, lmsType, "Demo Quiz 11 (MOCKUP)",
|
||||||
|
|
|
@ -34,17 +34,26 @@ public interface ScreenProctoringService extends SessionUpdateTask {
|
||||||
* @return Result refer to the settings or to an error when happened */
|
* @return Result refer to the settings or to an error when happened */
|
||||||
Result<ScreenProctoringSettings> testSettings(ScreenProctoringSettings settings);
|
Result<ScreenProctoringSettings> testSettings(ScreenProctoringSettings settings);
|
||||||
|
|
||||||
//Result<ScreenProctoringSettings> saveSettingsForExam(ScreenProctoringSettings settings);
|
/** This applies the stored screen proctoring for the given exam.
|
||||||
|
|
||||||
/** This applies the screen proctoring for the given exam.
|
|
||||||
* If screen proctoring for the exam is enabled, this initializes or re-activate all
|
* If screen proctoring for the exam is enabled, this initializes or re-activate all
|
||||||
* needed things for screen proctoring for the given exam.
|
* needed things for screen proctoring for the given exam.
|
||||||
* If screen proctoring is set to disable, this disables the whole service for the
|
* If screen proctoring is set to disable, this disables the whole service for the
|
||||||
* given exam also on SPS side.
|
* given exam also on SPS side.
|
||||||
*
|
*
|
||||||
* @param settings the actual ScreenProctoringSettings of the exam
|
* @param examId use the screen proctoring settings of the exam with the given exam id
|
||||||
* @return Result refer to the given ScreenProctoringSettings or to an error when happened */
|
* @return Result refer to the given Exam or to an error when happened */
|
||||||
Result<ScreenProctoringSettings> applyScreenProctoingForExam(ScreenProctoringSettings settings);
|
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 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.
|
* This deactivates and dispose or deletes all exam relevant domain entities on the SPS service side.
|
||||||
|
|
|
@ -18,7 +18,6 @@ import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
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.model.session.ClientConnection;
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
|
@ -52,7 +51,6 @@ public class ExamSessionCacheService {
|
||||||
private final ClientConnectionDAO clientConnectionDAO;
|
private final ClientConnectionDAO clientConnectionDAO;
|
||||||
private final InternalClientConnectionDataFactory internalClientConnectionDataFactory;
|
private final InternalClientConnectionDataFactory internalClientConnectionDataFactory;
|
||||||
private final ExamConfigService sebExamConfigService;
|
private final ExamConfigService sebExamConfigService;
|
||||||
private final ExamUpdateHandler examUpdateHandler;
|
|
||||||
|
|
||||||
protected ExamSessionCacheService(
|
protected ExamSessionCacheService(
|
||||||
final ExamDAO examDAO,
|
final ExamDAO examDAO,
|
||||||
|
@ -60,7 +58,6 @@ public class ExamSessionCacheService {
|
||||||
final ClientConnectionDAO clientConnectionDAO,
|
final ClientConnectionDAO clientConnectionDAO,
|
||||||
final InternalClientConnectionDataFactory internalClientConnectionDataFactory,
|
final InternalClientConnectionDataFactory internalClientConnectionDataFactory,
|
||||||
final ExamConfigService sebExamConfigService,
|
final ExamConfigService sebExamConfigService,
|
||||||
final ExamUpdateHandler examUpdateHandler,
|
|
||||||
final RemoteProctoringRoomDAO remoteProctoringRoomDAO) {
|
final RemoteProctoringRoomDAO remoteProctoringRoomDAO) {
|
||||||
|
|
||||||
this.examDAO = examDAO;
|
this.examDAO = examDAO;
|
||||||
|
@ -68,7 +65,6 @@ public class ExamSessionCacheService {
|
||||||
this.clientConnectionDAO = clientConnectionDAO;
|
this.clientConnectionDAO = clientConnectionDAO;
|
||||||
this.internalClientConnectionDataFactory = internalClientConnectionDataFactory;
|
this.internalClientConnectionDataFactory = internalClientConnectionDataFactory;
|
||||||
this.sebExamConfigService = sebExamConfigService;
|
this.sebExamConfigService = sebExamConfigService;
|
||||||
this.examUpdateHandler = examUpdateHandler;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Cacheable(
|
@Cacheable(
|
||||||
|
@ -132,9 +128,11 @@ public class ExamSessionCacheService {
|
||||||
}
|
}
|
||||||
case UP_COMING:
|
case UP_COMING:
|
||||||
case FINISHED: {
|
case FINISHED: {
|
||||||
return this.examUpdateHandler.updateRunning(exam.id)
|
return false;
|
||||||
.map(e -> e.status == ExamStatus.RUNNING)
|
// TODO do we really need to double-check here?
|
||||||
.getOr(false);
|
// return this.examUpdateHandler.updateRunning(exam.id)
|
||||||
|
// .map(e -> e.status == ExamStatus.RUNNING)
|
||||||
|
// .getOr(false);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -532,6 +532,9 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
||||||
@Override
|
@Override
|
||||||
public Result<Exam> updateExamCache(final Long examId) {
|
public Result<Exam> updateExamCache(final Long examId) {
|
||||||
|
|
||||||
|
// TODO check how often this is called in distributed environments
|
||||||
|
System.out.println("************** performance check: updateExamCache");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final Cache cache = this.cacheManager.getCache(ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM);
|
final Cache cache = this.cacheManager.getCache(ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM);
|
||||||
final ValueWrapper valueWrapper = cache.get(examId);
|
final ValueWrapper valueWrapper = cache.get(examId);
|
||||||
|
@ -547,7 +550,8 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
||||||
return Result.ofEmpty();
|
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))
|
.onError(t -> log.error("Failed to verify if cached exam is up to date: {}", exam, t))
|
||||||
.getOr(false);
|
.getOr(false);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||||
|
@ -216,18 +217,16 @@ class ExamUpdateHandler implements ExamUpdateTask {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<Exam> updateRunning(final Long examId) {
|
@EventListener(ExamUpdateEvent.class)
|
||||||
return this.examDAO.byPK(examId)
|
void updateRunning(final ExamUpdateEvent event) {
|
||||||
.map(exam -> {
|
this.examDAO
|
||||||
final DateTime now = DateTime.now(DateTimeZone.UTC);
|
.byPK(event.examId)
|
||||||
if (exam.getStatus() == ExamStatus.UP_COMING
|
.onSuccess(exam -> updateState(
|
||||||
&& exam.endTime.plus(this.examTimeSuffix).isBefore(now)) {
|
exam,
|
||||||
return setRunning(exam, this.createUpdateId())
|
DateTime.now(DateTimeZone.UTC),
|
||||||
.getOr(exam);
|
this.examTimePrefix,
|
||||||
} else {
|
this.examTimeSuffix,
|
||||||
return exam;
|
this.createUpdateId()));
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateState(
|
void updateState(
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
|
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.api.JSONMapper;
|
||||||
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
|
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
|
||||||
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
|
import ch.ethz.seb.sebserver.gbl.model.exam.CollectingStrategy;
|
||||||
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.Exam;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
|
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.AdditionalAttributesDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
|
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.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.ScreenProctoringGroupDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO;
|
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.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.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.SEBClientInstructionService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService;
|
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
|
@Lazy
|
||||||
@Service
|
@Service
|
||||||
|
@ -58,16 +59,18 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
||||||
private static final Logger log = LoggerFactory.getLogger(ScreenProctoringServiceImpl.class);
|
private static final Logger log = LoggerFactory.getLogger(ScreenProctoringServiceImpl.class);
|
||||||
|
|
||||||
private final Cryptor cryptor;
|
private final Cryptor cryptor;
|
||||||
|
private final JSONMapper jsonMapper;
|
||||||
private final ScreenProctoringAPIBinding screenProctoringAPIBinding;
|
private final ScreenProctoringAPIBinding screenProctoringAPIBinding;
|
||||||
private final ScreenProctoringGroupDAO screenProctoringGroupDAO;
|
private final ScreenProctoringGroupDAO screenProctoringGroupDAO;
|
||||||
|
private final ProctoringSettingsDAO proctoringSettingsDAO;
|
||||||
private final ClientConnectionDAO clientConnectionDAO;
|
private final ClientConnectionDAO clientConnectionDAO;
|
||||||
private final ExamDAO examDAO;
|
private final ExamDAO examDAO;
|
||||||
private final ProctoringSettingsDAOImpl proctoringSettingsSupplier;
|
|
||||||
private final SEBClientInstructionService sebInstructionService;
|
private final SEBClientInstructionService sebInstructionService;
|
||||||
|
private final ExamSessionCacheService examSessionCacheService;
|
||||||
|
|
||||||
public ScreenProctoringServiceImpl(
|
public ScreenProctoringServiceImpl(
|
||||||
final Cryptor cryptor,
|
final Cryptor cryptor,
|
||||||
final ProctoringSettingsDAOImpl proctoringSettingsSupplier,
|
final ProctoringSettingsDAO proctoringSettingsDAO,
|
||||||
final AsyncService asyncService,
|
final AsyncService asyncService,
|
||||||
final JSONMapper jsonMapper,
|
final JSONMapper jsonMapper,
|
||||||
final UserDAO userDAO,
|
final UserDAO userDAO,
|
||||||
|
@ -75,21 +78,24 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
||||||
final ClientConnectionDAO clientConnectionDAO,
|
final ClientConnectionDAO clientConnectionDAO,
|
||||||
final AdditionalAttributesDAO additionalAttributesDAO,
|
final AdditionalAttributesDAO additionalAttributesDAO,
|
||||||
final ScreenProctoringGroupDAO screenProctoringGroupDAO,
|
final ScreenProctoringGroupDAO screenProctoringGroupDAO,
|
||||||
final SEBClientInstructionService sebInstructionService) {
|
final SEBClientInstructionService sebInstructionService,
|
||||||
|
final ExamSessionCacheService examSessionCacheService) {
|
||||||
|
|
||||||
this.cryptor = cryptor;
|
this.cryptor = cryptor;
|
||||||
|
this.jsonMapper = jsonMapper;
|
||||||
this.examDAO = examDAO;
|
this.examDAO = examDAO;
|
||||||
this.proctoringSettingsSupplier = proctoringSettingsSupplier;
|
|
||||||
this.screenProctoringGroupDAO = screenProctoringGroupDAO;
|
this.screenProctoringGroupDAO = screenProctoringGroupDAO;
|
||||||
this.clientConnectionDAO = clientConnectionDAO;
|
this.clientConnectionDAO = clientConnectionDAO;
|
||||||
this.sebInstructionService = sebInstructionService;
|
this.sebInstructionService = sebInstructionService;
|
||||||
|
this.examSessionCacheService = examSessionCacheService;
|
||||||
|
this.proctoringSettingsDAO = proctoringSettingsDAO;
|
||||||
|
|
||||||
this.screenProctoringAPIBinding = new ScreenProctoringAPIBinding(
|
this.screenProctoringAPIBinding = new ScreenProctoringAPIBinding(
|
||||||
userDAO,
|
userDAO,
|
||||||
cryptor,
|
cryptor,
|
||||||
asyncService,
|
asyncService,
|
||||||
jsonMapper,
|
jsonMapper,
|
||||||
proctoringSettingsSupplier,
|
proctoringSettingsDAO,
|
||||||
additionalAttributesDAO);
|
additionalAttributesDAO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,14 +103,19 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
||||||
public Result<ScreenProctoringSettings> testSettings(final ScreenProctoringSettings screenProctoringSettings) {
|
public Result<ScreenProctoringSettings> testSettings(final ScreenProctoringSettings screenProctoringSettings) {
|
||||||
return Result.tryCatch(() -> {
|
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
|
if (screenProctoringSettings.spsServiceURL != null
|
||||||
&& screenProctoringSettings.spsServiceURL.contains("?")) {
|
&& screenProctoringSettings.spsServiceURL.contains("?")) {
|
||||||
throw new FieldValidationException(
|
fieldChecks.add(APIMessage.fieldValidationError(
|
||||||
"serverURL",
|
"appKey",
|
||||||
"screenProctoringSettings:spsServiceURL:invalidURL");
|
"screenProctoringSettings:spsServiceURL:invalidURL"));
|
||||||
}
|
}
|
||||||
|
|
||||||
final Collection<APIMessage> fieldChecks = new ArrayList<>();
|
|
||||||
if (StringUtils.isBlank(screenProctoringSettings.spsAPIKey)) {
|
if (StringUtils.isBlank(screenProctoringSettings.spsAPIKey)) {
|
||||||
fieldChecks.add(APIMessage.fieldValidationError(
|
fieldChecks.add(APIMessage.fieldValidationError(
|
||||||
"appKey",
|
"appKey",
|
||||||
|
@ -150,27 +161,41 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<ScreenProctoringSettings> applyScreenProctoingForExam(final ScreenProctoringSettings settings) {
|
public Result<Exam> applyScreenProctoingForExam(final Long examId) {
|
||||||
|
|
||||||
return this.examDAO
|
return this.examDAO
|
||||||
.byPK(settings.examId)
|
.byPK(examId)
|
||||||
.map(exam -> {
|
.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
|
this.screenProctoringAPIBinding
|
||||||
.startScreenProctoring(exam)
|
.startScreenProctoring(exam)
|
||||||
|
.onError(error -> log.error(
|
||||||
|
"Failed to apply screen proctoring for exam: {}",
|
||||||
|
exam,
|
||||||
|
error))
|
||||||
.getOrThrow()
|
.getOrThrow()
|
||||||
.stream()
|
.stream()
|
||||||
.forEach(newGroup -> createNewLocalGroup(exam, newGroup));
|
.forEach(newGroup -> createNewLocalGroup(exam, newGroup));
|
||||||
} else {
|
|
||||||
|
this.examDAO.markUpdate(exam.id);
|
||||||
|
|
||||||
|
} else if (!isEnabling && isSPSActive) {
|
||||||
|
|
||||||
this.screenProctoringAPIBinding
|
this.screenProctoringAPIBinding
|
||||||
.dispsoseScreenProctoring(exam)
|
.dispsoseScreenProctoring(exam)
|
||||||
|
.onError(error -> log.error("Failed to dispose screen proctoring for exam: {}",
|
||||||
|
exam,
|
||||||
|
error))
|
||||||
.getOrThrow();
|
.getOrThrow();
|
||||||
}
|
|
||||||
|
|
||||||
return settings;
|
this.examDAO.markUpdate(exam.id);
|
||||||
|
}
|
||||||
|
return exam;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,20 +229,51 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
||||||
public void updateClientConnections() {
|
public void updateClientConnections() {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
final Map<Long, ExamInfo> examInfoCache = new HashMap<>();
|
|
||||||
|
|
||||||
this.examDAO
|
this.examDAO
|
||||||
.allIdsOfRunningWithScreenProctoringEnabled()
|
.allIdsOfRunningWithScreenProctoringEnabled()
|
||||||
.flatMap(this.clientConnectionDAO::getAllForScreenProctoringUpdate)
|
.flatMap(this.clientConnectionDAO::getAllForScreenProctoringUpdate)
|
||||||
.getOrThrow()
|
.getOrThrow()
|
||||||
.stream()
|
.stream()
|
||||||
.forEach(cc -> applyScreenProctoringSession(cc, examInfoCache));
|
.forEach(cc -> applyScreenProctoringSession(cc));
|
||||||
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
log.error("Failed to update active SEB connections for screen proctoring");
|
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
|
@Override
|
||||||
public void notifyExamDeletion(final ExamDeletionEvent event) {
|
public void notifyExamDeletion(final ExamDeletionEvent event) {
|
||||||
event.ids
|
event.ids
|
||||||
|
@ -234,86 +290,42 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyScreenProctoringSession(
|
private void applyScreenProctoringSession(final ClientConnectionRecord ccRecord) {
|
||||||
final ClientConnectionRecord ccRecord,
|
|
||||||
final Map<Long, ExamInfo> examInfoCache) {
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
final Long examId = ccRecord.getExamId();
|
final Long examId = ccRecord.getExamId();
|
||||||
|
final Exam runningExam = this.examSessionCacheService.getRunningExam(examId);
|
||||||
// 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);
|
|
||||||
|
|
||||||
// apply SEB connection to screen proctoring group
|
// apply SEB connection to screen proctoring group
|
||||||
final ScreenProctoringGroup group = applySEBConnectionToGroup(
|
final ScreenProctoringGroup group = applySEBConnectionToGroup(
|
||||||
ccRecord,
|
ccRecord,
|
||||||
examInfo.screenProctoringSettings);
|
runningExam);
|
||||||
|
|
||||||
// create screen proctoring session for SEB connection on SPS service
|
// create screen proctoring session for SEB connection on SPS service
|
||||||
final String spsSessionToken = this.screenProctoringAPIBinding
|
final String spsSessionToken = this.screenProctoringAPIBinding
|
||||||
.createSEBSession(examId, group, ccRecord);
|
.createSEBSession(examId, group, ccRecord);
|
||||||
|
|
||||||
// create instruction for SEB and add it to instruction queue for SEB connection
|
// 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) {
|
} catch (final Exception e) {
|
||||||
log.error("Failed to apply screen proctoring session to SEB with connection: ", ccRecord, 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(
|
private ScreenProctoringGroup applySEBConnectionToGroup(
|
||||||
final ClientConnectionRecord ccRecord,
|
final ClientConnectionRecord ccRecord,
|
||||||
final ScreenProctoringSettings settings) {
|
final Exam exam) {
|
||||||
|
|
||||||
final Long examId = ccRecord.getExamId();
|
if (!exam.additionalAttributes.containsKey(ScreenProctoringSettings.ATTR_COLLECTING_STRATEGY)) {
|
||||||
switch (settings.collectingStrategy) {
|
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: {
|
case SEB_GROUP: {
|
||||||
// TODO
|
// TODO
|
||||||
throw new UnsupportedOperationException("SEB_GROUP based group collection is not supported yet");
|
throw new UnsupportedOperationException("SEB_GROUP based group collection is not supported yet");
|
||||||
|
@ -321,30 +333,43 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
||||||
case EXAM:
|
case EXAM:
|
||||||
case FIX_SIZE:
|
case FIX_SIZE:
|
||||||
default: {
|
default: {
|
||||||
|
return applyToDefaultGroup(ccRecord, exam);
|
||||||
final ScreenProctoringGroup screenProctoringGroup = getProctoringGroup(settings);
|
|
||||||
this.clientConnectionDAO.assignToScreenProctoringGroup(
|
|
||||||
examId,
|
|
||||||
ccRecord.getConnectionToken(),
|
|
||||||
screenProctoringGroup.id)
|
|
||||||
.getOrThrow();
|
|
||||||
|
|
||||||
return screenProctoringGroup;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
final Result<ScreenProctoringGroup> reserve = this.screenProctoringGroupDAO
|
||||||
.reservePlaceInCollectingGroup(
|
.reservePlaceInCollectingGroup(
|
||||||
settings.examId,
|
exam.id,
|
||||||
settings.collectingGroupSize != null ? settings.collectingGroupSize : 0);
|
collectingGroupSize);
|
||||||
|
|
||||||
ScreenProctoringGroup screenProctoringGroup = null;
|
ScreenProctoringGroup screenProctoringGroup = null;
|
||||||
if (reserve.hasError()) {
|
if (reserve.hasError()) {
|
||||||
if (reserve.getError() instanceof AllGroupsFullException) {
|
if (reserve.getError() instanceof AllGroupsFullException) {
|
||||||
screenProctoringGroup = applyNewGroup(settings.examId, settings.collectingGroupSize);
|
screenProctoringGroup = applyNewGroup(exam, collectingGroupSize);
|
||||||
} else {
|
} else {
|
||||||
throw new RuntimeException(
|
throw new RuntimeException(
|
||||||
"Failed to create new screen proctoring group: ",
|
"Failed to create new screen proctoring group: ",
|
||||||
|
@ -356,11 +381,9 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
||||||
return screenProctoringGroup;
|
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 = this.getSPSData(exam).spsExamUUID;
|
||||||
final String spsExamUUID = exam.getAdditionalAttribute(
|
|
||||||
ScreenProctoringAPIBinding.SPS_API.SEB_SERVER_EXAM_SETTINGS.ATTR_SPS_EXAM_UUID);
|
|
||||||
|
|
||||||
return this.screenProctoringGroupDAO
|
return this.screenProctoringGroupDAO
|
||||||
.getCollectingGroups(exam.id)
|
.getCollectingGroups(exam.id)
|
||||||
|
@ -368,14 +391,16 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
||||||
.createGroup(spsExamUUID, count.size() + 1, "Created by SEB Server", exam))
|
.createGroup(spsExamUUID, count.size() + 1, "Created by SEB Server", exam))
|
||||||
.flatMap(this.screenProctoringGroupDAO::createNewGroup)
|
.flatMap(this.screenProctoringGroupDAO::createNewGroup)
|
||||||
.flatMap(group -> this.screenProctoringGroupDAO
|
.flatMap(group -> this.screenProctoringGroupDAO
|
||||||
.reservePlaceInCollectingGroup(examId, groupSize != null ? groupSize : 0))
|
.reservePlaceInCollectingGroup(exam.id, groupSize != null ? groupSize : 0))
|
||||||
.getOrThrow();
|
.getOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Result<Exam> deleteForExam(final Long examId) {
|
private Result<Exam> deleteForExam(final Long examId) {
|
||||||
return this.examDAO.byPK(examId)
|
return this.examDAO
|
||||||
|
.byPK(examId)
|
||||||
.flatMap(this.screenProctoringAPIBinding::deleteScreenProctoring)
|
.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) {
|
private Exam cleanupAllLocalGroups(final Exam exam) {
|
||||||
|
@ -401,26 +426,54 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
||||||
newGroup, error));
|
newGroup, error));
|
||||||
}
|
}
|
||||||
|
|
||||||
// private Exam saveSettings(final Exam exam, final ScreenProctoringSettings settings) {
|
private void registerJoinInstruction(
|
||||||
//
|
final ClientConnectionRecord ccRecord,
|
||||||
// this.proctoringAdminService
|
final String spsSessionToken,
|
||||||
// .saveScreenProctoringSettings(exam.getEntityKey(), settings)
|
final ScreenProctoringGroup group,
|
||||||
// .getOrThrow();
|
final Exam exam) {
|
||||||
//
|
|
||||||
// return exam;
|
|
||||||
// }
|
|
||||||
|
|
||||||
private static final class ExamInfo {
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("Register JOIN instruction for client ");
|
||||||
|
}
|
||||||
|
|
||||||
final ScreenProctoringSettings screenProctoringSettings;
|
final SPSData spsData = getSPSData(exam);
|
||||||
final ClientCredentials sebClientCredential;
|
final String url = exam.additionalAttributes.get(ScreenProctoringSettings.ATTR_SPS_SERVICE_URL);
|
||||||
|
final Map<String, String> attributes = new HashMap<>();
|
||||||
|
|
||||||
public ExamInfo(
|
attributes.put(SERVICE_TYPE, SERVICE_TYPE_NAME);
|
||||||
final ScreenProctoringSettings screenProctoringSettings,
|
attributes.put(METHOD, ClientInstruction.ProctoringInstructionMethod.JOIN.name());
|
||||||
final ClientCredentials sebClientCredential) {
|
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.sebInstructionService
|
||||||
this.sebClientCredential = sebClientCredential;
|
.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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -85,6 +85,8 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(ExamAdministrationController.class);
|
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 ExamDAO examDAO;
|
||||||
private final UserDAO userDAO;
|
private final UserDAO userDAO;
|
||||||
private final ExamAdminService examAdminService;
|
private final ExamAdminService examAdminService;
|
||||||
|
@ -658,7 +660,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
||||||
@Override
|
@Override
|
||||||
protected Result<Exam> notifySaved(final Exam entity) {
|
protected Result<Exam> notifySaved(final Exam entity) {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
this.examAdminService.updateAdditionalExamConfigAttributes(entity.id);
|
this.examAdminService.notifyExamSaved(entity);
|
||||||
this.examSessionService.flushCache(entity);
|
this.examSessionService.flushCache(entity);
|
||||||
return entity;
|
return entity;
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,10 +12,10 @@ logging.level.ROOT=INFO
|
||||||
logging.level.ch=INFO
|
logging.level.ch=INFO
|
||||||
logging.level.ch.ethz.seb.sebserver.webservice.datalayer=INFO
|
logging.level.ch.ethz.seb.sebserver.webservice.datalayer=INFO
|
||||||
logging.level.org.springframework.cache=DEBUG
|
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=DEBUG
|
||||||
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=INFO
|
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=DEBUG
|
||||||
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator=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.weblayer.oauth=DEBUG
|
||||||
#logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl=DEBUG
|
#logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl=DEBUG
|
||||||
#logging.level.ch.ethz.seb.sebserver.webservice.datalayer.batis=DEBUG
|
#logging.level.ch.ethz.seb.sebserver.webservice.datalayer.batis=DEBUG
|
||||||
|
|
|
@ -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.actions.open=Proctoring Settings
|
||||||
sebserver.exam.proctoring.form.title=Exam Proctoring Settings
|
sebserver.exam.proctoring.form.title=Exam Proctoring Settings
|
||||||
sebserver.exam.proctoring.form.info.title=Remote Proctoring
|
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.sebserver.exam.proctoring.form.enabled=Proctoring enabled
|
||||||
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.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=Type
|
||||||
sebserver.exam.proctoring.form.type.tooltip=The type and server type of the external proctoring service.
|
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=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.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=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.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
|
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.open.error=Failed to open the collecting room.
|
||||||
sebserver.exam.proctoring.collecting.close.error=Failed to close the collecting room properly.
|
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.edit=App Signature Key
|
||||||
sebserver.exam.signaturekey.action.save=Save Settings
|
sebserver.exam.signaturekey.action.save=Save Settings
|
||||||
sebserver.exam.signaturekey.action.cancel=Cancel and back to Exam
|
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.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=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.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.ScreenProctoring=SEB Screen Proctoring Settings
|
||||||
sebserver.examconfig.props.group.screenshot=Screenshot Settings
|
sebserver.examconfig.props.group.screenshot=Screenshot Settings
|
||||||
|
|
BIN
src/main/resources/static/images/screen_proc_off.png
Normal file
BIN
src/main/resources/static/images/screen_proc_off.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 224 B |
BIN
src/main/resources/static/images/screen_proc_on.png
Normal file
BIN
src/main/resources/static/images/screen_proc_on.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 186 B |
Loading…
Reference in a new issue