SEBSERV-158 preparation - refactoring of LmsAPITemplate

This commit is contained in:
anhefti 2022-03-21 13:37:33 +01:00
parent 8dae41f754
commit eb08df6c00
26 changed files with 1204 additions and 1187 deletions

View file

@ -56,7 +56,7 @@ public final class LmsSetup implements GrantEntity, Activatable {
public enum LmsType {
/** Mockup LMS type used to create test setups */
MOCKUP(Features.COURSE_API),
/** The Open edX LMS binding features both APIs, course access as well as SEB restrcition */
/** The Open edX LMS binding features both APIs, course access as well as SEB restriction */
OPEN_EDX(Features.COURSE_API, Features.SEB_RESTRICTION),
/** The Moodle binding features only the course access API so far */
MOODLE(Features.COURSE_API /* , Features.SEB_RESTRICTION */),

View file

@ -28,6 +28,7 @@ public final class LmsSetupTestResult {
public static final String ATTR_MISSING_ATTRIBUTE = "missingLMSSetupAttribute";
public enum ErrorType {
API_NOT_SUPPORTED,
MISSING_ATTRIBUTE,
TOKEN_REQUEST,
QUIZ_ACCESS_API_REQUEST,
@ -109,7 +110,12 @@ public final class LmsSetupTestResult {
return new LmsSetupTestResult(lmsType);
}
public static LmsSetupTestResult ofMissingAttributes(final LmsSetup.LmsType lmsType,
public static LmsSetupTestResult ofAPINotSupported(final LmsSetup.LmsType lmsType) {
return new LmsSetupTestResult(lmsType, new Error(ErrorType.TOKEN_REQUEST, "Not Supported"));
}
public static LmsSetupTestResult ofMissingAttributes(
final LmsSetup.LmsType lmsType,
final Collection<APIMessage> attrs) {
return new LmsSetupTestResult(lmsType, new Error(ErrorType.MISSING_ATTRIBUTE, "missing attribute(s)"), attrs);
}

View file

@ -0,0 +1,108 @@
/*
* Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
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.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
public interface CourseAccessAPI {
Logger log = LoggerFactory.getLogger(CourseAccessAPI.class);
/** Fetch status that indicates an asynchronous quiz data fetch status if the
* concrete implementation has such. */
public enum FetchStatus {
ALL_FETCHED,
ASYNC_FETCH_RUNNING,
FETCH_ERROR
}
/** Performs a test for the underling {@link LmsSetup } configuration and checks if the
* LMS and the course API of the LMS can be accessed or if there are some difficulties,
* missing configuration data or connection/authentication errors.
*
* @return {@link LmsSetupTestResult } instance with the test result report */
LmsSetupTestResult testCourseAccessAPI();
/** Get an unsorted List of filtered {@link QuizData } from the LMS course/quiz API
*
* @param filterMap the {@link FilterMap } to get a filtered result. Possible filter attributes are:
*
* <pre>
* {@link QuizData.FILTER_ATTR_QUIZ_NAME } The quiz name filter text (exclude all names that do not contain the given text)
* {@link QuizData.FILTER_ATTR_START_TIME } The quiz start time (exclude all quizzes that starts before)
* </pre>
*
* @return Result of an unsorted List of filtered {@link QuizData } from the LMS course/quiz API
* or refer to an error when happened */
Result<List<QuizData>> getQuizzes(FilterMap filterMap);
/** Get all {@link QuizData } for the set of {@link QuizData } identifiers from LMS API in a collection
* of Result. If particular quizzes cannot be loaded because of errors or deletion,
* the the referencing QuizData will not be in the resulting list and an error is logged.
*
* @param ids the Set of Quiz identifiers to get the {@link QuizData } for
* @return Collection of all {@link QuizData } from the given id set */
Result<Collection<QuizData>> getQuizzes(Set<String> ids);
/** Get the quiz data with specified identifier.
*
* @param id the quiz data identifier
* @return Result refer to the quiz data or to an error when happened */
Result<QuizData> getQuiz(final String id);
/** Clears the underling caches if there are some for a particular implementation. */
void clearCourseCache();
/** Convert an anonymous or temporary examineeUserId, sent by the SEB Client on LMS login,
* to LMS examinee account details by requesting them on the LMS API with the given examineeUserId
*
* @param examineeUserId the examinee user identifier derived from SEB Client
* @return a Result refer to the {@link ExamineeAccountDetails } instance or to an error when happened or not
* supported */
Result<ExamineeAccountDetails> getExamineeAccountDetails(String examineeUserId);
/** Used to convert an anonymous or temporary examineeUserId, sent by the SEB Client on LMS login,
* to a readable LMS examinee account name by requesting this on the LMS API with the given examineeUserId.
*
* If the underling concrete template implementation does not support this user name conversion,
* the given examineeSessionId shall be returned.
*
* @param examineeUserId the examinee user identifier derived from SEB Client
* @return a user account display name if supported or the given examineeSessionId if not. */
String getExamineeName(final String examineeUserId);
/** Used to get a list of chapters (display name and chapter-identifier) that can be used to
* apply chapter-based SEB restriction for a specified course.
*
* The availability of this depends on the type of LMS and on installed plugins that supports this feature.
* If this is not supported by the underling LMS a UnsupportedOperationException will be presented
* within the Result.
*
* @param courseId The course identifier
* @return Result referencing to the Chapters model for the given course or to an error when happened. */
Result<Chapters> getCourseChapters(String courseId);
default FetchStatus getFetchStatus() {
return FetchStatus.ALL_FETCHED;
}
}

View file

@ -8,20 +8,9 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
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.QuizData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCourseAccess;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAccess;
@ -75,7 +64,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAcce
* or partial API Access and can flag missing or wrong {@link LmsSetup } attributes with the resulting
* {@link LmsSetupTestResult }.</br>
* SEB Server than uses an instance of this template to communicate with the an LMS. */
public interface LmsAPITemplate {
public interface LmsAPITemplate extends CourseAccessAPI, SEBRestrictionAPI {
/** Get the LMS type of the concrete template implementation
*
@ -87,110 +76,8 @@ public interface LmsAPITemplate {
* @return the underling {@link LmsSetup } configuration for this LmsAPITemplate */
LmsSetup lmsSetup();
// *******************************************************************
// **** Course API functions *****************************************
/** Performs a test for the underling {@link LmsSetup } configuration and checks if the
* LMS and the course API of the LMS can be accessed or if there are some difficulties,
* missing configuration data or connection/authentication errors.
*
* @return {@link LmsSetupTestResult } instance with the test result report */
LmsSetupTestResult testCourseAccessAPI();
/** Get an unsorted List of filtered {@link QuizData } from the LMS course/quiz API
*
* @param filterMap the {@link FilterMap } to get a filtered result. Possible filter attributes are:
*
* <pre>
* {@link QuizData.FILTER_ATTR_QUIZ_NAME } The quiz name filter text (exclude all names that do not contain the given text)
* {@link QuizData.FILTER_ATTR_START_TIME } The quiz start time (exclude all quizzes that starts before)
* </pre>
*
* @return Result of an unsorted List of filtered {@link QuizData } from the LMS course/quiz API
* or refer to an error when happened */
Result<List<QuizData>> getQuizzes(FilterMap filterMap);
/** Get all {@link QuizData } for the set of {@link QuizData } identifiers from LMS API in a collection
* of Result. If particular quizzes cannot be loaded because of errors or deletion,
* the the referencing QuizData will not be in the resulting list and an error is logged.
*
* @param ids the Set of Quiz identifiers to get the {@link QuizData } for
* @return Collection of all {@link QuizData } from the given id set */
Result<Collection<QuizData>> getQuizzes(Set<String> ids);
/** Get the quiz data with specified identifier.
*
* @param id the quiz data identifier
* @return Result refer to the quiz data or to an error when happened */
Result<QuizData> getQuiz(final String id);
/** Clears the underling caches if there are some for a particular implementation. */
void clearCache();
/** Convert an anonymous or temporary examineeUserId, sent by the SEB Client on LMS login,
* to LMS examinee account details by requesting them on the LMS API with the given examineeUserId
*
* @param examineeUserId the examinee user identifier derived from SEB Client
* @return a Result refer to the {@link ExamineeAccountDetails } instance or to an error when happened or not
* supported */
Result<ExamineeAccountDetails> getExamineeAccountDetails(String examineeUserId);
/** Used to convert an anonymous or temporary examineeUserId, sent by the SEB Client on LMS login,
* to a readable LMS examinee account name by requesting this on the LMS API with the given examineeUserId.
*
* If the underling concrete template implementation does not support this user name conversion,
* the given examineeSessionId shall be returned.
*
* @param examineeUserId the examinee user identifier derived from SEB Client
* @return a user account display name if supported or the given examineeSessionId if not. */
String getExamineeName(String examineeUserId);
/** Used to get a list of chapters (display name and chapter-identifier) that can be used to
* apply chapter-based SEB restriction for a specified course.
*
* The availability of this depends on the type of LMS and on installed plugins that supports this feature.
* If this is not supported by the underling LMS a UnsupportedOperationException will be presented
* within the Result.
*
* @param courseId The course identifier
* @return Result referencing to the Chapters model for the given course or to an error when happened. */
Result<Chapters> getCourseChapters(String courseId);
// ****************************************************************************
// **** SEB restriction API functions *****************************************
/** Performs a test for the underling {@link LmsSetup } configuration and checks if the
* LMS and the course restriction API of the LMS can be accessed or if there are some difficulties,
* missing configuration data or connection/authentication errors.
*
* @return {@link LmsSetupTestResult } instance with the test result report */
LmsSetupTestResult testCourseRestrictionAPI();
/** Get SEB restriction data form LMS within a {@link SEBRestrictionData } instance. The available restriction
* details
* depends on the type of LMS but shall at least contains the config-key(s) and the browser-exam-key(s).
*
* @param exam the exam to get the SEB restriction data for
* @return Result refer to the {@link SEBRestrictionData } instance or to an ResourceNotFoundException if the
* restriction is
* missing or to another exception on unexpected error case */
Result<SEBRestriction> getSEBClientRestriction(Exam exam);
/** Applies SEB Client restrictions to the LMS with the given attributes.
*
* @param externalExamId The exam/course identifier from LMS side (Exam.externalId)
* @param sebRestrictionData containing all data for SEB Client restriction to apply to the LMS
* @return Result refer to the given {@link SEBRestrictionData } if restriction was successful or to an error if
* not */
Result<SEBRestriction> applySEBClientRestriction(
String externalExamId,
SEBRestriction sebRestrictionData);
/** Releases an already applied SEB Client restriction within the LMS for a given Exam.
* This completely removes the SEB Client restriction on LMS side.
*
* @param exam the Exam to release the restriction for.
* @return Result refer to the given Exam if successful or to an error if not */
Result<Exam> releaseSEBClientRestriction(Exam exam);
default void dispose() {
clearCourseCache();
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result;
public interface SEBRestrictionAPI {
/** Performs a test for the underling {@link LmsSetup } configuration and checks if the
* LMS and the course restriction API of the LMS can be accessed or if there are some difficulties,
* missing configuration data or connection/authentication errors.
*
* @return {@link LmsSetupTestResult } instance with the test result report */
LmsSetupTestResult testCourseRestrictionAPI();
/** Get SEB restriction data form LMS within a {@link SEBRestrictionData } instance. The available restriction
* details
* depends on the type of LMS but shall at least contains the config-key(s) and the browser-exam-key(s).
*
* @param exam the exam to get the SEB restriction data for
* @return Result refer to the {@link SEBRestrictionData } instance or to an ResourceNotFoundException if the
* restriction is
* missing or to another exception on unexpected error case */
Result<SEBRestriction> getSEBClientRestriction(Exam exam);
/** Applies SEB Client restrictions to the LMS with the given attributes.
*
* @param externalExamId The exam/course identifier from LMS side (Exam.externalId)
* @param sebRestrictionData containing all data for SEB Client restriction to apply to the LMS
* @return Result refer to the given {@link SEBRestrictionData } if restriction was successful or to an error if
* not */
Result<SEBRestriction> applySEBClientRestriction(
String externalExamId,
SEBRestriction sebRestrictionData);
/** Releases an already applied SEB Client restriction within the LMS for a given Exam.
* This completely removes the SEB Client restriction on LMS side.
*
* @param exam the Exam to release the restriction for.
* @return Result refer to the given Exam if successful or to an error if not */
Result<Exam> releaseSEBClientRestriction(Exam exam);
}

View file

@ -16,10 +16,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.core.env.Environment;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
/** This implements an overall short time cache for QuizData objects for all implementing
@ -28,7 +26,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
* The QuizData are stored with a key composed from the id of the key
* </p>
* The EH-Cache can be configured in file ehcache.xml **/
public abstract class AbstractCachedCourseAccess extends AbstractCourseAccess {
public abstract class AbstractCachedCourseAccess {
private static final Logger log = LoggerFactory.getLogger(AbstractCachedCourseAccess.class);
@ -37,17 +35,12 @@ public abstract class AbstractCachedCourseAccess extends AbstractCourseAccess {
private final Cache cache;
protected AbstractCachedCourseAccess(
final AsyncService asyncService,
final Environment environment,
final CacheManager cacheManager) {
super(asyncService, environment);
protected AbstractCachedCourseAccess(final CacheManager cacheManager) {
this.cache = cacheManager.getCache(CACHE_NAME_QUIZ_DATA);
}
/** Used to clear the entire cache */
public void clearCache() {
public void clearCourseCache() {
final Object nativeCache = this.cache.getNativeCache();
if (nativeCache instanceof javax.cache.Cache) {
try {

View file

@ -20,7 +20,6 @@ import org.springframework.core.env.Environment;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker.State;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
@ -35,14 +34,6 @@ public abstract class AbstractCourseAccess {
private static final Logger log = LoggerFactory.getLogger(AbstractCourseAccess.class);
/** Fetch status that indicates an asynchronous quiz data fetch status if the
* concrete implementation has such. */
public enum FetchStatus {
ALL_FETCHED,
ASYNC_FETCH_RUNNING,
FETCH_ERROR
}
/** CircuitBreaker for protected quiz and course data requests */
protected final CircuitBreaker<List<QuizData>> allQuizzesRequest;
/** CircuitBreaker for protected quiz and course data requests */
@ -194,10 +185,4 @@ public abstract class AbstractCourseAccess {
/** Provides a supplier for the course chapter data request to use within the circuit breaker */
protected abstract Supplier<Chapters> getCourseChaptersSupplier(final String courseId);
protected FetchStatus getFetchStatus() {
if (this.quizzesRequest.getState() != State.CLOSED) {
return FetchStatus.FETCH_ERROR;
}
return FetchStatus.ALL_FETCHED;
}
}

View file

@ -96,12 +96,13 @@ public class LmsAPIServiceImpl implements LmsAPIService {
final LmsAPITemplate removedTemplate = this.cache
.remove(new CacheKey(lmsSetup.getModelId(), 0));
if (removedTemplate != null) {
removedTemplate.clearCache();
removedTemplate.clearCourseCache();
}
}
@Override
public void cleanup() {
this.cache.values().forEach(LmsAPITemplate::dispose);
this.cache.clear();
}

View file

@ -0,0 +1,407 @@
/*
* Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker.State;
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.QuizData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionAPI;
public class LmsAPITemplateAdapter implements LmsAPITemplate {
private static final Logger log = LoggerFactory.getLogger(LmsAPITemplateAdapter.class);
private final CourseAccessAPI courseAccessAPI;
private final SEBRestrictionAPI sebBestrictionAPI;
private final APITemplateDataSupplier apiTemplateDataSupplier;
/** CircuitBreaker for protected quiz and course data requests */
private final CircuitBreaker<List<QuizData>> allQuizzesRequest;
/** CircuitBreaker for protected quiz and course data requests */
private final CircuitBreaker<Collection<QuizData>> quizzesRequest;
/** CircuitBreaker for protected quiz and course data requests */
private final CircuitBreaker<QuizData> quizRequest;
/** CircuitBreaker for protected chapter data requests */
private final CircuitBreaker<Chapters> chaptersRequest;
/** CircuitBreaker for protected examinee account details requests */
private final CircuitBreaker<ExamineeAccountDetails> accountDetailRequest;
private final CircuitBreaker<SEBRestriction> restrictionRequest;
private final CircuitBreaker<Exam> releaseRestrictionRequest;
public LmsAPITemplateAdapter(
final AsyncService asyncService,
final Environment environment,
final APITemplateDataSupplier apiTemplateDataSupplier,
final CourseAccessAPI courseAccessAPI,
final SEBRestrictionAPI sebBestrictionAPI) {
this.courseAccessAPI = courseAccessAPI;
this.sebBestrictionAPI = sebBestrictionAPI;
this.apiTemplateDataSupplier = apiTemplateDataSupplier;
this.allQuizzesRequest = asyncService.createCircuitBreaker(
environment.getProperty(
"sebserver.webservice.circuitbreaker.quizzesRequest.attempts",
Integer.class,
3),
environment.getProperty(
"sebserver.webservice.circuitbreaker.quizzesRequest.blockingTime",
Long.class,
Constants.MINUTE_IN_MILLIS),
environment.getProperty(
"sebserver.webservice.circuitbreaker.quizzesRequest.timeToRecover",
Long.class,
Constants.MINUTE_IN_MILLIS));
this.quizzesRequest = asyncService.createCircuitBreaker(
environment.getProperty(
"sebserver.webservice.circuitbreaker.quizzesRequest.attempts",
Integer.class,
3),
environment.getProperty(
"sebserver.webservice.circuitbreaker.quizzesRequest.blockingTime",
Long.class,
Constants.SECOND_IN_MILLIS * 10),
environment.getProperty(
"sebserver.webservice.circuitbreaker.quizzesRequest.timeToRecover",
Long.class,
Constants.MINUTE_IN_MILLIS));
this.quizRequest = asyncService.createCircuitBreaker(
environment.getProperty(
"sebserver.webservice.circuitbreaker.quizzesRequest.attempts",
Integer.class,
3),
environment.getProperty(
"sebserver.webservice.circuitbreaker.quizzesRequest.blockingTime",
Long.class,
Constants.SECOND_IN_MILLIS * 10),
environment.getProperty(
"sebserver.webservice.circuitbreaker.quizzesRequest.timeToRecover",
Long.class,
Constants.MINUTE_IN_MILLIS));
this.chaptersRequest = asyncService.createCircuitBreaker(
environment.getProperty(
"sebserver.webservice.circuitbreaker.chaptersRequest.attempts",
Integer.class,
3),
environment.getProperty(
"sebserver.webservice.circuitbreaker.chaptersRequest.blockingTime",
Long.class,
Constants.SECOND_IN_MILLIS * 10),
environment.getProperty(
"sebserver.webservice.circuitbreaker.chaptersRequest.timeToRecover",
Long.class,
Constants.SECOND_IN_MILLIS * 30));
this.accountDetailRequest = asyncService.createCircuitBreaker(
environment.getProperty(
"sebserver.webservice.circuitbreaker.accountDetailRequest.attempts",
Integer.class,
2),
environment.getProperty(
"sebserver.webservice.circuitbreaker.accountDetailRequest.blockingTime",
Long.class,
Constants.SECOND_IN_MILLIS * 10),
environment.getProperty(
"sebserver.webservice.circuitbreaker.accountDetailRequest.timeToRecover",
Long.class,
Constants.SECOND_IN_MILLIS * 30));
this.restrictionRequest = asyncService.createCircuitBreaker(
environment.getProperty(
"sebserver.webservice.circuitbreaker.sebrestriction.attempts",
Integer.class,
2),
environment.getProperty(
"sebserver.webservice.circuitbreaker.sebrestriction.blockingTime",
Long.class,
Constants.SECOND_IN_MILLIS * 10),
environment.getProperty(
"sebserver.webservice.circuitbreaker.sebrestriction.timeToRecover",
Long.class,
Constants.SECOND_IN_MILLIS * 30));
this.releaseRestrictionRequest = asyncService.createCircuitBreaker(
environment.getProperty(
"sebserver.webservice.circuitbreaker.sebrestriction.attempts",
Integer.class,
2),
environment.getProperty(
"sebserver.webservice.circuitbreaker.sebrestriction.blockingTime",
Long.class,
Constants.SECOND_IN_MILLIS * 10),
environment.getProperty(
"sebserver.webservice.circuitbreaker.sebrestriction.timeToRecover",
Long.class,
Constants.SECOND_IN_MILLIS * 30));
}
@Override
public LmsType getType() {
return this.lmsSetup().getLmsType();
}
@Override
public LmsSetup lmsSetup() {
return this.apiTemplateDataSupplier.getLmsSetup();
}
@Override
public LmsSetupTestResult testCourseAccessAPI() {
if (this.courseAccessAPI != null) {
return this.courseAccessAPI.testCourseAccessAPI();
}
if (log.isDebugEnabled()) {
log.debug("Test Course Access API for LMSSetup: {}", lmsSetup());
}
return LmsSetupTestResult.ofAPINotSupported(getType());
}
@Override
public FetchStatus getFetchStatus() {
if (this.courseAccessAPI == null) {
return FetchStatus.FETCH_ERROR;
}
if (this.allQuizzesRequest.getState() != State.CLOSED) {
return FetchStatus.FETCH_ERROR;
}
return this.courseAccessAPI.getFetchStatus();
}
@Override
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
if (this.courseAccessAPI == null) {
return Result
.ofError(new UnsupportedOperationException("Course API Not Supported For: " + getType().name()));
}
if (log.isDebugEnabled()) {
log.debug("Get quizzes for LMSSetup: {}", lmsSetup());
}
return this.allQuizzesRequest.protectedRun(() -> this.courseAccessAPI
.getQuizzes(filterMap)
.onError(error -> log.error(
"Failed to run protectedQuizzesRequest: {}",
error.getMessage()))
.getOrThrow());
}
@Override
public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) {
if (this.courseAccessAPI == null) {
return Result
.ofError(new UnsupportedOperationException("Course API Not Supported For: " + getType().name()));
}
if (log.isDebugEnabled()) {
log.debug("Get quizzes {} for LMSSetup: {}", ids, lmsSetup());
}
return this.quizzesRequest.protectedRun(() -> this.courseAccessAPI
.getQuizzes(ids)
.onError(error -> log.error(
"Failed to run protectedQuizzesRequest: {}",
error.getMessage()))
.getOrThrow());
}
@Override
public Result<QuizData> getQuiz(final String id) {
if (this.courseAccessAPI == null) {
return Result
.ofError(new UnsupportedOperationException("Course API Not Supported For: " + getType().name()));
}
if (log.isDebugEnabled()) {
log.debug("Get quiz {} for LMSSetup: {}", id, lmsSetup());
}
return this.quizRequest.protectedRun(() -> this.courseAccessAPI
.getQuiz(id)
.onError(error -> log.error(
"Failed to run protectedQuizRequest: {}",
error.getMessage()))
.getOrThrow());
}
@Override
public void clearCourseCache() {
if (this.courseAccessAPI != null) {
if (log.isDebugEnabled()) {
log.debug("Clear course cache for LMSSetup: {}", lmsSetup());
}
this.courseAccessAPI.clearCourseCache();
}
}
@Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
if (this.courseAccessAPI == null) {
return Result
.ofError(new UnsupportedOperationException("Course API Not Supported For: " + getType().name()));
}
if (log.isDebugEnabled()) {
log.debug("Get examinee details {} for LMSSetup: {}", examineeUserId, lmsSetup());
}
return this.accountDetailRequest.protectedRun(() -> this.courseAccessAPI
.getExamineeAccountDetails(examineeUserId)
.onError(error -> log.error(
"Unexpected error while trying to get examinee account details: {}",
error.getMessage()))
.getOrThrow());
}
@Override
public String getExamineeName(final String examineeUserId) {
if (this.courseAccessAPI == null) {
throw new UnsupportedOperationException("Course API Not Supported For: " + getType().name());
}
if (log.isDebugEnabled()) {
log.debug("Get examinee name {} for LMSSetup: {}", examineeUserId, lmsSetup());
}
return this.courseAccessAPI.getExamineeName(examineeUserId);
}
@Override
public Result<Chapters> getCourseChapters(final String courseId) {
if (this.courseAccessAPI == null) {
return Result
.ofError(new UnsupportedOperationException("Course API Not Supported For: " + getType().name()));
}
if (log.isDebugEnabled()) {
log.debug("Get course chapters {} for LMSSetup: {}", courseId, lmsSetup());
}
return this.chaptersRequest.protectedRun(() -> this.courseAccessAPI
.getCourseChapters(courseId)
.onError(error -> log.error(
"Failed to run getCourseChapters: {}",
error.getMessage()))
.getOrThrow());
}
@Override
public LmsSetupTestResult testCourseRestrictionAPI() {
if (this.sebBestrictionAPI != null) {
return this.sebBestrictionAPI.testCourseRestrictionAPI();
}
if (log.isDebugEnabled()) {
log.debug("Test course restriction API for LMSSetup: {}", lmsSetup());
}
return LmsSetupTestResult.ofAPINotSupported(getType());
}
@Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
if (this.sebBestrictionAPI == null) {
return Result.ofError(
new UnsupportedOperationException("SEB Restriction API Not Supported For: " + getType().name()));
}
if (log.isDebugEnabled()) {
log.debug("Get course restriction: {} for LMSSetup: {}", exam.externalId, lmsSetup());
}
return this.restrictionRequest.protectedRun(() -> this.sebBestrictionAPI
.getSEBClientRestriction(exam)
.onError(error -> log.error(
"Failed to get SEB restrictions: {}",
error.getMessage()))
.getOrThrow());
}
@Override
public Result<SEBRestriction> applySEBClientRestriction(
final String externalExamId,
final SEBRestriction sebRestrictionData) {
if (this.sebBestrictionAPI == null) {
return Result.ofError(
new UnsupportedOperationException("SEB Restriction API Not Supported For: " + getType().name()));
}
if (log.isDebugEnabled()) {
log.debug("Apply course restriction: {} for LMSSetup: {}", externalExamId, lmsSetup());
}
return this.restrictionRequest.protectedRun(() -> this.sebBestrictionAPI
.applySEBClientRestriction(externalExamId, sebRestrictionData)
.onError(error -> log.error(
"Failed to apply SEB restrictions: {}",
error.getMessage()))
.getOrThrow());
}
@Override
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
if (this.sebBestrictionAPI == null) {
return Result.ofError(
new UnsupportedOperationException("SEB Restriction API Not Supported For: " + getType().name()));
}
if (log.isDebugEnabled()) {
log.debug("Release course restriction: {} for LMSSetup: {}", exam.externalId, lmsSetup());
}
return this.releaseRestrictionRequest.protectedRun(() -> this.sebBestrictionAPI
.releaseSEBClientRestriction(exam)
.onError(error -> log.error(
"Failed to release SEB restrictions: {}",
error.getMessage()))
.getOrThrow());
}
}

View file

@ -18,7 +18,6 @@ import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -28,7 +27,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.CacheManager;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@ -39,7 +37,6 @@ import org.springframework.web.client.RestTemplate;
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
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.ProxyData;
@ -78,11 +75,9 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final ClientCredentialService clientCredentialService,
final APITemplateDataSupplier apiTemplateDataSupplier,
final AsyncService asyncService,
final Environment environment,
final CacheManager cacheManager) {
super(asyncService, environment, cacheManager);
super(cacheManager);
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.clientCredentialService = clientCredentialService;
@ -170,7 +165,7 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
@Override
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
return this
.protectedQuizzesRequest(filterMap)
.allQuizzesRequest(filterMap)
.map(quizzes -> quizzes.stream()
.filter(LmsAPIService.quizFilterPredicate(filterMap))
.collect(Collectors.toList()));
@ -191,7 +186,7 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
});
if (!leftIds.isEmpty()) {
result.addAll(super.protectedQuizzesRequest(leftIds).getOrThrow());
result.addAll(quizzesRequest(leftIds).getOrThrow());
}
return result;
@ -205,7 +200,7 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
return Result.of(fromCache);
}
return super.protectedQuizRequest(id);
return quizRequest(id);
}
private List<QuizData> collectAllQuizzes(final AnsPersonalRestTemplate restTemplate) {
@ -279,33 +274,28 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
.collect(Collectors.toList());
}
@Override
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) {
protected Result<List<QuizData>> allQuizzesRequest(final FilterMap filterMap) {
// We cannot filter by from-date or partial names using the Ans search API.
// Only exact matches are permitted. So we're not implementing filtering
// on the API level and always retrieve all assignments and let SEB server
// do the filtering.
return () -> {
return Result.tryCatch(() -> {
final List<QuizData> res = getRestTemplate()
.map(this::collectAllQuizzes)
.getOrThrow();
super.putToCache(res);
return res;
};
});
}
@Override
protected Supplier<Collection<QuizData>> quizzesSupplier(final Set<String> ids) {
return () -> getRestTemplate()
.map(t -> this.getQuizzesByIds(t, ids))
.getOrThrow();
protected Result<Collection<QuizData>> quizzesRequest(final Set<String> ids) {
return getRestTemplate()
.map(t -> this.getQuizzesByIds(t, ids));
}
@Override
protected Supplier<QuizData> quizSupplier(final String id) {
return () -> getRestTemplate()
.map(t -> this.getQuizByAssignmentId(t, id))
.getOrThrow();
protected Result<QuizData> quizRequest(final String id) {
return getRestTemplate()
.map(t -> this.getQuizByAssignmentId(t, id));
}
private ExamineeAccountDetails getExamineeById(final RestTemplate restTemplate, final String id) {
@ -324,17 +314,21 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
}
@Override
protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String id) {
return () -> getRestTemplate()
.map(t -> this.getExamineeById(t, id))
.getOrThrow();
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
return getRestTemplate().map(t -> this.getExamineeById(t, examineeUserId));
}
@Override
protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) {
return () -> {
throw new UnsupportedOperationException("not available yet");
};
public String getExamineeName(final String examineeUserId) {
return getExamineeAccountDetails(examineeUserId)
.map(ExamineeAccountDetails::getDisplayName)
.onError(error -> log.warn("Failed to request user-name for ID: {}", error.getMessage(), error))
.getOr(examineeUserId);
}
@Override
public Result<Chapters> getCourseChapters(final String courseId) {
return Result.ofError(new UnsupportedOperationException("not available yet"));
}
@Override

View file

@ -22,6 +22,7 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter;
@Lazy
@Service
@ -63,13 +64,17 @@ public class AnsLmsAPITemplateFactory implements LmsAPITemplateFactory {
@Override
public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
return Result.tryCatch(() -> {
return new AnsLmsAPITemplate(
final AnsLmsAPITemplate ansLmsAPITemplate = new AnsLmsAPITemplate(
this.clientHttpRequestFactoryService,
this.clientCredentialService,
apiTemplateDataSupplier,
this.cacheManager);
return new LmsAPITemplateAdapter(
this.asyncService,
this.environment,
this.cacheManager);
apiTemplateDataSupplier,
ansLmsAPITemplate,
ansLmsAPITemplate);
});
}

View file

@ -17,14 +17,12 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.CacheManager;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@ -45,7 +43,6 @@ import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
@ -57,12 +54,13 @@ import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCourseAccess;
/** Implements the LmsAPITemplate for Open edX LMS Course API access.
*
* See also: https://course-catalog-api-guide.readthedocs.io */
final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
final class OpenEdxCourseAccess extends AbstractCachedCourseAccess implements CourseAccessAPI {
private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseAccess.class);
@ -84,11 +82,9 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
final JSONMapper jsonMapper,
final OpenEdxRestTemplateFactory openEdxRestTemplateFactory,
final WebserviceInfo webserviceInfo,
final AsyncService asyncService,
final Environment environment,
final CacheManager cacheManager) {
super(asyncService, environment, cacheManager);
super(cacheManager);
this.jsonMapper = jsonMapper;
this.openEdxRestTemplateFactory = openEdxRestTemplateFactory;
this.webserviceInfo = webserviceInfo;
@ -104,7 +100,8 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
return this.lmsSetupId;
}
LmsSetupTestResult initAPIAccess() {
@Override
public LmsSetupTestResult testCourseAccessAPI() {
final LmsSetupTestResult attributesCheck = this.openEdxRestTemplateFactory.test();
if (!attributesCheck.isOk()) {
@ -141,68 +138,18 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
}
@Override
protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String examineeSessionId) {
return () -> {
try {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final HttpHeaders httpHeaders = new HttpHeaders();
final OAuth2RestTemplate template = getRestTemplate()
.getOrThrow();
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
return getRestTemplate().map(this::collectAllQuizzes);
}
final String externalStartURI = this.webserviceInfo
.getLmsExternalAddressAlias(lmsSetup.lmsApiUrl);
final String uri = (externalStartURI != null)
? externalStartURI + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId
: lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeSessionId;
final String responseJSON = template.exchange(
uri,
HttpMethod.GET,
new HttpEntity<>(httpHeaders),
String.class)
.getBody();
final EdxUserDetails[] userDetails = this.jsonMapper.<EdxUserDetails[]> readValue(
responseJSON,
new TypeReference<EdxUserDetails[]>() {
});
if (userDetails == null || userDetails.length <= 0) {
throw new RuntimeException("No user details on Open edX API request");
}
final Map<String, String> additionalAttributes = new HashMap<>();
additionalAttributes.put("bio", userDetails[0].bio);
additionalAttributes.put("country", userDetails[0].country);
additionalAttributes.put("date_joined", userDetails[0].date_joined);
additionalAttributes.put("gender", userDetails[0].gender);
additionalAttributes.put("is_active", String.valueOf(userDetails[0].is_active));
additionalAttributes.put("mailing_address", userDetails[0].mailing_address);
additionalAttributes.put("secondary_email", userDetails[0].secondary_email);
return new ExamineeAccountDetails(
userDetails[0].username,
userDetails[0].name,
userDetails[0].username,
userDetails[0].email,
additionalAttributes);
} catch (final Exception e) {
throw new RuntimeException(e);
@Override
public Result<QuizData> getQuiz(final String id) {
return Result.tryCatch(() -> {
final QuizData fromCache = super.getFromCache(id);
if (fromCache != null) {
return fromCache;
}
};
}
@Override
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) {
return () -> getRestTemplate()
.map(this::collectAllQuizzes)
.getOrThrow();
}
@Override
protected Supplier<QuizData> quizSupplier(final String id) {
return () -> {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final String externalStartURI = getExternalLMSServerAddress(lmsSetup);
final QuizData quizData = quizDataOf(
@ -214,13 +161,13 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
super.putToCache(quizData);
}
return quizData;
};
});
}
@Override
protected Supplier<Collection<QuizData>> quizzesSupplier(final Set<String> ids) {
public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) {
if (ids.size() == 1) {
return () -> {
return Result.tryCatch(() -> {
final String id = ids.iterator().next();
@ -239,17 +186,73 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
getRestTemplate().getOrThrow(),
id),
externalStartURI));
};
});
} else {
return () -> getRestTemplate()
.map(template -> this.collectQuizzes(template, ids))
.getOrThrow();
return getRestTemplate().map(template -> this.collectQuizzes(template, ids));
}
}
@Override
protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) {
return () -> {
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
return Result.tryCatch(() -> {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final HttpHeaders httpHeaders = new HttpHeaders();
final OAuth2RestTemplate template = getRestTemplate()
.getOrThrow();
final String externalStartURI = this.webserviceInfo
.getLmsExternalAddressAlias(lmsSetup.lmsApiUrl);
final String uri = (externalStartURI != null)
? externalStartURI + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeUserId
: lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_USER_PROFILE_ENDPOINT + examineeUserId;
final String responseJSON = template.exchange(
uri,
HttpMethod.GET,
new HttpEntity<>(httpHeaders),
String.class)
.getBody();
final EdxUserDetails[] userDetails = this.jsonMapper.<EdxUserDetails[]> readValue(
responseJSON,
new TypeReference<EdxUserDetails[]>() {
});
if (userDetails == null || userDetails.length <= 0) {
throw new RuntimeException("No user details on Open edX API request");
}
final Map<String, String> additionalAttributes = new HashMap<>();
additionalAttributes.put("bio", userDetails[0].bio);
additionalAttributes.put("country", userDetails[0].country);
additionalAttributes.put("date_joined", userDetails[0].date_joined);
additionalAttributes.put("gender", userDetails[0].gender);
additionalAttributes.put("is_active", String.valueOf(userDetails[0].is_active));
additionalAttributes.put("mailing_address", userDetails[0].mailing_address);
additionalAttributes.put("secondary_email", userDetails[0].secondary_email);
return new ExamineeAccountDetails(
userDetails[0].username,
userDetails[0].name,
userDetails[0].username,
userDetails[0].email,
additionalAttributes);
});
}
@Override
public String getExamineeName(final String examineeUserId) {
return getExamineeAccountDetails(examineeUserId)
.map(ExamineeAccountDetails::getDisplayName)
.onError(error -> log.warn("Failed to request user-name for ID: {}", error.getMessage(), error))
.getOr(examineeUserId);
}
@Override
public Result<Chapters> getCourseChapters(final String courseId) {
return Result.tryCatch(() -> {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final String uri =
@ -262,7 +265,7 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
.filter(block -> OPEN_EDX_DEFAULT_BLOCKS_TYPE_CHAPTER.equals(block.type))
.map(block -> new Chapters.Chapter(block.display_name, block.block_id))
.collect(Collectors.toList()));
};
});
}
public Result<Collection<QuizData>> getQuizzesFromCache(final Set<String> ids) {
@ -279,7 +282,7 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
});
if (!leftIds.isEmpty()) {
result.addAll(super.protectedQuizzesRequest(leftIds).getOrThrow());
result.addAll(getQuizzes(leftIds).getOrThrow());
}
return result;

View file

@ -22,17 +22,20 @@ import org.springframework.web.client.HttpClientErrorException;
import com.fasterxml.jackson.core.JsonProcessingException;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.NoSEBRestrictionException;
/** The open edX SEB course restriction API implementation.
*
* See also : https://seb-openedx.readthedocs.io/en/latest/ */
public class OpenEdxCourseRestriction {
public class OpenEdxCourseRestriction implements SEBRestrictionAPI {
private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseRestriction.class);
@ -54,7 +57,8 @@ public class OpenEdxCourseRestriction {
this.openEdxRestTemplateFactory = openEdxRestTemplateFactory;
}
LmsSetupTestResult initAPIAccess() {
@Override
public LmsSetupTestResult testCourseRestrictionAPI() {
final LmsSetupTestResult attributesCheck = this.openEdxRestTemplateFactory.test();
if (!attributesCheck.isOk()) {
@ -95,21 +99,22 @@ public class OpenEdxCourseRestriction {
}
if (log.isDebugEnabled()) {
log.debug("Sucessfully checked SEB Open edX integration Plugin");
log.debug("Successfully checked SEB Open edX integration Plugin");
}
}
return LmsSetupTestResult.ofOkay(LmsType.OPEN_EDX);
}
Result<OpenEdxSEBRestriction> getSEBRestriction(final String courseId) {
@Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
if (log.isDebugEnabled()) {
log.debug("GET SEB Client restriction on course: {}", courseId);
log.debug("GET SEB Client restriction on exam: {}", exam);
}
final String courseId = exam.externalId;
final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
return Result.tryCatch(() -> {
final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId);
final HttpHeaders httpHeaders = new HttpHeaders();
@ -127,7 +132,7 @@ public class OpenEdxCourseRestriction {
if (log.isDebugEnabled()) {
log.debug("Successfully GET SEB Client restriction on course: {}", courseId);
}
return data;
return SEBRestriction.from(exam.id, data);
} catch (final HttpClientErrorException ce) {
if (ce.getStatusCode() == HttpStatus.NOT_FOUND || ce.getStatusCode() == HttpStatus.UNAUTHORIZED) {
throw new NoSEBRestrictionException(ce);
@ -137,17 +142,18 @@ public class OpenEdxCourseRestriction {
});
}
Result<Boolean> putSEBRestriction(
final String courseId,
final OpenEdxSEBRestriction restriction) {
@Override
public Result<SEBRestriction> applySEBClientRestriction(
final String externalExamId,
final SEBRestriction sebRestrictionData) {
if (log.isDebugEnabled()) {
log.debug("PUT SEB Client restriction on course: {} : {}", courseId, restriction);
log.debug("PUT SEB Client restriction on course: {} : {}", externalExamId, sebRestrictionData);
}
return Result.tryCatch(() -> {
final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId);
final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(externalExamId);
final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
@ -157,24 +163,26 @@ public class OpenEdxCourseRestriction {
.exchange(
url,
HttpMethod.PUT,
new HttpEntity<>(toJson(restriction), httpHeaders),
new HttpEntity<>(toJson(OpenEdxSEBRestriction.from(sebRestrictionData)), httpHeaders),
OpenEdxSEBRestriction.class)
.getBody();
if (log.isDebugEnabled()) {
log.debug("Successfully PUT SEB Client restriction on course: {} : {}", courseId, body);
log.debug("Successfully PUT SEB Client restriction on course: {} : {}", externalExamId, body);
}
return true;
return sebRestrictionData;
});
}
Result<Boolean> deleteSEBRestriction(final String courseId) {
@Override
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
if (log.isDebugEnabled()) {
log.debug("DELETE SEB Client restriction on course: {}", courseId);
log.debug("DELETE SEB Client restriction on exam: {}", exam);
}
final String courseId = exam.externalId;
return Result.tryCatch(() -> {
final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId);
@ -193,7 +201,7 @@ public class OpenEdxCourseRestriction {
if (log.isDebugEnabled()) {
log.debug("Successfully PUT SEB Client restriction on course: {}", courseId);
}
return true;
return exam;
} else {
throw new RuntimeException("Unexpected response for deletion: " + exchange);
}
@ -201,74 +209,6 @@ public class OpenEdxCourseRestriction {
}
// private BooleanSupplier pushSEBRestrictionFunction(
// final OpenEdxSEBRestriction restriction,
// final String courseId) {
//
// final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
// final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId);
// final HttpHeaders httpHeaders = new HttpHeaders();
// httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
// httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
// return () -> {
// final OpenEdxSEBRestriction body = this.restTemplate.exchange(
// url,
// HttpMethod.PUT,
// new HttpEntity<>(toJson(restriction), httpHeaders),
// OpenEdxSEBRestriction.class)
// .getBody();
//
// if (log.isDebugEnabled()) {
// log.debug("Successfully PUT SEB Client restriction on course: {} : {}", courseId, body);
// }
//
// return true;
// };
// }
// private BooleanSupplier deleteSEBRestrictionFunction(final String courseId) {
//
// final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
// final String url = lmsSetup.lmsApiUrl + getSEBRestrictionUrl(courseId);
// return () -> {
// final HttpHeaders httpHeaders = new HttpHeaders();
// httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
// final ResponseEntity<Object> exchange = this.restTemplate.exchange(
// url,
// HttpMethod.DELETE,
// new HttpEntity<>(httpHeaders),
// Object.class);
//
// if (exchange.getStatusCode() == HttpStatus.NO_CONTENT) {
// if (log.isDebugEnabled()) {
// log.debug("Successfully PUT SEB Client restriction on course: {}", courseId);
// }
// } else {
// log.error("Unexpected response for deletion: {}", exchange);
// return false;
// }
//
// return true;
// };
// }
// private Result<Boolean> handleSEBRestriction(final BooleanSupplier task) {
// return getRestTemplate()
// .map(restTemplate -> {
// try {
// return task.getAsBoolean();
// } catch (final HttpClientErrorException ce) {
// if (ce.getStatusCode() == HttpStatus.UNAUTHORIZED) {
// throw new APIMessageException(APIMessage.ErrorMessage.UNAUTHORIZED.of(ce.getMessage()
// + " Unable to get access for API. Please check the corresponding LMS Setup "));
// }
// throw ce;
// } catch (final Exception e) {
// throw new RuntimeException("Unexpected: ", e);
// }
// });
// }
private String getSEBRestrictionUrl(final String courseId) {
return String.format(OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_PATH, courseId);
}

View file

@ -1,159 +0,0 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.OpenEdxSEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
/** The OpenEdxLmsAPITemplate is separated into two parts:
* - OpenEdxCourseAccess implements the course access API
* - OpenEdxCourseRestriction implements the SEB restriction API
* - Both uses the OpenEdxRestTemplateFactory to create a spring based RestTemplate to access the LMS API */
final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
private static final Logger log = LoggerFactory.getLogger(OpenEdxLmsAPITemplate.class);
private final OpenEdxCourseAccess openEdxCourseAccess;
private final OpenEdxCourseRestriction openEdxCourseRestriction;
OpenEdxLmsAPITemplate(
final OpenEdxCourseAccess openEdxCourseAccess,
final OpenEdxCourseRestriction openEdxCourseRestriction) {
this.openEdxCourseAccess = openEdxCourseAccess;
this.openEdxCourseRestriction = openEdxCourseRestriction;
}
@Override
public LmsType getType() {
return LmsType.OPEN_EDX;
}
@Override
public LmsSetup lmsSetup() {
return this.openEdxCourseAccess
.getApiTemplateDataSupplier()
.getLmsSetup();
}
@Override
public LmsSetupTestResult testCourseAccessAPI() {
return this.openEdxCourseAccess.initAPIAccess();
}
@Override
public LmsSetupTestResult testCourseRestrictionAPI() {
return this.openEdxCourseRestriction.initAPIAccess();
}
@Override
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
return this.openEdxCourseAccess
.protectedQuizzesRequest(filterMap)
.map(quizzes -> quizzes.stream()
.filter(LmsAPIService.quizFilterPredicate(filterMap))
.collect(Collectors.toList()));
}
@Override
public Result<QuizData> getQuiz(final String id) {
final QuizData quizFromCache = this.openEdxCourseAccess.getQuizFromCache(id);
if (quizFromCache != null) {
return Result.of(quizFromCache);
}
return this.openEdxCourseAccess.protectedQuizRequest(id);
}
@Override
public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) {
return this.openEdxCourseAccess.getQuizzesFromCache(ids);
}
@Override
public void clearCache() {
this.openEdxCourseAccess.clearCache();
}
@Override
public Result<Chapters> getCourseChapters(final String courseId) {
return Result.tryCatch(() -> this.openEdxCourseAccess
.getCourseChaptersSupplier(courseId)
.get());
}
@Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
return this.openEdxCourseAccess.getExamineeAccountDetails(examineeSessionId);
}
@Override
public String getExamineeName(final String examineeSessionId) {
return this.openEdxCourseAccess.getExamineeName(examineeSessionId);
}
@Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
if (log.isDebugEnabled()) {
log.debug("Get SEB Client restriction for Exam: {}", exam);
}
return this.openEdxCourseRestriction
.getSEBRestriction(exam.externalId)
.map(restriction -> SEBRestriction.from(exam.id, restriction));
}
@Override
public Result<SEBRestriction> applySEBClientRestriction(
final String externalExamId,
final SEBRestriction sebRestrictionData) {
if (log.isDebugEnabled()) {
log.debug("Apply SEB Client restriction: {}", sebRestrictionData);
}
return this.openEdxCourseRestriction
.putSEBRestriction(
externalExamId,
OpenEdxSEBRestriction.from(sebRestrictionData))
.map(result -> sebRestrictionData);
}
@Override
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
if (log.isDebugEnabled()) {
log.debug("Release SEB Client restriction for Exam: {}", exam);
}
return this.openEdxCourseRestriction
.deleteSEBRestriction(exam.externalId)
.map(result -> exam);
}
}

View file

@ -27,6 +27,7 @@ import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter;
@Lazy
@Service
@ -87,8 +88,6 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory {
this.jsonMapper,
openEdxRestTemplateFactory,
this.webserviceInfo,
this.asyncService,
this.environment,
this.cacheManager);
final OpenEdxCourseRestriction openEdxCourseRestriction = new OpenEdxCourseRestriction(
@ -96,7 +95,10 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory {
openEdxRestTemplateFactory,
this.restrictionAPIPushCount);
return new OpenEdxLmsAPITemplate(
return new LmsAPITemplateAdapter(
this.asyncService,
this.environment,
apiTemplateDataSupplier,
openEdxCourseAccess,
openEdxCourseRestriction);
});

View file

@ -1,323 +1,224 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
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.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
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.impl.AbstractCourseAccess;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.NoSEBRestrictionException;
public class MockupLmsAPITemplate implements LmsAPITemplate {
private static final Logger log = LoggerFactory.getLogger(MockupLmsAPITemplate.class);
private final Collection<QuizData> mockups;
private final WebserviceInfo webserviceInfo;
private final APITemplateDataSupplier apiTemplateDataSupplier;
private final AbstractCourseAccess abstractCourseAccess;
MockupLmsAPITemplate(
final AsyncService asyncService,
final Environment environment,
final APITemplateDataSupplier apiTemplateDataSupplier,
final WebserviceInfo webserviceInfo) {
this.apiTemplateDataSupplier = apiTemplateDataSupplier;
this.webserviceInfo = webserviceInfo;
this.mockups = new ArrayList<>();
this.abstractCourseAccess = new AbstractCourseAccess(asyncService, environment) {
@Override
protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String examineeSessionId) {
return () -> MockupLmsAPITemplate.this
.getExamineeAccountDetails_protected(examineeSessionId)
.getOrThrow();
}
@Override
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) {
return () -> MockupLmsAPITemplate.this.getQuizzes_protected(filterMap).getOrThrow();
}
@Override
protected Supplier<Collection<QuizData>> quizzesSupplier(final Set<String> ids) {
return () -> MockupLmsAPITemplate.this.getQuizzes_protected(ids).getOrThrow();
}
@Override
protected Supplier<QuizData> quizSupplier(final String id) {
return () -> MockupLmsAPITemplate.this.getQuiz_protected(id).getOrThrow();
}
@Override
protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) {
throw new UnsupportedOperationException("Course Chapter feature not supported");
}
};
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final Long lmsSetupId = lmsSetup.id;
final Long institutionId = lmsSetup.getInstitutionId();
final LmsType lmsType = lmsSetup.getLmsType();
this.mockups.add(new QuizData(
"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>",
"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>",
"2018-07-30T09:00:00Z", "2018-08-01T00:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz4", institutionId, lmsSetupId, lmsType, "Demo Quiz 4 (MOCKUP)", "<p>Demo Quiz Mockup</p>",
"2018-01-01T00:00:00Z", "2025-01-01T00:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz5", institutionId, lmsSetupId, lmsType, "Demo Quiz 5 (MOCKUP)", "<p>Demo Quiz Mockup</p>",
"2018-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz6", institutionId, lmsSetupId, lmsType, "Demo Quiz 6 (MOCKUP)", "<p>Demo Quiz Mockup</p>",
"2019-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz7", institutionId, lmsSetupId, lmsType, "Demo Quiz 7 (MOCKUP)", "<p>Demo Quiz Mockup</p>",
"2018-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz10", institutionId, lmsSetupId, lmsType, "Demo Quiz 10 (MOCKUP)",
"Starts in a minute and ends after five minutes",
DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS)
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
DateTime.now(DateTimeZone.UTC).plus(6 * Constants.MINUTE_IN_MILLIS)
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
"http://lms.mockup.com/api/"));
}
@Override
public LmsType getType() {
return LmsType.MOCKUP;
}
@Override
public LmsSetup lmsSetup() {
return this.apiTemplateDataSupplier.getLmsSetup();
}
private List<APIMessage> checkAttributes() {
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final ClientCredentials lmsClientCredentials = this.apiTemplateDataSupplier.getLmsClientCredentials();
final List<APIMessage> missingAttrs = new ArrayList<>();
if (StringUtils.isBlank(lmsSetup.lmsApiUrl)) {
missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_URL,
"lmsSetup:lmsUrl:notNull"));
}
if (!lmsClientCredentials.hasClientId()) {
missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_CLIENTNAME,
"lmsSetup:lmsClientname:notNull"));
}
if (!lmsClientCredentials.hasSecret()) {
missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_CLIENTSECRET,
"lmsSetup:lmsClientsecret:notNull"));
}
return missingAttrs;
}
@Override
public LmsSetupTestResult testCourseAccessAPI() {
log.info("Test Lms Binding for Mockup and LmsSetup: {}", this.apiTemplateDataSupplier.getLmsSetup());
final List<APIMessage> missingAttrs = checkAttributes();
if (!missingAttrs.isEmpty()) {
return LmsSetupTestResult.ofMissingAttributes(LmsType.MOCKUP, missingAttrs);
}
if (authenticate()) {
return LmsSetupTestResult.ofOkay(LmsType.MOCKUP);
} else {
return LmsSetupTestResult.ofTokenRequestError(LmsType.MOCKUP, "Illegal access");
}
}
@Override
public LmsSetupTestResult testCourseRestrictionAPI() {
return LmsSetupTestResult.ofQuizRestrictionAPIError(LmsType.MOCKUP, "unsupported");
}
@Override
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
return this.abstractCourseAccess.protectedQuizzesRequest(filterMap);
}
private Result<List<QuizData>> getQuizzes_protected(final FilterMap filterMap) {
return Result.tryCatch(() -> {
if (!authenticate()) {
throw new IllegalArgumentException("Wrong clientId or secret");
}
return this.mockups
.stream()
.map(this::getExternalAddressAlias)
.filter(LmsAPIService.quizFilterPredicate(filterMap))
.collect(Collectors.toList());
});
}
@Override
public Result<QuizData> getQuiz(final String id) {
return this.abstractCourseAccess.protectedQuizRequest(id);
}
private Result<QuizData> getQuiz_protected(final String id) {
return Result.of(this.mockups
.stream()
.filter(q -> id.equals(q.id))
.findFirst()
.get());
}
@Override
public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) {
return this.abstractCourseAccess.protectedQuizzesRequest(ids);
}
private Result<Collection<QuizData>> getQuizzes_protected(final Set<String> ids) {
return Result.tryCatch(() -> {
if (!authenticate()) {
throw new IllegalArgumentException("Wrong clientId or secret");
}
return this.mockups
.stream()
.map(this::getExternalAddressAlias)
.filter(mock -> ids.contains(mock.id))
.collect(Collectors.toList());
});
}
@Override
public void clearCache() {
}
@Override
public Result<Chapters> getCourseChapters(final String courseId) {
return this.abstractCourseAccess.getCourseChapters(courseId);
}
@Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
return this.abstractCourseAccess.getExamineeAccountDetails(examineeSessionId);
}
private Result<ExamineeAccountDetails> getExamineeAccountDetails_protected(final String examineeSessionId) {
return Result.ofError(new UnsupportedOperationException());
}
@Override
public String getExamineeName(final String examineeSessionId) {
return "--" + " (" + examineeSessionId + ")";
}
@Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
log.info("Apply SEB Client restriction for Exam: {}", exam);
return Result.ofError(new NoSEBRestrictionException());
}
@Override
public Result<SEBRestriction> applySEBClientRestriction(
final String externalExamId,
final SEBRestriction sebRestrictionData) {
log.info("Apply SEB Client restriction: {}", sebRestrictionData);
return Result.of(sebRestrictionData);
}
@Override
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
log.info("Release SEB Client restriction for Exam: {}", exam);
return Result.of(exam);
}
private QuizData getExternalAddressAlias(final QuizData quizData) {
final String externalAddressAlias = this.webserviceInfo.getLmsExternalAddressAlias("lms.mockup.com");
if (StringUtils.isNoneBlank(externalAddressAlias)) {
try {
final String _externalStartURI =
this.webserviceInfo.getHttpScheme() +
"://" + externalAddressAlias + "/api/";
return new QuizData(
quizData.id, quizData.institutionId, quizData.lmsSetupId, quizData.lmsType,
quizData.name, quizData.description, quizData.startTime,
quizData.endTime, _externalStartURI, quizData.additionalAttributes);
} catch (final Exception e) {
log.error("Failed to create external address from alias: ", e);
return quizData;
}
} else {
return quizData;
}
}
private boolean authenticate() {
try {
final CharSequence plainClientId = this.apiTemplateDataSupplier.getLmsClientCredentials().clientId;
if (plainClientId == null || plainClientId.length() <= 0) {
throw new IllegalAccessException("Wrong client credential");
}
return true;
} catch (final Exception e) {
log.info("Authentication failed: ", e);
return false;
}
}
}
/*
* Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
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.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
public class MockCourseAccessAPI implements CourseAccessAPI {
private final Collection<QuizData> mockups;
private final WebserviceInfo webserviceInfo;
private final APITemplateDataSupplier apiTemplateDataSupplier;
public MockCourseAccessAPI(
final APITemplateDataSupplier apiTemplateDataSupplier,
final WebserviceInfo webserviceInfo) {
this.apiTemplateDataSupplier = apiTemplateDataSupplier;
this.webserviceInfo = webserviceInfo;
this.mockups = new ArrayList<>();
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final Long lmsSetupId = lmsSetup.id;
final Long institutionId = lmsSetup.getInstitutionId();
final LmsType lmsType = lmsSetup.getLmsType();
this.mockups.add(new QuizData(
"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>",
"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>",
"2018-07-30T09:00:00Z", "2018-08-01T00:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz4", institutionId, lmsSetupId, lmsType, "Demo Quiz 4 (MOCKUP)", "<p>Demo Quiz Mockup</p>",
"2018-01-01T00:00:00Z", "2025-01-01T00:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz5", institutionId, lmsSetupId, lmsType, "Demo Quiz 5 (MOCKUP)", "<p>Demo Quiz Mockup</p>",
"2018-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz6", institutionId, lmsSetupId, lmsType, "Demo Quiz 6 (MOCKUP)", "<p>Demo Quiz Mockup</p>",
"2019-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz7", institutionId, lmsSetupId, lmsType, "Demo Quiz 7 (MOCKUP)", "<p>Demo Quiz Mockup</p>",
"2018-01-01T09:00:00Z", "2025-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz10", institutionId, lmsSetupId, lmsType, "Demo Quiz 10 (MOCKUP)",
"Starts in a minute and ends after five minutes",
DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS)
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
DateTime.now(DateTimeZone.UTC).plus(6 * Constants.MINUTE_IN_MILLIS)
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
"http://lms.mockup.com/api/"));
}
@Override
public LmsSetupTestResult testCourseAccessAPI() {
log.info("Test Lms Binding for Mockup and LmsSetup: {}", this.apiTemplateDataSupplier.getLmsSetup());
final List<APIMessage> missingAttrs = checkAttributes();
if (!missingAttrs.isEmpty()) {
return LmsSetupTestResult.ofMissingAttributes(LmsType.MOCKUP, missingAttrs);
}
if (authenticate()) {
return LmsSetupTestResult.ofOkay(LmsType.MOCKUP);
} else {
return LmsSetupTestResult.ofTokenRequestError(LmsType.MOCKUP, "Illegal access");
}
}
private List<APIMessage> checkAttributes() {
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final ClientCredentials lmsClientCredentials = this.apiTemplateDataSupplier.getLmsClientCredentials();
final List<APIMessage> missingAttrs = new ArrayList<>();
if (StringUtils.isBlank(lmsSetup.lmsApiUrl)) {
missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_URL,
"lmsSetup:lmsUrl:notNull"));
}
if (!lmsClientCredentials.hasClientId()) {
missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_CLIENTNAME,
"lmsSetup:lmsClientname:notNull"));
}
if (!lmsClientCredentials.hasSecret()) {
missingAttrs.add(APIMessage.fieldValidationError(
LMS_SETUP.ATTR_LMS_CLIENTSECRET,
"lmsSetup:lmsClientsecret:notNull"));
}
return missingAttrs;
}
@Override
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
return Result.tryCatch(() -> {
if (!authenticate()) {
throw new IllegalArgumentException("Wrong clientId or secret");
}
return this.mockups
.stream()
.map(this::getExternalAddressAlias)
.filter(LmsAPIService.quizFilterPredicate(filterMap))
.collect(Collectors.toList());
});
}
@Override
public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) {
return Result.tryCatch(() -> {
if (!authenticate()) {
throw new IllegalArgumentException("Wrong clientId or secret");
}
return this.mockups
.stream()
.map(this::getExternalAddressAlias)
.filter(mock -> ids.contains(mock.id))
.collect(Collectors.toList());
});
}
@Override
public Result<QuizData> getQuiz(final String id) {
return Result.of(this.mockups
.stream()
.filter(q -> id.equals(q.id))
.findFirst()
.get());
}
@Override
public void clearCourseCache() {
// No cache here
}
@Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
return Result.ofError(new UnsupportedOperationException());
}
@Override
public String getExamineeName(final String examineeUserId) {
return "--" + " (" + examineeUserId + ")";
}
@Override
public Result<Chapters> getCourseChapters(final String courseId) {
return Result.ofError(new UnsupportedOperationException("Course Chapter feature not supported"));
}
private boolean authenticate() {
try {
final CharSequence plainClientId = this.apiTemplateDataSupplier.getLmsClientCredentials().clientId;
if (plainClientId == null || plainClientId.length() <= 0) {
throw new IllegalAccessException("Wrong client credential");
}
return true;
} catch (final Exception e) {
log.info("Authentication failed: ", e);
return false;
}
}
private QuizData getExternalAddressAlias(final QuizData quizData) {
final String externalAddressAlias = this.webserviceInfo.getLmsExternalAddressAlias("lms.mockup.com");
if (StringUtils.isNoneBlank(externalAddressAlias)) {
try {
final String _externalStartURI =
this.webserviceInfo.getHttpScheme() +
"://" + externalAddressAlias + "/api/";
return new QuizData(
quizData.id, quizData.institutionId, quizData.lmsSetupId, quizData.lmsType,
quizData.name, quizData.description, quizData.startTime,
quizData.endTime, _externalStartURI, quizData.additionalAttributes);
} catch (final Exception e) {
log.error("Failed to create external address from alias: ", e);
return quizData;
}
} else {
return quizData;
}
}
}

View file

@ -20,6 +20,7 @@ import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter;
@Lazy
@Service
@ -47,11 +48,19 @@ public class MockLmsAPITemplateFactory implements LmsAPITemplateFactory {
@Override
public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
return Result.tryCatch(() -> new MockupLmsAPITemplate(
final MockCourseAccessAPI mockCourseAccessAPI = new MockCourseAccessAPI(
apiTemplateDataSupplier,
this.webserviceInfo);
final MockSEBRestrictionAPI mockSEBRestrictionAPI = new MockSEBRestrictionAPI();
return Result.tryCatch(() -> new LmsAPITemplateAdapter(
this.asyncService,
this.environment,
apiTemplateDataSupplier,
this.webserviceInfo));
mockCourseAccessAPI,
mockSEBRestrictionAPI));
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.NoSEBRestrictionException;
public class MockSEBRestrictionAPI implements SEBRestrictionAPI {
private static final Logger log = LoggerFactory.getLogger(MockSEBRestrictionAPI.class);
@Override
public LmsSetupTestResult testCourseRestrictionAPI() {
return LmsSetupTestResult.ofQuizRestrictionAPIError(LmsType.MOCKUP, "unsupported");
}
@Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
log.info("Apply SEB Client restriction for Exam: {}", exam);
return Result.ofError(new NoSEBRestrictionException());
}
@Override
public Result<SEBRestriction> applySEBClientRestriction(
final String externalExamId,
final SEBRestriction sebRestrictionData) {
log.info("Apply SEB Client restriction: {}", sebRestrictionData);
return Result.of(sebRestrictionData);
}
@Override
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
log.info("Release SEB Client restriction for Exam: {}", exam);
return Result.of(exam);
}
}

View file

@ -16,7 +16,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -37,9 +36,6 @@ import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker.State;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
@ -50,7 +46,7 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAccess;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseDataAsyncLoader.CourseDataShort;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseDataAsyncLoader.CourseQuizShort;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate;
@ -69,7 +65,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestT
* background task if needed and return immediately to do not block the request.
* The planed Moodle integration on moodle side also defines an improved course access API. This will
* possibly make this synchronous fetch strategy obsolete in the future. */
public class MoodleCourseAccess extends AbstractCourseAccess {
public class MoodleCourseAccess implements CourseAccessAPI {
private static final long INITIAL_WAIT_TIME = 3 * Constants.SECOND_IN_MILLIS;
@ -94,7 +90,6 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
private final JSONMapper jsonMapper;
private final MoodleRestTemplateFactory moodleRestTemplateFactory;
private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader;
private final CircuitBreaker<List<QuizData>> allQuizzesRequest;
private final boolean prependShortCourseName;
private MoodleAPIRestTemplate restTemplate;
@ -103,28 +98,12 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
final JSONMapper jsonMapper,
final MoodleRestTemplateFactory moodleRestTemplateFactory,
final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader,
final AsyncService asyncService,
final Environment environment) {
super(asyncService, environment);
this.jsonMapper = jsonMapper;
this.moodleCourseDataAsyncLoader = moodleCourseDataAsyncLoader;
this.moodleRestTemplateFactory = moodleRestTemplateFactory;
this.allQuizzesRequest = asyncService.createCircuitBreaker(
environment.getProperty(
"sebserver.webservice.circuitbreaker.allQuizzesRequest.attempts",
Integer.class,
3),
environment.getProperty(
"sebserver.webservice.circuitbreaker.allQuizzesRequest.blockingTime",
Long.class,
Constants.MINUTE_IN_MILLIS),
environment.getProperty(
"sebserver.webservice.circuitbreaker.allQuizzesRequest.timeToRecover",
Long.class,
Constants.MINUTE_IN_MILLIS));
this.prependShortCourseName = BooleanUtils.toBoolean(environment.getProperty(
"sebserver.webservice.lms.moodle.prependShortCourseName",
Constants.TRUE_STRING));
@ -135,67 +114,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
}
@Override
protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String examineeSessionId) {
return () -> {
try {
final MoodleAPIRestTemplate template = getRestTemplate()
.getOrThrow();
final MultiValueMap<String, String> queryAttributes = new LinkedMultiValueMap<>();
queryAttributes.add("field", "id");
queryAttributes.add("values[0]", examineeSessionId);
final String userDetailsJSON = template.callMoodleAPIFunction(
MOODLE_USER_PROFILE_API_FUNCTION_NAME,
queryAttributes);
if (checkAccessDeniedError(userDetailsJSON)) {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
log.error("Get access denied error from Moodle: {} for API call: {}, response: {}",
lmsSetup,
MOODLE_USER_PROFILE_API_FUNCTION_NAME,
Utils.truncateText(userDetailsJSON, 2000));
throw new RuntimeException("No user details on Moodle API request (access-denied)");
}
final MoodleUserDetails[] userDetails = this.jsonMapper.<MoodleUserDetails[]> readValue(
userDetailsJSON,
new TypeReference<MoodleUserDetails[]>() {
});
if (userDetails == null || userDetails.length <= 0) {
throw new RuntimeException("No user details on Moodle API request");
}
final Map<String, String> additionalAttributes = new HashMap<>();
additionalAttributes.put("firstname", userDetails[0].firstname);
additionalAttributes.put("lastname", userDetails[0].lastname);
additionalAttributes.put("department", userDetails[0].department);
additionalAttributes.put("firstaccess", String.valueOf(userDetails[0].firstaccess));
additionalAttributes.put("lastaccess", String.valueOf(userDetails[0].lastaccess));
additionalAttributes.put("auth", userDetails[0].auth);
additionalAttributes.put("suspended", String.valueOf(userDetails[0].suspended));
additionalAttributes.put("confirmed", String.valueOf(userDetails[0].confirmed));
additionalAttributes.put("lang", userDetails[0].lang);
additionalAttributes.put("theme", userDetails[0].theme);
additionalAttributes.put("timezone", userDetails[0].timezone);
additionalAttributes.put("description", userDetails[0].description);
additionalAttributes.put("mailformat", String.valueOf(userDetails[0].mailformat));
additionalAttributes.put("descriptionformat", String.valueOf(userDetails[0].descriptionformat));
return new ExamineeAccountDetails(
userDetails[0].id,
userDetails[0].fullname,
userDetails[0].username,
userDetails[0].email,
additionalAttributes);
} catch (final Exception e) {
throw new RuntimeException(e);
}
};
}
LmsSetupTestResult initAPIAccess() {
public LmsSetupTestResult testCourseAccessAPI() {
final LmsSetupTestResult attributesCheck = this.moodleRestTemplateFactory.test();
if (!attributesCheck.isOk()) {
return attributesCheck;
@ -223,7 +142,45 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
return LmsSetupTestResult.ofOkay(LmsType.MOODLE);
}
public Result<QuizData> getQuizFromCache(final String id) {
@Override
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
return Result.tryCatch(() -> getRestTemplate()
.map(template -> collectAllQuizzes(template, filterMap))
.getOr(Collections.emptyList()));
}
@Override
public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) {
return Result.tryCatch(() -> {
final List<QuizData> cached = getCached();
final List<QuizData> available = (cached != null)
? cached
: Collections.emptyList();
final Map<String, QuizData> quizMapping = available
.stream()
.collect(Collectors.toMap(q -> q.id, Function.identity()));
if (!quizMapping.keySet().containsAll(ids)) {
final Map<String, QuizData> collect = getRestTemplate()
.map(template -> getQuizzesForIds(template, ids))
.getOrElse(() -> Collections.emptyList())
.stream()
.collect(Collectors.toMap(qd -> qd.id, Function.identity()));
if (collect != null) {
quizMapping.clear();
quizMapping.putAll(collect);
}
}
return quizMapping.values();
});
}
@Override
public Result<QuizData> getQuiz(final String id) {
return Result.tryCatch(() -> {
final Map<String, CourseDataShort> cachedCourseData = this.moodleCourseDataAsyncLoader
@ -255,76 +212,93 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
}
// get from LMS in protected request
return super.protectedQuizRequest(id).getOrThrow();
});
}
public Result<Collection<QuizData>> getQuizzesFromCache(final Set<String> ids) {
return Result.tryCatch(() -> {
final List<QuizData> cached = getCached();
final List<QuizData> available = (cached != null)
? cached
: Collections.emptyList();
final Map<String, QuizData> quizMapping = available
.stream()
.collect(Collectors.toMap(q -> q.id, Function.identity()));
if (!quizMapping.keySet().containsAll(ids)) {
final Map<String, QuizData> collect = super.quizzesRequest
.protectedRun(quizzesSupplier(ids))
.onError(error -> log.error("Failed to get quizzes by ids: ", error))
.getOrElse(() -> Collections.emptyList())
.stream()
.collect(Collectors.toMap(qd -> qd.id, Function.identity()));
if (collect != null) {
quizMapping.clear();
quizMapping.putAll(collect);
}
}
return quizMapping.values();
});
}
@Override
protected Supplier<QuizData> quizSupplier(final String id) {
return () -> {
final Set<String> ids = Stream.of(id).collect(Collectors.toSet());
return getRestTemplate()
.map(template -> getQuizzesForIds(template, ids))
.getOr(Collections.emptyList())
.get(0);
};
});
}
@Override
protected Supplier<Collection<QuizData>> quizzesSupplier(final Set<String> ids) {
return () -> getRestTemplate()
.map(template -> getQuizzesForIds(template, ids))
.getOr(Collections.emptyList());
public void clearCourseCache() {
// TODO Auto-generated method stub
}
@Override
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) {
return () -> getRestTemplate()
.map(template -> collectAllQuizzes(template, filterMap))
.getOr(Collections.emptyList());
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
return Result.tryCatch(() -> {
final MoodleAPIRestTemplate template = getRestTemplate()
.getOrThrow();
final MultiValueMap<String, String> queryAttributes = new LinkedMultiValueMap<>();
queryAttributes.add("field", "id");
queryAttributes.add("values[0]", examineeSessionId);
final String userDetailsJSON = template.callMoodleAPIFunction(
MOODLE_USER_PROFILE_API_FUNCTION_NAME,
queryAttributes);
if (checkAccessDeniedError(userDetailsJSON)) {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
log.error("Get access denied error from Moodle: {} for API call: {}, response: {}",
lmsSetup,
MOODLE_USER_PROFILE_API_FUNCTION_NAME,
Utils.truncateText(userDetailsJSON, 2000));
throw new RuntimeException("No user details on Moodle API request (access-denied)");
}
final MoodleUserDetails[] userDetails = this.jsonMapper.<MoodleUserDetails[]> readValue(
userDetailsJSON,
new TypeReference<MoodleUserDetails[]>() {
});
if (userDetails == null || userDetails.length <= 0) {
throw new RuntimeException("No user details on Moodle API request");
}
final Map<String, String> additionalAttributes = new HashMap<>();
additionalAttributes.put("firstname", userDetails[0].firstname);
additionalAttributes.put("lastname", userDetails[0].lastname);
additionalAttributes.put("department", userDetails[0].department);
additionalAttributes.put("firstaccess", String.valueOf(userDetails[0].firstaccess));
additionalAttributes.put("lastaccess", String.valueOf(userDetails[0].lastaccess));
additionalAttributes.put("auth", userDetails[0].auth);
additionalAttributes.put("suspended", String.valueOf(userDetails[0].suspended));
additionalAttributes.put("confirmed", String.valueOf(userDetails[0].confirmed));
additionalAttributes.put("lang", userDetails[0].lang);
additionalAttributes.put("theme", userDetails[0].theme);
additionalAttributes.put("timezone", userDetails[0].timezone);
additionalAttributes.put("description", userDetails[0].description);
additionalAttributes.put("mailformat", String.valueOf(userDetails[0].mailformat));
additionalAttributes.put("descriptionformat", String.valueOf(userDetails[0].descriptionformat));
return new ExamineeAccountDetails(
userDetails[0].id,
userDetails[0].fullname,
userDetails[0].username,
userDetails[0].email,
additionalAttributes);
});
}
@Override
protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) {
throw new UnsupportedOperationException("not available yet");
public String getExamineeName(final String examineeUserId) {
return getExamineeAccountDetails(examineeUserId)
.map(ExamineeAccountDetails::getDisplayName)
.onError(error -> log.warn("Failed to request user-name for ID: {}", error.getMessage(), error))
.getOr(examineeUserId);
}
@Override
protected FetchStatus getFetchStatus() {
if (this.allQuizzesRequest.getState() != State.CLOSED) {
return FetchStatus.FETCH_ERROR;
}
public Result<Chapters> getCourseChapters(final String courseId) {
return Result.ofError(new UnsupportedOperationException("not available yet"));
}
@Override
public FetchStatus getFetchStatus() {
if (this.moodleCourseDataAsyncLoader.isRunning()) {
return FetchStatus.ASYNC_FETCH_RUNNING;
}

View file

@ -22,10 +22,13 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.MoodleSEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.NoSEBRestrictionException;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate;
@ -57,7 +60,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestT
* Delete all key (and remove restrictions):
* POST:
* http://yourmoodle.org/webservice/rest/server.php?wstoken={token}&moodlewsrestformat=json&wsfunction=seb_restriction_delete&courseId=123 */
public class MoodleCourseRestriction {
public class MoodleCourseRestriction implements SEBRestrictionAPI {
private static final Logger log = LoggerFactory.getLogger(MoodleCourseRestriction.class);
@ -84,7 +87,8 @@ public class MoodleCourseRestriction {
this.moodleRestTemplateFactory = moodleRestTemplateFactory;
}
LmsSetupTestResult initAPIAccess() {
@Override
public LmsSetupTestResult testCourseRestrictionAPI() {
// try to call the SEB Restrictions API
try {
@ -108,18 +112,35 @@ public class MoodleCourseRestriction {
return LmsSetupTestResult.ofOkay(LmsType.MOODLE);
}
Result<MoodleSEBRestriction> getSEBRestriction(
final String internalId) {
@Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
return Result.tryCatch(() -> {
return getSEBRestriction(
MoodleCourseAccess.getQuizId(internalId),
MoodleCourseAccess.getShortname(internalId),
MoodleCourseAccess.getIdnumber(internalId))
MoodleCourseAccess.getQuizId(exam.externalId),
MoodleCourseAccess.getShortname(exam.externalId),
MoodleCourseAccess.getIdnumber(exam.externalId))
.map(restriction -> SEBRestriction.from(exam.id, restriction))
.getOrThrow();
});
}
@Override
public Result<SEBRestriction> applySEBClientRestriction(
final String externalExamId,
final SEBRestriction sebRestrictionData) {
return this.updateSEBRestriction(
externalExamId,
MoodleSEBRestriction.from(sebRestrictionData))
.map(result -> sebRestrictionData);
}
@Override
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
return this.deleteSEBRestriction(exam.externalId)
.map(result -> exam);
}
Result<MoodleSEBRestriction> getSEBRestriction(
final String quizId,
final String shortname,

View file

@ -1,165 +0,0 @@
/*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.MoodleSEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.NoSEBRestrictionException;
/** The MoodleLmsAPITemplate is separated into two parts:
* - MoodleCourseAccess implements the course access API
* - MoodleCourseRestriction implements the SEB restriction API
* - Both uses the MoodleRestTemplateFactore to create a spring based RestTemplate to access the LMS API
*
* NOTE: Because of the missing integration on Moodle side so far the MoodleCourseAccess
* needs to deal with Moodle's standard API functions that don't allow to filter and page course/quiz data
* in an easy and proper way. Therefore we have to fetch all course and quiz data from Moodle before
* filtering and paging can be applied. Since there are possibly thousands of active courses and quizzes
* this moodle course access implements an synchronous fetch as well as an asynchronous fetch strategy.
* The asynchronous fetch strategy is started within a background task that batches the course and quiz
* requests to Moodle and fill up a shared cache. A SEB Server LMS API request will start the
* background task if needed and return immediately to do not block the request.
* The planed Moodle integration on moodle side also defines an improved course access API. This will
* possibly make this synchronous fetch strategy obsolete in the future. */
public class MoodleLmsAPITemplate implements LmsAPITemplate {
private static final Logger log = LoggerFactory.getLogger(MoodleLmsAPITemplate.class);
private final MoodleCourseAccess moodleCourseAccess;
private final MoodleCourseRestriction moodleCourseRestriction;
protected MoodleLmsAPITemplate(
final MoodleCourseAccess moodleCourseAccess,
final MoodleCourseRestriction moodleCourseRestriction) {
this.moodleCourseAccess = moodleCourseAccess;
this.moodleCourseRestriction = moodleCourseRestriction;
}
@Override
public LmsType getType() {
return LmsType.MOODLE;
}
@Override
public LmsSetup lmsSetup() {
return this.moodleCourseAccess
.getApiTemplateDataSupplier()
.getLmsSetup();
}
@Override
public LmsSetupTestResult testCourseAccessAPI() {
return this.moodleCourseAccess.initAPIAccess();
}
@Override
public LmsSetupTestResult testCourseRestrictionAPI() {
throw new NoSEBRestrictionException();
}
@Override
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
return this.moodleCourseAccess
.protectedQuizzesRequest(filterMap)
.map(quizzes -> quizzes.stream()
.filter(LmsAPIService.quizFilterPredicate(filterMap))
.collect(Collectors.toList()));
}
@Override
public Result<QuizData> getQuiz(final String id) {
return this.moodleCourseAccess.getQuizFromCache(id);
}
@Override
public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) {
return this.moodleCourseAccess.getQuizzesFromCache(ids);
}
@Override
public void clearCache() {
this.moodleCourseAccess.clearCache();
}
@Override
public Result<Chapters> getCourseChapters(final String courseId) {
return Result.tryCatch(() -> this.moodleCourseAccess
.getCourseChaptersSupplier(courseId)
.get());
}
@Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
return this.moodleCourseAccess.getExamineeAccountDetails(examineeSessionId);
}
@Override
public String getExamineeName(final String examineeSessionId) {
return this.moodleCourseAccess.getExamineeName(examineeSessionId);
}
@Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
if (log.isDebugEnabled()) {
log.debug("Get SEB Client restriction for Exam: {}", exam.externalId);
}
return this.moodleCourseRestriction
.getSEBRestriction(exam.externalId)
.map(restriction -> SEBRestriction.from(exam.id, restriction));
}
@Override
public Result<SEBRestriction> applySEBClientRestriction(
final String externalExamId,
final SEBRestriction sebRestrictionData) {
if (log.isDebugEnabled()) {
log.debug("Apply SEB Client restriction: {}", sebRestrictionData);
}
return this.moodleCourseRestriction
.updateSEBRestriction(
externalExamId,
MoodleSEBRestriction.from(sebRestrictionData))
.map(result -> sebRestrictionData);
}
@Override
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
if (log.isDebugEnabled()) {
log.debug("Release SEB Client restriction for Exam: {}", exam);
}
return this.moodleCourseRestriction
.deleteSEBRestriction(exam.externalId)
.map(result -> exam);
}
}

View file

@ -27,6 +27,7 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter;
@Lazy
@Service
@ -88,14 +89,16 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
this.jsonMapper,
moodleRestTemplateFactory,
asyncLoaderPrototype,
this.asyncService,
this.environment);
final MoodleCourseRestriction moodleCourseRestriction = new MoodleCourseRestriction(
this.jsonMapper,
moodleRestTemplateFactory);
return new MoodleLmsAPITemplate(
return new LmsAPITemplateAdapter(
this.asyncService,
this.environment,
apiTemplateDataSupplier,
moodleCourseAccess,
moodleCourseRestriction);
});

