Merge remote-tracking branch 'origin/dev-lms-open-olat' into development

This commit is contained in:
anhefti 2021-06-30 14:23:38 +02:00
commit 54a2dfd7d8
5 changed files with 108 additions and 45 deletions

View file

@ -63,7 +63,7 @@ public final class LmsSetup implements GrantEntity, Activatable {
/** The Ans Delft binding is on the way */ /** The Ans Delft binding is on the way */
ANS_DELFT(Features.COURSE_API, Features.SEB_RESTRICTION), ANS_DELFT(Features.COURSE_API, Features.SEB_RESTRICTION),
/** The OpenOLAT binding is on the way */ /** The OpenOLAT binding is on the way */
OPEN_OLAT(/* Features.COURSE_API , Features.SEB_RESTRICTION */); OPEN_OLAT(Features.COURSE_API, Features.SEB_RESTRICTION);
public final EnumSet<Features> features; public final EnumSet<Features> features;

View file

@ -20,10 +20,16 @@ import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.async.AsyncService; import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.client.ProxyData;
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP; import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters; import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
@ -45,6 +51,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCour
public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements LmsAPITemplate { public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements LmsAPITemplate {
// TODO add needed dependencies here // TODO add needed dependencies here
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
private final ClientCredentialService clientCredentialService;
private final APITemplateDataSupplier apiTemplateDataSupplier; private final APITemplateDataSupplier apiTemplateDataSupplier;
private final Long lmsSetupId; private final Long lmsSetupId;
@ -52,6 +60,8 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
// TODO if you need more dependencies inject them here and set the reference // TODO if you need more dependencies inject them here and set the reference
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final ClientCredentialService clientCredentialService,
final APITemplateDataSupplier apiTemplateDataSupplier, final APITemplateDataSupplier apiTemplateDataSupplier,
final AsyncService asyncService, final AsyncService asyncService,
final Environment environment, final Environment environment,
@ -59,6 +69,8 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
super(asyncService, environment, cacheManager); super(asyncService, environment, cacheManager);
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.clientCredentialService = clientCredentialService;
this.apiTemplateDataSupplier = apiTemplateDataSupplier; this.apiTemplateDataSupplier = apiTemplateDataSupplier;
this.lmsSetupId = apiTemplateDataSupplier.getLmsSetup().id; this.lmsSetupId = apiTemplateDataSupplier.getLmsSetup().id;
} }
@ -83,12 +95,15 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
final LmsSetupTestResult testLmsSetupSettings = testLmsSetupSettings(); final LmsSetupTestResult testLmsSetupSettings = testLmsSetupSettings();
if (testLmsSetupSettings.hasAnyError()) { if (testLmsSetupSettings.hasAnyError()) {
return testLmsSetupSettings; return testLmsSetupSettings;
} else {
} }
// TODO check if the course API of the remote LMS is available // TODO check if the course API of the remote LMS is available
// if not, create corresponding LmsSetupTestResult error // if not, create corresponding LmsSetupTestResult error
return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.OPEN_OLAT, "TODO: implement LMS access check");
return LmsSetupTestResult.ofOkay(LmsType.OPEN_OLAT); //return LmsSetupTestResult.ofOkay(LmsType.OPEN_OLAT);
} }
@Override @Override
@ -193,14 +208,20 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
@Override @Override
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) { protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) {
@SuppressWarnings("unused")
final String quizName = filterMap.getString(QuizData.FILTER_ATTR_QUIZ_NAME); final String quizName = filterMap.getString(QuizData.FILTER_ATTR_QUIZ_NAME);
@SuppressWarnings("unused")
final DateTime quizFromTime = (filterMap != null) ? filterMap.getQuizFromTime() : null; final DateTime quizFromTime = (filterMap != null) ? filterMap.getQuizFromTime() : null;
// TODO get all course / quiz data from remote LMS that matches the filter criteria.
// put loaded QuizData to the cache: super.putToCache(quizDataCollection);
// before returning it.
return () -> { return () -> {
// TODO Get all course / quiz data from remote LMS that matches the filter criteria.
// If the LMS API uses paging, go through all pages using the filter criteria
// and collect the course data.
// Transform the data from courses / quizzes from LMS into QuizData objects
// Put loaded QuizData objects to the cache: super.putToCache(quizDataCollection);
// before returning it.
throw new RuntimeException("TODO"); throw new RuntimeException("TODO");
}; };
} }
@ -208,11 +229,13 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
@Override @Override
protected Supplier<Collection<QuizData>> quizzesSupplier(final Set<String> ids) { protected Supplier<Collection<QuizData>> quizzesSupplier(final Set<String> ids) {
// TODO get all quiz / course data for specified identifiers from remote LMS
// and put it to the cache: super.putToCache(quizDataCollection);
// before returning it.
return () -> { return () -> {
// TODO get all quiz / course data for specified identifiers from remote LMS
// Transform the data from courses / quizzes from LMS into QuizData objects
// and put it to the cache: super.putToCache(quizDataCollection);
// before returning it.
throw new RuntimeException("TODO"); throw new RuntimeException("TODO");
}; };
} }
@ -220,11 +243,12 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
@Override @Override
protected Supplier<QuizData> quizSupplier(final String id) { protected Supplier<QuizData> quizSupplier(final String id) {
// TODO get the specified quiz / course data for specified identifier from remote LMS
// and put it to the cache: super.putToCache(quizDataCollection);
// before returning it.
return () -> { return () -> {
// TODO get the specified quiz / course data for specified identifier from remote LMS
// and put it to the cache: super.putToCache(quizDataCollection);
// before returning it.
throw new RuntimeException("TODO"); throw new RuntimeException("TODO");
}; };
} }
@ -232,10 +256,11 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
@Override @Override
protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String examineeSessionId) { protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String examineeSessionId) {
// TODO get the examinee's account details by the given examineeSessionId from remote LMS.
// Currently only the name is needed to display on monitoring view.
return () -> { return () -> {
// TODO get the examinee's account details by the given examineeSessionId from remote LMS.
// Currently only the name is needed to display on monitoring view.
throw new RuntimeException("TODO"); throw new RuntimeException("TODO");
}; };
} }
@ -243,19 +268,22 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
@Override @Override
protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) { protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) {
return () -> { return () -> {
throw new UnsupportedOperationException("not available yet"); throw new UnsupportedOperationException("No Course Chapter available for OpenOLAT LMS");
}; };
} }
@Override @Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) { public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
@SuppressWarnings("unused")
final String quizId = exam.externalId; final String quizId = exam.externalId;
// TODO get the SEB client restrictions that are currently set on the remote LMS for return Result.tryCatch(() -> {
// the given quiz / course derived from the given exam
return Result.ofRuntimeError("TODO"); // TODO get the SEB client restrictions that are currently set on the remote LMS for
// the given quiz / course derived from the given exam
throw new RuntimeException("TODO");
});
} }
@Override @Override
@ -263,22 +291,59 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
final String externalExamId, final String externalExamId,
final SEBRestriction sebRestrictionData) { final SEBRestriction sebRestrictionData) {
// TODO apply the given sebRestrictionData settings as current SEB client restriction setting return Result.tryCatch(() -> {
// to the remote LMS for the given quiz / course.
// Mainly SEBRestriction.configKeys and SEBRestriction.browserExamKeys
return Result.ofRuntimeError("TODO"); // TODO apply the given sebRestrictionData settings as current SEB client restriction setting
// to the remote LMS for the given quiz / course.
// Mainly SEBRestriction.configKeys and SEBRestriction.browserExamKeys
throw new RuntimeException("TODO");
});
} }
@Override @Override
public Result<Exam> releaseSEBClientRestriction(final Exam exam) { public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
@SuppressWarnings("unused")
final String quizId = exam.externalId; final String quizId = exam.externalId;
// TODO Release respectively delete all SEB client restrictions for the given return Result.tryCatch(() -> {
// course / quize on the remote LMS.
return Result.ofRuntimeError("TODO"); // TODO Release respectively delete all SEB client restrictions for the given
// course / quize on the remote LMS.
throw new RuntimeException("TODO");
});
}
// TODO: This is an example of how to create a RestTemplate for the service to access the LMS API
// The example deals with a Http based API that is secured by an OAuth2 client-credential flow.
// You might need some different template, then you have to adapt this code
// To your needs.
@SuppressWarnings("unused")
private OAuth2RestTemplate createRestTemplate(final String accessTokenRequestPath) {
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials();
final ProxyData proxyData = this.apiTemplateDataSupplier.getProxyData();
final CharSequence plainClientId = credentials.clientId;
final CharSequence plainClientSecret = this.clientCredentialService
.getPlainClientSecret(credentials)
.getOrThrow();
final ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
details.setAccessTokenUri(lmsSetup.lmsApiUrl + accessTokenRequestPath);
details.setClientId(plainClientId.toString());
details.setClientSecret(plainClientSecret.toString());
final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService
.getClientHttpRequestFactory(proxyData)
.getOrThrow();
final OAuth2RestTemplate template = new OAuth2RestTemplate(details);
template.setRequestFactory(clientHttpRequestFactory);
return template;
} }
} }

