Merge remote-tracking branch 'origin/rel-1.5.1'

Conflicts:
	README.rst
This commit is contained in:
anhefti 2023-06-19 14:42:53 +02:00
commit ac880674d0
30 changed files with 179 additions and 77 deletions

View file

@ -101,6 +101,11 @@ Docker-Image:
- Exact release version: docker pull anhefti/seb-server:v1.5.0 (sha256:21d62e24dd5cf697ab5f2b437dc458e6c7492ea294f77a424d39d05164d6c8cc)
- Latest stable minor version with patches: docker pull anhefti/seb-server:v1.5-stable
Latest Version is 1.5.1 with Docker-Image:
- Exact release version: docker pull anhefti/seb-server:v1.5.1 (sha256:a866faa18848d15301e9f06d17aab1c7293d2a27d967038d32410f817e478408)
- Latest stable minor version with patches: docker pull anhefti/seb-server:v1.5-latest
SEB - SEB Server Compatibility
------------------------------

View file

@ -18,7 +18,7 @@
<packaging>jar</packaging>
<properties>
<sebserver-version>1.5.0</sebserver-version>
<sebserver-version>1.5.1</sebserver-version>
<build-version>${sebserver-version}</build-version>
<revision>${sebserver-version}</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View file

@ -76,7 +76,7 @@ public class ClientHttpRequestFactoryService {
final ClientCredentialService clientCredentialService,
@Value("${sebserver.http.client.connect-timeout:15000}") final int connectTimeout,
@Value("${sebserver.http.client.connection-request-timeout:20000}") final int connectionRequestTimeout,
@Value("${sebserver.http.client.read-timeout:20000}") final int readTimeout) {
@Value("${sebserver.http.client.read-timeout:30000}") final int readTimeout) {
this.environment = environment;
this.clientCredentialService = clientCredentialService;

View file

@ -16,6 +16,7 @@ import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.text.StringEscapeUtils;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
@ -161,7 +162,7 @@ public class ExamDeletePopup {
new ActionEvent(action),
action.pageContext());
final String examName = examToDelete.toName().name;
final String examName = StringEscapeUtils.escapeXml11(examToDelete.toName().name);
final List<EntityKey> dependencies = report.results.stream()
.filter(key -> !key.equals(entityKey))
.collect(Collectors.toList());

View file

@ -59,6 +59,7 @@ 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;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.ImageIcon;
@Lazy
@Component
@ -109,6 +110,8 @@ public class QuizLookupList implements TemplateComposer {
new LocTextKey("sebserver.quizdiscovery.quiz.import.existing");
private final static LocTextKey TEXT_FETCH_NOTE =
new LocTextKey("sebserver.quizdiscovery.list.fetchnote");
private final static LocTextKey TEXT_FETCH_NOTE_TOOLTIP =
new LocTextKey("sebserver.quizdiscovery.list.fetchnote.tooltip");
private final static String TEXT_KEY_ADDITIONAL_ATTR_PREFIX =
"sebserver.quizdiscovery.quiz.details.additional.";
@ -448,7 +451,7 @@ public class QuizLookupList implements TemplateComposer {
}
}
private boolean showingFetchNote = false;
private Composite warningPanel = null;
private void handelPageReload(
final Composite notePanel,
@ -456,29 +459,28 @@ public class QuizLookupList implements TemplateComposer {
if (table.isComplete()) {
PageService.clearComposite(notePanel);
this.showingFetchNote = false;
} else {
if (!this.showingFetchNote) {
final Composite warningPanel = this.widgetFactory.createWarningPanel(notePanel, 15, true);
GridData gridData = new GridData(SWT.CENTER, SWT.CENTER, false, true);
gridData.heightHint = 28;
gridData.widthHint = 25;
gridData.verticalIndent = 5;
final Label action = new Label(warningPanel, SWT.NONE);
action.setImage(WidgetFactory.ImageIcon.SWITCH.getImage(notePanel.getDisplay()));
action.setLayoutData(gridData);
action.addListener(SWT.MouseDown, event -> {
table.applyFilter();
});
final Label text = new Label(warningPanel, SWT.NONE);
text.setData(RWT.MARKUP_ENABLED, Boolean.TRUE);
text.setText(this.pageService.getI18nSupport().getText(TEXT_FETCH_NOTE));
gridData = new GridData(SWT.LEFT, SWT.FILL, true, true);
gridData.heightHint = 16;
text.setLayoutData(gridData);
this.showingFetchNote = true;
if (this.warningPanel != null) {
this.warningPanel.dispose();
}
this.warningPanel = null;
} else {
if (this.warningPanel != null && !this.warningPanel.isDisposed()) {
this.warningPanel.dispose();
}
this.warningPanel = this.widgetFactory.createWarningPanel(notePanel, 15, true);
this.widgetFactory.imageButton(
ImageIcon.SWITCH,
this.warningPanel,
TEXT_FETCH_NOTE_TOOLTIP,
event -> table.applyFilter());
final Label text = new Label(this.warningPanel, SWT.NONE);
text.setData(RWT.MARKUP_ENABLED, Boolean.TRUE);
text.setText(this.pageService.getI18nSupport().getText(TEXT_FETCH_NOTE));
final GridData gridData = new GridData(SWT.LEFT, SWT.FILL, true, true);
gridData.heightHint = 28;
text.setLayoutData(gridData);
}
notePanel.getParent().layout(true, true);
}

View file

@ -18,6 +18,7 @@ import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import org.apache.commons.text.StringEscapeUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
@ -151,7 +152,9 @@ public class MonitoringRunningExam implements TemplateComposer {
final Composite content = this.pageService.getWidgetFactory().defaultPageLayout(
pageContext.getParent(),
new LocTextKey("sebserver.monitoring.exam", exam.name));
new LocTextKey(
"sebserver.monitoring.exam",
StringEscapeUtils.escapeXml11(exam.name)));
final Composite tablePane = new Composite(content, SWT.NONE);
tablePane.setLayout(new GridLayout());

View file

@ -270,7 +270,16 @@ public final class PolyglotPageServiceImpl implements PolyglotPageService {
return label -> {
if (locTextKey != null) {
label.setText(i18nSupport.getText(locTextKey));
try {
label.setText(i18nSupport.getText(locTextKey));
} catch (final Exception e) {
label.setData(RWT.MARKUP_ENABLED, false);
try {
label.setText(i18nSupport.getText(locTextKey));
} catch (final Exception ee) {
label.setText(locTextKey.name);
}
}
}
if (i18nSupport.hasText(locToolTipKey)) {
label.setToolTipText(Utils.formatLineBreaks(i18nSupport.getText(locToolTipKey)));

View file

@ -185,6 +185,9 @@ public final class PageAction {
} catch (final PageMessageException pme) {
PageAction.this.pageContext.publishPageMessage(pme);
return;
} catch (final Exception e) {
this.pageContext.notifyUnexpectedError(e);
return;
}
} else {
callback.accept(exec());

View file

@ -56,14 +56,14 @@ public final class UpdateErrorHandler implements Function<Exception, Boolean> {
} 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(), error);
log.warn("Failed to update server push: {}", error.getMessage());
if (this.errors > 5) {
checkUserSession();
}

View file

@ -180,7 +180,7 @@ public abstract class RestCall<T> {
e,
"NO RESPONSE AVAILABLE",
String.valueOf(builder)));
return Result.ofError(e);
return Result.ofError(restCallError);
}
}
@ -216,6 +216,10 @@ public abstract class RestCall<T> {
} else {
restCallError.errors.add(APIMessage.ErrorMessage.GENERIC.of(responseEntity.getBody()));
}
} catch (final Exception e) {
final String body = responseEntity.getBody();
log.error("Failed to parse rest response error message: {}", body);
throw e;
}
log.debug(

View file

@ -371,7 +371,6 @@ public final class ClientConnectionTable implements FullPageMonitoringGUIUpdate
}
public void updateGUI() {
if (this.needsSort) {
sortTable();
}
@ -725,10 +724,12 @@ public final class ClientConnectionTable implements FullPageMonitoringGUIUpdate
this.indicatorWeights[i] = -1;
}
}
this.monitoringData = monitoringData;
if (this.indicatorValueChanged) {
updateIndicatorWeight();
}
this.monitoringData = monitoringData;
return this.staticData == null
|| this.staticData == ClientStaticData.NULL_DATA

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.gui.widget;
import java.util.Locale;
import org.apache.commons.text.StringEscapeUtils;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.rap.rwt.widgets.DialogCallback;
import org.eclipse.swt.graphics.Rectangle;
@ -17,7 +18,6 @@ import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Shell;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant;
@ -46,7 +46,7 @@ public final class Message extends MessageBox {
super.prepareOpen();
} catch (final IllegalArgumentException e) {
// fallback on markup text error
super.setMessage(Utils.escapeHTML_XML_EcmaScript(super.getMessage()));
super.setMessage(StringEscapeUtils.escapeXml11(super.getMessage()));
super.prepareOpen();
}
final GridLayout layout = (GridLayout) super.shell.getLayout();