View file

@ -14,8 +14,8 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
@ -24,7 +24,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.CacheManager;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@ -35,7 +34,6 @@ import org.springframework.web.client.RestTemplate;
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
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.ProxyData;
@ -75,11 +73,9 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final ClientCredentialService clientCredentialService,
final APITemplateDataSupplier apiTemplateDataSupplier,
final AsyncService asyncService,
final Environment environment,
final CacheManager cacheManager) {
super(asyncService, environment, cacheManager);
super(cacheManager);
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.clientCredentialService = clientCredentialService;
@ -166,7 +162,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
@Override
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
return this
.protectedQuizzesRequest(filterMap)
.allQuizzesRequest(filterMap)
.map(quizzes -> quizzes.stream()
.filter(LmsAPIService.quizFilterPredicate(filterMap))
.collect(Collectors.toList()));
@ -187,7 +183,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
});
if (!leftIds.isEmpty()) {
result.addAll(super.protectedQuizzesRequest(leftIds).getOrThrow());
result.addAll(quizzesRequest(leftIds).getOrThrow());
}
return result;
@ -201,18 +197,35 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
return Result.of(fromCache);
}
return super.protectedQuizRequest(id);
return quizRequest(id);
}
@Override
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) {
return () -> {
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
return getRestTemplate().map(t -> this.getExamineeById(t, examineeUserId));
}
@Override
public String getExamineeName(final String examineeUserId) {
return getExamineeAccountDetails(examineeUserId)
.map(ExamineeAccountDetails::getDisplayName)
.onError(error -> log.warn("Failed to request user-name for ID: {}", error.getMessage(), error))
.getOr(examineeUserId);
}
@Override
public Result<Chapters> getCourseChapters(final String courseId) {
return Result.ofError(new UnsupportedOperationException("No Course Chapter available for OpenOLAT LMS"));
}
protected Result<List<QuizData>> allQuizzesRequest(final FilterMap filterMap) {
return Result.tryCatch(() -> {
final List<QuizData> res = getRestTemplate()
.map(t -> this.collectAllQuizzes(t, filterMap))
.getOrThrow();
super.putToCache(res);
return res;
};
});
}
private String examUrl(final long olatRepositoryId) {
@ -254,16 +267,15 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
.collect(Collectors.toList());
}
@Override
protected Supplier<Collection<QuizData>> quizzesSupplier(final Set<String> ids) {
return () -> ids.stream().map(id -> quizSupplier(id).get()).collect(Collectors.toList());
protected Result<Collection<QuizData>> quizzesRequest(final Set<String> ids) {
return Result.tryCatch(() -> ids.stream()
.map(id -> quizRequest(id).getOr(null))
.filter(Objects::nonNull)
.collect(Collectors.toList()));
}
@Override
protected Supplier<QuizData> quizSupplier(final String id) {
return () -> getRestTemplate()
.map(t -> this.quizById(t, id))
.getOrThrow();
protected Result<QuizData> quizRequest(final String id) {
return getRestTemplate().map(t -> this.quizById(t, id));
}
private QuizData quizById(final OlatLmsRestTemplate restTemplate, final String id) {
@ -295,20 +307,6 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
attrs);
}
@Override
protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String id) {
return () -> getRestTemplate()
.map(t -> this.getExamineeById(t, id))
.getOrThrow();
}
@Override
protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) {
return () -> {
throw new UnsupportedOperationException("No Course Chapter available for OpenOLAT LMS");
};
}
private SEBRestriction getRestrictionForAssignmentId(final RestTemplate restTemplate, final String id) {
final String url = String.format("/restapi/assessment_modes/%s/seb_restriction", id);
final RestrictionData r = this.apiGet(restTemplate, url, RestrictionData.class);

View file

@ -22,6 +22,7 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsAPITemplateAdapter;
@Lazy
@Service
@ -63,13 +64,17 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
@Override
public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
return Result.tryCatch(() -> {
return new OlatLmsAPITemplate(
final OlatLmsAPITemplate olatLmsAPITemplate = new OlatLmsAPITemplate(
this.clientHttpRequestFactoryService,
this.clientCredentialService,
apiTemplateDataSupplier,
this.cacheManager);
return new LmsAPITemplateAdapter(
this.asyncService,
this.environment,
this.cacheManager);
apiTemplateDataSupplier,
olatLmsAPITemplate,
olatLmsAPITemplate);
});
}

