Merge remote-tracking branch 'origin/dev-1.2' into development

This commit is contained in:
anhefti 2021-07-12 17:06:15 +02:00
commit 2e4dcca89e
51 changed files with 884 additions and 412 deletions

View file

@ -35,7 +35,7 @@ public class ProctoringServiceSettings implements Entity {
TOWN_HALL, TOWN_HALL,
ONE_TO_ONE, ONE_TO_ONE,
BROADCAST, BROADCAST,
ENABLE_CHAT ENABLE_CHAT,
} }
public static final String ATTR_ENABLE_PROCTORING = "enableProctoring"; public static final String ATTR_ENABLE_PROCTORING = "enableProctoring";

View file

@ -61,9 +61,9 @@ public final class LmsSetup implements GrantEntity, Activatable {
/** The Moodle binding features only the course access API so far */ /** The Moodle binding features only the course access API so far */
MOODLE(Features.COURSE_API /* , Features.SEB_RESTRICTION */), MOODLE(Features.COURSE_API /* , Features.SEB_RESTRICTION */),
/** The Ans Delft binding is on the way */ /** 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 */ /** The OpenOLAT binding is on the way */
OPEN_OLAT(Features.COURSE_API, Features.SEB_RESTRICTION); OPEN_OLAT();
public final EnumSet<Features> features; public final EnumSet<Features> features;

View file

@ -34,12 +34,12 @@ public final class ClientConnection implements GrantEntity {
public final boolean connectingStatus; public final boolean connectingStatus;
public final boolean establishedStatus; public final boolean establishedStatus;
public final boolean indicatorActiveStatus; public final boolean clientActiveStatus;
ConnectionStatus(final boolean connectingStatus, final boolean establishedStatus) { ConnectionStatus(final boolean connectingStatus, final boolean establishedStatus) {
this.connectingStatus = connectingStatus; this.connectingStatus = connectingStatus;
this.establishedStatus = establishedStatus; this.establishedStatus = establishedStatus;
this.indicatorActiveStatus = connectingStatus || establishedStatus; this.clientActiveStatus = connectingStatus || establishedStatus;
} }
} }

View file

