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,
ONE_TO_ONE,
BROADCAST,
ENABLE_CHAT
ENABLE_CHAT,
}
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 */
MOODLE(Features.COURSE_API /* , Features.SEB_RESTRICTION */),
/** The Ans Delft binding is on the way */
ANS_DELFT(Features.COURSE_API, Features.SEB_RESTRICTION),
ANS_DELFT(),
/** The OpenOLAT binding is on the way */
OPEN_OLAT(Features.COURSE_API, Features.SEB_RESTRICTION);
OPEN_OLAT();
public final EnumSet<Features> features;

View file

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

View file

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

View file

@ -73,6 +73,12 @@ public class RAPConfiguration implements ApplicationConfiguration {
properties.put(WebClient.FAVICON, "fav_icon");
application.addEntryPoint(guiEntrypoint, new RAPSpringEntryPointFactory(), properties);
properties.put(WebClient.PAGE_TITLE, "SEB Server Proctoring");
properties.put(WebClient.BODY_HTML, "<big>Loading Application<big>");
properties.put(WebClient.THEME_ID, DEFAULT_THEME_NAME);
properties.put(WebClient.FAVICON, "fav_icon");
application.addEntryPoint(proctoringEntrypoint, new RAPRemoteProcotringEntryPointFactory(), properties);
} catch (final RuntimeException re) {

View file

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

View file

@ -12,10 +12,12 @@ import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.SWT;
import org.eclipse.swt.internal.widgets.IControlAdapter;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Text;
@ -27,6 +29,8 @@ import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.ComposerService;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
@ -43,6 +47,11 @@ public class LoginPage implements TemplateComposer {
private static final Logger log = LoggerFactory.getLogger(LoginPage.class);
private static final LocTextKey TEXT_REGISTER = new LocTextKey("sebserver.login.register");
private static final LocTextKey TEXT_LOGIN = new LocTextKey("sebserver.login.login");
private static final LocTextKey TEXT_PWD = new LocTextKey("sebserver.login.pwd");
private static final LocTextKey TEXT_USERNAME = new LocTextKey("sebserver.login.username");
private final PageService pageService;
private final AuthorizationContextHolder authorizationContextHolder;
private final WidgetFactory widgetFactory;
@ -67,7 +76,6 @@ public class LoginPage implements TemplateComposer {
public void compose(final PageContext pageContext) {
final Composite parent = pageContext.getParent();
WidgetFactory.setTestId(parent, "login-page");
WidgetFactory.setARIARole(parent, "composite");
final Composite loginGroup = new Composite(parent, SWT.NONE);
final GridLayout rowLayout = new GridLayout();
@ -76,16 +84,17 @@ public class LoginPage implements TemplateComposer {
loginGroup.setLayout(rowLayout);
loginGroup.setData(RWT.CUSTOM_VARIANT, WidgetFactory.CustomVariant.LOGIN.key);
final Label name = this.widgetFactory.labelLocalized(loginGroup, "sebserver.login.username");
final Label name = this.widgetFactory.labelLocalized(loginGroup, TEXT_USERNAME);
name.setLayoutData(new GridData(300, -1));
name.setAlignment(SWT.BOTTOM);
final Text loginName = this.widgetFactory.textInput(loginGroup);
final Text loginName = this.widgetFactory.textInput(loginGroup, TEXT_USERNAME);
loginName.setLayoutData(new GridData(SWT.FILL, SWT.TOP, false, false));
GridData gridData = new GridData(SWT.FILL, SWT.TOP, false, false);
gridData.verticalIndent = 10;
final Label pwd = this.widgetFactory.labelLocalized(loginGroup, "sebserver.login.pwd");
final Label pwd = this.widgetFactory.labelLocalized(loginGroup, TEXT_PWD);
pwd.setLayoutData(gridData);
final Text loginPassword = this.widgetFactory.passwordInput(loginGroup);
final Text loginPassword = this.widgetFactory.passwordInput(loginGroup, TEXT_PWD);
loginPassword.setLayoutData(new GridData(SWT.FILL, SWT.TOP, false, false));
final Composite buttons = new Composite(loginGroup, SWT.NONE);
@ -93,7 +102,7 @@ public class LoginPage implements TemplateComposer {
buttons.setLayout(new GridLayout(2, false));
buttons.setData(RWT.CUSTOM_VARIANT, WidgetFactory.CustomVariant.LOGIN_BACK.key);
final Button loginButton = this.widgetFactory.buttonLocalized(buttons, "sebserver.login.login");
final Button loginButton = this.widgetFactory.buttonLocalized(buttons, TEXT_LOGIN);
gridData = new GridData(SWT.LEFT, SWT.TOP, false, false);
gridData.verticalIndent = 10;
loginButton.setLayoutData(gridData);
@ -127,12 +136,17 @@ public class LoginPage implements TemplateComposer {
});
if (this.registeringEnabled) {
final Button registerButton = this.widgetFactory.buttonLocalized(buttons, "sebserver.login.register");
final Button registerButton = this.widgetFactory.buttonLocalized(buttons, TEXT_REGISTER);
gridData = new GridData(SWT.LEFT, SWT.TOP, false, false);
gridData.verticalIndent = 10;
registerButton.setLayoutData(gridData);
registerButton.addListener(SWT.Selection, event -> pageContext.forwardToPage(this.defaultRegisterPage));
}
ComposerService.traversePageTree(
parent,
comp -> comp instanceof Control,
comp -> comp.getAdapter(IControlAdapter.class).setTabIndex(0));
}
private void login(

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

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

View file

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

View file

@ -8,21 +8,28 @@
package ch.ethz.seb.sebserver.gui.content;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder;
import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRoomConnections;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition;
import ch.ethz.seb.sebserver.gui.table.EntityTable;
@Lazy
@Component
@ -31,6 +38,10 @@ public class ProctorRoomConnectionsPopup {
private static final LocTextKey TITLE_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.proctoring.room.connections.title");
private static final LocTextKey EMPTY_LIST_TEXT_KEY =
new LocTextKey("sebserver.monitoring.search.list.empty");
private static final LocTextKey TABLE_COLUMN_NAME =
new LocTextKey("sebserver.monitoring.search.list.name");
private final PageService pageService;
@ -43,29 +54,60 @@ public class ProctorRoomConnectionsPopup {
pageContext.getParent().getShell(),
this.pageService.getWidgetFactory());
dialog.setLargeDialogWidth();
dialog.setDialogHeight(380);
dialog.open(
new LocTextKey(TITLE_TEXT_KEY.name, roomSubject),
pageContext,
this::compose);
c -> this.compose(c, dialog));
}
private void compose(final PageContext pageContext) {
final Composite parent = pageContext.getParent();
final Composite grid = this.pageService.getWidgetFactory().createPopupScrollComposite(parent);
private void compose(final PageContext pageContext, final ModalInputDialog<Void> dialog) {
final EntityKey entityKey = pageContext.getEntityKey();
final EntityKey parentEntityKey = pageContext.getParentEntityKey();
this.pageService.getRestService()
final PageActionBuilder actionBuilder = this.pageService
.pageActionBuilder(pageContext.clearEntityKeys());
final List<ClientConnection> connections = new ArrayList<>(this.pageService.getRestService()
.getBuilder(GetCollectingRoomConnections.class)
.withURIVariable(API.PARAM_MODEL_ID, parentEntityKey.modelId)
.withQueryParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, entityKey.modelId)
.call()
.getOrThrow()
.stream()
.forEach(connection -> {
final Label label = new Label(grid, SWT.NONE);
label.setText(connection.userSessionId);
});
.getOrThrow());
this.pageService.staticListTableBuilder(connections, EntityType.CLIENT_CONNECTION)
.withEmptyMessage(EMPTY_LIST_TEXT_KEY)
.withPaging(10)
.withColumn(new ColumnDefinition<>(
Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_ID,
TABLE_COLUMN_NAME,
ClientConnection::getUserSessionId))
.withDefaultAction(t -> actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION)
.withParentEntityKey(parentEntityKey)
.withExec(action -> showClientConnection(action, dialog, t))
.create())
.compose(pageContext);
}
private PageAction showClientConnection(
final PageAction pageAction,
final ModalInputDialog<Void> dialog,
final EntityTable<ClientConnection> table) {
final ClientConnection singleSelectedROWData = table.getSingleSelectedROWData();
dialog.close();
return pageAction
.withEntityKey(new EntityKey(
singleSelectedROWData.id,
EntityType.CLIENT_CONNECTION))
.withAttribute(
Domain.CLIENT_CONNECTION.ATTR_CONNECTION_TOKEN,
singleSelectedROWData.getConnectionToken());
}
}

View file

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

View file

@ -567,6 +567,14 @@ public class ResourceService {
.getText(SEB_CONNECTION_STATUS_KEY_PREFIX + name, name);
}
public List<Tuple<String>> localizedClientConnectionStatusResources() {
return Arrays.stream(ConnectionStatus.values())
.map(type -> new Tuple<>(type.name(),
this.i18nSupport.getText(SEB_CONNECTION_STATUS_KEY_PREFIX + type.name())))
.sorted(RESOURCE_COMPARATOR)
.collect(Collectors.toList());
}
public String localizedExamTypeName(final ExamConfigurationMap examMap) {
if (examMap.examType == null) {
return Constants.EMPTY_NOTE;

View file

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

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

View file

@ -17,6 +17,7 @@ import org.eclipse.swt.layout.RowData;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
@ -78,6 +79,11 @@ public class JitsiMeetProctoringView extends AbstractProctoringView {
final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true);
content.setLayoutData(headerCell);
final Label title = this.pageService
.getWidgetFactory()
.label(content, proctoringWindowData.connectionData.subject);
title.setLayoutData(new GridData(SWT.CENTER, SWT.TOP, true, true));
parent.addListener(SWT.Dispose, event -> closeRoom(proctoringGUIService, proctoringWindowData));
final String url = this.guiServiceInfo

View file

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

View file

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

View file

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

View file

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

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

View file

@ -10,7 +10,6 @@ package ch.ethz.seb.sebserver.gui.service.session.proctoring;
import java.util.Arrays;
import java.util.Collections;
import java.util.Optional;
import org.apache.commons.lang3.BooleanUtils;
import org.eclipse.rap.rwt.RWT;
@ -47,7 +46,7 @@ import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder;
import ch.ethz.seb.sebserver.gui.service.page.event.ActionActivationEvent;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.push.ServerPushContext;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRooms;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnection;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.IsTownhallRoomAvailable;
@ -80,6 +79,7 @@ public class MonitoringProctoringService {
static final String OPEN_ROOM_SCRIPT =
"try {\n" +
"var existingWin = window.open('', '%s', 'height=%s,width=%s,location=no,scrollbars=yes,status=no,menubar=0,toolbar=no,titlebar=no,dialog=no');\n" +
"existingWin.document.title = '%s';\n" +
"if(existingWin.location.href === 'about:blank'){\n" +
" existingWin.location.href = '%s%s';\n" +
" existingWin.focus();\n" +
@ -152,6 +152,7 @@ public class MonitoringProctoringService {
}
public void initCollectingRoomActions(
final ServerPushContext pushContext,
final PageContext pageContext,
final PageActionBuilder actionBuilder,
final ProctoringServiceSettings proctoringSettings,
@ -159,6 +160,7 @@ public class MonitoringProctoringService {
proctoringGUIService.clearCollectingRoomActionState();
updateCollectingRoomActions(
pushContext,
pageContext,
actionBuilder,
proctoringSettings,
@ -166,6 +168,7 @@ public class MonitoringProctoringService {
}
public void updateCollectingRoomActions(
final ServerPushContext pushContext,
final PageContext pageContext,
final PageActionBuilder actionBuilder,
final ProctoringServiceSettings proctoringSettings,
@ -179,7 +182,7 @@ public class MonitoringProctoringService {
.getBuilder(GetCollectingRooms.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.onError(error -> log.error("Failed to update proctoring rooms on GUI {}", error.getMessage()))
.onError(error -> pushContext.reportError(error))
.getOr(Collections.emptyList())
.stream()
.forEach(room -> {
@ -198,14 +201,11 @@ public class MonitoringProctoringService {
final PageAction action =
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM)
.withEntityKey(entityKey)
.withExec(_action -> {
final int actualRoomSize = proctoringGUIService
.getActualCollectingRoomSize(room.name);
if (actualRoomSize <= 0) {
return _action;
}
return showExamProctoringRoom(proctoringSettings, room, _action);
})
.withExec(_action -> openExamProctoringRoom(
proctoringGUIService,
proctoringSettings,
room,
_action))
.withNameAttributes(
room.subject,
room.roomSize,
@ -226,6 +226,7 @@ public class MonitoringProctoringService {
.withParentEntityKey(entityKey);
this.proctorRoomConnectionsPopup.show(pc, collectingRoom.subject);
}));
processProctorRoomActionActivation(
proctoringGUIService.getCollectingRoomActionItem(room.name),
room, pageContext);
@ -235,60 +236,53 @@ public class MonitoringProctoringService {
updateTownhallButton(proctoringGUIService, pageContext);
}
public PageAction openExamCollectionProctorScreen(
final PageAction action,
final ClientConnectionData connectionData) {
private PageAction openExamProctoringRoom(
final ProctoringGUIService proctoringGUIService,
final ProctoringServiceSettings proctoringSettings,
final RemoteProctoringRoom room,
final PageAction action) {
try {
final String examId = action.getEntityKey().modelId;
if (!proctoringGUIService.isCollectingRoomEnabled(room.name)) {
return action;
}
final ProctoringServiceSettings proctoringSettings = this.pageService.getRestService()
.getBuilder(GetProctoringSettings.class)
.withURIVariable(API.PARAM_MODEL_ID, examId)
final ProctoringRoomConnection proctoringConnectionData = this.pageService
.getRestService()
.getBuilder(GetProctorRoomConnection.class)
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(proctoringSettings.examId))
.withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room.name)
.withQueryParam(ProctoringRoomConnection.ATTR_SUBJECT, Utils.encodeFormURL_UTF_8(room.subject))
.call()
.getOrThrow();
ProctoringGUIService.setCurrentProctoringWindowData(
String.valueOf(proctoringSettings.examId),
proctoringConnectionData);
final String script = String.format(
OPEN_ROOM_SCRIPT,
room.name,
800,
1200,
room.name,
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
this.remoteProctoringEndpoint);
RWT.getClient()
.getService(JavaScriptExecutor.class)
.execute(script);
final boolean newWindow = this.pageService.getCurrentUser()
.getProctoringGUIService()
.registerProctoringWindow(String.valueOf(room.examId), room.name, room.name);
if (newWindow) {
this.pageService.getRestService()
.getBuilder(NotifyProctoringRoomOpened.class)
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(proctoringSettings.examId))
.withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room.name)
.call()
.getOrThrow();
final Optional<RemoteProctoringRoom> roomOptional =
this.pageService.getRestService().getBuilder(GetCollectingRooms.class)
.withURIVariable(API.PARAM_MODEL_ID, examId)
.call()
.getOrThrow()
.stream()
.filter(room -> room.id.equals(connectionData.clientConnection.remoteProctoringRoomId))
.findFirst();
if (roomOptional.isPresent()) {
final RemoteProctoringRoom room = roomOptional.get();
final ProctoringRoomConnection proctoringConnectionData = this.pageService
.getRestService()
.getBuilder(GetProctorRoomConnection.class)
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(proctoringSettings.examId))
.withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room.name)
.withQueryParam(ProctoringRoomConnection.ATTR_SUBJECT, Utils.encodeFormURL_UTF_8(room.subject))
.call()
.getOrThrow();
ProctoringGUIService.setCurrentProctoringWindowData(examId, proctoringConnectionData);
final String script = String.format(
MonitoringProctoringService.OPEN_ROOM_SCRIPT,
room.name,
800,
1200,
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
this.remoteProctoringEndpoint);
RWT.getClient()
.getService(JavaScriptExecutor.class)
.execute(script);
this.pageService.getCurrentUser()
.getProctoringGUIService()
.registerProctoringWindow(examId, room.name, room.name);
}
} catch (final Exception e) {
log.error("Failed to open popup for collecting room: ", e);
action.pageContext().notifyError(CLOSE_COLLECTING_ERROR, e);
.onError(error -> log.error("Failed to notify proctoring room opened: ", error));
}
return action;
@ -329,6 +323,7 @@ public class MonitoringProctoringService {
connectionToken,
420,
640,
connectionData.clientConnection.userSessionId,
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
this.remoteProctoringEndpoint);
javaScriptExecutor.execute(script);
@ -370,6 +365,7 @@ public class MonitoringProctoringService {
windowName,
800,
1200,
"Town-Hall",
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
this.remoteProctoringEndpoint);
javaScriptExecutor.execute(script);
@ -444,59 +440,19 @@ public class MonitoringProctoringService {
final PageContext pageContext) {
try {
final boolean active = room.roomSize > 0 && !room.isOpen;
final Display display = pageContext.getRoot().getDisplay();
final PageAction action = (PageAction) treeItem.getData(ActionPane.ACTION_EVENT_CALL_KEY);
final Image image = room.roomSize > 0
final Image image = active
? action.definition.icon.getImage(display)
: action.definition.icon.getGreyedImage(display);
treeItem.setImage(image);
treeItem.setForeground(room.roomSize > 0 ? null : new Color(display, Constants.GREY_DISABLED));
treeItem.setForeground(active ? null : new Color(display, Constants.GREY_DISABLED));
} catch (final Exception e) {
log.warn("Failed to set Proctor-Room-Activation: ", e.getMessage());
}
}
private PageAction showExamProctoringRoom(
final ProctoringServiceSettings proctoringSettings,
final RemoteProctoringRoom room,
final PageAction action) {
final ProctoringRoomConnection proctoringConnectionData = this.pageService
.getRestService()
.getBuilder(GetProctorRoomConnection.class)
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(proctoringSettings.examId))
.withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room.name)
.withQueryParam(ProctoringRoomConnection.ATTR_SUBJECT, Utils.encodeFormURL_UTF_8(room.subject))
.call()
.getOrThrow();
ProctoringGUIService.setCurrentProctoringWindowData(
String.valueOf(proctoringSettings.examId),
proctoringConnectionData);
final String script = String.format(
OPEN_ROOM_SCRIPT,
room.name,
800,
1200,
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
this.remoteProctoringEndpoint);
RWT.getClient()
.getService(JavaScriptExecutor.class)
.execute(script);
this.pageService.getCurrentUser()
.getProctoringGUIService()
.registerProctoringWindow(String.valueOf(room.examId), room.name, room.name);
this.pageService.getRestService().getBuilder(NotifyProctoringRoomOpened.class)
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(proctoringSettings.examId))
.withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room.name)
.call()
.onError(error -> log.error("Failed to notify proctoring room opened: ", error));
return action;
}
}

View file

@ -78,7 +78,6 @@ public class ProctoringGUIService {
final RemoteProctoringRoom remoteProctoringRoom = getRemoteProctoringRoom(item);
if (remoteProctoringRoom != null && remoteProctoringRoom.roomSize > 0) {
showConnectionsPopup.accept(remoteProctoringRoom);
//this.proctorRoomConnectionsPopup.show(pc, remoteProctoringRoom.subject);
}
}
});
@ -99,12 +98,13 @@ public class ProctoringGUIService {
.orElse(null);
}
public int getActualCollectingRoomSize(final String roomName) {
public boolean isCollectingRoomEnabled(final String roomName) {
try {
return this.collectingRoomsActionState.get(roomName).a.roomSize;
final Pair<RemoteProctoringRoom, TreeItem> pair = this.collectingRoomsActionState.get(roomName);
return pair.a.roomSize > 0 && !pair.a.isOpen;
} catch (final Exception e) {
log.error("Failed to get actual collecting room size for room: {} cause: ", roomName, e.getMessage());
return -1;
return false;
}
}
@ -117,12 +117,14 @@ public class ProctoringGUIService {
this.collectingRoomsActionState.clear();
}
public void registerProctoringWindow(
public boolean registerProctoringWindow(
final String examId,
final String windowName,
final String roomName) {
this.openWindows.put(windowName, new RoomData(roomName, examId));
return this.openWindows.putIfAbsent(
windowName,
new RoomData(roomName, examId)) == null;
}
public String getTownhallWindowName(final String examId) {

View file

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

View file

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

View file

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

View file

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

View file

@ -60,7 +60,5 @@ public class CacheConfig extends JCacheConfigurerSupport {
log.error("Failed to initialize caching with EHCache. Fallback to simple caching");
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.JNCryptor;
import org.flywaydb.core.Flyway;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Profile;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@ -25,9 +21,6 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@WebServiceProfile
public class WebserviceConfig {
@Value("${sebserver.webservice.clean-db-on-startup:false}")
boolean cleanDBOnStartup;
@Lazy
@Bean
public JNCryptor jnCryptor() {
@ -36,24 +29,4 @@ public class WebserviceConfig {
return aes256jnCryptor;
}
/** For test, development and demo profile, we want to always clean up and
* Start the migration from scratch to work with the same data.
*
* @return FlywayMigrationStrategy for "dev-ws", "test", "demo" profiles */
@Bean
@Profile(value = { "dev-ws", "test", "demo" })
public FlywayMigrationStrategy cleanMigrateStrategy() {
final FlywayMigrationStrategy strategy = new FlywayMigrationStrategy() {
@Override
public void migrate(final Flyway flyway) {
if (WebserviceConfig.this.cleanDBOnStartup) {
flyway.clean();
}
flyway.migrate();
}
};
return strategy;
}
}

View file

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

View file

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

View file

@ -121,9 +121,17 @@ public interface RemoteProctoringRoomDAO {
/** Get currently active break-out rooms for given connectionToken
*
* @param examId The exam identifier of the connection
* @param connectionTokens The connection token of the client connection
* @return Result refer to active break-out rooms or to an error when happened */
Result<Collection<RemoteProctoringRoom>> getBreakoutRooms(String connectionToken);
/** Get a list of client connection tokens of connections that currently are in
* break-out rooms, including the town-hall room
*
* @param examId The exam identifier of the connection
* @return Result refer to active break-out rooms or to an error when happened */
Result<Collection<String>> getConnectionsInBreakoutRooms(Long examId);
void setCollectingRoomOpenFlag(Long roomId, boolean isOpen);
}

View file

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

View file

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

View file

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

View file

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

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.RemoteProctoringRoomRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.RemoteProctoringRoomRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.RemoteProctoringRoomDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.NewRoom;
@ -49,11 +50,14 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
private static final Object RESERVE_ROOM_LOCK = new Object();
private final RemoteProctoringRoomRecordMapper remoteProctoringRoomRecordMapper;
private final AdditionalAttributesDAO additionalAttributesDAO;
protected RemoteProctoringRoomDAOImpl(
final RemoteProctoringRoomRecordMapper remoteProctoringRoomRecordMapper) {
final RemoteProctoringRoomRecordMapper remoteProctoringRoomRecordMapper,
final AdditionalAttributesDAO additionalAttributesDAO) {
this.remoteProctoringRoomRecordMapper = remoteProctoringRoomRecordMapper;
this.additionalAttributesDAO = additionalAttributesDAO;
}
@Override
@ -227,6 +231,10 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
@Transactional
public Result<EntityKey> deleteRoom(final Long roomId) {
return Result.tryCatch(() -> {
this.additionalAttributesDAO
.deleteAll(EntityType.REMOTE_PROCTORING_ROOM, roomId);
this.remoteProctoringRoomRecordMapper
.deleteByPrimaryKey(roomId);
@ -249,7 +257,15 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
.execute();
return ids.stream()
.map(id -> new EntityKey(String.valueOf(id), EntityType.REMOTE_PROCTORING_ROOM))
.map(roomId -> {
this.additionalAttributesDAO.deleteAll(
EntityType.REMOTE_PROCTORING_ROOM,
roomId);
return roomId;
})
.map(roomId -> new EntityKey(
String.valueOf(roomId),
EntityType.REMOTE_PROCTORING_ROOM))
.collect(Collectors.toList());
});
@ -322,6 +338,37 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
.collect(Collectors.toList()));
}
@Override
@Transactional(readOnly = true)
public Result<Collection<String>> getConnectionsInBreakoutRooms(final Long examId) {
return Result.tryCatch(() -> this.remoteProctoringRoomRecordMapper
.selectByExample()
.where(RemoteProctoringRoomRecordDynamicSqlSupport.examId, isEqualTo(examId))
.and(RemoteProctoringRoomRecordDynamicSqlSupport.breakOutConnections, isNotNull())
.build()
.execute()
.stream()
.flatMap(room -> Arrays.asList(
StringUtils.split(
room.getBreakOutConnections(),
Constants.LIST_SEPARATOR_CHAR))
.stream())
.collect(Collectors.toList()));
}
@Override
@Transactional
public void setCollectingRoomOpenFlag(final Long roomId, final boolean isOpen) {
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.REMOTE_PROCTORING_ROOM,
roomId,
RemoteProctoringRoom.ATTR_IS_OPEN,
BooleanUtils.toStringTrueFalse(isOpen))
.onError(error -> log.error("Failed to set open flag for proctoring room: {} : {}",
roomId,
error.getMessage()));
}
private RemoteProctoringRoom toDomainModel(final RemoteProctoringRoomRecord record) {
final String breakOutConnections = record.getBreakOutConnections();
final Collection<String> connections = StringUtils.isNotBlank(breakOutConnections)
@ -337,7 +384,22 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
BooleanUtils.toBooleanObject(record.getTownhallRoom()),
connections,
record.getJoinKey(),
record.getRoomData());
record.getRoomData(),
isOpen(record));
}
private boolean isOpen(final RemoteProctoringRoomRecord record) {
if (record.getTownhallRoom() != 0 || !StringUtils.isBlank(record.getBreakOutConnections())) {
return false;
} else {
return BooleanUtils.toBoolean(this.additionalAttributesDAO
.getAdditionalAttribute(
EntityType.REMOTE_PROCTORING_ROOM,
record.getId(),
RemoteProctoringRoom.ATTR_IS_OPEN)
.map(rec -> rec.getValue())
.getOrElse(() -> Constants.FALSE_STRING));
}
}
private RemoteProctoringRoomRecord createNewCollectingRoom(

View file

@ -47,6 +47,20 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO {
this.forceMaster = forceMaster;
}
@Override
@Transactional
public boolean isInitialized() {
try {
this.webserviceServerInfoRecordMapper
.selectByExample()
.build()
.execute();
return true;
} catch (final Exception e) {
return false;
}
}
@Transactional
@Override
public boolean register(final String uuid, final String address) {
@ -173,7 +187,7 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO {
.execute();
return true;
} catch (final Exception e) {
log.error("Failed to register webservice: uuid: {}", uuid, e);
log.warn("Failed to unregister webservice: uuid: {}, cause: ", uuid, e);
return false;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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.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'),
(961, 'allowPDFReaderToolbar', 'CHECKBOX', null, null, null, null, 'false'),
(970, 'setVmwareConfiguration', 'CHECKBOX', null, null, null, null, 'false')
(970, 'setVmwareConfiguration', 'CHECKBOX', null, null, null, null, 'false'),
(971, 'allowedDisplaysIgnoreFailure', 'CHECKBOX', null, null, null, null, 'false')
;
-- -----------------------------------------------------
@ -66,6 +68,11 @@ INSERT IGNORE INTO orientation (config_attribute_id, template_id, view_id, group
-- insert Set VMWare Configuration
INSERT IGNORE INTO orientation (config_attribute_id, template_id, view_id, group_id, x_position, y_position, width, height, title) VALUES
(970, 0, 10, 'registry', 0, 7, 4, 1, 'NONE');
-- insert mac settings
INSERT IGNORE INTO orientation (config_attribute_id, template_id, view_id, group_id, x_position, y_position, width, height, title) VALUES
(971, 0, 9, 'macSettings', 7, 11, 5, 1, 'NONE');
-- -----------------------------------------------------
-- Update old orientations
@ -105,7 +112,11 @@ UPDATE orientation SET group_id='additionalWindow', x_position=0, y_position=14,
UPDATE orientation SET x_position=7, y_position=11, width=5 WHERE config_attribute_id=57;
UPDATE orientation SET x_position=7, y_position=13, width=5 WHERE config_attribute_id=58;
-- insert Set VMWare Configuration
-- update Set VMWare Configuration
UPDATE orientation SET y_position=8 WHERE config_attribute_id=406;
UPDATE orientation SET y_position=9 WHERE config_attribute_id=407;
UPDATE orientation SET y_position=10 WHERE config_attribute_id=408;
-- update mac settings
UPDATE orientation SET y_position=10 WHERE config_attribute_id=315;
UPDATE orientation SET y_position=12 WHERE config_attribute_id=316;

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.BROADCAST=Broadcasting Feature
sebserver.exam.proctoring.form.features.ENABLE_CHAT=Chat Feature
sebserver.exam.proctoring.form.features.WAITING_ROOM=Enable waiting room for collecting rooms
sebserver.exam.proctoring.form.features.SEND_REJOIN_COLLECTING_ROOM=Force rejoin for collecting rooms
sebserver.exam.proctoring.form.features.RESET_BROADCAST_ON_LAVE=Reset broadcast on leave
sebserver.exam.proctoring.type.servertype.JITSI_MEET=Jitsi Meet Server
sebserver.exam.proctoring.type.servertype.JITSI_MEET.tooltip=Use a Jitsi Meet server for proctoring
@ -1296,6 +1299,8 @@ sebserver.examconfig.props.label.logLevel.4=Verbose
sebserver.examconfig.props.label.logLevel.4.tooltip=Verbose level contains events of all levels
sebserver.examconfig.props.label.allowApplicationLog=Allow access to application log (Win)
sebserver.examconfig.props.label.showApplicationLogButton=Show log button on taskbar (Win)
sebserver.examconfig.props.label.allowedDisplaysIgnoreFailure=Ignore errors when validating display configuration.
sebserver.examconfig.props.label.allowedDisplaysIgnoreFailure.tooltip=Needs to be active when using SEB inside a virtual machine
sebserver.examconfig.props.group.registry=While running SEB
sebserver.examconfig.props.group.registry.tooltip=Options in the Windows Security Screen invoked by Ctrl-Alt-Del
@ -1628,7 +1633,7 @@ sebserver.monitoring.exam.action.proctoring.openTownhall=Open Townhall
sebserver.monitoring.exam.action.proctoring.showTownhall=Show Townhall
sebserver.monitoring.exam.action.proctoring.closeTownhall=Close Townhall
sebserver.monitoring.exam.proctoring.room.all.name=Exam Room
sebserver.monitoring.exam.proctoring.room.all.name=Townhall Room
sebserver.monitoring.exam.proctoring.action.close=Close Window
sebserver.monitoring.exam.proctoring.action.broadcaston.audio=Start Audio Broadcast
sebserver.monitoring.exam.proctoring.action.broadcastoff.audio=End Audio Broadcast
@ -1674,6 +1679,7 @@ sebserver.monitoring.search.action=Search
sebserver.monitoring.search.list.empty=No Client Connections available
sebserver.monitoring.search.list.name=Session or User Name
sebserver.monitoring.search.list.ip=IP Address
sebserver.monitoring.search.list.status=Status
sebserver.monitoring.exam.connection.emptySelection=At first please select a Connection from the list

View file

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