fixed various issues

This commit is contained in:
anhefti 2020-12-10 20:27:43 +01:00
parent 873391394a
commit 4c002b4ac2
19 changed files with 270 additions and 35 deletions

View file

@ -46,7 +46,7 @@ public final class LmsSetup implements GrantEntity, Activatable {
public enum LmsType {
MOCKUP(Features.COURSE_API),
OPEN_EDX(Features.COURSE_API, Features.SEB_RESTRICTION),
MOODLE(Features.COURSE_API, Features.SEB_RESTRICTION);
MOODLE(Features.COURSE_API /* , Features.SEB_RESTRICTION */);
public final EnumSet<Features> features;

View file

@ -27,6 +27,7 @@ public final class LmsSetupTestResult {
public static final String ATTR_MISSING_ATTRIBUTE = "missingLMSSetupAttribute";
public enum ErrorType {
FEATURE_NOT_AVAILABLE,
MISSING_ATTRIBUTE,
TOKEN_REQUEST,
QUIZ_ACCESS_API_REQUEST,
@ -118,6 +119,10 @@ public final class LmsSetupTestResult {
return new LmsSetupTestResult(new Error(ErrorType.QUIZ_RESTRICTION_API_REQUEST, message));
}
public static LmsSetupTestResult ofQuizRestrictionNotAvailable() {
return new LmsSetupTestResult(new Error(ErrorType.FEATURE_NOT_AVAILABLE, "Restriction Feature Not Available"));
}
public final static class Error {
@JsonProperty(ATTR_ERROR_TYPE)

View file

@ -55,6 +55,7 @@ public final class ClientConnection implements GrantEntity {
public static final String FILTER_ATTR_EXAM_ID = Domain.CLIENT_CONNECTION.ATTR_EXAM_ID;
public static final String FILTER_ATTR_STATUS = Domain.CLIENT_CONNECTION.ATTR_STATUS;
public static final String FILTER_ATTR_SESSION_ID = Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_ID;
public static final String FILTER_ATTR_IP_STRING = Domain.CLIENT_CONNECTION.ATTR_CLIENT_ADDRESS;
@JsonProperty(Domain.CLIENT_CONNECTION.ATTR_ID)
public final Long id;

View file

@ -474,7 +474,8 @@ public class ExamForm implements TemplateComposer {
}
final LmsSetupTestResult lmsSetupTestResult = result.get();
return !lmsSetupTestResult.hasError(ErrorType.QUIZ_RESTRICTION_API_REQUEST);
return !lmsSetupTestResult.hasError(ErrorType.QUIZ_RESTRICTION_API_REQUEST)
&& !lmsSetupTestResult.hasError(ErrorType.FEATURE_NOT_AVAILABLE);
}
private void showConsistencyChecks(final Collection<APIMessage> result, final Composite parent) {

View file

@ -0,0 +1,108 @@
/*
* Copyright (c) 2020 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.gui.content;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
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.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionPage;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition.TableFilterAttribute;
import ch.ethz.seb.sebserver.gui.table.EntityTable;
import ch.ethz.seb.sebserver.gui.table.TableFilter.CriteriaType;
@Lazy
@Component
@GuiProfile
public class MonitoringExamSearchPopup {
private static final LocTextKey TITLE_TEXT_KEY =
new LocTextKey("sebserver.monitoring.search.title");
private static final LocTextKey EMPTY_LIST_TEXT_KEY =
new LocTextKey("sebserver.monitoring.search.list.empty");
private static final LocTextKey TABLE_COLUMN_NAME =
new LocTextKey("sebserver.monitoring.search.list.name");
private final PageService pageService;
private final TableFilterAttribute nameFilter =
new TableFilterAttribute(CriteriaType.TEXT, ClientConnection.FILTER_ATTR_SESSION_ID);
protected MonitoringExamSearchPopup(final PageService pageService) {
this.pageService = pageService;
}
public void show(final PageContext pageContext) {
final ModalInputDialog<Void> dialog = new ModalInputDialog<>(
pageContext.getParent().getShell(),
this.pageService.getWidgetFactory());
dialog.setLargeDialogWidth();
dialog.open(
TITLE_TEXT_KEY,
pageContext,
pc -> this.compose(pc, dialog));
}
private void compose(final PageContext pageContext, final ModalInputDialog<Void> dialog) {
final EntityKey examKey = pageContext.getEntityKey();
final RestService restService = this.pageService.getRestService();
final PageActionBuilder actionBuilder = this.pageService
.pageActionBuilder(pageContext.clearEntityKeys());
final EntityTable<ClientConnection> table =
this.pageService.entityTableBuilder(restService.getRestCall(GetClientConnectionPage.class))
.withEmptyMessage(EMPTY_LIST_TEXT_KEY)
.withPaging(10)
.withStaticFilter(ClientConnection.FILTER_ATTR_EXAM_ID, examKey.modelId)
.withColumn(new ColumnDefinition<>(
Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_ID,
TABLE_COLUMN_NAME,
ClientConnection::getUserSessionId)
.withFilter(this.nameFilter))
.withDefaultAction(t -> actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION)
.withParentEntityKey(examKey)
.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

@ -125,16 +125,18 @@ public class MonitoringRunningExam implements TemplateComposer {
private final ResourceService resourceService;
private final InstructionProcessor instructionProcessor;
private final GuiServiceInfo guiServiceInfo;
private final MonitoringExamSearchPopup monitoringExamSearchPopup;
private final ProctorRoomConnectionsPopup proctorRoomConnectionsPopup;
private final long pollInterval;
private final long proctoringRoomUpdateInterval;
private final String remoteProctoringEndpoint;
private final ProctorRoomConnectionsPopup proctorRoomConnectionsPopup;
protected MonitoringRunningExam(
final ServerPushService serverPushService,
final PageService pageService,
final InstructionProcessor instructionProcessor,
final GuiServiceInfo guiServiceInfo,
final MonitoringExamSearchPopup monitoringExamSearchPopup,
final ProctorRoomConnectionsPopup proctorRoomConnectionsPopup,
@Value("${sebserver.gui.webservice.poll-interval:1000}") final long pollInterval,
@Value("${sebserver.gui.remote.proctoring.entrypoint:/remote-proctoring}") final String remoteProctoringEndpoint,
@ -146,6 +148,7 @@ public class MonitoringRunningExam implements TemplateComposer {
this.instructionProcessor = instructionProcessor;
this.guiServiceInfo = guiServiceInfo;
this.pollInterval = pollInterval;
this.monitoringExamSearchPopup = monitoringExamSearchPopup;
this.remoteProctoringEndpoint = remoteProctoringEndpoint;
this.proctorRoomConnectionsPopup = proctorRoomConnectionsPopup;
this.proctoringRoomUpdateInterval = proctoringRoomUpdateInterval;
@ -243,6 +246,12 @@ public class MonitoringRunningExam implements TemplateComposer {
.noEventPropagation()
.publishIf(privilege)
.newAction(ActionDefinition.MONITORING_EXAM_SEARCH_CONNECTIONS)
.withEntityKey(entityKey)
.withExec(this::openSearchPopup)
.noEventPropagation()
.publishIf(privilege)
.newAction(ActionDefinition.MONITOR_EXAM_QUIT_SELECTED)
.withEntityKey(entityKey)
.withConfirm(() -> CONFIRM_QUIT_SELECTED)
@ -391,6 +400,11 @@ public class MonitoringRunningExam implements TemplateComposer {
return townhall != null && townhall.id != null;
}
private PageAction openSearchPopup(final PageAction action) {
this.monitoringExamSearchPopup.show(action.pageContext());
return action;
}
private PageAction toggleTownhallRoom(final PageAction action) {
if (isTownhallRoomActive(action.getEntityKey().modelId)) {
closeTownhallRoom(action);

View file

@ -699,6 +699,17 @@ public enum ActionDefinition {
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.FILTER),
MONITORING_EXAM_SEARCH_CONNECTIONS(
new LocTextKey("sebserver.monitoring.search.action"),
ImageIcon.SEARCH,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.FORM),
MONITORING_EXAM_SEARCH_VIEW_CONNECTION(
new LocTextKey("sebserver.monitoring.search.action.view"),
ImageIcon.SEARCH,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.CLIENT_EVENT_LIST),
MONITOR_EXAM_NEW_PROCTOR_ROOM(
new LocTextKey("sebserver.monitoring.exam.action.newroom"),
ImageIcon.VISIBILITY,

View file

@ -170,16 +170,16 @@ public class ModalInputDialog<T> extends Dialog {
final Consumer<PageContext> contentComposer) {
// Create the info dialog window
final Shell shell = new Shell(getParent(), getStyle());
shell.setText(getText());
shell.setData(RWT.CUSTOM_VARIANT, CustomVariant.MESSAGE.key);
shell.setText(this.widgetFactory.getI18nSupport().getText(title));
shell.setLayout(new GridLayout());
this.shell = new Shell(getParent(), getStyle());
this.shell.setText(getText());
this.shell.setData(RWT.CUSTOM_VARIANT, CustomVariant.MESSAGE.key);
this.shell.setText(this.widgetFactory.getI18nSupport().getText(title));
this.shell.setLayout(new GridLayout());
final GridData gridData2 = new GridData(SWT.FILL, SWT.TOP, true, true);
shell.setLayoutData(gridData2);
this.shell.setLayoutData(gridData2);
final Composite main = new Composite(shell, SWT.NONE);
final Composite main = new Composite(this.shell, SWT.NONE);
main.setLayout(new GridLayout());
final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true);
gridData.widthHint = this.dialogWidth;
@ -188,13 +188,19 @@ public class ModalInputDialog<T> extends Dialog {
contentComposer.accept(pageContext.copyOf(main));
gridData.heightHint = calcDialogHeight(main);
final Button close = this.widgetFactory.buttonLocalized(shell, CLOSE_TEXT_KEY);
final Button close = this.widgetFactory.buttonLocalized(this.shell, CLOSE_TEXT_KEY);
final GridData data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
data.widthHint = this.buttonWidth;
close.setLayoutData(data);
close.addListener(SWT.Selection, event -> shell.close());
close.addListener(SWT.Selection, event -> this.shell.close());
finishUp(shell);
finishUp(this.shell);
}
public void close() {
if (this.shell != null) {
this.shell.close();
}
}
private void finishUp(final Shell shell) {

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2020 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.gui.service.remote.webservice.api.session;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class GetClientConnectionPage extends RestCall<Page<ClientConnection>> {
public GetClientConnectionPage() {
super(new TypeKey<>(
CallType.GET_PAGE,
EntityType.CLIENT_CONNECTION,
new TypeReference<Page<ClientConnection>>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,
API.SEB_CLIENT_CONNECTION_ENDPOINT);
}
}

View file

@ -205,6 +205,14 @@ public class FilterMap extends POSTMapper {
return getString(ClientConnection.FILTER_ATTR_STATUS);
}
public String getClientConnectionUserId() {
return getSQLWildcard(ClientConnection.FILTER_ATTR_SESSION_ID);
}
public String getClientConnectionIPAddress() {
return getSQLWildcard(ClientConnection.FILTER_ATTR_IP_STRING);
}
public Long getClientEventConnectionId() {
return getLong(ClientEvent.FILTER_ATTR_CONNECTION_ID);
}

View file

@ -99,6 +99,12 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
.and(
ClientConnectionRecordDynamicSqlSupport.status,
isEqualToWhenPresent(filterMap.getClientConnectionStatus()))
.and(
ClientConnectionRecordDynamicSqlSupport.examUserSessionId,
isLikeWhenPresent(filterMap.getClientConnectionUserId()))
.and(
ClientConnectionRecordDynamicSqlSupport.clientAddress,
isLikeWhenPresent(filterMap.getClientConnectionIPAddress()))
.build()
.execute()
.stream()

View file

@ -118,7 +118,11 @@ public class LmsAPIServiceImpl implements LmsAPIService {
return testCourseAccessAPI;
}
return template.testCourseRestrictionAPI();
if (template.lmsSetup().getLmsType().features.contains(LmsSetup.Features.SEB_RESTRICTION)) {
return template.testCourseRestrictionAPI();
} else {
return LmsSetupTestResult.ofQuizRestrictionNotAvailable();
}
}
@Override

View file

@ -149,7 +149,7 @@ public class MoodleCourseAccess extends CourseAccess {
if (restTemplateRequest.hasError()) {
final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " +
this.moodleRestTemplateFactory.knownTokenAccessPaths;
log.error(message, restTemplateRequest.getError().getMessage());
log.error(message + " cause: ", restTemplateRequest.getError());
return LmsSetupTestResult.ofTokenRequestError(message);
}
@ -254,13 +254,15 @@ public class MoodleCourseAccess extends CourseAccess {
CourseQuizData.class);
final Map<String, CourseData> finalCourseDataRef = courseData;
courseQuizData.quizzes
.forEach(quiz -> {
final CourseData course = finalCourseDataRef.get(quiz.course);
if (course != null) {
course.quizzes.add(quiz);
}
});
if (courseQuizData.quizzes != null) {
courseQuizData.quizzes
.forEach(quiz -> {
final CourseData course = finalCourseDataRef.get(quiz.course);
if (course != null) {
course.quizzes.add(quiz);
}
});
}
return courseData.values()
.stream()
@ -273,12 +275,17 @@ public class MoodleCourseAccess extends CourseAccess {
}
private Predicate<CourseData> getCourseFilter(final long from) {
final long now = DateTime.now(DateTimeZone.UTC).getMillis();
return course -> course.time_created == null
|| course.time_created.longValue() > from
|| (course.end_date == null
|| (course.end_date <= 0
|| course.end_date > now));
final long now = DateTime.now(DateTimeZone.UTC).getMillis() / 1000;
return course -> {
if (course.end_date != null && course.end_date > 0 && course.end_date < now) {
return false;
}
if (course.time_created != null && course.time_created.longValue() < from) {
return false;
}
return true;
};
}
private Collection<CourseData> getCoursesPage(
@ -287,7 +294,7 @@ public class MoodleCourseAccess extends CourseAccess {
final int size) throws JsonParseException, JsonMappingException, IOException {
try {
final long aYearAgo = DateTime.now(DateTimeZone.UTC).minusYears(1).getMillis();
final long aYearAgo = DateTime.now(DateTimeZone.UTC).minusYears(1).getMillis() / 1000;
// get course ids per page
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
attributes.add(MOODLE_COURSE_API_SEARCH_CRITERIA_NAME, "search");

View file

@ -64,7 +64,6 @@ public class MoodleCourseRestriction {
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_CREATE = "seb_restriction_create";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_UPDATE = "seb_restriction_update";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_DELETE = "seb_restriction_delete";
//private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_COURSE_ID = "courseId";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_SHORT_NAME = "shortname";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_ID_NUMBER = "idnumber";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_QUIZ_ID = "quizId";

View file

@ -29,6 +29,7 @@ import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.NoSEBRestrictionException;
public class MoodleLmsAPITemplate implements LmsAPITemplate {
@ -60,7 +61,8 @@ public class MoodleLmsAPITemplate implements LmsAPITemplate {
@Override
public LmsSetupTestResult testCourseRestrictionAPI() {
return this.moodleCourseRestriction.initAPIAccess();
throw new NoSEBRestrictionException();
//return this.moodleCourseRestriction.initAPIAccess();
}
@Override

View file

@ -20,6 +20,8 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@ -50,6 +52,8 @@ import ch.ethz.seb.sebserver.gbl.util.Utils;
class MoodleRestTemplateFactory {
private static final Logger log = LoggerFactory.getLogger(MoodleRestTemplateFactory.class);
final JSONMapper jsonMapper;
final LmsSetup lmsSetup;
final ClientCredentials credentials;
@ -113,6 +117,12 @@ class MoodleRestTemplateFactory {
return this.knownTokenAccessPaths
.stream()
.map(this::createRestTemplate)
.map(result -> {
if (result.hasError()) {
log.error("Failed to get access token: ", result.getError());
}
return result;
})
.filter(Result::hasValue)
.findFirst()
.orElse(Result.ofRuntimeError(

View file

@ -61,7 +61,7 @@ public abstract class AbstractLogLevelCountIndicator extends AbstractLogIndicato
return errors.doubleValue();
} catch (final Exception e) {
log.error("Failed to get indicator count from persistent storage: ", e);
return this.currentValue;
return 0;
}
}

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator;
import static org.mybatis.dynamic.sql.SqlBuilder.*;
import java.math.BigDecimal;
import java.util.List;
import org.mybatis.dynamic.sql.SqlBuilder;
@ -64,12 +65,17 @@ public abstract class AbstractLogNumberIndicator extends AbstractLogIndicator {
.execute();
if (execute == null || execute.isEmpty()) {
return this.currentValue;
return 0;
}
return execute.get(execute.size() - 1).getNumericValue().doubleValue();
final BigDecimal numericValue = execute.get(execute.size() - 1).getNumericValue();
if (numericValue != null) {
return numericValue.doubleValue();
} else {
return 0;
}
} catch (final Exception e) {
log.error("Failed to get indicator number from persistent storage: ", e);
log.error("Failed to get indicator number from persistent storage: {}", e.getMessage());
return this.currentValue;
}
}

View file

@ -1498,6 +1498,12 @@ sebserver.monitoring.connection.form.status.tooltip=The current connection statu
sebserver.monitoring.connection.form.exam=Exam
sebserver.monitoring.connection.form.exam.tooltip=The exam name
sebserver.monitoring.search.title=Search Connections
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.exam.connection.emptySelection=At first please select a Connection from the list
sebserver.monitoring.exam.connection.emptySelection.active=At first please select an active Connection from the list
sebserver.monitoring.exam.connection.title=SEB Client Connection