View file

@ -11,7 +11,7 @@ logging.level.ROOT=INFO
logging.level.ch=INFO
logging.level.ch.ethz.seb.sebserver.webservice.datalayer=INFO
logging.level.org.springframework.cache=INFO
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=INFO
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=DEBUG
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=DEBUG
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=INFO
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator=DEBUG

View file

@ -22,8 +22,6 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.async.AsyncRunner;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult.ErrorType;
@ -75,7 +73,6 @@ public class MoodleCourseAccessTest {
new JSONMapper(),
moodleRestTemplateFactory,
null,
new AsyncService(new AsyncRunner()),
this.env);
final String examId = "123";
@ -123,10 +120,9 @@ public class MoodleCourseAccessTest {
new JSONMapper(),
moodleRestTemplateFactory,
null,
mock(AsyncService.class),
this.env);
final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess();
final LmsSetupTestResult initAPIAccess = moodleCourseAccess.testCourseAccessAPI();
assertNotNull(initAPIAccess);
assertFalse(initAPIAccess.errors.isEmpty());
assertTrue(initAPIAccess.hasError(ErrorType.TOKEN_REQUEST));
@ -145,10 +141,9 @@ public class MoodleCourseAccessTest {
new JSONMapper(),
moodleRestTemplateFactory,
null,
mock(AsyncService.class),
this.env);
final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess();
final LmsSetupTestResult initAPIAccess = moodleCourseAccess.testCourseAccessAPI();
assertNotNull(initAPIAccess);
assertFalse(initAPIAccess.errors.isEmpty());
assertTrue(initAPIAccess.hasError(ErrorType.QUIZ_ACCESS_API_REQUEST));
@ -166,10 +161,9 @@ public class MoodleCourseAccessTest {
new JSONMapper(),
moodleRestTemplateFactory,
null,
mock(AsyncService.class),
this.env);
final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess();
final LmsSetupTestResult initAPIAccess = moodleCourseAccess.testCourseAccessAPI();
assertNotNull(initAPIAccess);
assertTrue(initAPIAccess.errors.isEmpty());