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 isScreenProcteringEnabled();
|
||||
|
||||
}
|
||||
|
|
|
@ -59,4 +59,12 @@ public class FeatureServiceImpl implements FeatureService {
|
|||
return key.replaceAll("_", "-");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScreenProcteringEnabled() {
|
||||
return this.environment.getProperty(toConfigName(
|
||||
FEATURE_SETTINGS_PREFIX + "seb.screenProctoring"),
|
||||
Boolean.class,
|
||||
Boolean.FALSE);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -90,6 +90,10 @@ public class ClientCredentialServiceImpl implements ClientCredentialService {
|
|||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~`!@#$%^*()-_=+[{]}?"
|
||||
.toCharArray();
|
||||
|
||||
private final static char[] possibleLess =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
.toCharArray();
|
||||
|
||||
public static CharSequence generateClientId() {
|
||||
return RandomStringUtils.random(
|
||||
16, 0, possibleCharacters.length - 1, false, false,
|
||||
|
@ -102,6 +106,12 @@ public class ClientCredentialServiceImpl implements ClientCredentialService {
|
|||
possibleCharacters, new SecureRandom());
|
||||
}
|
||||
|
||||
public static CharSequence generateClientSecretLess() {
|
||||
return RandomStringUtils.random(
|
||||
16, 0, possibleLess.length - 1, false, false,
|
||||
possibleLess, new SecureRandom());
|
||||
}
|
||||
|
||||
public static void clearChars(final CharSequence sequence) {
|
||||
if (sequence == null) {
|
||||
return;
|
||||
|
|
|
@ -343,6 +343,16 @@ public enum ActionDefinition {
|
|||
ImageIcon.VISIBILITY_OFF,
|
||||
PageStateDefinitionImpl.EXAM_VIEW,
|
||||
ActionCategory.EXAM_SECURITY),
|
||||
SCREEN_PROCTORING_ON(
|
||||
new LocTextKey("sebserver.exam.sps.actions.open"),
|
||||
ImageIcon.SCREEN_PROC_ON,
|
||||
PageStateDefinitionImpl.EXAM_VIEW,
|
||||
ActionCategory.EXAM_SECURITY),
|
||||
SCREEN_PROCTORING_OFF(
|
||||
new LocTextKey("sebserver.exam.sps.actions.open"),
|
||||
ImageIcon.SCREEN_PROC_OFF,
|
||||
PageStateDefinitionImpl.EXAM_VIEW,
|
||||
ActionCategory.EXAM_SECURITY),
|
||||
|
||||
EXAM_CONFIGURATION_NEW(
|
||||
new LocTextKey("sebserver.exam.configuration.action.list.new"),
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.springframework.context.annotation.Lazy;
|
|||
import org.springframework.stereotype.Component;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.FeatureService;
|
||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
|
||||
|
@ -38,6 +39,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
|
|||
import ch.ethz.seb.sebserver.gbl.model.exam.ExamTemplate;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult.ErrorType;
|
||||
|
@ -66,6 +68,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckExamCon
|
|||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckSEBRestriction;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetScreenProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveExam;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetDefaultExamTemplate;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetExamTemplate;
|
||||
|
@ -149,6 +152,7 @@ public class ExamForm implements TemplateComposer {
|
|||
private final ResourceService resourceService;
|
||||
private final ExamSEBRestrictionSettings examSEBRestrictionSettings;
|
||||
private final ProctoringSettingsPopup proctoringSettingsPopup;
|
||||
private final ScreenProctoringSettingsPopup screenProctoringSettingsPopup;
|
||||
private final WidgetFactory widgetFactory;
|
||||
private final RestService restService;
|
||||
private final ExamDeletePopup examDeletePopup;
|
||||
|
@ -156,22 +160,26 @@ public class ExamForm implements TemplateComposer {
|
|||
private final ExamIndicatorsList examIndicatorsList;
|
||||
private final ExamClientGroupList examClientGroupList;
|
||||
private final ExamCreateClientConfigPopup examCreateClientConfigPopup;
|
||||
private final FeatureService featureService;
|
||||
|
||||
protected ExamForm(
|
||||
final PageService pageService,
|
||||
final ExamSEBRestrictionSettings examSEBRestrictionSettings,
|
||||
final ProctoringSettingsPopup proctoringSettingsPopup,
|
||||
final ScreenProctoringSettingsPopup screenProctoringSettingsPopup,
|
||||
final ExamToConfigBindingForm examToConfigBindingForm,
|
||||
final DownloadService downloadService,
|
||||
final ExamDeletePopup examDeletePopup,
|
||||
final ExamFormConfigs examFormConfigs,
|
||||
final ExamIndicatorsList examIndicatorsList,
|
||||
final ExamClientGroupList examClientGroupList,
|
||||
final ExamCreateClientConfigPopup examCreateClientConfigPopup) {
|
||||
final ExamCreateClientConfigPopup examCreateClientConfigPopup,
|
||||
final FeatureService featureService) {
|
||||
|
||||
this.pageService = pageService;
|
||||
this.resourceService = pageService.getResourceService();
|
||||
this.examSEBRestrictionSettings = examSEBRestrictionSettings;
|
||||
this.screenProctoringSettingsPopup = screenProctoringSettingsPopup;
|
||||
this.proctoringSettingsPopup = proctoringSettingsPopup;
|
||||
this.widgetFactory = pageService.getWidgetFactory();
|
||||
this.restService = this.resourceService.getRestService();
|
||||
|
@ -180,6 +188,7 @@ public class ExamForm implements TemplateComposer {
|
|||
this.examIndicatorsList = examIndicatorsList;
|
||||
this.examClientGroupList = examClientGroupList;
|
||||
this.examCreateClientConfigPopup = examCreateClientConfigPopup;
|
||||
this.featureService = featureService;
|
||||
|
||||
this.consistencyMessageMapping = new HashMap<>();
|
||||
this.consistencyMessageMapping.put(
|
||||
|
@ -410,6 +419,13 @@ public class ExamForm implements TemplateComposer {
|
|||
.map(ProctoringServiceSettings::getEnableProctoring)
|
||||
.getOr(false);
|
||||
|
||||
final boolean screenProctoringEnabled = importFromQuizData ? false : this.restService
|
||||
.getBuilder(GetScreenProctoringSettings.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
|
||||
.call()
|
||||
.map(ScreenProctoringSettings::getEnableScreenProctoring)
|
||||
.getOr(false);
|
||||
|
||||
final PageActionBuilder actionBuilder = this.pageService.pageActionBuilder(formContext
|
||||
.clearEntityKeys()
|
||||
.removeAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA));
|
||||
|
@ -491,7 +507,24 @@ public class ExamForm implements TemplateComposer {
|
|||
.withEntityKey(entityKey)
|
||||
.withExec(this.proctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable))
|
||||
.noEventPropagation()
|
||||
.publishIf(() -> !proctoringEnabled && readonly);
|
||||
.publishIf(() -> !proctoringEnabled && readonly)
|
||||
|
||||
.newAction(ActionDefinition.SCREEN_PROCTORING_ON)
|
||||
.withEntityKey(entityKey)
|
||||
.withExec(
|
||||
this.screenProctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable))
|
||||
.noEventPropagation()
|
||||
.publishIf(() -> this.featureService.isScreenProcteringEnabled() && screenProctoringEnabled && readonly)
|
||||
|
||||
.newAction(ActionDefinition.SCREEN_PROCTORING_OFF)
|
||||
.withEntityKey(entityKey)
|
||||
.withExec(
|
||||
this.screenProctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable))
|
||||
.noEventPropagation()
|
||||
.publishIf(
|
||||
() -> this.featureService.isScreenProcteringEnabled() && !screenProctoringEnabled && readonly)
|
||||
|
||||
;
|
||||
|
||||
// additional data in read-only view
|
||||
if (readonly && !importFromQuizData) {
|
||||
|
|
|
@ -486,8 +486,11 @@ public class ProctoringSettingsPopup {
|
|||
|
||||
}
|
||||
|
||||
private FormBuilder buildHeader(final ProctoringServiceSettings proctoringSettings,
|
||||
final FormHandleAnchor formHandleAnchor, final boolean isReadonly) {
|
||||
private FormBuilder buildHeader(
|
||||
final ProctoringServiceSettings proctoringSettings,
|
||||
final FormHandleAnchor formHandleAnchor,
|
||||
final boolean isReadonly) {
|
||||
|
||||
final FormBuilder formBuilder = this.pageService.formBuilder(
|
||||
formHandleAnchor.formContext)
|
||||
.withDefaultSpanInput(5)
|
||||
|
|
|
@ -8,19 +8,297 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.gui.content.exam;
|
||||
|
||||
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.swt.SWT;
|
||||
import org.eclipse.swt.layout.RowData;
|
||||
import org.eclipse.swt.widgets.Button;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.Entity;
|
||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.CollectingStrategy;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
|
||||
import ch.ethz.seb.sebserver.gui.form.Form;
|
||||
import ch.ethz.seb.sebserver.gui.form.FormBuilder;
|
||||
import ch.ethz.seb.sebserver.gui.form.FormHandle;
|
||||
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.ModalInputDialogComposer;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.PageService;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.event.ActionEvent;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetScreenProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveScreenProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetExamTemplateScreenProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.SaveExamTemplateScreenProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
|
||||
|
||||
@Lazy
|
||||
@Component
|
||||
@GuiProfile
|
||||
public class ScreenProctoringSettingsPopup {
|
||||
|
||||
private final static LocTextKey SEB_PROCTORING_FORM_APPKEY_SPS =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.appkey.sps");
|
||||
private final static LocTextKey SEB_PROCTORING_FORM_SECRET_SPS =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.appsecret.sps");
|
||||
private final static LocTextKey SEB_PROCTORING_FORM_ACCOUNT_ID_SPS =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.accountId.sps");
|
||||
private final static LocTextKey SEB_PROCTORING_FORM_ACCOUNT_SECRET_SPS =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.accountSecret.sps");
|
||||
private final static LocTextKey SEB_PROCTORING_FORM_COLLECT_STRATEGY =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.collect.strategy");
|
||||
private static final Logger log = LoggerFactory.getLogger(ScreenProctoringSettingsPopup.class);
|
||||
|
||||
private final static LocTextKey FORM_TITLE =
|
||||
new LocTextKey("sebserver.exam.sps.form.title");
|
||||
private final static LocTextKey FORM_INFO_TITLE =
|
||||
new LocTextKey("sebserver.exam.sps.form.info.title");
|
||||
private final static LocTextKey FORM_INFO =
|
||||
new LocTextKey("sebserver.exam.sps.form.info");
|
||||
private final static LocTextKey FORM_ENABLE =
|
||||
new LocTextKey("sebserver.exam.sps.form.enable");
|
||||
private final static LocTextKey FORM_URL =
|
||||
new LocTextKey("sebserver.exam.sps.form.url");
|
||||
private final static LocTextKey FORM_APPKEY_SPS =
|
||||
new LocTextKey("sebserver.exam.sps.form.appkey");
|
||||
private final static LocTextKey FORM_APPSECRET_SPS =
|
||||
new LocTextKey("sebserver.exam.sps.form.appsecret");
|
||||
private final static LocTextKey FORM_ACCOUNT_ID_SPS =
|
||||
new LocTextKey("sebserver.exam.sps.form.accountId");
|
||||
private final static LocTextKey FORM_ACCOUNT_SECRET_SPS =
|
||||
new LocTextKey("sebserver.exam.sps.form.accountSecret");
|
||||
private final static LocTextKey FORM_COLLECT_STRATEGY =
|
||||
new LocTextKey("sebserver.exam.sps.form.collect.strategy");
|
||||
|
||||
private final static LocTextKey SAVE_TEXT_KEY =
|
||||
new LocTextKey("sebserver.exam.sps.form.saveSettings");
|
||||
|
||||
Function<PageAction, PageAction> settingsFunction(final PageService pageService, final boolean modifyGrant) {
|
||||
return action -> {
|
||||
|
||||
final PageContext pageContext = action.pageContext()
|
||||
.withAttribute(
|
||||
PageContext.AttributeKeys.FORCE_READ_ONLY,
|
||||
(modifyGrant) ? Constants.FALSE_STRING : Constants.TRUE_STRING);
|
||||
|
||||
final ModalInputDialog<FormHandle<?>> dialog =
|
||||
new ModalInputDialog<FormHandle<?>>(
|
||||
action.pageContext().getParent().getShell(),
|
||||
pageService.getWidgetFactory())
|
||||
.setDialogWidth(860);
|
||||
|
||||
if (modifyGrant) {
|
||||
|
||||
final BiConsumer<Composite, Supplier<FormHandle<?>>> actionComposer = (composite, handle) -> {
|
||||
final WidgetFactory widgetFactory = pageService.getWidgetFactory();
|
||||
|
||||
final Button save = widgetFactory.buttonLocalized(composite, SAVE_TEXT_KEY);
|
||||
save.setLayoutData(new RowData());
|
||||
save.addListener(SWT.Selection, event -> {
|
||||
if (doSaveSettings(pageService, pageContext, handle.get())) {
|
||||
dialog.close();
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
final ScreenProctoringPropertiesForm bindFormContext = new ScreenProctoringPropertiesForm(
|
||||
pageService,
|
||||
pageContext);
|
||||
|
||||
dialog.openWithActions(
|
||||
FORM_TITLE,
|
||||
actionComposer,
|
||||
Utils.EMPTY_EXECUTION,
|
||||
bindFormContext);
|
||||
} else {
|
||||
dialog.open(
|
||||
FORM_TITLE,
|
||||
pageContext,
|
||||
pc -> new ScreenProctoringPropertiesForm(
|
||||
pageService,
|
||||
pageContext)
|
||||
.compose(pc.getParent()));
|
||||
}
|
||||
|
||||
return action;
|
||||
};
|
||||
}
|
||||
|
||||
private boolean doSaveSettings(
|
||||
final PageService pageService,
|
||||
final PageContext pageContext,
|
||||
final FormHandle<?> formHandle) {
|
||||
|
||||
final boolean isReadonly = BooleanUtils.toBoolean(
|
||||
pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY));
|
||||
if (isReadonly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final EntityKey entityKey = pageContext.getEntityKey();
|
||||
ScreenProctoringSettings settings = null;
|
||||
try {
|
||||
final Form form = formHandle.getForm();
|
||||
form.clearErrors();
|
||||
|
||||
final boolean enabled = BooleanUtils.toBoolean(
|
||||
form.getFieldValue(ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING));
|
||||
final String groupSizeString = form.getFieldValue(ScreenProctoringSettings.ATTR_COLLECTING_GROUP_SIZE);
|
||||
final int groupSize = StringUtils.isNotBlank(groupSizeString) ? Integer.parseInt(groupSizeString) : 0;
|
||||
|
||||
settings = new ScreenProctoringSettings(
|
||||
Long.parseLong(entityKey.modelId),
|
||||
enabled,
|
||||
form.getFieldValue(ScreenProctoringSettings.ATTR_SPS_SERVICE_URL),
|
||||
form.getFieldValue(ScreenProctoringSettings.ATTR_SPS_API_KEY),
|
||||
form.getFieldValue(ScreenProctoringSettings.ATTR_SPS_API_SECRET),
|
||||
form.getFieldValue(ScreenProctoringSettings.ATTR_SPS_ACCOUNT_ID),
|
||||
form.getFieldValue(ScreenProctoringSettings.ATTR_SPS_ACCOUNT_PASSWORD),
|
||||
CollectingStrategy.EXAM,
|
||||
groupSize);
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Unexpected error while trying to get settings from form: ", e);
|
||||
}
|
||||
|
||||
if (settings == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Result<ScreenProctoringSettings> saveRequest = pageService
|
||||
.getRestService()
|
||||
.getBuilder(
|
||||
entityKey.entityType == EntityType.EXAM
|
||||
? SaveScreenProctoringSettings.class
|
||||
: SaveExamTemplateScreenProctoringSettings.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
|
||||
.withBody(settings)
|
||||
.call();
|
||||
|
||||
final boolean saveOk = !saveRequest
|
||||
.onError(formHandle::handleError)
|
||||
.hasError();
|
||||
|
||||
if (saveOk) {
|
||||
final PageAction action = pageService.pageActionBuilder(pageContext)
|
||||
.newAction(
|
||||
entityKey.entityType == EntityType.EXAM
|
||||
? ActionDefinition.EXAM_VIEW_FROM_LIST
|
||||
: ActionDefinition.EXAM_TEMPLATE_VIEW_FROM_LIST)
|
||||
.create();
|
||||
|
||||
pageService.firePageEvent(
|
||||
new ActionEvent(action),
|
||||
action.pageContext());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private final class ScreenProctoringPropertiesForm
|
||||
implements ModalInputDialogComposer<FormHandle<?>> {
|
||||
|
||||
private final PageService pageService;
|
||||
private final PageContext pageContext;
|
||||
|
||||
protected ScreenProctoringPropertiesForm(
|
||||
final PageService pageService,
|
||||
final PageContext pageContext) {
|
||||
|
||||
this.pageService = pageService;
|
||||
this.pageContext = pageContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Supplier<FormHandle<?>> compose(final Composite parent) {
|
||||
final RestService restService = this.pageService.getRestService();
|
||||
final EntityKey entityKey = this.pageContext.getEntityKey();
|
||||
|
||||
final Composite content = this.pageService
|
||||
.getWidgetFactory()
|
||||
.createPopupScrollComposite(parent);
|
||||
|
||||
final PageContext formContext = this.pageContext
|
||||
.copyOf(content)
|
||||
.clearEntityKeys();
|
||||
|
||||
final ScreenProctoringSettings settings = restService
|
||||
.getBuilder(
|
||||
entityKey.entityType == EntityType.EXAM
|
||||
? GetScreenProctoringSettings.class
|
||||
: GetExamTemplateScreenProctoringSettings.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
|
||||
.call()
|
||||
.getOrThrow();
|
||||
|
||||
final boolean isReadonly = BooleanUtils.toBoolean(
|
||||
this.pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY));
|
||||
|
||||
final FormHandle<Entity> form = this.pageService.formBuilder(formContext)
|
||||
.withDefaultSpanInput(5)
|
||||
.withEmptyCellSeparation(true)
|
||||
.withDefaultSpanEmptyCell(1)
|
||||
.readonly(isReadonly)
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
"Info",
|
||||
FORM_INFO_TITLE,
|
||||
this.pageService.getI18nSupport().getText(FORM_INFO))
|
||||
.asArea(80)
|
||||
.asHTML()
|
||||
.readonly(true))
|
||||
|
||||
.addField(FormBuilder.checkbox(
|
||||
ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING,
|
||||
FORM_ENABLE,
|
||||
String.valueOf(settings.enableScreenProctoring)))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
ScreenProctoringSettings.ATTR_SPS_SERVICE_URL,
|
||||
FORM_URL,
|
||||
settings.spsServiceURL)
|
||||
.mandatory())
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
ScreenProctoringSettings.ATTR_SPS_API_KEY,
|
||||
FORM_APPKEY_SPS,
|
||||
settings.spsAPIKey))
|
||||
.withEmptyCellSeparation(false)
|
||||
|
||||
.addField(FormBuilder.password(
|
||||
ScreenProctoringSettings.ATTR_SPS_API_SECRET,
|
||||
FORM_APPSECRET_SPS,
|
||||
(settings.spsAPISecret != null)
|
||||
? String.valueOf(settings.spsAPISecret)
|
||||
: null))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
ScreenProctoringSettings.ATTR_SPS_ACCOUNT_ID,
|
||||
FORM_ACCOUNT_ID_SPS,
|
||||
settings.spsAccountId))
|
||||
.withEmptyCellSeparation(false)
|
||||
|
||||
.addField(FormBuilder.password(
|
||||
ScreenProctoringSettings.ATTR_SPS_ACCOUNT_PASSWORD,
|
||||
FORM_ACCOUNT_SECRET_SPS,
|
||||
(settings.spsAccountPassword != null)
|
||||
? String.valueOf(settings.spsAccountPassword)
|
||||
: null))
|
||||
|
||||
.build();
|
||||
|
||||
return () -> form;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
SHIELD("shield.png"),
|
||||
NO_SHIELD("no_shield.png"),
|
||||
BACK("back.png");
|
||||
BACK("back.png"),
|
||||
SCREEN_PROC_ON("screen_proc_on.png"),
|
||||
SCREEN_PROC_OFF("screen_proc_off.png");
|
||||
|
||||
public String fileName;
|
||||
private ImageData image = null;
|
||||
|
|
|
@ -211,6 +211,16 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
|
|||
key = "#examId")
|
||||
Result<QuizData> updateQuizData(Long examId, QuizData quizData, String updateId);
|
||||
|
||||
/** Marks the given exam by setting the last-update-time to current time.
|
||||
* This provokes a cache update on distributed setup.
|
||||
* Additionally this flushes the local cache immediately.
|
||||
*
|
||||
* @param examId the Exam identifier */
|
||||
@CacheEvict(
|
||||
cacheNames = ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM,
|
||||
key = "#examId")
|
||||
void markUpdate(Long examId);
|
||||
|
||||
/** This is used by the internal update process to mark exams for which the LMS related data availability
|
||||
*
|
||||
* @param externalQuizId The exams external UUID or quiz id of the exam to mark
|
||||
|
|
|
@ -27,4 +27,8 @@ public interface ProctoringSettingsDAO {
|
|||
final EntityKey entityKey,
|
||||
final ScreenProctoringSettings screenProctoringSettings);
|
||||
|
||||
void disableScreenProctoring(Long examId);
|
||||
|
||||
boolean isScreenProctoringEnabled(Long examId);
|
||||
|
||||
}
|
||||
|
|
|
@ -238,7 +238,7 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
|
|||
.build()
|
||||
.execute();
|
||||
|
||||
final List<ClientConnectionRecord> execute = this.clientConnectionRecordMapper
|
||||
this.clientConnectionRecordMapper
|
||||
.selectByExample()
|
||||
.build()
|
||||
.execute();
|
||||
|
@ -340,8 +340,6 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
|
|||
final long millisecondsNow = Utils.getMillisecondsNow();
|
||||
// NOTE: we use nanoseconds here to get a better precision to better avoid
|
||||
// same value of real concurrent calls on distributed systems
|
||||
// TODO: Better solution for the future would be to count this value and
|
||||
// isolation is done via DB transaction
|
||||
final long nanosecondsNow = System.nanoTime();
|
||||
final ClientConnectionRecord newRecord = new ClientConnectionRecord(
|
||||
null,
|
||||
|
|
|
@ -159,6 +159,28 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
.map(rec -> saveAdditionalQuizAttributes(examId, quizData));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void markUpdate(final Long examId) {
|
||||
|
||||
try {
|
||||
|
||||
final long millisecondsNow = Utils.getMillisecondsNow();
|
||||
|
||||
UpdateDSL.updateWithMapper(
|
||||
this.examRecordMapper::update,
|
||||
ExamRecordDynamicSqlSupport.examRecord)
|
||||
.set(ExamRecordDynamicSqlSupport.lastModified)
|
||||
.equalTo(millisecondsNow)
|
||||
.where(ExamRecordDynamicSqlSupport.id, isEqualTo(examId))
|
||||
.build()
|
||||
.execute();
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to mark exam for update on distributed setup. exam: {}", examId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markLMSAvailability(final String externalQuizId, final boolean available, final String updateId) {
|
||||
|
||||
|
|
|
@ -235,6 +235,30 @@ public class ProctoringSettingsDAOImpl implements ProctoringSettingsDAO {
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void disableScreenProctoring(final Long examId) {
|
||||
this.additionalAttributesDAO.saveAdditionalAttribute(
|
||||
EntityType.EXAM,
|
||||
examId,
|
||||
ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING,
|
||||
Constants.FALSE_STRING)
|
||||
.onError(error -> log.warn(
|
||||
"Failed to disable screen proctoring for exam: {} error: {}",
|
||||
examId,
|
||||
error.getMessage()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public boolean isScreenProctoringEnabled(final Long examId) {
|
||||
return this.additionalAttributesDAO.getAdditionalAttribute(EntityType.EXAM,
|
||||
examId,
|
||||
ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING)
|
||||
.map(attrRec -> BooleanUtils.toBoolean(attrRec.getValue()))
|
||||
.getOr(false);
|
||||
}
|
||||
|
||||
private Boolean getEnabled(final Map<String, AdditionalAttributeRecord> mapping) {
|
||||
if (mapping.containsKey(ProctoringServiceSettings.ATTR_ENABLE_PROCTORING)) {
|
||||
return BooleanUtils.toBoolean(mapping.get(ProctoringServiceSettings.ATTR_ENABLE_PROCTORING).getValue());
|
||||
|
|
|
@ -136,6 +136,11 @@ public interface ExamAdminService {
|
|||
* @return Result refer to the archived exam or to an error when happened */
|
||||
Result<Exam> archiveExam(Exam exam);
|
||||
|
||||
/** Gets invoked after an exam has been changed and saved.
|
||||
*
|
||||
* @param exam the exam that has been changed and saved */
|
||||
void notifyExamSaved(Exam exam);
|
||||
|
||||
/** Used to check threshold consistency for a given list of thresholds.
|
||||
* Checks if all values are present (none null value)
|
||||
* Checks if there are duplicates
|
||||
|
|
|
@ -20,6 +20,7 @@ import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
|||
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
|
||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
|
||||
|
@ -70,6 +71,11 @@ public interface ProctoringAdminService {
|
|||
* @return ExamProctoringService instance */
|
||||
Result<RemoteProctoringService> getExamProctoringService(final ProctoringServerType type);
|
||||
|
||||
/** Gets invoked after an exam has been changed and saved.
|
||||
*
|
||||
* @param exam the exam that has been changed and saved */
|
||||
void notifyExamSaved(Exam exam);
|
||||
|
||||
/** Use this to test the proctoring service settings against the remote proctoring server.
|
||||
*
|
||||
* @param proctoringSettings the settings to test
|
||||
|
|
|
@ -299,6 +299,12 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
|||
.map(settings -> exam);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyExamSaved(final Exam exam) {
|
||||
updateAdditionalExamConfigAttributes(exam.id);
|
||||
this.proctoringAdminService.notifyExamSaved(exam);
|
||||
}
|
||||
|
||||
private Result<Exam> initAdditionalAttributesForMoodleExams(final Exam exam) {
|
||||
return Result.tryCatch(() -> {
|
||||
final LmsAPITemplate lmsTemplate = this.lmsAPIService
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.springframework.stereotype.Service;
|
|||
|
||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
|
||||
|
@ -92,8 +93,6 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
|
|||
|
||||
checkType(parentEntityKey);
|
||||
|
||||
final boolean notifyChangesToService = notifyChangesToService(parentEntityKey, screenProctoringSettings);
|
||||
|
||||
this.screenProctoringService
|
||||
.testSettings(screenProctoringSettings)
|
||||
.flatMap(settings -> this.proctoringSettingsDAO.storeScreenProctoringSettings(
|
||||
|
@ -101,9 +100,12 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
|
|||
screenProctoringSettings))
|
||||
.getOrThrow();
|
||||
|
||||
if (notifyChangesToService) {
|
||||
if (parentEntityKey.entityType == EntityType.EXAM) {
|
||||
|
||||
this.screenProctoringService
|
||||
.applyScreenProctoingForExam(screenProctoringSettings)
|
||||
.applyScreenProctoingForExam(screenProctoringSettings.examId)
|
||||
.onError(error -> this.proctoringSettingsDAO
|
||||
.disableScreenProctoring(screenProctoringSettings.examId))
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
|
@ -111,26 +113,17 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
|
|||
});
|
||||
}
|
||||
|
||||
private boolean notifyChangesToService(
|
||||
final EntityKey entityKey,
|
||||
final ScreenProctoringSettings newSettings) {
|
||||
|
||||
if (entityKey.entityType != EntityType.EXAM) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.proctoringSettingsDAO
|
||||
.getScreenProctoringSettings(entityKey)
|
||||
.map(oldSettings -> oldSettings.enableScreenProctoring != newSettings.enableScreenProctoring)
|
||||
.getOr(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RemoteProctoringService> getExamProctoringService(final ProctoringServerType type) {
|
||||
return this.remoteProctoringServiceFactory
|
||||
.getExamProctoringService(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyExamSaved(final Exam exam) {
|
||||
this.screenProctoringService.notifyExamSaved(exam);
|
||||
}
|
||||
|
||||
private void checkType(final EntityKey parentEntityKey) {
|
||||
if (!SUPPORTED_PARENT_ENTITES.contains(parentEntityKey.entityType)) {
|
||||
throw new UnsupportedOperationException(
|
||||
|
|
|
@ -40,6 +40,11 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
|||
|
||||
public class MockCourseAccessAPI implements CourseAccessAPI {
|
||||
|
||||
private static final String startTime10 = DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS)
|
||||
.toString(Constants.DEFAULT_DATE_TIME_FORMAT);
|
||||
private static final String endTime10 = DateTime.now(DateTimeZone.UTC).plus(6 * Constants.MINUTE_IN_MILLIS)
|
||||
.toString(Constants.DEFAULT_DATE_TIME_FORMAT);
|
||||
|
||||
private final Collection<QuizData> mockups;
|
||||
private final WebserviceInfo webserviceInfo;
|
||||
private final APITemplateDataSupplier apiTemplateDataSupplier;
|
||||
|
@ -85,10 +90,8 @@ public class MockCourseAccessAPI implements CourseAccessAPI {
|
|||
this.mockups.add(new QuizData(
|
||||
"quiz10", institutionId, lmsSetupId, lmsType, "Demo Quiz 10 (MOCKUP)",
|
||||
"Starts in a minute and ends after five minutes",
|
||||
DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS)
|
||||
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
|
||||
DateTime.now(DateTimeZone.UTC).plus(6 * Constants.MINUTE_IN_MILLIS)
|
||||
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
|
||||
MockCourseAccessAPI.startTime10,
|
||||
MockCourseAccessAPI.endTime10,
|
||||
"http://lms.mockup.com/api/"));
|
||||
this.mockups.add(new QuizData(
|
||||
"quiz11", institutionId, lmsSetupId, lmsType, "Demo Quiz 11 (MOCKUP)",
|
||||
|
|
|
@ -34,17 +34,26 @@ public interface ScreenProctoringService extends SessionUpdateTask {
|
|||
* @return Result refer to the settings or to an error when happened */
|
||||
Result<ScreenProctoringSettings> testSettings(ScreenProctoringSettings settings);
|
||||
|
||||
//Result<ScreenProctoringSettings> saveSettingsForExam(ScreenProctoringSettings settings);
|
||||
|
||||
/** This applies the screen proctoring for the given exam.
|
||||
/** This applies the stored screen proctoring for the given exam.
|
||||
* If screen proctoring for the exam is enabled, this initializes or re-activate all
|
||||
* needed things for screen proctoring for the given exam.
|
||||
* If screen proctoring is set to disable, this disables the whole service for the
|
||||
* given exam also on SPS side.
|
||||
*
|
||||
* @param settings the actual ScreenProctoringSettings of the exam
|
||||
* @return Result refer to the given ScreenProctoringSettings or to an error when happened */
|
||||
Result<ScreenProctoringSettings> applyScreenProctoingForExam(ScreenProctoringSettings settings);
|
||||
* @param examId use the screen proctoring settings of the exam with the given exam id
|
||||
* @return Result refer to the given Exam or to an error when happened */
|
||||
Result<Exam> applyScreenProctoingForExam(Long examId);
|
||||
|
||||
/** Gets invoked after an exam has been changed and saved.
|
||||
*
|
||||
* @param exam the exam that has been changed and saved */
|
||||
void notifyExamSaved(Exam exam);
|
||||
|
||||
@EventListener(ExamStartedEvent.class)
|
||||
void notifyExamStarted(ExamStartedEvent event);
|
||||
|
||||
@EventListener(ExamFinishedEvent.class)
|
||||
void notifyExamFinished(ExamFinishedEvent event);
|
||||
|
||||
/** This is been called just before an Exam gets deleted on the permanent storage.
|
||||
* This deactivates and dispose or deletes all exam relevant domain entities on the SPS service side.
|
||||
|
|
|
@ -18,7 +18,6 @@ import org.springframework.context.annotation.Lazy;
|
|||
import org.springframework.stereotype.Service;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
|
@ -52,7 +51,6 @@ public class ExamSessionCacheService {
|
|||
private final ClientConnectionDAO clientConnectionDAO;
|
||||
private final InternalClientConnectionDataFactory internalClientConnectionDataFactory;
|
||||
private final ExamConfigService sebExamConfigService;
|
||||
private final ExamUpdateHandler examUpdateHandler;
|
||||
|
||||
protected ExamSessionCacheService(
|
||||
final ExamDAO examDAO,
|
||||
|
@ -60,7 +58,6 @@ public class ExamSessionCacheService {
|
|||
final ClientConnectionDAO clientConnectionDAO,
|
||||
final InternalClientConnectionDataFactory internalClientConnectionDataFactory,
|
||||
final ExamConfigService sebExamConfigService,
|
||||
final ExamUpdateHandler examUpdateHandler,
|
||||
final RemoteProctoringRoomDAO remoteProctoringRoomDAO) {
|
||||
|
||||
this.examDAO = examDAO;
|
||||
|
@ -68,7 +65,6 @@ public class ExamSessionCacheService {
|
|||
this.clientConnectionDAO = clientConnectionDAO;
|
||||
this.internalClientConnectionDataFactory = internalClientConnectionDataFactory;
|
||||
this.sebExamConfigService = sebExamConfigService;
|
||||
this.examUpdateHandler = examUpdateHandler;
|
||||
}
|
||||
|
||||
@Cacheable(
|
||||
|
@ -132,9 +128,11 @@ public class ExamSessionCacheService {
|
|||
}
|
||||
case UP_COMING:
|
||||
case FINISHED: {
|
||||
return this.examUpdateHandler.updateRunning(exam.id)
|
||||
.map(e -> e.status == ExamStatus.RUNNING)
|
||||
.getOr(false);
|
||||
return false;
|
||||
// TODO do we really need to double-check here?
|
||||
// return this.examUpdateHandler.updateRunning(exam.id)
|
||||
// .map(e -> e.status == ExamStatus.RUNNING)
|
||||
// .getOr(false);
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
|
|
|
@ -532,6 +532,9 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
@Override
|
||||
public Result<Exam> updateExamCache(final Long examId) {
|
||||
|
||||
// TODO check how often this is called in distributed environments
|
||||
System.out.println("************** performance check: updateExamCache");
|
||||
|
||||
try {
|
||||
final Cache cache = this.cacheManager.getCache(ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM);
|
||||
final ValueWrapper valueWrapper = cache.get(examId);
|
||||
|
@ -547,7 +550,8 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
return Result.ofEmpty();
|
||||
}
|
||||
|
||||
final Boolean isUpToDate = this.examDAO.upToDate(exam)
|
||||
final Boolean isUpToDate = this.examDAO
|
||||
.upToDate(exam)
|
||||
.onError(t -> log.error("Failed to verify if cached exam is up to date: {}", exam, t))
|
||||
.getOr(false);
|
||||
|
||||
|
|
|
@ -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.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||
|
@ -216,18 +217,16 @@ class ExamUpdateHandler implements ExamUpdateTask {
|
|||
});
|
||||
}
|
||||
|
||||
Result<Exam> updateRunning(final Long examId) {
|
||||
return this.examDAO.byPK(examId)
|
||||
.map(exam -> {
|
||||
final DateTime now = DateTime.now(DateTimeZone.UTC);
|
||||
if (exam.getStatus() == ExamStatus.UP_COMING
|
||||
&& exam.endTime.plus(this.examTimeSuffix).isBefore(now)) {
|
||||
return setRunning(exam, this.createUpdateId())
|
||||
.getOr(exam);
|
||||
} else {
|
||||
return exam;
|
||||
}
|
||||
});
|
||||
@EventListener(ExamUpdateEvent.class)
|
||||
void updateRunning(final ExamUpdateEvent event) {
|
||||
this.examDAO
|
||||
.byPK(event.examId)
|
||||
.onSuccess(exam -> updateState(
|
||||
exam,
|
||||
DateTime.now(DateTimeZone.UTC),
|
||||
this.examTimePrefix,
|
||||
this.examTimeSuffix,
|
||||
this.createUpdateId()));
|
||||
}
|
||||
|
||||
void updateState(
|
||||
|
|
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.APIMessageException;
|
||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException;
|
||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
|
||||
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
|
||||
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
|
||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.CollectingStrategy;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
|
||||
|
@ -42,13 +39,17 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRe
|
|||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ProctoringSettingsDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ScreenProctoringGroupDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ExamDeletionEvent;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ProctoringSettingsDAOImpl;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ScreenProctoringGroupDAOImpl.AllGroupsFullException;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ExamSessionCacheService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.ScreenProctoringAPIBinding.SPSData;
|
||||
|
||||
@Lazy
|
||||
@Service
|
||||
|
@ -58,16 +59,18 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
|||
private static final Logger log = LoggerFactory.getLogger(ScreenProctoringServiceImpl.class);
|
||||
|
||||
private final Cryptor cryptor;
|
||||
private final JSONMapper jsonMapper;
|
||||
private final ScreenProctoringAPIBinding screenProctoringAPIBinding;
|
||||
private final ScreenProctoringGroupDAO screenProctoringGroupDAO;
|
||||
private final ProctoringSettingsDAO proctoringSettingsDAO;
|
||||
private final ClientConnectionDAO clientConnectionDAO;
|
||||
private final ExamDAO examDAO;
|
||||
private final ProctoringSettingsDAOImpl proctoringSettingsSupplier;
|
||||
private final SEBClientInstructionService sebInstructionService;
|
||||
private final ExamSessionCacheService examSessionCacheService;
|
||||
|
||||
public ScreenProctoringServiceImpl(
|
||||
final Cryptor cryptor,
|
||||
final ProctoringSettingsDAOImpl proctoringSettingsSupplier,
|
||||
final ProctoringSettingsDAO proctoringSettingsDAO,
|
||||
final AsyncService asyncService,
|
||||
final JSONMapper jsonMapper,
|
||||
final UserDAO userDAO,
|
||||
|
@ -75,21 +78,24 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
|||
final ClientConnectionDAO clientConnectionDAO,
|
||||
final AdditionalAttributesDAO additionalAttributesDAO,
|
||||
final ScreenProctoringGroupDAO screenProctoringGroupDAO,
|
||||
final SEBClientInstructionService sebInstructionService) {
|
||||
final SEBClientInstructionService sebInstructionService,
|
||||
final ExamSessionCacheService examSessionCacheService) {
|
||||
|
||||
this.cryptor = cryptor;
|
||||
this.jsonMapper = jsonMapper;
|
||||
this.examDAO = examDAO;
|
||||
this.proctoringSettingsSupplier = proctoringSettingsSupplier;
|
||||
this.screenProctoringGroupDAO = screenProctoringGroupDAO;
|
||||
this.clientConnectionDAO = clientConnectionDAO;
|
||||
this.sebInstructionService = sebInstructionService;
|
||||
this.examSessionCacheService = examSessionCacheService;
|
||||
this.proctoringSettingsDAO = proctoringSettingsDAO;
|
||||
|
||||
this.screenProctoringAPIBinding = new ScreenProctoringAPIBinding(
|
||||
userDAO,
|
||||
cryptor,
|
||||
asyncService,
|
||||
jsonMapper,
|
||||
proctoringSettingsSupplier,
|
||||
proctoringSettingsDAO,
|
||||
additionalAttributesDAO);
|
||||
}
|
||||
|
||||
|
@ -97,14 +103,19 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
|||
public Result<ScreenProctoringSettings> testSettings(final ScreenProctoringSettings screenProctoringSettings) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
final Collection<APIMessage> fieldChecks = new ArrayList<>();
|
||||
if (StringUtils.isBlank(screenProctoringSettings.spsServiceURL)) {
|
||||
fieldChecks.add(APIMessage.fieldValidationError(
|
||||
"appKey",
|
||||
"screenProctoringSettings:spsServiceURL:notNull"));
|
||||
}
|
||||
if (screenProctoringSettings.spsServiceURL != null
|
||||
&& screenProctoringSettings.spsServiceURL.contains("?")) {
|
||||
throw new FieldValidationException(
|
||||
"serverURL",
|
||||
"screenProctoringSettings:spsServiceURL:invalidURL");
|
||||
fieldChecks.add(APIMessage.fieldValidationError(
|
||||
"appKey",
|
||||
"screenProctoringSettings:spsServiceURL:invalidURL"));
|
||||
}
|
||||
|
||||
final Collection<APIMessage> fieldChecks = new ArrayList<>();
|
||||
if (StringUtils.isBlank(screenProctoringSettings.spsAPIKey)) {
|
||||
fieldChecks.add(APIMessage.fieldValidationError(
|
||||
"appKey",
|
||||
|
@ -150,27 +161,41 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Result<ScreenProctoringSettings> applyScreenProctoingForExam(final ScreenProctoringSettings settings) {
|
||||
public Result<Exam> applyScreenProctoingForExam(final Long examId) {
|
||||
|
||||
return this.examDAO
|
||||
.byPK(settings.examId)
|
||||
.byPK(examId)
|
||||
.map(exam -> {
|
||||
|
||||
if (BooleanUtils.toBoolean(settings.enableScreenProctoring)) {
|
||||
final boolean isSPSActive = this.screenProctoringAPIBinding.isSPSActive(exam);
|
||||
final boolean isEnabling = this.proctoringSettingsDAO.isScreenProctoringEnabled(examId);
|
||||
|
||||
if (isEnabling && !isSPSActive) {
|
||||
|
||||
this.screenProctoringAPIBinding
|
||||
.startScreenProctoring(exam)
|
||||
.onError(error -> log.error(
|
||||
"Failed to apply screen proctoring for exam: {}",
|
||||
exam,
|
||||
error))
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
.forEach(newGroup -> createNewLocalGroup(exam, newGroup));
|
||||
} else {
|
||||
|
||||
this.examDAO.markUpdate(exam.id);
|
||||
|
||||
} else if (!isEnabling && isSPSActive) {
|
||||
|
||||
this.screenProctoringAPIBinding
|
||||
.dispsoseScreenProctoring(exam)
|
||||
.onError(error -> log.error("Failed to dispose screen proctoring for exam: {}",
|
||||
exam,
|
||||
error))
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
return settings;
|
||||
this.examDAO.markUpdate(exam.id);
|
||||
}
|
||||
return exam;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -204,20 +229,51 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
|||
public void updateClientConnections() {
|
||||
try {
|
||||
|
||||
final Map<Long, ExamInfo> examInfoCache = new HashMap<>();
|
||||
|
||||
this.examDAO
|
||||
.allIdsOfRunningWithScreenProctoringEnabled()
|
||||
.flatMap(this.clientConnectionDAO::getAllForScreenProctoringUpdate)
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
.forEach(cc -> applyScreenProctoringSession(cc, examInfoCache));
|
||||
.forEach(cc -> applyScreenProctoringSession(cc));
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to update active SEB connections for screen proctoring");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyExamSaved(final Exam exam) {
|
||||
final String enabeld = exam.additionalAttributes
|
||||
.get(ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING);
|
||||
|
||||
if (!BooleanUtils.toBoolean(enabeld)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.screenProctoringAPIBinding.synchronizeUserAccounts(exam);
|
||||
this.screenProctoringAPIBinding.createExamReadPrivileges(exam);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyExamStarted(final ExamStartedEvent event) {
|
||||
final Exam exam = event.exam;
|
||||
if (!BooleanUtils.toBoolean(exam.additionalAttributes.get(SPSData.ATTR_SPS_ACTIVE))) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.screenProctoringAPIBinding.activateSEBAccessOnSPS(exam, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyExamFinished(final ExamFinishedEvent event) {
|
||||
final Exam exam = event.exam;
|
||||
if (!BooleanUtils.toBoolean(exam.additionalAttributes.get(SPSData.ATTR_SPS_ACTIVE))) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.screenProctoringAPIBinding.activateSEBAccessOnSPS(exam, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyExamDeletion(final ExamDeletionEvent event) {
|
||||
event.ids
|
||||
|
@ -234,86 +290,42 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
|||
});
|
||||
}
|
||||
|
||||
private void applyScreenProctoringSession(
|
||||
final ClientConnectionRecord ccRecord,
|
||||
final Map<Long, ExamInfo> examInfoCache) {
|
||||
private void applyScreenProctoringSession(final ClientConnectionRecord ccRecord) {
|
||||
|
||||
try {
|
||||
|
||||
final Long examId = ccRecord.getExamId();
|
||||
|
||||
// process based caching...
|
||||
if (!examInfoCache.containsKey(examId)) {
|
||||
final ScreenProctoringSettings settings = this.proctoringSettingsSupplier
|
||||
.getScreenProctoringSettings(new EntityKey(examId, EntityType.EXAM))
|
||||
.getOrThrow();
|
||||
final ClientCredentials sebClientCredential = this.screenProctoringAPIBinding
|
||||
.getSEBClientCredentials(examId)
|
||||
.getOrThrow();
|
||||
|
||||
examInfoCache.put(examId, new ExamInfo(settings, sebClientCredential));
|
||||
}
|
||||
|
||||
final ExamInfo examInfo = examInfoCache.get(examId);
|
||||
final Exam runningExam = this.examSessionCacheService.getRunningExam(examId);
|
||||
|
||||
// apply SEB connection to screen proctoring group
|
||||
final ScreenProctoringGroup group = applySEBConnectionToGroup(
|
||||
ccRecord,
|
||||
examInfo.screenProctoringSettings);
|
||||
runningExam);
|
||||
|
||||
// create screen proctoring session for SEB connection on SPS service
|
||||
final String spsSessionToken = this.screenProctoringAPIBinding
|
||||
.createSEBSession(examId, group, ccRecord);
|
||||
|
||||
// create instruction for SEB and add it to instruction queue for SEB connection
|
||||
registerJoinInstruction(ccRecord, spsSessionToken, group, examInfo);
|
||||
registerJoinInstruction(ccRecord, spsSessionToken, group, runningExam);
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to apply screen proctoring session to SEB with connection: ", ccRecord, e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void registerJoinInstruction(
|
||||
final ClientConnectionRecord ccRecord,
|
||||
final String spsSessionToken,
|
||||
final ScreenProctoringGroup group,
|
||||
final ExamInfo examInfo) {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Register JOIN instruction for client ");
|
||||
}
|
||||
|
||||
final Long examId = examInfo.screenProctoringSettings.examId;
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
attributes.put(SERVICE_TYPE, SERVICE_TYPE_NAME);
|
||||
attributes.put(METHOD, ClientInstruction.ProctoringInstructionMethod.JOIN.name());
|
||||
attributes.put(URL, examInfo.screenProctoringSettings.getSpsServiceURL());
|
||||
attributes.put(CLIENT_ID, examInfo.sebClientCredential.clientIdAsString());
|
||||
attributes.put(CLIENT_SECRET, this.cryptor.decrypt(examInfo.sebClientCredential.secret).toString());
|
||||
attributes.put(GROUP_ID, group.uuid);
|
||||
attributes.put(SESSION_ID, spsSessionToken);
|
||||
|
||||
this.sebInstructionService
|
||||
.registerInstruction(
|
||||
examId,
|
||||
InstructionType.SEB_PROCTORING,
|
||||
attributes,
|
||||
ccRecord.getConnectionToken(),
|
||||
true)
|
||||
.onError(error -> log.error(
|
||||
"Failed to register screen proctoring join instruction for SEB connection: {}",
|
||||
ccRecord,
|
||||
error));
|
||||
|
||||
}
|
||||
|
||||
private ScreenProctoringGroup applySEBConnectionToGroup(
|
||||
final ClientConnectionRecord ccRecord,
|
||||
final ScreenProctoringSettings settings) {
|
||||
final Exam exam) {
|
||||
|
||||
final Long examId = ccRecord.getExamId();
|
||||
switch (settings.collectingStrategy) {
|
||||
if (!exam.additionalAttributes.containsKey(ScreenProctoringSettings.ATTR_COLLECTING_STRATEGY)) {
|
||||
log.warn("Can't verify collecting strategy for exam: {} use default group assignment.", exam.id);
|
||||
return applyToDefaultGroup(ccRecord, exam);
|
||||
}
|
||||
|
||||
final CollectingStrategy strategy = CollectingStrategy.valueOf(exam.additionalAttributes
|
||||
.get(ScreenProctoringSettings.ATTR_COLLECTING_STRATEGY));
|
||||
|
||||
switch (strategy) {
|
||||
case SEB_GROUP: {
|
||||
// TODO
|
||||
throw new UnsupportedOperationException("SEB_GROUP based group collection is not supported yet");
|
||||
|
@ -321,30 +333,43 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
|||
case EXAM:
|
||||
case FIX_SIZE:
|
||||
default: {
|
||||
|
||||
final ScreenProctoringGroup screenProctoringGroup = getProctoringGroup(settings);
|
||||
this.clientConnectionDAO.assignToScreenProctoringGroup(
|
||||
examId,
|
||||
ccRecord.getConnectionToken(),
|
||||
screenProctoringGroup.id)
|
||||
.getOrThrow();
|
||||
|
||||
return screenProctoringGroup;
|
||||
return applyToDefaultGroup(ccRecord, exam);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private ScreenProctoringGroup getProctoringGroup(final ScreenProctoringSettings settings) {
|
||||
private ScreenProctoringGroup applyToDefaultGroup(
|
||||
final ClientConnectionRecord ccRecord,
|
||||
final Exam exam) {
|
||||
|
||||
final ScreenProctoringGroup screenProctoringGroup = getProctoringGroup(exam);
|
||||
this.clientConnectionDAO.assignToScreenProctoringGroup(
|
||||
exam.id,
|
||||
ccRecord.getConnectionToken(),
|
||||
screenProctoringGroup.id)
|
||||
.getOrThrow();
|
||||
|
||||
return screenProctoringGroup;
|
||||
}
|
||||
|
||||
private ScreenProctoringGroup getProctoringGroup(final Exam exam) {
|
||||
|
||||
int collectingGroupSize = 0;
|
||||
if (exam.additionalAttributes.containsKey(ScreenProctoringSettings.ATTR_COLLECTING_GROUP_SIZE)) {
|
||||
collectingGroupSize = Integer.parseInt(exam.additionalAttributes
|
||||
.get(ScreenProctoringSettings.ATTR_COLLECTING_GROUP_SIZE));
|
||||
}
|
||||
|
||||
final Result<ScreenProctoringGroup> reserve = this.screenProctoringGroupDAO
|
||||
.reservePlaceInCollectingGroup(
|
||||
settings.examId,
|
||||
settings.collectingGroupSize != null ? settings.collectingGroupSize : 0);
|
||||
exam.id,
|
||||
collectingGroupSize);
|
||||
|
||||
ScreenProctoringGroup screenProctoringGroup = null;
|
||||
if (reserve.hasError()) {
|
||||
if (reserve.getError() instanceof AllGroupsFullException) {
|
||||
screenProctoringGroup = applyNewGroup(settings.examId, settings.collectingGroupSize);
|
||||
screenProctoringGroup = applyNewGroup(exam, collectingGroupSize);
|
||||
} else {
|
||||
throw new RuntimeException(
|
||||
"Failed to create new screen proctoring group: ",
|
||||
|
@ -356,11 +381,9 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
|||
return screenProctoringGroup;
|
||||
}
|
||||
|
||||
private ScreenProctoringGroup applyNewGroup(final Long examId, final Integer groupSize) {
|
||||
private ScreenProctoringGroup applyNewGroup(final Exam exam, final Integer groupSize) {
|
||||
|
||||
final Exam exam = this.examDAO.byPK(examId).getOrThrow();
|
||||
final String spsExamUUID = exam.getAdditionalAttribute(
|
||||
ScreenProctoringAPIBinding.SPS_API.SEB_SERVER_EXAM_SETTINGS.ATTR_SPS_EXAM_UUID);
|
||||
final String spsExamUUID = this.getSPSData(exam).spsExamUUID;
|
||||
|
||||
return this.screenProctoringGroupDAO
|
||||
.getCollectingGroups(exam.id)
|
||||
|
@ -368,14 +391,16 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
|||
.createGroup(spsExamUUID, count.size() + 1, "Created by SEB Server", exam))
|
||||
.flatMap(this.screenProctoringGroupDAO::createNewGroup)
|
||||
.flatMap(group -> this.screenProctoringGroupDAO
|
||||
.reservePlaceInCollectingGroup(examId, groupSize != null ? groupSize : 0))
|
||||
.reservePlaceInCollectingGroup(exam.id, groupSize != null ? groupSize : 0))
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
private Result<Exam> deleteForExam(final Long examId) {
|
||||
return this.examDAO.byPK(examId)
|
||||
return this.examDAO
|
||||
.byPK(examId)
|
||||
.flatMap(this.screenProctoringAPIBinding::deleteScreenProctoring)
|
||||
.map(this::cleanupAllLocalGroups);
|
||||
.map(this::cleanupAllLocalGroups)
|
||||
.onError(error -> log.error("Failed to delete SPS integration for exam: {}", examId, error));
|
||||
}
|
||||
|
||||
private Exam cleanupAllLocalGroups(final Exam exam) {
|
||||
|
@ -401,26 +426,54 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
|||
newGroup, error));
|
||||
}
|
||||
|
||||
// private Exam saveSettings(final Exam exam, final ScreenProctoringSettings settings) {
|
||||
//
|
||||
// this.proctoringAdminService
|
||||
// .saveScreenProctoringSettings(exam.getEntityKey(), settings)
|
||||
// .getOrThrow();
|
||||
//
|
||||
// return exam;
|
||||
// }
|
||||
private void registerJoinInstruction(
|
||||
final ClientConnectionRecord ccRecord,
|
||||
final String spsSessionToken,
|
||||
final ScreenProctoringGroup group,
|
||||
final Exam exam) {
|
||||
|
||||
private static final class ExamInfo {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Register JOIN instruction for client ");
|
||||
}
|
||||
|
||||
final ScreenProctoringSettings screenProctoringSettings;
|
||||
final ClientCredentials sebClientCredential;
|
||||
final SPSData spsData = getSPSData(exam);
|
||||
final String url = exam.additionalAttributes.get(ScreenProctoringSettings.ATTR_SPS_SERVICE_URL);
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
|
||||
public ExamInfo(
|
||||
final ScreenProctoringSettings screenProctoringSettings,
|
||||
final ClientCredentials sebClientCredential) {
|
||||
attributes.put(SERVICE_TYPE, SERVICE_TYPE_NAME);
|
||||
attributes.put(METHOD, ClientInstruction.ProctoringInstructionMethod.JOIN.name());
|
||||
attributes.put(URL, url);
|
||||
attributes.put(CLIENT_ID, spsData.spsSEBAccessName);
|
||||
attributes.put(CLIENT_SECRET, spsData.spsSEBAccessPWD);
|
||||
attributes.put(GROUP_ID, group.uuid);
|
||||
attributes.put(SESSION_ID, spsSessionToken);
|
||||
|
||||
this.screenProctoringSettings = screenProctoringSettings;
|
||||
this.sebClientCredential = sebClientCredential;
|
||||
this.sebInstructionService
|
||||
.registerInstruction(
|
||||
exam.id,
|
||||
InstructionType.SEB_PROCTORING,
|
||||
attributes,
|
||||
ccRecord.getConnectionToken(),
|
||||
true)
|
||||
.onError(error -> log.error(
|
||||
"Failed to register screen proctoring join instruction for SEB connection: {}",
|
||||
ccRecord,
|
||||
error));
|
||||
}
|
||||
|
||||
// TODO make this with caching if performance is not good
|
||||
private SPSData getSPSData(final Exam exam) {
|
||||
try {
|
||||
|
||||
final String dataEncrypted = exam.additionalAttributes.get(SPSData.ATTR_SPS_ACCESS_DATA);
|
||||
|
||||
return this.jsonMapper.readValue(
|
||||
this.cryptor.decrypt(dataEncrypted).getOrThrow().toString(),
|
||||
SPSData.class);
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to get local SPSData for exam: {}", exam);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -85,6 +85,8 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
|
||||
private static final Logger log = LoggerFactory.getLogger(ExamAdministrationController.class);
|
||||
|
||||
// TODO reduce dependencies here.
|
||||
// Move SecurityKeyService, SEBRestrictionService RemoteProctoringRoomService into ExamAdminService
|
||||
private final ExamDAO examDAO;
|
||||
private final UserDAO userDAO;
|
||||
private final ExamAdminService examAdminService;
|
||||
|
@ -658,7 +660,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
@Override
|
||||
protected Result<Exam> notifySaved(final Exam entity) {
|
||||
return Result.tryCatch(() -> {
|
||||
this.examAdminService.updateAdditionalExamConfigAttributes(entity.id);
|
||||
this.examAdminService.notifyExamSaved(entity);
|
||||
this.examSessionService.flushCache(entity);
|
||||
return entity;
|
||||
});
|
||||
|
|
|
@ -12,10 +12,10 @@ logging.level.ROOT=INFO
|
|||
logging.level.ch=INFO
|
||||
logging.level.ch.ethz.seb.sebserver.webservice.datalayer=INFO
|
||||
logging.level.org.springframework.cache=DEBUG
|
||||
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=DEBUG
|
||||
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=INFO
|
||||
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=DEBUG
|
||||
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=INFO
|
||||
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator=DEBUG
|
||||
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=DEBUG
|
||||
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator=INFO
|
||||
logging.level.ch.ethz.seb.sebserver.webservice.weblayer.oauth=DEBUG
|
||||
#logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl=DEBUG
|
||||
#logging.level.ch.ethz.seb.sebserver.webservice.datalayer.batis=DEBUG
|
||||
|
|
|
@ -806,8 +806,7 @@ sebserver.exam.delete.report.list.empty=No dependencies will be deleted.
|
|||
sebserver.exam.proctoring.actions.open=Proctoring Settings
|
||||
sebserver.exam.proctoring.form.title=Exam Proctoring Settings
|
||||
sebserver.exam.proctoring.form.info.title=Remote Proctoring
|
||||
sebserver.exam.proctoring.form.info=This allows to integrate a supported external proctoring service.<br/>To integrate Jitsi Meet, JWT based authentication must be enabled.<br/>To integrate Zoom a Zoom higher-plan account is needed and also JWT based authentication.
|
||||
sebserver.exam.proctoring.form.enabled=Proctoring enabled
|
||||
sebserver.exam.proctoring.form.sebserver.exam.proctoring.form.enabled=Proctoring enabled
|
||||
sebserver.exam.proctoring.form.enabled.tooltip=Indicates whether the exam proctoring feature is enabled for this exam or not.
|
||||
sebserver.exam.proctoring.form.type=Type
|
||||
sebserver.exam.proctoring.form.type.tooltip=The type and server type of the external proctoring service.
|
||||
|
@ -828,17 +827,6 @@ sebserver.exam.proctoring.form.appkey.zoom.tooltip=Since Zoom no longer supports
|
|||
sebserver.exam.proctoring.form.secret.zoom=App Secret (Deprecated)
|
||||
sebserver.exam.proctoring.form.secret.zoom.tooltip=Since Zoom no longer supports JWT App, this is deprecated and the below Account ID, Client ID and Client Secret must be used
|
||||
|
||||
sebserver.exam.proctoring.form.appkey.sps=App Key
|
||||
sebserver.exam.proctoring.form.appkey.sps.tooltip=The application key for witch SEB Server can connect to the proctoring service. This is defined by the screen proctoring service.
|
||||
sebserver.exam.proctoring.form.appsecret.sps=App Secret
|
||||
sebserver.exam.proctoring.form.appsecret.sps.tooltip=The secret used to access the proctoring service by SEB Server.
|
||||
sebserver.exam.proctoring.form.accountId.sps=API Account Id
|
||||
sebserver.exam.proctoring.form.accountId.sps.tooltip=This is the API account id on the proctoring service that SEB Server shall use to connect and manage the proctoring service
|
||||
sebserver.exam.proctoring.form.accountSecret.sps=API Account Secret
|
||||
sebserver.exam.proctoring.form.accountSecret.sps.tooltip=This is the secret for the above API account id
|
||||
|
||||
|
||||
|
||||
sebserver.exam.proctoring.form.accountId=Account ID
|
||||
sebserver.exam.proctoring.form.accountId.tooltip=This is the Account ID from the Zoom Server-to-Server OAuth application and needed by SEB server to automatically create meetings
|
||||
sebserver.exam.proctoring.form.clientId=Client ID
|
||||
|
@ -887,6 +875,25 @@ sebserver.exam.proctoring.one.close.error=Failed to close the one-to-one room pr
|
|||
sebserver.exam.proctoring.collecting.open.error=Failed to open the collecting room.
|
||||
sebserver.exam.proctoring.collecting.close.error=Failed to close the collecting room properly.
|
||||
|
||||
sebserver.exam.sps.actions.open=Screen Recording Settings
|
||||
sebserver.exam.sps.form.title=Screen Recording Settings
|
||||
sebserver.exam.sps.form.info.title=Info
|
||||
sebserver.exam.sps.form.info=This allows to integrate a SEB screen proctoring service for this exam to record screens and interaction-data for all SEBs applying the exam.<br/>The SEB screen proctoring service must be available within the given URL and accessible as administrator with the given account credentials.<br/>Also the service API credentials are needed to connect to the service securely.
|
||||
sebserver.exam.sps.form.enable=Enable Screen Recording
|
||||
sebserver.exam.sps.form.enable.tooltip=To enable screen recording with SEB and SEB Server you need to integrate an available SEB screen proctoring service
|
||||
sebserver.exam.sps.form.url=Service URL
|
||||
sebserver.exam.sps.form.url.tooltip=The root URL of the SEB screen proctoring service for integration
|
||||
sebserver.exam.sps.form.appkey=API Key
|
||||
sebserver.exam.sps.form.appkey.tooltip=The application key for witch SEB Server can connect to the proctoring service. This is defined by the screen proctoring service.
|
||||
sebserver.exam.sps.form.appsecret=API Secret
|
||||
sebserver.exam.sps.form.accountId=Account Name
|
||||
sebserver.exam.sps.form.accountId.tooltip=This is the API account id on the proctoring service that SEB Server shall use to connect and manage the proctoring service
|
||||
sebserver.exam.sps.form.accountSecret=Account Password
|
||||
sebserver.exam.sps.form.accountSecret.tooltip=This is the secret/password for the above API account id
|
||||
sebserver.exam.sps.form.collect.strategy=Grouping Strategy
|
||||
sebserver.exam.sps.form.saveSettings=Save Settings
|
||||
|
||||
|
||||
sebserver.exam.signaturekey.action.edit=App Signature Key
|
||||
sebserver.exam.signaturekey.action.save=Save Settings
|
||||
sebserver.exam.signaturekey.action.cancel=Cancel and back to Exam
|
||||
|
@ -1927,6 +1934,8 @@ sebserver.examconfig.props.label.screenProctoringMetadataURLEnabled=Enable URL R
|
|||
sebserver.examconfig.props.label.screenProctoringMetadataURLEnabled.tooltip=Enable the recording of browser URL input during a screen proctoring session
|
||||
sebserver.examconfig.props.label.screenProctoringMetadataWindowTitleEnabled=Enable Window Title Recording
|
||||
sebserver.examconfig.props.label.screenProctoringMetadataWindowTitleEnabled.tooltip=Enable the recording of the title of the active window during a screen proctoring session
|
||||
sebserver.examconfig.props.label.screenProctoringMetadataActiveAppEnabled=Enable Active Application Recording
|
||||
sebserver.examconfig.props.label.screenProctoringMetadataActiveAppEnabled.tooltip=Enable the recording of the name of the active application. The active application is the one with the actual user focus
|
||||
|
||||
sebserver.examconfig.props.group.ScreenProctoring=SEB Screen Proctoring Settings
|
||||
sebserver.examconfig.props.group.screenshot=Screenshot Settings
|
||||
|
|
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