Merge remote-tracking branch 'origin/rel-1.5.2' into development

Conflicts:
	pom.xml
	src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java
	src/main/resources/config/ehcache.xml
	src/main/resources/messages.properties
This commit is contained in:
anhefti 2023-10-03 08:13:03 +02:00
commit 1cbc97ef8f
9 changed files with 95 additions and 19 deletions

View file

@ -23,7 +23,6 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties> </properties>
<!-- NOTE: There currently are two profiles, a default one to build on <!-- NOTE: There currently are two profiles, a default one to build on
Java 11 (from eclipse and command-line) and one to build still on Java 8 Java 11 (from eclipse and command-line) and one to build still on Java 8
to support the Jenkins build on CI-Server that still no Java 11 installed --> to support the Jenkins build on CI-Server that still no Java 11 installed -->

View file

@ -18,6 +18,7 @@ import org.eclipse.rap.rwt.RWT;
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.widgets.Composite; import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Label;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.joda.time.DateTimeZone; import org.joda.time.DateTimeZone;
@ -451,31 +452,28 @@ public class QuizLookupList implements TemplateComposer {
} }
} }
private Composite warningPanel = null;
private void handelPageReload( private void handelPageReload(
final Composite notePanel, final Composite notePanel,
final EntityTable<QuizData> table) { final EntityTable<QuizData> table) {
if (table.isComplete()) { if (table.isComplete()) {
PageService.clearComposite(notePanel); PageService.clearComposite(notePanel);
if (this.warningPanel != null) {
this.warningPanel.dispose();
}
this.warningPanel = null;
} else { } else {
if (this.warningPanel != null && !this.warningPanel.isDisposed()) {
this.warningPanel.dispose(); final Control[] children = notePanel.getChildren();
if (children != null && children.length > 0) {
return;
} }
this.warningPanel = this.widgetFactory.createWarningPanel(notePanel, 15, true); final Composite warningPanel = this.widgetFactory.createWarningPanel(notePanel, 15, true);
warningPanel.setData("warningPanel", "TRUE");
this.widgetFactory.imageButton( this.widgetFactory.imageButton(
ImageIcon.SWITCH, ImageIcon.SWITCH,
this.warningPanel, warningPanel,
TEXT_FETCH_NOTE_TOOLTIP, TEXT_FETCH_NOTE_TOOLTIP,
event -> table.applyFilter()); event -> table.applyFilter());
final Label text = new Label(this.warningPanel, SWT.NONE); final Label text = new Label(warningPanel, SWT.NONE);
text.setData(RWT.MARKUP_ENABLED, Boolean.TRUE); text.setData(RWT.MARKUP_ENABLED, Boolean.TRUE);
text.setText(this.pageService.getI18nSupport().getText(TEXT_FETCH_NOTE)); text.setText(this.pageService.getI18nSupport().getText(TEXT_FETCH_NOTE));
final GridData gridData = new GridData(SWT.LEFT, SWT.FILL, true, true); final GridData gridData = new GridData(SWT.LEFT, SWT.FILL, true, true);

View file

@ -98,7 +98,7 @@ public class PasswordInput extends Composite {
final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true);
passwordInput.setLayoutData(gridData); passwordInput.setLayoutData(gridData);
passwordInput.setText(value != null passwordInput.setText(value != null
? Utils.escapeHTML_XML_EcmaScript(value) ? value
: StringUtils.EMPTY); : StringUtils.EMPTY);
if (!buildPassword) { if (!buildPassword) {
passwordInput.setEditable(false); passwordInput.setEditable(false);

View file

@ -531,6 +531,10 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
lmsSetup); lmsSetup);
} }
final Set<String> qIdSet = internalIds.stream()
.map(MoodleUtils::getQuizId)
.collect(Collectors.toSet());
return getCoursesForIds(restTemplate, moodleCourseIds) return getCoursesForIds(restTemplate, moodleCourseIds)
.stream() .stream()
.filter(courseData -> !courseData.quizzes.isEmpty()) .filter(courseData -> !courseData.quizzes.isEmpty())
@ -540,7 +544,7 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
urlPrefix, urlPrefix,
this.prependShortCourseName) this.prependShortCourseName)
.stream() .stream()
.filter(q -> internalIds.contains(q.id))) .filter(q -> qIdSet.contains(MoodleUtils.getQuizId(q.id))))
.collect(Collectors.toList()); .collect(Collectors.toList());
} catch (final Exception e) { } catch (final Exception e) {

View file

@ -163,6 +163,23 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
"lmsSetup:lmsClientsecret:notNull")); "lmsSetup:lmsClientsecret:notNull"));
} }
final Result<OlatLmsRestTemplate> restTemplateResult = getRestTemplate();
if (restTemplateResult.hasError()) {
missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_URL,
"lmsSetup:lmsUrl:url.noservice"));
} else {
final OlatLmsRestTemplate olatLmsRestTemplate = restTemplateResult.get();
try {
olatLmsRestTemplate.testAuthentication();
} catch (final Exception e) {
log.error("Failed to test Authentication: {}", e.getMessage());
missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_URL,
"lmsSetup:lmsUrl:url.noaccess"));
}
}
if (!missingAttrs.isEmpty()) { if (!missingAttrs.isEmpty()) {
return LmsSetupTestResult.ofMissingAttributes(LmsType.OPEN_OLAT, missingAttrs); return LmsSetupTestResult.ofMissingAttributes(LmsType.OPEN_OLAT, missingAttrs);
} }

