Zoom improvements and accessibility additions
This commit is contained in:
parent
c8932e9ce8
commit
5724a750b7
14 changed files with 197 additions and 76 deletions
|
@ -35,7 +35,10 @@ public class ProctoringServiceSettings implements Entity {
|
|||
TOWN_HALL,
|
||||
ONE_TO_ONE,
|
||||
BROADCAST,
|
||||
ENABLE_CHAT
|
||||
ENABLE_CHAT,
|
||||
WAITING_ROOM,
|
||||
SEND_REJOIN_COLLECTING_ROOM,
|
||||
RESET_BROADCAST_ON_LAVE
|
||||
}
|
||||
|
||||
public static final String ATTR_ENABLE_PROCTORING = "enableProctoring";
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -321,7 +321,9 @@ public class TableFilter<ROW> {
|
|||
final Composite innerComposite = createInnerComposite(parent);
|
||||
final GridData gridData = new GridData(SWT.FILL, SWT.END, true, true);
|
||||
|
||||
this.textInput = TableFilter.this.entityTable.widgetFactory.textInput(innerComposite);
|
||||
this.textInput = TableFilter.this.entityTable.widgetFactory.textInput(
|
||||
innerComposite,
|
||||
super.attribute.columnName);
|
||||
this.textInput.setLayoutData(gridData);
|
||||
return this;
|
||||
}
|
||||
|
|
|
@ -8,9 +8,11 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.gui.widget;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Tuple;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.PageService;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.rap.rwt.widgets.DropDown;
|
||||
import org.eclipse.swt.SWT;
|
||||
|
@ -25,10 +27,9 @@ import org.eclipse.swt.widgets.Text;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Tuple;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.PageService;
|
||||
|
||||
public final class MultiSelectionCombo extends Composite implements Selection {
|
||||
|
||||
|
@ -68,7 +69,7 @@ public final class MultiSelectionCombo extends Composite implements Selection {
|
|||
setLayout(gridLayout);
|
||||
|
||||
this.addListener(SWT.Resize, this::adaptColumnWidth);
|
||||
this.textInput = widgetFactory.textInput(this);
|
||||
this.textInput = widgetFactory.textInput(this, "selection");
|
||||
this.textCell = new GridData(SWT.LEFT, SWT.CENTER, true, true);
|
||||
this.textInput.setLayoutData(this.textCell);
|
||||
this.dropDown = new DropDown(this.textInput, SWT.NONE);
|
||||
|
|
|
@ -152,7 +152,8 @@ public final class ThresholdList extends Composite {
|
|||
} else {
|
||||
Double.parseDouble(s);
|
||||
}
|
||||
});
|
||||
},
|
||||
VALUE_TEXT_KEY);
|
||||
final GridData valueCell = new GridData(SWT.FILL, SWT.CENTER, true, false);
|
||||
valueInput.setLayoutData(valueCell);
|
||||
|
||||
|
|
|
@ -70,6 +70,10 @@ public class WidgetFactory {
|
|||
private static final String ADD_HTML_ATTR_TEST_ID = "test-id";
|
||||
private static final String SUB_TITLE_TExT_SUFFIX = ".subtitle";
|
||||
|
||||
public enum AriaRole {
|
||||
link
|
||||
}
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WidgetFactory.class);
|
||||
|
||||
public static final int TEXT_AREA_INPUT_MIN_HEIGHT = 100;
|
||||
|
@ -363,18 +367,21 @@ public class WidgetFactory {
|
|||
|
||||
public Button buttonLocalized(final Composite parent, final String locTextKey) {
|
||||
final Button button = new Button(parent, SWT.NONE);
|
||||
setAttribute(button, "role", "button");
|
||||
this.polyglotPageService.injectI18n(button, new LocTextKey(locTextKey));
|
||||
return button;
|
||||
}
|
||||
|
||||
public Button buttonLocalized(final Composite parent, final LocTextKey locTextKey) {
|
||||
final Button button = new Button(parent, SWT.NONE);
|
||||
setAttribute(button, "role", "button");
|
||||
this.polyglotPageService.injectI18n(button, locTextKey);
|
||||
return button;
|
||||
}
|
||||
|
||||
public Button buttonLocalized(final Composite parent, final CustomVariant variant, final String locTextKey) {
|
||||
final Button button = new Button(parent, SWT.NONE);
|
||||
setAttribute(button, "role", "button");
|
||||
this.polyglotPageService.injectI18n(button, new LocTextKey(locTextKey));
|
||||
button.setData(RWT.CUSTOM_VARIANT, variant.key);
|
||||
return button;
|
||||
|
@ -387,6 +394,7 @@ public class WidgetFactory {
|
|||
final LocTextKey toolTipKey) {
|
||||
|
||||
final Button button = new Button(parent, type);
|
||||
setAttribute(button, "role", "button");
|
||||
this.polyglotPageService.injectI18n(button, locTextKey, toolTipKey);
|
||||
return button;
|
||||
}
|
||||
|
@ -453,42 +461,85 @@ public class WidgetFactory {
|
|||
return labelLocalized;
|
||||
}
|
||||
|
||||
public Text textInput(final Composite content) {
|
||||
return textInput(content, false, false);
|
||||
public Text textInput(final Composite content, final LocTextKey label) {
|
||||
return textInput(content, false, false, this.i18nSupport.getText(label));
|
||||
}
|
||||
|
||||
public Text textLabel(final Composite content) {
|
||||
return textInput(content, false, true);
|
||||
public Text textInput(final Composite content, final String label) {
|
||||
return textInput(content, false, false, label);
|
||||
}
|
||||
|
||||
public Text passwordInput(final Composite content) {
|
||||
return textInput(content, true, false);
|
||||
public Text passwordInput(final Composite content, final LocTextKey label) {
|
||||
return textInput(content, true, false, this.i18nSupport.getText(label));
|
||||
}
|
||||
|
||||
public Text textAreaInput(final Composite content, final boolean readonly) {
|
||||
return readonly
|
||||
public Text passwordInput(final Composite content, final String label) {
|
||||
return textInput(content, true, false, label);
|
||||
}
|
||||
|
||||
public Text textAreaInput(
|
||||
final Composite content,
|
||||
final boolean readonly,
|
||||
final LocTextKey label) {
|
||||
|
||||
return textAreaInput(content, readonly, this.i18nSupport.getText(label));
|
||||
}
|
||||
|
||||
public Text textAreaInput(
|
||||
final Composite content,
|
||||
final boolean readonly,
|
||||
final String label) {
|
||||
|
||||
final Text input = readonly
|
||||
? new Text(content, SWT.LEFT | SWT.MULTI)
|
||||
: new Text(content, SWT.LEFT | SWT.BORDER | SWT.MULTI);
|
||||
if (label != null) {
|
||||
WidgetFactory.setAttribute(input, "aria-label", label);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
public Text textInput(final Composite content, final boolean password, final boolean readonly) {
|
||||
return readonly
|
||||
public Text textInput(
|
||||
final Composite content,
|
||||
final boolean password,
|
||||
final boolean readonly,
|
||||
final LocTextKey label) {
|
||||
|
||||
return textInput(content, password, readonly, this.i18nSupport.getText(label));
|
||||
}
|
||||
|
||||
public Text textInput(
|
||||
final Composite content,
|
||||
final boolean password,
|
||||
final boolean readonly,
|
||||
final String label) {
|
||||
|
||||
final Text input = readonly
|
||||
? new Text(content, SWT.LEFT)
|
||||
: new Text(content, (password)
|
||||
? SWT.LEFT | SWT.BORDER | SWT.PASSWORD
|
||||
: SWT.LEFT | SWT.BORDER);
|
||||
|
||||
if (label != null) {
|
||||
WidgetFactory.setAttribute(input, "aria-label", label);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
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 LocTextKey label) {
|
||||
return numberInput(content, numberCheck, false, label);
|
||||
}
|
||||
|
||||
public Text numberInput(final Composite content, final Consumer<String> numberCheck, final boolean readonly) {
|
||||
if (readonly) {
|
||||
return new Text(content, SWT.LEFT | SWT.READ_ONLY);
|
||||
}
|
||||
public Text numberInput(
|
||||
final Composite content,
|
||||
final Consumer<String> numberCheck,
|
||||
final boolean readonly,
|
||||
final LocTextKey label) {
|
||||
|
||||
final Text numberInput = new Text(content, SWT.RIGHT | SWT.BORDER);
|
||||
final Text numberInput = new Text(content, (readonly) ? SWT.LEFT | SWT.READ_ONLY : SWT.RIGHT | SWT.BORDER);
|
||||
if (label != null) {
|
||||
WidgetFactory.setAttribute(numberInput, "aria-label", this.i18nSupport.getText(label));
|
||||
}
|
||||
if (numberCheck != null) {
|
||||
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);
|
||||
|
|
|
@ -27,6 +27,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
|||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringFeature;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType;
|
||||
|
@ -230,7 +231,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
|||
} else if (remoteProctoringRoom.townhallRoom) {
|
||||
closeTownhall(examId, settings, examProctoringService);
|
||||
} else {
|
||||
closeCollectingRoom(examId, roomName, examProctoringService);
|
||||
closeCollectingRoom(examId, roomName, settings, examProctoringService);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -377,11 +378,13 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
|||
.getActiveConnectionTokens(examId)
|
||||
.getOrThrow();
|
||||
|
||||
// Send default settings to clients
|
||||
// Send default settings to clients if fearture is enabled
|
||||
if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.RESET_BROADCAST_ON_LAVE)) {
|
||||
this.sendReconfigurationInstructions(
|
||||
examId,
|
||||
connectionTokens,
|
||||
examProctoringService.getDefaultReconfigInstructionAttributes());
|
||||
}
|
||||
|
||||
// Close and delete town-hall room
|
||||
this.remoteProctoringRoomDAO
|
||||
|
@ -403,6 +406,7 @@ 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
|
||||
|
@ -412,12 +416,14 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
|||
.map(cc -> cc.connectionToken)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Send default settings to clients
|
||||
// Send default settings to clients if feature is enabled
|
||||
if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.RESET_BROADCAST_ON_LAVE)) {
|
||||
this.sendReconfigurationInstructions(
|
||||
examId,
|
||||
connectionTokens,
|
||||
examProctoringService.getDefaultReconfigInstructionAttributes());
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupBreakOutRooms(final ClientConnectionRecord cc) {
|
||||
|
||||
|
@ -453,11 +459,13 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
|||
final ExamProctoringService examProctoringService,
|
||||
final RemoteProctoringRoom remoteProctoringRoom) {
|
||||
|
||||
// Send default settings to clients
|
||||
// Send default settings to clients if feature is enabled
|
||||
if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.RESET_BROADCAST_ON_LAVE)) {
|
||||
this.sendReconfigurationInstructions(
|
||||
examId,
|
||||
remoteProctoringRoom.breakOutConnections,
|
||||
examProctoringService.getDefaultReconfigInstructionAttributes());
|
||||
}
|
||||
|
||||
// Dispose the proctoring room on service side
|
||||
examProctoringService
|
||||
|
|
|
@ -57,6 +57,7 @@ import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
|
|||
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringFeature;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
|
||||
|
@ -443,8 +444,13 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
if (!proctoringSettings.enabledFeatures.contains(ProctoringFeature.SEND_REJOIN_COLLECTING_ROOM)) {
|
||||
// do 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
|
||||
// do 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;
|
||||
}
|
||||
|
@ -502,6 +508,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
proctoringSettings.serverURL,
|
||||
credentials,
|
||||
roomName);
|
||||
|
||||
final UserResponse userResponse = this.jsonMapper.readValue(
|
||||
createUser.getBody(),
|
||||
UserResponse.class);
|
||||
|
@ -514,7 +521,9 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
userResponse.id,
|
||||
subject,
|
||||
duration,
|
||||
meetingPwd);
|
||||
meetingPwd,
|
||||
proctoringSettings.enabledFeatures.contains(ProctoringFeature.WAITING_ROOM));
|
||||
|
||||
final MeetingResponse meetingResponse = this.jsonMapper.readValue(
|
||||
createMeeting.getBody(),
|
||||
MeetingResponse.class);
|
||||
|
@ -525,6 +534,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
userResponse.id,
|
||||
meetingResponse.start_url,
|
||||
meetingResponse.join_url);
|
||||
|
||||
final String additionalZoomRoomDataString = this.jsonMapper
|
||||
.writeValueAsString(additionalZoomRoomData);
|
||||
|
||||
|
@ -563,23 +573,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(".")
|
||||
|
@ -710,7 +726,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 {
|
||||
|
||||
|
@ -723,7 +740,8 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
final CreateMeetingRequest createRoomRequest = new CreateMeetingRequest(
|
||||
topic,
|
||||
duration,
|
||||
password);
|
||||
password,
|
||||
waitingRoom);
|
||||
|
||||
final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(createRoomRequest);
|
||||
final HttpHeaders headers = getHeaders(credentials);
|
||||
|
|
|
@ -123,7 +123,8 @@ public interface ZoomRoomRequestResponse {
|
|||
public CreateMeetingRequest(
|
||||
final String topic,
|
||||
final int duration,
|
||||
final CharSequence password) {
|
||||
final CharSequence password,
|
||||
final boolean waitingRoom) {
|
||||
|
||||
this.type = 2; // Scheduled Meeting
|
||||
this.start_time = DateTime.now(DateTimeZone.UTC).toString("yyyy-MM-dd'T'HH:mm:ss");
|
||||
|
@ -131,18 +132,25 @@ 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 participant_video = false;
|
||||
@JsonProperty final boolean mute_upon_entry = true;
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -99,6 +99,9 @@
|
|||
})
|
||||
|
||||
window.addEventListener('unload', () => {
|
||||
ZoomMtg.muteAll({
|
||||
muteAll: true
|
||||
});
|
||||
ZoomMtg.endMeeting({});
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -166,6 +166,9 @@ public class ZoomWindowScriptResolverTest {
|
|||
+ " })\r\n"
|
||||
+ " \r\n"
|
||||
+ " window.addEventListener('unload', () => {\r\n"
|
||||
+ " ZoomMtg.muteAll({\r\n"
|
||||
+ " muteAll: true\r\n"
|
||||
+ " });\r\n"
|
||||
+ " ZoomMtg.endMeeting({});\r\n"
|
||||
+ " });\r\n"
|
||||
+ " </script>\r\n"
|
||||
|
|
Loading…
Reference in a new issue