@ -22,8 +22,10 @@ import ch.ethz.seb.sebserver.gbl.util.Utils;
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
public class RemoteProctoringRoom { public class RemoteProctoringRoom {
public static final String ATTR_IS_OPEN = "isOpen";
public static final RemoteProctoringRoom NULL_ROOM = new RemoteProctoringRoom( 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) @JsonProperty(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID)
public final Long id; public final Long id;
@ -52,6 +54,9 @@ public class RemoteProctoringRoom {
@JsonProperty(Domain.REMOTE_PROCTORING_ROOM.ATTR_ROOM_DATA) @JsonProperty(Domain.REMOTE_PROCTORING_ROOM.ATTR_ROOM_DATA)
public final String additionalRoomData; public final String additionalRoomData;
@JsonProperty(ATTR_IS_OPEN)
public final Boolean isOpen;
@JsonCreator @JsonCreator
public RemoteProctoringRoom( public RemoteProctoringRoom(
@JsonProperty(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID) final Long id, @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_TOWNHALL_ROOM) final Boolean townhallRoom,
@JsonProperty(Domain.REMOTE_PROCTORING_ROOM.ATTR_BREAK_OUT_CONNECTIONS) final Collection<String> breakOutConnections, @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_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.id = id;
this.examId = examId; this.examId = examId;
@ -73,6 +79,7 @@ public class RemoteProctoringRoom {
this.breakOutConnections = Utils.immutableCollectionOf(breakOutConnections); this.breakOutConnections = Utils.immutableCollectionOf(breakOutConnections);
this.joinKey = joinKey; this.joinKey = joinKey;
this.additionalRoomData = additionalRoomData; this.additionalRoomData = additionalRoomData;
this.isOpen = isOpen;
} }
public Long getId() { public Long getId() {
@ -111,6 +118,10 @@ public class RemoteProctoringRoom {
return this.additionalRoomData; return this.additionalRoomData;
} }
public Boolean getIsOpen() {
return this.isOpen;
}
@Override @Override
public String toString() { public String toString() {
final StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();
@ -128,6 +139,12 @@ public class RemoteProctoringRoom {
builder.append(this.townhallRoom); builder.append(this.townhallRoom);
builder.append(", breakOutConnections="); builder.append(", breakOutConnections=");
builder.append(this.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("]"); builder.append("]");
return builder.toString(); return builder.toString();
} }

View file

@ -73,6 +73,12 @@ public class RAPConfiguration implements ApplicationConfiguration {
properties.put(WebClient.FAVICON, "fav_icon"); properties.put(WebClient.FAVICON, "fav_icon");
application.addEntryPoint(guiEntrypoint, new RAPSpringEntryPointFactory(), properties); 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); application.addEntryPoint(proctoringEntrypoint, new RAPRemoteProcotringEntryPointFactory(), properties);
} catch (final RuntimeException re) { } catch (final RuntimeException re) {

View file

@ -139,11 +139,12 @@ public class ExamProctoringSettings {
.valueOf(form.getFieldValue(ProctoringServiceSettings.ATTR_SERVER_TYPE)); .valueOf(form.getFieldValue(ProctoringServiceSettings.ATTR_SERVER_TYPE));
final String features = form.getFieldValue(ProctoringServiceSettings.ATTR_ENABLED_FEATURES); final String features = form.getFieldValue(ProctoringServiceSettings.ATTR_ENABLED_FEATURES);
final EnumSet<ProctoringFeature> featureFlags = final EnumSet<ProctoringFeature> featureFlags = (StringUtils.isNotBlank(features))
EnumSet.copyOf(Arrays.asList(StringUtils.split(features, Constants.LIST_SEPARATOR)) ? EnumSet.copyOf(Arrays.asList(StringUtils.split(features, Constants.LIST_SEPARATOR))
.stream() .stream()
.map(str -> ProctoringFeature.valueOf(str)) .map(str -> ProctoringFeature.valueOf(str))
.collect(Collectors.toSet())); .collect(Collectors.toSet()))
: EnumSet.noneOf(ProctoringFeature.class);
examProctoring = new ProctoringServiceSettings( examProctoring = new ProctoringServiceSettings(
Long.parseLong(entityKey.modelId), Long.parseLong(entityKey.modelId),

View file

@ -12,10 +12,12 @@ import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.SWT; import org.eclipse.swt.SWT;
import org.eclipse.swt.internal.widgets.IControlAdapter;
import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.MessageBox; import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Text; 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.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; 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.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService; import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; 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 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 PageService pageService;
private final AuthorizationContextHolder authorizationContextHolder; private final AuthorizationContextHolder authorizationContextHolder;
private final WidgetFactory widgetFactory; private final WidgetFactory widgetFactory;
@ -67,7 +76,6 @@ public class LoginPage implements TemplateComposer {
public void compose(final PageContext pageContext) { public void compose(final PageContext pageContext) {
final Composite parent = pageContext.getParent(); final Composite parent = pageContext.getParent();
WidgetFactory.setTestId(parent, "login-page"); WidgetFactory.setTestId(parent, "login-page");
WidgetFactory.setARIARole(parent, "composite");
final Composite loginGroup = new Composite(parent, SWT.NONE); final Composite loginGroup = new Composite(parent, SWT.NONE);
final GridLayout rowLayout = new GridLayout(); final GridLayout rowLayout = new GridLayout();
@ -76,16 +84,17 @@ public class LoginPage implements TemplateComposer {
loginGroup.setLayout(rowLayout); loginGroup.setLayout(rowLayout);
loginGroup.setData(RWT.CUSTOM_VARIANT, WidgetFactory.CustomVariant.LOGIN.key); 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.setLayoutData(new GridData(300, -1));
name.setAlignment(SWT.BOTTOM); 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)); loginName.setLayoutData(new GridData(SWT.FILL, SWT.TOP, false, false));
GridData gridData = new GridData(SWT.FILL, SWT.TOP, false, false); GridData gridData = new GridData(SWT.FILL, SWT.TOP, false, false);
gridData.verticalIndent = 10; 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); 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)); loginPassword.setLayoutData(new GridData(SWT.FILL, SWT.TOP, false, false));
final Composite buttons = new Composite(loginGroup, SWT.NONE); final Composite buttons = new Composite(loginGroup, SWT.NONE);
@ -93,7 +102,7 @@ public class LoginPage implements TemplateComposer {
buttons.setLayout(new GridLayout(2, false)); buttons.setLayout(new GridLayout(2, false));
buttons.setData(RWT.CUSTOM_VARIANT, WidgetFactory.CustomVariant.LOGIN_BACK.key); 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 = new GridData(SWT.LEFT, SWT.TOP, false, false);
gridData.verticalIndent = 10; gridData.verticalIndent = 10;
loginButton.setLayoutData(gridData); loginButton.setLayoutData(gridData);
@ -127,12 +136,17 @@ public class LoginPage implements TemplateComposer {
}); });
if (this.registeringEnabled) { 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 = new GridData(SWT.LEFT, SWT.TOP, false, false);
gridData.verticalIndent = 10; gridData.verticalIndent = 10;
registerButton.setLayoutData(gridData); registerButton.setLayoutData(gridData);
registerButton.addListener(SWT.Selection, event -> pageContext.forwardToPage(this.defaultRegisterPage)); 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( private void login(

View file

@ -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.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils; 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.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.service.ResourceService; import ch.ethz.seb.sebserver.gui.service.ResourceService;
import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
@ -269,11 +270,14 @@ public class MonitoringClientConnection implements TemplateComposer {
final Supplier<EntityTable<ClientNotification>> notificationTableSupplier = _notificationTableSupplier; final Supplier<EntityTable<ClientNotification>> notificationTableSupplier = _notificationTableSupplier;
// server push update // server push update
final ProctoringUpdateErrorHandler proctoringUpdateErrorHandler =
new ProctoringUpdateErrorHandler(this.pageService, pageContext);
this.serverPushService.runServerPush( this.serverPushService.runServerPush(
new ServerPushContext( new ServerPushContext(
content, content,
Utils.truePredicate(), Utils.truePredicate(),
MonitoringRunningExam.createServerPushUpdateErrorHandler(this.pageService, pageContext)), proctoringUpdateErrorHandler),
this.pollInterval, this.pollInterval,
context -> clientConnectionDetails.updateData(), context -> clientConnectionDetails.updateData(),
context -> clientConnectionDetails.updateGUI(notificationTableSupplier, pageContext)); context -> clientConnectionDetails.updateGUI(notificationTableSupplier, pageContext));
@ -365,7 +369,7 @@ public class MonitoringClientConnection implements TemplateComposer {
}) })
.noEventPropagation() .noEventPropagation()
.publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER) && .publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER) &&
connectionData.clientConnection.status.indicatorActiveStatus); connectionData.clientConnection.status.clientActiveStatus);
if (connectionData.clientConnection.status == ConnectionStatus.ACTIVE) { if (connectionData.clientConnection.status == ConnectionStatus.ACTIVE) {
final ProctoringServiceSettings proctoringSettings = restService final ProctoringServiceSettings proctoringSettings = restService

View file

@ -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.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; 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;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; 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.i18n.LocTextKey;
@ -43,6 +44,8 @@ public class MonitoringExamSearchPopup {
new LocTextKey("sebserver.monitoring.search.list.name"); new LocTextKey("sebserver.monitoring.search.list.name");
private static final LocTextKey TABLE_COLUMN_IP_ADDRESS = private static final LocTextKey TABLE_COLUMN_IP_ADDRESS =
new LocTextKey("sebserver.monitoring.search.list.ip"); 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; private final PageService pageService;
@ -50,9 +53,16 @@ public class MonitoringExamSearchPopup {
new TableFilterAttribute(CriteriaType.TEXT, ClientConnection.FILTER_ATTR_SESSION_ID); new TableFilterAttribute(CriteriaType.TEXT, ClientConnection.FILTER_ATTR_SESSION_ID);
private final TableFilterAttribute ipFilter = private final TableFilterAttribute ipFilter =
new TableFilterAttribute(CriteriaType.TEXT, ClientConnection.FILTER_ATTR_IP_STRING); new TableFilterAttribute(CriteriaType.TEXT, ClientConnection.FILTER_ATTR_IP_STRING);
private final TableFilterAttribute statusFilter;
protected MonitoringExamSearchPopup(final PageService pageService) { protected MonitoringExamSearchPopup(final PageService pageService) {
this.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) { public void show(final PageContext pageContext) {
@ -60,6 +70,7 @@ public class MonitoringExamSearchPopup {
pageContext.getParent().getShell(), pageContext.getParent().getShell(),
this.pageService.getWidgetFactory()); this.pageService.getWidgetFactory());
dialog.setLargeDialogWidth(); dialog.setLargeDialogWidth();
dialog.setDialogHeight(380);
dialog.open( dialog.open(
TITLE_TEXT_KEY, TITLE_TEXT_KEY,
pageContext, pageContext,
@ -90,6 +101,12 @@ public class MonitoringExamSearchPopup {
ClientConnection::getClientAddress) ClientConnection::getClientAddress)
.withFilter(this.ipFilter)) .withFilter(this.ipFilter))
.withColumn(new ColumnDefinition<>(
Domain.CLIENT_CONNECTION.ATTR_STATUS,
TABLE_COLUMN_STATUS,
ClientConnection::getStatus)
.withFilter(this.statusFilter))
.withDefaultAction(t -> actionBuilder .withDefaultAction(t -> actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION) .newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION)
.withParentEntityKey(examKey) .withParentEntityKey(examKey)

View file

@ -149,13 +149,22 @@ public class MonitoringRunningExam implements TemplateComposer {
restService.getBuilder(GetClientConnectionDataList.class) restService.getBuilder(GetClientConnectionDataList.class)
.withURIVariable(API.PARAM_PARENT_MODEL_ID, exam.getModelId()); .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( final ClientConnectionTable clientTable = new ClientConnectionTable(
this.pageService, this.pageService,
tablePane, tablePane,
this.asyncRunner, this.asyncRunner,
exam, exam,
indicators, indicators,
restCall); restCall,
pushContext);
clientTable clientTable
.withDefaultAction( .withDefaultAction(
@ -172,10 +181,7 @@ public class MonitoringRunningExam implements TemplateComposer {
ActionDefinition.MONITOR_EXAM_NEW_PROCTOR_ROOM)); ActionDefinition.MONITOR_EXAM_NEW_PROCTOR_ROOM));
this.serverPushService.runServerPush( this.serverPushService.runServerPush(
new ServerPushContext( pushContext,
content,
Utils.truePredicate(),
createServerPushUpdateErrorHandler(this.pageService, pageContext)),
this.pollInterval, this.pollInterval,
context -> clientTable.updateValues(), context -> clientTable.updateValues(),
updateTableGUI(clientTable)); 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( this.monitoringProctoringService.initCollectingRoomActions(
pushContext,
pageContext, pageContext,
actionBuilder, actionBuilder,
proctoringSettings, proctoringSettings,
proctoringGUIService); proctoringGUIService);
this.serverPushService.runServerPush( this.serverPushService.runServerPush(
new ServerPushContext( pushContext,
parent,
Utils.truePredicate(),
createServerPushUpdateErrorHandler(this.pageService, pageContext)),
this.proctoringRoomUpdateInterval, this.proctoringRoomUpdateInterval,
context -> this.monitoringProctoringService.updateCollectingRoomActions( context -> this.monitoringProctoringService.updateCollectingRoomActions(
context,
pageContext, pageContext,
actionBuilder, actionBuilder,
proctoringSettings, proctoringSettings,
@ -422,7 +436,7 @@ public class MonitoringRunningExam implements TemplateComposer {
private Set<EntityKey> selectionForQuitInstruction(final ClientConnectionTable clientTable) { private Set<EntityKey> selectionForQuitInstruction(final ClientConnectionTable clientTable) {
final Set<String> connectionTokens = clientTable.getConnectionTokens( final Set<String> connectionTokens = clientTable.getConnectionTokens(
cc -> cc.status.indicatorActiveStatus, cc -> cc.status.clientActiveStatus,
true); true);
if (connectionTokens == null || connectionTokens.isEmpty()) { if (connectionTokens == null || connectionTokens.isEmpty()) {
return Collections.emptySet(); return Collections.emptySet();
@ -480,30 +494,53 @@ public class MonitoringRunningExam implements TemplateComposer {
}; };
} }
static final Function<Exception, Boolean> createServerPushUpdateErrorHandler( static final class ProctoringUpdateErrorHandler implements Function<Exception, Boolean> {
private final PageService pageService;
private final PageContext pageContext;
private int errors = 0;
public ProctoringUpdateErrorHandler(
final PageService pageService, final PageService pageService,
final PageContext pageContext) { final PageContext pageContext) {
return error -> { this.pageService = pageService;
log.error("Fialed to update server push: {}", error.getMessage()); this.pageContext = pageContext;
}
private boolean checkUserSession() {
try { try {
pageService.getCurrentUser().get(); this.pageService.getCurrentUser().get();
return true;
} catch (final Exception e) { } catch (final Exception e) {
log.error("Failed to verify current user after server push error: {}", e.getMessage()); try {
log.info("Force logout and session cleanup..."); this.pageContext.forwardToLoginPage();
pageContext.forwardToLoginPage();
final MessageBox logoutSuccess = new Message( final MessageBox logoutSuccess = new Message(
pageContext.getShell(), this.pageContext.getShell(),
pageService.getI18nSupport().getText("sebserver.logout"), this.pageService.getI18nSupport().getText("sebserver.logout"),
Utils.formatLineBreaks( Utils.formatLineBreaks(
pageService.getI18nSupport().getText("sebserver.logout.invalid-session.message")), this.pageService.getI18nSupport()
.getText("sebserver.logout.invalid-session.message")),
SWT.ICON_INFORMATION, SWT.ICON_INFORMATION,
pageService.getI18nSupport()); this.pageService.getI18nSupport());
logoutSuccess.open(null); logoutSuccess.open(null);
} catch (final Exception ee) {
log.warn("Unable to auto-logout: ", ee.getMessage());
}
return true; 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;
}
} }
} }

View file

@ -8,21 +8,28 @@
package ch.ethz.seb.sebserver.gui.content; package ch.ethz.seb.sebserver.gui.content;
import org.eclipse.swt.SWT; import java.util.ArrayList;
import org.eclipse.swt.widgets.Composite; import java.util.List;
import org.eclipse.swt.widgets.Label;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.api.API; 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.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; 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.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.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.PageContext; 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;
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.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.service.remote.webservice.api.session.GetCollectingRoomConnections;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition;
import ch.ethz.seb.sebserver.gui.table.EntityTable;
@Lazy @Lazy
@Component @Component
@ -31,6 +38,10 @@ public class ProctorRoomConnectionsPopup {
private static final LocTextKey TITLE_TEXT_KEY = private static final LocTextKey TITLE_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.proctoring.room.connections.title"); 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; private final PageService pageService;
@ -43,29 +54,60 @@ public class ProctorRoomConnectionsPopup {
pageContext.getParent().getShell(), pageContext.getParent().getShell(),
this.pageService.getWidgetFactory()); this.pageService.getWidgetFactory());
dialog.setLargeDialogWidth(); dialog.setLargeDialogWidth();
dialog.setDialogHeight(380);
dialog.open( dialog.open(
new LocTextKey(TITLE_TEXT_KEY.name, roomSubject), new LocTextKey(TITLE_TEXT_KEY.name, roomSubject),
pageContext, pageContext,
this::compose); c -> this.compose(c, dialog));
} }
private void compose(final PageContext pageContext) { private void compose(final PageContext pageContext, final ModalInputDialog<Void> dialog) {
final Composite parent = pageContext.getParent();
final Composite grid = this.pageService.getWidgetFactory().createPopupScrollComposite(parent);
final EntityKey entityKey = pageContext.getEntityKey(); final EntityKey entityKey = pageContext.getEntityKey();
final EntityKey parentEntityKey = pageContext.getParentEntityKey(); 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) .getBuilder(GetCollectingRoomConnections.class)
.withURIVariable(API.PARAM_MODEL_ID, parentEntityKey.modelId) .withURIVariable(API.PARAM_MODEL_ID, parentEntityKey.modelId)
.withQueryParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, entityKey.modelId) .withQueryParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, entityKey.modelId)
.call() .call()
.getOrThrow() .getOrThrow());
.stream()
.forEach(connection -> { this.pageService.staticListTableBuilder(connections, EntityType.CLIENT_CONNECTION)
final Label label = new Label(grid, SWT.NONE);
label.setText(connection.userSessionId); .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());
} }
} }

View file

@ -119,10 +119,10 @@ public final class TextFieldBuilder extends FieldBuilder<String> {
} }
final Text textInput = (this.isNumber) final Text textInput = (this.isNumber)
? builder.widgetFactory.numberInput(fieldGrid, this.numberCheck, readonly) ? builder.widgetFactory.numberInput(fieldGrid, this.numberCheck, readonly, this.label)
: (this.isArea) : (this.isArea)
? builder.widgetFactory.textAreaInput(fieldGrid, readonly) ? builder.widgetFactory.textAreaInput(fieldGrid, readonly, this.label)
: builder.widgetFactory.textInput(fieldGrid, this.isPassword, readonly); : builder.widgetFactory.textInput(fieldGrid, this.isPassword, readonly, this.label);
if (builder.pageService.getFormTooltipMode() == PageService.FormTooltipMode.INPUT) { if (builder.pageService.getFormTooltipMode() == PageService.FormTooltipMode.INPUT) {
builder.pageService.getPolyglotPageService().injectI18nTooltip( builder.pageService.getPolyglotPageService().injectI18nTooltip(

View file

@ -567,6 +567,14 @@ public class ResourceService {
.getText(SEB_CONNECTION_STATUS_KEY_PREFIX + name, name); .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) { public String localizedExamTypeName(final ExamConfigurationMap examMap) {
if (examMap.examType == null) { if (examMap.examType == null) {
return Constants.EMPTY_NOTE; return Constants.EMPTY_NOTE;

View file

@ -93,12 +93,16 @@ public abstract class AbstractProctoringView implements RemoteProctoringView {
final BroadcastActionState state = final BroadcastActionState state =
(BroadcastActionState) videoAction.getData(BroadcastActionState.KEY_NAME); (BroadcastActionState) videoAction.getData(BroadcastActionState.KEY_NAME);
if (audioAction != null) {
this.pageService.getPolyglotPageService().injectI18n( this.pageService.getPolyglotPageService().injectI18n(
audioAction, audioAction,
state.video ? BROADCAST_AUDIO_ON_TEXT_KEY : BROADCAST_AUDIO_OFF_TEXT_KEY); state.video ? BROADCAST_AUDIO_ON_TEXT_KEY : BROADCAST_AUDIO_OFF_TEXT_KEY);
}
if (videoAction != null) {
this.pageService.getPolyglotPageService().injectI18n( this.pageService.getPolyglotPageService().injectI18n(
videoAction, videoAction,
state.video ? BROADCAST_VIDEO_ON_TEXT_KEY : BROADCAST_VIDEO_OFF_TEXT_KEY); state.video ? BROADCAST_VIDEO_ON_TEXT_KEY : BROADCAST_VIDEO_OFF_TEXT_KEY);
}
state.video = !state.video; state.video = !state.video;
state.audio = state.video; state.audio = state.video;

View file

@ -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.service.remote.webservice.auth.AuthorizationContextHolder;
import ch.ethz.seb.sebserver.gui.widget.Message; import ch.ethz.seb.sebserver.gui.widget.Message;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; 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; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant;
@Lazy @Lazy
@ -285,6 +286,8 @@ public class DefaultPageLayout implements TemplateComposer {
log.error("Invalid markup for 'Imprint'", e); log.error("Invalid markup for 'Imprint'", e);
} }
}); });
WidgetFactory.setARIARole(imprint, AriaRole.link);
} }
if (StringUtils.isNoneBlank(i18nSupport.getText(ABOUT_TEXT_KEY, ""))) { if (StringUtils.isNoneBlank(i18nSupport.getText(ABOUT_TEXT_KEY, ""))) {
final Label about = this.widgetFactory.labelLocalized( final Label about = this.widgetFactory.labelLocalized(
@ -299,6 +302,8 @@ public class DefaultPageLayout implements TemplateComposer {
log.error("Invalid markup for 'About'", e); log.error("Invalid markup for 'About'", e);
} }
}); });
WidgetFactory.setARIARole(about, AriaRole.link);
} }
if (StringUtils.isNoneBlank(i18nSupport.getText(HELP_TEXT_KEY, ""))) { if (StringUtils.isNoneBlank(i18nSupport.getText(HELP_TEXT_KEY, ""))) {
final Label help = this.widgetFactory.labelLocalized( final Label help = this.widgetFactory.labelLocalized(
@ -318,6 +323,7 @@ public class DefaultPageLayout implements TemplateComposer {
} }
}); });
WidgetFactory.setARIARole(help, AriaRole.link);
} }
this.widgetFactory.labelLocalized( this.widgetFactory.labelLocalized(
footerRight, footerRight,

View file

@ -17,6 +17,7 @@ import org.eclipse.swt.layout.RowData;
import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; 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); final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true);
content.setLayoutData(headerCell); 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)); parent.addListener(SWT.Dispose, event -> closeRoom(proctoringGUIService, proctoringWindowData));
final String url = this.guiServiceInfo final String url = this.guiServiceInfo

View file

@ -49,6 +49,7 @@ public class ModalInputDialog<T> extends Dialog {
private int dialogWidth = DEFAULT_DIALOG_WIDTH; private int dialogWidth = DEFAULT_DIALOG_WIDTH;
private int dialogHeight = DEFAULT_DIALOG_HEIGHT; private int dialogHeight = DEFAULT_DIALOG_HEIGHT;
private int buttonWidth = DEFAULT_DIALOG_BUTTON_WIDTH; private int buttonWidth = DEFAULT_DIALOG_BUTTON_WIDTH;
private boolean forceHeight = false;
public ModalInputDialog( public ModalInputDialog(
final Shell parent, final Shell parent,
@ -75,6 +76,7 @@ public class ModalInputDialog<T> extends Dialog {
public ModalInputDialog<T> setDialogHeight(final int dialogHeight) { public ModalInputDialog<T> setDialogHeight(final int dialogHeight) {
this.dialogHeight = dialogHeight; this.dialogHeight = dialogHeight;
this.forceHeight = true;
return this; return this;
} }
@ -215,6 +217,9 @@ public class ModalInputDialog<T> extends Dialog {
} }
private int calcDialogHeight(final Composite main) { private int calcDialogHeight(final Composite main) {
if (this.forceHeight) {
return this.dialogHeight;
}
final int actualHeight = main.computeSize(SWT.DEFAULT, SWT.DEFAULT).y; final int actualHeight = main.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
final int displayHeight = main.getDisplay().getClientArea().height; final int displayHeight = main.getDisplay().getClientArea().height;
final int availableHeight = (displayHeight < actualHeight + 100) final int availableHeight = (displayHeight < actualHeight + 100)

View file

@ -17,6 +17,7 @@ import org.eclipse.swt.layout.RowData;
import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; 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); final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true);
content.setLayoutData(headerCell); 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)); parent.addListener(SWT.Dispose, event -> closeRoom(proctoringGUIService, proctoringWindowData));
final String url = this.guiServiceInfo final String url = this.guiServiceInfo
@ -111,13 +117,13 @@ public class ZoomProctoringView extends AbstractProctoringView {
final BroadcastActionState broadcastActionState = new BroadcastActionState(); final BroadcastActionState broadcastActionState = new BroadcastActionState();
if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.BROADCAST)) { if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.BROADCAST)) {
final Button broadcastAudioAction = widgetFactory.buttonLocalized(footer, BROADCAST_AUDIO_ON_TEXT_KEY); // final Button broadcastAudioAction = widgetFactory.buttonLocalized(footer, BROADCAST_AUDIO_ON_TEXT_KEY);
broadcastAudioAction.setLayoutData(new RowData()); // broadcastAudioAction.setLayoutData(new RowData());
broadcastAudioAction.addListener(SWT.Selection, event -> toggleBroadcastAudio( // broadcastAudioAction.addListener(SWT.Selection, event -> toggleBroadcastAudio(
proctoringWindowData.examId, // proctoringWindowData.examId,
proctoringWindowData.connectionData.roomName, // proctoringWindowData.connectionData.roomName,
broadcastAudioAction)); // broadcastAudioAction));
broadcastAudioAction.setData(BroadcastActionState.KEY_NAME, broadcastActionState); // broadcastAudioAction.setData(BroadcastActionState.KEY_NAME, broadcastActionState);
final Button broadcastVideoAction = widgetFactory.buttonLocalized(footer, BROADCAST_VIDEO_ON_TEXT_KEY); final Button broadcastVideoAction = widgetFactory.buttonLocalized(footer, BROADCAST_VIDEO_ON_TEXT_KEY);
broadcastVideoAction.setLayoutData(new RowData()); broadcastVideoAction.setLayoutData(new RowData());
@ -125,7 +131,7 @@ public class ZoomProctoringView extends AbstractProctoringView {
proctoringWindowData.examId, proctoringWindowData.examId,
proctoringWindowData.connectionData.roomName, proctoringWindowData.connectionData.roomName,
broadcastVideoAction, broadcastVideoAction,
broadcastAudioAction)); null));
broadcastVideoAction.setData(BroadcastActionState.KEY_NAME, broadcastActionState); broadcastVideoAction.setData(BroadcastActionState.KEY_NAME, broadcastActionState);
} }
if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.ENABLE_CHAT)) { if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.ENABLE_CHAT)) {

View file

@ -21,7 +21,7 @@ public final class ServerPushContext {
public final Composite anchor; public final Composite anchor;
public final Predicate<ServerPushContext> runAgain; public final Predicate<ServerPushContext> runAgain;
public final Function<Exception, Boolean> errorHandler; final Function<Exception, Boolean> errorHandler;
boolean internalStop = false; boolean internalStop = false;
public ServerPushContext( public ServerPushContext(
@ -36,6 +36,12 @@ public final class ServerPushContext {
this.runAgain = runAgain; this.runAgain = runAgain;
} }
public void reportError(final Exception error) {
if (this.errorHandler != null) {
this.internalStop = this.errorHandler.apply(error);
}
}
public boolean runAgain() { public boolean runAgain() {
return !this.internalStop && this.runAgain.test(this); return !this.internalStop && this.runAgain.test(this);
} }

View file

@ -189,7 +189,7 @@ public class ClientConnectionDetails {
final double value = indValue.getValue(); final double value = indValue.getValue();
final String displayValue = IndicatorValue.getDisplayValue(indValue); final String displayValue = IndicatorValue.getDisplayValue(indValue);
if (!this.connectionData.clientConnection.status.indicatorActiveStatus) { if (!this.connectionData.clientConnection.status.clientActiveStatus) {
form.setFieldValue( form.setFieldValue(
indData.indicator.name, indData.indicator.name,

View file

@ -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.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.PageService; 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.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.api.RestCall;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.DisposedOAuth2RestTemplateException; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.DisposedOAuth2RestTemplateException;
import ch.ethz.seb.sebserver.gui.service.session.IndicatorData.ThresholdColor; import ch.ethz.seb.sebserver.gui.service.session.IndicatorData.ThresholdColor;
@ -95,6 +96,8 @@ public final class ClientConnectionTable {
private final AsyncRunner asyncRunner; private final AsyncRunner asyncRunner;
private final Exam exam; private final Exam exam;
private final RestCall<Collection<ClientConnectionData>>.RestCallBuilder restCallBuilder; private final RestCall<Collection<ClientConnectionData>>.RestCallBuilder restCallBuilder;
private final ServerPushContext pushConext;
private final Map<Long, IndicatorData> indicatorMapping; private final Map<Long, IndicatorData> indicatorMapping;
private final Table table; private final Table table;
private final ColorData colorData; private final ColorData colorData;
@ -116,18 +119,22 @@ public final class ClientConnectionTable {
private boolean forceUpdateAll = false; private boolean forceUpdateAll = false;
private boolean updateInProgress = false; private boolean updateInProgress = false;
//private int updateErrors = 0;
public ClientConnectionTable( public ClientConnectionTable(
final PageService pageService, final PageService pageService,
final Composite tableRoot, final Composite tableRoot,
final AsyncRunner asyncRunner, final AsyncRunner asyncRunner,
final Exam exam, final Exam exam,
final Collection<Indicator> indicators, final Collection<Indicator> indicators,
final RestCall<Collection<ClientConnectionData>>.RestCallBuilder restCallBuilder) { final RestCall<Collection<ClientConnectionData>>.RestCallBuilder restCallBuilder,
final ServerPushContext pushConext) {
this.pageService = pageService; this.pageService = pageService;
this.asyncRunner = asyncRunner; this.asyncRunner = asyncRunner;
this.exam = exam; this.exam = exam;
this.restCallBuilder = restCallBuilder; this.restCallBuilder = restCallBuilder;
this.pushConext = pushConext;
final WidgetFactory widgetFactory = pageService.getWidgetFactory(); final WidgetFactory widgetFactory = pageService.getWidgetFactory();
final ResourceService resourceService = pageService.getResourceService(); final ResourceService resourceService = pageService.getResourceService();
@ -188,6 +195,10 @@ public final class ClientConnectionTable {
this.table.layout(); this.table.layout();
} }
// public int getUpdateErrors() {
// return this.updateErrors;
// }
public WidgetFactory getWidgetFactory() { public WidgetFactory getWidgetFactory() {
return this.pageService.getWidgetFactory(); return this.pageService.getWidgetFactory();
} }
@ -316,11 +327,17 @@ public final class ClientConnectionTable {
} }
this.updateInProgress = true; 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() { private void updateValuesAsync(final boolean needsSync) {
if (this.statusFilterChanged || this.forceUpdateAll) {
try {
if (this.statusFilterChanged || this.forceUpdateAll || needsSync) {
this.toDelete.clear(); this.toDelete.clear();
this.toDelete.addAll(this.tableMapping.keySet()); this.toDelete.addAll(this.tableMapping.keySet());
} }
@ -328,8 +345,8 @@ public final class ClientConnectionTable {
.withHeader(API.EXAM_MONITORING_STATE_FILTER, this.statusFilterParam) .withHeader(API.EXAM_MONITORING_STATE_FILTER, this.statusFilterParam)
.call() .call()
.get(error -> { .get(error -> {
log.error("Unexpected error while trying to get client connection table data: ", error);
recoverFromDisposedRestTemplate(error); recoverFromDisposedRestTemplate(error);
this.pushConext.reportError(error);
return Collections.emptyList(); return Collections.emptyList();
}) })
.forEach(data -> { .forEach(data -> {
@ -337,7 +354,7 @@ public final class ClientConnectionTable {
data.getConnectionId(), data.getConnectionId(),
UpdatableTableItem::new); UpdatableTableItem::new);
tableItem.push(data); tableItem.push(data);
if (this.statusFilterChanged || this.forceUpdateAll) { if (this.statusFilterChanged || this.forceUpdateAll || needsSync) {
this.toDelete.remove(data.getConnectionId()); this.toDelete.remove(data.getConnectionId());
} }
}); });
@ -358,6 +375,10 @@ public final class ClientConnectionTable {
this.forceUpdateAll = false; this.forceUpdateAll = false;
this.updateInProgress = false; this.updateInProgress = false;
} catch (final Exception e) {
this.pushConext.reportError(e);
}
} }
public void updateGUI() { public void updateGUI() {
@ -576,7 +597,7 @@ public final class ClientConnectionTable {
continue; continue;
} }
if (!this.connectionData.clientConnection.status.indicatorActiveStatus) { if (!this.connectionData.clientConnection.status.clientActiveStatus) {
final String value = (indicatorData.indicator.type.showOnlyInActiveState) final String value = (indicatorData.indicator.type.showOnlyInActiveState)
? Constants.EMPTY_NOTE ? Constants.EMPTY_NOTE
: IndicatorValue.getDisplayValue(indicatorValue); : IndicatorValue.getDisplayValue(indicatorValue);

View file

@ -10,7 +10,6 @@ package ch.ethz.seb.sebserver.gui.service.session.proctoring;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Optional;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.eclipse.rap.rwt.RWT; 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.PageService.PageActionBuilder;
import ch.ethz.seb.sebserver.gui.service.page.event.ActionActivationEvent; 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.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.GetCollectingRooms;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnection; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnection;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.IsTownhallRoomAvailable; 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 = static final String OPEN_ROOM_SCRIPT =
"try {\n" + "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" + "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" + "if(existingWin.location.href === 'about:blank'){\n" +
" existingWin.location.href = '%s%s';\n" + " existingWin.location.href = '%s%s';\n" +
" existingWin.focus();\n" + " existingWin.focus();\n" +
@ -152,6 +152,7 @@ public class MonitoringProctoringService {
} }
public void initCollectingRoomActions( public void initCollectingRoomActions(
final ServerPushContext pushContext,
final PageContext pageContext, final PageContext pageContext,
final PageActionBuilder actionBuilder, final PageActionBuilder actionBuilder,
final ProctoringServiceSettings proctoringSettings, final ProctoringServiceSettings proctoringSettings,
@ -159,6 +160,7 @@ public class MonitoringProctoringService {
proctoringGUIService.clearCollectingRoomActionState(); proctoringGUIService.clearCollectingRoomActionState();
updateCollectingRoomActions( updateCollectingRoomActions(
pushContext,
pageContext, pageContext,
actionBuilder, actionBuilder,
proctoringSettings, proctoringSettings,
@ -166,6 +168,7 @@ public class MonitoringProctoringService {
} }
public void updateCollectingRoomActions( public void updateCollectingRoomActions(
final ServerPushContext pushContext,
final PageContext pageContext, final PageContext pageContext,
final PageActionBuilder actionBuilder, final PageActionBuilder actionBuilder,
final ProctoringServiceSettings proctoringSettings, final ProctoringServiceSettings proctoringSettings,
@ -179,7 +182,7 @@ public class MonitoringProctoringService {
.getBuilder(GetCollectingRooms.class) .getBuilder(GetCollectingRooms.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call() .call()
.onError(error -> log.error("Failed to update proctoring rooms on GUI {}", error.getMessage())) .onError(error -> pushContext.reportError(error))
.getOr(Collections.emptyList()) .getOr(Collections.emptyList())
.stream() .stream()
.forEach(room -> { .forEach(room -> {
@ -198,14 +201,11 @@ public class MonitoringProctoringService {
final PageAction action = final PageAction action =
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM) actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.withExec(_action -> { .withExec(_action -> openExamProctoringRoom(
final int actualRoomSize = proctoringGUIService proctoringGUIService,
.getActualCollectingRoomSize(room.name); proctoringSettings,
if (actualRoomSize <= 0) { room,
return _action; _action))
}
return showExamProctoringRoom(proctoringSettings, room, _action);
})
.withNameAttributes( .withNameAttributes(
room.subject, room.subject,
room.roomSize, room.roomSize,
@ -226,6 +226,7 @@ public class MonitoringProctoringService {
.withParentEntityKey(entityKey); .withParentEntityKey(entityKey);
this.proctorRoomConnectionsPopup.show(pc, collectingRoom.subject); this.proctorRoomConnectionsPopup.show(pc, collectingRoom.subject);
})); }));
processProctorRoomActionActivation( processProctorRoomActionActivation(
proctoringGUIService.getCollectingRoomActionItem(room.name), proctoringGUIService.getCollectingRoomActionItem(room.name),
room, pageContext); room, pageContext);
@ -235,30 +236,16 @@ public class MonitoringProctoringService {
updateTownhallButton(proctoringGUIService, pageContext); updateTownhallButton(proctoringGUIService, pageContext);
} }
public PageAction openExamCollectionProctorScreen( private PageAction openExamProctoringRoom(
final PageAction action, final ProctoringGUIService proctoringGUIService,
final ClientConnectionData connectionData) { final ProctoringServiceSettings proctoringSettings,
final RemoteProctoringRoom room,
final PageAction action) {
try { if (!proctoringGUIService.isCollectingRoomEnabled(room.name)) {
final String examId = action.getEntityKey().modelId; return action;
}
final ProctoringServiceSettings proctoringSettings = this.pageService.getRestService()
.getBuilder(GetProctoringSettings.class)
.withURIVariable(API.PARAM_MODEL_ID, examId)
.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 final ProctoringRoomConnection proctoringConnectionData = this.pageService
.getRestService() .getRestService()
.getBuilder(GetProctorRoomConnection.class) .getBuilder(GetProctorRoomConnection.class)
@ -268,12 +255,16 @@ public class MonitoringProctoringService {
.call() .call()
.getOrThrow(); .getOrThrow();
ProctoringGUIService.setCurrentProctoringWindowData(examId, proctoringConnectionData); ProctoringGUIService.setCurrentProctoringWindowData(
String.valueOf(proctoringSettings.examId),
proctoringConnectionData);
final String script = String.format( final String script = String.format(
MonitoringProctoringService.OPEN_ROOM_SCRIPT, OPEN_ROOM_SCRIPT,
room.name, room.name,
800, 800,
1200, 1200,
room.name,
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
this.remoteProctoringEndpoint); this.remoteProctoringEndpoint);
@ -281,14 +272,17 @@ public class MonitoringProctoringService {
.getService(JavaScriptExecutor.class) .getService(JavaScriptExecutor.class)
.execute(script); .execute(script);
this.pageService.getCurrentUser() final boolean newWindow = this.pageService.getCurrentUser()
.getProctoringGUIService() .getProctoringGUIService()
.registerProctoringWindow(examId, room.name, room.name); .registerProctoringWindow(String.valueOf(room.examId), room.name, room.name);
}
} catch (final Exception e) { if (newWindow) {
log.error("Failed to open popup for collecting room: ", e); this.pageService.getRestService()
action.pageContext().notifyError(CLOSE_COLLECTING_ERROR, e); .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; return action;
@ -329,6 +323,7 @@ public class MonitoringProctoringService {
connectionToken, connectionToken,
420, 420,
640, 640,
connectionData.clientConnection.userSessionId,
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
this.remoteProctoringEndpoint); this.remoteProctoringEndpoint);
javaScriptExecutor.execute(script); javaScriptExecutor.execute(script);
@ -370,6 +365,7 @@ public class MonitoringProctoringService {
windowName, windowName,
800, 800,
1200, 1200,
"Town-Hall",
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
this.remoteProctoringEndpoint); this.remoteProctoringEndpoint);
javaScriptExecutor.execute(script); javaScriptExecutor.execute(script);
@ -444,59 +440,19 @@ public class MonitoringProctoringService {
final PageContext pageContext) { final PageContext pageContext) {
try { try {
final boolean active = room.roomSize > 0 && !room.isOpen;
final Display display = pageContext.getRoot().getDisplay(); final Display display = pageContext.getRoot().getDisplay();
final PageAction action = (PageAction) treeItem.getData(ActionPane.ACTION_EVENT_CALL_KEY); 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.getImage(display)
: action.definition.icon.getGreyedImage(display); : action.definition.icon.getGreyedImage(display);
treeItem.setImage(image); 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) { } catch (final Exception e) {
log.warn("Failed to set Proctor-Room-Activation: ", e.getMessage()); 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;
}
} }

View file

@ -78,7 +78,6 @@ public class ProctoringGUIService {
final RemoteProctoringRoom remoteProctoringRoom = getRemoteProctoringRoom(item); final RemoteProctoringRoom remoteProctoringRoom = getRemoteProctoringRoom(item);
if (remoteProctoringRoom != null && remoteProctoringRoom.roomSize > 0) { if (remoteProctoringRoom != null && remoteProctoringRoom.roomSize > 0) {
showConnectionsPopup.accept(remoteProctoringRoom); showConnectionsPopup.accept(remoteProctoringRoom);
//this.proctorRoomConnectionsPopup.show(pc, remoteProctoringRoom.subject);
} }
} }
}); });
@ -99,12 +98,13 @@ public class ProctoringGUIService {
.orElse(null); .orElse(null);
} }
public int getActualCollectingRoomSize(final String roomName) { public boolean isCollectingRoomEnabled(final String roomName) {
try { 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) { } catch (final Exception e) {
log.error("Failed to get actual collecting room size for room: {} cause: ", roomName, e.getMessage()); 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(); this.collectingRoomsActionState.clear();
} }
public void registerProctoringWindow( public boolean registerProctoringWindow(
final String examId, final String examId,
final String windowName, final String windowName,
final String roomName) { 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) { public String getTownhallWindowName(final String examId) {

View file

@ -321,7 +321,9 @@ public class TableFilter<ROW> {
final Composite innerComposite = createInnerComposite(parent); final Composite innerComposite = createInnerComposite(parent);
final GridData gridData = new GridData(SWT.FILL, SWT.END, true, true); 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); this.textInput.setLayoutData(gridData);
return this; return this;
} }

View file

@ -8,9 +8,11 @@
package ch.ethz.seb.sebserver.gui.widget; package ch.ethz.seb.sebserver.gui.widget;
import ch.ethz.seb.sebserver.gbl.Constants; import java.util.ArrayList;
import ch.ethz.seb.sebserver.gbl.util.Tuple; import java.util.Arrays;
import ch.ethz.seb.sebserver.gui.service.page.PageService; import java.util.List;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.widgets.DropDown; import org.eclipse.rap.rwt.widgets.DropDown;
import org.eclipse.swt.SWT; import org.eclipse.swt.SWT;
@ -25,10 +27,9 @@ import org.eclipse.swt.widgets.Text;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.ArrayList; import ch.ethz.seb.sebserver.gbl.Constants;
import java.util.Arrays; import ch.ethz.seb.sebserver.gbl.util.Tuple;
import java.util.List; import ch.ethz.seb.sebserver.gui.service.page.PageService;
import java.util.Optional;
public final class MultiSelectionCombo extends Composite implements Selection { public final class MultiSelectionCombo extends Composite implements Selection {
@ -68,7 +69,7 @@ public final class MultiSelectionCombo extends Composite implements Selection {
setLayout(gridLayout); setLayout(gridLayout);
this.addListener(SWT.Resize, this::adaptColumnWidth); 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.textCell = new GridData(SWT.LEFT, SWT.CENTER, true, true);
this.textInput.setLayoutData(this.textCell); this.textInput.setLayoutData(this.textCell);
this.dropDown = new DropDown(this.textInput, SWT.NONE); this.dropDown = new DropDown(this.textInput, SWT.NONE);

View file

@ -152,7 +152,8 @@ public final class ThresholdList extends Composite {
} else { } else {
Double.parseDouble(s); Double.parseDouble(s);
} }
}); },
VALUE_TEXT_KEY);
final GridData valueCell = new GridData(SWT.FILL, SWT.CENTER, true, false); final GridData valueCell = new GridData(SWT.FILL, SWT.CENTER, true, false);
valueInput.setLayoutData(valueCell); valueInput.setLayoutData(valueCell);

View file

@ -70,6 +70,10 @@ public class WidgetFactory {
private static final String ADD_HTML_ATTR_TEST_ID = "test-id"; private static final String ADD_HTML_ATTR_TEST_ID = "test-id";
private static final String SUB_TITLE_TExT_SUFFIX = ".subtitle"; private static final String SUB_TITLE_TExT_SUFFIX = ".subtitle";
public enum AriaRole {
link
}
private static final Logger log = LoggerFactory.getLogger(WidgetFactory.class); private static final Logger log = LoggerFactory.getLogger(WidgetFactory.class);
public static final int TEXT_AREA_INPUT_MIN_HEIGHT = 100; 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) { public Button buttonLocalized(final Composite parent, final String locTextKey) {
final Button button = new Button(parent, SWT.NONE); final Button button = new Button(parent, SWT.NONE);
setAttribute(button, "role", "button");
this.polyglotPageService.injectI18n(button, new LocTextKey(locTextKey)); this.polyglotPageService.injectI18n(button, new LocTextKey(locTextKey));
return button; return button;
} }
public Button buttonLocalized(final Composite parent, final LocTextKey locTextKey) { public Button buttonLocalized(final Composite parent, final LocTextKey locTextKey) {
final Button button = new Button(parent, SWT.NONE); final Button button = new Button(parent, SWT.NONE);
setAttribute(button, "role", "button");
this.polyglotPageService.injectI18n(button, locTextKey); this.polyglotPageService.injectI18n(button, locTextKey);
return button; return button;
} }
public Button buttonLocalized(final Composite parent, final CustomVariant variant, final String locTextKey) { public Button buttonLocalized(final Composite parent, final CustomVariant variant, final String locTextKey) {
final Button button = new Button(parent, SWT.NONE); final Button button = new Button(parent, SWT.NONE);
setAttribute(button, "role", "button");
this.polyglotPageService.injectI18n(button, new LocTextKey(locTextKey)); this.polyglotPageService.injectI18n(button, new LocTextKey(locTextKey));
button.setData(RWT.CUSTOM_VARIANT, variant.key); button.setData(RWT.CUSTOM_VARIANT, variant.key);
return button; return button;
@ -387,6 +394,7 @@ public class WidgetFactory {
final LocTextKey toolTipKey) { final LocTextKey toolTipKey) {
final Button button = new Button(parent, type); final Button button = new Button(parent, type);
setAttribute(button, "role", "button");
this.polyglotPageService.injectI18n(button, locTextKey, toolTipKey); this.polyglotPageService.injectI18n(button, locTextKey, toolTipKey);
return button; return button;
} }
@ -453,42 +461,85 @@ public class WidgetFactory {
return labelLocalized; return labelLocalized;
} }
public Text textInput(final Composite content) { public Text textInput(final Composite content, final LocTextKey label) {
return textInput(content, false, false); return textInput(content, false, false, this.i18nSupport.getText(label));
} }
public Text textLabel(final Composite content) { public Text textInput(final Composite content, final String label) {
return textInput(content, false, true); return textInput(content, false, false, label);
} }
public Text passwordInput(final Composite content) { public Text passwordInput(final Composite content, final LocTextKey label) {
return textInput(content, true, false); return textInput(content, true, false, this.i18nSupport.getText(label));
} }
public Text textAreaInput(final Composite content, final boolean readonly) { public Text passwordInput(final Composite content, final String label) {
return readonly 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.MULTI)
: new Text(content, SWT.LEFT | SWT.BORDER | 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) { public Text textInput(
return readonly 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, SWT.LEFT)
: new Text(content, (password) : new Text(content, (password)
? SWT.LEFT | SWT.BORDER | SWT.PASSWORD ? SWT.LEFT | SWT.BORDER | SWT.PASSWORD
: SWT.LEFT | SWT.BORDER); : SWT.LEFT | SWT.BORDER);
if (label != null) {
WidgetFactory.setAttribute(input, "aria-label", label);
}
return input;
} }
public Text numberInput(final Composite content, final Consumer<String> numberCheck) { public Text numberInput(final Composite content, final Consumer<String> numberCheck, final LocTextKey label) {
return numberInput(content, numberCheck, false); return numberInput(content, numberCheck, false, label);
} }
public Text numberInput(final Composite content, final Consumer<String> numberCheck, final boolean readonly) { public Text numberInput(
if (readonly) { final Composite content,
return new Text(content, SWT.LEFT | SWT.READ_ONLY); final Consumer<String> numberCheck,
} final boolean readonly,
final LocTextKey label) {
final Text numberInput = new Text(content, SWT.RIGHT | SWT.BORDER); 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) { if (numberCheck != null) {
numberInput.addListener(SWT.Verify, event -> { numberInput.addListener(SWT.Verify, event -> {
final String value = event.text; final String value = event.text;
@ -884,11 +935,11 @@ public class WidgetFactory {
setAttribute(widget, ADD_HTML_ATTR_TEST_ID, value); setAttribute(widget, ADD_HTML_ATTR_TEST_ID, value);
} }
public static void setARIARole(final Widget widget, final String value) { public static void setARIARole(final Widget widget, final AriaRole role) {
setAttribute(widget, ADD_HTML_ATTR_ARIA_ROLE, value); 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()) { if (!widget.isDisposed()) {
final String $el = widget instanceof Text ? "$input" : "$el"; final String $el = widget instanceof Text ? "$input" : "$el";
final String id = WidgetUtil.getId(widget); final String id = WidgetUtil.getId(widget);

View file

@ -60,7 +60,5 @@ public class CacheConfig extends JCacheConfigurerSupport {
log.error("Failed to initialize caching with EHCache. Fallback to simple caching"); log.error("Failed to initialize caching with EHCache. Fallback to simple caching");
return new ConcurrentMapCacheManager(); return new ConcurrentMapCacheManager();
} }
} }
} }

View file

@ -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);
}
}
}

View file

@ -10,13 +10,9 @@ package ch.ethz.seb.sebserver.webservice;
import org.cryptonode.jncryptor.AES256JNCryptor; import org.cryptonode.jncryptor.AES256JNCryptor;
import org.cryptonode.jncryptor.JNCryptor; 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.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy; 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.Constants;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@ -25,9 +21,6 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@WebServiceProfile @WebServiceProfile
public class WebserviceConfig { public class WebserviceConfig {
@Value("${sebserver.webservice.clean-db-on-startup:false}")
boolean cleanDBOnStartup;
@Lazy @Lazy
@Bean @Bean
public JNCryptor jnCryptor() { public JNCryptor jnCryptor() {
@ -36,24 +29,4 @@ public class WebserviceConfig {
return aes256jnCryptor; 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;
}
} }

View file

@ -86,6 +86,10 @@ public class WebserviceInit implements ApplicationListener<ApplicationReadyEvent
SEBServerInit.INIT_LOGGER.info("---->"); SEBServerInit.INIT_LOGGER.info("---->");
SEBServerInit.INIT_LOGGER.info("----> *** Info:"); SEBServerInit.INIT_LOGGER.info("----> *** Info:");
if (this.webserviceInfo.isDistributed()) {
SEBServerInit.INIT_LOGGER.info("----> Distributed Setup: {}", this.webserviceInfo.getWebserviceUUID());
}
try { try {
SEBServerInit.INIT_LOGGER.info("----> Server address: {}", this.environment.getProperty("server.address")); SEBServerInit.INIT_LOGGER.info("----> Server address: {}", this.environment.getProperty("server.address"));
SEBServerInit.INIT_LOGGER.info("----> Server port: {}", this.environment.getProperty("server.port")); 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("---->");
SEBServerInit.INIT_LOGGER.info("----> Unregister Webservice: {}", this.webserviceInfo.getWebserviceUUID()); SEBServerInit.INIT_LOGGER.info("----> Unregister Webservice: {}", this.webserviceInfo.getWebserviceUUID());
this.webserviceInfoDAO.unregister(this.webserviceInfo.getWebserviceUUID()); this.webserviceInfoDAO.unregister(this.webserviceInfo.getWebserviceUUID());
SEBServerInit.INIT_LOGGER.info("---->"); SEBServerInit.INIT_LOGGER.info("---->");

View file

@ -69,7 +69,8 @@ public interface AdditionalAttributesDAO {
/** Use this to delete all additional attributes for a given entity. /** Use this to delete all additional attributes for a given entity.
* *
* @param type the entity type
* @param entityId the entity identifier (primary-key) */ * @param entityId the entity identifier (primary-key) */
void deleteAll(Long entityId); void deleteAll(EntityType type, Long entityId);
} }

View file

@ -121,9 +121,17 @@ public interface RemoteProctoringRoomDAO {
/** Get currently active break-out rooms for given connectionToken /** 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 * @param connectionTokens The connection token of the client connection
* @return Result refer to active break-out rooms or to an error when happened */ * @return Result refer to active break-out rooms or to an error when happened */
Result<Collection<RemoteProctoringRoom>> getBreakoutRooms(String connectionToken); 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);
} }

View file

@ -14,6 +14,8 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
* the persistent data table. */ * the persistent data table. */
public interface WebserviceInfoDAO { public interface WebserviceInfoDAO {
boolean isInitialized();
/** Register a SEB webservice within the persistent storage /** Register a SEB webservice within the persistent storage
* *
* @param uuid A unique identifier that was generated by the webservice on startup * @param uuid A unique identifier that was generated by the webservice on startup

View file

@ -12,6 +12,8 @@ import java.util.Collection;
import java.util.Optional; import java.util.Optional;
import org.mybatis.dynamic.sql.SqlBuilder; import org.mybatis.dynamic.sql.SqlBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -29,6 +31,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO
@WebServiceProfile @WebServiceProfile
public class AdditionalAttributesDAOImpl implements AdditionalAttributesDAO { public class AdditionalAttributesDAOImpl implements AdditionalAttributesDAO {
private static final Logger log = LoggerFactory.getLogger(AdditionalAttributesDAOImpl.class);
private final AdditionalAttributeRecordMapper additionalAttributeRecordMapper; private final AdditionalAttributeRecordMapper additionalAttributeRecordMapper;
protected AdditionalAttributesDAOImpl(final AdditionalAttributeRecordMapper additionalAttributeRecordMapper) { protected AdditionalAttributesDAOImpl(final AdditionalAttributeRecordMapper additionalAttributeRecordMapper) {
@ -164,14 +168,21 @@ public class AdditionalAttributesDAOImpl implements AdditionalAttributesDAO {
@Override @Override
@Transactional @Transactional
public void deleteAll(final Long entityId) { public void deleteAll(final EntityType type, final Long entityId) {
try {
this.additionalAttributeRecordMapper this.additionalAttributeRecordMapper
.deleteByExample() .deleteByExample()
.where( .where(
AdditionalAttributeRecordDynamicSqlSupport.entityType,
SqlBuilder.isEqualTo(type.name()))
.and(
AdditionalAttributeRecordDynamicSqlSupport.entityId, AdditionalAttributeRecordDynamicSqlSupport.entityId,
SqlBuilder.isEqualTo(entityId)) SqlBuilder.isEqualTo(entityId))
.build() .build()
.execute(); .execute();
} catch (final Exception e) {
log.warn("Failed to delete all additional attributes for: {} cause: {}", entityId, e.getMessage());
}
} }
} }

View file

@ -216,6 +216,7 @@ public class CertificateDAOImpl implements CertificateDAO {
final boolean[] keyUsage = cert.getKeyUsage(); final boolean[] keyUsage = cert.getKeyUsage();
final EnumSet<CertificateType> result = EnumSet.noneOf(CertificateType.class); final EnumSet<CertificateType> result = EnumSet.noneOf(CertificateType.class);
if (keyUsage != null) {
// digitalSignature // digitalSignature
if (keyUsage[0]) { if (keyUsage[0]) {
result.add(CertificateType.DIGITAL_SIGNATURE); result.add(CertificateType.DIGITAL_SIGNATURE);
@ -230,9 +231,6 @@ public class CertificateDAOImpl implements CertificateDAO {
if (keyUsage[5]) { if (keyUsage[5]) {
result.add(CertificateType.KEY_CERT_SIGN); result.add(CertificateType.KEY_CERT_SIGN);
} }
if (result.isEmpty()) {
result.add(CertificateType.UNKNOWN);
} }
final String alias = certificates.keyStore.engineGetCertificateAlias(cert); final String alias = certificates.keyStore.engineGetCertificateAlias(cert);
@ -240,6 +238,10 @@ public class CertificateDAOImpl implements CertificateDAO {
result.add(CertificateType.DATA_ENCIPHERMENT_PRIVATE_KEY); result.add(CertificateType.DATA_ENCIPHERMENT_PRIVATE_KEY);
} }
if (result.isEmpty()) {
result.add(CertificateType.UNKNOWN);
}
return result; return result;
} }

View file

@ -635,7 +635,8 @@ public class ExamDAOImpl implements ExamDAO {
.execute(); .execute();
// delete all additional attributes // delete all additional attributes
this.additionalAttributeRecordMapper.deleteByExample() this.additionalAttributeRecordMapper
.deleteByExample()
.where(AdditionalAttributeRecordDynamicSqlSupport.entityType, isEqualTo(EntityType.EXAM.name())) .where(AdditionalAttributeRecordDynamicSqlSupport.entityType, isEqualTo(EntityType.EXAM.name()))
.and(AdditionalAttributeRecordDynamicSqlSupport.entityId, isIn(ids)) .and(AdditionalAttributeRecordDynamicSqlSupport.entityId, isIn(ids))
.build() .build()

View file

@ -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.RemoteProctoringRoomRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.RemoteProctoringRoomRecordMapper; 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.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.RemoteProctoringRoomDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.NewRoom; 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 static final Object RESERVE_ROOM_LOCK = new Object();
private final RemoteProctoringRoomRecordMapper remoteProctoringRoomRecordMapper; private final RemoteProctoringRoomRecordMapper remoteProctoringRoomRecordMapper;
private final AdditionalAttributesDAO additionalAttributesDAO;
protected RemoteProctoringRoomDAOImpl( protected RemoteProctoringRoomDAOImpl(
final RemoteProctoringRoomRecordMapper remoteProctoringRoomRecordMapper) { final RemoteProctoringRoomRecordMapper remoteProctoringRoomRecordMapper,
final AdditionalAttributesDAO additionalAttributesDAO) {
this.remoteProctoringRoomRecordMapper = remoteProctoringRoomRecordMapper; this.remoteProctoringRoomRecordMapper = remoteProctoringRoomRecordMapper;
this.additionalAttributesDAO = additionalAttributesDAO;
} }
@Override @Override
@ -227,6 +231,10 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
@Transactional @Transactional
public Result<EntityKey> deleteRoom(final Long roomId) { public Result<EntityKey> deleteRoom(final Long roomId) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
this.additionalAttributesDAO
.deleteAll(EntityType.REMOTE_PROCTORING_ROOM, roomId);
this.remoteProctoringRoomRecordMapper this.remoteProctoringRoomRecordMapper
.deleteByPrimaryKey(roomId); .deleteByPrimaryKey(roomId);
@ -249,7 +257,15 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
.execute(); .execute();
return ids.stream() 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()); .collect(Collectors.toList());
}); });
@ -322,6 +338,37 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
.collect(Collectors.toList())); .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) { private RemoteProctoringRoom toDomainModel(final RemoteProctoringRoomRecord record) {
final String breakOutConnections = record.getBreakOutConnections(); final String breakOutConnections = record.getBreakOutConnections();
final Collection<String> connections = StringUtils.isNotBlank(breakOutConnections) final Collection<String> connections = StringUtils.isNotBlank(breakOutConnections)
@ -337,7 +384,22 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
BooleanUtils.toBooleanObject(record.getTownhallRoom()), BooleanUtils.toBooleanObject(record.getTownhallRoom()),
connections, connections,
record.getJoinKey(), 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( private RemoteProctoringRoomRecord createNewCollectingRoom(

View file

@ -47,6 +47,20 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO {
this.forceMaster = forceMaster; this.forceMaster = forceMaster;
} }
@Override
@Transactional
public boolean isInitialized() {
try {
this.webserviceServerInfoRecordMapper
.selectByExample()
.build()
.execute();
return true;
} catch (final Exception e) {
return false;
}
}
@Transactional @Transactional
@Override @Override
public boolean register(final String uuid, final String address) { public boolean register(final String uuid, final String address) {
@ -173,7 +187,7 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO {
.execute(); .execute();
return true; return true;
} catch (final Exception e) { } 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; return false;
} }
} }

View file

@ -328,7 +328,8 @@ public class ExamAdminServiceImpl implements ExamAdminService {
if (mapping.containsKey(ProctoringServiceSettings.ATTR_ENABLED_FEATURES)) { if (mapping.containsKey(ProctoringServiceSettings.ATTR_ENABLED_FEATURES)) {
try { try {
final String value = mapping.get(ProctoringServiceSettings.ATTR_ENABLED_FEATURES).getValue(); final String value = mapping.get(ProctoringServiceSettings.ATTR_ENABLED_FEATURES).getValue();
return EnumSet.copyOf(Arrays.asList(StringUtils.split(value, Constants.LIST_SEPARATOR)) return StringUtils.isNotBlank(value)
? EnumSet.copyOf(Arrays.asList(StringUtils.split(value, Constants.LIST_SEPARATOR))
.stream() .stream()
.map(str -> { .map(str -> {
try { try {
@ -341,7 +342,8 @@ public class ExamAdminServiceImpl implements ExamAdminService {
} }
}) })
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toSet())); .collect(Collectors.toSet()))
: EnumSet.noneOf(ProctoringFeature.class);
} catch (final Exception e) { } catch (final Exception e) {
log.error("Failed to get enabled features for proctoring settings. Enable all. {}", e.getMessage()); log.error("Failed to get enabled features for proctoring settings. Enable all. {}", e.getMessage());
return EnumSet.allOf(ProctoringFeature.class); return EnumSet.allOf(ProctoringFeature.class);

View file

@ -33,13 +33,20 @@ public interface ExamProctoringRoomService {
* @return Result refer to the resulting collection of ClientConnection or to an error when happened */ * @return Result refer to the resulting collection of ClientConnection or to an error when happened */
Result<Collection<ClientConnection>> getRoomConnections(Long roomId); 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 examId The exam identifier of the room
* @param roomName The room name * @param roomName The room name
* @return Result refer to the resulting collection of ClientConnection or to an error when happened */ * @return Result refer to the resulting collection of ClientConnection or to an error when happened */
Result<Collection<ClientConnection>> getCollectingRoomConnections(Long examId, String roomName); 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 /** This is internally used to update client connections that are flagged for updating
* the proctoring room assignment. * the proctoring room assignment.
* This attaches or detaches client connections from or to proctoring rooms of an exam in one batch. * This attaches or detaches client connections from or to proctoring rooms of an exam in one batch.

View file

@ -14,14 +14,12 @@ import java.util.function.Predicate;
import org.springframework.cache.CacheManager; 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.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; 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.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.util.Result; 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.ClientConnectionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
@ -185,19 +183,7 @@ public interface ExamSessionService {
* @param connection ClientConnectionData instance * @param connection ClientConnectionData instance
* @return true if the given ClientConnectionData is an active SEB client connection */ * @return true if the given ClientConnectionData is an active SEB client connection */
static boolean isActiveConnection(final ClientConnectionData connection) { static boolean isActiveConnection(final ClientConnectionData connection) {
if (connection.clientConnection.status.establishedStatus) { return connection.clientConnection.status.clientActiveStatus;
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;
} }
} }

View file

@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -18,6 +19,7 @@ import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -54,19 +56,22 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
private final ExamAdminService examAdminService; private final ExamAdminService examAdminService;
private final ExamSessionService examSessionService; private final ExamSessionService examSessionService;
private final SEBClientInstructionService sebInstructionService; private final SEBClientInstructionService sebInstructionService;
private final boolean sendBroadcastReset;
public ExamProctoringRoomServiceImpl( public ExamProctoringRoomServiceImpl(
final RemoteProctoringRoomDAO remoteProctoringRoomDAO, final RemoteProctoringRoomDAO remoteProctoringRoomDAO,
final ClientConnectionDAO clientConnectionDAO, final ClientConnectionDAO clientConnectionDAO,
final ExamAdminService examAdminService, final ExamAdminService examAdminService,
final ExamSessionService examSessionService, final ExamSessionService examSessionService,
final SEBClientInstructionService sebInstructionService) { final SEBClientInstructionService sebInstructionService,
@Value("${sebserver.webservice.proctoring.resetBroadcastOnLeav:true}") final boolean sendBroadcastReset) {
this.remoteProctoringRoomDAO = remoteProctoringRoomDAO; this.remoteProctoringRoomDAO = remoteProctoringRoomDAO;
this.clientConnectionDAO = clientConnectionDAO; this.clientConnectionDAO = clientConnectionDAO;
this.examAdminService = examAdminService; this.examAdminService = examAdminService;
this.examSessionService = examSessionService; this.examSessionService = examSessionService;
this.sebInstructionService = sebInstructionService; this.sebInstructionService = sebInstructionService;
this.sendBroadcastReset = sendBroadcastReset;
} }
@Override @Override
@ -80,8 +85,34 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
} }
@Override @Override
public Result<Collection<ClientConnection>> getCollectingRoomConnections(final Long examId, final String roomName) { public Result<Collection<ClientConnection>> getCollectingRoomConnections(
return this.clientConnectionDAO.getCollectingRoomConnections(examId, roomName); 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 @Override
@ -230,7 +261,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
} else if (remoteProctoringRoom.townhallRoom) { } else if (remoteProctoringRoom.townhallRoom) {
closeTownhall(examId, settings, examProctoringService); closeTownhall(examId, settings, examProctoringService);
} else { } else {
closeCollectingRoom(examId, roomName, examProctoringService); closeCollectingRoom(examId, roomName, settings, examProctoringService);
} }
}); });
} }
@ -356,13 +387,16 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
.notifyBreakOutRoomOpened(proctoringSettings, room) .notifyBreakOutRoomOpened(proctoringSettings, room)
.getOrThrow(); .getOrThrow();
} else { } else {
final Collection<ClientConnection> clientConnections = this.clientConnectionDAO final Collection<ClientConnection> clientConnections = this
.getCollectingRoomConnections(examId, roomName) .getActiveCollectingRoomConnections(examId, roomName)
.getOrThrow(); .getOrThrow();
examProctoringService examProctoringService
.notifyCollectingRoomOpened(proctoringSettings, room, clientConnections) .notifyCollectingRoomOpened(proctoringSettings, room, clientConnections)
.getOrThrow(); .getOrThrow();
this.remoteProctoringRoomDAO
.setCollectingRoomOpenFlag(room.id, true);
} }
}); });
} }
@ -377,11 +411,13 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
.getActiveConnectionTokens(examId) .getActiveConnectionTokens(examId)
.getOrThrow(); .getOrThrow();
// Send default settings to clients // Send default settings to clients if fearture is enabled
if (this.sendBroadcastReset) {
this.sendReconfigurationInstructions( this.sendReconfigurationInstructions(
examId, examId,
connectionTokens, connectionTokens,
examProctoringService.getDefaultReconfigInstructionAttributes()); examProctoringService.getDefaultReconfigInstructionAttributes());
}
// Close and delete town-hall room // Close and delete town-hall room
this.remoteProctoringRoomDAO this.remoteProctoringRoomDAO
@ -403,21 +439,37 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
private void closeCollectingRoom( private void closeCollectingRoom(
final Long examId, final Long examId,
final String roomName, final String roomName,
final ProctoringServiceSettings proctoringSettings,
final ExamProctoringService examProctoringService) { final ExamProctoringService examProctoringService) {
// get all connections of the room // get all connections of the room
final List<String> connectionTokens = this.getCollectingRoomConnections(examId, roomName) final List<String> connectionTokens = this.getActiveCollectingRoomConnections(examId, roomName)
.getOrThrow() .getOrThrow()
.stream() .stream()
.map(cc -> cc.connectionToken) .map(cc -> cc.connectionToken)
.collect(Collectors.toList()); .collect(Collectors.toList());
// Send default settings to clients 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( this.sendReconfigurationInstructions(
examId, examId,
connectionTokens, connectionTokens,
examProctoringService.getDefaultReconfigInstructionAttributes()); examProctoringService.getDefaultReconfigInstructionAttributes());
} }
}
private void cleanupBreakOutRooms(final ClientConnectionRecord cc) { private void cleanupBreakOutRooms(final ClientConnectionRecord cc) {
@ -453,11 +505,13 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
final ExamProctoringService examProctoringService, final ExamProctoringService examProctoringService,
final RemoteProctoringRoom remoteProctoringRoom) { final RemoteProctoringRoom remoteProctoringRoom) {
// Send default settings to clients // Send default settings to clients if feature is enabled
if (this.sendBroadcastReset) {
this.sendReconfigurationInstructions( this.sendReconfigurationInstructions(
examId, examId,
remoteProctoringRoom.breakOutConnections, remoteProctoringRoom.breakOutConnections,
examProctoringService.getDefaultReconfigInstructionAttributes()); examProctoringService.getDefaultReconfigInstructionAttributes());
}
// Dispose the proctoring room on service side // Dispose the proctoring room on service side
examProctoringService examProctoringService
@ -509,7 +563,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
} else if (!room.breakOutConnections.isEmpty()) { } else if (!room.breakOutConnections.isEmpty()) {
connectionTokens.addAll(room.breakOutConnections); connectionTokens.addAll(room.breakOutConnections);
} else { } else {
connectionTokens.addAll(this.getCollectingRoomConnections(examId, roomName) connectionTokens.addAll(this.getActiveCollectingRoomConnections(examId, roomName)
.getOrThrow() .getOrThrow()
.stream() .stream()
.map(cc -> cc.connectionToken) .map(cc -> cc.connectionToken)
@ -630,16 +684,17 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
final ClientConnectionData clientConnection = this.examSessionService final ClientConnectionData clientConnection = this.examSessionService
.getConnectionData(connectionToken) .getConnectionData(connectionToken)
.getOrThrow(); .getOrThrow();
final String roomName = this.remoteProctoringRoomDAO
.getRoomName(clientConnection.clientConnection.getRemoteProctoringRoomId()) final RemoteProctoringRoom remoteProctoringRoom = this.remoteProctoringRoomDAO
.getRoom(clientConnection.clientConnection.getRemoteProctoringRoomId())
.getOrThrow(); .getOrThrow();
final ProctoringRoomConnection proctoringConnection = examProctoringService final ProctoringRoomConnection proctoringConnection = examProctoringService
.getClientRoomConnection( .getClientRoomConnection(
proctoringSettings, proctoringSettings,
clientConnection.clientConnection.connectionToken, clientConnection.clientConnection.connectionToken,
roomName, remoteProctoringRoom.name,
clientConnection.clientConnection.userSessionId) remoteProctoringRoom.subject)
.getOrThrow(); .getOrThrow();
sendJoinInstruction( sendJoinInstruction(

View file

@ -29,6 +29,7 @@ import org.joda.time.DateTimeZone;
import org.joda.time.Interval; import org.joda.time.Interval;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@ -125,6 +126,8 @@ public class ZoomProctoringService implements ExamProctoringService {
private final RemoteProctoringRoomDAO remoteProctoringRoomDAO; private final RemoteProctoringRoomDAO remoteProctoringRoomDAO;
private final AuthorizationService authorizationService; private final AuthorizationService authorizationService;
private final SEBClientInstructionService sebInstructionService; private final SEBClientInstructionService sebInstructionService;
private final boolean enableWaitingRoom;
private final boolean sendRejoinForCollectingRoom;
public ZoomProctoringService( public ZoomProctoringService(
final ExamSessionService examSessionService, final ExamSessionService examSessionService,
@ -134,7 +137,9 @@ public class ZoomProctoringService implements ExamProctoringService {
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final RemoteProctoringRoomDAO remoteProctoringRoomDAO, final RemoteProctoringRoomDAO remoteProctoringRoomDAO,
final AuthorizationService authorizationService, 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.examSessionService = examSessionService;
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
@ -145,6 +150,8 @@ public class ZoomProctoringService implements ExamProctoringService {
this.remoteProctoringRoomDAO = remoteProctoringRoomDAO; this.remoteProctoringRoomDAO = remoteProctoringRoomDAO;
this.authorizationService = authorizationService; this.authorizationService = authorizationService;
this.sebInstructionService = sebInstructionService; this.sebInstructionService = sebInstructionService;
this.enableWaitingRoom = enableWaitingRoom;
this.sendRejoinForCollectingRoom = sendRejoinForCollectingRoom;
} }
@Override @Override
@ -443,23 +450,33 @@ public class ZoomProctoringService implements ExamProctoringService {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
if (!this.sendRejoinForCollectingRoom) {
// does nothing if the rejoin feature is not enabled
return;
}
if (this.remoteProctoringRoomDAO.isTownhallRoomActive(proctoringSettings.examId)) { 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 // the meeting once the town-hall has been closed
return; return;
} }
final ProctoringRoomConnection proctoringRoomConnection = this.getProctorRoomConnection(
proctoringSettings,
room.name,
room.subject)
.getOrThrow();
clientConnections.stream() clientConnections.stream()
.forEach(cc -> sendJoinInstruction( .forEach(cc -> {
try {
sendJoinInstruction(
proctoringSettings.examId, proctoringSettings.examId,
cc.connectionToken, cc.connectionToken,
proctoringRoomConnection)); 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, proctoringSettings.serverURL,
credentials, credentials,
roomName); roomName);
final UserResponse userResponse = this.jsonMapper.readValue( final UserResponse userResponse = this.jsonMapper.readValue(
createUser.getBody(), createUser.getBody(),
UserResponse.class); UserResponse.class);
@ -509,7 +527,9 @@ public class ZoomProctoringService implements ExamProctoringService {
userResponse.id, userResponse.id,
subject, subject,
duration, duration,
meetingPwd); meetingPwd,
this.enableWaitingRoom);
final MeetingResponse meetingResponse = this.jsonMapper.readValue( final MeetingResponse meetingResponse = this.jsonMapper.readValue(
createMeeting.getBody(), createMeeting.getBody(),
MeetingResponse.class); MeetingResponse.class);
@ -520,6 +540,7 @@ public class ZoomProctoringService implements ExamProctoringService {
userResponse.id, userResponse.id,
meetingResponse.start_url, meetingResponse.start_url,
meetingResponse.join_url); meetingResponse.join_url);
final String additionalZoomRoomDataString = this.jsonMapper final String additionalZoomRoomDataString = this.jsonMapper
.writeValueAsString(additionalZoomRoomData); .writeValueAsString(additionalZoomRoomData);
@ -558,23 +579,29 @@ public class ZoomProctoringService implements ExamProctoringService {
final StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding(); final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
final String jwtHeaderPart = urlEncoder.encodeToString( final String jwtHeaderPart = urlEncoder
ZOOM_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8)); .encodeToString(ZOOM_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8));
final String jwtPayload = String.format( final String jwtPayload = String.format(
ZOOM_API_ACCESS_TOKEN_PAYLOAD.replaceAll(" ", "").replaceAll("\n", ""), ZOOM_API_ACCESS_TOKEN_PAYLOAD
.replaceAll(" ", "")
.replaceAll("\n", ""),
credentials.clientIdAsString(), credentials.clientIdAsString(),
expTime); 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 String message = jwtHeaderPart + "." + jwtPayloadPart;
final Mac sha256_HMAC = Mac.getInstance(TOKEN_ENCODE_ALG); final Mac sha256_HMAC = Mac.getInstance(TOKEN_ENCODE_ALG);
final SecretKeySpec secret_key = new SecretKeySpec( final SecretKeySpec secret_key = new SecretKeySpec(
Utils.toByteArray(decryptedSecret), Utils.toByteArray(decryptedSecret),
TOKEN_ENCODE_ALG); TOKEN_ENCODE_ALG);
sha256_HMAC.init(secret_key); sha256_HMAC.init(secret_key);
final String hash = urlEncoder.encodeToString( final String hash = urlEncoder
sha256_HMAC.doFinal(Utils.toByteArray(message))); .encodeToString(sha256_HMAC.doFinal(Utils.toByteArray(message)));
builder.append(message) builder.append(message)
.append(".") .append(".")
@ -705,7 +732,8 @@ public class ZoomProctoringService implements ExamProctoringService {
final String userId, final String userId,
final String topic, final String topic,
final int duration, final int duration,
final CharSequence password) { final CharSequence password,
final boolean waitingRoom) {
try { try {
@ -718,7 +746,8 @@ public class ZoomProctoringService implements ExamProctoringService {
final CreateMeetingRequest createRoomRequest = new CreateMeetingRequest( final CreateMeetingRequest createRoomRequest = new CreateMeetingRequest(
topic, topic,
duration, duration,
password); password,
waitingRoom);
final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(createRoomRequest); final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(createRoomRequest);
final HttpHeaders headers = getHeaders(credentials); final HttpHeaders headers = getHeaders(credentials);

View file

@ -123,7 +123,8 @@ public interface ZoomRoomRequestResponse {
public CreateMeetingRequest( public CreateMeetingRequest(
final String topic, final String topic,
final int duration, final int duration,
final CharSequence password) { final CharSequence password,
final boolean waitingRoom) {
this.type = 2; // Scheduled Meeting this.type = 2; // Scheduled Meeting
this.start_time = DateTime.now(DateTimeZone.UTC).toString("yyyy-MM-dd'T'HH:mm:ss"); 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.timezone = DateTimeZone.UTC.getID();
this.topic = topic; this.topic = topic;
this.password = password; this.password = password;
this.settings = new Settings(); this.settings = new Settings(waitingRoom);
} }
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
static class Settings { static class Settings {
@JsonProperty final boolean host_video = true; @JsonProperty final boolean host_video = false;
@JsonProperty final boolean participant_video = true; @JsonProperty final boolean mute_upon_entry = false;
@JsonProperty final boolean join_before_host = true; @JsonProperty final boolean join_before_host;
@JsonProperty final int jbh_time = 0; @JsonProperty final int jbh_time = 0;
@JsonProperty final boolean use_pmi = false; @JsonProperty final boolean use_pmi = false;
@JsonProperty final String audio = "voip"; @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;
}
} }
} }

View file

@ -269,6 +269,10 @@ public class ExamAPI_V1_Controller {
final String pingNumString = request.getParameter(API.EXAM_API_PING_NUMBER); final String pingNumString = request.getParameter(API.EXAM_API_PING_NUMBER);
final String instructionConfirm = request.getParameter(API.EXAM_API_PING_INSTRUCTION_CONFIRM); final String instructionConfirm = request.getParameter(API.EXAM_API_PING_INSTRUCTION_CONFIRM);
if (log.isTraceEnabled()) {
log.trace("****************** SEB client connection: {}", connectionToken);
}
if (instructionConfirm != null) { if (instructionConfirm != null) {
this.sebClientConnectionService.confirmInstructionDone(connectionToken, instructionConfirm); this.sebClientConnectionService.confirmInstructionDone(connectionToken, instructionConfirm);
} }

View file

@ -11,6 +11,7 @@ logging.level.ch=INFO
logging.level.org.springframework.cache=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.lms.impl=DEBUG
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=DEBUG logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=DEBUG
logging.level.ch.ethz.seb.sebserver.webservice.weblayer.api.ExamAPI_V1_Controller=TRACE
sebserver.http.client.connect-timeout=150000 sebserver.http.client.connect-timeout=150000
sebserver.http.client.connection-request-timeout=100000 sebserver.http.client.connection-request-timeout=100000

View file

@ -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.moodle.api.token.request.paths=/login/token.php
sebserver.webservice.lms.address.alias= sebserver.webservice.lms.address.alias=
sebserver.webservice.proctoring.resetBroadcastOnLeav=true
sebserver.webservice.proctoring.zoom.enableWaitingRoom=false
sebserver.webservice.proctoring.zoom.sendRejoinForCollectingRoom=true

View file

@ -10,7 +10,9 @@ INSERT IGNORE INTO configuration_attribute VALUES
(960, 'allowFind', 'CHECKBOX', null, null, null, null, 'true'), (960, 'allowFind', 'CHECKBOX', null, null, null, null, 'true'),
(961, 'allowPDFReaderToolbar', 'CHECKBOX', null, null, null, null, 'false'), (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')
; ;
-- ----------------------------------------------------- -- -----------------------------------------------------
@ -67,6 +69,11 @@ INSERT IGNORE INTO orientation (config_attribute_id, template_id, view_id, group
INSERT IGNORE INTO orientation (config_attribute_id, template_id, view_id, group_id, x_position, y_position, width, height, title) VALUES 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'); (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 -- 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=11, width=5 WHERE config_attribute_id=57;
UPDATE orientation SET x_position=7, y_position=13, width=5 WHERE config_attribute_id=58; 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=8 WHERE config_attribute_id=406;
UPDATE orientation SET y_position=9 WHERE config_attribute_id=407; UPDATE orientation SET y_position=9 WHERE config_attribute_id=407;
UPDATE orientation SET y_position=10 WHERE config_attribute_id=408; 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;

View file

@ -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.ONE_TO_ONE=One to One Room
sebserver.exam.proctoring.form.features.BROADCAST=Broadcasting Feature sebserver.exam.proctoring.form.features.BROADCAST=Broadcasting Feature
sebserver.exam.proctoring.form.features.ENABLE_CHAT=Chat 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=Jitsi Meet Server
sebserver.exam.proctoring.type.servertype.JITSI_MEET.tooltip=Use a Jitsi Meet server for proctoring 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.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.allowApplicationLog=Allow access to application log (Win)
sebserver.examconfig.props.label.showApplicationLogButton=Show log button on taskbar (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=While running SEB
sebserver.examconfig.props.group.registry.tooltip=Options in the Windows Security Screen invoked by Ctrl-Alt-Del 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.showTownhall=Show Townhall
sebserver.monitoring.exam.action.proctoring.closeTownhall=Close 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.close=Close Window
sebserver.monitoring.exam.proctoring.action.broadcaston.audio=Start Audio Broadcast sebserver.monitoring.exam.proctoring.action.broadcaston.audio=Start Audio Broadcast
sebserver.monitoring.exam.proctoring.action.broadcastoff.audio=End 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.empty=No Client Connections available
sebserver.monitoring.search.list.name=Session or User Name sebserver.monitoring.search.list.name=Session or User Name
sebserver.monitoring.search.list.ip=IP Address 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 sebserver.monitoring.exam.connection.emptySelection=At first please select a Connection from the list

View file

@ -21,7 +21,7 @@ import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService
public class ZoomWindowScriptResolverTest { public class ZoomWindowScriptResolverTest {
@Test @Test
public void testJitsiWindowScriptResolver() { public void testZoomWindowScriptResolver() {
final DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader(); final DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader();
final Resource resource = defaultResourceLoader.getResource(ZoomWindowScriptResolver.RES_PATH); final Resource resource = defaultResourceLoader.getResource(ZoomWindowScriptResolver.RES_PATH);
final ZoomWindowScriptResolver zoomWindowScriptResolver = new ZoomWindowScriptResolver(resource); final ZoomWindowScriptResolver zoomWindowScriptResolver = new ZoomWindowScriptResolver(resource);