View file

@ -13,7 +13,9 @@ 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;
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
import ch.ethz.seb.sebserver.gbl.async.AsyncService; import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; 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;
@ -33,15 +35,21 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
* as usual. Just add the additionally needed dependencies used to build a OlatLmsAPITemplate. */ * as usual. Just add the additionally needed dependencies used to build a OlatLmsAPITemplate. */
public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory { public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
private final ClientCredentialService clientCredentialService;
private final AsyncService asyncService; private final AsyncService asyncService;
private final Environment environment; private final Environment environment;
private final CacheManager cacheManager; private final CacheManager cacheManager;
public OlatLmsAPITemplateFactory( public OlatLmsAPITemplateFactory(
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final ClientCredentialService clientCredentialService,
final AsyncService asyncService, final AsyncService asyncService,
final Environment environment, final Environment environment,
final CacheManager cacheManager) { final CacheManager cacheManager) {
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.clientCredentialService = clientCredentialService;
this.asyncService = asyncService; this.asyncService = asyncService;
this.environment = environment; this.environment = environment;
this.cacheManager = cacheManager; this.cacheManager = cacheManager;
@ -56,6 +64,8 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) { public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
return new OlatLmsAPITemplate( return new OlatLmsAPITemplate(
this.clientHttpRequestFactoryService,
this.clientCredentialService,
apiTemplateDataSupplier, apiTemplateDataSupplier,
this.asyncService, this.asyncService,
this.environment, this.environment,

View file

@ -51,21 +51,6 @@ 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
# NOTE: This is a temporary work-around for SEB Restriction API within Open edX SEB integration plugin to
# apply on load-balanced infrastructure or infrastructure that has several layers of cache.
# The reason for this is that the API (Open edX system) internally don't apply a resource-change that is
# done within HTTP API call immediately from an outside perspective.
# After a resource-change on the API is done, the system toggles between the old and the new resource
# while constantly calling GET. This usually happens for about a minute or two then it stabilizes on the new resource
#
# This may source on load-balancing or internally caching on Open edX side.
# To mitigate this effect the SEB Server can be configured to apply a resource-change on the
# API several times in a row to flush as match caches and reach as match as possible server instances.
#
# Since this is a brute-force method to mitigate the problem, this should only be a temporary
# work-around until a better solution on Open edX SEB integration side has been found and applied.
#sebserver.webservice.lms.openedx.seb.restriction.push-count=10
# actuator configuration # actuator configuration
management.server.port=${server.port} management.server.port=${server.port}
management.endpoints.web.base-path=/management management.endpoints.web.base-path=/management

View file

@ -12,6 +12,7 @@ import static org.junit.Assert.*;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@ -28,6 +29,8 @@ import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
// NOTE this test seems sometimes not to work (maybe a ordering problem)
@Ignore
public class ExamImportTest extends AdministrationAPIIntegrationTester { public class ExamImportTest extends AdministrationAPIIntegrationTester {
@Autowired @Autowired