View file

@ -32,6 +32,12 @@ public class OlatLmsRestTemplate extends RestTemplate {
private String token; private String token;
private ClientCredentialsResourceDetails details; private ClientCredentialsResourceDetails details;
public void testAuthentication() {
if (this.token == null) {
authenticate();
}
}
public OlatLmsRestTemplate(final ClientCredentialsResourceDetails details) { public OlatLmsRestTemplate(final ClientCredentialsResourceDetails details) {
super(); super();
this.details = details; this.details = details;
@ -59,6 +65,7 @@ public class OlatLmsRestTemplate extends RestTemplate {
return execution.execute(request, body); return execution.execute(request, body);
} }
// otherwise, add the X-OLAT-TOKEN // otherwise, add the X-OLAT-TOKEN
request.getHeaders().set("accept", "application/json"); request.getHeaders().set("accept", "application/json");
request.getHeaders().set("X-OLAT-TOKEN", OlatLmsRestTemplate.this.token); request.getHeaders().set("X-OLAT-TOKEN", OlatLmsRestTemplate.this.token);
@ -85,6 +92,7 @@ public class OlatLmsRestTemplate extends RestTemplate {
log.debug("OLAT [retry API call]: URL {}", request.getURI()); log.debug("OLAT [retry API call]: URL {}", request.getURI());
} }
response.close();
response = execution.execute(request, body); response = execution.execute(request, body);
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
@ -97,7 +105,7 @@ public class OlatLmsRestTemplate extends RestTemplate {
} catch (final Exception e) { } catch (final Exception e) {
// TODO find a way to better deal with Olat temporary unavailability // TODO find a way to better deal with Olat temporary unavailability
log.error("Unexpected error: ", e); log.error("Unexpected error: {}", e.getMessage());
throw e; throw e;
} }
} }

View file

