SEBSERV-560 SEBSERV-563 implementation

This commit is contained in:
anhefti 2024-07-16 09:38:29 +02:00
parent f2f8a561a8
commit 303b3ac548
8 changed files with 167 additions and 68 deletions

View file

@ -8,11 +8,21 @@
package ch.ethz.seb.sebserver.gui.content.monitoring;
import static ch.ethz.seb.sebserver.gbl.model.user.UserFeatures.Feature.EXAM_SCREEN_PROCTORING;
import static ch.ethz.seb.sebserver.gbl.model.user.UserFeatures.Feature.MONITORING_RUNNING_EXAM_SCREEN_PROCTORING;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BooleanSupplier;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetScreenProctoringGroups;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.MonitoringProctoringService;
import org.apache.commons.lang3.BooleanUtils;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.rap.rwt.client.service.UrlLauncher;
import org.eclipse.swt.widgets.Composite;
@ -93,6 +103,7 @@ public class FinishedExam implements TemplateComposer {
private final RestService restService;
private final I18nSupport i18nSupport;
private final DownloadService downloadService;
private final MonitoringProctoringService monitoringProctoringService;
private final String exportFileName;
private final int pageSize;
@ -100,12 +111,14 @@ public class FinishedExam implements TemplateComposer {
final ServerPushService serverPushService,
final PageService pageService,
final DownloadService downloadService,
final MonitoringProctoringService monitoringProctoringService,
@Value("${sebserver.gui.seb.client.logs.export.filename:SEBClientLogs}") final String exportFileName,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
this.pageService = pageService;
this.restService = pageService.getRestService();
this.downloadService = downloadService;
this.monitoringProctoringService = monitoringProctoringService;
this.exportFileName = exportFileName;
this.pageSize = pageSize;
@ -209,6 +222,33 @@ public class FinishedExam implements TemplateComposer {
.withExec(this::exportCSV)
.noEventPropagation()
.publish();
// screen proctoring link
final ScreenProctoringSettings screenProctoringSettings = new ScreenProctoringSettings(exam);
final boolean screenProctoringEnabled =
currentUser.isFeatureEnabled(MONITORING_RUNNING_EXAM_SCREEN_PROCTORING)
&& BooleanUtils.toBoolean(screenProctoringSettings.enableScreenProctoring);
if (screenProctoringEnabled) {
this.pageService
.getRestService()
.getBuilder(GetScreenProctoringGroups.class)
.withURIVariable(API.PARAM_MODEL_ID, exam.getModelId())
.call()
.onError(error -> log.error("Failed to get screen proctoring group data:", error))
.getOr(Collections.emptyList())
.forEach(group -> {
actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_VIEW_SCREEN_PROCTOR_GROUP)
.withEntityKey(exam.getEntityKey())
.withExec(_action -> monitoringProctoringService.openScreenProctoringTab(
screenProctoringSettings,
group,
_action))
.withNameAttributes(group.name, group.size)
.noEventPropagation()
.publish();
});
}
}
private PageAction exportCSV(final PageAction action) {

View file

@ -22,7 +22,6 @@ import java.util.function.Consumer;
import java.util.function.Function;
import ch.ethz.seb.sebserver.gbl.model.exam.AllowedSEBVersion;
import ch.ethz.seb.sebserver.gbl.model.user.UserFeatures;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.eclipse.swt.SWT;
@ -391,7 +390,7 @@ public class MonitoringRunningExam implements TemplateComposer {
.getBuilder(GetScreenProctoringGroups.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.onError(error -> log.error("Failed to get collecting room data:", error))
.onError(error -> log.error("\"Failed to get screen proctoring group data:", error))
.getOr(Collections.emptyList())
: Collections.emptyList();
@ -684,7 +683,7 @@ public class MonitoringRunningExam implements TemplateComposer {
if (!this.actionItemPerClientGroup.isEmpty()) {
this.actionItemPerClientGroup.entrySet().stream().forEach(entry -> {
this.actionItemPerClientGroup.entrySet().forEach(entry -> {
final int numOfConnections = monitoringStatus.getNumOfConnections(entry.getKey());
if (numOfConnections >= 0) {
final TreeItem treeItem = entry.getValue();

View file

@ -215,13 +215,8 @@ public class MonitoringProctoringService {
this.pageService.publishAction(
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_VIEW_SCREEN_PROCTOR_GROUP)
.withEntityKey(entityKey)
.withExec(_action -> openScreenProctoringTab(
settings,
group,
_action))
.withNameAttributes(
group.name,
group.size)
.withExec(_action -> openScreenProctoringTab(settings, group, _action))
.withNameAttributes(group.name, group.size)
.noEventPropagation()
.create(),
_treeItem -> proctoringGUIService.registerScreeProctoringGroupAction(group, _treeItem));
@ -308,7 +303,7 @@ public class MonitoringProctoringService {
this.proctorRoomConnectionsPopup.show(pc, collectingRoom.subject);
}
private PageAction openScreenProctoringTab(
public PageAction openScreenProctoringTab(
final ScreenProctoringSettings settings,
final ScreenProctoringGroup group,
final PageAction _action) {

View file

@ -54,9 +54,7 @@ public interface ScreenProctoringGroupDAO {
*
* @param examId the exam identifier
* @param maxSize the maximum size of connection collected in one collecting group. Size of 0 means no limit.
* @param newGroupFunction Function to create data for a new collecting group if needed.
* @return Result refer to the collecting group record of place or to an error when happened
* @throws If the Result contains a AllGroupsFullException, there must be created a new Group first */
* @return Result refer to the collecting group record of place or to an error when happened*/
Result<ScreenProctoringGroup> reservePlaceInCollectingGroup(Long examId, int maxSize);
Result<ScreenProctoringGroup> releasePlaceInCollectingGroup(Long examId, Long groupId);
@ -80,4 +78,5 @@ public interface ScreenProctoringGroupDAO {
* @return Result refer to a collection of entity keys for all delete group records or to an error when happened */
Result<Collection<EntityKey>> deleteGroups(Long examId);
void updateGroupSize(String groupUUID, Integer activeCount, Integer totalCount);
}

View file

@ -8,8 +8,7 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl;
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
import static org.mybatis.dynamic.sql.SqlBuilder.isIn;
import static org.mybatis.dynamic.sql.SqlBuilder.*;
import java.util.Collection;
import java.util.Collections;
@ -142,7 +141,8 @@ public class ScreenProctoringGroupDAOImpl implements ScreenProctoringGroupDAO {
.findFirst();
if (room.isPresent()) {
return updateCollectingGroup(room.get());
return room.get();
//return updateCollectingGroup(room.get());
} else {
throw new AllGroupsFullException();
}
@ -234,6 +234,31 @@ public class ScreenProctoringGroupDAOImpl implements ScreenProctoringGroupDAO {
return tryCatch.onError(TransactionHandler::rollback);
}
@Override
@Transactional
public void updateGroupSize(
final String groupUUID,
final Integer activeCount,
final Integer totalCount) {
try {
UpdateDSL.updateWithMapper(
this.screenProctoringGroopRecordMapper::update,
ScreenProctoringGroopRecordDynamicSqlSupport.screenProctoringGroopRecord)
.set(ScreenProctoringGroopRecordDynamicSqlSupport.size)
.equalTo(activeCount)
.where(ScreenProctoringGroopRecordDynamicSqlSupport.uuid, isEqualTo(groupUUID))
.and(ScreenProctoringGroopRecordDynamicSqlSupport.size, isNotEqualTo(activeCount))
.build()
.execute();
} catch (final Exception e) {
log.warn("Failed to update SPS group size: {}", e.getMessage());
}
}
private ScreenProctoringGroup toDomainModel(final ScreenProctoringGroopRecord record) {
return new ScreenProctoringGroup(
record.getId(),
@ -244,22 +269,22 @@ public class ScreenProctoringGroupDAOImpl implements ScreenProctoringGroupDAO {
record.getData());
}
private ScreenProctoringGroopRecord updateCollectingGroup(
final ScreenProctoringGroopRecord screenProctoringGroopRecord) {
final Long id = screenProctoringGroopRecord.getId();
UpdateDSL.updateWithMapper(
this.screenProctoringGroopRecordMapper::update,
ScreenProctoringGroopRecordDynamicSqlSupport.screenProctoringGroopRecord)
.set(ScreenProctoringGroopRecordDynamicSqlSupport.size)
.equalTo(screenProctoringGroopRecord.getSize() + 1)
.where(ScreenProctoringGroopRecordDynamicSqlSupport.id, isEqualTo(id))
.build()
.execute();
return this.screenProctoringGroopRecordMapper.selectByPrimaryKey(id);
}
// private ScreenProctoringGroopRecord updateCollectingGroup(
// final ScreenProctoringGroopRecord screenProctoringGroupRecord) {
//
// final Long id = screenProctoringGroupRecord.getId();
//
// UpdateDSL.updateWithMapper(
// this.screenProctoringGroopRecordMapper::update,
// ScreenProctoringGroopRecordDynamicSqlSupport.screenProctoringGroopRecord)
// .set(ScreenProctoringGroopRecordDynamicSqlSupport.size)
// .equalTo(screenProctoringGroupRecord.getSize() + 1)
// .where(ScreenProctoringGroopRecordDynamicSqlSupport.id, isEqualTo(id))
// .build()
// .execute();
//
// return this.screenProctoringGroopRecordMapper.selectByPrimaryKey(id);
// }
public static final class AllGroupsFullException extends RuntimeException {
private static final long serialVersionUID = 3283129187819160485L;

View file

@ -33,6 +33,7 @@ public interface ScreenProctoringService extends SessionUpdateTask {
@Override
default void processSessionUpdateTask() {
updateClientConnections();
updateActiveGroups();
}
boolean isScreenProctoringEnabled(Long examId);
@ -98,6 +99,10 @@ public interface ScreenProctoringService extends SessionUpdateTask {
* SPS connection instruction to SEB client to connect and start sending screenshots. */
void updateClientConnections();
/** This goes through all running exams with screen proctoring enabled and updates the group attributes
* (mainly the number of active clients in the group) by call ing SPS API and store newest data. */
void updateActiveGroups();
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
void synchronizeSPSUser(final String userUUID);

View file

@ -82,6 +82,8 @@ class ScreenProctoringAPIBinding {
String TOKEN_ENDPOINT = "/oauth/token";
String TEST_ENDPOINT = "/admin-api/v1/proctoring/group";
String GROUP_COUNT_ENDPOINT = "/admin-api/v1/proctoring/active_counts";
String USER_ACCOUNT_ENDPOINT = "/admin-api/v1/useraccount/";
String USERSYNC_SEBSERVER_ENDPOINT = USER_ACCOUNT_ENDPOINT + "usersync/sebserver";
String ENTITY_PRIVILEGES_ENDPOINT = USER_ACCOUNT_ENDPOINT + "entityprivilege";
@ -151,7 +153,7 @@ class ScreenProctoringAPIBinding {
}
@JsonIgnoreProperties(ignoreUnknown = true)
static final class ExamUpdate {
final class ExamUpdate {
@JsonProperty(EXAM.ATTR_NAME)
final String name;
@JsonProperty(EXAM.ATTR_DESCRIPTION)
@ -187,6 +189,27 @@ class ScreenProctoringAPIBinding {
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
static final class GroupSessionCount {
@JsonProperty("uuid")
public final String groupUUID;
@JsonProperty("activeCount")
public final Integer activeCount;
@JsonProperty("totalCount")
public final Integer totalCount;
@JsonCreator
public GroupSessionCount(
@JsonProperty("uuid") final String groupUUID,
@JsonProperty("activeCount") final Integer activeCount,
@JsonProperty("totalCount") final Integer totalCount) {
this.groupUUID = groupUUID;
this.activeCount = activeCount;
this.totalCount = totalCount;
}
}
private final UserDAO userDAO;
private final Cryptor cryptor;
private final AsyncService asyncService;
@ -749,6 +772,35 @@ class ScreenProctoringAPIBinding {
return;
}
public Collection<GroupSessionCount> getActiveGroupSessionCounts() {
try {
final ScreenProctoringServiceOAuthTemplate apiTemplate = this.getAPITemplate(null);
final String uri = UriComponentsBuilder
.fromUriString(apiTemplate.spsAPIAccessData.getSpsServiceURL())
.path(SPS_API.GROUP_COUNT_ENDPOINT)
.build()
.toUriString();
final ResponseEntity<String> exchange = apiTemplate.exchange(uri, HttpMethod.POST);
if (exchange.getStatusCode() != HttpStatus.OK) {
log.error("Failed to request active group session counts: {}", exchange);
return Collections.emptyList();
}
return this.jsonMapper.readValue(
exchange.getBody(),
new TypeReference<Collection<GroupSessionCount>>() {
});
} catch (final Exception e) {
log.error("Failed to get active group session counts: {}", e.getMessage());
return Collections.emptyList();
}
}
private void synchronizeUserAccount(
final String userUUID,
final ScreenProctoringServiceOAuthTemplate apiTemplate) {
@ -1187,39 +1239,6 @@ class ScreenProctoringAPIBinding {
return apiTemplateExam;
}
// if (this.apiTemplate == null || !this.apiTemplate.isValid(examId)) {
// if (examId != null) {
//
// if (log.isDebugEnabled()) {
// log.debug("Create new ScreenProctoringServiceOAuthTemplate for exam: {}", examId);
// }
//
// final ScreenProctoringSettings settings = this.proctoringSettingsDAO
// .getScreenProctoringSettings(new EntityKey(examId, EntityType.EXAM))
// .getOrThrow();
// this.testConnection(settings).getOrThrow();
// this.apiTemplate = new ScreenProctoringServiceOAuthTemplate(this, settings);
//
// } else if (this.webserviceInfo.getScreenProctoringServiceBundle().bundled) {
//
// if (log.isDebugEnabled()) {
// log.debug("Create new ScreenProctoringServiceOAuthTemplate for exam: {}", examId);
// }
//
// final WebserviceInfo.ScreenProctoringServiceBundle bundle = this.webserviceInfo
// .getScreenProctoringServiceBundle();
//
// this.testConnection(bundle).getOrThrow();
// this.apiTemplate = new ScreenProctoringServiceOAuthTemplate(this, bundle);
//
//
// } else {
// throw new IllegalStateException("No SPS API access information found!");
// }
// }
//
// return this.apiTemplate;
}
private static List<String> getSupporterIds(final Exam exam) {

View file

@ -18,7 +18,6 @@ import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsSetupChangeEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.*;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -244,6 +243,24 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
}
}
@Override
public void updateActiveGroups() {
try {
screenProctoringAPIBinding
.getActiveGroupSessionCounts()
.forEach(groupCount -> {
screenProctoringGroupDAO.updateGroupSize(
groupCount.groupUUID,
groupCount.activeCount,
groupCount.totalCount);
});
} catch (final Exception e) {
log.warn("Failed to update actual group session counts.");
}
}
@Override
public void notifyExamSaved(final Exam exam) {
if (!this.isScreenProctoringEnabled(exam.id)) {