Merge remote-tracking branch 'origin/dev-1.2' into development
This commit is contained in:
commit
2e4dcca89e
51 changed files with 884 additions and 412 deletions
|
@ -35,7 +35,7 @@ public class ProctoringServiceSettings implements Entity {
|
|||
TOWN_HALL,
|
||||
ONE_TO_ONE,
|
||||
BROADCAST,
|
||||
ENABLE_CHAT
|
||||
ENABLE_CHAT,
|
||||
}
|
||||
|
||||
public static final String ATTR_ENABLE_PROCTORING = "enableProctoring";
|
||||
|
|
|
@ -61,9 +61,9 @@ public final class LmsSetup implements GrantEntity, Activatable {
|
|||
/** The Moodle binding features only the course access API so far */
|
||||
MOODLE(Features.COURSE_API /* , Features.SEB_RESTRICTION */),
|
||||
/** The Ans Delft binding is on the way */
|
||||
ANS_DELFT(Features.COURSE_API, Features.SEB_RESTRICTION),
|
||||
ANS_DELFT(),
|
||||
/** The OpenOLAT binding is on the way */
|
||||
OPEN_OLAT(Features.COURSE_API, Features.SEB_RESTRICTION);
|
||||
OPEN_OLAT();
|
||||
|
||||
public final EnumSet<Features> features;
|
||||
|
||||
|
|
|
@ -34,12 +34,12 @@ public final class ClientConnection implements GrantEntity {
|
|||
|
||||
public final boolean connectingStatus;
|
||||
public final boolean establishedStatus;
|
||||
public final boolean indicatorActiveStatus;
|
||||
public final boolean clientActiveStatus;
|
||||
|
||||
ConnectionStatus(final boolean connectingStatus, final boolean establishedStatus) {
|
||||
this.connectingStatus = connectingStatus;
|
||||
this.establishedStatus = establishedStatus;
|
||||
this.indicatorActiveStatus = connectingStatus || establishedStatus;
|
||||
this.clientActiveStatus = connectingStatus || establishedStatus;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -22,8 +22,10 @@ import ch.ethz.seb.sebserver.gbl.util.Utils;
|
|||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class RemoteProctoringRoom {
|
||||
|
||||
public static final String ATTR_IS_OPEN = "isOpen";
|
||||
|
||||
public static final RemoteProctoringRoom NULL_ROOM = new RemoteProctoringRoom(
|
||||
null, null, null, null, null, false, null, null, null);
|
||||
null, null, null, null, null, false, null, null, null, false);
|
||||
|
||||
@JsonProperty(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID)
|
||||
public final Long id;
|
||||
|
@ -52,6 +54,9 @@ public class RemoteProctoringRoom {
|
|||
@JsonProperty(Domain.REMOTE_PROCTORING_ROOM.ATTR_ROOM_DATA)
|
||||
public final String additionalRoomData;
|
||||
|
||||
@JsonProperty(ATTR_IS_OPEN)
|
||||
public final Boolean isOpen;
|
||||
|
||||
@JsonCreator
|
||||
public RemoteProctoringRoom(
|
||||
@JsonProperty(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID) final Long id,
|
||||
|
@ -62,7 +67,8 @@ public class RemoteProctoringRoom {
|
|||
@JsonProperty(Domain.REMOTE_PROCTORING_ROOM.ATTR_TOWNHALL_ROOM) final Boolean townhallRoom,
|
||||
@JsonProperty(Domain.REMOTE_PROCTORING_ROOM.ATTR_BREAK_OUT_CONNECTIONS) final Collection<String> breakOutConnections,
|
||||
@JsonProperty(Domain.REMOTE_PROCTORING_ROOM.ATTR_JOIN_KEY) final CharSequence joinKey,
|
||||
@JsonProperty(Domain.REMOTE_PROCTORING_ROOM.ATTR_ROOM_DATA) final String additionalRoomData) {
|
||||
@JsonProperty(Domain.REMOTE_PROCTORING_ROOM.ATTR_ROOM_DATA) final String additionalRoomData,
|
||||
@JsonProperty(ATTR_IS_OPEN) final Boolean isOpen) {
|
||||
|
||||
this.id = id;
|
||||
this.examId = examId;
|
||||
|
@ -73,6 +79,7 @@ public class RemoteProctoringRoom {
|
|||
this.breakOutConnections = Utils.immutableCollectionOf(breakOutConnections);
|
||||
this.joinKey = joinKey;
|
||||
this.additionalRoomData = additionalRoomData;
|
||||
this.isOpen = isOpen;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
|
@ -111,6 +118,10 @@ public class RemoteProctoringRoom {
|
|||
return this.additionalRoomData;
|
||||
}
|
||||
|
||||
public Boolean getIsOpen() {
|
||||
return this.isOpen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
|
@ -128,6 +139,12 @@ public class RemoteProctoringRoom {
|
|||
builder.append(this.townhallRoom);
|
||||
builder.append(", breakOutConnections=");
|
||||
builder.append(this.breakOutConnections);
|
||||
builder.append(", joinKey=");
|
||||
builder.append(this.joinKey);
|
||||
builder.append(", additionalRoomData=");
|
||||
builder.append(this.additionalRoomData);
|
||||
builder.append(", isOpen=");
|
||||
builder.append(this.isOpen);
|
||||
builder.append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
|
|
|
@ -73,6 +73,12 @@ public class RAPConfiguration implements ApplicationConfiguration {
|
|||
properties.put(WebClient.FAVICON, "fav_icon");
|
||||
|
||||
application.addEntryPoint(guiEntrypoint, new RAPSpringEntryPointFactory(), properties);
|
||||
|
||||
properties.put(WebClient.PAGE_TITLE, "SEB Server Proctoring");
|
||||
properties.put(WebClient.BODY_HTML, "<big>Loading Application<big>");
|
||||
properties.put(WebClient.THEME_ID, DEFAULT_THEME_NAME);
|
||||
properties.put(WebClient.FAVICON, "fav_icon");
|
||||
|
||||
application.addEntryPoint(proctoringEntrypoint, new RAPRemoteProcotringEntryPointFactory(), properties);
|
||||
|
||||
} catch (final RuntimeException re) {
|
||||
|
|
|
@ -139,11 +139,12 @@ public class ExamProctoringSettings {
|
|||
.valueOf(form.getFieldValue(ProctoringServiceSettings.ATTR_SERVER_TYPE));
|
||||
|
||||
final String features = form.getFieldValue(ProctoringServiceSettings.ATTR_ENABLED_FEATURES);
|
||||
final EnumSet<ProctoringFeature> featureFlags =
|
||||
EnumSet.copyOf(Arrays.asList(StringUtils.split(features, Constants.LIST_SEPARATOR))
|
||||
final EnumSet<ProctoringFeature> featureFlags = (StringUtils.isNotBlank(features))
|
||||
? EnumSet.copyOf(Arrays.asList(StringUtils.split(features, Constants.LIST_SEPARATOR))
|
||||
.stream()
|
||||
.map(str -> ProctoringFeature.valueOf(str))
|
||||
.collect(Collectors.toSet()));
|
||||
.collect(Collectors.toSet()))
|
||||
: EnumSet.noneOf(ProctoringFeature.class);
|
||||
|
||||
examProctoring = new ProctoringServiceSettings(
|
||||
Long.parseLong(entityKey.modelId),
|
||||
|
|
|
@ -12,10 +12,12 @@ import org.apache.commons.lang3.BooleanUtils;
|
|||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.rap.rwt.RWT;
|
||||
import org.eclipse.swt.SWT;
|
||||
import org.eclipse.swt.internal.widgets.IControlAdapter;
|
||||
import org.eclipse.swt.layout.GridData;
|
||||
import org.eclipse.swt.layout.GridLayout;
|
||||
import org.eclipse.swt.widgets.Button;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.eclipse.swt.widgets.Control;
|
||||
import org.eclipse.swt.widgets.Label;
|
||||
import org.eclipse.swt.widgets.MessageBox;
|
||||
import org.eclipse.swt.widgets.Text;
|
||||
|
@ -27,6 +29,8 @@ import org.springframework.stereotype.Component;
|
|||
|
||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||
import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
|
||||
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.ComposerService;
|
||||
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.TemplateComposer;
|
||||
|
@ -43,6 +47,11 @@ public class LoginPage implements TemplateComposer {
|
|||
|
||||
private static final Logger log = LoggerFactory.getLogger(LoginPage.class);
|
||||
|
||||
private static final LocTextKey TEXT_REGISTER = new LocTextKey("sebserver.login.register");
|
||||
private static final LocTextKey TEXT_LOGIN = new LocTextKey("sebserver.login.login");
|
||||
private static final LocTextKey TEXT_PWD = new LocTextKey("sebserver.login.pwd");
|
||||
private static final LocTextKey TEXT_USERNAME = new LocTextKey("sebserver.login.username");
|
||||
|
||||
private final PageService pageService;
|
||||
private final AuthorizationContextHolder authorizationContextHolder;
|
||||
private final WidgetFactory widgetFactory;
|
||||
|
@ -67,7 +76,6 @@ public class LoginPage implements TemplateComposer {
|
|||
public void compose(final PageContext pageContext) {
|
||||
final Composite parent = pageContext.getParent();
|
||||
WidgetFactory.setTestId(parent, "login-page");
|
||||
WidgetFactory.setARIARole(parent, "composite");
|
||||
|
||||
final Composite loginGroup = new Composite(parent, SWT.NONE);
|
||||
final GridLayout rowLayout = new GridLayout();
|
||||
|
@ -76,16 +84,17 @@ public class LoginPage implements TemplateComposer {
|
|||
loginGroup.setLayout(rowLayout);
|
||||
loginGroup.setData(RWT.CUSTOM_VARIANT, WidgetFactory.CustomVariant.LOGIN.key);
|
||||
|
||||
final Label name = this.widgetFactory.labelLocalized(loginGroup, "sebserver.login.username");
|
||||
final Label name = this.widgetFactory.labelLocalized(loginGroup, TEXT_USERNAME);
|
||||
name.setLayoutData(new GridData(300, -1));
|
||||
name.setAlignment(SWT.BOTTOM);
|
||||
final Text loginName = this.widgetFactory.textInput(loginGroup);
|
||||
final Text loginName = this.widgetFactory.textInput(loginGroup, TEXT_USERNAME);
|
||||
loginName.setLayoutData(new GridData(SWT.FILL, SWT.TOP, false, false));
|
||||
|
||||
GridData gridData = new GridData(SWT.FILL, SWT.TOP, false, false);
|
||||
gridData.verticalIndent = 10;
|
||||
final Label pwd = this.widgetFactory.labelLocalized(loginGroup, "sebserver.login.pwd");
|
||||
final Label pwd = this.widgetFactory.labelLocalized(loginGroup, TEXT_PWD);
|
||||
pwd.setLayoutData(gridData);
|
||||
final Text loginPassword = this.widgetFactory.passwordInput(loginGroup);
|
||||
final Text loginPassword = this.widgetFactory.passwordInput(loginGroup, TEXT_PWD);
|
||||
loginPassword.setLayoutData(new GridData(SWT.FILL, SWT.TOP, false, false));
|
||||
|
||||
final Composite buttons = new Composite(loginGroup, SWT.NONE);
|
||||
|
@ -93,7 +102,7 @@ public class LoginPage implements TemplateComposer {
|
|||
buttons.setLayout(new GridLayout(2, false));
|
||||
buttons.setData(RWT.CUSTOM_VARIANT, WidgetFactory.CustomVariant.LOGIN_BACK.key);
|
||||
|
||||
final Button loginButton = this.widgetFactory.buttonLocalized(buttons, "sebserver.login.login");
|
||||
final Button loginButton = this.widgetFactory.buttonLocalized(buttons, TEXT_LOGIN);
|
||||
gridData = new GridData(SWT.LEFT, SWT.TOP, false, false);
|
||||
gridData.verticalIndent = 10;
|
||||
loginButton.setLayoutData(gridData);
|
||||
|
@ -127,12 +136,17 @@ public class LoginPage implements TemplateComposer {
|
|||
});
|
||||
|
||||
if (this.registeringEnabled) {
|
||||
final Button registerButton = this.widgetFactory.buttonLocalized(buttons, "sebserver.login.register");
|
||||
final Button registerButton = this.widgetFactory.buttonLocalized(buttons, TEXT_REGISTER);
|
||||
gridData = new GridData(SWT.LEFT, SWT.TOP, false, false);
|
||||
gridData.verticalIndent = 10;
|
||||
registerButton.setLayoutData(gridData);
|
||||
registerButton.addListener(SWT.Selection, event -> pageContext.forwardToPage(this.defaultRegisterPage));
|
||||
}
|
||||
|
||||
ComposerService.traversePageTree(
|
||||
parent,
|
||||
comp -> comp instanceof Control,
|
||||
comp -> comp.getAdapter(IControlAdapter.class).setTabIndex(0));
|
||||
}
|
||||
|
||||
private void login(
|
||||
|
|
|
@ -36,6 +36,7 @@ import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
|
|||
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
import ch.ethz.seb.sebserver.gui.content.MonitoringRunningExam.ProctoringUpdateErrorHandler;
|
||||
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
|
||||
import ch.ethz.seb.sebserver.gui.service.ResourceService;
|
||||
import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
|
||||
|
@ -269,11 +270,14 @@ public class MonitoringClientConnection implements TemplateComposer {
|
|||
|
||||
final Supplier<EntityTable<ClientNotification>> notificationTableSupplier = _notificationTableSupplier;
|
||||
// server push update
|
||||
final ProctoringUpdateErrorHandler proctoringUpdateErrorHandler =
|
||||
new ProctoringUpdateErrorHandler(this.pageService, pageContext);
|
||||
|
||||
this.serverPushService.runServerPush(
|
||||
new ServerPushContext(
|
||||
content,
|
||||
Utils.truePredicate(),
|
||||
MonitoringRunningExam.createServerPushUpdateErrorHandler(this.pageService, pageContext)),
|
||||
proctoringUpdateErrorHandler),
|
||||
this.pollInterval,
|
||||
context -> clientConnectionDetails.updateData(),
|
||||
context -> clientConnectionDetails.updateGUI(notificationTableSupplier, pageContext));
|
||||
|
@ -365,7 +369,7 @@ public class MonitoringClientConnection implements TemplateComposer {
|
|||
})
|
||||
.noEventPropagation()
|
||||
.publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER) &&
|
||||
connectionData.clientConnection.status.indicatorActiveStatus);
|
||||
connectionData.clientConnection.status.clientActiveStatus);
|
||||
|
||||
if (connectionData.clientConnection.status == ConnectionStatus.ACTIVE) {
|
||||
final ProctoringServiceSettings proctoringSettings = restService
|
||||
|
|
|
@ -15,6 +15,7 @@ import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
|||
import ch.ethz.seb.sebserver.gbl.model.Domain;
|
||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
|
||||
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
|
||||
|
@ -43,6 +44,8 @@ public class MonitoringExamSearchPopup {
|
|||
new LocTextKey("sebserver.monitoring.search.list.name");
|
||||
private static final LocTextKey TABLE_COLUMN_IP_ADDRESS =
|
||||
new LocTextKey("sebserver.monitoring.search.list.ip");
|
||||
private static final LocTextKey TABLE_COLUMN_STATUS =
|
||||
new LocTextKey("sebserver.monitoring.search.list.status");
|
||||
|
||||
private final PageService pageService;
|
||||
|
||||
|
@ -50,9 +53,16 @@ public class MonitoringExamSearchPopup {
|
|||
new TableFilterAttribute(CriteriaType.TEXT, ClientConnection.FILTER_ATTR_SESSION_ID);
|
||||
private final TableFilterAttribute ipFilter =
|
||||
new TableFilterAttribute(CriteriaType.TEXT, ClientConnection.FILTER_ATTR_IP_STRING);
|
||||
private final TableFilterAttribute statusFilter;
|
||||
|
||||
protected MonitoringExamSearchPopup(final PageService pageService) {
|
||||
this.pageService = pageService;
|
||||
|
||||
this.statusFilter = new TableFilterAttribute(
|
||||
CriteriaType.SINGLE_SELECTION,
|
||||
ClientConnection.FILTER_ATTR_STATUS,
|
||||
ConnectionStatus.ACTIVE.name(),
|
||||
pageService.getResourceService()::localizedClientConnectionStatusResources);
|
||||
}
|
||||
|
||||
public void show(final PageContext pageContext) {
|
||||
|
@ -60,6 +70,7 @@ public class MonitoringExamSearchPopup {
|
|||
pageContext.getParent().getShell(),
|
||||
this.pageService.getWidgetFactory());
|
||||
dialog.setLargeDialogWidth();
|
||||
dialog.setDialogHeight(380);
|
||||
dialog.open(
|
||||
TITLE_TEXT_KEY,
|
||||
pageContext,
|
||||
|
@ -90,6 +101,12 @@ public class MonitoringExamSearchPopup {
|
|||
ClientConnection::getClientAddress)
|
||||
.withFilter(this.ipFilter))
|
||||
|
||||
.withColumn(new ColumnDefinition<>(
|
||||
Domain.CLIENT_CONNECTION.ATTR_STATUS,
|
||||
TABLE_COLUMN_STATUS,
|
||||
ClientConnection::getStatus)
|
||||
.withFilter(this.statusFilter))
|
||||
|
||||
.withDefaultAction(t -> actionBuilder
|
||||
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION)
|
||||
.withParentEntityKey(examKey)
|
||||
|
|
|
@ -149,13 +149,22 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
restService.getBuilder(GetClientConnectionDataList.class)
|
||||
.withURIVariable(API.PARAM_PARENT_MODEL_ID, exam.getModelId());
|
||||
|
||||
final ProctoringUpdateErrorHandler proctoringUpdateErrorHandler =
|
||||
new ProctoringUpdateErrorHandler(this.pageService, pageContext);
|
||||
|
||||
final ServerPushContext pushContext = new ServerPushContext(
|
||||
content,
|
||||
Utils.truePredicate(),
|
||||
proctoringUpdateErrorHandler);
|
||||
|
||||
final ClientConnectionTable clientTable = new ClientConnectionTable(
|
||||
this.pageService,
|
||||
tablePane,
|
||||
this.asyncRunner,
|
||||
exam,
|
||||
indicators,
|
||||
restCall);
|
||||
restCall,
|
||||
pushContext);
|
||||
|
||||
clientTable
|
||||
.withDefaultAction(
|
||||
|
@ -172,10 +181,7 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
ActionDefinition.MONITOR_EXAM_NEW_PROCTOR_ROOM));
|
||||
|
||||
this.serverPushService.runServerPush(
|
||||
new ServerPushContext(
|
||||
content,
|
||||
Utils.truePredicate(),
|
||||
createServerPushUpdateErrorHandler(this.pageService, pageContext)),
|
||||
pushContext,
|
||||
this.pollInterval,
|
||||
context -> clientTable.updateValues(),
|
||||
updateTableGUI(clientTable));
|
||||
|
@ -281,18 +287,26 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
}
|
||||
}
|
||||
|
||||
final ProctoringUpdateErrorHandler proctoringUpdateErrorHandler =
|
||||
new ProctoringUpdateErrorHandler(this.pageService, pageContext);
|
||||
|
||||
final ServerPushContext pushContext = new ServerPushContext(
|
||||
parent,
|
||||
Utils.truePredicate(),
|
||||
proctoringUpdateErrorHandler);
|
||||
|
||||
this.monitoringProctoringService.initCollectingRoomActions(
|
||||
pushContext,
|
||||
pageContext,
|
||||
actionBuilder,
|
||||
proctoringSettings,
|
||||
proctoringGUIService);
|
||||
|
||||
this.serverPushService.runServerPush(
|
||||
new ServerPushContext(
|
||||
parent,
|
||||
Utils.truePredicate(),
|
||||
createServerPushUpdateErrorHandler(this.pageService, pageContext)),
|
||||
pushContext,
|
||||
this.proctoringRoomUpdateInterval,
|
||||
context -> this.monitoringProctoringService.updateCollectingRoomActions(
|
||||
context,
|
||||
pageContext,
|
||||
actionBuilder,
|
||||
proctoringSettings,
|
||||
|
@ -422,7 +436,7 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
|
||||
private Set<EntityKey> selectionForQuitInstruction(final ClientConnectionTable clientTable) {
|
||||
final Set<String> connectionTokens = clientTable.getConnectionTokens(
|
||||
cc -> cc.status.indicatorActiveStatus,
|
||||
cc -> cc.status.clientActiveStatus,
|
||||
true);
|
||||
if (connectionTokens == null || connectionTokens.isEmpty()) {
|
||||
return Collections.emptySet();
|
||||
|
@ -480,30 +494,53 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
};
|
||||
}
|
||||
|
||||
static final Function<Exception, Boolean> createServerPushUpdateErrorHandler(
|
||||
final PageService pageService,
|
||||
final PageContext pageContext) {
|
||||
static final class ProctoringUpdateErrorHandler implements Function<Exception, Boolean> {
|
||||
|
||||
return error -> {
|
||||
log.error("Fialed to update server push: {}", error.getMessage());
|
||||
private final PageService pageService;
|
||||
private final PageContext pageContext;
|
||||
|
||||
private int errors = 0;
|
||||
|
||||
public ProctoringUpdateErrorHandler(
|
||||
final PageService pageService,
|
||||
final PageContext pageContext) {
|
||||
|
||||
this.pageService = pageService;
|
||||
this.pageContext = pageContext;
|
||||
}
|
||||
|
||||
private boolean checkUserSession() {
|
||||
try {
|
||||
pageService.getCurrentUser().get();
|
||||
this.pageService.getCurrentUser().get();
|
||||
return true;
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to verify current user after server push error: {}", e.getMessage());
|
||||
log.info("Force logout and session cleanup...");
|
||||
pageContext.forwardToLoginPage();
|
||||
final MessageBox logoutSuccess = new Message(
|
||||
pageContext.getShell(),
|
||||
pageService.getI18nSupport().getText("sebserver.logout"),
|
||||
Utils.formatLineBreaks(
|
||||
pageService.getI18nSupport().getText("sebserver.logout.invalid-session.message")),
|
||||
SWT.ICON_INFORMATION,
|
||||
pageService.getI18nSupport());
|
||||
logoutSuccess.open(null);
|
||||
try {
|
||||
this.pageContext.forwardToLoginPage();
|
||||
final MessageBox logoutSuccess = new Message(
|
||||
this.pageContext.getShell(),
|
||||
this.pageService.getI18nSupport().getText("sebserver.logout"),
|
||||
Utils.formatLineBreaks(
|
||||
this.pageService.getI18nSupport()
|
||||
.getText("sebserver.logout.invalid-session.message")),
|
||||
SWT.ICON_INFORMATION,
|
||||
this.pageService.getI18nSupport());
|
||||
logoutSuccess.open(null);
|
||||
} catch (final Exception ee) {
|
||||
log.warn("Unable to auto-logout: ", ee.getMessage());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean apply(final Exception error) {
|
||||
this.errors++;
|
||||
log.error("Failed to update server push: {}", error.getMessage());
|
||||
if (this.errors > 5) {
|
||||
checkUserSession();
|
||||
}
|
||||
return this.errors > 5;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,21 +8,28 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.gui.content;
|
||||
|
||||
import org.eclipse.swt.SWT;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.eclipse.swt.widgets.Label;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.Domain;
|
||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
|
||||
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
|
||||
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.PageService.PageActionBuilder;
|
||||
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.session.GetCollectingRoomConnections;
|
||||
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition;
|
||||
import ch.ethz.seb.sebserver.gui.table.EntityTable;
|
||||
|
||||
@Lazy
|
||||
@Component
|
||||
|
@ -31,6 +38,10 @@ public class ProctorRoomConnectionsPopup {
|
|||
|
||||
private static final LocTextKey TITLE_TEXT_KEY =
|
||||
new LocTextKey("sebserver.monitoring.exam.proctoring.room.connections.title");
|
||||
private static final LocTextKey EMPTY_LIST_TEXT_KEY =
|
||||
new LocTextKey("sebserver.monitoring.search.list.empty");
|
||||
private static final LocTextKey TABLE_COLUMN_NAME =
|
||||
new LocTextKey("sebserver.monitoring.search.list.name");
|
||||
|
||||
private final PageService pageService;
|
||||
|
||||
|
@ -43,29 +54,60 @@ public class ProctorRoomConnectionsPopup {
|
|||
pageContext.getParent().getShell(),
|
||||
this.pageService.getWidgetFactory());
|
||||
dialog.setLargeDialogWidth();
|
||||
dialog.setDialogHeight(380);
|
||||
dialog.open(
|
||||
new LocTextKey(TITLE_TEXT_KEY.name, roomSubject),
|
||||
pageContext,
|
||||
this::compose);
|
||||
c -> this.compose(c, dialog));
|
||||
}
|
||||
|
||||
private void compose(final PageContext pageContext) {
|
||||
final Composite parent = pageContext.getParent();
|
||||
final Composite grid = this.pageService.getWidgetFactory().createPopupScrollComposite(parent);
|
||||
private void compose(final PageContext pageContext, final ModalInputDialog<Void> dialog) {
|
||||
final EntityKey entityKey = pageContext.getEntityKey();
|
||||
final EntityKey parentEntityKey = pageContext.getParentEntityKey();
|
||||
|
||||
this.pageService.getRestService()
|
||||
final PageActionBuilder actionBuilder = this.pageService
|
||||
.pageActionBuilder(pageContext.clearEntityKeys());
|
||||
|
||||
final List<ClientConnection> connections = new ArrayList<>(this.pageService.getRestService()
|
||||
.getBuilder(GetCollectingRoomConnections.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, parentEntityKey.modelId)
|
||||
.withQueryParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, entityKey.modelId)
|
||||
.call()
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
.forEach(connection -> {
|
||||
final Label label = new Label(grid, SWT.NONE);
|
||||
label.setText(connection.userSessionId);
|
||||
});
|
||||
.getOrThrow());
|
||||
|
||||
this.pageService.staticListTableBuilder(connections, EntityType.CLIENT_CONNECTION)
|
||||
|
||||
.withEmptyMessage(EMPTY_LIST_TEXT_KEY)
|
||||
.withPaging(10)
|
||||
|
||||
.withColumn(new ColumnDefinition<>(
|
||||
Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_ID,
|
||||
TABLE_COLUMN_NAME,
|
||||
ClientConnection::getUserSessionId))
|
||||
|
||||
.withDefaultAction(t -> actionBuilder
|
||||
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION)
|
||||
.withParentEntityKey(parentEntityKey)
|
||||
.withExec(action -> showClientConnection(action, dialog, t))
|
||||
.create())
|
||||
|
||||
.compose(pageContext);
|
||||
}
|
||||
|
||||
private PageAction showClientConnection(
|
||||
final PageAction pageAction,
|
||||
final ModalInputDialog<Void> dialog,
|
||||
final EntityTable<ClientConnection> table) {
|
||||
|
||||
final ClientConnection singleSelectedROWData = table.getSingleSelectedROWData();
|
||||
dialog.close();
|
||||
return pageAction
|
||||
.withEntityKey(new EntityKey(
|
||||
singleSelectedROWData.id,
|
||||
EntityType.CLIENT_CONNECTION))
|
||||
.withAttribute(
|
||||
Domain.CLIENT_CONNECTION.ATTR_CONNECTION_TOKEN,
|
||||
singleSelectedROWData.getConnectionToken());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -119,10 +119,10 @@ public final class TextFieldBuilder extends FieldBuilder<String> {
|
|||
}
|
||||
|
||||
final Text textInput = (this.isNumber)
|
||||
? builder.widgetFactory.numberInput(fieldGrid, this.numberCheck, readonly)
|
||||
? builder.widgetFactory.numberInput(fieldGrid, this.numberCheck, readonly, this.label)
|
||||
: (this.isArea)
|
||||
? builder.widgetFactory.textAreaInput(fieldGrid, readonly)
|
||||
: builder.widgetFactory.textInput(fieldGrid, this.isPassword, readonly);
|
||||
? builder.widgetFactory.textAreaInput(fieldGrid, readonly, this.label)
|
||||
: builder.widgetFactory.textInput(fieldGrid, this.isPassword, readonly, this.label);
|
||||
|
||||
if (builder.pageService.getFormTooltipMode() == PageService.FormTooltipMode.INPUT) {
|
||||
builder.pageService.getPolyglotPageService().injectI18nTooltip(
|
||||
|
|
|
@ -567,6 +567,14 @@ public class ResourceService {
|
|||
.getText(SEB_CONNECTION_STATUS_KEY_PREFIX + name, name);
|
||||
}
|
||||
|
||||
public List<Tuple<String>> localizedClientConnectionStatusResources() {
|
||||
return Arrays.stream(ConnectionStatus.values())
|
||||
.map(type -> new Tuple<>(type.name(),
|
||||
this.i18nSupport.getText(SEB_CONNECTION_STATUS_KEY_PREFIX + type.name())))
|
||||
.sorted(RESOURCE_COMPARATOR)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public String localizedExamTypeName(final ExamConfigurationMap examMap) {
|
||||
if (examMap.examType == null) {
|
||||
return Constants.EMPTY_NOTE;
|
||||
|
|
|
@ -93,12 +93,16 @@ public abstract class AbstractProctoringView implements RemoteProctoringView {
|
|||
final BroadcastActionState state =
|
||||
(BroadcastActionState) videoAction.getData(BroadcastActionState.KEY_NAME);
|
||||
|
||||
this.pageService.getPolyglotPageService().injectI18n(
|
||||
audioAction,
|
||||
state.video ? BROADCAST_AUDIO_ON_TEXT_KEY : BROADCAST_AUDIO_OFF_TEXT_KEY);
|
||||
this.pageService.getPolyglotPageService().injectI18n(
|
||||
videoAction,
|
||||
state.video ? BROADCAST_VIDEO_ON_TEXT_KEY : BROADCAST_VIDEO_OFF_TEXT_KEY);
|
||||
if (audioAction != null) {
|
||||
this.pageService.getPolyglotPageService().injectI18n(
|
||||
audioAction,
|
||||
state.video ? BROADCAST_AUDIO_ON_TEXT_KEY : BROADCAST_AUDIO_OFF_TEXT_KEY);
|
||||
}
|
||||
if (videoAction != null) {
|
||||
this.pageService.getPolyglotPageService().injectI18n(
|
||||
videoAction,
|
||||
state.video ? BROADCAST_VIDEO_ON_TEXT_KEY : BROADCAST_VIDEO_OFF_TEXT_KEY);
|
||||
}
|
||||
|
||||
state.video = !state.video;
|
||||
state.audio = state.video;
|
||||
|
|
|
@ -46,6 +46,7 @@ import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
|
|||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder;
|
||||
import ch.ethz.seb.sebserver.gui.widget.Message;
|
||||
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
|
||||
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.AriaRole;
|
||||
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant;
|
||||
|
||||
@Lazy
|
||||
|
@ -285,6 +286,8 @@ public class DefaultPageLayout implements TemplateComposer {
|
|||
log.error("Invalid markup for 'Imprint'", e);
|
||||
}
|
||||
});
|
||||
|
||||
WidgetFactory.setARIARole(imprint, AriaRole.link);
|
||||
}
|
||||
if (StringUtils.isNoneBlank(i18nSupport.getText(ABOUT_TEXT_KEY, ""))) {
|
||||
final Label about = this.widgetFactory.labelLocalized(
|
||||
|
@ -299,6 +302,8 @@ public class DefaultPageLayout implements TemplateComposer {
|
|||
log.error("Invalid markup for 'About'", e);
|
||||
}
|
||||
});
|
||||
|
||||
WidgetFactory.setARIARole(about, AriaRole.link);
|
||||
}
|
||||
if (StringUtils.isNoneBlank(i18nSupport.getText(HELP_TEXT_KEY, ""))) {
|
||||
final Label help = this.widgetFactory.labelLocalized(
|
||||
|
@ -318,6 +323,7 @@ public class DefaultPageLayout implements TemplateComposer {
|
|||
}
|
||||
});
|
||||
|
||||
WidgetFactory.setARIARole(help, AriaRole.link);
|
||||
}
|
||||
this.widgetFactory.labelLocalized(
|
||||
footerRight,
|
||||
|
|
|
@ -17,6 +17,7 @@ import org.eclipse.swt.layout.RowData;
|
|||
import org.eclipse.swt.layout.RowLayout;
|
||||
import org.eclipse.swt.widgets.Button;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.eclipse.swt.widgets.Label;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
@ -78,6 +79,11 @@ public class JitsiMeetProctoringView extends AbstractProctoringView {
|
|||
final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true);
|
||||
content.setLayoutData(headerCell);
|
||||
|
||||
final Label title = this.pageService
|
||||
.getWidgetFactory()
|
||||
.label(content, proctoringWindowData.connectionData.subject);
|
||||
title.setLayoutData(new GridData(SWT.CENTER, SWT.TOP, true, true));
|
||||
|
||||
parent.addListener(SWT.Dispose, event -> closeRoom(proctoringGUIService, proctoringWindowData));
|
||||
|
||||
final String url = this.guiServiceInfo
|
||||
|
|
|
@ -49,6 +49,7 @@ public class ModalInputDialog<T> extends Dialog {
|
|||
private int dialogWidth = DEFAULT_DIALOG_WIDTH;
|
||||
private int dialogHeight = DEFAULT_DIALOG_HEIGHT;
|
||||
private int buttonWidth = DEFAULT_DIALOG_BUTTON_WIDTH;
|
||||
private boolean forceHeight = false;
|
||||
|
||||
public ModalInputDialog(
|
||||
final Shell parent,
|
||||
|
@ -75,6 +76,7 @@ public class ModalInputDialog<T> extends Dialog {
|
|||
|
||||
public ModalInputDialog<T> setDialogHeight(final int dialogHeight) {
|
||||
this.dialogHeight = dialogHeight;
|
||||
this.forceHeight = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -215,6 +217,9 @@ public class ModalInputDialog<T> extends Dialog {
|
|||
}
|
||||
|
||||
private int calcDialogHeight(final Composite main) {
|
||||
if (this.forceHeight) {
|
||||
return this.dialogHeight;
|
||||
}
|
||||
final int actualHeight = main.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
|
||||
final int displayHeight = main.getDisplay().getClientArea().height;
|
||||
final int availableHeight = (displayHeight < actualHeight + 100)
|
||||
|
|
|
@ -17,6 +17,7 @@ import org.eclipse.swt.layout.RowData;
|
|||
import org.eclipse.swt.layout.RowLayout;
|
||||
import org.eclipse.swt.widgets.Button;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.eclipse.swt.widgets.Label;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
@ -78,6 +79,11 @@ public class ZoomProctoringView extends AbstractProctoringView {
|
|||
final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true);
|
||||
content.setLayoutData(headerCell);
|
||||
|
||||
final Label title = this.pageService
|
||||
.getWidgetFactory()
|
||||
.label(content, proctoringWindowData.connectionData.subject);
|
||||
title.setLayoutData(new GridData(SWT.CENTER, SWT.TOP, true, false));
|
||||
|
||||
parent.addListener(SWT.Dispose, event -> closeRoom(proctoringGUIService, proctoringWindowData));
|
||||
|
||||
final String url = this.guiServiceInfo
|
||||
|
@ -111,13 +117,13 @@ public class ZoomProctoringView extends AbstractProctoringView {
|
|||
|
||||
final BroadcastActionState broadcastActionState = new BroadcastActionState();
|
||||
if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.BROADCAST)) {
|
||||
final Button broadcastAudioAction = widgetFactory.buttonLocalized(footer, BROADCAST_AUDIO_ON_TEXT_KEY);
|
||||
broadcastAudioAction.setLayoutData(new RowData());
|
||||
broadcastAudioAction.addListener(SWT.Selection, event -> toggleBroadcastAudio(
|
||||
proctoringWindowData.examId,
|
||||
proctoringWindowData.connectionData.roomName,
|
||||
broadcastAudioAction));
|
||||
broadcastAudioAction.setData(BroadcastActionState.KEY_NAME, broadcastActionState);
|
||||
// final Button broadcastAudioAction = widgetFactory.buttonLocalized(footer, BROADCAST_AUDIO_ON_TEXT_KEY);
|
||||
// broadcastAudioAction.setLayoutData(new RowData());
|
||||
// broadcastAudioAction.addListener(SWT.Selection, event -> toggleBroadcastAudio(
|
||||
// proctoringWindowData.examId,
|
||||
// proctoringWindowData.connectionData.roomName,
|
||||
// broadcastAudioAction));
|
||||
// broadcastAudioAction.setData(BroadcastActionState.KEY_NAME, broadcastActionState);
|
||||
|
||||
final Button broadcastVideoAction = widgetFactory.buttonLocalized(footer, BROADCAST_VIDEO_ON_TEXT_KEY);
|
||||
broadcastVideoAction.setLayoutData(new RowData());
|
||||
|
@ -125,7 +131,7 @@ public class ZoomProctoringView extends AbstractProctoringView {
|
|||
proctoringWindowData.examId,
|
||||
proctoringWindowData.connectionData.roomName,
|
||||
broadcastVideoAction,
|
||||
broadcastAudioAction));
|
||||
null));
|
||||
broadcastVideoAction.setData(BroadcastActionState.KEY_NAME, broadcastActionState);
|
||||
}
|
||||
if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.ENABLE_CHAT)) {
|
||||
|
|
|
@ -21,7 +21,7 @@ public final class ServerPushContext {
|
|||
|
||||
public final Composite anchor;
|
||||
public final Predicate<ServerPushContext> runAgain;
|
||||
public final Function<Exception, Boolean> errorHandler;
|
||||
final Function<Exception, Boolean> errorHandler;
|
||||
boolean internalStop = false;
|
||||
|
||||
public ServerPushContext(
|
||||
|
@ -36,6 +36,12 @@ public final class ServerPushContext {
|
|||
this.runAgain = runAgain;
|
||||
}
|
||||
|
||||
public void reportError(final Exception error) {
|
||||
if (this.errorHandler != null) {
|
||||
this.internalStop = this.errorHandler.apply(error);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean runAgain() {
|
||||
return !this.internalStop && this.runAgain.test(this);
|
||||
}
|
||||
|
|
|
@ -189,7 +189,7 @@ public class ClientConnectionDetails {
|
|||
final double value = indValue.getValue();
|
||||
final String displayValue = IndicatorValue.getDisplayValue(indValue);
|
||||
|
||||
if (!this.connectionData.clientConnection.status.indicatorActiveStatus) {
|
||||
if (!this.connectionData.clientConnection.status.clientActiveStatus) {
|
||||
|
||||
form.setFieldValue(
|
||||
indData.indicator.name,
|
||||
|
|
|
@ -61,6 +61,7 @@ import ch.ethz.seb.sebserver.gui.service.ResourceService;
|
|||
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.PageService;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
|
||||
import ch.ethz.seb.sebserver.gui.service.push.ServerPushContext;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.DisposedOAuth2RestTemplateException;
|
||||
import ch.ethz.seb.sebserver.gui.service.session.IndicatorData.ThresholdColor;
|
||||
|
@ -95,6 +96,8 @@ public final class ClientConnectionTable {
|
|||
private final AsyncRunner asyncRunner;
|
||||
private final Exam exam;
|
||||
private final RestCall<Collection<ClientConnectionData>>.RestCallBuilder restCallBuilder;
|
||||
private final ServerPushContext pushConext;
|
||||
|
||||
private final Map<Long, IndicatorData> indicatorMapping;
|
||||
private final Table table;
|
||||
private final ColorData colorData;
|
||||
|
@ -116,18 +119,22 @@ public final class ClientConnectionTable {
|
|||
private boolean forceUpdateAll = false;
|
||||
private boolean updateInProgress = false;
|
||||
|
||||
//private int updateErrors = 0;
|
||||
|
||||
public ClientConnectionTable(
|
||||
final PageService pageService,
|
||||
final Composite tableRoot,
|
||||
final AsyncRunner asyncRunner,
|
||||
final Exam exam,
|
||||
final Collection<Indicator> indicators,
|
||||
final RestCall<Collection<ClientConnectionData>>.RestCallBuilder restCallBuilder) {
|
||||
final RestCall<Collection<ClientConnectionData>>.RestCallBuilder restCallBuilder,
|
||||
final ServerPushContext pushConext) {
|
||||
|
||||
this.pageService = pageService;
|
||||
this.asyncRunner = asyncRunner;
|
||||
this.exam = exam;
|
||||
this.restCallBuilder = restCallBuilder;
|
||||
this.pushConext = pushConext;
|
||||
|
||||
final WidgetFactory widgetFactory = pageService.getWidgetFactory();
|
||||
final ResourceService resourceService = pageService.getResourceService();
|
||||
|
@ -188,6 +195,10 @@ public final class ClientConnectionTable {
|
|||
this.table.layout();
|
||||
}
|
||||
|
||||
// public int getUpdateErrors() {
|
||||
// return this.updateErrors;
|
||||
// }
|
||||
|
||||
public WidgetFactory getWidgetFactory() {
|
||||
return this.pageService.getWidgetFactory();
|
||||
}
|
||||
|
@ -316,48 +327,58 @@ public final class ClientConnectionTable {
|
|||
}
|
||||
|
||||
this.updateInProgress = true;
|
||||
this.asyncRunner.runAsync(this::updateValuesAsync);
|
||||
final boolean needsSync = this.tableMapping != null &&
|
||||
this.table != null &&
|
||||
this.tableMapping.size() != this.table.getItemCount();
|
||||
this.asyncRunner.runAsync(() -> updateValuesAsync(needsSync));
|
||||
}
|
||||
|
||||
private void updateValuesAsync() {
|
||||
if (this.statusFilterChanged || this.forceUpdateAll) {
|
||||
this.toDelete.clear();
|
||||
this.toDelete.addAll(this.tableMapping.keySet());
|
||||
}
|
||||
this.restCallBuilder
|
||||
.withHeader(API.EXAM_MONITORING_STATE_FILTER, this.statusFilterParam)
|
||||
.call()
|
||||
.get(error -> {
|
||||
log.error("Unexpected error while trying to get client connection table data: ", error);
|
||||
recoverFromDisposedRestTemplate(error);
|
||||
return Collections.emptyList();
|
||||
})
|
||||
.forEach(data -> {
|
||||
final UpdatableTableItem tableItem = this.tableMapping.computeIfAbsent(
|
||||
data.getConnectionId(),
|
||||
UpdatableTableItem::new);
|
||||
tableItem.push(data);
|
||||
if (this.statusFilterChanged || this.forceUpdateAll) {
|
||||
this.toDelete.remove(data.getConnectionId());
|
||||
private void updateValuesAsync(final boolean needsSync) {
|
||||
|
||||
try {
|
||||
|
||||
if (this.statusFilterChanged || this.forceUpdateAll || needsSync) {
|
||||
this.toDelete.clear();
|
||||
this.toDelete.addAll(this.tableMapping.keySet());
|
||||
}
|
||||
this.restCallBuilder
|
||||
.withHeader(API.EXAM_MONITORING_STATE_FILTER, this.statusFilterParam)
|
||||
.call()
|
||||
.get(error -> {
|
||||
recoverFromDisposedRestTemplate(error);
|
||||
this.pushConext.reportError(error);
|
||||
return Collections.emptyList();
|
||||
})
|
||||
.forEach(data -> {
|
||||
final UpdatableTableItem tableItem = this.tableMapping.computeIfAbsent(
|
||||
data.getConnectionId(),
|
||||
UpdatableTableItem::new);
|
||||
tableItem.push(data);
|
||||
if (this.statusFilterChanged || this.forceUpdateAll || needsSync) {
|
||||
this.toDelete.remove(data.getConnectionId());
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.toDelete.isEmpty()) {
|
||||
this.toDelete.forEach(id -> {
|
||||
final UpdatableTableItem item = this.tableMapping.remove(id);
|
||||
if (item != null) {
|
||||
final List<Long> list = this.sessionIds.get(item.connectionData.clientConnection.userSessionId);
|
||||
if (list != null) {
|
||||
list.remove(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.statusFilterChanged = false;
|
||||
this.toDelete.clear();
|
||||
}
|
||||
|
||||
if (!this.toDelete.isEmpty()) {
|
||||
this.toDelete.forEach(id -> {
|
||||
final UpdatableTableItem item = this.tableMapping.remove(id);
|
||||
if (item != null) {
|
||||
final List<Long> list = this.sessionIds.get(item.connectionData.clientConnection.userSessionId);
|
||||
if (list != null) {
|
||||
list.remove(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.statusFilterChanged = false;
|
||||
this.toDelete.clear();
|
||||
this.forceUpdateAll = false;
|
||||
this.updateInProgress = false;
|
||||
|
||||
} catch (final Exception e) {
|
||||
this.pushConext.reportError(e);
|
||||
}
|
||||
|
||||
this.forceUpdateAll = false;
|
||||
this.updateInProgress = false;
|
||||
}
|
||||
|
||||
public void updateGUI() {
|
||||
|
@ -576,7 +597,7 @@ public final class ClientConnectionTable {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (!this.connectionData.clientConnection.status.indicatorActiveStatus) {
|
||||
if (!this.connectionData.clientConnection.status.clientActiveStatus) {
|
||||
final String value = (indicatorData.indicator.type.showOnlyInActiveState)
|
||||
? Constants.EMPTY_NOTE
|
||||
: IndicatorValue.getDisplayValue(indicatorValue);
|
||||
|
|
|
@ -10,7 +10,6 @@ package ch.ethz.seb.sebserver.gui.service.session.proctoring;
|
|||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.eclipse.rap.rwt.RWT;
|
||||
|
@ -47,7 +46,7 @@ import ch.ethz.seb.sebserver.gui.service.page.PageService;
|
|||
import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.event.ActionActivationEvent;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gui.service.push.ServerPushContext;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRooms;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnection;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.IsTownhallRoomAvailable;
|
||||
|
@ -80,6 +79,7 @@ public class MonitoringProctoringService {
|
|||
static final String OPEN_ROOM_SCRIPT =
|
||||
"try {\n" +
|
||||
"var existingWin = window.open('', '%s', 'height=%s,width=%s,location=no,scrollbars=yes,status=no,menubar=0,toolbar=no,titlebar=no,dialog=no');\n" +
|
||||
"existingWin.document.title = '%s';\n" +
|
||||
"if(existingWin.location.href === 'about:blank'){\n" +
|
||||
" existingWin.location.href = '%s%s';\n" +
|
||||
" existingWin.focus();\n" +
|
||||
|
@ -152,6 +152,7 @@ public class MonitoringProctoringService {
|
|||
}
|
||||
|
||||
public void initCollectingRoomActions(
|
||||
final ServerPushContext pushContext,
|
||||
final PageContext pageContext,
|
||||
final PageActionBuilder actionBuilder,
|
||||
final ProctoringServiceSettings proctoringSettings,
|
||||
|
@ -159,6 +160,7 @@ public class MonitoringProctoringService {
|
|||
|
||||
proctoringGUIService.clearCollectingRoomActionState();
|
||||
updateCollectingRoomActions(
|
||||
pushContext,
|
||||
pageContext,
|
||||
actionBuilder,
|
||||
proctoringSettings,
|
||||
|
@ -166,6 +168,7 @@ public class MonitoringProctoringService {
|
|||
}
|
||||
|
||||
public void updateCollectingRoomActions(
|
||||
final ServerPushContext pushContext,
|
||||
final PageContext pageContext,
|
||||
final PageActionBuilder actionBuilder,
|
||||
final ProctoringServiceSettings proctoringSettings,
|
||||
|
@ -179,7 +182,7 @@ public class MonitoringProctoringService {
|
|||
.getBuilder(GetCollectingRooms.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
|
||||
.call()
|
||||
.onError(error -> log.error("Failed to update proctoring rooms on GUI {}", error.getMessage()))
|
||||
.onError(error -> pushContext.reportError(error))
|
||||
.getOr(Collections.emptyList())
|
||||
.stream()
|
||||
.forEach(room -> {
|
||||
|
@ -198,14 +201,11 @@ public class MonitoringProctoringService {
|
|||
final PageAction action =
|
||||
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM)
|
||||
.withEntityKey(entityKey)
|
||||
.withExec(_action -> {
|
||||
final int actualRoomSize = proctoringGUIService
|
||||
.getActualCollectingRoomSize(room.name);
|
||||
if (actualRoomSize <= 0) {
|
||||
return _action;
|
||||
}
|
||||
return showExamProctoringRoom(proctoringSettings, room, _action);
|
||||
})
|
||||
.withExec(_action -> openExamProctoringRoom(
|
||||
proctoringGUIService,
|
||||
proctoringSettings,
|
||||
room,
|
||||
_action))
|
||||
.withNameAttributes(
|
||||
room.subject,
|
||||
room.roomSize,
|
||||
|
@ -226,6 +226,7 @@ public class MonitoringProctoringService {
|
|||
.withParentEntityKey(entityKey);
|
||||
this.proctorRoomConnectionsPopup.show(pc, collectingRoom.subject);
|
||||
}));
|
||||
|
||||
processProctorRoomActionActivation(
|
||||
proctoringGUIService.getCollectingRoomActionItem(room.name),
|
||||
room, pageContext);
|
||||
|
@ -235,60 +236,53 @@ public class MonitoringProctoringService {
|
|||
updateTownhallButton(proctoringGUIService, pageContext);
|
||||
}
|
||||
|
||||
public PageAction openExamCollectionProctorScreen(
|
||||
final PageAction action,
|
||||
final ClientConnectionData connectionData) {
|
||||
private PageAction openExamProctoringRoom(
|
||||
final ProctoringGUIService proctoringGUIService,
|
||||
final ProctoringServiceSettings proctoringSettings,
|
||||
final RemoteProctoringRoom room,
|
||||
final PageAction action) {
|
||||
|
||||
try {
|
||||
final String examId = action.getEntityKey().modelId;
|
||||
if (!proctoringGUIService.isCollectingRoomEnabled(room.name)) {
|
||||
return action;
|
||||
}
|
||||
|
||||
final ProctoringServiceSettings proctoringSettings = this.pageService.getRestService()
|
||||
.getBuilder(GetProctoringSettings.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, examId)
|
||||
final ProctoringRoomConnection proctoringConnectionData = this.pageService
|
||||
.getRestService()
|
||||
.getBuilder(GetProctorRoomConnection.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(proctoringSettings.examId))
|
||||
.withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room.name)
|
||||
.withQueryParam(ProctoringRoomConnection.ATTR_SUBJECT, Utils.encodeFormURL_UTF_8(room.subject))
|
||||
.call()
|
||||
.getOrThrow();
|
||||
|
||||
ProctoringGUIService.setCurrentProctoringWindowData(
|
||||
String.valueOf(proctoringSettings.examId),
|
||||
proctoringConnectionData);
|
||||
|
||||
final String script = String.format(
|
||||
OPEN_ROOM_SCRIPT,
|
||||
room.name,
|
||||
800,
|
||||
1200,
|
||||
room.name,
|
||||
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
|
||||
this.remoteProctoringEndpoint);
|
||||
|
||||
RWT.getClient()
|
||||
.getService(JavaScriptExecutor.class)
|
||||
.execute(script);
|
||||
|
||||
final boolean newWindow = this.pageService.getCurrentUser()
|
||||
.getProctoringGUIService()
|
||||
.registerProctoringWindow(String.valueOf(room.examId), room.name, room.name);
|
||||
|
||||
if (newWindow) {
|
||||
this.pageService.getRestService()
|
||||
.getBuilder(NotifyProctoringRoomOpened.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(proctoringSettings.examId))
|
||||
.withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room.name)
|
||||
.call()
|
||||
.getOrThrow();
|
||||
|
||||
final Optional<RemoteProctoringRoom> roomOptional =
|
||||
this.pageService.getRestService().getBuilder(GetCollectingRooms.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, examId)
|
||||
.call()
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
.filter(room -> room.id.equals(connectionData.clientConnection.remoteProctoringRoomId))
|
||||
.findFirst();
|
||||
|
||||
if (roomOptional.isPresent()) {
|
||||
final RemoteProctoringRoom room = roomOptional.get();
|
||||
final ProctoringRoomConnection proctoringConnectionData = this.pageService
|
||||
.getRestService()
|
||||
.getBuilder(GetProctorRoomConnection.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(proctoringSettings.examId))
|
||||
.withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room.name)
|
||||
.withQueryParam(ProctoringRoomConnection.ATTR_SUBJECT, Utils.encodeFormURL_UTF_8(room.subject))
|
||||
.call()
|
||||
.getOrThrow();
|
||||
|
||||
ProctoringGUIService.setCurrentProctoringWindowData(examId, proctoringConnectionData);
|
||||
final String script = String.format(
|
||||
MonitoringProctoringService.OPEN_ROOM_SCRIPT,
|
||||
room.name,
|
||||
800,
|
||||
1200,
|
||||
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
|
||||
this.remoteProctoringEndpoint);
|
||||
|
||||
RWT.getClient()
|
||||
.getService(JavaScriptExecutor.class)
|
||||
.execute(script);
|
||||
|
||||
this.pageService.getCurrentUser()
|
||||
.getProctoringGUIService()
|
||||
.registerProctoringWindow(examId, room.name, room.name);
|
||||
}
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to open popup for collecting room: ", e);
|
||||
action.pageContext().notifyError(CLOSE_COLLECTING_ERROR, e);
|
||||
.onError(error -> log.error("Failed to notify proctoring room opened: ", error));
|
||||
}
|
||||
|
||||
return action;
|
||||
|
@ -329,6 +323,7 @@ public class MonitoringProctoringService {
|
|||
connectionToken,
|
||||
420,
|
||||
640,
|
||||
connectionData.clientConnection.userSessionId,
|
||||
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
|
||||
this.remoteProctoringEndpoint);
|
||||
javaScriptExecutor.execute(script);
|
||||
|
@ -370,6 +365,7 @@ public class MonitoringProctoringService {
|
|||
windowName,
|
||||
800,
|
||||
1200,
|
||||
"Town-Hall",
|
||||
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
|
||||
this.remoteProctoringEndpoint);
|
||||
javaScriptExecutor.execute(script);
|
||||
|
@ -444,59 +440,19 @@ public class MonitoringProctoringService {
|
|||
final PageContext pageContext) {
|
||||
|
||||
try {
|
||||
|
||||
final boolean active = room.roomSize > 0 && !room.isOpen;
|
||||
final Display display = pageContext.getRoot().getDisplay();
|
||||
final PageAction action = (PageAction) treeItem.getData(ActionPane.ACTION_EVENT_CALL_KEY);
|
||||
final Image image = room.roomSize > 0
|
||||
final Image image = active
|
||||
? action.definition.icon.getImage(display)
|
||||
: action.definition.icon.getGreyedImage(display);
|
||||
treeItem.setImage(image);
|
||||
treeItem.setForeground(room.roomSize > 0 ? null : new Color(display, Constants.GREY_DISABLED));
|
||||
treeItem.setForeground(active ? null : new Color(display, Constants.GREY_DISABLED));
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.warn("Failed to set Proctor-Room-Activation: ", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private PageAction showExamProctoringRoom(
|
||||
final ProctoringServiceSettings proctoringSettings,
|
||||
final RemoteProctoringRoom room,
|
||||
final PageAction action) {
|
||||
|
||||
final ProctoringRoomConnection proctoringConnectionData = this.pageService
|
||||
.getRestService()
|
||||
.getBuilder(GetProctorRoomConnection.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(proctoringSettings.examId))
|
||||
.withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room.name)
|
||||
.withQueryParam(ProctoringRoomConnection.ATTR_SUBJECT, Utils.encodeFormURL_UTF_8(room.subject))
|
||||
.call()
|
||||
.getOrThrow();
|
||||
|
||||
ProctoringGUIService.setCurrentProctoringWindowData(
|
||||
String.valueOf(proctoringSettings.examId),
|
||||
proctoringConnectionData);
|
||||
|
||||
final String script = String.format(
|
||||
OPEN_ROOM_SCRIPT,
|
||||
room.name,
|
||||
800,
|
||||
1200,
|
||||
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
|
||||
this.remoteProctoringEndpoint);
|
||||
|
||||
RWT.getClient()
|
||||
.getService(JavaScriptExecutor.class)
|
||||
.execute(script);
|
||||
|
||||
this.pageService.getCurrentUser()
|
||||
.getProctoringGUIService()
|
||||
.registerProctoringWindow(String.valueOf(room.examId), room.name, room.name);
|
||||
|
||||
this.pageService.getRestService().getBuilder(NotifyProctoringRoomOpened.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(proctoringSettings.examId))
|
||||
.withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room.name)
|
||||
.call()
|
||||
.onError(error -> log.error("Failed to notify proctoring room opened: ", error));
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -78,7 +78,6 @@ public class ProctoringGUIService {
|
|||
final RemoteProctoringRoom remoteProctoringRoom = getRemoteProctoringRoom(item);
|
||||
if (remoteProctoringRoom != null && remoteProctoringRoom.roomSize > 0) {
|
||||
showConnectionsPopup.accept(remoteProctoringRoom);
|
||||
//this.proctorRoomConnectionsPopup.show(pc, remoteProctoringRoom.subject);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -99,12 +98,13 @@ public class ProctoringGUIService {
|
|||
.orElse(null);
|
||||
}
|
||||
|
||||
public int getActualCollectingRoomSize(final String roomName) {
|
||||
public boolean isCollectingRoomEnabled(final String roomName) {
|
||||
try {
|
||||
return this.collectingRoomsActionState.get(roomName).a.roomSize;
|
||||
final Pair<RemoteProctoringRoom, TreeItem> pair = this.collectingRoomsActionState.get(roomName);
|
||||
return pair.a.roomSize > 0 && !pair.a.isOpen;
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to get actual collecting room size for room: {} cause: ", roomName, e.getMessage());
|
||||
return -1;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,12 +117,14 @@ public class ProctoringGUIService {
|
|||
this.collectingRoomsActionState.clear();
|
||||
}
|
||||
|
||||
public void registerProctoringWindow(
|
||||
public boolean registerProctoringWindow(
|
||||
final String examId,
|
||||
final String windowName,
|
||||
final String roomName) {
|
||||
|
||||
this.openWindows.put(windowName, new RoomData(roomName, examId));
|
||||
return this.openWindows.putIfAbsent(
|
||||
windowName,
|
||||
new RoomData(roomName, examId)) == null;
|
||||
}
|
||||
|
||||
public String getTownhallWindowName(final String examId) {
|
||||
|
|
|
@ -321,7 +321,9 @@ public class TableFilter<ROW> {
|
|||
final Composite innerComposite = createInnerComposite(parent);
|
||||
final GridData gridData = new GridData(SWT.FILL, SWT.END, true, true);
|
||||
|
||||
this.textInput = TableFilter.this.entityTable.widgetFactory.textInput(innerComposite);
|
||||
this.textInput = TableFilter.this.entityTable.widgetFactory.textInput(
|
||||
innerComposite,
|
||||
super.attribute.columnName);
|
||||
this.textInput.setLayoutData(gridData);
|
||||
return this;
|
||||
}
|
||||
|
|
|
@ -8,9 +8,11 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.gui.widget;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Tuple;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.PageService;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.rap.rwt.widgets.DropDown;
|
||||
import org.eclipse.swt.SWT;
|
||||
|
@ -25,10 +27,9 @@ import org.eclipse.swt.widgets.Text;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Tuple;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.PageService;
|
||||
|
||||
public final class MultiSelectionCombo extends Composite implements Selection {
|
||||
|
||||
|
@ -68,7 +69,7 @@ public final class MultiSelectionCombo extends Composite implements Selection {
|
|||
setLayout(gridLayout);
|
||||
|
||||
this.addListener(SWT.Resize, this::adaptColumnWidth);
|
||||
this.textInput = widgetFactory.textInput(this);
|
||||
this.textInput = widgetFactory.textInput(this, "selection");
|
||||
this.textCell = new GridData(SWT.LEFT, SWT.CENTER, true, true);
|
||||
this.textInput.setLayoutData(this.textCell);
|
||||
this.dropDown = new DropDown(this.textInput, SWT.NONE);
|
||||
|
|
|
@ -152,7 +152,8 @@ public final class ThresholdList extends Composite {
|
|||
} else {
|
||||
Double.parseDouble(s);
|
||||
}
|
||||
});
|
||||
},
|
||||
VALUE_TEXT_KEY);
|
||||
final GridData valueCell = new GridData(SWT.FILL, SWT.CENTER, true, false);
|
||||
valueInput.setLayoutData(valueCell);
|
||||
|
||||
|
|
|
@ -70,6 +70,10 @@ public class WidgetFactory {
|
|||
private static final String ADD_HTML_ATTR_TEST_ID = "test-id";
|
||||
private static final String SUB_TITLE_TExT_SUFFIX = ".subtitle";
|
||||
|
||||
public enum AriaRole {
|
||||
link
|
||||
}
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WidgetFactory.class);
|
||||
|
||||
public static final int TEXT_AREA_INPUT_MIN_HEIGHT = 100;
|
||||
|
@ -363,18 +367,21 @@ public class WidgetFactory {
|
|||
|
||||
public Button buttonLocalized(final Composite parent, final String locTextKey) {
|
||||
final Button button = new Button(parent, SWT.NONE);
|
||||
setAttribute(button, "role", "button");
|
||||
this.polyglotPageService.injectI18n(button, new LocTextKey(locTextKey));
|
||||
return button;
|
||||
}
|
||||
|
||||
public Button buttonLocalized(final Composite parent, final LocTextKey locTextKey) {
|
||||
final Button button = new Button(parent, SWT.NONE);
|
||||
setAttribute(button, "role", "button");
|
||||
this.polyglotPageService.injectI18n(button, locTextKey);
|
||||
return button;
|
||||
}
|
||||
|
||||
public Button buttonLocalized(final Composite parent, final CustomVariant variant, final String locTextKey) {
|
||||
final Button button = new Button(parent, SWT.NONE);
|
||||
setAttribute(button, "role", "button");
|
||||
this.polyglotPageService.injectI18n(button, new LocTextKey(locTextKey));
|
||||
button.setData(RWT.CUSTOM_VARIANT, variant.key);
|
||||
return button;
|
||||
|
@ -387,6 +394,7 @@ public class WidgetFactory {
|
|||
final LocTextKey toolTipKey) {
|
||||
|
||||
final Button button = new Button(parent, type);
|
||||
setAttribute(button, "role", "button");
|
||||
this.polyglotPageService.injectI18n(button, locTextKey, toolTipKey);
|
||||
return button;
|
||||
}
|
||||
|
@ -453,42 +461,85 @@ public class WidgetFactory {
|
|||
return labelLocalized;
|
||||
}
|
||||
|
||||
public Text textInput(final Composite content) {
|
||||
return textInput(content, false, false);
|
||||
public Text textInput(final Composite content, final LocTextKey label) {
|
||||
return textInput(content, false, false, this.i18nSupport.getText(label));
|
||||
}
|
||||
|
||||
public Text textLabel(final Composite content) {
|
||||
return textInput(content, false, true);
|
||||
public Text textInput(final Composite content, final String label) {
|
||||
return textInput(content, false, false, label);
|
||||
}
|
||||
|
||||
public Text passwordInput(final Composite content) {
|
||||
return textInput(content, true, false);
|
||||
public Text passwordInput(final Composite content, final LocTextKey label) {
|
||||
return textInput(content, true, false, this.i18nSupport.getText(label));
|
||||
}
|
||||
|
||||
public Text textAreaInput(final Composite content, final boolean readonly) {
|
||||
return readonly
|
||||
public Text passwordInput(final Composite content, final String label) {
|
||||
return textInput(content, true, false, label);
|
||||
}
|
||||
|
||||
public Text textAreaInput(
|
||||
final Composite content,
|
||||
final boolean readonly,
|
||||
final LocTextKey label) {
|
||||
|
||||
return textAreaInput(content, readonly, this.i18nSupport.getText(label));
|
||||
}
|
||||
|
||||
public Text textAreaInput(
|
||||
final Composite content,
|
||||
final boolean readonly,
|
||||
final String label) {
|
||||
|
||||
final Text input = readonly
|
||||
? new Text(content, SWT.LEFT | SWT.MULTI)
|
||||
: new Text(content, SWT.LEFT | SWT.BORDER | SWT.MULTI);
|
||||
if (label != null) {
|
||||
WidgetFactory.setAttribute(input, "aria-label", label);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
public Text textInput(final Composite content, final boolean password, final boolean readonly) {
|
||||
return readonly
|
||||
public Text textInput(
|
||||
final Composite content,
|
||||
final boolean password,
|
||||
final boolean readonly,
|
||||
final LocTextKey label) {
|
||||
|
||||
return textInput(content, password, readonly, this.i18nSupport.getText(label));
|
||||
}
|
||||
|
||||
public Text textInput(
|
||||
final Composite content,
|
||||
final boolean password,
|
||||
final boolean readonly,
|
||||
final String label) {
|
||||
|
||||
final Text input = readonly
|
||||
? new Text(content, SWT.LEFT)
|
||||
: new Text(content, (password)
|
||||
? SWT.LEFT | SWT.BORDER | SWT.PASSWORD
|
||||
: SWT.LEFT | SWT.BORDER);
|
||||
}
|
||||
|
||||
public Text numberInput(final Composite content, final Consumer<String> numberCheck) {
|
||||
return numberInput(content, numberCheck, false);
|
||||
}
|
||||
|
||||
public Text numberInput(final Composite content, final Consumer<String> numberCheck, final boolean readonly) {
|
||||
if (readonly) {
|
||||
return new Text(content, SWT.LEFT | SWT.READ_ONLY);
|
||||
if (label != null) {
|
||||
WidgetFactory.setAttribute(input, "aria-label", label);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
final Text numberInput = new Text(content, SWT.RIGHT | SWT.BORDER);
|
||||
public Text numberInput(final Composite content, final Consumer<String> numberCheck, final LocTextKey label) {
|
||||
return numberInput(content, numberCheck, false, label);
|
||||
}
|
||||
|
||||
public Text numberInput(
|
||||
final Composite content,
|
||||
final Consumer<String> numberCheck,
|
||||
final boolean readonly,
|
||||
final LocTextKey label) {
|
||||
|
||||
final Text numberInput = new Text(content, (readonly) ? SWT.LEFT | SWT.READ_ONLY : SWT.RIGHT | SWT.BORDER);
|
||||
if (label != null) {
|
||||
WidgetFactory.setAttribute(numberInput, "aria-label", this.i18nSupport.getText(label));
|
||||
}
|
||||
if (numberCheck != null) {
|
||||
numberInput.addListener(SWT.Verify, event -> {
|
||||
final String value = event.text;
|
||||
|
@ -884,11 +935,11 @@ public class WidgetFactory {
|
|||
setAttribute(widget, ADD_HTML_ATTR_TEST_ID, value);
|
||||
}
|
||||
|
||||
public static void setARIARole(final Widget widget, final String value) {
|
||||
setAttribute(widget, ADD_HTML_ATTR_ARIA_ROLE, value);
|
||||
public static void setARIARole(final Widget widget, final AriaRole role) {
|
||||
setAttribute(widget, ADD_HTML_ATTR_ARIA_ROLE, role.name());
|
||||
}
|
||||
|
||||
private static void setAttribute(final Widget widget, final String name, final String value) {
|
||||
public static void setAttribute(final Widget widget, final String name, final String value) {
|
||||
if (!widget.isDisposed()) {
|
||||
final String $el = widget instanceof Text ? "$input" : "$el";
|
||||
final String id = WidgetUtil.getId(widget);
|
||||
|
|
|
@ -60,7 +60,5 @@ public class CacheConfig extends JCacheConfigurerSupport {
|
|||
log.error("Failed to initialize caching with EHCache. Fallback to simple caching");
|
||||
return new ConcurrentMapCacheManager();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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;
|
||||
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.WebserviceInfoDAO;
|
||||
|
||||
@Component
|
||||
@WebServiceProfile
|
||||
public class SEBServerMigrationStrategy implements FlywayMigrationStrategy {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SEBServerMigrationStrategy.class);
|
||||
|
||||
private final boolean cleanDBOnStartup;
|
||||
private final WebserviceInfo webserviceInfo;
|
||||
private final WebserviceInfoDAO webserviceInfoDAO;
|
||||
|
||||
public SEBServerMigrationStrategy(
|
||||
final WebserviceInfo webserviceInfo,
|
||||
final WebserviceInfoDAO webserviceInfoDAO,
|
||||
@Value("${sebserver.webservice.clean-db-on-startup:false}") final boolean cleanDBOnStartup) {
|
||||
|
||||
this.webserviceInfo = webserviceInfo;
|
||||
this.webserviceInfoDAO = webserviceInfoDAO;
|
||||
this.cleanDBOnStartup = cleanDBOnStartup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void migrate(final Flyway flyway) {
|
||||
try {
|
||||
|
||||
// If we are in a distributed setup only apply migration task if this is the master service
|
||||
// or if there was no data base initialization yet at all.
|
||||
if (this.webserviceInfo.isDistributed()) {
|
||||
if (this.webserviceInfoDAO.isInitialized()) {
|
||||
final boolean isMaster = this.webserviceInfoDAO.isMaster(this.webserviceInfo.getWebserviceUUID());
|
||||
if (!isMaster) {
|
||||
log.info(
|
||||
"Skip migration task since this is not a master instance: {}",
|
||||
this.webserviceInfo.getWebserviceUUID());
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.cleanDBOnStartup) {
|
||||
flyway.clean();
|
||||
}
|
||||
flyway.migrate();
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to apply migration task: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -10,13 +10,9 @@ package ch.ethz.seb.sebserver.webservice;
|
|||
|
||||
import org.cryptonode.jncryptor.AES256JNCryptor;
|
||||
import org.cryptonode.jncryptor.JNCryptor;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
|
@ -25,9 +21,6 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
|||
@WebServiceProfile
|
||||
public class WebserviceConfig {
|
||||
|
||||
@Value("${sebserver.webservice.clean-db-on-startup:false}")
|
||||
boolean cleanDBOnStartup;
|
||||
|
||||
@Lazy
|
||||
@Bean
|
||||
public JNCryptor jnCryptor() {
|
||||
|
@ -36,24 +29,4 @@ public class WebserviceConfig {
|
|||
return aes256jnCryptor;
|
||||
}
|
||||
|
||||
/** For test, development and demo profile, we want to always clean up and
|
||||
* Start the migration from scratch to work with the same data.
|
||||
*
|
||||
* @return FlywayMigrationStrategy for "dev-ws", "test", "demo" profiles */
|
||||
@Bean
|
||||
@Profile(value = { "dev-ws", "test", "demo" })
|
||||
public FlywayMigrationStrategy cleanMigrateStrategy() {
|
||||
final FlywayMigrationStrategy strategy = new FlywayMigrationStrategy() {
|
||||
@Override
|
||||
public void migrate(final Flyway flyway) {
|
||||
if (WebserviceConfig.this.cleanDBOnStartup) {
|
||||
flyway.clean();
|
||||
}
|
||||
flyway.migrate();
|
||||
}
|
||||
};
|
||||
|
||||
return strategy;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -86,6 +86,10 @@ public class WebserviceInit implements ApplicationListener<ApplicationReadyEvent
|
|||
SEBServerInit.INIT_LOGGER.info("---->");
|
||||
SEBServerInit.INIT_LOGGER.info("----> *** Info:");
|
||||
|
||||
if (this.webserviceInfo.isDistributed()) {
|
||||
SEBServerInit.INIT_LOGGER.info("----> Distributed Setup: {}", this.webserviceInfo.getWebserviceUUID());
|
||||
}
|
||||
|
||||
try {
|
||||
SEBServerInit.INIT_LOGGER.info("----> Server address: {}", this.environment.getProperty("server.address"));
|
||||
SEBServerInit.INIT_LOGGER.info("----> Server port: {}", this.environment.getProperty("server.port"));
|
||||
|
@ -126,6 +130,7 @@ public class WebserviceInit implements ApplicationListener<ApplicationReadyEvent
|
|||
|
||||
SEBServerInit.INIT_LOGGER.info("---->");
|
||||
SEBServerInit.INIT_LOGGER.info("----> Unregister Webservice: {}", this.webserviceInfo.getWebserviceUUID());
|
||||
|
||||
this.webserviceInfoDAO.unregister(this.webserviceInfo.getWebserviceUUID());
|
||||
|
||||
SEBServerInit.INIT_LOGGER.info("---->");
|
||||
|
|
|
@ -69,7 +69,8 @@ public interface AdditionalAttributesDAO {
|
|||
|
||||
/** Use this to delete all additional attributes for a given entity.
|
||||
*
|
||||
* @param type the entity type
|
||||
* @param entityId the entity identifier (primary-key) */
|
||||
void deleteAll(Long entityId);
|
||||
void deleteAll(EntityType type, Long entityId);
|
||||
|
||||
}
|
||||
|
|
|
@ -121,9 +121,17 @@ public interface RemoteProctoringRoomDAO {
|
|||
|
||||
/** Get currently active break-out rooms for given connectionToken
|
||||
*
|
||||
* @param examId The exam identifier of the connection
|
||||
* @param connectionTokens The connection token of the client connection
|
||||
* @return Result refer to active break-out rooms or to an error when happened */
|
||||
Result<Collection<RemoteProctoringRoom>> getBreakoutRooms(String connectionToken);
|
||||
|
||||
/** Get a list of client connection tokens of connections that currently are in
|
||||
* break-out rooms, including the town-hall room
|
||||
*
|
||||
* @param examId The exam identifier of the connection
|
||||
* @return Result refer to active break-out rooms or to an error when happened */
|
||||
Result<Collection<String>> getConnectionsInBreakoutRooms(Long examId);
|
||||
|
||||
void setCollectingRoomOpenFlag(Long roomId, boolean isOpen);
|
||||
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
|
|||
* the persistent data table. */
|
||||
public interface WebserviceInfoDAO {
|
||||
|
||||
boolean isInitialized();
|
||||
|
||||
/** Register a SEB webservice within the persistent storage
|
||||
*
|
||||
* @param uuid A unique identifier that was generated by the webservice on startup
|
||||
|
|
|
@ -12,6 +12,8 @@ import java.util.Collection;
|
|||
import java.util.Optional;
|
||||
|
||||
import org.mybatis.dynamic.sql.SqlBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
@ -29,6 +31,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO
|
|||
@WebServiceProfile
|
||||
public class AdditionalAttributesDAOImpl implements AdditionalAttributesDAO {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AdditionalAttributesDAOImpl.class);
|
||||
|
||||
private final AdditionalAttributeRecordMapper additionalAttributeRecordMapper;
|
||||
|
||||
protected AdditionalAttributesDAOImpl(final AdditionalAttributeRecordMapper additionalAttributeRecordMapper) {
|
||||
|
@ -164,14 +168,21 @@ public class AdditionalAttributesDAOImpl implements AdditionalAttributesDAO {
|
|||
|
||||
@Override
|
||||
@Transactional
|
||||
public void deleteAll(final Long entityId) {
|
||||
this.additionalAttributeRecordMapper
|
||||
.deleteByExample()
|
||||
.where(
|
||||
AdditionalAttributeRecordDynamicSqlSupport.entityId,
|
||||
SqlBuilder.isEqualTo(entityId))
|
||||
.build()
|
||||
.execute();
|
||||
public void deleteAll(final EntityType type, final Long entityId) {
|
||||
try {
|
||||
this.additionalAttributeRecordMapper
|
||||
.deleteByExample()
|
||||
.where(
|
||||
AdditionalAttributeRecordDynamicSqlSupport.entityType,
|
||||
SqlBuilder.isEqualTo(type.name()))
|
||||
.and(
|
||||
AdditionalAttributeRecordDynamicSqlSupport.entityId,
|
||||
SqlBuilder.isEqualTo(entityId))
|
||||
.build()
|
||||
.execute();
|
||||
} catch (final Exception e) {
|
||||
log.warn("Failed to delete all additional attributes for: {} cause: {}", entityId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -216,23 +216,21 @@ public class CertificateDAOImpl implements CertificateDAO {
|
|||
final boolean[] keyUsage = cert.getKeyUsage();
|
||||
final EnumSet<CertificateType> result = EnumSet.noneOf(CertificateType.class);
|
||||
|
||||
// digitalSignature
|
||||
if (keyUsage[0]) {
|
||||
result.add(CertificateType.DIGITAL_SIGNATURE);
|
||||
}
|
||||
if (keyUsage != null) {
|
||||
// digitalSignature
|
||||
if (keyUsage[0]) {
|
||||
result.add(CertificateType.DIGITAL_SIGNATURE);
|
||||
}
|
||||
|
||||
// dataEncipherment
|
||||
if (keyUsage[2] || keyUsage[3]) {
|
||||
result.add(CertificateType.DATA_ENCIPHERMENT);
|
||||
}
|
||||
// dataEncipherment
|
||||
if (keyUsage[2] || keyUsage[3]) {
|
||||
result.add(CertificateType.DATA_ENCIPHERMENT);
|
||||
}
|
||||
|
||||
// keyCertSign
|
||||
if (keyUsage[5]) {
|
||||
result.add(CertificateType.KEY_CERT_SIGN);
|
||||
}
|
||||
|
||||
if (result.isEmpty()) {
|
||||
result.add(CertificateType.UNKNOWN);
|
||||
// keyCertSign
|
||||
if (keyUsage[5]) {
|
||||
result.add(CertificateType.KEY_CERT_SIGN);
|
||||
}
|
||||
}
|
||||
|
||||
final String alias = certificates.keyStore.engineGetCertificateAlias(cert);
|
||||
|
@ -240,6 +238,10 @@ public class CertificateDAOImpl implements CertificateDAO {
|
|||
result.add(CertificateType.DATA_ENCIPHERMENT_PRIVATE_KEY);
|
||||
}
|
||||
|
||||
if (result.isEmpty()) {
|
||||
result.add(CertificateType.UNKNOWN);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -635,7 +635,8 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
.execute();
|
||||
|
||||
// delete all additional attributes
|
||||
this.additionalAttributeRecordMapper.deleteByExample()
|
||||
this.additionalAttributeRecordMapper
|
||||
.deleteByExample()
|
||||
.where(AdditionalAttributeRecordDynamicSqlSupport.entityType, isEqualTo(EntityType.EXAM.name()))
|
||||
.and(AdditionalAttributeRecordDynamicSqlSupport.entityId, isIn(ids))
|
||||
.build()
|
||||
|
|
|
@ -35,6 +35,7 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
|
|||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.RemoteProctoringRoomRecordDynamicSqlSupport;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.RemoteProctoringRoomRecordMapper;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.RemoteProctoringRoomRecord;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.RemoteProctoringRoomDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.NewRoom;
|
||||
|
@ -49,11 +50,14 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
|
|||
private static final Object RESERVE_ROOM_LOCK = new Object();
|
||||
|
||||
private final RemoteProctoringRoomRecordMapper remoteProctoringRoomRecordMapper;
|
||||
private final AdditionalAttributesDAO additionalAttributesDAO;
|
||||
|
||||
protected RemoteProctoringRoomDAOImpl(
|
||||
final RemoteProctoringRoomRecordMapper remoteProctoringRoomRecordMapper) {
|
||||
final RemoteProctoringRoomRecordMapper remoteProctoringRoomRecordMapper,
|
||||
final AdditionalAttributesDAO additionalAttributesDAO) {
|
||||
|
||||
this.remoteProctoringRoomRecordMapper = remoteProctoringRoomRecordMapper;
|
||||
this.additionalAttributesDAO = additionalAttributesDAO;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -227,6 +231,10 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
|
|||
@Transactional
|
||||
public Result<EntityKey> deleteRoom(final Long roomId) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
this.additionalAttributesDAO
|
||||
.deleteAll(EntityType.REMOTE_PROCTORING_ROOM, roomId);
|
||||
|
||||
this.remoteProctoringRoomRecordMapper
|
||||
.deleteByPrimaryKey(roomId);
|
||||
|
||||
|
@ -249,7 +257,15 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
|
|||
.execute();
|
||||
|
||||
return ids.stream()
|
||||
.map(id -> new EntityKey(String.valueOf(id), EntityType.REMOTE_PROCTORING_ROOM))
|
||||
.map(roomId -> {
|
||||
this.additionalAttributesDAO.deleteAll(
|
||||
EntityType.REMOTE_PROCTORING_ROOM,
|
||||
roomId);
|
||||
return roomId;
|
||||
})
|
||||
.map(roomId -> new EntityKey(
|
||||
String.valueOf(roomId),
|
||||
EntityType.REMOTE_PROCTORING_ROOM))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
});
|
||||
|
@ -322,6 +338,37 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
|
|||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Result<Collection<String>> getConnectionsInBreakoutRooms(final Long examId) {
|
||||
return Result.tryCatch(() -> this.remoteProctoringRoomRecordMapper
|
||||
.selectByExample()
|
||||
.where(RemoteProctoringRoomRecordDynamicSqlSupport.examId, isEqualTo(examId))
|
||||
.and(RemoteProctoringRoomRecordDynamicSqlSupport.breakOutConnections, isNotNull())
|
||||
.build()
|
||||
.execute()
|
||||
.stream()
|
||||
.flatMap(room -> Arrays.asList(
|
||||
StringUtils.split(
|
||||
room.getBreakOutConnections(),
|
||||
Constants.LIST_SEPARATOR_CHAR))
|
||||
.stream())
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void setCollectingRoomOpenFlag(final Long roomId, final boolean isOpen) {
|
||||
this.additionalAttributesDAO.saveAdditionalAttribute(
|
||||
EntityType.REMOTE_PROCTORING_ROOM,
|
||||
roomId,
|
||||
RemoteProctoringRoom.ATTR_IS_OPEN,
|
||||
BooleanUtils.toStringTrueFalse(isOpen))
|
||||
.onError(error -> log.error("Failed to set open flag for proctoring room: {} : {}",
|
||||
roomId,
|
||||
error.getMessage()));
|
||||
}
|
||||
|
||||
private RemoteProctoringRoom toDomainModel(final RemoteProctoringRoomRecord record) {
|
||||
final String breakOutConnections = record.getBreakOutConnections();
|
||||
final Collection<String> connections = StringUtils.isNotBlank(breakOutConnections)
|
||||
|
@ -337,7 +384,22 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
|
|||
BooleanUtils.toBooleanObject(record.getTownhallRoom()),
|
||||
connections,
|
||||
record.getJoinKey(),
|
||||
record.getRoomData());
|
||||
record.getRoomData(),
|
||||
isOpen(record));
|
||||
}
|
||||
|
||||
private boolean isOpen(final RemoteProctoringRoomRecord record) {
|
||||
if (record.getTownhallRoom() != 0 || !StringUtils.isBlank(record.getBreakOutConnections())) {
|
||||
return false;
|
||||
} else {
|
||||
return BooleanUtils.toBoolean(this.additionalAttributesDAO
|
||||
.getAdditionalAttribute(
|
||||
EntityType.REMOTE_PROCTORING_ROOM,
|
||||
record.getId(),
|
||||
RemoteProctoringRoom.ATTR_IS_OPEN)
|
||||
.map(rec -> rec.getValue())
|
||||
.getOrElse(() -> Constants.FALSE_STRING));
|
||||
}
|
||||
}
|
||||
|
||||
private RemoteProctoringRoomRecord createNewCollectingRoom(
|
||||
|
|
|
@ -47,6 +47,20 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO {
|
|||
this.forceMaster = forceMaster;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean isInitialized() {
|
||||
try {
|
||||
this.webserviceServerInfoRecordMapper
|
||||
.selectByExample()
|
||||
.build()
|
||||
.execute();
|
||||
return true;
|
||||
} catch (final Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public boolean register(final String uuid, final String address) {
|
||||
|
@ -173,7 +187,7 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO {
|
|||
.execute();
|
||||
return true;
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to register webservice: uuid: {}", uuid, e);
|
||||
log.warn("Failed to unregister webservice: uuid: {}, cause: ", uuid, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -328,20 +328,22 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
|||
if (mapping.containsKey(ProctoringServiceSettings.ATTR_ENABLED_FEATURES)) {
|
||||
try {
|
||||
final String value = mapping.get(ProctoringServiceSettings.ATTR_ENABLED_FEATURES).getValue();
|
||||
return EnumSet.copyOf(Arrays.asList(StringUtils.split(value, Constants.LIST_SEPARATOR))
|
||||
.stream()
|
||||
.map(str -> {
|
||||
try {
|
||||
return ProctoringFeature.valueOf(str);
|
||||
} catch (final Exception e) {
|
||||
log.error(
|
||||
"Failed to enabled single features for proctoring settings. Skipping. {}",
|
||||
e.getMessage());
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet()));
|
||||
return StringUtils.isNotBlank(value)
|
||||
? EnumSet.copyOf(Arrays.asList(StringUtils.split(value, Constants.LIST_SEPARATOR))
|
||||
.stream()
|
||||
.map(str -> {
|
||||
try {
|
||||
return ProctoringFeature.valueOf(str);
|
||||
} catch (final Exception e) {
|
||||
log.error(
|
||||
"Failed to enabled single features for proctoring settings. Skipping. {}",
|
||||
e.getMessage());
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet()))
|
||||
: EnumSet.noneOf(ProctoringFeature.class);
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to get enabled features for proctoring settings. Enable all. {}", e.getMessage());
|
||||
return EnumSet.allOf(ProctoringFeature.class);
|
||||
|
|
|
@ -33,13 +33,20 @@ public interface ExamProctoringRoomService {
|
|||
* @return Result refer to the resulting collection of ClientConnection or to an error when happened */
|
||||
Result<Collection<ClientConnection>> getRoomConnections(Long roomId);
|
||||
|
||||
/** Get a collection of all ClientConnection that are currently connected to a specified collecting room.
|
||||
/** Get a collection of all ClientConnection that are registered to a specified collecting room.
|
||||
*
|
||||
* @param examId The exam identifier of the room
|
||||
* @param roomName The room name
|
||||
* @return Result refer to the resulting collection of ClientConnection or to an error when happened */
|
||||
Result<Collection<ClientConnection>> getCollectingRoomConnections(Long examId, String roomName);
|
||||
|
||||
/** Get a collection of all ClientConnection that are currently connected to a specified collecting room.
|
||||
*
|
||||
* @param examId The exam identifier of the room
|
||||
* @param roomName The room name
|
||||
* @return Result refer to the resulting collection of ClientConnection or to an error when happened */
|
||||
Result<Collection<ClientConnection>> getActiveCollectingRoomConnections(Long examId, String roomName);
|
||||
|
||||
/** This is internally used to update client connections that are flagged for updating
|
||||
* the proctoring room assignment.
|
||||
* This attaches or detaches client connections from or to proctoring rooms of an exam in one batch.
|
||||
|
|
|
@ -14,14 +14,12 @@ import java.util.function.Predicate;
|
|||
|
||||
import org.springframework.cache.CacheManager;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
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.FilterMap;
|
||||
|
@ -185,19 +183,7 @@ public interface ExamSessionService {
|
|||
* @param connection ClientConnectionData instance
|
||||
* @return true if the given ClientConnectionData is an active SEB client connection */
|
||||
static boolean isActiveConnection(final ClientConnectionData connection) {
|
||||
if (connection.clientConnection.status.establishedStatus) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (connection.clientConnection.status == ConnectionStatus.CONNECTION_REQUESTED) {
|
||||
final Long creationTime = connection.clientConnection.getCreationTime();
|
||||
final long millisecondsNow = Utils.getMillisecondsNow();
|
||||
if (millisecondsNow - creationTime < 30 * Constants.SECOND_IN_MILLIS) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return connection.clientConnection.status.clientActiveStatus;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -18,6 +19,7 @@ import java.util.stream.Collectors;
|
|||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
@ -54,19 +56,22 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
|||
private final ExamAdminService examAdminService;
|
||||
private final ExamSessionService examSessionService;
|
||||
private final SEBClientInstructionService sebInstructionService;
|
||||
private final boolean sendBroadcastReset;
|
||||
|
||||
public ExamProctoringRoomServiceImpl(
|
||||
final RemoteProctoringRoomDAO remoteProctoringRoomDAO,
|
||||
final ClientConnectionDAO clientConnectionDAO,
|
||||
final ExamAdminService examAdminService,
|
||||
final ExamSessionService examSessionService,
|
||||
final SEBClientInstructionService sebInstructionService) {
|
||||
final SEBClientInstructionService sebInstructionService,
|
||||
@Value("${sebserver.webservice.proctoring.resetBroadcastOnLeav:true}") final boolean sendBroadcastReset) {
|
||||
|
||||
this.remoteProctoringRoomDAO = remoteProctoringRoomDAO;
|
||||
this.clientConnectionDAO = clientConnectionDAO;
|
||||
this.examAdminService = examAdminService;
|
||||
this.examSessionService = examSessionService;
|
||||
this.sebInstructionService = sebInstructionService;
|
||||
this.sendBroadcastReset = sendBroadcastReset;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -80,8 +85,34 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
|||
}
|
||||
|
||||
@Override
|
||||
public Result<Collection<ClientConnection>> getCollectingRoomConnections(final Long examId, final String roomName) {
|
||||
return this.clientConnectionDAO.getCollectingRoomConnections(examId, roomName);
|
||||
public Result<Collection<ClientConnection>> getCollectingRoomConnections(
|
||||
final Long examId,
|
||||
final String roomName) {
|
||||
|
||||
return this.clientConnectionDAO
|
||||
.getCollectingRoomConnections(examId, roomName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Collection<ClientConnection>> getActiveCollectingRoomConnections(
|
||||
final Long examId,
|
||||
final String roomName) {
|
||||
|
||||
final Collection<String> currentlyInBreakoutRooms = this.remoteProctoringRoomDAO
|
||||
.getConnectionsInBreakoutRooms(examId)
|
||||
.getOrElse(() -> Collections.emptyList());
|
||||
|
||||
if (currentlyInBreakoutRooms.isEmpty()) {
|
||||
return this.clientConnectionDAO
|
||||
.getCollectingRoomConnections(examId, roomName);
|
||||
} else {
|
||||
return this.clientConnectionDAO
|
||||
.getCollectingRoomConnections(examId, roomName)
|
||||
.map(connections -> connections
|
||||
.stream()
|
||||
.filter(cc -> !currentlyInBreakoutRooms.contains(cc.connectionToken))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -230,7 +261,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
|||
} else if (remoteProctoringRoom.townhallRoom) {
|
||||
closeTownhall(examId, settings, examProctoringService);
|
||||
} else {
|
||||
closeCollectingRoom(examId, roomName, examProctoringService);
|
||||
closeCollectingRoom(examId, roomName, settings, examProctoringService);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -356,13 +387,16 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
|||
.notifyBreakOutRoomOpened(proctoringSettings, room)
|
||||
.getOrThrow();
|
||||
} else {
|
||||
final Collection<ClientConnection> clientConnections = this.clientConnectionDAO
|
||||
.getCollectingRoomConnections(examId, roomName)
|
||||
final Collection<ClientConnection> clientConnections = this
|
||||
.getActiveCollectingRoomConnections(examId, roomName)
|
||||
.getOrThrow();
|
||||
|
||||
examProctoringService
|
||||
.notifyCollectingRoomOpened(proctoringSettings, room, clientConnections)
|
||||
.getOrThrow();
|
||||
|
||||
this.remoteProctoringRoomDAO
|
||||
.setCollectingRoomOpenFlag(room.id, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -377,11 +411,13 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
|||
.getActiveConnectionTokens(examId)
|
||||
.getOrThrow();
|
||||
|
||||
// Send default settings to clients
|
||||
this.sendReconfigurationInstructions(
|
||||
examId,
|
||||
connectionTokens,
|
||||
examProctoringService.getDefaultReconfigInstructionAttributes());
|
||||
// Send default settings to clients if fearture is enabled
|
||||
if (this.sendBroadcastReset) {
|
||||
this.sendReconfigurationInstructions(
|
||||
examId,
|
||||
connectionTokens,
|
||||
examProctoringService.getDefaultReconfigInstructionAttributes());
|
||||
}
|
||||
|
||||
// Close and delete town-hall room
|
||||
this.remoteProctoringRoomDAO
|
||||
|
@ -403,20 +439,36 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
|||
private void closeCollectingRoom(
|
||||
final Long examId,
|
||||
final String roomName,
|
||||
final ProctoringServiceSettings proctoringSettings,
|
||||
final ExamProctoringService examProctoringService) {
|
||||
|
||||
// get all connections of the room
|
||||
final List<String> connectionTokens = this.getCollectingRoomConnections(examId, roomName)
|
||||
final List<String> connectionTokens = this.getActiveCollectingRoomConnections(examId, roomName)
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
.map(cc -> cc.connectionToken)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Send default settings to clients
|
||||
this.sendReconfigurationInstructions(
|
||||
examId,
|
||||
connectionTokens,
|
||||
examProctoringService.getDefaultReconfigInstructionAttributes());
|
||||
final RemoteProctoringRoom room = this.remoteProctoringRoomDAO
|
||||
.getRoom(examId, roomName)
|
||||
.onError(error -> log.error("Failed to get room for setting closed: {} {} {}",
|
||||
examId,
|
||||
roomName,
|
||||
error.getMessage()))
|
||||
.getOr(null);
|
||||
|
||||
if (room != null) {
|
||||
this.remoteProctoringRoomDAO
|
||||
.setCollectingRoomOpenFlag(room.id, false);
|
||||
}
|
||||
|
||||
// Send default settings to clients if feature is enabled
|
||||
if (this.sendBroadcastReset) {
|
||||
this.sendReconfigurationInstructions(
|
||||
examId,
|
||||
connectionTokens,
|
||||
examProctoringService.getDefaultReconfigInstructionAttributes());
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupBreakOutRooms(final ClientConnectionRecord cc) {
|
||||
|
@ -453,11 +505,13 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
|||
final ExamProctoringService examProctoringService,
|
||||
final RemoteProctoringRoom remoteProctoringRoom) {
|
||||
|
||||
// Send default settings to clients
|
||||
this.sendReconfigurationInstructions(
|
||||
examId,
|
||||
remoteProctoringRoom.breakOutConnections,
|
||||
examProctoringService.getDefaultReconfigInstructionAttributes());
|
||||
// Send default settings to clients if feature is enabled
|
||||
if (this.sendBroadcastReset) {
|
||||
this.sendReconfigurationInstructions(
|
||||
examId,
|
||||
remoteProctoringRoom.breakOutConnections,
|
||||
examProctoringService.getDefaultReconfigInstructionAttributes());
|
||||
}
|
||||
|
||||
// Dispose the proctoring room on service side
|
||||
examProctoringService
|
||||
|
@ -509,7 +563,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
|||
} else if (!room.breakOutConnections.isEmpty()) {
|
||||
connectionTokens.addAll(room.breakOutConnections);
|
||||
} else {
|
||||
connectionTokens.addAll(this.getCollectingRoomConnections(examId, roomName)
|
||||
connectionTokens.addAll(this.getActiveCollectingRoomConnections(examId, roomName)
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
.map(cc -> cc.connectionToken)
|
||||
|
@ -630,16 +684,17 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
|||
final ClientConnectionData clientConnection = this.examSessionService
|
||||
.getConnectionData(connectionToken)
|
||||
.getOrThrow();
|
||||
final String roomName = this.remoteProctoringRoomDAO
|
||||
.getRoomName(clientConnection.clientConnection.getRemoteProctoringRoomId())
|
||||
|
||||
final RemoteProctoringRoom remoteProctoringRoom = this.remoteProctoringRoomDAO
|
||||
.getRoom(clientConnection.clientConnection.getRemoteProctoringRoomId())
|
||||
.getOrThrow();
|
||||
|
||||
final ProctoringRoomConnection proctoringConnection = examProctoringService
|
||||
.getClientRoomConnection(
|
||||
proctoringSettings,
|
||||
clientConnection.clientConnection.connectionToken,
|
||||
roomName,
|
||||
clientConnection.clientConnection.userSessionId)
|
||||
remoteProctoringRoom.name,
|
||||
remoteProctoringRoom.subject)
|
||||
.getOrThrow();
|
||||
|
||||
sendJoinInstruction(
|
||||
|
|
|
@ -29,6 +29,7 @@ import org.joda.time.DateTimeZone;
|
|||
import org.joda.time.Interval;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
@ -125,6 +126,8 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
private final RemoteProctoringRoomDAO remoteProctoringRoomDAO;
|
||||
private final AuthorizationService authorizationService;
|
||||
private final SEBClientInstructionService sebInstructionService;
|
||||
private final boolean enableWaitingRoom;
|
||||
private final boolean sendRejoinForCollectingRoom;
|
||||
|
||||
public ZoomProctoringService(
|
||||
final ExamSessionService examSessionService,
|
||||
|
@ -134,7 +137,9 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
final JSONMapper jsonMapper,
|
||||
final RemoteProctoringRoomDAO remoteProctoringRoomDAO,
|
||||
final AuthorizationService authorizationService,
|
||||
final SEBClientInstructionService sebInstructionService) {
|
||||
final SEBClientInstructionService sebInstructionService,
|
||||
@Value("${sebserver.webservice.proctoring.enableWaitingRoom:false}") final boolean enableWaitingRoom,
|
||||
@Value("${sebserver.webservice.proctoring.sendRejoinForCollectingRoom:true}") final boolean sendRejoinForCollectingRoom) {
|
||||
|
||||
this.examSessionService = examSessionService;
|
||||
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
|
||||
|
@ -145,6 +150,8 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
this.remoteProctoringRoomDAO = remoteProctoringRoomDAO;
|
||||
this.authorizationService = authorizationService;
|
||||
this.sebInstructionService = sebInstructionService;
|
||||
this.enableWaitingRoom = enableWaitingRoom;
|
||||
this.sendRejoinForCollectingRoom = sendRejoinForCollectingRoom;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -443,23 +450,33 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
if (!this.sendRejoinForCollectingRoom) {
|
||||
// does nothing if the rejoin feature is not enabled
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.remoteProctoringRoomDAO.isTownhallRoomActive(proctoringSettings.examId)) {
|
||||
// do nothing is the town-hall of this exam is open. The clients will automatically join
|
||||
// does nothing if the town-hall of this exam is open. The clients will automatically join
|
||||
// the meeting once the town-hall has been closed
|
||||
return;
|
||||
}
|
||||
|
||||
final ProctoringRoomConnection proctoringRoomConnection = this.getProctorRoomConnection(
|
||||
proctoringSettings,
|
||||
room.name,
|
||||
room.subject)
|
||||
.getOrThrow();
|
||||
|
||||
clientConnections.stream()
|
||||
.forEach(cc -> sendJoinInstruction(
|
||||
proctoringSettings.examId,
|
||||
cc.connectionToken,
|
||||
proctoringRoomConnection));
|
||||
.forEach(cc -> {
|
||||
try {
|
||||
sendJoinInstruction(
|
||||
proctoringSettings.examId,
|
||||
cc.connectionToken,
|
||||
getClientRoomConnection(
|
||||
proctoringSettings,
|
||||
cc.connectionToken,
|
||||
room.name,
|
||||
room.subject)
|
||||
.getOrThrow());
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to send rejoin instruction to SEB client: {}", cc.connectionToken, e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -497,6 +514,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
proctoringSettings.serverURL,
|
||||
credentials,
|
||||
roomName);
|
||||
|
||||
final UserResponse userResponse = this.jsonMapper.readValue(
|
||||
createUser.getBody(),
|
||||
UserResponse.class);
|
||||
|
@ -509,7 +527,9 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
userResponse.id,
|
||||
subject,
|
||||
duration,
|
||||
meetingPwd);
|
||||
meetingPwd,
|
||||
this.enableWaitingRoom);
|
||||
|
||||
final MeetingResponse meetingResponse = this.jsonMapper.readValue(
|
||||
createMeeting.getBody(),
|
||||
MeetingResponse.class);
|
||||
|
@ -520,6 +540,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
userResponse.id,
|
||||
meetingResponse.start_url,
|
||||
meetingResponse.join_url);
|
||||
|
||||
final String additionalZoomRoomDataString = this.jsonMapper
|
||||
.writeValueAsString(additionalZoomRoomData);
|
||||
|
||||
|
@ -558,23 +579,29 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
final StringBuilder builder = new StringBuilder();
|
||||
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
|
||||
|
||||
final String jwtHeaderPart = urlEncoder.encodeToString(
|
||||
ZOOM_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8));
|
||||
final String jwtHeaderPart = urlEncoder
|
||||
.encodeToString(ZOOM_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
final String jwtPayload = String.format(
|
||||
ZOOM_API_ACCESS_TOKEN_PAYLOAD.replaceAll(" ", "").replaceAll("\n", ""),
|
||||
ZOOM_API_ACCESS_TOKEN_PAYLOAD
|
||||
.replaceAll(" ", "")
|
||||
.replaceAll("\n", ""),
|
||||
credentials.clientIdAsString(),
|
||||
expTime);
|
||||
final String jwtPayloadPart = urlEncoder.encodeToString(
|
||||
jwtPayload.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
final String jwtPayloadPart = urlEncoder
|
||||
.encodeToString(jwtPayload.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
final String message = jwtHeaderPart + "." + jwtPayloadPart;
|
||||
|
||||
final Mac sha256_HMAC = Mac.getInstance(TOKEN_ENCODE_ALG);
|
||||
final SecretKeySpec secret_key = new SecretKeySpec(
|
||||
Utils.toByteArray(decryptedSecret),
|
||||
TOKEN_ENCODE_ALG);
|
||||
|
||||
sha256_HMAC.init(secret_key);
|
||||
final String hash = urlEncoder.encodeToString(
|
||||
sha256_HMAC.doFinal(Utils.toByteArray(message)));
|
||||
final String hash = urlEncoder
|
||||
.encodeToString(sha256_HMAC.doFinal(Utils.toByteArray(message)));
|
||||
|
||||
builder.append(message)
|
||||
.append(".")
|
||||
|
@ -705,7 +732,8 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
final String userId,
|
||||
final String topic,
|
||||
final int duration,
|
||||
final CharSequence password) {
|
||||
final CharSequence password,
|
||||
final boolean waitingRoom) {
|
||||
|
||||
try {
|
||||
|
||||
|
@ -718,7 +746,8 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
final CreateMeetingRequest createRoomRequest = new CreateMeetingRequest(
|
||||
topic,
|
||||
duration,
|
||||
password);
|
||||
password,
|
||||
waitingRoom);
|
||||
|
||||
final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(createRoomRequest);
|
||||
final HttpHeaders headers = getHeaders(credentials);
|
||||
|
|
|
@ -123,7 +123,8 @@ public interface ZoomRoomRequestResponse {
|
|||
public CreateMeetingRequest(
|
||||
final String topic,
|
||||
final int duration,
|
||||
final CharSequence password) {
|
||||
final CharSequence password,
|
||||
final boolean waitingRoom) {
|
||||
|
||||
this.type = 2; // Scheduled Meeting
|
||||
this.start_time = DateTime.now(DateTimeZone.UTC).toString("yyyy-MM-dd'T'HH:mm:ss");
|
||||
|
@ -131,18 +132,24 @@ public interface ZoomRoomRequestResponse {
|
|||
this.timezone = DateTimeZone.UTC.getID();
|
||||
this.topic = topic;
|
||||
this.password = password;
|
||||
this.settings = new Settings();
|
||||
this.settings = new Settings(waitingRoom);
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
static class Settings {
|
||||
@JsonProperty final boolean host_video = true;
|
||||
@JsonProperty final boolean participant_video = true;
|
||||
@JsonProperty final boolean join_before_host = true;
|
||||
@JsonProperty final boolean host_video = false;
|
||||
@JsonProperty final boolean mute_upon_entry = false;
|
||||
@JsonProperty final boolean join_before_host;
|
||||
@JsonProperty final int jbh_time = 0;
|
||||
@JsonProperty final boolean use_pmi = false;
|
||||
@JsonProperty final String audio = "voip";
|
||||
@JsonProperty final boolean waiting_room = false;
|
||||
@JsonProperty final boolean waiting_room;
|
||||
@JsonProperty final boolean allow_multiple_devices = false;
|
||||
|
||||
public Settings(final boolean waitingRoom) {
|
||||
this.join_before_host = !waitingRoom;
|
||||
this.waiting_room = waitingRoom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -269,6 +269,10 @@ public class ExamAPI_V1_Controller {
|
|||
final String pingNumString = request.getParameter(API.EXAM_API_PING_NUMBER);
|
||||
final String instructionConfirm = request.getParameter(API.EXAM_API_PING_INSTRUCTION_CONFIRM);
|
||||
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("****************** SEB client connection: {}", connectionToken);
|
||||
}
|
||||
|
||||
if (instructionConfirm != null) {
|
||||
this.sebClientConnectionService.confirmInstructionDone(connectionToken, instructionConfirm);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ logging.level.ch=INFO
|
|||
logging.level.org.springframework.cache=INFO
|
||||
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=DEBUG
|
||||
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=DEBUG
|
||||
logging.level.ch.ethz.seb.sebserver.webservice.weblayer.api.ExamAPI_V1_Controller=TRACE
|
||||
|
||||
sebserver.http.client.connect-timeout=150000
|
||||
sebserver.http.client.connection-request-timeout=100000
|
||||
|
|
|
@ -66,3 +66,7 @@ sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token
|
|||
sebserver.webservice.lms.moodle.api.token.request.paths=/login/token.php
|
||||
sebserver.webservice.lms.address.alias=
|
||||
|
||||
sebserver.webservice.proctoring.resetBroadcastOnLeav=true
|
||||
sebserver.webservice.proctoring.zoom.enableWaitingRoom=false
|
||||
sebserver.webservice.proctoring.zoom.sendRejoinForCollectingRoom=true
|
||||
|
||||
|
|
|
@ -10,7 +10,9 @@ INSERT IGNORE INTO configuration_attribute VALUES
|
|||
(960, 'allowFind', 'CHECKBOX', null, null, null, null, 'true'),
|
||||
(961, 'allowPDFReaderToolbar', 'CHECKBOX', null, null, null, null, 'false'),
|
||||
|
||||
(970, 'setVmwareConfiguration', 'CHECKBOX', null, null, null, null, 'false')
|
||||
(970, 'setVmwareConfiguration', 'CHECKBOX', null, null, null, null, 'false'),
|
||||
(971, 'allowedDisplaysIgnoreFailure', 'CHECKBOX', null, null, null, null, 'false')
|
||||
|
||||
;
|
||||
|
||||
-- -----------------------------------------------------
|
||||
|
@ -66,6 +68,11 @@ INSERT IGNORE INTO orientation (config_attribute_id, template_id, view_id, group
|
|||
-- insert Set VMWare Configuration
|
||||
INSERT IGNORE INTO orientation (config_attribute_id, template_id, view_id, group_id, x_position, y_position, width, height, title) VALUES
|
||||
(970, 0, 10, 'registry', 0, 7, 4, 1, 'NONE');
|
||||
|
||||
-- insert mac settings
|
||||
|
||||
INSERT IGNORE INTO orientation (config_attribute_id, template_id, view_id, group_id, x_position, y_position, width, height, title) VALUES
|
||||
(971, 0, 9, 'macSettings', 7, 11, 5, 1, 'NONE');
|
||||
|
||||
-- -----------------------------------------------------
|
||||
-- Update old orientations
|
||||
|
@ -105,7 +112,11 @@ UPDATE orientation SET group_id='additionalWindow', x_position=0, y_position=14,
|
|||
UPDATE orientation SET x_position=7, y_position=11, width=5 WHERE config_attribute_id=57;
|
||||
UPDATE orientation SET x_position=7, y_position=13, width=5 WHERE config_attribute_id=58;
|
||||
|
||||
-- insert Set VMWare Configuration
|
||||
-- update Set VMWare Configuration
|
||||
UPDATE orientation SET y_position=8 WHERE config_attribute_id=406;
|
||||
UPDATE orientation SET y_position=9 WHERE config_attribute_id=407;
|
||||
UPDATE orientation SET y_position=10 WHERE config_attribute_id=408;
|
||||
|
||||
-- update mac settings
|
||||
UPDATE orientation SET y_position=10 WHERE config_attribute_id=315;
|
||||
UPDATE orientation SET y_position=12 WHERE config_attribute_id=316;
|
||||
|
|
|
@ -666,6 +666,9 @@ sebserver.exam.proctoring.form.features.TOWN_HALL=Town-Hall Room
|
|||
sebserver.exam.proctoring.form.features.ONE_TO_ONE=One to One Room
|
||||
sebserver.exam.proctoring.form.features.BROADCAST=Broadcasting Feature
|
||||
sebserver.exam.proctoring.form.features.ENABLE_CHAT=Chat Feature
|
||||
sebserver.exam.proctoring.form.features.WAITING_ROOM=Enable waiting room for collecting rooms
|
||||
sebserver.exam.proctoring.form.features.SEND_REJOIN_COLLECTING_ROOM=Force rejoin for collecting rooms
|
||||
sebserver.exam.proctoring.form.features.RESET_BROADCAST_ON_LAVE=Reset broadcast on leave
|
||||
|
||||
sebserver.exam.proctoring.type.servertype.JITSI_MEET=Jitsi Meet Server
|
||||
sebserver.exam.proctoring.type.servertype.JITSI_MEET.tooltip=Use a Jitsi Meet server for proctoring
|
||||
|
@ -1296,6 +1299,8 @@ sebserver.examconfig.props.label.logLevel.4=Verbose
|
|||
sebserver.examconfig.props.label.logLevel.4.tooltip=Verbose level contains events of all levels
|
||||
sebserver.examconfig.props.label.allowApplicationLog=Allow access to application log (Win)
|
||||
sebserver.examconfig.props.label.showApplicationLogButton=Show log button on taskbar (Win)
|
||||
sebserver.examconfig.props.label.allowedDisplaysIgnoreFailure=Ignore errors when validating display configuration.
|
||||
sebserver.examconfig.props.label.allowedDisplaysIgnoreFailure.tooltip=Needs to be active when using SEB inside a virtual machine
|
||||
|
||||
sebserver.examconfig.props.group.registry=While running SEB
|
||||
sebserver.examconfig.props.group.registry.tooltip=Options in the Windows Security Screen invoked by Ctrl-Alt-Del
|
||||
|
@ -1628,7 +1633,7 @@ sebserver.monitoring.exam.action.proctoring.openTownhall=Open Townhall
|
|||
sebserver.monitoring.exam.action.proctoring.showTownhall=Show Townhall
|
||||
sebserver.monitoring.exam.action.proctoring.closeTownhall=Close Townhall
|
||||
|
||||
sebserver.monitoring.exam.proctoring.room.all.name=Exam Room
|
||||
sebserver.monitoring.exam.proctoring.room.all.name=Townhall Room
|
||||
sebserver.monitoring.exam.proctoring.action.close=Close Window
|
||||
sebserver.monitoring.exam.proctoring.action.broadcaston.audio=Start Audio Broadcast
|
||||
sebserver.monitoring.exam.proctoring.action.broadcastoff.audio=End Audio Broadcast
|
||||
|
@ -1674,6 +1679,7 @@ sebserver.monitoring.search.action=Search
|
|||
sebserver.monitoring.search.list.empty=No Client Connections available
|
||||
sebserver.monitoring.search.list.name=Session or User Name
|
||||
sebserver.monitoring.search.list.ip=IP Address
|
||||
sebserver.monitoring.search.list.status=Status
|
||||
|
||||
|
||||
sebserver.monitoring.exam.connection.emptySelection=At first please select a Connection from the list
|
||||
|
|
|
@ -21,7 +21,7 @@ import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService
|
|||
public class ZoomWindowScriptResolverTest {
|
||||
|
||||
@Test
|
||||
public void testJitsiWindowScriptResolver() {
|
||||
public void testZoomWindowScriptResolver() {
|
||||
final DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader();
|
||||
final Resource resource = defaultResourceLoader.getResource(ZoomWindowScriptResolver.RES_PATH);
|
||||
final ZoomWindowScriptResolver zoomWindowScriptResolver = new ZoomWindowScriptResolver(resource);
|
||||
|
|
Loading…
Add table
Reference in a new issue