@ -13,6 +13,7 @@ import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import org.joda.time.DateTime; import org.joda.time.DateTime;
@ -30,6 +31,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus; import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.Features; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.Features;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
@ -40,6 +42,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamResetEvent; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamResetEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent;
@ -167,7 +170,12 @@ class ExamUpdateHandler implements ExamUpdateTask {
.forEach(quiz -> { .forEach(quiz -> {
try { try {
final Exam exam = exams.get(quiz.id); final Exam exam = getExamForQuizWithMoodleSpecialCase(exams, quiz);
if (exam == null) {
log.warn("Failed to find map exam to fetched quiz-data: {}", quiz);
return;
}
if (hasChanges(exam, quiz)) { if (hasChanges(exam, quiz)) {
@ -389,7 +397,8 @@ class ExamUpdateHandler implements ExamUpdateTask {
!Objects.equals(exam.startTime, quizData.startTime) || !Objects.equals(exam.startTime, quizData.startTime) ||
!Objects.equals(exam.endTime, quizData.endTime) || !Objects.equals(exam.endTime, quizData.endTime) ||
!Utils.isEqualsWithEmptyCheckTruncated(exam.getDescription(), quizData.description) || !Utils.isEqualsWithEmptyCheckTruncated(exam.getDescription(), quizData.description) ||
!Utils.isEqualsWithEmptyCheck(exam.getStartURL(), quizData.startURL)) { !Utils.isEqualsWithEmptyCheck(exam.getStartURL(), quizData.startURL) ||
!Objects.equals(exam.externalId, quizData.id)) {
if (!Utils.isEqualsWithEmptyCheck(exam.name, quizData.name)) { if (!Utils.isEqualsWithEmptyCheck(exam.name, quizData.name)) {
log.info("Update name difference from LMS. Exam: {}, QuizData: {}", exam.name, quizData.name); log.info("Update name difference from LMS. Exam: {}, QuizData: {}", exam.name, quizData.name);
@ -409,6 +418,11 @@ class ExamUpdateHandler implements ExamUpdateTask {
exam.getStartURL(), exam.getStartURL(),
quizData.startURL); quizData.startURL);
} }
if (!Objects.equals(exam.externalId, quizData.id)) {
log.info("Update quizId difference from LMS. Exam:{}, QuizData: {}",
exam.externalId,
quizData.id);
}
return true; return true;
} }
@ -579,4 +593,33 @@ class ExamUpdateHandler implements ExamUpdateTask {
} }
} }
/** NOTE: LMS binding for Moodle uses a composed quiz-identifier with also contains the course short name
* for the reason to be able to re-identify a quiz when the main quiz-id changes in case of backup-restore on Moodle
* But since course names also can change this function tries to find old Exam mappings for course-names hat has
* changed */
private Exam getExamForQuizWithMoodleSpecialCase(final Map<String, Exam> exams, final QuizData quiz) {
Exam exam = exams.get(quiz.id);
if (exam == null) {
try {
final LmsAPITemplate lms = this.lmsAPIService
.getLmsAPITemplate(quiz.lmsSetupId)
.getOrThrow();
if (lms.getType() == LmsType.MOODLE || lms.getType() == LmsType.MOODLE_PLUGIN) {
final String quizId = MoodleUtils.getQuizId(quiz.id);
final Optional<String> find =
exams.keySet().stream().filter(key -> key.startsWith(quizId)).findFirst();
if (find.isPresent()) {
exam = exams.get(find.get());
}
}
} catch (final Exception e) {
log.error("Failed to verify changed external Exam id from moodle course: {}", e.getMessage());
}
}
return exam;
}
} }

View file

@ -33,8 +33,8 @@
<expiry> <expiry>
<ttl unit="hours">24</ttl> <ttl unit="hours">24</ttl>
</expiry> </expiry>
<resources> <resources>
<heap unit="entries">100000</heap> <heap unit="entries">100000</heap>
</resources> </resources>
</cache> </cache>

View file

@ -108,9 +108,16 @@ sebserver.form.validation.fieldError.invalidURL=The input does not match the URL
sebserver.form.validation.fieldError.exists=This name already exists. Please choose another one sebserver.form.validation.fieldError.exists=This name already exists. Please choose another one
sebserver.form.validation.fieldError.email=Invalid mail address sebserver.form.validation.fieldError.email=Invalid mail address
sebserver.form.validation.fieldError.serverNotAvailable=No service seems to be available within the given URL sebserver.form.validation.fieldError.serverNotAvailable=No service seems to be available within the given URL
<<<<<<< HEAD
sebserver.form.validation.fieldError.url.invalid=Invalid URL. The given URL cannot be reached sebserver.form.validation.fieldError.url.invalid=Invalid URL. The given URL cannot be reached
sebserver.form.validation.fieldError.typeInvalid=This type is not implemented yet and cannot be used sebserver.form.validation.fieldError.typeInvalid=This type is not implemented yet and cannot be used
sebserver.form.validation.fieldError.url.noservice=The expected service is not available within the given URL and API access sebserver.form.validation.fieldError.url.noservice=The expected service is not available within the given URL and API access
=======
sebserver.form.validation.fieldError.url.invalid=Invalid URL. The given URL cannot be reached.
sebserver.form.validation.fieldError.typeInvalid=This type is not implemented yet and cannot be used.
sebserver.form.validation.fieldError.url.noservice=The expected service is not available within the given URL and API access.
sebserver.form.validation.fieldError.url.noaccess=There has no access been granted by the service. Please check the given access credentials.
>>>>>>> refs/remotes/origin/rel-1.5.2
sebserver.form.validation.fieldError.thresholdDuplicate=There are duplicate threshold values. sebserver.form.validation.fieldError.thresholdDuplicate=There are duplicate threshold values.
sebserver.form.validation.fieldError.thresholdEmpty=There are missing values or colors for the threshold declaration sebserver.form.validation.fieldError.thresholdEmpty=There are missing values or colors for the threshold declaration
sebserver.form.validation.fieldError.invalidIP=Invalid IP v4. Please enter a valid IP-address (v4) sebserver.form.validation.fieldError.invalidIP=Invalid IP v4. Please enter a valid IP-address (v4)