SEBSERV-158 preparation - refactoring of LmsAPITemplate
This commit is contained in:
parent
8dae41f754
commit
eb08df6c00
26 changed files with 1204 additions and 1187 deletions
|
@ -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 */),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
Loading…
Reference in a new issue