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) - 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 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 SEB - SEB Server Compatibility
------------------------------ ------------------------------

View file

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

View file

@ -76,7 +76,7 @@ public class ClientHttpRequestFactoryService {
final ClientCredentialService clientCredentialService, final ClientCredentialService clientCredentialService,
@Value("${sebserver.http.client.connect-timeout:15000}") final int connectTimeout, @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.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.environment = environment;
this.clientCredentialService = clientCredentialService; this.clientCredentialService = clientCredentialService;

View file

@ -16,6 +16,7 @@ import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.text.StringEscapeUtils;
import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Label;
@ -161,7 +162,7 @@ public class ExamDeletePopup {
new ActionEvent(action), new ActionEvent(action),
action.pageContext()); action.pageContext());
final String examName = examToDelete.toName().name; final String examName = StringEscapeUtils.escapeXml11(examToDelete.toName().name);
final List<EntityKey> dependencies = report.results.stream() final List<EntityKey> dependencies = report.results.stream()
.filter(key -> !key.equals(entityKey)) .filter(key -> !key.equals(entityKey))
.collect(Collectors.toList()); .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.EntityTable;
import ch.ethz.seb.sebserver.gui.table.TableFilter.CriteriaType; 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;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.ImageIcon;
@Lazy @Lazy
@Component @Component
@ -109,6 +110,8 @@ public class QuizLookupList implements TemplateComposer {
new LocTextKey("sebserver.quizdiscovery.quiz.import.existing"); new LocTextKey("sebserver.quizdiscovery.quiz.import.existing");
private final static LocTextKey TEXT_FETCH_NOTE = private final static LocTextKey TEXT_FETCH_NOTE =
new LocTextKey("sebserver.quizdiscovery.list.fetchnote"); 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 = private final static String TEXT_KEY_ADDITIONAL_ATTR_PREFIX =
"sebserver.quizdiscovery.quiz.details.additional."; "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( private void handelPageReload(
final Composite notePanel, final Composite notePanel,
@ -456,29 +459,28 @@ public class QuizLookupList implements TemplateComposer {
if (table.isComplete()) { if (table.isComplete()) {
PageService.clearComposite(notePanel); PageService.clearComposite(notePanel);
this.showingFetchNote = false; if (this.warningPanel != null) {
} else { this.warningPanel.dispose();
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;
} }
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); 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.Consumer;
import java.util.function.Function; import java.util.function.Function;
import org.apache.commons.text.StringEscapeUtils;
import org.eclipse.swt.SWT; import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.layout.GridLayout;
@ -151,7 +152,9 @@ public class MonitoringRunningExam implements TemplateComposer {
final Composite content = this.pageService.getWidgetFactory().defaultPageLayout( final Composite content = this.pageService.getWidgetFactory().defaultPageLayout(
pageContext.getParent(), 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); final Composite tablePane = new Composite(content, SWT.NONE);
tablePane.setLayout(new GridLayout()); tablePane.setLayout(new GridLayout());

View file

@ -270,7 +270,16 @@ public final class PolyglotPageServiceImpl implements PolyglotPageService {
return label -> { return label -> {
if (locTextKey != null) { 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)) { if (i18nSupport.hasText(locToolTipKey)) {
label.setToolTipText(Utils.formatLineBreaks(i18nSupport.getText(locToolTipKey))); label.setToolTipText(Utils.formatLineBreaks(i18nSupport.getText(locToolTipKey)));

View file

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

View file

@ -56,14 +56,14 @@ public final class UpdateErrorHandler implements Function<Exception, Boolean> {
} catch (final Exception ee) { } catch (final Exception ee) {
log.warn("Unable to auto-logout: ", ee.getMessage()); log.warn("Unable to auto-logout: ", ee.getMessage());
} }
return true; return false;
} }
} }
@Override @Override
public Boolean apply(final Exception error) { public Boolean apply(final Exception error) {
this.errors++; 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) { if (this.errors > 5) {
checkUserSession(); checkUserSession();
} }

View file

@ -180,7 +180,7 @@ public abstract class RestCall<T> {
e, e,
"NO RESPONSE AVAILABLE", "NO RESPONSE AVAILABLE",
String.valueOf(builder))); String.valueOf(builder)));
return Result.ofError(e); return Result.ofError(restCallError);
} }
} }
@ -216,6 +216,10 @@ public abstract class RestCall<T> {
} else { } else {
restCallError.errors.add(APIMessage.ErrorMessage.GENERIC.of(responseEntity.getBody())); 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( log.debug(

View file

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

View file

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

View file

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

View file

@ -22,6 +22,7 @@ import java.util.stream.Collectors;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -53,16 +54,19 @@ public class QuizLookupServiceImpl implements QuizLookupService {
private final UserService userService; private final UserService userService;
private final LmsSetupDAO lmsSetupDAO; private final LmsSetupDAO lmsSetupDAO;
private final AsyncRunner asyncRunner; private final AsyncRunner asyncRunner;
private final long fetchedDataValiditySeconds;
public QuizLookupServiceImpl( public QuizLookupServiceImpl(
final UserService userService, final UserService userService,
final LmsSetupDAO lmsSetupDAO, final LmsSetupDAO lmsSetupDAO,
final AsyncService asyncService, final AsyncService asyncService,
final Environment environment) { final Environment environment,
@Value("${sebserver.webservice.lms.datafetch.validity.seconds:600}") final long fetchedDataValiditySeconds) {
this.userService = userService; this.userService = userService;
this.lmsSetupDAO = lmsSetupDAO; this.lmsSetupDAO = lmsSetupDAO;
this.asyncRunner = asyncService.getAsyncRunner(); this.asyncRunner = asyncService.getAsyncRunner();
this.fetchedDataValiditySeconds = fetchedDataValiditySeconds;
} }
@Override @Override
@ -158,7 +162,10 @@ public class QuizLookupServiceImpl implements QuizLookupService {
} }
if (!asyncLookup.isValid(filterMap)) { 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); this.createNewAsyncLookup(userId, filterMap, lmsAPITemplateSupplier);
} }
@ -198,7 +205,12 @@ public class QuizLookupServiceImpl implements QuizLookupService {
} }
final LookupFilterCriteria criteria = new LookupFilterCriteria(filterMap); 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()) { if (log.isDebugEnabled()) {
log.debug("Create new AsyncLookup: user={} criteria={}", userId, criteria); log.debug("Create new AsyncLookup: user={} criteria={}", userId, criteria);
@ -278,18 +290,21 @@ public class QuizLookupServiceImpl implements QuizLookupService {
final Collection<AsyncQuizFetchBuffer> asyncBuffers; final Collection<AsyncQuizFetchBuffer> asyncBuffers;
final long timeCreated; final long timeCreated;
long timeCompleted = Long.MAX_VALUE; long timeCompleted = Long.MAX_VALUE;
private final long fetchedDataValiditySeconds;
public AsyncLookup( public AsyncLookup(
final long institutionId, final long institutionId,
final String userId, final String userId,
final LookupFilterCriteria lookupFilterCriteria, final LookupFilterCriteria lookupFilterCriteria,
final Collection<AsyncQuizFetchBuffer> asyncBuffers) { final Collection<AsyncQuizFetchBuffer> asyncBuffers,
final long fetchedDataValiditySeconds) {
this.institutionId = institutionId; this.institutionId = institutionId;
this.userId = userId; this.userId = userId;
this.lookupFilterCriteria = lookupFilterCriteria; this.lookupFilterCriteria = lookupFilterCriteria;
this.asyncBuffers = asyncBuffers; this.asyncBuffers = asyncBuffers;
this.timeCreated = Utils.getMillisecondsNow(); this.timeCreated = Utils.getMillisecondsNow();
this.fetchedDataValiditySeconds = fetchedDataValiditySeconds;
} }
LookupResult getAvailable() { LookupResult getAvailable() {
@ -307,10 +322,7 @@ public class QuizLookupServiceImpl implements QuizLookupService {
boolean isUpToDate() { boolean isUpToDate() {
final long now = Utils.getMillisecondsNow(); final long now = Utils.getMillisecondsNow();
if (now - this.timeCreated > 5 * Constants.MINUTE_IN_MILLIS) { if (now - this.timeCreated > this.fetchedDataValiditySeconds * Constants.SECOND_IN_MILLIS) {
return false;
}
if (now - this.timeCompleted > Constants.MINUTE_IN_MILLIS) {
return false; return false;
} }
return true; 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>", "quiz1", institutionId, lmsSetupId, lmsType, "Demo Quiz 1 (MOCKUP)", "<p>Demo Quiz Mockup</p>",
"2020-01-01T09:00:00Z", null, "http://lms.mockup.com/api/")); "2020-01-01T09:00:00Z", null, "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData( 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/")); "2020-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData( this.mockups.add(new QuizData(
"quiz3", institutionId, lmsSetupId, lmsType, "Demo Quiz 3 (MOCKUP)", "<p>Demo Quiz Mockup</p>", "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.Courses;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CoursesPlugin; 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 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 { 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_START = "startneedle";
public static final String PARAM_PAGE_SIZE = "perpage"; 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 = public static final String SQL_CONDITION_TEMPLATE =
//"(startdate >= %s or timecreated >=%s) and (enddate is null or enddate = 0 or enddate >= %s)"; //"(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)"; "(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 pageSize;
private final int maxSize; private final int maxSize;
private final int cutoffTimeOffset; private final int cutoffTimeOffset;
private final boolean applyNameCriteria;
private MoodleAPIRestTemplate restTemplate; private MoodleAPIRestTemplate restTemplate;
@ -100,11 +105,13 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
final AsyncService asyncService, final AsyncService asyncService,
final MoodleRestTemplateFactory restTemplateFactory, final MoodleRestTemplateFactory restTemplateFactory,
final CacheManager cacheManager, final CacheManager cacheManager,
final Environment environment) { final Environment environment,
final boolean applyNameCriteria) {
super(cacheManager); super(cacheManager);
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
this.restTemplateFactory = restTemplateFactory; this.restTemplateFactory = restTemplateFactory;
this.applyNameCriteria = applyNameCriteria;
this.prependShortCourseName = BooleanUtils.toBoolean(environment.getProperty( this.prependShortCourseName = BooleanUtils.toBoolean(environment.getProperty(
"sebserver.webservice.lms.moodle.prependShortCourseName", "sebserver.webservice.lms.moodle.prependShortCourseName",
@ -118,7 +125,7 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
environment.getProperty( environment.getProperty(
"sebserver.webservice.circuitbreaker.moodleRestCall.blockingTime", "sebserver.webservice.circuitbreaker.moodleRestCall.blockingTime",
Long.class, Long.class,
Constants.SECOND_IN_MILLIS * 20), Constants.SECOND_IN_MILLIS * 30),
environment.getProperty( environment.getProperty(
"sebserver.webservice.circuitbreaker.moodleRestCall.timeToRecover", "sebserver.webservice.circuitbreaker.moodleRestCall.timeToRecover",
Long.class, Long.class,
@ -184,10 +191,11 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
quizFromTime = DateTime.now(DateTimeZone.UTC).minusYears(this.cutoffTimeOffset); quizFromTime = DateTime.now(DateTimeZone.UTC).minusYears(this.cutoffTimeOffset);
} }
final Predicate<QuizData> quizFilter = LmsAPIService.quizFilterPredicate(filterMap); final Predicate<QuizData> quizFilter = LmsAPIService.quizFilterPredicate(filterMap);
final String quizName = filterMap.getQuizName();
while (!asyncQuizFetchBuffer.finished && !asyncQuizFetchBuffer.canceled) { while (!asyncQuizFetchBuffer.finished && !asyncQuizFetchBuffer.canceled) {
try { try {
fetchQuizzesPage(page, quizFromTime, asyncQuizFetchBuffer, quizFilter); fetchQuizzesPage(page, quizFromTime, quizName, asyncQuizFetchBuffer, quizFilter);
page++; page++;
} catch (final Exception e) { } catch (final Exception e) {
log.error("Unexpected error while trying to fetch moodle quiz page: {}", page, 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( private void fetchQuizzesPage(
final int page, final int page,
final DateTime quizFromTime, final DateTime quizFromTime,
final String nameCondition,
final AsyncQuizFetchBuffer asyncQuizFetchBuffer, final AsyncQuizFetchBuffer asyncQuizFetchBuffer,
final Predicate<QuizData> quizFilter) throws JsonParseException, JsonMappingException, IOException { 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; : lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
final Collection<CourseData> fetchCoursesPage = 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 // finish if page is empty (no courses left
if (fetchCoursesPage.isEmpty()) { if (fetchCoursesPage.isEmpty()) {
asyncQuizFetchBuffer.finish(); asyncQuizFetchBuffer.finish();
@ -408,6 +417,7 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
private Collection<CourseData> fetchCoursesPage( private Collection<CourseData> fetchCoursesPage(
final MoodleAPIRestTemplate restTemplate, final MoodleAPIRestTemplate restTemplate,
final DateTime quizFromTime, final DateTime quizFromTime,
final String nameCondition,
final int page, final int page,
final int size) throws JsonParseException, JsonMappingException, IOException { final int size) throws JsonParseException, JsonMappingException, IOException {
@ -422,13 +432,25 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
final long defaultCutOff = Utils.toUnixTimeInSeconds( final long defaultCutOff = Utils.toUnixTimeInSeconds(
DateTime.now(DateTimeZone.UTC).minusYears(this.cutoffTimeOffset)); DateTime.now(DateTimeZone.UTC).minusYears(this.cutoffTimeOffset));
final long cutoffDate = (filterDate < defaultCutOff) ? filterDate : defaultCutOff; final long cutoffDate = (filterDate < defaultCutOff) ? filterDate : defaultCutOff;
final String sqlCondition = String.format( String sqlCondition = String.format(
SQL_CONDITION_TEMPLATE, SQL_CONDITION_TEMPLATE,
String.valueOf(cutoffDate), String.valueOf(cutoffDate),
String.valueOf(filterDate)); String.valueOf(filterDate));
final String fromElement = String.valueOf(page * size); final String fromElement = String.valueOf(page * size);
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>(); 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 // Note: courseid[]=0 means all courses. Moodle don't like empty parameter
attributes.add(PARAM_COURSE_ID_ARRAY, "0"); attributes.add(PARAM_COURSE_ID_ARRAY, "0");
attributes.add(PARAM_SQL_CONDITIONS, sqlCondition); attributes.add(PARAM_SQL_CONDITIONS, sqlCondition);

View file

@ -46,6 +46,7 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
private final ExamConfigurationValueService examConfigurationValueService; private final ExamConfigurationValueService examConfigurationValueService;
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
private final String[] alternativeTokenRequestPaths; private final String[] alternativeTokenRequestPaths;
private final boolean applyNameCriteria;
protected MooldePluginLmsAPITemplateFactory( protected MooldePluginLmsAPITemplateFactory(
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
@ -55,7 +56,8 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
final ClientCredentialService clientCredentialService, final ClientCredentialService clientCredentialService,
final ExamConfigurationValueService examConfigurationValueService, final ExamConfigurationValueService examConfigurationValueService,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService, 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.jsonMapper = jsonMapper;
this.cacheManager = cacheManager; this.cacheManager = cacheManager;
@ -67,6 +69,7 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null) this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
? StringUtils.split(alternativeTokenRequestPaths, Constants.LIST_SEPARATOR) ? StringUtils.split(alternativeTokenRequestPaths, Constants.LIST_SEPARATOR)
: null; : null;
this.applyNameCriteria = applyNameCriteria;
} }
@Override @Override
@ -90,7 +93,8 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
this.asyncService, this.asyncService,
moodleRestTemplateFactory, moodleRestTemplateFactory,
this.cacheManager, this.cacheManager,
this.environment); this.environment,
this.applyNameCriteria);
final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction( final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction(
this.jsonMapper, this.jsonMapper,

View file

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

View file

@ -410,11 +410,14 @@ class ExamUpdateHandler {
.getLmsAPITemplate(lmsSetupId) .getLmsAPITemplate(lmsSetupId)
.getOrThrow(); .getOrThrow();
final Exam exam = exams.get(quizId);
if (!lmsTemplate.getType().features.contains(Features.COURSE_RECOVERY)) { 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"); throw new UnsupportedOperationException("No Course Recovery");
} }
final Exam exam = exams.get(quizId);
final int attempts = Integer.parseInt(this.additionalAttributesDAO.getAdditionalAttribute( final int attempts = Integer.parseInt(this.additionalAttributesDAO.getAdditionalAttribute(
EntityType.EXAM, EntityType.EXAM,
exam.id, exam.id,

View file

@ -329,7 +329,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
if (StringUtils.isNoneBlank(clientAddress) && if (StringUtils.isNoneBlank(clientAddress) &&
StringUtils.isNotBlank(clientConnection.clientAddress) && StringUtils.isNotBlank(clientConnection.clientAddress) &&
!clientAddress.equals(clientConnection.clientAddress)) { !clientAddress.equals(clientConnection.clientAddress)) {
log.error( log.warn(
"ClientConnection integrity violation: client address mismatch: {}, {}", "ClientConnection integrity violation: client address mismatch: {}, {}",
clientAddress, clientAddress,
clientConnection.clientAddress); clientConnection.clientAddress);
@ -337,7 +337,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
"ClientConnection integrity violation: client address mismatch"); "ClientConnection integrity violation: client address mismatch");
} }
} else if (!clientConnection.status.clientActiveStatus) { } 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); clientConnection);
throw new IllegalArgumentException( throw new IllegalArgumentException(
"ClientConnection integrity violation: client connection is not in expected state"); "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) this.clientEventDAO.getPendingNotificationByValue(clientConnection.id, notificationId)
.flatMap(notification -> this.clientEventDAO.confirmPendingNotification(notification.id)) .flatMap(notification -> this.clientEventDAO.confirmPendingNotification(notification.id))
.map(this::removeFromCache) .map(this::removeFromCache)
.onError(error -> log.error("Failed to confirm pending notification: {}", event, error)); .getOrThrow();
} catch (final Exception e) { } catch (final Exception e) {
log.error( log.error(
@ -110,8 +110,7 @@ public class SEBClientNotificationServiceImpl implements SEBClientNotificationSe
return this.clientEventDAO.getPendingNotification(notificationId) return this.clientEventDAO.getPendingNotification(notificationId)
.map(notification -> this.confirmClientSide(notification, examId, connectionToken)) .map(notification -> this.confirmClientSide(notification, examId, connectionToken))
.flatMap(notification -> this.clientEventDAO.confirmPendingNotification(notificationId)) .flatMap(notification -> this.clientEventDAO.confirmPendingNotification(notificationId))
.map(this::removeFromCache) .map(this::removeFromCache);
.onError(error -> log.error("Failed to confirm pending notification: {}", notificationId, error));
} }
@Override @Override

View file

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

View file

@ -149,6 +149,15 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler {
HttpStatus.BAD_REQUEST); 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) @ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<Object> handleResourceNotFoundException( public ResponseEntity<Object> handleResourceNotFoundException(
final ResourceNotFoundException ex, final ResourceNotFoundException ex,
@ -182,7 +191,7 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler {
final ExamNotRunningException ex, final ExamNotRunningException ex,
final WebRequest request) { final WebRequest request) {
log.info("{}", ex.getMessage()); log.debug("{}", ex.getMessage());
return APIMessage.ErrorMessage.INTEGRITY_VALIDATION return APIMessage.ErrorMessage.INTEGRITY_VALIDATION
.createErrorResponse(ex.getMessage()); .createErrorResponse(ex.getMessage());
} }

View file

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

View file

@ -18,7 +18,7 @@ spring.datasource.hikari.leakDetectionThreshold=2000
sebserver.http.client.connect-timeout=15000 sebserver.http.client.connect-timeout=15000
sebserver.http.client.connection-request-timeout=10000 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.updateInterval=1000
sebserver.webservice.distributed.connectionUpdate=2000 sebserver.webservice.distributed.connectionUpdate=2000
sebserver.webservice.clean-db-on-startup=false 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.openedx.api.token.request.paths=/oauth2/access_token
sebserver.webservice.lms.moodle.api.token.request.paths= sebserver.webservice.lms.moodle.api.token.request.paths=
sebserver.webservice.lms.address.alias=lms.mockup.com=lms.address.alias 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.api-docs.enabled=true
springdoc.swagger-ui.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.api.token.request.paths=/login/token.php
sebserver.webservice.lms.moodle.prependShortCourseName=true sebserver.webservice.lms.moodle.prependShortCourseName=true
sebserver.webservice.lms.moodle.fetch.cutoffdate.yearsBeforeNow=2 sebserver.webservice.lms.moodle.fetch.cutoffdate.yearsBeforeNow=2
sebserver.webservice.lms.moodle.fetch.applyNameCriteria=true
sebserver.webservice.lms.olat.sendAdditionalAttributesWithRestriction=false sebserver.webservice.lms.olat.sendAdditionalAttributesWithRestriction=false
sebserver.webservice.lms.address.alias= sebserver.webservice.lms.address.alias=
sebserver.webservice.lms.datafetch.validity.seconds=600
sebserver.webservice.proctoring.resetBroadcastOnLeav=true sebserver.webservice.proctoring.resetBroadcastOnLeav=true
sebserver.webservice.proctoring.zoom.enableWaitingRoom=false sebserver.webservice.proctoring.zoom.enableWaitingRoom=false

View file

@ -15,6 +15,12 @@ server.servlet.context-path=/
# Tomcat # Tomcat
server.tomcat.max-threads=2000 server.tomcat.max-threads=2000
server.tomcat.accept-count=300 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 server.tomcat.uri-encoding=UTF-8
### encoding ### 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.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.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=<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.list=LMS Exam Lookup
sebserver.quizdiscovery.action.import=Import as Exam 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.title=Exam Configuration Mapping
sebserver.exam.configuration.form.name=Exam Configuration 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.name.tooltip=Please select an exam configuration to attach to the exam
sebserver.exam.configuration.form.encryptSecret=Encryption Password sebserver.exam.configuration.form.encryptSecret=Settings 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.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=Description
sebserver.exam.configuration.form.description.tooltip=The description of the selected exam configuration sebserver.exam.configuration.form.description.tooltip=The description of the selected exam configuration
sebserver.exam.configuration.form.status=Status sebserver.exam.configuration.form.status=Status
sebserver.exam.configuration.form.status.tooltip=The current status of the selected exam configuration 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=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.actions=&nbsp;
sebserver.exam.indicator.list.title=Indicators sebserver.exam.indicator.list.title=Indicators

View file

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

View file

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