View file

@ -814,10 +814,13 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
.selectByExample()
.where(
ClientConnectionRecordDynamicSqlSupport.status,
SqlBuilder.isIn(ClientConnection.SECURE_CHECK_STATES))
SqlBuilder.isEqualTo(ConnectionStatus.ACTIVE.name()))
.and(
ClientConnectionRecordDynamicSqlSupport.examId,
SqlBuilder.isEqualTo(examId))
.and(
ClientConnectionRecordDynamicSqlSupport.clientVersion,
SqlBuilder.isNotNull())
.and(
ClientConnectionRecordDynamicSqlSupport.clientVersionGranted,
SqlBuilder.isNull())

View file

@ -22,6 +22,7 @@ import java.util.stream.Collectors;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
@ -53,16 +54,19 @@ public class QuizLookupServiceImpl implements QuizLookupService {
private final UserService userService;
private final LmsSetupDAO lmsSetupDAO;
private final AsyncRunner asyncRunner;
private final long fetchedDataValiditySeconds;
public QuizLookupServiceImpl(
final UserService userService,
final LmsSetupDAO lmsSetupDAO,
final AsyncService asyncService,
final Environment environment) {
final Environment environment,
@Value("${sebserver.webservice.lms.datafetch.validity.seconds:600}") final long fetchedDataValiditySeconds) {
this.userService = userService;
this.lmsSetupDAO = lmsSetupDAO;
this.asyncRunner = asyncService.getAsyncRunner();
this.fetchedDataValiditySeconds = fetchedDataValiditySeconds;
}
@Override
@ -158,7 +162,10 @@ public class QuizLookupServiceImpl implements QuizLookupService {
}
if (!asyncLookup.isValid(filterMap)) {
this.lookups.remove(userId);
final AsyncLookup removed = this.lookups.remove(userId);
if (removed != null) {
removed.cancel();
}
this.createNewAsyncLookup(userId, filterMap, lmsAPITemplateSupplier);
}
@ -198,7 +205,12 @@ public class QuizLookupServiceImpl implements QuizLookupService {
}
final LookupFilterCriteria criteria = new LookupFilterCriteria(filterMap);
final AsyncLookup asyncLookup = new AsyncLookup(userInstitutionId, userId, criteria, buffers);
final AsyncLookup asyncLookup = new AsyncLookup(
userInstitutionId,
userId,
criteria,
buffers,
this.fetchedDataValiditySeconds);
if (log.isDebugEnabled()) {
log.debug("Create new AsyncLookup: user={} criteria={}", userId, criteria);
@ -278,18 +290,21 @@ public class QuizLookupServiceImpl implements QuizLookupService {
final Collection<AsyncQuizFetchBuffer> asyncBuffers;
final long timeCreated;
long timeCompleted = Long.MAX_VALUE;
private final long fetchedDataValiditySeconds;
public AsyncLookup(
final long institutionId,
final String userId,
final LookupFilterCriteria lookupFilterCriteria,
final Collection<AsyncQuizFetchBuffer> asyncBuffers) {
final Collection<AsyncQuizFetchBuffer> asyncBuffers,
final long fetchedDataValiditySeconds) {
this.institutionId = institutionId;
this.userId = userId;
this.lookupFilterCriteria = lookupFilterCriteria;
this.asyncBuffers = asyncBuffers;
this.timeCreated = Utils.getMillisecondsNow();
this.fetchedDataValiditySeconds = fetchedDataValiditySeconds;
}
LookupResult getAvailable() {
@ -307,10 +322,7 @@ public class QuizLookupServiceImpl implements QuizLookupService {
boolean isUpToDate() {
final long now = Utils.getMillisecondsNow();
if (now - this.timeCreated > 5 * Constants.MINUTE_IN_MILLIS) {
return false;
}
if (now - this.timeCompleted > Constants.MINUTE_IN_MILLIS) {
if (now - this.timeCreated > this.fetchedDataValiditySeconds * Constants.SECOND_IN_MILLIS) {
return false;
}
return true;

View file

@ -63,7 +63,8 @@ public class MockCourseAccessAPI implements CourseAccessAPI {
"quiz1", institutionId, lmsSetupId, lmsType, "Demo Quiz 1 (MOCKUP)", "<p>Demo Quiz Mockup</p>",
"2020-01-01T09:00:00Z", null, "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz2", institutionId, lmsSetupId, lmsType, "Demo Quiz 2 (MOCKUP)", "<p>Demo Quiz Mockup</p>",
"quiz2 äöüèÜÄÖ ?<", institutionId, lmsSetupId, lmsType, "Demo Quiz 2 (MOCKUP) äöüèÜÄÖ ?< ",
"<p>Demo Quiz Mockup</p>",
"2020-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz3", institutionId, lmsSetupId, lmsType, "Demo Quiz 3 (MOCKUP)", "<p>Demo Quiz Mockup</p>",

View file

@ -61,6 +61,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.Courses;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CoursesPlugin;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.MoodleUserDetails;
import io.micrometer.core.instrument.util.StringUtils;
public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess implements CourseAccessAPI {
@ -81,6 +82,9 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
public static final String PARAM_PAGE_START = "startneedle";
public static final String PARAM_PAGE_SIZE = "perpage";
public static final String SQL_QUIZ_NAME = "m.name";
public static final String SQL_COURSE_NAME = "shortname";
public static final String SQL_CONDITION_TEMPLATE =
//"(startdate >= %s or timecreated >=%s) and (enddate is null or enddate = 0 or enddate >= %s)";
"(startdate is null OR startdate = 0 OR startdate >= %s) AND (enddate is null or enddate = 0 OR enddate >= %s)";
@ -92,6 +96,7 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
private final int pageSize;
private final int maxSize;
private final int cutoffTimeOffset;
private final boolean applyNameCriteria;
private MoodleAPIRestTemplate restTemplate;
@ -100,11 +105,13 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
final AsyncService asyncService,
final MoodleRestTemplateFactory restTemplateFactory,
final CacheManager cacheManager,
final Environment environment) {
final Environment environment,
final boolean applyNameCriteria) {
super(cacheManager);
this.jsonMapper = jsonMapper;
this.restTemplateFactory = restTemplateFactory;
this.applyNameCriteria = applyNameCriteria;
this.prependShortCourseName = BooleanUtils.toBoolean(environment.getProperty(
"sebserver.webservice.lms.moodle.prependShortCourseName",
@ -118,7 +125,7 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
environment.getProperty(
"sebserver.webservice.circuitbreaker.moodleRestCall.blockingTime",
Long.class,
Constants.SECOND_IN_MILLIS * 20),
Constants.SECOND_IN_MILLIS * 30),
environment.getProperty(
"sebserver.webservice.circuitbreaker.moodleRestCall.timeToRecover",
Long.class,
@ -184,10 +191,11 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
quizFromTime = DateTime.now(DateTimeZone.UTC).minusYears(this.cutoffTimeOffset);
}
final Predicate<QuizData> quizFilter = LmsAPIService.quizFilterPredicate(filterMap);
final String quizName = filterMap.getQuizName();
while (!asyncQuizFetchBuffer.finished && !asyncQuizFetchBuffer.canceled) {
try {
fetchQuizzesPage(page, quizFromTime, asyncQuizFetchBuffer, quizFilter);
fetchQuizzesPage(page, quizFromTime, quizName, asyncQuizFetchBuffer, quizFilter);
page++;
} catch (final Exception e) {
log.error("Unexpected error while trying to fetch moodle quiz page: {}", page, e);
@ -371,6 +379,7 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
private void fetchQuizzesPage(
final int page,
final DateTime quizFromTime,
final String nameCondition,
final AsyncQuizFetchBuffer asyncQuizFetchBuffer,
final Predicate<QuizData> quizFilter) throws JsonParseException, JsonMappingException, IOException {
@ -382,7 +391,7 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
final Collection<CourseData> fetchCoursesPage =
fetchCoursesPage(restTemplate, quizFromTime, page, this.pageSize);
fetchCoursesPage(restTemplate, quizFromTime, nameCondition, page, this.pageSize);
// finish if page is empty (no courses left
if (fetchCoursesPage.isEmpty()) {
asyncQuizFetchBuffer.finish();
@ -408,6 +417,7 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
private Collection<CourseData> fetchCoursesPage(
final MoodleAPIRestTemplate restTemplate,
final DateTime quizFromTime,
final String nameCondition,
final int page,
final int size) throws JsonParseException, JsonMappingException, IOException {
@ -422,13 +432,25 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
final long defaultCutOff = Utils.toUnixTimeInSeconds(
DateTime.now(DateTimeZone.UTC).minusYears(this.cutoffTimeOffset));
final long cutoffDate = (filterDate < defaultCutOff) ? filterDate : defaultCutOff;
final String sqlCondition = String.format(
String sqlCondition = String.format(
SQL_CONDITION_TEMPLATE,
String.valueOf(cutoffDate),
String.valueOf(filterDate));
final String fromElement = String.valueOf(page * size);
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
if (this.applyNameCriteria && StringUtils.isNotBlank(nameCondition)) {
sqlCondition = sqlCondition + " AND (" +
SQL_QUIZ_NAME +
" LIKE '" +
Utils.toSQLWildcard(nameCondition) +
"' OR " +
SQL_COURSE_NAME +
" LIKE '" +
Utils.toSQLWildcard(nameCondition) +
"')";
}
// Note: courseid[]=0 means all courses. Moodle don't like empty parameter
attributes.add(PARAM_COURSE_ID_ARRAY, "0");
attributes.add(PARAM_SQL_CONDITIONS, sqlCondition);

View file

@ -46,6 +46,7 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
private final ExamConfigurationValueService examConfigurationValueService;
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
private final String[] alternativeTokenRequestPaths;
private final boolean applyNameCriteria;
protected MooldePluginLmsAPITemplateFactory(
final JSONMapper jsonMapper,
@ -55,7 +56,8 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
final ClientCredentialService clientCredentialService,
final ExamConfigurationValueService examConfigurationValueService,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
@Value("${sebserver.webservice.lms.moodle.api.token.request.paths:}") final String alternativeTokenRequestPaths) {
@Value("${sebserver.webservice.lms.moodle.api.token.request.paths:}") final String alternativeTokenRequestPaths,
@Value("${sebserver.webservice.lms.moodle.fetch.applyNameCriteria:true}") final boolean applyNameCriteria) {
this.jsonMapper = jsonMapper;
this.cacheManager = cacheManager;
@ -67,6 +69,7 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
? StringUtils.split(alternativeTokenRequestPaths, Constants.LIST_SEPARATOR)
: null;
this.applyNameCriteria = applyNameCriteria;
}
@Override
@ -90,7 +93,8 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
this.asyncService,
moodleRestTemplateFactory,
this.cacheManager,
this.environment);
this.environment,
this.applyNameCriteria);
final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction(
this.jsonMapper,

View file

@ -241,7 +241,9 @@ public class ExamSessionServiceImpl implements ExamSessionService {
flushCache(exam);
}
log.info("Exam {} is not currently running", examId);
if (log.isDebugEnabled()) {
log.info("Exam {} is not currently running", examId);
}
return Result.ofError(new NoSuchElementException(
"No currently running exam found for id: " + examId));
@ -265,8 +267,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
filterMap
.putIfAbsent(Exam.FILTER_ATTR_ACTIVE, Constants.TRUE_STRING)
.putIfAbsent(Exam.FILTER_ATTR_STATUS, ExamStatus.RUNNING.name())
.putIfAbsent(Exam.FILTER_ATTR_HIDE_MISSING, Constants.TRUE_STRING);
.putIfAbsent(Exam.FILTER_ATTR_STATUS, ExamStatus.RUNNING.name());
return this.examDAO.allMatching(filterMap, predicate)
.map(col -> col.stream()
@ -640,12 +641,14 @@ public class ExamSessionServiceImpl implements ExamSessionService {
final ClientConnectionDataInternal cc = this.examSessionCacheService.getClientConnection(token);
if (cc.clientConnection.status.duplicateCheckStatus) {
final Long id = this.duplicateCheck.put(
cc.clientConnection.userSessionId,
cc.getConnectionId());
if (id != null) {
duplicates.add(id);
duplicates.add(cc.getConnectionId());
if (cc.clientConnection.userSessionId != null) {
final Long id = this.duplicateCheck.put(
cc.clientConnection.userSessionId,
cc.getConnectionId());
if (id != null) {
duplicates.add(id);
duplicates.add(cc.getConnectionId());
}
}
}
return cc;

View file

@ -410,11 +410,14 @@ class ExamUpdateHandler {
.getLmsAPITemplate(lmsSetupId)
.getOrThrow();
final Exam exam = exams.get(quizId);
if (!lmsTemplate.getType().features.contains(Features.COURSE_RECOVERY)) {
if (exam.lmsAvailable == null || exam.isLmsAvailable()) {
this.examDAO.markLMSAvailability(quizId, false, updateId);
}
throw new UnsupportedOperationException("No Course Recovery");
}
final Exam exam = exams.get(quizId);
final int attempts = Integer.parseInt(this.additionalAttributesDAO.getAdditionalAttribute(
EntityType.EXAM,
exam.id,

View file

@ -329,7 +329,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
if (StringUtils.isNoneBlank(clientAddress) &&
StringUtils.isNotBlank(clientConnection.clientAddress) &&
!clientAddress.equals(clientConnection.clientAddress)) {
log.error(
log.warn(
"ClientConnection integrity violation: client address mismatch: {}, {}",
clientAddress,
clientConnection.clientAddress);
@ -337,7 +337,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
"ClientConnection integrity violation: client address mismatch");
}
} else if (!clientConnection.status.clientActiveStatus) {
log.error("ClientConnection integrity violation: client connection is not in expected state: {}",
log.warn("ClientConnection integrity violation: client connection is not in expected state: {}",
clientConnection);
throw new IllegalArgumentException(
"ClientConnection integrity violation: client connection is not in expected state");

View file

@ -93,7 +93,7 @@ public class SEBClientNotificationServiceImpl implements SEBClientNotificationSe
this.clientEventDAO.getPendingNotificationByValue(clientConnection.id, notificationId)
.flatMap(notification -> this.clientEventDAO.confirmPendingNotification(notification.id))
.map(this::removeFromCache)
.onError(error -> log.error("Failed to confirm pending notification: {}", event, error));
.getOrThrow();
} catch (final Exception e) {
log.error(
@ -110,8 +110,7 @@ public class SEBClientNotificationServiceImpl implements SEBClientNotificationSe
return this.clientEventDAO.getPendingNotification(notificationId)
.map(notification -> this.confirmClientSide(notification, examId, connectionToken))
.flatMap(notification -> this.clientEventDAO.confirmPendingNotification(notificationId))
.map(this::removeFromCache)
.onError(error -> log.error("Failed to confirm pending notification: {}", notificationId, error));
.map(this::removeFromCache);
}
@Override

View file

@ -326,6 +326,11 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
cc.getExamId(),
cc.getConnectionToken());
if (proctoringRoom == null) {
log.warn("Assign SEB client to proctoring room failed for: {}", cc);
return;
}
if (log.isDebugEnabled()) {
log.debug("Assigning new SEB client to proctoring room: {}, connection: {}",
proctoringRoom.id,

View file

@ -149,6 +149,15 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler {
HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Object> handleIllegalArgumentException(
final IllegalArgumentException ex,
final WebRequest request) {
log.warn("Illegal argument or state detected: {}\n send 400 Bad Request response", ex.getMessage());
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<Object> handleResourceNotFoundException(
final ResourceNotFoundException ex,
@ -182,7 +191,7 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler {
final ExamNotRunningException ex,
final WebRequest request) {
log.info("{}", ex.getMessage());
log.debug("{}", ex.getMessage());
return APIMessage.ErrorMessage.INTEGRITY_VALIDATION
.createErrorResponse(ex.getMessage());
}

View file

@ -425,7 +425,10 @@ public class ExamMonitoringController {
notificationId,
examId,
connectionToken)
.getOrThrow();
.onError(error -> {
log.error("Failed to confirm pending notification: {} for exam {}, cause: {}",
notificationId, examId, error.getMessage());
});
}
@RequestMapping(

View file

@ -18,7 +18,7 @@ spring.datasource.hikari.leakDetectionThreshold=2000
sebserver.http.client.connect-timeout=15000
sebserver.http.client.connection-request-timeout=10000
sebserver.http.client.read-timeout=20000
sebserver.http.client.read-timeout=30000
sebserver.webservice.distributed.updateInterval=1000
sebserver.webservice.distributed.connectionUpdate=2000
sebserver.webservice.clean-db-on-startup=false
@ -52,7 +52,7 @@ sebserver.webservice.api.pagination.maxPageSize=500
sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token
sebserver.webservice.lms.moodle.api.token.request.paths=
sebserver.webservice.lms.address.alias=lms.mockup.com=lms.address.alias
sebserver.webservice.cache.moodle.course.pageSize=10
sebserver.webservice.cache.moodle.course.pageSize=250
springdoc.api-docs.enabled=true
springdoc.swagger-ui.enabled=true

View file

@ -81,8 +81,10 @@ sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token
sebserver.webservice.lms.moodle.api.token.request.paths=/login/token.php
sebserver.webservice.lms.moodle.prependShortCourseName=true
sebserver.webservice.lms.moodle.fetch.cutoffdate.yearsBeforeNow=2
sebserver.webservice.lms.moodle.fetch.applyNameCriteria=true
sebserver.webservice.lms.olat.sendAdditionalAttributesWithRestriction=false
sebserver.webservice.lms.address.alias=
sebserver.webservice.lms.datafetch.validity.seconds=600
sebserver.webservice.proctoring.resetBroadcastOnLeav=true
sebserver.webservice.proctoring.zoom.enableWaitingRoom=false

View file

@ -15,6 +15,12 @@ server.servlet.context-path=/
# Tomcat
server.tomcat.max-threads=2000
server.tomcat.accept-count=300
server.tomcat.socket.soKeepAlive=true
server.tomcat.socket.performanceConnectionTime=1
server.tomcat.socket.performanceLatency=2
server.tomcat.socket.performanceBandwidth=0
server.tomcat.keepAliveTimeout(3000);
server.tomcat.maxKeepAliveRequests(3000);
server.tomcat.uri-encoding=UTF-8
### encoding

View file

@ -448,6 +448,7 @@ sebserver.quizdiscovery.list.column.endtime.tooltip=The end time of the LMS exam
sebserver.quizdiscovery.info.pleaseSelect=At first please select an LMS exam from the list
sebserver.quizdiscovery.list.action.no.modify.privilege=No Access: A LMS exam from other institution cannot be imported.
sebserver.quizdiscovery.list.fetchnote=<b>Note:</b> This list is not complete yet since the service is still fetching data from LMS.<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Use the reload button on the left or the search icon from the list for update.
sebserver.quizdiscovery.list.fetchnote.tooltip=Click to reload the list and get all currently fetched results.
sebserver.quizdiscovery.action.list=LMS Exam Lookup
sebserver.quizdiscovery.action.import=Import as Exam
@ -674,14 +675,14 @@ sebserver.exam.configuration.form.title.new=Add exam configuration mapping
sebserver.exam.configuration.form.title=Exam Configuration Mapping
sebserver.exam.configuration.form.name=Exam Configuration
sebserver.exam.configuration.form.name.tooltip=Please select an exam configuration to attach to the exam
sebserver.exam.configuration.form.encryptSecret=Encryption Password
sebserver.exam.configuration.form.encryptSecret.tooltip=Define an encryption password if the exam configuration should be encrypted by password
sebserver.exam.configuration.form.encryptSecret=Settings Password
sebserver.exam.configuration.form.encryptSecret.tooltip=Define an encryption password if the exam configuration settings should be encrypted by password
sebserver.exam.configuration.form.description=Description
sebserver.exam.configuration.form.description.tooltip=The description of the selected exam configuration
sebserver.exam.configuration.form.status=Status
sebserver.exam.configuration.form.status.tooltip=The current status of the selected exam configuration
sebserver.exam.configuration.form.encryptSecret.confirm=Confirm Password
sebserver.exam.configuration.form.encryptSecret.confirm.tooltip=Please confirm the encryption password if there is one
sebserver.exam.configuration.form.encryptSecret.confirm.tooltip=Please confirm the settings password if there is one
sebserver.exam.indicator.list.actions=&nbsp;
sebserver.exam.indicator.list.title=Indicators

View file

@ -36,12 +36,12 @@ public class ExamAPITest extends AdministrationAPIIntegrationTester {
sebAdminAccess,
sebAdminAccess,
"LmsSetupMock",
"quiz2",
"quiz2 äöüèÜÄÖ ?<",
ExamType.MANAGED,
"user5");
assertNotNull(exam);
assertEquals("quiz2", exam.getExternalId());
assertEquals("quiz2 äöüèÜÄÖ ?<", exam.getExternalId()); // Note cannot set right collation on h2
assertEquals(ExamType.MANAGED, exam.getType());
assertFalse(exam.getSupporter().isEmpty());

View file

@ -324,7 +324,8 @@ public class MoodlePluginCourseAccessTest {
asyncService,
moodleMockupRestTemplateFactory,
new NoOpCacheManager(),
mockEnvironment);
mockEnvironment,
false);
}
}