SEBSERV-301 implementation
This commit is contained in:
parent
d866b219fa
commit
aa040fc615
27 changed files with 2210 additions and 1566 deletions
|
@ -71,8 +71,6 @@ public final class Exam implements GrantEntity {
|
|||
public static final String ADDITIONAL_ATTR_SIGNATURE_KEY_CERT_ALIAS = "SIGNATURE_KEY_CERT_ALIAS";
|
||||
/** This attribute name is used to store the per exam generated app-signature-key encryption salt */
|
||||
public static final String ADDITIONAL_ATTR_SIGNATURE_KEY_SALT = "SIGNATURE_KEY_SALT";
|
||||
/** This attribute name is used to store the per Moolde(plugin) exam generated alternative BEK */
|
||||
public static final String ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK = "ALTERNATIVE_SEB_BEK";
|
||||
|
||||
public enum ExamStatus {
|
||||
UP_COMING,
|
||||
|
|
|
@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.gbl.model.exam;
|
|||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.joda.time.DateTime;
|
||||
|
@ -211,6 +212,23 @@ public final class QuizData implements GrantEntity {
|
|||
return this.additionalAttributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.id, this.lmsSetupId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
final QuizData other = (QuizData) obj;
|
||||
return Objects.equals(this.id, other.id) && Objects.equals(this.lmsSetupId, other.lmsSetupId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
|
|
|
@ -10,6 +10,9 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.exam;
|
|||
|
||||
public interface ExamConfigurationValueService {
|
||||
|
||||
public static final String CONFIG_ATTR_NAME_QUIT_LINK = "quitURL";
|
||||
public static final String CONFIG_ATTR_NAME_QUIT_SECRET = "hashedQuitPassword";
|
||||
|
||||
/** Get the actual SEB settings attribute value for the exam configuration mapped as default configuration
|
||||
* to the given exam
|
||||
*
|
||||
|
@ -18,4 +21,8 @@ public interface ExamConfigurationValueService {
|
|||
* @return The current value of the above SEB settings attribute and given exam. */
|
||||
String getMappedDefaultConfigAttributeValue(Long examId, String configAttributeName);
|
||||
|
||||
String getQuitSecret(Long examId);
|
||||
|
||||
String getQuitLink(Long examId);
|
||||
|
||||
}
|
||||
|
|
|
@ -276,7 +276,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
|||
this.additionalAttributesDAO.saveAdditionalAttribute(
|
||||
EntityType.EXAM,
|
||||
exam.id,
|
||||
Exam.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK,
|
||||
SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK,
|
||||
moodleBEK).getOrThrow();
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to create additional moodle SEB BEK attribute: ", e);
|
||||
|
|
|
@ -8,17 +8,23 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationAttributeDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationValueDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
|
||||
|
||||
@Lazy
|
||||
@Service
|
||||
@WebServiceProfile
|
||||
public class ExamConfigurationValueServiceImpl implements ExamConfigurationValueService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ExamConfigurationValueServiceImpl.class);
|
||||
|
@ -27,17 +33,20 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue
|
|||
private final ConfigurationDAO configurationDAO;
|
||||
private final ConfigurationAttributeDAO configurationAttributeDAO;
|
||||
private final ConfigurationValueDAO configurationValueDAO;
|
||||
private final Cryptor cryptor;
|
||||
|
||||
public ExamConfigurationValueServiceImpl(
|
||||
final ExamConfigurationMapDAO examConfigurationMapDAO,
|
||||
final ConfigurationDAO configurationDAO,
|
||||
final ConfigurationAttributeDAO configurationAttributeDAO,
|
||||
final ConfigurationValueDAO configurationValueDAO) {
|
||||
final ConfigurationValueDAO configurationValueDAO,
|
||||
final Cryptor cryptor) {
|
||||
|
||||
this.examConfigurationMapDAO = examConfigurationMapDAO;
|
||||
this.configurationDAO = configurationDAO;
|
||||
this.configurationAttributeDAO = configurationAttributeDAO;
|
||||
this.configurationValueDAO = configurationValueDAO;
|
||||
this.cryptor = cryptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -66,4 +75,45 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQuitSecret(final Long examId) {
|
||||
try {
|
||||
|
||||
final String quitSecretEncrypted = getMappedDefaultConfigAttributeValue(
|
||||
examId,
|
||||
CONFIG_ATTR_NAME_QUIT_SECRET);
|
||||
|
||||
if (StringUtils.isNotEmpty(quitSecretEncrypted)) {
|
||||
try {
|
||||
|
||||
return this.cryptor
|
||||
.decrypt(quitSecretEncrypted)
|
||||
.getOrThrow()
|
||||
.toString();
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to decrypt quitSecret: ", e);
|
||||
}
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to get SEB restriction with quit secret: ", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQuitLink(final Long examId) {
|
||||
try {
|
||||
|
||||
return getMappedDefaultConfigAttributeValue(
|
||||
examId,
|
||||
CONFIG_ATTR_NAME_QUIT_LINK);
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to get SEB restriction with quit link: ", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
|
@ -113,10 +113,20 @@ public interface CourseAccessAPI {
|
|||
* @return Result referencing to the Chapters model for the given course or to an error when happened. */
|
||||
Result<Chapters> getCourseChapters(String courseId);
|
||||
|
||||
/** This is used to buffer fetch results of asynchronous LMS quiz data fetch processes.
|
||||
* An asynchronous LMS quiz data fetch processes will buffer its fetch results within this buffer
|
||||
* during processing and a request can get already buffered results on a none-blocking manner.
|
||||
*
|
||||
* Use it like a Future but with the ability to get already fetched data. */
|
||||
static class AsyncQuizFetchBuffer {
|
||||
public List<QuizData> buffer = new ArrayList<>();
|
||||
|
||||
/** The buffer set where already fetched data is stored and can be get */
|
||||
public Set<QuizData> buffer = new HashSet<>();
|
||||
/** Indicates whether the asynchronous fetch is still running or has finished */
|
||||
public boolean finished = false;
|
||||
/** Indicates if the fetch is been canceled. Set this to true to cancel the asynchronous process */
|
||||
public boolean canceled = false;
|
||||
/** Reference to an error when the asynchronous fetch stopped with an error */
|
||||
public Exception error = null;
|
||||
|
||||
public void finish() {
|
||||
|
|
|
@ -18,6 +18,9 @@ public interface SEBRestrictionService {
|
|||
String SEB_RESTRICTION_ADDITIONAL_PROPERTY_NAME_PREFIX = "sebRestrictionProp_";
|
||||
|
||||
String SEB_RESTRICTION_ADDITIONAL_PROPERTY_CONFIG_KEY = "config_key";
|
||||
/** This attribute name is used to store the per Moolde(plugin) exam generated alternative BEK */
|
||||
String ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK =
|
||||
SEB_RESTRICTION_ADDITIONAL_PROPERTY_NAME_PREFIX + "ALTERNATIVE_SEB_BEK";
|
||||
|
||||
/** Get the LmsAPIService that is used by the SEBRestrictionService */
|
||||
LmsAPIService getLmsAPIService();
|
||||
|
|
|
@ -98,9 +98,14 @@ public abstract class AbstractCachedCourseAccess {
|
|||
|
||||
/** Put all QuizData to short time cache.
|
||||
*
|
||||
* @param quizData Collection of QuizData */
|
||||
protected void putToCache(final Collection<QuizData> quizData) {
|
||||
* @param quizData Collection of QuizData
|
||||
* @return the given collection of QuizData */
|
||||
protected final Collection<QuizData> putToCache(final Collection<QuizData> quizData) {
|
||||
if (quizData == null || quizData.isEmpty()) {
|
||||
return quizData;
|
||||
}
|
||||
quizData.stream().forEach(q -> this.cache.put(createCacheKey(q.id), q));
|
||||
return quizData;
|
||||
}
|
||||
|
||||
protected void evict(final String id) {
|
||||
|
|
|
@ -0,0 +1,263 @@
|
|||
/*
|
||||
* 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.moodle;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
|
||||
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.gbl.util.Utils;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseAccess;
|
||||
|
||||
public class MockupRestTemplateFactory implements MoodleRestTemplateFactory {
|
||||
|
||||
private final APITemplateDataSupplier apiTemplateDataSupplier;
|
||||
|
||||
public MockupRestTemplateFactory(final APITemplateDataSupplier apiTemplateDataSupplier) {
|
||||
this.apiTemplateDataSupplier = apiTemplateDataSupplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LmsSetupTestResult test() {
|
||||
return LmsSetupTestResult.ofOkay(LmsType.MOODLE_PLUGIN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public APITemplateDataSupplier getApiTemplateDataSupplier() {
|
||||
return this.apiTemplateDataSupplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getKnownTokenAccessPaths() {
|
||||
final Set<String> paths = new HashSet<>();
|
||||
paths.add(MoodleAPIRestTemplate.MOODLE_DEFAULT_TOKEN_REQUEST_PATH);
|
||||
return paths;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<MoodleAPIRestTemplate> createRestTemplate() {
|
||||
return Result.of(new MockupMoodleRestTemplate(this.apiTemplateDataSupplier.getLmsSetup().lmsApiUrl));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath) {
|
||||
return Result.of(new MockupMoodleRestTemplate(this.apiTemplateDataSupplier.getLmsSetup().lmsApiUrl));
|
||||
}
|
||||
|
||||
public static final class MockupMoodleRestTemplate implements MoodleAPIRestTemplate {
|
||||
|
||||
private final String accessToken = UUID.randomUUID().toString();
|
||||
private final String url;
|
||||
|
||||
public MockupMoodleRestTemplate(final String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getService() {
|
||||
return "mockup-service";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setService(final String service) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getAccessToken() {
|
||||
System.out.println("***** getAccessToken: " + this.accessToken);
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void testAPIConnection(final String... functions) {
|
||||
System.out.println("***** testAPIConnection functions: " + functions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String callMoodleAPIFunction(final String functionName) {
|
||||
return callMoodleAPIFunction(functionName, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String callMoodleAPIFunction(
|
||||
final String functionName,
|
||||
final MultiValueMap<String, String> queryAttributes) {
|
||||
return callMoodleAPIFunction(functionName, null, queryAttributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String callMoodleAPIFunction(
|
||||
final String functionName,
|
||||
final MultiValueMap<String, String> queryParams,
|
||||
final MultiValueMap<String, String> queryAttributes) {
|
||||
|
||||
final UriComponentsBuilder queryParam = UriComponentsBuilder
|
||||
.fromHttpUrl(this.url + MOODLE_DEFAULT_REST_API_PATH)
|
||||
.queryParam(REST_REQUEST_TOKEN_NAME, this.accessToken)
|
||||
.queryParam(REST_REQUEST_FUNCTION_NAME, functionName)
|
||||
.queryParam(REST_REQUEST_FORMAT_NAME, "json");
|
||||
|
||||
if (queryParams != null && !queryParams.isEmpty()) {
|
||||
queryParam.queryParams(queryParams);
|
||||
}
|
||||
|
||||
final boolean usePOST = queryAttributes != null && !queryAttributes.isEmpty();
|
||||
HttpEntity<?> functionReqEntity;
|
||||
if (usePOST) {
|
||||
final HttpHeaders headers = new HttpHeaders();
|
||||
headers.set(
|
||||
HttpHeaders.CONTENT_TYPE,
|
||||
MediaType.APPLICATION_FORM_URLENCODED_VALUE);
|
||||
|
||||
final String body = Utils.toAppFormUrlEncodedBody(queryAttributes);
|
||||
functionReqEntity = new HttpEntity<>(body, headers);
|
||||
|
||||
} else {
|
||||
functionReqEntity = new HttpEntity<>(new LinkedMultiValueMap<>());
|
||||
}
|
||||
|
||||
System.out.println("***** callMoodleAPIFunction HttpEntity: " + functionReqEntity);
|
||||
|
||||
// TODO return json
|
||||
if (MoodlePluginCourseAccess.COURSES_API_FUNCTION_NAME.equals(functionName)) {
|
||||
return respondCourses(queryAttributes);
|
||||
} else if (MoodlePluginCourseAccess.QUIZZES_BY_COURSES_API_FUNCTION_NAME.equals(functionName)) {
|
||||
return respondQuizzes(queryAttributes);
|
||||
} else if (MoodlePluginCourseAccess.USERS_API_FUNCTION_NAME.equals(functionName)) {
|
||||
return respondUsers(queryAttributes);
|
||||
} else {
|
||||
throw new RuntimeException("Unknown function: " + functionName);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MockCD {
|
||||
public final String id;
|
||||
public final String shortname;
|
||||
public final String categoryid;
|
||||
public final String fullname;
|
||||
public final String displayname;
|
||||
public final String idnumber;
|
||||
public final Long startdate; // unix-time seconds UTC
|
||||
public final Long enddate; // unix-time seconds UTC
|
||||
public final Long timecreated; // unix-time seconds UTC
|
||||
public final boolean visible;
|
||||
|
||||
public MockCD(final String num) {
|
||||
this.id = num;
|
||||
this.shortname = "c" + num;
|
||||
this.categoryid = "mock";
|
||||
this.fullname = "course" + num;
|
||||
this.displayname = this.fullname;
|
||||
this.idnumber = "i" + num;
|
||||
this.startdate = Long.valueOf(num);
|
||||
this.enddate = null;
|
||||
this.timecreated = Long.valueOf(num);
|
||||
this.visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MockQ {
|
||||
public final String id;
|
||||
public final String coursemodule;
|
||||
public final String course;
|
||||
public final String name;
|
||||
public final String intro;
|
||||
public final Long timeopen; // unix-time seconds UTC
|
||||
public final Long timeclose; // unix-time seconds UTC
|
||||
|
||||
public MockQ(final String courseId, final String num) {
|
||||
this.id = num;
|
||||
this.coursemodule = courseId;
|
||||
this.course = courseId;
|
||||
this.name = "quiz " + num;
|
||||
this.intro = this.name;
|
||||
this.timeopen = Long.valueOf(num);
|
||||
this.timeclose = null;
|
||||
}
|
||||
}
|
||||
|
||||
private String respondCourses(final MultiValueMap<String, String> queryAttributes) {
|
||||
try {
|
||||
final List<String> ids = queryAttributes.get(MoodlePluginCourseAccess.CRITERIA_COURSE_IDS);
|
||||
final String from = queryAttributes.getFirst(MoodlePluginCourseAccess.CRITERIA_LIMIT_FROM);
|
||||
System.out.println("************* from: " + from);
|
||||
final List<MockCD> courses;
|
||||
if (ids != null && !ids.isEmpty()) {
|
||||
courses = ids.stream().map(id -> new MockCD(id)).collect(Collectors.toList());
|
||||
} else if (from != null && Integer.valueOf(from) < 11) {
|
||||
courses = new ArrayList<>();
|
||||
final int num = (Integer.valueOf(from) > 0) ? 10 : 1;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
courses.add(new MockCD(String.valueOf(num + i)));
|
||||
}
|
||||
} else {
|
||||
courses = new ArrayList<>();
|
||||
}
|
||||
|
||||
final Map<String, Object> response = new HashMap<>();
|
||||
response.put("courses", courses);
|
||||
final JSONMapper jsonMapper = new JSONMapper();
|
||||
final String result = jsonMapper.writeValueAsString(response);
|
||||
System.out.println("******** courses response: " + result);
|
||||
return result;
|
||||
} catch (final JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private String respondQuizzes(final MultiValueMap<String, String> queryAttributes) {
|
||||
try {
|
||||
final List<String> ids = queryAttributes.get(MoodlePluginCourseAccess.CRITERIA_COURSE_IDS);
|
||||
final List<MockQ> quizzes;
|
||||
if (ids != null && !ids.isEmpty()) {
|
||||
quizzes = ids.stream().map(id -> new MockQ(id, "10" + id)).collect(Collectors.toList());
|
||||
} else {
|
||||
quizzes = Collections.emptyList();
|
||||
}
|
||||
|
||||
final Map<String, Object> response = new HashMap<>();
|
||||
response.put("quizzes", quizzes);
|
||||
final JSONMapper jsonMapper = new JSONMapper();
|
||||
final String result = jsonMapper.writeValueAsString(response);
|
||||
System.out.println("******** quizzes response: " + result);
|
||||
return result;
|
||||
} catch (final JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private String respondUsers(final MultiValueMap<String, String> queryAttributes) {
|
||||
// TODO
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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.moodle;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseAccess;
|
||||
|
||||
@Lazy
|
||||
@Service
|
||||
@WebServiceProfile
|
||||
public class MoodlePluginCheck {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MoodlePluginCheck.class);
|
||||
|
||||
/** Used to check if the moodle SEB Server plugin is available for a given LMSSetup.
|
||||
*
|
||||
* @param lmsSetup The LMS Setup
|
||||
* @return true if the SEB Server plugin is available */
|
||||
public boolean checkPluginAvailable(final MoodleRestTemplateFactory restTemplateFactory) {
|
||||
try {
|
||||
|
||||
log.info("Check Moodle SEB Server Plugin available...");
|
||||
|
||||
final LmsSetupTestResult test = restTemplateFactory.test();
|
||||
|
||||
if (!test.isOk()) {
|
||||
log.warn("Failed to check Moodle SEB Server Plugin because of invalid LMS Setup: ", test);
|
||||
return false;
|
||||
}
|
||||
|
||||
final MoodleAPIRestTemplate restTemplate = restTemplateFactory
|
||||
.createRestTemplate()
|
||||
.getOrThrow();
|
||||
|
||||
try {
|
||||
restTemplate.testAPIConnection(
|
||||
MoodlePluginCourseAccess.COURSES_API_FUNCTION_NAME,
|
||||
MoodlePluginCourseAccess.QUIZZES_BY_COURSES_API_FUNCTION_NAME,
|
||||
MoodlePluginCourseAccess.USERS_API_FUNCTION_NAME);
|
||||
} catch (final Exception e) {
|
||||
log.info("Moodle SEB Server Plugin not available: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info("Moodle SEB Server Plugin not available for: {}",
|
||||
restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup());
|
||||
|
||||
return true;
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to check Moodle SEB Server Plugin because of unexpected error: ", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,463 +1,29 @@
|
|||
/*
|
||||
* 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.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
|
||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
|
||||
import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService;
|
||||
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
|
||||
import ch.ethz.seb.sebserver.gbl.client.ProxyData;
|
||||
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
|
||||
import ch.ethz.seb.sebserver.gbl.model.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.gbl.util.Utils;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
|
||||
|
||||
public class MoodleRestTemplateFactory {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MoodleRestTemplateFactory.class);
|
||||
|
||||
public final JSONMapper jsonMapper;
|
||||
public final APITemplateDataSupplier apiTemplateDataSupplier;
|
||||
public final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
|
||||
public final ClientCredentialService clientCredentialService;
|
||||
public final Set<String> knownTokenAccessPaths;
|
||||
|
||||
public MoodleRestTemplateFactory(
|
||||
final JSONMapper jsonMapper,
|
||||
final APITemplateDataSupplier apiTemplateDataSupplier,
|
||||
final ClientCredentialService clientCredentialService,
|
||||
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
|
||||
final String[] alternativeTokenRequestPaths) {
|
||||
|
||||
this.jsonMapper = jsonMapper;
|
||||
this.apiTemplateDataSupplier = apiTemplateDataSupplier;
|
||||
this.clientCredentialService = clientCredentialService;
|
||||
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
|
||||
|
||||
final Set<String> paths = new HashSet<>();
|
||||
paths.add(MoodleAPIRestTemplate.MOODLE_DEFAULT_TOKEN_REQUEST_PATH);
|
||||
if (alternativeTokenRequestPaths != null) {
|
||||
paths.addAll(Arrays.asList(alternativeTokenRequestPaths));
|
||||
}
|
||||
this.knownTokenAccessPaths = Utils.immutableSetOf(paths);
|
||||
}
|
||||
|
||||
APITemplateDataSupplier getApiTemplateDataSupplier() {
|
||||
return this.apiTemplateDataSupplier;
|
||||
}
|
||||
|
||||
public LmsSetupTestResult test() {
|
||||
|
||||
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||
final ClientCredentials credentials = 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"));
|
||||
} else {
|
||||
// try to connect to the url
|
||||
if (!Utils.pingHost(lmsSetup.lmsApiUrl)) {
|
||||
missingAttrs.add(APIMessage.fieldValidationError(
|
||||
LMS_SETUP.ATTR_LMS_URL,
|
||||
"lmsSetup:lmsUrl:url.invalid"));
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(lmsSetup.lmsRestApiToken)) {
|
||||
if (!credentials.hasClientId()) {
|
||||
missingAttrs.add(APIMessage.fieldValidationError(
|
||||
LMS_SETUP.ATTR_LMS_CLIENTNAME,
|
||||
"lmsSetup:lmsClientname:notNull"));
|
||||
}
|
||||
if (!credentials.hasSecret()) {
|
||||
missingAttrs.add(APIMessage.fieldValidationError(
|
||||
LMS_SETUP.ATTR_LMS_CLIENTSECRET,
|
||||
"lmsSetup:lmsClientsecret:notNull"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!missingAttrs.isEmpty()) {
|
||||
return LmsSetupTestResult.ofMissingAttributes(LmsType.MOODLE, missingAttrs);
|
||||
}
|
||||
|
||||
return LmsSetupTestResult.ofOkay(LmsType.MOODLE);
|
||||
}
|
||||
|
||||
public Result<MoodleAPIRestTemplate> createRestTemplate() {
|
||||
|
||||
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||
|
||||
return this.knownTokenAccessPaths
|
||||
.stream()
|
||||
.map(this::createRestTemplate)
|
||||
.map(result -> {
|
||||
if (result.hasError()) {
|
||||
log.warn("Failed to get access token for LMS: {}({})",
|
||||
lmsSetup.name,
|
||||
lmsSetup.id,
|
||||
result.getError().getMessage());
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.filter(Result::hasValue)
|
||||
.findFirst()
|
||||
.orElse(Result.ofRuntimeError(
|
||||
"Failed to gain any access for LMS " +
|
||||
lmsSetup.name + "(" + lmsSetup.id +
|
||||
") on paths: " + this.knownTokenAccessPaths));
|
||||
}
|
||||
|
||||
public Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath) {
|
||||
|
||||
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials();
|
||||
final ProxyData proxyData = this.apiTemplateDataSupplier.getProxyData();
|
||||
|
||||
final CharSequence plainClientId = credentials.clientId;
|
||||
final CharSequence plainClientSecret = this.clientCredentialService
|
||||
.getPlainClientSecret(credentials)
|
||||
.getOrThrow();
|
||||
|
||||
final MoodleAPIRestTemplateImpl restTemplate = new MoodleAPIRestTemplateImpl(
|
||||
this.jsonMapper,
|
||||
this.apiTemplateDataSupplier,
|
||||
lmsSetup.lmsApiUrl,
|
||||
accessTokenPath,
|
||||
lmsSetup.lmsRestApiToken,
|
||||
plainClientId,
|
||||
plainClientSecret);
|
||||
|
||||
final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService
|
||||
.getClientHttpRequestFactory(proxyData)
|
||||
.getOrThrow();
|
||||
|
||||
restTemplate.setRequestFactory(clientHttpRequestFactory);
|
||||
final CharSequence accessToken = restTemplate.getAccessToken();
|
||||
|
||||
if (accessToken == null) {
|
||||
throw new RuntimeException("Failed to get access token for LMS " +
|
||||
lmsSetup.name + "(" + lmsSetup.id +
|
||||
") on path: " + accessTokenPath);
|
||||
}
|
||||
|
||||
return restTemplate;
|
||||
});
|
||||
}
|
||||
|
||||
public static class MoodleAPIRestTemplateImpl extends RestTemplate implements MoodleAPIRestTemplate {
|
||||
|
||||
private static final String REST_API_TEST_FUNCTION = "core_webservice_get_site_info";
|
||||
|
||||
final JSONMapper jsonMapper;
|
||||
final APITemplateDataSupplier apiTemplateDataSupplier;
|
||||
|
||||
private final String serverURL;
|
||||
private final String tokenPath;
|
||||
|
||||
private CharSequence accessToken;
|
||||
|
||||
private final Map<String, String> tokenReqURIVars;
|
||||
private final HttpEntity<?> tokenReqEntity = new HttpEntity<>(new LinkedMultiValueMap<>());
|
||||
|
||||
protected MoodleAPIRestTemplateImpl(
|
||||
final JSONMapper jsonMapper,
|
||||
final APITemplateDataSupplier apiTemplateDataSupplier,
|
||||
final String serverURL,
|
||||
final String tokenPath,
|
||||
final CharSequence accessToken,
|
||||
final CharSequence username,
|
||||
final CharSequence password) {
|
||||
|
||||
this.jsonMapper = jsonMapper;
|
||||
this.apiTemplateDataSupplier = apiTemplateDataSupplier;
|
||||
|
||||
this.serverURL = serverURL;
|
||||
this.tokenPath = tokenPath;
|
||||
this.accessToken = StringUtils.isNotBlank(accessToken) ? accessToken : null;
|
||||
|
||||
this.tokenReqURIVars = new HashMap<>();
|
||||
this.tokenReqURIVars.put(URI_VAR_USER_NAME, String.valueOf(username));
|
||||
this.tokenReqURIVars.put(URI_VAR_PASSWORD, String.valueOf(password));
|
||||
this.tokenReqURIVars.put(URI_VAR_SERVICE, "moodle_mobile_app");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getService() {
|
||||
return this.tokenReqURIVars.get(URI_VAR_SERVICE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setService(final String service) {
|
||||
this.tokenReqURIVars.put(URI_VAR_SERVICE, service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getAccessToken() {
|
||||
if (this.accessToken == null) {
|
||||
requestAccessToken();
|
||||
}
|
||||
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void testAPIConnection(final String... functions) {
|
||||
try {
|
||||
final String apiInfo = this.callMoodleAPIFunction(REST_API_TEST_FUNCTION);
|
||||
final WebserviceInfo webserviceInfo = this.jsonMapper.readValue(
|
||||
apiInfo,
|
||||
WebserviceInfo.class);
|
||||
|
||||
if (StringUtils.isBlank(webserviceInfo.username) || StringUtils.isBlank(webserviceInfo.userid)) {
|
||||
throw new RuntimeException("Invalid WebserviceInfo: " + webserviceInfo);
|
||||
}
|
||||
|
||||
if (functions != null) {
|
||||
|
||||
final List<String> missingAPIFunctions = Arrays.stream(functions)
|
||||
.filter(f -> !webserviceInfo.functions.containsKey(f))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!missingAPIFunctions.isEmpty()) {
|
||||
throw new RuntimeException("Missing Moodle Webservice API functions: " + missingAPIFunctions);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (final RuntimeException re) {
|
||||
throw re;
|
||||
} catch (final Exception e) {
|
||||
throw new RuntimeException("Failed to test Moodle rest API: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String callMoodleAPIFunction(final String functionName) {
|
||||
return callMoodleAPIFunction(functionName, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String callMoodleAPIFunction(
|
||||
final String functionName,
|
||||
final MultiValueMap<String, String> queryAttributes) {
|
||||
return callMoodleAPIFunction(functionName, null, queryAttributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String callMoodleAPIFunction(
|
||||
final String functionName,
|
||||
final MultiValueMap<String, String> queryParams,
|
||||
final MultiValueMap<String, String> queryAttributes) {
|
||||
|
||||
getAccessToken();
|
||||
|
||||
final UriComponentsBuilder queryParam = UriComponentsBuilder
|
||||
.fromHttpUrl(this.serverURL + MOODLE_DEFAULT_REST_API_PATH)
|
||||
.queryParam(REST_REQUEST_TOKEN_NAME, this.accessToken)
|
||||
.queryParam(REST_REQUEST_FUNCTION_NAME, functionName)
|
||||
.queryParam(REST_REQUEST_FORMAT_NAME, "json");
|
||||
|
||||
if (queryParams != null && !queryParams.isEmpty()) {
|
||||
queryParam.queryParams(queryParams);
|
||||
}
|
||||
|
||||
final boolean usePOST = queryAttributes != null && !queryAttributes.isEmpty();
|
||||
HttpEntity<?> functionReqEntity;
|
||||
if (usePOST) {
|
||||
final HttpHeaders headers = new HttpHeaders();
|
||||
headers.set(
|
||||
HttpHeaders.CONTENT_TYPE,
|
||||
MediaType.APPLICATION_FORM_URLENCODED_VALUE);
|
||||
|
||||
final String body = Utils.toAppFormUrlEncodedBody(queryAttributes);
|
||||
functionReqEntity = new HttpEntity<>(body, headers);
|
||||
|
||||
} else {
|
||||
functionReqEntity = new HttpEntity<>(new LinkedMultiValueMap<>());
|
||||
}
|
||||
|
||||
final ResponseEntity<String> response = super.exchange(
|
||||
queryParam.toUriString(),
|
||||
usePOST ? HttpMethod.POST : HttpMethod.GET,
|
||||
functionReqEntity,
|
||||
String.class);
|
||||
|
||||
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
throw new RuntimeException(
|
||||
"Failed to call Moodle webservice API function: " + functionName + " lms setup: " +
|
||||
lmsSetup + " response: " + response.getBody());
|
||||
}
|
||||
|
||||
final String body = response.getBody();
|
||||
|
||||
// NOTE: for some unknown reason, Moodles API error responses come with a 200 OK response HTTP Status
|
||||
// So this is a special Moodle specific error handling here...
|
||||
if (body.startsWith("{exception") || body.contains("\"exception\":")) {
|
||||
// Reset access token to get new on next call (fix access if token is expired)
|
||||
// TODO find a way to verify token invalidity response from Moodle.
|
||||
// Unfortunately there is not a lot of Moodle documentation for the API error handling around.
|
||||
this.accessToken = null;
|
||||
throw new RuntimeException(
|
||||
"Failed to call Moodle webservice API function: " + functionName + " lms setup: " +
|
||||
lmsSetup + " response: " + body);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
private void requestAccessToken() {
|
||||
|
||||
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||
try {
|
||||
|
||||
final ResponseEntity<String> response = super.exchange(
|
||||
this.serverURL + this.tokenPath,
|
||||
HttpMethod.GET,
|
||||
this.tokenReqEntity,
|
||||
String.class,
|
||||
this.tokenReqURIVars);
|
||||
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} response: {} : {}",
|
||||
lmsSetup,
|
||||
response.getStatusCode(),
|
||||
response.getBody());
|
||||
throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " +
|
||||
lmsSetup + " response: " + response.getBody());
|
||||
}
|
||||
|
||||
try {
|
||||
final MoodleToken moodleToken = this.jsonMapper.readValue(
|
||||
response.getBody(),
|
||||
MoodleToken.class);
|
||||
|
||||
if (moodleToken == null || moodleToken.token == null) {
|
||||
throw new RuntimeException("Access Token request with 200 but no or invalid token body");
|
||||
} else {
|
||||
log.info("Successfully get access token from Moodle: {}",
|
||||
lmsSetup);
|
||||
}
|
||||
|
||||
this.accessToken = moodleToken.token;
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} response: {} : {}",
|
||||
lmsSetup,
|
||||
response.getStatusCode(),
|
||||
response.getBody());
|
||||
throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " +
|
||||
lmsSetup + " response: " + response.getBody(), e);
|
||||
}
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} :",
|
||||
lmsSetup,
|
||||
e);
|
||||
throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " +
|
||||
lmsSetup + " cause: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private final static class MoodleToken {
|
||||
final String token;
|
||||
@SuppressWarnings("unused")
|
||||
final String privatetoken;
|
||||
|
||||
@JsonCreator
|
||||
protected MoodleToken(
|
||||
@JsonProperty(value = "token") final String token,
|
||||
@JsonProperty(value = "privatetoken", required = false) final String privatetoken) {
|
||||
|
||||
this.token = token;
|
||||
this.privatetoken = privatetoken;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private final static class WebserviceInfo {
|
||||
String username;
|
||||
String userid;
|
||||
Map<String, FunctionInfo> functions;
|
||||
|
||||
@JsonCreator
|
||||
protected WebserviceInfo(
|
||||
@JsonProperty(value = "username") final String username,
|
||||
@JsonProperty(value = "userid") final String userid,
|
||||
@JsonProperty(value = "functions") final Collection<FunctionInfo> functions) {
|
||||
|
||||
this.username = username;
|
||||
this.userid = userid;
|
||||
this.functions = (functions != null)
|
||||
? functions
|
||||
.stream()
|
||||
.collect(Collectors.toMap(fi -> fi.name, Function.identity()))
|
||||
: Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private final static class FunctionInfo {
|
||||
String name;
|
||||
@SuppressWarnings("unused")
|
||||
String version;
|
||||
|
||||
@JsonCreator
|
||||
protected FunctionInfo(
|
||||
@JsonProperty(value = "name") final String name,
|
||||
@JsonProperty(value = "version") final String version) {
|
||||
|
||||
this.name = name;
|
||||
this.version = version;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
* 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.moodle;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
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.APITemplateDataSupplier;
|
||||
|
||||
public interface MoodleRestTemplateFactory {
|
||||
|
||||
LmsSetupTestResult test();
|
||||
|
||||
APITemplateDataSupplier getApiTemplateDataSupplier();
|
||||
|
||||
Set<String> getKnownTokenAccessPaths();
|
||||
|
||||
Result<MoodleAPIRestTemplate> createRestTemplate();
|
||||
|
||||
Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath);
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,472 @@
|
|||
/*
|
||||
* 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.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
|
||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
|
||||
import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService;
|
||||
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
|
||||
import ch.ethz.seb.sebserver.gbl.client.ProxyData;
|
||||
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
|
||||
import ch.ethz.seb.sebserver.gbl.model.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.gbl.util.Utils;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
|
||||
|
||||
public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MoodleRestTemplateFactoryImpl.class);
|
||||
|
||||
public final JSONMapper jsonMapper;
|
||||
public final APITemplateDataSupplier apiTemplateDataSupplier;
|
||||
public final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
|
||||
public final ClientCredentialService clientCredentialService;
|
||||
public final Set<String> knownTokenAccessPaths;
|
||||
|
||||
public MoodleRestTemplateFactoryImpl(
|
||||
final JSONMapper jsonMapper,
|
||||
final APITemplateDataSupplier apiTemplateDataSupplier,
|
||||
final ClientCredentialService clientCredentialService,
|
||||
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
|
||||
final String[] alternativeTokenRequestPaths) {
|
||||
|
||||
this.jsonMapper = jsonMapper;
|
||||
this.apiTemplateDataSupplier = apiTemplateDataSupplier;
|
||||
this.clientCredentialService = clientCredentialService;
|
||||
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
|
||||
|
||||
final Set<String> paths = new HashSet<>();
|
||||
paths.add(MoodleAPIRestTemplate.MOODLE_DEFAULT_TOKEN_REQUEST_PATH);
|
||||
if (alternativeTokenRequestPaths != null) {
|
||||
paths.addAll(Arrays.asList(alternativeTokenRequestPaths));
|
||||
}
|
||||
this.knownTokenAccessPaths = Utils.immutableSetOf(paths);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getKnownTokenAccessPaths() {
|
||||
return this.knownTokenAccessPaths;
|
||||
}
|
||||
|
||||
@Override
|
||||
public APITemplateDataSupplier getApiTemplateDataSupplier() {
|
||||
return this.apiTemplateDataSupplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LmsSetupTestResult test() {
|
||||
|
||||
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||
final ClientCredentials credentials = 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"));
|
||||
} else {
|
||||
// try to connect to the url
|
||||
if (!Utils.pingHost(lmsSetup.lmsApiUrl)) {
|
||||
missingAttrs.add(APIMessage.fieldValidationError(
|
||||
LMS_SETUP.ATTR_LMS_URL,
|
||||
"lmsSetup:lmsUrl:url.invalid"));
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(lmsSetup.lmsRestApiToken)) {
|
||||
if (!credentials.hasClientId()) {
|
||||
missingAttrs.add(APIMessage.fieldValidationError(
|
||||
LMS_SETUP.ATTR_LMS_CLIENTNAME,
|
||||
"lmsSetup:lmsClientname:notNull"));
|
||||
}
|
||||
if (!credentials.hasSecret()) {
|
||||
missingAttrs.add(APIMessage.fieldValidationError(
|
||||
LMS_SETUP.ATTR_LMS_CLIENTSECRET,
|
||||
"lmsSetup:lmsClientsecret:notNull"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!missingAttrs.isEmpty()) {
|
||||
return LmsSetupTestResult.ofMissingAttributes(LmsType.MOODLE, missingAttrs);
|
||||
}
|
||||
|
||||
return LmsSetupTestResult.ofOkay(LmsType.MOODLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<MoodleAPIRestTemplate> createRestTemplate() {
|
||||
|
||||
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||
|
||||
return this.knownTokenAccessPaths
|
||||
.stream()
|
||||
.map(this::createRestTemplate)
|
||||
.map(result -> {
|
||||
if (result.hasError()) {
|
||||
log.warn("Failed to get access token for LMS: {}({})",
|
||||
lmsSetup.name,
|
||||
lmsSetup.id,
|
||||
result.getError().getMessage());
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.filter(Result::hasValue)
|
||||
.findFirst()
|
||||
.orElse(Result.ofRuntimeError(
|
||||
"Failed to gain any access for LMS " +
|
||||
lmsSetup.name + "(" + lmsSetup.id +
|
||||
") on paths: " + this.knownTokenAccessPaths));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath) {
|
||||
|
||||
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials();
|
||||
final ProxyData proxyData = this.apiTemplateDataSupplier.getProxyData();
|
||||
|
||||
final CharSequence plainClientId = credentials.clientId;
|
||||
final CharSequence plainClientSecret = this.clientCredentialService
|
||||
.getPlainClientSecret(credentials)
|
||||
.getOrThrow();
|
||||
|
||||
final MoodleAPIRestTemplateImpl restTemplate = new MoodleAPIRestTemplateImpl(
|
||||
this.jsonMapper,
|
||||
this.apiTemplateDataSupplier,
|
||||
lmsSetup.lmsApiUrl,
|
||||
accessTokenPath,
|
||||
lmsSetup.lmsRestApiToken,
|
||||
plainClientId,
|
||||
plainClientSecret);
|
||||
|
||||
final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService
|
||||
.getClientHttpRequestFactory(proxyData)
|
||||
.getOrThrow();
|
||||
|
||||
restTemplate.setRequestFactory(clientHttpRequestFactory);
|
||||
final CharSequence accessToken = restTemplate.getAccessToken();
|
||||
|
||||
if (accessToken == null) {
|
||||
throw new RuntimeException("Failed to get access token for LMS " +
|
||||
lmsSetup.name + "(" + lmsSetup.id +
|
||||
") on path: " + accessTokenPath);
|
||||
}
|
||||
|
||||
return restTemplate;
|
||||
});
|
||||
}
|
||||
|
||||
public static class MoodleAPIRestTemplateImpl extends RestTemplate implements MoodleAPIRestTemplate {
|
||||
|
||||
private static final String REST_API_TEST_FUNCTION = "core_webservice_get_site_info";
|
||||
|
||||
final JSONMapper jsonMapper;
|
||||
final APITemplateDataSupplier apiTemplateDataSupplier;
|
||||
|
||||
private final String serverURL;
|
||||
private final String tokenPath;
|
||||
|
||||
private CharSequence accessToken;
|
||||
|
||||
private final Map<String, String> tokenReqURIVars;
|
||||
private final HttpEntity<?> tokenReqEntity = new HttpEntity<>(new LinkedMultiValueMap<>());
|
||||
|
||||
protected MoodleAPIRestTemplateImpl(
|
||||
final JSONMapper jsonMapper,
|
||||
final APITemplateDataSupplier apiTemplateDataSupplier,
|
||||
final String serverURL,
|
||||
final String tokenPath,
|
||||
final CharSequence accessToken,
|
||||
final CharSequence username,
|
||||
final CharSequence password) {
|
||||
|
||||
this.jsonMapper = jsonMapper;
|
||||
this.apiTemplateDataSupplier = apiTemplateDataSupplier;
|
||||
|
||||
this.serverURL = serverURL;
|
||||
this.tokenPath = tokenPath;
|
||||
this.accessToken = StringUtils.isNotBlank(accessToken) ? accessToken : null;
|
||||
|
||||
this.tokenReqURIVars = new HashMap<>();
|
||||
this.tokenReqURIVars.put(URI_VAR_USER_NAME, String.valueOf(username));
|
||||
this.tokenReqURIVars.put(URI_VAR_PASSWORD, String.valueOf(password));
|
||||
this.tokenReqURIVars.put(URI_VAR_SERVICE, "moodle_mobile_app");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getService() {
|
||||
return this.tokenReqURIVars.get(URI_VAR_SERVICE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setService(final String service) {
|
||||
this.tokenReqURIVars.put(URI_VAR_SERVICE, service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getAccessToken() {
|
||||
if (this.accessToken == null) {
|
||||
requestAccessToken();
|
||||
}
|
||||
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void testAPIConnection(final String... functions) {
|
||||
try {
|
||||
final String apiInfo = this.callMoodleAPIFunction(REST_API_TEST_FUNCTION);
|
||||
final WebserviceInfo webserviceInfo = this.jsonMapper.readValue(
|
||||
apiInfo,
|
||||
WebserviceInfo.class);
|
||||
|
||||
if (StringUtils.isBlank(webserviceInfo.username) || StringUtils.isBlank(webserviceInfo.userid)) {
|
||||
throw new RuntimeException("Invalid WebserviceInfo: " + webserviceInfo);
|
||||
}
|
||||
|
||||
if (functions != null) {
|
||||
|
||||
final List<String> missingAPIFunctions = Arrays.stream(functions)
|
||||
.filter(f -> !webserviceInfo.functions.containsKey(f))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!missingAPIFunctions.isEmpty()) {
|
||||
throw new RuntimeException("Missing Moodle Webservice API functions: " + missingAPIFunctions);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (final RuntimeException re) {
|
||||
throw re;
|
||||
} catch (final Exception e) {
|
||||
throw new RuntimeException("Failed to test Moodle rest API: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String callMoodleAPIFunction(final String functionName) {
|
||||
return callMoodleAPIFunction(functionName, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String callMoodleAPIFunction(
|
||||
final String functionName,
|
||||
final MultiValueMap<String, String> queryAttributes) {
|
||||
return callMoodleAPIFunction(functionName, null, queryAttributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String callMoodleAPIFunction(
|
||||
final String functionName,
|
||||
final MultiValueMap<String, String> queryParams,
|
||||
final MultiValueMap<String, String> queryAttributes) {
|
||||
|
||||
getAccessToken();
|
||||
|
||||
final UriComponentsBuilder queryParam = UriComponentsBuilder
|
||||
.fromHttpUrl(this.serverURL + MOODLE_DEFAULT_REST_API_PATH)
|
||||
.queryParam(REST_REQUEST_TOKEN_NAME, this.accessToken)
|
||||
.queryParam(REST_REQUEST_FUNCTION_NAME, functionName)
|
||||
.queryParam(REST_REQUEST_FORMAT_NAME, "json");
|
||||
|
||||
if (queryParams != null && !queryParams.isEmpty()) {
|
||||
queryParam.queryParams(queryParams);
|
||||
}
|
||||
|
||||
final boolean usePOST = queryAttributes != null && !queryAttributes.isEmpty();
|
||||
HttpEntity<?> functionReqEntity;
|
||||
if (usePOST) {
|
||||
final HttpHeaders headers = new HttpHeaders();
|
||||
headers.set(
|
||||
HttpHeaders.CONTENT_TYPE,
|
||||
MediaType.APPLICATION_FORM_URLENCODED_VALUE);
|
||||
|
||||
final String body = Utils.toAppFormUrlEncodedBody(queryAttributes);
|
||||
functionReqEntity = new HttpEntity<>(body, headers);
|
||||
|
||||
} else {
|
||||
functionReqEntity = new HttpEntity<>(new LinkedMultiValueMap<>());
|
||||
}
|
||||
|
||||
final ResponseEntity<String> response = super.exchange(
|
||||
queryParam.toUriString(),
|
||||
usePOST ? HttpMethod.POST : HttpMethod.GET,
|
||||
functionReqEntity,
|
||||
String.class);
|
||||
|
||||
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
throw new RuntimeException(
|
||||
"Failed to call Moodle webservice API function: " + functionName + " lms setup: " +
|
||||
lmsSetup + " response: " + response.getBody());
|
||||
}
|
||||
|
||||
final String body = response.getBody();
|
||||
|
||||
// NOTE: for some unknown reason, Moodles API error responses come with a 200 OK response HTTP Status
|
||||
// So this is a special Moodle specific error handling here...
|
||||
if (body.startsWith("{exception") || body.contains("\"exception\":")) {
|
||||
// Reset access token to get new on next call (fix access if token is expired)
|
||||
// NOTE: find a way to verify token invalidity response from Moodle.
|
||||
// Unfortunately there is not a lot of Moodle documentation for the API error handling around.
|
||||
this.accessToken = null;
|
||||
throw new RuntimeException(
|
||||
"Failed to call Moodle webservice API function: " + functionName + " lms setup: " +
|
||||
lmsSetup + " response: " + body);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
private void requestAccessToken() {
|
||||
|
||||
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||
try {
|
||||
|
||||
final ResponseEntity<String> response = super.exchange(
|
||||
this.serverURL + this.tokenPath,
|
||||
HttpMethod.GET,
|
||||
this.tokenReqEntity,
|
||||
String.class,
|
||||
this.tokenReqURIVars);
|
||||
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} response: {} : {}",
|
||||
lmsSetup,
|
||||
response.getStatusCode(),
|
||||
response.getBody());
|
||||
throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " +
|
||||
lmsSetup + " response: " + response.getBody());
|
||||
}
|
||||
|
||||
try {
|
||||
final MoodleToken moodleToken = this.jsonMapper.readValue(
|
||||
response.getBody(),
|
||||
MoodleToken.class);
|
||||
|
||||
if (moodleToken == null || moodleToken.token == null) {
|
||||
throw new RuntimeException("Access Token request with 200 but no or invalid token body");
|
||||
} else {
|
||||
log.info("Successfully get access token from Moodle: {}",
|
||||
lmsSetup);
|
||||
}
|
||||
|
||||
this.accessToken = moodleToken.token;
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} response: {} : {}",
|
||||
lmsSetup,
|
||||
response.getStatusCode(),
|
||||
response.getBody());
|
||||
throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " +
|
||||
lmsSetup + " response: " + response.getBody(), e);
|
||||
}
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to gain access token for LMS (Moodle): lmsSetup: {} :",
|
||||
lmsSetup,
|
||||
e);
|
||||
throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " +
|
||||
lmsSetup + " cause: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private final static class MoodleToken {
|
||||
final String token;
|
||||
@SuppressWarnings("unused")
|
||||
final String privatetoken;
|
||||
|
||||
@JsonCreator
|
||||
protected MoodleToken(
|
||||
@JsonProperty(value = "token") final String token,
|
||||
@JsonProperty(value = "privatetoken", required = false) final String privatetoken) {
|
||||
|
||||
this.token = token;
|
||||
this.privatetoken = privatetoken;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private final static class WebserviceInfo {
|
||||
String username;
|
||||
String userid;
|
||||
Map<String, FunctionInfo> functions;
|
||||
|
||||
@JsonCreator
|
||||
protected WebserviceInfo(
|
||||
@JsonProperty(value = "username") final String username,
|
||||
@JsonProperty(value = "userid") final String userid,
|
||||
@JsonProperty(value = "functions") final Collection<FunctionInfo> functions) {
|
||||
|
||||
this.username = username;
|
||||
this.userid = userid;
|
||||
this.functions = (functions != null)
|
||||
? functions
|
||||
.stream()
|
||||
.collect(Collectors.toMap(fi -> fi.name, Function.identity()))
|
||||
: Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private final static class FunctionInfo {
|
||||
String name;
|
||||
@SuppressWarnings("unused")
|
||||
String version;
|
||||
|
||||
@JsonCreator
|
||||
protected FunctionInfo(
|
||||
@JsonProperty(value = "name") final String name,
|
||||
@JsonProperty(value = "version") final String version) {
|
||||
|
||||
this.name = name;
|
||||
this.version = version;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,518 @@
|
|||
/*
|
||||
* 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.moodle;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
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 com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate.Warning;
|
||||
|
||||
public abstract class MoodleUtils {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MoodleUtils.class);
|
||||
|
||||
public static final String getInternalQuizId(
|
||||
final String quizId,
|
||||
final String courseId,
|
||||
final String shortname,
|
||||
final String idnumber) {
|
||||
|
||||
return StringUtils.join(
|
||||
new String[] {
|
||||
quizId,
|
||||
courseId,
|
||||
StringUtils.isNotBlank(shortname) ? shortname : Constants.EMPTY_NOTE,
|
||||
StringUtils.isNotBlank(idnumber) ? idnumber : Constants.EMPTY_NOTE
|
||||
},
|
||||
Constants.COLON);
|
||||
}
|
||||
|
||||
public static final String getQuizId(final String internalQuizId) {
|
||||
if (StringUtils.isBlank(internalQuizId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return StringUtils.split(internalQuizId, Constants.COLON)[0];
|
||||
}
|
||||
|
||||
public static final String getCourseId(final String internalQuizId) {
|
||||
if (StringUtils.isBlank(internalQuizId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return StringUtils.split(internalQuizId, Constants.COLON)[1];
|
||||
}
|
||||
|
||||
public static final String getShortname(final String internalQuizId) {
|
||||
if (StringUtils.isBlank(internalQuizId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String[] split = StringUtils.split(internalQuizId, Constants.COLON);
|
||||
if (split.length < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String shortName = split[2];
|
||||
return shortName.equals(Constants.EMPTY_NOTE) ? null : shortName;
|
||||
}
|
||||
|
||||
public static final String getIdnumber(final String internalQuizId) {
|
||||
if (StringUtils.isBlank(internalQuizId)) {
|
||||
return null;
|
||||
}
|
||||
final String[] split = StringUtils.split(internalQuizId, Constants.COLON);
|
||||
if (split.length < 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String idNumber = split[3];
|
||||
return idNumber.equals(Constants.EMPTY_NOTE) ? null : idNumber;
|
||||
}
|
||||
|
||||
public static final void logMoodleWarning(
|
||||
final Collection<Warning> warnings,
|
||||
final String lmsSetupName,
|
||||
final String function) {
|
||||
|
||||
log.warn(
|
||||
"There are warnings from Moodle response: Moodle: {} request: {} warnings: {} warning sample: {}",
|
||||
lmsSetupName,
|
||||
function,
|
||||
warnings.size(),
|
||||
warnings.iterator().next().toString());
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("All warnings from Moodle: {}", warnings.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static final Pattern ACCESS_DENIED_PATTERN_1 =
|
||||
Pattern.compile(Pattern.quote("No access rights"), Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern ACCESS_DENIED_PATTERN_2 =
|
||||
Pattern.compile(Pattern.quote("access denied"), Pattern.CASE_INSENSITIVE);
|
||||
|
||||
public static final boolean checkAccessDeniedError(final String courseKeyPageJSON) {
|
||||
return ACCESS_DENIED_PATTERN_1
|
||||
.matcher(courseKeyPageJSON)
|
||||
.find() ||
|
||||
ACCESS_DENIED_PATTERN_2
|
||||
.matcher(courseKeyPageJSON)
|
||||
.find();
|
||||
}
|
||||
|
||||
public static Predicate<CourseData> getCourseFilter() {
|
||||
final long now = Utils.getSecondsNow();
|
||||
return course -> {
|
||||
if (course.start_date != null
|
||||
&& course.start_date < Utils.toUnixTimeInSeconds(DateTime.now(DateTimeZone.UTC).minusYears(3))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (course.end_date == null || course.end_date == 0 || course.end_date > now) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.info("remove course {} end_time {} now {}",
|
||||
course.short_name,
|
||||
course.end_date,
|
||||
now);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
public static Predicate<CourseQuiz> getQuizFilter() {
|
||||
final long now = Utils.getSecondsNow();
|
||||
return quiz -> {
|
||||
if (quiz.time_close == null || quiz.time_close == 0 || quiz.time_close > now) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("remove quiz {} end_time {} now {}",
|
||||
quiz.name,
|
||||
quiz.time_close,
|
||||
now);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
public static List<QuizData> quizDataOf(
|
||||
final LmsSetup lmsSetup,
|
||||
final CourseData courseData,
|
||||
final String uriPrefix,
|
||||
final boolean prependShortCourseName) {
|
||||
|
||||
final Map<String, String> additionalAttrs = new HashMap<>();
|
||||
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_CREATION_TIME, String.valueOf(courseData.time_created));
|
||||
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_SHORT_NAME, courseData.short_name);
|
||||
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_ID_NUMBER, courseData.idnumber);
|
||||
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_FULL_NAME, courseData.full_name);
|
||||
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_DISPLAY_NAME, courseData.display_name);
|
||||
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_SUMMARY, courseData.summary);
|
||||
|
||||
final List<QuizData> courseAndQuiz = courseData.quizzes
|
||||
.stream()
|
||||
.map(courseQuizData -> {
|
||||
final String startURI = uriPrefix + courseQuizData.course_module;
|
||||
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_TIME_LIMIT, String.valueOf(courseQuizData.time_limit));
|
||||
return new QuizData(
|
||||
MoodleUtils.getInternalQuizId(
|
||||
courseQuizData.course_module,
|
||||
courseData.id,
|
||||
courseData.short_name,
|
||||
courseData.idnumber),
|
||||
lmsSetup.getInstitutionId(),
|
||||
lmsSetup.id,
|
||||
lmsSetup.getLmsType(),
|
||||
(prependShortCourseName)
|
||||
? courseData.short_name + " : " + courseQuizData.name
|
||||
: courseQuizData.name,
|
||||
courseQuizData.intro,
|
||||
(courseQuizData.time_open != null && courseQuizData.time_open > 0)
|
||||
? Utils.toDateTimeUTCUnix(courseQuizData.time_open)
|
||||
: Utils.toDateTimeUTCUnix(courseData.start_date),
|
||||
(courseQuizData.time_close != null && courseQuizData.time_close > 0)
|
||||
? Utils.toDateTimeUTCUnix(courseQuizData.time_close)
|
||||
: Utils.toDateTimeUTCUnix(courseData.end_date),
|
||||
startURI,
|
||||
additionalAttrs);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return courseAndQuiz;
|
||||
}
|
||||
|
||||
public static final void fillSelectedQuizzes(
|
||||
final Set<String> quizIds,
|
||||
final Map<String, CourseData> finalCourseDataRef,
|
||||
final CourseQuiz quiz) {
|
||||
try {
|
||||
final CourseData course = finalCourseDataRef.get(quiz.course);
|
||||
if (course != null) {
|
||||
final String internalQuizId = MoodleUtils.getInternalQuizId(
|
||||
quiz.course_module,
|
||||
course.id,
|
||||
course.short_name,
|
||||
course.idnumber);
|
||||
if (quizIds.contains(internalQuizId)) {
|
||||
course.quizzes.add(quiz);
|
||||
}
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to verify selected quiz for course: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Mapping Classes ---
|
||||
|
||||
/** Maps the Moodle course API course data */
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static final class CourseData {
|
||||
public final String id;
|
||||
public final String short_name;
|
||||
public final String idnumber;
|
||||
public final String full_name;
|
||||
public final String display_name;
|
||||
public final String summary;
|
||||
public final Long start_date; // unix-time seconds UTC
|
||||
public final Long end_date; // unix-time seconds UTC
|
||||
public final Long time_created; // unix-time seconds UTC
|
||||
public final String category_id;
|
||||
|
||||
@JsonIgnore
|
||||
public final Collection<CourseQuiz> quizzes = new ArrayList<>();
|
||||
|
||||
@JsonCreator
|
||||
public CourseData(
|
||||
@JsonProperty(value = "id") final String id,
|
||||
@JsonProperty(value = "shortname") final String short_name,
|
||||
@JsonProperty(value = "idnumber") final String idnumber,
|
||||
@JsonProperty(value = "fullname") final String full_name,
|
||||
@JsonProperty(value = "displayname") final String display_name,
|
||||
@JsonProperty(value = "summary") final String summary,
|
||||
@JsonProperty(value = "startdate") final Long start_date,
|
||||
@JsonProperty(value = "enddate") final Long end_date,
|
||||
@JsonProperty(value = "timecreated") final Long time_created,
|
||||
@JsonProperty(value = "categoryid") final String category_id) {
|
||||
|
||||
this.id = id;
|
||||
this.short_name = short_name;
|
||||
this.idnumber = idnumber;
|
||||
this.full_name = full_name;
|
||||
this.display_name = display_name;
|
||||
this.summary = summary;
|
||||
this.start_date = start_date;
|
||||
this.end_date = end_date;
|
||||
this.time_created = time_created;
|
||||
this.category_id = category_id;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static final class Courses {
|
||||
public final Collection<CourseData> courses;
|
||||
public final Collection<Warning> warnings;
|
||||
|
||||
@JsonCreator
|
||||
public Courses(
|
||||
@JsonProperty(value = "courses") final Collection<CourseData> courses,
|
||||
@JsonProperty(value = "warnings") final Collection<Warning> warnings) {
|
||||
this.courses = courses;
|
||||
this.warnings = warnings;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static final class CourseQuizData {
|
||||
public final Collection<CourseQuiz> quizzes;
|
||||
public final Collection<Warning> warnings;
|
||||
|
||||
@JsonCreator
|
||||
public CourseQuizData(
|
||||
@JsonProperty(value = "quizzes") final Collection<CourseQuiz> quizzes,
|
||||
@JsonProperty(value = "warnings") final Collection<Warning> warnings) {
|
||||
this.quizzes = quizzes;
|
||||
this.warnings = warnings;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static final class CourseQuiz {
|
||||
public final String id;
|
||||
public final String course;
|
||||
public final String course_module;
|
||||
public final String name;
|
||||
public final String intro; // HTML
|
||||
public final Long time_open; // unix-time seconds UTC
|
||||
public final Long time_close; // unix-time seconds UTC
|
||||
public final Long time_limit; // unix-time seconds UTC
|
||||
|
||||
@JsonCreator
|
||||
public CourseQuiz(
|
||||
@JsonProperty(value = "id") final String id,
|
||||
@JsonProperty(value = "course") final String course,
|
||||
@JsonProperty(value = "coursemodule") final String course_module,
|
||||
@JsonProperty(value = "name") final String name,
|
||||
@JsonProperty(value = "intro") final String intro,
|
||||
@JsonProperty(value = "timeopen") final Long time_open,
|
||||
@JsonProperty(value = "timeclose") final Long time_close,
|
||||
@JsonProperty(value = "timelimit") final Long time_limit) {
|
||||
|
||||
this.id = id;
|
||||
this.course = course;
|
||||
this.course_module = course_module;
|
||||
this.name = name;
|
||||
this.intro = intro;
|
||||
this.time_open = time_open;
|
||||
this.time_close = time_close;
|
||||
this.time_limit = time_limit;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static final class MoodleUserDetails {
|
||||
public final String id;
|
||||
public final String username;
|
||||
public final String firstname;
|
||||
public final String lastname;
|
||||
public final String fullname;
|
||||
public final String email;
|
||||
public final String department;
|
||||
public final Long firstaccess;
|
||||
public final Long lastaccess;
|
||||
public final String auth;
|
||||
public final Boolean suspended;
|
||||
public final Boolean confirmed;
|
||||
public final String lang;
|
||||
public final String theme;
|
||||
public final String timezone;
|
||||
public final String description;
|
||||
public final Integer mailformat;
|
||||
public final Integer descriptionformat;
|
||||
|
||||
@JsonCreator
|
||||
public MoodleUserDetails(
|
||||
@JsonProperty(value = "id") final String id,
|
||||
@JsonProperty(value = "username") final String username,
|
||||
@JsonProperty(value = "firstname") final String firstname,
|
||||
@JsonProperty(value = "lastname") final String lastname,
|
||||
@JsonProperty(value = "fullname") final String fullname,
|
||||
@JsonProperty(value = "email") final String email,
|
||||
@JsonProperty(value = "department") final String department,
|
||||
@JsonProperty(value = "firstaccess") final Long firstaccess,
|
||||
@JsonProperty(value = "lastaccess") final Long lastaccess,
|
||||
@JsonProperty(value = "auth") final String auth,
|
||||
@JsonProperty(value = "suspended") final Boolean suspended,
|
||||
@JsonProperty(value = "confirmed") final Boolean confirmed,
|
||||
@JsonProperty(value = "lang") final String lang,
|
||||
@JsonProperty(value = "theme") final String theme,
|
||||
@JsonProperty(value = "timezone") final String timezone,
|
||||
@JsonProperty(value = "description") final String description,
|
||||
@JsonProperty(value = "mailformat") final Integer mailformat,
|
||||
@JsonProperty(value = "descriptionformat") final Integer descriptionformat) {
|
||||
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.firstname = firstname;
|
||||
this.lastname = lastname;
|
||||
this.fullname = fullname;
|
||||
this.email = email;
|
||||
this.department = department;
|
||||
this.firstaccess = firstaccess;
|
||||
this.lastaccess = lastaccess;
|
||||
this.auth = auth;
|
||||
this.suspended = suspended;
|
||||
this.confirmed = confirmed;
|
||||
this.lang = lang;
|
||||
this.theme = theme;
|
||||
this.timezone = timezone;
|
||||
this.description = description;
|
||||
this.mailformat = mailformat;
|
||||
this.descriptionformat = descriptionformat;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static final class MoodlePluginUserDetails {
|
||||
public final String id;
|
||||
public final String fullname;
|
||||
public final String username;
|
||||
public final String firstname;
|
||||
public final String lastname;
|
||||
public final String idnumber;
|
||||
public final String email;
|
||||
public final Map<String, String> customfields;
|
||||
|
||||
@JsonCreator
|
||||
public MoodlePluginUserDetails(
|
||||
final String id,
|
||||
final String username,
|
||||
final String firstname,
|
||||
final String lastname,
|
||||
final String idnumber,
|
||||
final String email,
|
||||
final Map<String, String> customfields) {
|
||||
|
||||
this.id = id;
|
||||
if (firstname != null && lastname != null) {
|
||||
this.fullname = firstname + Constants.SPACE + lastname;
|
||||
} else if (firstname != null) {
|
||||
this.fullname = firstname;
|
||||
} else {
|
||||
this.fullname = lastname;
|
||||
}
|
||||
this.username = username;
|
||||
this.firstname = firstname;
|
||||
this.lastname = lastname;
|
||||
this.idnumber = idnumber;
|
||||
this.email = email;
|
||||
this.customfields = Utils.immutableMapOf(customfields);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static final class CoursePage {
|
||||
public final Collection<CourseKey> courseKeys;
|
||||
public final Collection<Warning> warnings;
|
||||
|
||||
public CoursePage(
|
||||
@JsonProperty(value = "courses") final Collection<CourseKey> courseKeys,
|
||||
@JsonProperty(value = "warnings") final Collection<Warning> warnings) {
|
||||
|
||||
this.courseKeys = courseKeys;
|
||||
this.warnings = warnings;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static final class CourseKey {
|
||||
public final String id;
|
||||
public final String short_name;
|
||||
public final String category_name;
|
||||
public final String sort_order;
|
||||
|
||||
@JsonCreator
|
||||
public CourseKey(
|
||||
@JsonProperty(value = "id") final String id,
|
||||
@JsonProperty(value = "shortname") final String short_name,
|
||||
@JsonProperty(value = "categoryname") final String category_name,
|
||||
@JsonProperty(value = "sortorder") final String sort_order) {
|
||||
|
||||
this.id = id;
|
||||
this.short_name = short_name;
|
||||
this.category_name = category_name;
|
||||
this.sort_order = sort_order;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
builder.append("CourseKey [id=");
|
||||
builder.append(this.id);
|
||||
builder.append(", short_name=");
|
||||
builder.append(this.short_name);
|
||||
builder.append(", category_name=");
|
||||
builder.append(this.category_name);
|
||||
builder.append(", sort_order=");
|
||||
builder.append(this.sort_order);
|
||||
builder.append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static final class MoodleQuizRestriction {
|
||||
public final String quiz_id;
|
||||
public final String config_keys;
|
||||
public final String browser_exam_keys;
|
||||
public final String quit_link;
|
||||
public final String quit_secret;
|
||||
|
||||
@JsonCreator
|
||||
public MoodleQuizRestriction(
|
||||
final String quiz_id,
|
||||
final String config_keys,
|
||||
final String browser_exam_keys,
|
||||
final String quit_link,
|
||||
final String quit_secret) {
|
||||
|
||||
this.quiz_id = quiz_id;
|
||||
this.config_keys = config_keys;
|
||||
this.browser_exam_keys = browser_exam_keys;
|
||||
this.quit_link = quit_link;
|
||||
this.quit_secret = quit_secret;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -17,24 +17,18 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
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 org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonParseException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
|
@ -56,8 +50,13 @@ 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;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate.Warning;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CourseData;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CoursePage;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CourseQuizData;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.Courses;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.MoodleUserDetails;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseDataAsyncLoader.CourseDataShort;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseDataAsyncLoader.CourseQuizShort;
|
||||
|
||||
|
@ -98,7 +97,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
static final String MOODLE_COURSE_API_SEARCH_PAGE_SIZE = "perpage";
|
||||
|
||||
private final JSONMapper jsonMapper;
|
||||
private final MoodleRestTemplateFactory moodleRestTemplateFactory;
|
||||
private final MoodleRestTemplateFactory restTemplateFactory;
|
||||
private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader;
|
||||
private final boolean prependShortCourseName;
|
||||
private final CircuitBreaker<String> protectedMoodlePageCall;
|
||||
|
@ -110,13 +109,13 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
public MoodleCourseAccess(
|
||||
final JSONMapper jsonMapper,
|
||||
final AsyncService asyncService,
|
||||
final MoodleRestTemplateFactory moodleRestTemplateFactory,
|
||||
final MoodleRestTemplateFactory restTemplateFactory,
|
||||
final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader,
|
||||
final Environment environment) {
|
||||
|
||||
this.jsonMapper = jsonMapper;
|
||||
this.moodleCourseDataAsyncLoader = moodleCourseDataAsyncLoader;
|
||||
this.moodleRestTemplateFactory = moodleRestTemplateFactory;
|
||||
this.restTemplateFactory = restTemplateFactory;
|
||||
|
||||
this.prependShortCourseName = BooleanUtils.toBoolean(environment.getProperty(
|
||||
"sebserver.webservice.lms.moodle.prependShortCourseName",
|
||||
|
@ -142,12 +141,12 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
}
|
||||
|
||||
APITemplateDataSupplier getApiTemplateDataSupplier() {
|
||||
return this.moodleRestTemplateFactory.apiTemplateDataSupplier;
|
||||
return this.restTemplateFactory.getApiTemplateDataSupplier();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LmsSetupTestResult testCourseAccessAPI() {
|
||||
final LmsSetupTestResult attributesCheck = this.moodleRestTemplateFactory.test();
|
||||
final LmsSetupTestResult attributesCheck = this.restTemplateFactory.test();
|
||||
if (!attributesCheck.isOk()) {
|
||||
return attributesCheck;
|
||||
}
|
||||
|
@ -155,7 +154,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate();
|
||||
if (restTemplateRequest.hasError()) {
|
||||
final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " +
|
||||
this.moodleRestTemplateFactory.knownTokenAccessPaths;
|
||||
this.restTemplateFactory.getKnownTokenAccessPaths();
|
||||
log.error(message + " cause: {}", restTemplateRequest.getError().getMessage());
|
||||
return LmsSetupTestResult.ofTokenRequestError(LmsType.MOODLE, message);
|
||||
}
|
||||
|
@ -189,12 +188,12 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
try {
|
||||
int page = 0;
|
||||
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
|
||||
final MoodleAPIRestTemplate restTemplate = getRestTemplate().getOrThrow();
|
||||
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
|
||||
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
|
||||
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
|
||||
|
||||
while (!asyncQuizFetchBuffer.finished && !asyncQuizFetchBuffer.canceled) {
|
||||
final MoodleAPIRestTemplate restTemplate = getRestTemplate().getOrThrow();
|
||||
// first get courses from Moodle for page
|
||||
final Map<String, CourseData> courseData = new HashMap<>();
|
||||
final Collection<CourseData> coursesPage = getCoursesPage(restTemplate, page, this.pageSize);
|
||||
|
@ -236,15 +235,10 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
}
|
||||
|
||||
if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) {
|
||||
log.warn(
|
||||
"There are warnings from Moodle response: Moodle: {} request: {} warnings: {} warning sample: {}",
|
||||
MoodleUtils.logMoodleWarning(
|
||||
courseQuizData.warnings,
|
||||
lmsSetup.name,
|
||||
MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME,
|
||||
courseQuizData.warnings.size(),
|
||||
courseQuizData.warnings.iterator().next().toString());
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("All warnings from Moodle: {}", courseQuizData.warnings.toString());
|
||||
}
|
||||
MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME);
|
||||
}
|
||||
|
||||
if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) {
|
||||
|
@ -255,7 +249,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
|
||||
courseQuizData.quizzes
|
||||
.stream()
|
||||
.filter(getQuizFilter())
|
||||
.filter(MoodleUtils.getQuizFilter())
|
||||
.forEach(quiz -> {
|
||||
final CourseData data = courseData.get(quiz.course);
|
||||
if (data != null) {
|
||||
|
@ -266,10 +260,16 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
courseData.values().stream()
|
||||
.filter(c -> !c.quizzes.isEmpty())
|
||||
.forEach(c -> asyncQuizFetchBuffer.buffer.addAll(
|
||||
quizDataOf(lmsSetup, c, urlPrefix).stream()
|
||||
MoodleUtils.quizDataOf(lmsSetup, c, urlPrefix, this.prependShortCourseName)
|
||||
.stream()
|
||||
.filter(LmsAPIService.quizFilterPredicate(filterMap))
|
||||
.collect(Collectors.toList())));
|
||||
|
||||
if (asyncQuizFetchBuffer.buffer.size() > this.maxSize) {
|
||||
log.warn("Maximal moodle quiz fetch size of {} reached. Cancel fetch at this point.", this.maxSize);
|
||||
asyncQuizFetchBuffer.finish();
|
||||
}
|
||||
|
||||
page++;
|
||||
}
|
||||
|
||||
|
@ -298,8 +298,8 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
final Map<String, CourseDataShort> cachedCourseData = this.moodleCourseDataAsyncLoader
|
||||
.getCachedCourseData();
|
||||
|
||||
final String courseId = getCourseId(id);
|
||||
final String quizId = getQuizId(id);
|
||||
final String courseId = MoodleUtils.getCourseId(id);
|
||||
final String quizId = MoodleUtils.getQuizId(id);
|
||||
if (cachedCourseData.containsKey(courseId)) {
|
||||
final CourseDataShort courseData = cachedCourseData.get(courseId);
|
||||
final CourseQuizShort quiz = courseData.quizzes
|
||||
|
@ -352,7 +352,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
MOODLE_USER_PROFILE_API_FUNCTION_NAME,
|
||||
queryAttributes);
|
||||
|
||||
if (checkAccessDeniedError(userDetailsJSON)) {
|
||||
if (MoodleUtils.checkAccessDeniedError(userDetailsJSON)) {
|
||||
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
|
||||
log.error("Get access denied error from Moodle: {} for API call: {}, response: {}",
|
||||
lmsSetup,
|
||||
|
@ -486,7 +486,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
final Map<String, CourseData> courseData = getCoursesForIds(
|
||||
restTemplate,
|
||||
quizIds.stream()
|
||||
.map(MoodleCourseAccess::getCourseId)
|
||||
.map(MoodleUtils::getCourseId)
|
||||
.collect(Collectors.toSet()))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(cd -> cd.id, Function.identity()));
|
||||
|
@ -518,7 +518,12 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
logMoodleWarnings(courseQuizData.warnings);
|
||||
if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) {
|
||||
MoodleUtils.logMoodleWarning(
|
||||
courseQuizData.warnings,
|
||||
lmsSetup.name,
|
||||
MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME);
|
||||
}
|
||||
|
||||
if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) {
|
||||
log.error("No quizzes found for ids: {} on LMS; {}", quizIds, lmsSetup.name);
|
||||
|
@ -528,7 +533,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
final Map<String, CourseData> finalCourseDataRef = courseData;
|
||||
courseQuizData.quizzes
|
||||
.stream()
|
||||
.forEach(quiz -> fillSelectedQuizzes(quizIds, finalCourseDataRef, quiz));
|
||||
.forEach(quiz -> MoodleUtils.fillSelectedQuizzes(quizIds, finalCourseDataRef, quiz));
|
||||
|
||||
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
|
||||
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
|
||||
|
@ -537,7 +542,11 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
return courseData.values()
|
||||
.stream()
|
||||
.filter(c -> !c.quizzes.isEmpty())
|
||||
.flatMap(cd -> quizDataOf(lmsSetup, cd, urlPrefix).stream())
|
||||
.flatMap(cd -> MoodleUtils.quizDataOf(
|
||||
lmsSetup,
|
||||
cd,
|
||||
urlPrefix,
|
||||
this.prependShortCourseName).stream())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
} catch (final Exception e) {
|
||||
|
@ -546,27 +555,6 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
}
|
||||
}
|
||||
|
||||
private void fillSelectedQuizzes(
|
||||
final Set<String> quizIds,
|
||||
final Map<String, CourseData> finalCourseDataRef,
|
||||
final CourseQuiz quiz) {
|
||||
try {
|
||||
final CourseData course = finalCourseDataRef.get(quiz.course);
|
||||
if (course != null) {
|
||||
final String internalQuizId = getInternalQuizId(
|
||||
quiz.course_module,
|
||||
course.id,
|
||||
course.short_name,
|
||||
course.idnumber);
|
||||
if (quizIds.contains(internalQuizId)) {
|
||||
course.quizzes.add(quiz);
|
||||
}
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to verify selected quiz for course: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<CourseData> getCoursesForIds(
|
||||
final MoodleAPIRestTemplate restTemplate,
|
||||
final Set<String> ids) {
|
||||
|
@ -596,7 +584,12 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
logMoodleWarnings(courses.warnings);
|
||||
if (courses.warnings != null && !courses.warnings.isEmpty()) {
|
||||
MoodleUtils.logMoodleWarning(
|
||||
courses.warnings,
|
||||
lmsSetup.name,
|
||||
MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME);
|
||||
}
|
||||
|
||||
if (courses.courses == null || courses.courses.isEmpty()) {
|
||||
log.error("No courses found for ids: {} on LMS: {}", ids, lmsSetup.name);
|
||||
|
@ -610,51 +603,6 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
}
|
||||
}
|
||||
|
||||
private List<QuizData> quizDataOf(
|
||||
final LmsSetup lmsSetup,
|
||||
final CourseData courseData,
|
||||
final String uriPrefix) {
|
||||
|
||||
final Map<String, String> additionalAttrs = new HashMap<>();
|
||||
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_CREATION_TIME, String.valueOf(courseData.time_created));
|
||||
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_SHORT_NAME, courseData.short_name);
|
||||
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_ID_NUMBER, courseData.idnumber);
|
||||
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_FULL_NAME, courseData.full_name);
|
||||
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_DISPLAY_NAME, courseData.display_name);
|
||||
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_SUMMARY, courseData.summary);
|
||||
|
||||
final List<QuizData> courseAndQuiz = courseData.quizzes
|
||||
.stream()
|
||||
.map(courseQuizData -> {
|
||||
final String startURI = uriPrefix + courseQuizData.course_module;
|
||||
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_TIME_LIMIT, String.valueOf(courseQuizData.time_limit));
|
||||
return new QuizData(
|
||||
getInternalQuizId(
|
||||
courseQuizData.course_module,
|
||||
courseData.id,
|
||||
courseData.short_name,
|
||||
courseData.idnumber),
|
||||
lmsSetup.getInstitutionId(),
|
||||
lmsSetup.id,
|
||||
lmsSetup.getLmsType(),
|
||||
(this.prependShortCourseName)
|
||||
? courseData.short_name + " : " + courseQuizData.name
|
||||
: courseQuizData.name,
|
||||
courseQuizData.intro,
|
||||
(courseQuizData.time_open != null && courseQuizData.time_open > 0)
|
||||
? Utils.toDateTimeUTCUnix(courseQuizData.time_open)
|
||||
: Utils.toDateTimeUTCUnix(courseData.start_date),
|
||||
(courseQuizData.time_close != null && courseQuizData.time_close > 0)
|
||||
? Utils.toDateTimeUTCUnix(courseQuizData.time_close)
|
||||
: Utils.toDateTimeUTCUnix(courseData.end_date),
|
||||
startURI,
|
||||
additionalAttrs);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return courseAndQuiz;
|
||||
}
|
||||
|
||||
private List<QuizData> quizDataOf(
|
||||
final LmsSetup lmsSetup,
|
||||
final CourseDataShort courseData,
|
||||
|
@ -682,7 +630,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
|
||||
final String startURI = uriPrefix + courseQuizData.course_module;
|
||||
return new QuizData(
|
||||
getInternalQuizId(
|
||||
MoodleUtils.getInternalQuizId(
|
||||
courseQuizData.course_module,
|
||||
courseData.id,
|
||||
courseData.short_name,
|
||||
|
@ -706,7 +654,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
|
||||
private Result<MoodleAPIRestTemplate> getRestTemplate() {
|
||||
if (this.restTemplate == null) {
|
||||
final Result<MoodleAPIRestTemplate> templateRequest = this.moodleRestTemplateFactory
|
||||
final Result<MoodleAPIRestTemplate> templateRequest = this.restTemplateFactory
|
||||
.createRestTemplate();
|
||||
if (templateRequest.hasError()) {
|
||||
return templateRequest;
|
||||
|
@ -718,95 +666,6 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
return Result.of(this.restTemplate);
|
||||
}
|
||||
|
||||
public static final String getInternalQuizId(
|
||||
final String quizId,
|
||||
final String courseId,
|
||||
final String shortname,
|
||||
final String idnumber) {
|
||||
|
||||
return StringUtils.join(
|
||||
new String[] {
|
||||
quizId,
|
||||
courseId,
|
||||
StringUtils.isNotBlank(shortname) ? shortname : Constants.EMPTY_NOTE,
|
||||
StringUtils.isNotBlank(idnumber) ? idnumber : Constants.EMPTY_NOTE
|
||||
},
|
||||
Constants.COLON);
|
||||
}
|
||||
|
||||
public static final String getQuizId(final String internalQuizId) {
|
||||
if (StringUtils.isBlank(internalQuizId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return StringUtils.split(internalQuizId, Constants.COLON)[0];
|
||||
}
|
||||
|
||||
public static final String getCourseId(final String internalQuizId) {
|
||||
if (StringUtils.isBlank(internalQuizId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return StringUtils.split(internalQuizId, Constants.COLON)[1];
|
||||
}
|
||||
|
||||
public static final String getShortname(final String internalQuizId) {
|
||||
if (StringUtils.isBlank(internalQuizId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String[] split = StringUtils.split(internalQuizId, Constants.COLON);
|
||||
if (split.length < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String shortName = split[2];
|
||||
return shortName.equals(Constants.EMPTY_NOTE) ? null : shortName;
|
||||
}
|
||||
|
||||
public static final String getIdnumber(final String internalQuizId) {
|
||||
if (StringUtils.isBlank(internalQuizId)) {
|
||||
return null;
|
||||
}
|
||||
final String[] split = StringUtils.split(internalQuizId, Constants.COLON);
|
||||
if (split.length < 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String idNumber = split[3];
|
||||
return idNumber.equals(Constants.EMPTY_NOTE) ? null : idNumber;
|
||||
}
|
||||
|
||||
private void logMoodleWarnings(final Collection<Warning> warnings) {
|
||||
if (warnings != null && !warnings.isEmpty()) {
|
||||
if (log.isDebugEnabled()) {
|
||||
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
|
||||
log.debug(
|
||||
"There are warnings from Moodle response: Moodle: {} request: {} warnings: {} warning sample: {}",
|
||||
lmsSetup,
|
||||
MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME,
|
||||
warnings.size(),
|
||||
warnings.iterator().next().toString());
|
||||
} else if (log.isTraceEnabled()) {
|
||||
log.trace("All warnings from Moodle: {}", warnings.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final Pattern ACCESS_DENIED_PATTERN_1 =
|
||||
Pattern.compile(Pattern.quote("No access rights"), Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern ACCESS_DENIED_PATTERN_2 =
|
||||
Pattern.compile(Pattern.quote("access denied"), Pattern.CASE_INSENSITIVE);
|
||||
|
||||
public static final boolean checkAccessDeniedError(final String courseKeyPageJSON) {
|
||||
return ACCESS_DENIED_PATTERN_1
|
||||
.matcher(courseKeyPageJSON)
|
||||
.find() ||
|
||||
ACCESS_DENIED_PATTERN_2
|
||||
.matcher(courseKeyPageJSON)
|
||||
.find();
|
||||
}
|
||||
|
||||
private Collection<CourseData> getCoursesPage(
|
||||
final MoodleAPIRestTemplate restTemplate,
|
||||
final int page,
|
||||
|
@ -866,7 +725,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
|
||||
final Collection<CourseData> result = getCoursesForIds(restTemplate, ids)
|
||||
.stream()
|
||||
.filter(getCourseFilter())
|
||||
.filter(MoodleUtils.getCourseFilter())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
|
@ -882,258 +741,4 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
|||
}
|
||||
}
|
||||
|
||||
private Predicate<CourseData> getCourseFilter() {
|
||||
final long now = Utils.getSecondsNow();
|
||||
return course -> {
|
||||
if (course.start_date != null
|
||||
&& course.start_date < Utils.toUnixTimeInSeconds(DateTime.now(DateTimeZone.UTC).minusYears(3))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (course.end_date == null || course.end_date == 0 || course.end_date > now) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.info("remove course {} end_time {} now {}",
|
||||
course.short_name,
|
||||
course.end_date,
|
||||
now);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
private Predicate<CourseQuiz> getQuizFilter() {
|
||||
final long now = Utils.getSecondsNow();
|
||||
return quiz -> {
|
||||
if (quiz.time_close == null || quiz.time_close == 0 || quiz.time_close > now) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("remove quiz {} end_time {} now {}",
|
||||
quiz.name,
|
||||
quiz.time_close,
|
||||
now);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Mapping Classes ---
|
||||
|
||||
/** Maps the Moodle course API course data */
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static final class CourseData {
|
||||
final String id;
|
||||
final String short_name;
|
||||
final String idnumber;
|
||||
final String full_name;
|
||||
final String display_name;
|
||||
final String summary;
|
||||
final Long start_date; // unix-time seconds UTC
|
||||
final Long end_date; // unix-time seconds UTC
|
||||
final Long time_created; // unix-time seconds UTC
|
||||
final Collection<CourseQuiz> quizzes = new ArrayList<>();
|
||||
|
||||
@JsonCreator
|
||||
protected CourseData(
|
||||
@JsonProperty(value = "id") final String id,
|
||||
@JsonProperty(value = "shortname") final String short_name,
|
||||
@JsonProperty(value = "idnumber") final String idnumber,
|
||||
@JsonProperty(value = "fullname") final String full_name,
|
||||
@JsonProperty(value = "displayname") final String display_name,
|
||||
@JsonProperty(value = "summary") final String summary,
|
||||
@JsonProperty(value = "startdate") final Long start_date,
|
||||
@JsonProperty(value = "enddate") final Long end_date,
|
||||
@JsonProperty(value = "timecreated") final Long time_created) {
|
||||
|
||||
this.id = id;
|
||||
this.short_name = short_name;
|
||||
this.idnumber = idnumber;
|
||||
this.full_name = full_name;
|
||||
this.display_name = display_name;
|
||||
this.summary = summary;
|
||||
this.start_date = start_date;
|
||||
this.end_date = end_date;
|
||||
this.time_created = time_created;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static final class Courses {
|
||||
final Collection<CourseData> courses;
|
||||
final Collection<Warning> warnings;
|
||||
|
||||
@JsonCreator
|
||||
protected Courses(
|
||||
@JsonProperty(value = "courses") final Collection<CourseData> courses,
|
||||
@JsonProperty(value = "warnings") final Collection<Warning> warnings) {
|
||||
this.courses = courses;
|
||||
this.warnings = warnings;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static final class CourseQuizData {
|
||||
final Collection<CourseQuiz> quizzes;
|
||||
final Collection<Warning> warnings;
|
||||
|
||||
@JsonCreator
|
||||
protected CourseQuizData(
|
||||
@JsonProperty(value = "quizzes") final Collection<CourseQuiz> quizzes,
|
||||
@JsonProperty(value = "warnings") final Collection<Warning> warnings) {
|
||||
this.quizzes = quizzes;
|
||||
this.warnings = warnings;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
static final class CourseQuiz {
|
||||
final String id;
|
||||
final String course;
|
||||
final String course_module;
|
||||
final String name;
|
||||
final String intro; // HTML
|
||||
final Long time_open; // unix-time seconds UTC
|
||||
final Long time_close; // unix-time seconds UTC
|
||||
final Long time_limit; // unix-time seconds UTC
|
||||
|
||||
@JsonCreator
|
||||
protected CourseQuiz(
|
||||
@JsonProperty(value = "id") final String id,
|
||||
@JsonProperty(value = "course") final String course,
|
||||
@JsonProperty(value = "coursemodule") final String course_module,
|
||||
@JsonProperty(value = "name") final String name,
|
||||
@JsonProperty(value = "intro") final String intro,
|
||||
@JsonProperty(value = "timeopen") final Long time_open,
|
||||
@JsonProperty(value = "timeclose") final Long time_close,
|
||||
@JsonProperty(value = "timelimit") final Long time_limit) {
|
||||
|
||||
this.id = id;
|
||||
this.course = course;
|
||||
this.course_module = course_module;
|
||||
this.name = name;
|
||||
this.intro = intro;
|
||||
this.time_open = time_open;
|
||||
this.time_close = time_close;
|
||||
this.time_limit = time_limit;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
static final class MoodleUserDetails {
|
||||
final String id;
|
||||
final String username;
|
||||
final String firstname;
|
||||
final String lastname;
|
||||
final String fullname;
|
||||
final String email;
|
||||
final String department;
|
||||
final Long firstaccess;
|
||||
final Long lastaccess;
|
||||
final String auth;
|
||||
final Boolean suspended;
|
||||
final Boolean confirmed;
|
||||
final String lang;
|
||||
final String theme;
|
||||
final String timezone;
|
||||
final String description;
|
||||
final Integer mailformat;
|
||||
final Integer descriptionformat;
|
||||
|
||||
@JsonCreator
|
||||
protected MoodleUserDetails(
|
||||
@JsonProperty(value = "id") final String id,
|
||||
@JsonProperty(value = "username") final String username,
|
||||
@JsonProperty(value = "firstname") final String firstname,
|
||||
@JsonProperty(value = "lastname") final String lastname,
|
||||
@JsonProperty(value = "fullname") final String fullname,
|
||||
@JsonProperty(value = "email") final String email,
|
||||
@JsonProperty(value = "department") final String department,
|
||||
@JsonProperty(value = "firstaccess") final Long firstaccess,
|
||||
@JsonProperty(value = "lastaccess") final Long lastaccess,
|
||||
@JsonProperty(value = "auth") final String auth,
|
||||
@JsonProperty(value = "suspended") final Boolean suspended,
|
||||
@JsonProperty(value = "confirmed") final Boolean confirmed,
|
||||
@JsonProperty(value = "lang") final String lang,
|
||||
@JsonProperty(value = "theme") final String theme,
|
||||
@JsonProperty(value = "timezone") final String timezone,
|
||||
@JsonProperty(value = "description") final String description,
|
||||
@JsonProperty(value = "mailformat") final Integer mailformat,
|
||||
@JsonProperty(value = "descriptionformat") final Integer descriptionformat) {
|
||||
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.firstname = firstname;
|
||||
this.lastname = lastname;
|
||||
this.fullname = fullname;
|
||||
this.email = email;
|
||||
this.department = department;
|
||||
this.firstaccess = firstaccess;
|
||||
this.lastaccess = lastaccess;
|
||||
this.auth = auth;
|
||||
this.suspended = suspended;
|
||||
this.confirmed = confirmed;
|
||||
this.lang = lang;
|
||||
this.theme = theme;
|
||||
this.timezone = timezone;
|
||||
this.description = description;
|
||||
this.mailformat = mailformat;
|
||||
this.descriptionformat = descriptionformat;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
static final class CoursePage {
|
||||
final Collection<CourseKey> courseKeys;
|
||||
final Collection<Warning> warnings;
|
||||
|
||||
public CoursePage(
|
||||
@JsonProperty(value = "courses") final Collection<CourseKey> courseKeys,
|
||||
@JsonProperty(value = "warnings") final Collection<Warning> warnings) {
|
||||
|
||||
this.courseKeys = courseKeys;
|
||||
this.warnings = warnings;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
static final class CourseKey {
|
||||
final String id;
|
||||
final String short_name;
|
||||
final String category_name;
|
||||
final String sort_order;
|
||||
|
||||
@JsonCreator
|
||||
protected CourseKey(
|
||||
@JsonProperty(value = "id") final String id,
|
||||
@JsonProperty(value = "shortname") final String short_name,
|
||||
@JsonProperty(value = "categoryname") final String category_name,
|
||||
@JsonProperty(value = "sortorder") final String sort_order) {
|
||||
|
||||
this.id = id;
|
||||
this.short_name = short_name;
|
||||
this.category_name = category_name;
|
||||
this.sort_order = sort_order;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
builder.append("CourseKey [id=");
|
||||
builder.append(this.id);
|
||||
builder.append(", short_name=");
|
||||
builder.append(this.short_name);
|
||||
builder.append(", category_name=");
|
||||
builder.append(this.category_name);
|
||||
builder.append(", sort_order=");
|
||||
builder.append(this.sort_order);
|
||||
builder.append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
|||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate.Warning;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseAccess.CoursePage;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CoursePage;
|
||||
|
||||
@Lazy
|
||||
@Component
|
||||
|
|
|
@ -8,408 +8,34 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
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.MoodleAPIRestTemplate;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
|
||||
|
||||
/** GET:
|
||||
* http://yourmoodle.org/webservice/rest/server.php?wstoken={token}&moodlewsrestformat=json&wsfunction=seb_restriction&courseId=123
|
||||
*
|
||||
* Response (JSON):
|
||||
*
|
||||
* <pre>
|
||||
* {
|
||||
* "quizId": "456",
|
||||
* "configKeys": [
|
||||
* "key1",
|
||||
* "key2",
|
||||
* "key3"
|
||||
* ],
|
||||
* "browserKeys": [
|
||||
* "bkey1",
|
||||
* "bkey2",
|
||||
* "bkey3"
|
||||
* ]
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* Set keys:
|
||||
* POST:
|
||||
* http://yourmoodle.org/webservice/rest/server.php?wstoken={token}&moodlewsrestformat=json&wsfunction=seb_restriction_update&courseId=123&configKey[0]=key1&configKey[1]=key2&browserKey[0]=bkey1&browserKey[1]=bkey2
|
||||
*
|
||||
* Delete all key (and remove restrictions):
|
||||
* POST:
|
||||
* http://yourmoodle.org/webservice/rest/server.php?wstoken={token}&moodlewsrestformat=json&wsfunction=seb_restriction_delete&courseId=123 */
|
||||
/** Dummy Implementation */
|
||||
public class MoodleCourseRestriction implements SEBRestrictionAPI {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MoodleCourseRestriction.class);
|
||||
|
||||
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION = "seb_restriction";
|
||||
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_CREATE = "seb_restriction_create";
|
||||
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_UPDATE = "seb_restriction_update";
|
||||
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_DELETE = "seb_restriction_delete";
|
||||
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_SHORT_NAME = "shortname";
|
||||
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_ID_NUMBER = "idnumber";
|
||||
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_QUIZ_ID = "quizId";
|
||||
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_CONFIG_KEY = "configKey";
|
||||
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_BROWSER_KEY = "browserKey";
|
||||
|
||||
private final JSONMapper jsonMapper;
|
||||
private final MoodleRestTemplateFactory moodleRestTemplateFactory;
|
||||
|
||||
private MoodleAPIRestTemplate restTemplate;
|
||||
|
||||
public MoodleCourseRestriction(
|
||||
final JSONMapper jsonMapper,
|
||||
final MoodleRestTemplateFactory moodleRestTemplateFactory) {
|
||||
|
||||
this.jsonMapper = jsonMapper;
|
||||
this.moodleRestTemplateFactory = moodleRestTemplateFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LmsSetupTestResult testCourseRestrictionAPI() {
|
||||
// try to call the SEB Restrictions API
|
||||
try {
|
||||
|
||||
final MoodleAPIRestTemplate template = getRestTemplate()
|
||||
.getOrThrow();
|
||||
|
||||
final String jsonResponse = template.callMoodleAPIFunction(
|
||||
MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION,
|
||||
new LinkedMultiValueMap<>(),
|
||||
null);
|
||||
|
||||
final Error checkError = this.checkError(jsonResponse);
|
||||
if (checkError != null) {
|
||||
return LmsSetupTestResult.ofQuizRestrictionAPIError(LmsType.MOODLE, checkError.exception);
|
||||
}
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.debug("Moodle SEB restriction API not available: ", e);
|
||||
return LmsSetupTestResult.ofQuizRestrictionAPIError(LmsType.MOODLE, e.getMessage());
|
||||
}
|
||||
return LmsSetupTestResult.ofOkay(LmsType.MOODLE);
|
||||
return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.MOODLE, "SEB restriction not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
|
||||
return Result.tryCatch(() -> {
|
||||
return getSEBRestriction(
|
||||
MoodleCourseAccess.getQuizId(exam.externalId),
|
||||
MoodleCourseAccess.getShortname(exam.externalId),
|
||||
MoodleCourseAccess.getIdnumber(exam.externalId))
|
||||
.map(restriction -> SEBRestriction.from(exam.id, restriction))
|
||||
.getOrThrow();
|
||||
});
|
||||
return Result.ofError(new UnsupportedOperationException("SEB restriction not supported"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<SEBRestriction> applySEBClientRestriction(
|
||||
final Exam exam,
|
||||
final SEBRestriction sebRestrictionData) {
|
||||
|
||||
return this.updateSEBRestriction(
|
||||
exam.externalId,
|
||||
MoodleSEBRestriction.from(sebRestrictionData))
|
||||
.map(result -> sebRestrictionData);
|
||||
public Result<SEBRestriction> applySEBClientRestriction(final Exam exam, final SEBRestriction sebRestrictionData) {
|
||||
return Result.ofError(new UnsupportedOperationException("SEB restriction not supported"));
|
||||
}
|
||||
|
||||
@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,
|
||||
final String idnumber) {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("GET SEB Client restriction on course: {} quiz: {}", shortname, quizId);
|
||||
}
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
final MoodleAPIRestTemplate template = getRestTemplate()
|
||||
.getOrThrow();
|
||||
|
||||
final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_QUIZ_ID, quizId);
|
||||
if (StringUtils.isNotBlank(shortname)) {
|
||||
queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_SHORT_NAME, shortname);
|
||||
}
|
||||
if (StringUtils.isNotBlank(idnumber)) {
|
||||
queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_ID_NUMBER, idnumber);
|
||||
}
|
||||
|
||||
final String resultJSON = template.callMoodleAPIFunction(
|
||||
MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION,
|
||||
queryParams,
|
||||
null);
|
||||
|
||||
final Error error = this.checkError(resultJSON);
|
||||
if (error != null) {
|
||||
log.error("Failed to get SEB restriction: {}", error.toString());
|
||||
throw new NoSEBRestrictionException("Failed to get SEB restriction: " + error.exception);
|
||||
}
|
||||
|
||||
final MoodleSEBRestriction restrictiondata = this.jsonMapper.readValue(
|
||||
resultJSON,
|
||||
new TypeReference<MoodleSEBRestriction>() {
|
||||
});
|
||||
|
||||
return restrictiondata;
|
||||
});
|
||||
}
|
||||
|
||||
Result<MoodleSEBRestriction> createSEBRestriction(
|
||||
final String internalId,
|
||||
final MoodleSEBRestriction restriction) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
return createSEBRestriction(
|
||||
MoodleCourseAccess.getQuizId(internalId),
|
||||
MoodleCourseAccess.getShortname(internalId),
|
||||
MoodleCourseAccess.getIdnumber(internalId),
|
||||
restriction)
|
||||
.getOrThrow();
|
||||
});
|
||||
}
|
||||
|
||||
Result<MoodleSEBRestriction> createSEBRestriction(
|
||||
final String quizId,
|
||||
final String shortname,
|
||||
final String idnumber,
|
||||
final MoodleSEBRestriction restriction) {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("POST SEB Client restriction on course: {} quiz: restriction : {}",
|
||||
shortname,
|
||||
quizId,
|
||||
restriction);
|
||||
}
|
||||
|
||||
return postSEBRestriction(
|
||||
quizId,
|
||||
shortname,
|
||||
idnumber,
|
||||
MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_CREATE,
|
||||
restriction);
|
||||
}
|
||||
|
||||
Result<MoodleSEBRestriction> updateSEBRestriction(
|
||||
final String internalId,
|
||||
final MoodleSEBRestriction restriction) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
return updateSEBRestriction(
|
||||
MoodleCourseAccess.getQuizId(internalId),
|
||||
MoodleCourseAccess.getShortname(internalId),
|
||||
MoodleCourseAccess.getIdnumber(internalId),
|
||||
restriction)
|
||||
.getOrThrow();
|
||||
});
|
||||
}
|
||||
|
||||
Result<MoodleSEBRestriction> updateSEBRestriction(
|
||||
final String quizId,
|
||||
final String shortname,
|
||||
final String idnumber,
|
||||
final MoodleSEBRestriction restriction) {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("POST SEB Client restriction on course: {} quiz: restriction : {}",
|
||||
shortname,
|
||||
quizId,
|
||||
restriction);
|
||||
}
|
||||
|
||||
return postSEBRestriction(
|
||||
quizId,
|
||||
shortname,
|
||||
idnumber,
|
||||
MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_UPDATE,
|
||||
restriction);
|
||||
}
|
||||
|
||||
Result<Boolean> deleteSEBRestriction(
|
||||
final String internalId) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
return deleteSEBRestriction(
|
||||
MoodleCourseAccess.getQuizId(internalId),
|
||||
MoodleCourseAccess.getShortname(internalId),
|
||||
MoodleCourseAccess.getIdnumber(internalId))
|
||||
.getOrThrow();
|
||||
});
|
||||
}
|
||||
|
||||
Result<Boolean> deleteSEBRestriction(
|
||||
final String quizId,
|
||||
final String shortname,
|
||||
final String idnumber) {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("DELETE SEB Client restriction on course: {} quizId {}", shortname, quizId);
|
||||
}
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
final MoodleAPIRestTemplate template = getRestTemplate()
|
||||
.getOrThrow();
|
||||
|
||||
final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_QUIZ_ID, quizId);
|
||||
if (StringUtils.isNotBlank(shortname)) {
|
||||
queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_SHORT_NAME, shortname);
|
||||
}
|
||||
if (StringUtils.isNotBlank(idnumber)) {
|
||||
queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_ID_NUMBER, idnumber);
|
||||
}
|
||||
|
||||
final String jsonResponse = template.callMoodleAPIFunction(
|
||||
MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_DELETE,
|
||||
queryParams,
|
||||
null);
|
||||
|
||||
final Error error = this.checkError(jsonResponse);
|
||||
if (error != null) {
|
||||
log.error("Failed to delete SEB restriction: {}", error.toString());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private Result<MoodleAPIRestTemplate> getRestTemplate() {
|
||||
if (this.restTemplate == null) {
|
||||
final Result<MoodleAPIRestTemplate> templateRequest = this.moodleRestTemplateFactory
|
||||
.createRestTemplate();
|
||||
if (templateRequest.hasError()) {
|
||||
return templateRequest;
|
||||
} else {
|
||||
this.restTemplate = templateRequest.get();
|
||||
}
|
||||
}
|
||||
|
||||
return Result.of(this.restTemplate);
|
||||
}
|
||||
|
||||
private Result<MoodleSEBRestriction> postSEBRestriction(
|
||||
final String quizId,
|
||||
final String shortname,
|
||||
final String idnumber,
|
||||
final String function,
|
||||
final MoodleSEBRestriction restriction) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
final MoodleAPIRestTemplate template = getRestTemplate()
|
||||
.getOrThrow();
|
||||
|
||||
final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_QUIZ_ID, quizId);
|
||||
if (StringUtils.isNotBlank(shortname)) {
|
||||
queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_SHORT_NAME, shortname);
|
||||
}
|
||||
if (StringUtils.isNotBlank(idnumber)) {
|
||||
queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_ID_NUMBER, idnumber);
|
||||
}
|
||||
|
||||
final MultiValueMap<String, String> queryAttributes = new LinkedMultiValueMap<>();
|
||||
queryAttributes.addAll(
|
||||
MOODLE_DEFAULT_COURSE_RESTRICTION_CONFIG_KEY,
|
||||
new ArrayList<>(restriction.configKeys));
|
||||
queryAttributes.addAll(
|
||||
MOODLE_DEFAULT_COURSE_RESTRICTION_BROWSER_KEY,
|
||||
new ArrayList<>(restriction.browserExamKeys));
|
||||
|
||||
final String resultJSON = template.callMoodleAPIFunction(
|
||||
function,
|
||||
queryParams,
|
||||
queryAttributes);
|
||||
|
||||
final Error error = this.checkError(resultJSON);
|
||||
if (error != null) {
|
||||
log.error("Failed to post SEB restriction: {}", error.toString());
|
||||
throw new NoSEBRestrictionException("Failed to post SEB restriction: " + error.exception);
|
||||
}
|
||||
|
||||
final MoodleSEBRestriction restrictiondata = this.jsonMapper.readValue(
|
||||
resultJSON,
|
||||
new TypeReference<MoodleSEBRestriction>() {
|
||||
});
|
||||
|
||||
return restrictiondata;
|
||||
});
|
||||
}
|
||||
|
||||
public Error checkError(final String jsonResponse) {
|
||||
if (jsonResponse.contains("exception") || jsonResponse.contains("errorcode")) {
|
||||
try {
|
||||
return this.jsonMapper.readValue(
|
||||
jsonResponse,
|
||||
new TypeReference<Error>() {
|
||||
});
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to parse error response: {} cause: ", jsonResponse, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static class Error {
|
||||
public final String exception;
|
||||
public final String errorcode;
|
||||
public final String message;
|
||||
|
||||
@JsonCreator
|
||||
Error(
|
||||
@JsonProperty(value = "exception") final String exception,
|
||||
@JsonProperty(value = "errorcode") final String errorcode,
|
||||
@JsonProperty(value = "message") final String message) {
|
||||
this.exception = exception;
|
||||
this.errorcode = errorcode;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
builder.append("Error [exception=");
|
||||
builder.append(this.exception);
|
||||
builder.append(", errorcode=");
|
||||
builder.append(this.errorcode);
|
||||
builder.append(", message=");
|
||||
builder.append(this.message);
|
||||
builder.append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
return Result.ofError(new UnsupportedOperationException("SEB restriction not supported"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -25,12 +25,14 @@ 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.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
|
||||
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;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodlePluginCheck;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCheck;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactoryImpl;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseAccess;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseRestriction;
|
||||
|
||||
|
@ -46,6 +48,7 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
|||
private final Environment environment;
|
||||
private final ClientCredentialService clientCredentialService;
|
||||
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
|
||||
private final ExamConfigurationValueService examConfigurationValueService;
|
||||
private final ApplicationContext applicationContext;
|
||||
private final String[] alternativeTokenRequestPaths;
|
||||
|
||||
|
@ -56,6 +59,7 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
|||
final AsyncService asyncService,
|
||||
final Environment environment,
|
||||
final ClientCredentialService clientCredentialService,
|
||||
final ExamConfigurationValueService examConfigurationValueService,
|
||||
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
|
||||
final ApplicationContext applicationContext,
|
||||
@Value("${sebserver.webservice.lms.moodle.api.token.request.paths:}") final String alternativeTokenRequestPaths) {
|
||||
|
@ -66,6 +70,7 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
|||
this.asyncService = asyncService;
|
||||
this.environment = environment;
|
||||
this.clientCredentialService = clientCredentialService;
|
||||
this.examConfigurationValueService = examConfigurationValueService;
|
||||
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
|
||||
this.applicationContext = applicationContext;
|
||||
this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
|
||||
|
@ -88,20 +93,26 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
|||
.getBean(MoodleCourseDataAsyncLoader.class);
|
||||
asyncLoaderPrototype.init(lmsSetup.getModelId());
|
||||
|
||||
final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory(
|
||||
final MoodleRestTemplateFactory restTemplateFactory = new MoodleRestTemplateFactoryImpl(
|
||||
this.jsonMapper,
|
||||
apiTemplateDataSupplier,
|
||||
this.clientCredentialService,
|
||||
this.clientHttpRequestFactoryService,
|
||||
this.alternativeTokenRequestPaths);
|
||||
|
||||
if (this.moodlePluginCheck.checkPluginAvailable(lmsSetup)) {
|
||||
if (this.moodlePluginCheck.checkPluginAvailable(restTemplateFactory)) {
|
||||
|
||||
final MoodlePluginCourseAccess moodlePluginCourseAccess = new MoodlePluginCourseAccess(
|
||||
this.jsonMapper,
|
||||
moodleRestTemplateFactory,
|
||||
this.cacheManager);
|
||||
final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction();
|
||||
this.asyncService,
|
||||
restTemplateFactory,
|
||||
this.cacheManager,
|
||||
this.environment);
|
||||
|
||||
final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction(
|
||||
this.jsonMapper,
|
||||
restTemplateFactory,
|
||||
this.examConfigurationValueService);
|
||||
|
||||
return new LmsAPITemplateAdapter(
|
||||
this.asyncService,
|
||||
|
@ -115,20 +126,16 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
|||
final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess(
|
||||
this.jsonMapper,
|
||||
this.asyncService,
|
||||
moodleRestTemplateFactory,
|
||||
restTemplateFactory,
|
||||
asyncLoaderPrototype,
|
||||
this.environment);
|
||||
|
||||
final MoodleCourseRestriction moodleCourseRestriction = new MoodleCourseRestriction(
|
||||
this.jsonMapper,
|
||||
moodleRestTemplateFactory);
|
||||
|
||||
return new LmsAPITemplateAdapter(
|
||||
this.asyncService,
|
||||
this.environment,
|
||||
apiTemplateDataSupplier,
|
||||
moodleCourseAccess,
|
||||
moodleCourseRestriction);
|
||||
new MoodleCourseRestriction());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* 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.moodle.plugin;
|
||||
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
|
||||
@Lazy
|
||||
@Service
|
||||
@WebServiceProfile
|
||||
public class MoodlePluginCheck {
|
||||
|
||||
/** Used to check if the moodle SEB Server plugin is available for a given LMSSetup.
|
||||
*
|
||||
* @param lmsSetup The LMS Setup
|
||||
* @return true if the SEB Server plugin is available */
|
||||
public boolean checkPluginAvailable(final LmsSetup lmsSetup) {
|
||||
// TODO check if the moodle plugin is installed for the specified LMS Setup
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -8,64 +8,127 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.joda.time.DateTime;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonParseException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
|
||||
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.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.gbl.util.Utils;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCourseAccess;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate.Warning;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CourseData;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CourseQuizData;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.Courses;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.MoodlePluginUserDetails;
|
||||
|
||||
public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess implements CourseAccessAPI {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MoodlePluginCourseAccess.class);
|
||||
|
||||
static final String COURSES_API_FUNCTION_NAME = "local_sebserver_get_courses";
|
||||
static final String QUIZZES_BY_COURSES_API_FUNCTION_NAME = "local_sebserver_get_quizzes_by_courses";
|
||||
static final String USERS_API_FUNCTION_NAME = "local_sebserver_get_users";
|
||||
public static final String MOODLE_QUIZ_START_URL_PATH = "mod/quiz/view.php?id=";
|
||||
public static final String COURSES_API_FUNCTION_NAME = "quizaccess_sebserver_get_courses";
|
||||
public static final String QUIZZES_BY_COURSES_API_FUNCTION_NAME = "quizaccess_sebserver_get_quizzes_by_courses";
|
||||
public static final String USERS_API_FUNCTION_NAME = "quizaccess_sebserver_get_users";
|
||||
|
||||
static final String CRITERIA_FROM_DATE = "from_date";
|
||||
static final String CRITERIA_TO_DATE = "to_date";
|
||||
static final String CRITERIA_LIMIT_FROM = "limitfrom";
|
||||
static final String CRITERIA_LIMIT_NUM = "limitnum";
|
||||
public static final String ATTR_FIELD = "field";
|
||||
public static final String CRITERIA_COURSE_IDS = "ids";
|
||||
public static final String CRITERIA_FROM_DATE = "from_date";
|
||||
public static final String CRITERIA_TO_DATE = "to_date";
|
||||
public static final String CRITERIA_LIMIT_FROM = "limitfrom";
|
||||
public static final String CRITERIA_LIMIT_NUM = "limitnum";
|
||||
|
||||
private final JSONMapper jsonMapper;
|
||||
private final MoodleRestTemplateFactory moodleRestTemplateFactory;
|
||||
private final MoodleRestTemplateFactory restTemplateFactory;
|
||||
private final CircuitBreaker<String> protectedMoodlePageCall;
|
||||
private final boolean prependShortCourseName;
|
||||
private final int pageSize;
|
||||
private final int maxSize;
|
||||
|
||||
private MoodleAPIRestTemplate restTemplate;
|
||||
|
||||
public MoodlePluginCourseAccess(
|
||||
final JSONMapper jsonMapper,
|
||||
final MoodleRestTemplateFactory moodleRestTemplateFactory,
|
||||
final CacheManager cacheManager) {
|
||||
final AsyncService asyncService,
|
||||
final MoodleRestTemplateFactory restTemplateFactory,
|
||||
final CacheManager cacheManager,
|
||||
final Environment environment) {
|
||||
|
||||
super(cacheManager);
|
||||
this.jsonMapper = jsonMapper;
|
||||
this.moodleRestTemplateFactory = moodleRestTemplateFactory;
|
||||
this.restTemplateFactory = restTemplateFactory;
|
||||
|
||||
this.prependShortCourseName = BooleanUtils.toBoolean(environment.getProperty(
|
||||
"sebserver.webservice.lms.moodle.prependShortCourseName",
|
||||
Constants.TRUE_STRING));
|
||||
|
||||
this.protectedMoodlePageCall = asyncService.createCircuitBreaker(
|
||||
environment.getProperty(
|
||||
"sebserver.webservice.circuitbreaker.moodleRestCall.attempts",
|
||||
Integer.class,
|
||||
2),
|
||||
environment.getProperty(
|
||||
"sebserver.webservice.circuitbreaker.moodleRestCall.blockingTime",
|
||||
Long.class,
|
||||
Constants.SECOND_IN_MILLIS * 20),
|
||||
environment.getProperty(
|
||||
"sebserver.webservice.circuitbreaker.moodleRestCall.timeToRecover",
|
||||
Long.class,
|
||||
Constants.MINUTE_IN_MILLIS));
|
||||
this.maxSize =
|
||||
environment.getProperty("sebserver.webservice.cache.moodle.course.maxSize", Integer.class, 10000);
|
||||
this.pageSize =
|
||||
environment.getProperty("sebserver.webservice.cache.moodle.course.pageSize", Integer.class, 10);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Long getLmsSetupId() {
|
||||
return this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup().id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LmsSetupTestResult testCourseAccessAPI() {
|
||||
final LmsSetupTestResult attributesCheck = this.moodleRestTemplateFactory.test();
|
||||
final LmsSetupTestResult attributesCheck = this.restTemplateFactory.test();
|
||||
if (!attributesCheck.isOk()) {
|
||||
return attributesCheck;
|
||||
}
|
||||
|
@ -73,78 +136,417 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
|
|||
final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate();
|
||||
if (restTemplateRequest.hasError()) {
|
||||
final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " +
|
||||
this.moodleRestTemplateFactory.knownTokenAccessPaths;
|
||||
this.restTemplateFactory.getKnownTokenAccessPaths();
|
||||
log.error(message + " cause: {}", restTemplateRequest.getError().getMessage());
|
||||
return LmsSetupTestResult.ofTokenRequestError(LmsType.MOODLE_PLUGIN, message);
|
||||
}
|
||||
|
||||
final MoodleAPIRestTemplate restTemplate = restTemplateRequest.get();
|
||||
|
||||
// try {
|
||||
// restTemplate.testAPIConnection(
|
||||
// COURSES_API_FUNCTION_NAME,
|
||||
// QUIZZES_BY_COURSES_API_FUNCTION_NAME,
|
||||
// USERS_API_FUNCTION_NAME);
|
||||
// } catch (final RuntimeException e) {
|
||||
// log.error("Failed to access Moodle course API: ", e);
|
||||
// return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.MOODLE_PLUGIN, e.getMessage());
|
||||
// }
|
||||
try {
|
||||
|
||||
restTemplate.testAPIConnection(
|
||||
COURSES_API_FUNCTION_NAME,
|
||||
QUIZZES_BY_COURSES_API_FUNCTION_NAME,
|
||||
USERS_API_FUNCTION_NAME);
|
||||
|
||||
} catch (final RuntimeException e) {
|
||||
log.error("Failed to access Moodle course API: ", e);
|
||||
return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.MOODLE_PLUGIN, e.getMessage());
|
||||
}
|
||||
|
||||
return LmsSetupTestResult.ofOkay(LmsType.MOODLE_PLUGIN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
|
||||
System.out.println("***************** filterMap: " + filterMap);
|
||||
// TODO Auto-generated method stub
|
||||
return Result.of(Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
return Result.ofError(new UnsupportedOperationException());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fetchQuizzes(final FilterMap filterMap, final AsyncQuizFetchBuffer asyncQuizFetchBuffer) {
|
||||
// TODO Auto-generated method stub
|
||||
try {
|
||||
|
||||
int page = 0;
|
||||
int failedAttempts = 0;
|
||||
final DateTime quizFromTime = filterMap.getQuizFromTime();
|
||||
final Predicate<QuizData> quizFilter = LmsAPIService.quizFilterPredicate(filterMap);
|
||||
|
||||
while (!asyncQuizFetchBuffer.finished && !asyncQuizFetchBuffer.canceled) {
|
||||
try {
|
||||
fetchQuizzesPage(page, quizFromTime, asyncQuizFetchBuffer, quizFilter);
|
||||
page++;
|
||||
} catch (final Exception e) {
|
||||
log.error("Unexpected error while trying to fetch moodle quiz page: {}", page, e);
|
||||
failedAttempts++;
|
||||
if (failedAttempts > 3) {
|
||||
asyncQuizFetchBuffer.finish(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (final Exception e) {
|
||||
asyncQuizFetchBuffer.finish(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
final Set<String> missingIds = new HashSet<>(ids);
|
||||
final Collection<QuizData> result = new ArrayList<>();
|
||||
final Set<String> fromCache = ids.stream()
|
||||
.map(super::getFromCache).filter(Objects::nonNull)
|
||||
.map(qd -> {
|
||||
result.add(qd);
|
||||
return qd.id;
|
||||
}).collect(Collectors.toSet());
|
||||
missingIds.removeAll(fromCache);
|
||||
|
||||
if (!missingIds.isEmpty()) {
|
||||
|
||||
result.addAll(getRestTemplate()
|
||||
.map(template -> getQuizzesForIds(template, ids))
|
||||
.map(super::putToCache)
|
||||
.onError(error -> log.error("Failed to get courses for: {}", ids, error))
|
||||
.getOrElse(() -> Collections.emptyList()));
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<QuizData> getQuiz(final String id) {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
final QuizData fromCache = super.getFromCache(id);
|
||||
if (fromCache != null) {
|
||||
return fromCache;
|
||||
}
|
||||
|
||||
final Set<String> ids = Stream.of(id).collect(Collectors.toSet());
|
||||
final Iterator<QuizData> iterator = getRestTemplate()
|
||||
.map(template -> getQuizzesForIds(template, ids))
|
||||
.map(super::putToCache)
|
||||
.getOr(Collections.emptyList())
|
||||
.iterator();
|
||||
|
||||
if (!iterator.hasNext()) {
|
||||
throw new RuntimeException("Moodle Quiz for id " + id + " not found");
|
||||
}
|
||||
|
||||
return iterator.next();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
final MoodleAPIRestTemplate template = getRestTemplate()
|
||||
.getOrThrow();
|
||||
|
||||
final MultiValueMap<String, String> queryAttributes = new LinkedMultiValueMap<>();
|
||||
queryAttributes.add(ATTR_FIELD, "id");
|
||||
queryAttributes.add("values[0]", examineeUserId);
|
||||
|
||||
final String userDetailsJSON = template.callMoodleAPIFunction(
|
||||
USERS_API_FUNCTION_NAME,
|
||||
queryAttributes);
|
||||
|
||||
if (MoodleUtils.checkAccessDeniedError(userDetailsJSON)) {
|
||||
final LmsSetup lmsSetup = this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup();
|
||||
log.error("Get access denied error from Moodle: {} for API call: {}, response: {}",
|
||||
lmsSetup,
|
||||
USERS_API_FUNCTION_NAME,
|
||||
Utils.truncateText(userDetailsJSON, 2000));
|
||||
throw new RuntimeException("No user details on Moodle API request (access-denied)");
|
||||
}
|
||||
|
||||
final MoodlePluginUserDetails[] userDetails = this.jsonMapper.<MoodlePluginUserDetails[]> readValue(
|
||||
userDetailsJSON,
|
||||
new TypeReference<MoodlePluginUserDetails[]>() {
|
||||
});
|
||||
|
||||
if (userDetails == null || userDetails.length <= 0) {
|
||||
throw new RuntimeException("No user details on Moodle API request");
|
||||
}
|
||||
|
||||
return new ExamineeAccountDetails(
|
||||
userDetails[0].id,
|
||||
userDetails[0].fullname,
|
||||
userDetails[0].username,
|
||||
userDetails[0].email,
|
||||
userDetails[0].customfields);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExamineeName(final String examineeUserId) {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
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) {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
return Result.of(new Chapters(Collections.emptyList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Long getLmsSetupId() {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
private String getLmsSetupName() {
|
||||
return this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup().name;
|
||||
}
|
||||
|
||||
private void fetchQuizzesPage(
|
||||
final int page,
|
||||
final DateTime quizFromTime,
|
||||
final AsyncQuizFetchBuffer asyncQuizFetchBuffer,
|
||||
final Predicate<QuizData> quizFilter) throws JsonParseException, JsonMappingException, IOException {
|
||||
|
||||
final MoodleAPIRestTemplate restTemplate = getRestTemplate().getOrThrow();
|
||||
|
||||
final LmsSetup lmsSetup = this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup();
|
||||
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
|
||||
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
|
||||
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
|
||||
|
||||
// first get courses page from moodle
|
||||
final Map<String, CourseData> courseData = new HashMap<>();
|
||||
final Collection<CourseData> coursesPage = getCoursesPage(restTemplate, quizFromTime, page, this.pageSize);
|
||||
|
||||
// no courses for page --> finish
|
||||
if (coursesPage == null || coursesPage.isEmpty()) {
|
||||
asyncQuizFetchBuffer.finish();
|
||||
return;
|
||||
}
|
||||
|
||||
courseData.putAll(coursesPage
|
||||
.stream()
|
||||
.collect(Collectors.toMap(
|
||||
cd -> cd.id,
|
||||
Function.identity())));
|
||||
|
||||
// then get all quizzes of courses and filter
|
||||
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
|
||||
final List<String> courseIds = new ArrayList<>(courseData.keySet());
|
||||
attributes.put(CRITERIA_COURSE_IDS, courseIds);
|
||||
|
||||
final String quizzesJSON = this.protectedMoodlePageCall
|
||||
.protectedRun(() -> restTemplate.callMoodleAPIFunction(
|
||||
QUIZZES_BY_COURSES_API_FUNCTION_NAME,
|
||||
attributes))
|
||||
.getOrThrow();
|
||||
|
||||
final CourseQuizData courseQuizData = this.jsonMapper.readValue(
|
||||
quizzesJSON,
|
||||
CourseQuizData.class);
|
||||
|
||||
if (courseQuizData == null) {
|
||||
return; // SEBSERV-361
|
||||
}
|
||||
|
||||
if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) {
|
||||
MoodleUtils.logMoodleWarning(
|
||||
courseQuizData.warnings,
|
||||
lmsSetup.name,
|
||||
QUIZZES_BY_COURSES_API_FUNCTION_NAME);
|
||||
}
|
||||
|
||||
if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) {
|
||||
return; // no quizzes on this page
|
||||
}
|
||||
|
||||
courseQuizData.quizzes
|
||||
.stream()
|
||||
.filter(MoodleUtils.getQuizFilter())
|
||||
.forEach(quiz -> {
|
||||
final CourseData data = courseData.get(quiz.course);
|
||||
if (data != null) {
|
||||
data.quizzes.add(quiz);
|
||||
}
|
||||
});
|
||||
|
||||
courseData.values().stream()
|
||||
.filter(c -> !c.quizzes.isEmpty())
|
||||
.forEach(c -> asyncQuizFetchBuffer.buffer.addAll(
|
||||
MoodleUtils.quizDataOf(lmsSetup, c, urlPrefix, this.prependShortCourseName)
|
||||
.stream()
|
||||
.filter(quizFilter)
|
||||
.collect(Collectors.toList())));
|
||||
|
||||
if (asyncQuizFetchBuffer.buffer.size() > this.maxSize) {
|
||||
log.warn("Maximal moodle quiz fetch size of {} reached. Cancel fetch at this point.", this.maxSize);
|
||||
asyncQuizFetchBuffer.finish();
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<CourseData> getCoursesPage(
|
||||
final MoodleAPIRestTemplate restTemplate,
|
||||
final DateTime quizFromTime,
|
||||
final int page,
|
||||
final int size) throws JsonParseException, JsonMappingException, IOException {
|
||||
|
||||
final String lmsName = getLmsSetupName();
|
||||
try {
|
||||
// get course ids per page
|
||||
final String fromDate = String.valueOf(Utils.toUnixTimeInSeconds(quizFromTime));
|
||||
final String fromElement = String.valueOf(page * size);
|
||||
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
|
||||
attributes.add(CRITERIA_FROM_DATE, fromDate);
|
||||
attributes.add(CRITERIA_LIMIT_FROM, fromElement);
|
||||
|
||||
final String courseKeyPageJSON = this.protectedMoodlePageCall
|
||||
.protectedRun(() -> restTemplate.callMoodleAPIFunction(
|
||||
COURSES_API_FUNCTION_NAME,
|
||||
attributes))
|
||||
.getOrThrow();
|
||||
|
||||
final Courses coursePage = this.jsonMapper.readValue(courseKeyPageJSON, Courses.class);
|
||||
|
||||
if (coursePage == null) {
|
||||
log.error("No CoursePage Response");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if (coursePage.warnings != null && !coursePage.warnings.isEmpty()) {
|
||||
MoodleUtils.logMoodleWarning(coursePage.warnings, lmsName, COURSES_API_FUNCTION_NAME);
|
||||
}
|
||||
|
||||
Collection<CourseData> result;
|
||||
if (coursePage.courses == null || coursePage.courses.isEmpty()) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("LMS Setup: {} No courses found on page: {}", lmsName, page);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Moodle response: {}", courseKeyPageJSON);
|
||||
}
|
||||
}
|
||||
result = Collections.emptyList();
|
||||
} else {
|
||||
result = coursePage.courses;
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("course page with {} courses", result.size());
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (final Exception e) {
|
||||
log.error("LMS Setup: {} Unexpected error while trying to get courses page: ", lmsName, e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private List<QuizData> getQuizzesForIds(
|
||||
final MoodleAPIRestTemplate restTemplate,
|
||||
final Set<String> quizIds) {
|
||||
|
||||
try {
|
||||
|
||||
final LmsSetup lmsSetup = this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup();
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Get quizzes for ids: {} and LMSSetup: {}", quizIds, lmsSetup);
|
||||
}
|
||||
|
||||
// get involved courses and map by course id
|
||||
final Map<String, CourseData> courseData = getCoursesForIds(
|
||||
restTemplate,
|
||||
quizIds.stream()
|
||||
.map(MoodleUtils::getCourseId)
|
||||
.collect(Collectors.toSet()))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(cd -> cd.id, Function.identity()));
|
||||
|
||||
// then get all quizzes of courses and filter
|
||||
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
|
||||
attributes.put(CRITERIA_COURSE_IDS, new ArrayList<>(courseData.keySet()));
|
||||
|
||||
final String quizzesJSON = restTemplate.callMoodleAPIFunction(
|
||||
QUIZZES_BY_COURSES_API_FUNCTION_NAME,
|
||||
attributes);
|
||||
|
||||
final CourseQuizData courseQuizData = this.jsonMapper.readValue(
|
||||
quizzesJSON,
|
||||
CourseQuizData.class);
|
||||
|
||||
if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) {
|
||||
MoodleUtils.logMoodleWarning(
|
||||
courseQuizData.warnings,
|
||||
lmsSetup.name,
|
||||
QUIZZES_BY_COURSES_API_FUNCTION_NAME);
|
||||
}
|
||||
|
||||
final Map<String, CourseData> finalCourseDataRef = courseData;
|
||||
courseQuizData.quizzes
|
||||
.stream()
|
||||
.forEach(quiz -> MoodleUtils.fillSelectedQuizzes(quizIds, finalCourseDataRef, quiz));
|
||||
|
||||
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
|
||||
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
|
||||
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
|
||||
|
||||
return courseData.values()
|
||||
.stream()
|
||||
.filter(c -> !c.quizzes.isEmpty())
|
||||
.flatMap(cd -> MoodleUtils.quizDataOf(
|
||||
lmsSetup,
|
||||
cd,
|
||||
urlPrefix,
|
||||
this.prependShortCourseName).stream())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Unexpected error while trying to get quizzes for ids", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<CourseData> getCoursesForIds(
|
||||
final MoodleAPIRestTemplate restTemplate,
|
||||
final Set<String> courseIds) {
|
||||
|
||||
try {
|
||||
|
||||
final LmsSetup lmsSetup = this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup();
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Get courses for ids: {} on LMS: {}", courseIds, lmsSetup);
|
||||
}
|
||||
|
||||
final String joinedIds = StringUtils.join(courseIds, Constants.COMMA);
|
||||
|
||||
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
|
||||
attributes.add(CRITERIA_COURSE_IDS, joinedIds);
|
||||
final String coursePageJSON = restTemplate.callMoodleAPIFunction(
|
||||
COURSES_API_FUNCTION_NAME,
|
||||
attributes);
|
||||
|
||||
final Courses courses = this.jsonMapper.readValue(
|
||||
coursePageJSON,
|
||||
Courses.class);
|
||||
|
||||
if (courses.courses == null || courses.courses.isEmpty()) {
|
||||
log.warn("No courses found for ids: {} on LMS: {}", courseIds, lmsSetup.name);
|
||||
|
||||
if (courses != null && courses.warnings != null && !courses.warnings.isEmpty()) {
|
||||
MoodleUtils.logMoodleWarning(courses.warnings, lmsSetup.name, COURSES_API_FUNCTION_NAME);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return courses.courses;
|
||||
} catch (final Exception e) {
|
||||
log.error("Unexpected error while trying to get courses for ids", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private Result<MoodleAPIRestTemplate> getRestTemplate() {
|
||||
if (this.restTemplate == null) {
|
||||
final Result<MoodleAPIRestTemplate> templateRequest = this.moodleRestTemplateFactory
|
||||
final Result<MoodleAPIRestTemplate> templateRequest = this.restTemplateFactory
|
||||
.createRestTemplate();
|
||||
if (templateRequest.hasError()) {
|
||||
return templateRequest;
|
||||
|
@ -156,99 +558,4 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
|
|||
return Result.of(this.restTemplate);
|
||||
}
|
||||
|
||||
// ---- Mapping Classes ---
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static final class Courses {
|
||||
final Collection<CourseData> courses;
|
||||
final Collection<Warning> warnings;
|
||||
|
||||
@JsonCreator
|
||||
protected Courses(
|
||||
@JsonProperty(value = "courses") final Collection<CourseData> courses,
|
||||
@JsonProperty(value = "warnings") final Collection<Warning> warnings) {
|
||||
this.courses = courses;
|
||||
this.warnings = warnings;
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps the Moodle course API course data */
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static final class CourseData {
|
||||
final String id;
|
||||
final String short_name;
|
||||
final String idnumber;
|
||||
final String full_name;
|
||||
final String display_name;
|
||||
final Long start_date; // unix-time seconds UTC
|
||||
final Long end_date; // unix-time seconds UTC
|
||||
final Long time_created; // unix-time seconds UTC
|
||||
final Collection<CourseQuiz> quizzes = new ArrayList<>();
|
||||
|
||||
@JsonCreator
|
||||
protected CourseData(
|
||||
@JsonProperty(value = "id") final String id,
|
||||
@JsonProperty(value = "shortname") final String short_name,
|
||||
@JsonProperty(value = "idnumber") final String idnumber,
|
||||
@JsonProperty(value = "fullname") final String full_name,
|
||||
@JsonProperty(value = "displayname") final String display_name,
|
||||
@JsonProperty(value = "startdate") final Long start_date,
|
||||
@JsonProperty(value = "enddate") final Long end_date,
|
||||
@JsonProperty(value = "timecreated") final Long time_created) {
|
||||
|
||||
this.id = id;
|
||||
this.short_name = short_name;
|
||||
this.idnumber = idnumber;
|
||||
this.full_name = full_name;
|
||||
this.display_name = display_name;
|
||||
this.start_date = start_date;
|
||||
this.end_date = end_date;
|
||||
this.time_created = time_created;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private static final class CourseQuizData {
|
||||
final Collection<CourseQuiz> quizzes;
|
||||
final Collection<Warning> warnings;
|
||||
|
||||
@JsonCreator
|
||||
protected CourseQuizData(
|
||||
@JsonProperty(value = "quizzes") final Collection<CourseQuiz> quizzes,
|
||||
@JsonProperty(value = "warnings") final Collection<Warning> warnings) {
|
||||
this.quizzes = quizzes;
|
||||
this.warnings = warnings;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
static final class CourseQuiz {
|
||||
final String id;
|
||||
final String course;
|
||||
final String course_module;
|
||||
final String name;
|
||||
final String intro; // HTML
|
||||
final Long time_open; // unix-time seconds UTC
|
||||
final Long time_close; // unix-time seconds UTC
|
||||
|
||||
@JsonCreator
|
||||
protected CourseQuiz(
|
||||
@JsonProperty(value = "id") final String id,
|
||||
@JsonProperty(value = "course") final String course,
|
||||
@JsonProperty(value = "coursemodule") final String course_module,
|
||||
@JsonProperty(value = "name") final String name,
|
||||
@JsonProperty(value = "intro") final String intro,
|
||||
@JsonProperty(value = "timeopen") final Long time_open,
|
||||
@JsonProperty(value = "timeclose") final Long time_close) {
|
||||
|
||||
this.id = id;
|
||||
this.course = course;
|
||||
this.course_module = course_module;
|
||||
this.name = name;
|
||||
this.intro = intro;
|
||||
this.time_open = time_open;
|
||||
this.time_close = time_close;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,25 +8,116 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
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.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.exam.ExamConfigurationValueService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionAPI;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.MoodleQuizRestriction;
|
||||
|
||||
public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MoodlePluginCourseRestriction.class);
|
||||
|
||||
public static final String RESTRICTION_GET_FUNCTION_NAME = "quizaccess_sebserver_get_restriction";
|
||||
public static final String RESTRICTION_SET_FUNCTION_NAME = "quizaccess_sebserver_set_restriction";
|
||||
|
||||
public static final String ATTRIBUTE_QUIZ_ID = "quiz_id";
|
||||
public static final String ATTRIBUTE_CONFIG_KEYS = "config_keys";
|
||||
public static final String ATTRIBUTE_BROWSER_EXAM_KEYS = "browser_exam_keys";
|
||||
public static final String ATTRIBUTE_QUIT_URL = "quit_link";
|
||||
public static final String ATTRIBUTE_QUIT_SECRET = "quit_secret";
|
||||
|
||||
private final JSONMapper jsonMapper;
|
||||
private final MoodleRestTemplateFactory restTemplateFactory;
|
||||
private final ExamConfigurationValueService examConfigurationValueService;
|
||||
|
||||
private MoodleAPIRestTemplate restTemplate;
|
||||
|
||||
public MoodlePluginCourseRestriction(
|
||||
final JSONMapper jsonMapper,
|
||||
final MoodleRestTemplateFactory restTemplateFactory,
|
||||
final ExamConfigurationValueService examConfigurationValueService) {
|
||||
|
||||
this.jsonMapper = jsonMapper;
|
||||
this.restTemplateFactory = restTemplateFactory;
|
||||
this.examConfigurationValueService = examConfigurationValueService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LmsSetupTestResult testCourseRestrictionAPI() {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
final LmsSetupTestResult attributesCheck = this.restTemplateFactory.test();
|
||||
if (!attributesCheck.isOk()) {
|
||||
return attributesCheck;
|
||||
}
|
||||
|
||||
final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate();
|
||||
if (restTemplateRequest.hasError()) {
|
||||
final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " +
|
||||
this.restTemplateFactory.getKnownTokenAccessPaths();
|
||||
log.error(message + " cause: {}", restTemplateRequest.getError().getMessage());
|
||||
return LmsSetupTestResult.ofTokenRequestError(LmsType.MOODLE_PLUGIN, message);
|
||||
}
|
||||
|
||||
try {
|
||||
final MoodleAPIRestTemplate restTemplate = restTemplateRequest.get();
|
||||
restTemplate.testAPIConnection(
|
||||
RESTRICTION_GET_FUNCTION_NAME,
|
||||
RESTRICTION_SET_FUNCTION_NAME);
|
||||
|
||||
} catch (final RuntimeException e) {
|
||||
log.error("Failed to access Moodle course API: ", e);
|
||||
return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.MOODLE_PLUGIN, e.getMessage());
|
||||
}
|
||||
|
||||
return LmsSetupTestResult.ofOkay(LmsType.MOODLE_PLUGIN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
return getRestTemplate().map(restTemplate -> {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Get SEB Client restriction on exam: {}", exam);
|
||||
}
|
||||
|
||||
final String quizId = MoodleUtils.getQuizId(exam.getExternalId());
|
||||
final LinkedMultiValueMap<String, String> addQuery = new LinkedMultiValueMap<>();
|
||||
addQuery.add(ATTRIBUTE_QUIZ_ID, quizId);
|
||||
|
||||
final String srJSON = restTemplate.callMoodleAPIFunction(RESTRICTION_GET_FUNCTION_NAME, addQuery);
|
||||
|
||||
try {
|
||||
|
||||
final MoodleQuizRestriction moodleRestriction = this.jsonMapper.readValue(
|
||||
srJSON,
|
||||
MoodleUtils.MoodleQuizRestriction.class);
|
||||
|
||||
return toSEBRestriction(exam, moodleRestriction);
|
||||
} catch (final Exception e) {
|
||||
throw new RuntimeException("Unexpected error while get SEB restriction: ", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -34,14 +125,113 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
|
|||
final Exam exam,
|
||||
final SEBRestriction sebRestrictionData) {
|
||||
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Apply SEB Client restriction on exam: {}", exam);
|
||||
}
|
||||
|
||||
final String quizId = MoodleUtils.getQuizId(exam.getExternalId());
|
||||
final LinkedMultiValueMap<String, String> addQuery = new LinkedMultiValueMap<>();
|
||||
addQuery.add(ATTRIBUTE_QUIZ_ID, quizId);
|
||||
|
||||
final ArrayList<String> beks = new ArrayList<>(sebRestrictionData.browserExamKeys);
|
||||
final ArrayList<String> configKeys = new ArrayList<>(sebRestrictionData.configKeys);
|
||||
final String quitLink = this.examConfigurationValueService.getQuitLink(exam.id);
|
||||
final String quitSecret = this.examConfigurationValueService.getQuitSecret(exam.id);
|
||||
final String additionalBEK = sebRestrictionData.additionalProperties.get(
|
||||
SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK);
|
||||
if (additionalBEK != null) {
|
||||
beks.add(additionalBEK);
|
||||
}
|
||||
|
||||
final LinkedMultiValueMap<String, String> queryAttributes = new LinkedMultiValueMap<>();
|
||||
queryAttributes.put(ATTRIBUTE_CONFIG_KEYS, configKeys);
|
||||
queryAttributes.put(ATTRIBUTE_BROWSER_EXAM_KEYS, beks);
|
||||
queryAttributes.add(ATTRIBUTE_QUIT_URL, quitLink);
|
||||
queryAttributes.add(ATTRIBUTE_QUIT_SECRET, quitSecret);
|
||||
|
||||
final String srJSON = this.restTemplate.callMoodleAPIFunction(
|
||||
RESTRICTION_SET_FUNCTION_NAME,
|
||||
addQuery,
|
||||
queryAttributes);
|
||||
|
||||
try {
|
||||
|
||||
final MoodleQuizRestriction moodleRestriction = this.jsonMapper.readValue(
|
||||
srJSON,
|
||||
MoodleUtils.MoodleQuizRestriction.class);
|
||||
|
||||
return toSEBRestriction(exam, moodleRestriction);
|
||||
} catch (final Exception e) {
|
||||
throw new RuntimeException("Unexpected error while get SEB restriction: ", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
return Result.tryCatch(() -> {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Release SEB Client restriction on exam: {}", exam);
|
||||
}
|
||||
|
||||
final String quizId = MoodleUtils.getQuizId(exam.getExternalId());
|
||||
final LinkedMultiValueMap<String, String> addQuery = new LinkedMultiValueMap<>();
|
||||
addQuery.add(ATTRIBUTE_QUIZ_ID, quizId);
|
||||
|
||||
final String quitLink = this.examConfigurationValueService.getQuitLink(exam.id);
|
||||
final String quitSecret = this.examConfigurationValueService.getQuitSecret(exam.id);
|
||||
|
||||
final LinkedMultiValueMap<String, String> queryAttributes = new LinkedMultiValueMap<>();
|
||||
queryAttributes.add(ATTRIBUTE_QUIT_URL, quitLink);
|
||||
queryAttributes.add(ATTRIBUTE_QUIT_SECRET, quitSecret);
|
||||
|
||||
this.restTemplate.callMoodleAPIFunction(
|
||||
RESTRICTION_SET_FUNCTION_NAME,
|
||||
addQuery,
|
||||
queryAttributes);
|
||||
|
||||
return exam;
|
||||
});
|
||||
}
|
||||
|
||||
private SEBRestriction toSEBRestriction(final Exam exam, final MoodleQuizRestriction moodleRestriction) {
|
||||
final List<String> configKeys = Arrays.asList(StringUtils.split(
|
||||
moodleRestriction.config_keys,
|
||||
Constants.LIST_SEPARATOR));
|
||||
final List<String> browserExamKeys = Arrays.asList(StringUtils.split(
|
||||
moodleRestriction.browser_exam_keys,
|
||||
Constants.LIST_SEPARATOR));
|
||||
final Map<String, String> additionalProperties = new HashMap<>();
|
||||
additionalProperties.put(ATTRIBUTE_QUIT_URL, moodleRestriction.quit_link);
|
||||
|
||||
final String additionalBEK = exam.getAdditionalAttribute(
|
||||
SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK);
|
||||
|
||||
if (additionalBEK != null) {
|
||||
browserExamKeys.remove(additionalBEK);
|
||||
}
|
||||
|
||||
return new SEBRestriction(
|
||||
exam.id,
|
||||
configKeys,
|
||||
browserExamKeys,
|
||||
additionalProperties);
|
||||
}
|
||||
|
||||
private Result<MoodleAPIRestTemplate> getRestTemplate() {
|
||||
if (this.restTemplate == null) {
|
||||
final Result<MoodleAPIRestTemplate> templateRequest = this.restTemplateFactory
|
||||
.createRestTemplate();
|
||||
if (templateRequest.hasError()) {
|
||||
return templateRequest;
|
||||
} else {
|
||||
this.restTemplate = templateRequest.get();
|
||||
}
|
||||
}
|
||||
|
||||
return Result.of(this.restTemplate);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -24,10 +24,13 @@ import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService;
|
|||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
|
||||
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;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MockupRestTemplateFactory;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodlePluginCheck;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
|
||||
|
||||
@Lazy
|
||||
|
@ -41,6 +44,7 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
|
|||
private final AsyncService asyncService;
|
||||
private final Environment environment;
|
||||
private final ClientCredentialService clientCredentialService;
|
||||
private final ExamConfigurationValueService examConfigurationValueService;
|
||||
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
|
||||
private final ApplicationContext applicationContext;
|
||||
private final String[] alternativeTokenRequestPaths;
|
||||
|
@ -52,6 +56,7 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
|
|||
final AsyncService asyncService,
|
||||
final Environment environment,
|
||||
final ClientCredentialService clientCredentialService,
|
||||
final ExamConfigurationValueService examConfigurationValueService,
|
||||
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
|
||||
final ApplicationContext applicationContext,
|
||||
@Value("${sebserver.webservice.lms.moodle.api.token.request.paths:}") final String alternativeTokenRequestPaths) {
|
||||
|
@ -62,6 +67,7 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
|
|||
this.asyncService = asyncService;
|
||||
this.environment = environment;
|
||||
this.clientCredentialService = clientCredentialService;
|
||||
this.examConfigurationValueService = examConfigurationValueService;
|
||||
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
|
||||
this.applicationContext = applicationContext;
|
||||
this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
|
||||
|
@ -78,19 +84,27 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
|
|||
public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory(
|
||||
this.jsonMapper,
|
||||
apiTemplateDataSupplier,
|
||||
this.clientCredentialService,
|
||||
this.clientHttpRequestFactoryService,
|
||||
this.alternativeTokenRequestPaths);
|
||||
// final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactoryImpl(
|
||||
// this.jsonMapper,
|
||||
// apiTemplateDataSupplier,
|
||||
// this.clientCredentialService,
|
||||
// this.clientHttpRequestFactoryService,
|
||||
// this.alternativeTokenRequestPaths);
|
||||
|
||||
final MoodleRestTemplateFactory moodleRestTemplateFactory =
|
||||
new MockupRestTemplateFactory(apiTemplateDataSupplier);
|
||||
|
||||
final MoodlePluginCourseAccess moodlePluginCourseAccess = new MoodlePluginCourseAccess(
|
||||
this.jsonMapper,
|
||||
this.asyncService,
|
||||
moodleRestTemplateFactory,
|
||||
this.cacheManager);
|
||||
this.cacheManager,
|
||||
this.environment);
|
||||
|
||||
final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction();
|
||||
final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction(
|
||||
this.jsonMapper,
|
||||
moodleRestTemplateFactory,
|
||||
this.examConfigurationValueService);
|
||||
|
||||
return new LmsAPITemplateAdapter(
|
||||
this.asyncService,
|
||||
|
|
|
@ -47,7 +47,6 @@ 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.Cryptor;
|
||||
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;
|
||||
|
@ -68,14 +67,10 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
|
|||
private static final String ADDITIONAL_ATTR_QUIT_LINK = "ADDITIONAL_ATTR_QUIT_LINK";
|
||||
private static final String ADDITIONAL_ATTR_QUIT_SECRET = "ADDITIONAL_ATTR_QUIT_SECRET";
|
||||
|
||||
private static final String CONFIG_ATTR_NAME_QUIT_LINK = "quitURL";
|
||||
private static final String CONFIG_ATTR_NAME_QUIT_SECRET = "hashedQuitPassword";
|
||||
|
||||
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
|
||||
private final ClientCredentialService clientCredentialService;
|
||||
private final APITemplateDataSupplier apiTemplateDataSupplier;
|
||||
private final ExamConfigurationValueService examConfigurationValueService;
|
||||
private final Cryptor cryptor;
|
||||
private final Long lmsSetupId;
|
||||
|
||||
private OlatLmsRestTemplate cachedRestTemplate;
|
||||
|
@ -85,7 +80,6 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
|
|||
final ClientCredentialService clientCredentialService,
|
||||
final APITemplateDataSupplier apiTemplateDataSupplier,
|
||||
final ExamConfigurationValueService examConfigurationValueService,
|
||||
final Cryptor cryptor,
|
||||
final CacheManager cacheManager) {
|
||||
|
||||
super(cacheManager);
|
||||
|
@ -94,7 +88,6 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
|
|||
this.clientCredentialService = clientCredentialService;
|
||||
this.apiTemplateDataSupplier = apiTemplateDataSupplier;
|
||||
this.examConfigurationValueService = examConfigurationValueService;
|
||||
this.cryptor = cryptor;
|
||||
this.lmsSetupId = apiTemplateDataSupplier.getLmsSetup().id;
|
||||
}
|
||||
|
||||
|
@ -357,8 +350,8 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
|
|||
final RestrictionDataPost post = new RestrictionDataPost();
|
||||
post.browserExamKeys = new ArrayList<>(restriction.browserExamKeys);
|
||||
post.configKeys = new ArrayList<>(restriction.configKeys);
|
||||
post.quitLink = this.getQuitLink(restriction.examId);
|
||||
post.quitSecret = this.getQuitSecret(restriction.examId);
|
||||
post.quitLink = this.examConfigurationValueService.getQuitLink(restriction.examId);
|
||||
post.quitSecret = this.examConfigurationValueService.getQuitSecret(restriction.examId);
|
||||
final RestrictionData r =
|
||||
this.apiPost(restTemplate, url, post, RestrictionDataPost.class, RestrictionData.class);
|
||||
return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, new HashMap<String, String>());
|
||||
|
@ -476,43 +469,4 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
|
|||
});
|
||||
}
|
||||
|
||||
private String getQuitSecret(final Long examId) {
|
||||
try {
|
||||
|
||||
final String quitSecretEncrypted = this.examConfigurationValueService.getMappedDefaultConfigAttributeValue(
|
||||
examId,
|
||||
CONFIG_ATTR_NAME_QUIT_SECRET);
|
||||
|
||||
if (StringUtils.isNotEmpty(quitSecretEncrypted)) {
|
||||
try {
|
||||
|
||||
return this.cryptor
|
||||
.decrypt(quitSecretEncrypted)
|
||||
.getOrThrow()
|
||||
.toString();
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to decrypt quitSecret: ", e);
|
||||
}
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to get SEB restriction with quit secret: ", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getQuitLink(final Long examId) {
|
||||
try {
|
||||
|
||||
return this.examConfigurationValueService.getMappedDefaultConfigAttributeValue(
|
||||
examId,
|
||||
CONFIG_ATTR_NAME_QUIT_LINK);
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to get SEB restriction with quit link: ", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ import ch.ethz.seb.sebserver.gbl.async.AsyncService;
|
|||
import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
|
||||
|
@ -44,7 +43,6 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
|||
private final AsyncService asyncService;
|
||||
private final Environment environment;
|
||||
private final CacheManager cacheManager;
|
||||
private final Cryptor cryptor;
|
||||
|
||||
public OlatLmsAPITemplateFactory(
|
||||
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
|
||||
|
@ -52,8 +50,7 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
|||
final ExamConfigurationValueService examConfigurationValueService,
|
||||
final AsyncService asyncService,
|
||||
final Environment environment,
|
||||
final CacheManager cacheManager,
|
||||
final Cryptor cryptor) {
|
||||
final CacheManager cacheManager) {
|
||||
|
||||
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
|
||||
this.clientCredentialService = clientCredentialService;
|
||||
|
@ -61,7 +58,6 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
|||
this.asyncService = asyncService;
|
||||
this.environment = environment;
|
||||
this.cacheManager = cacheManager;
|
||||
this.cryptor = cryptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -72,13 +68,14 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
|||
@Override
|
||||
public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
final OlatLmsAPITemplate olatLmsAPITemplate = new OlatLmsAPITemplate(
|
||||
this.clientHttpRequestFactoryService,
|
||||
this.clientCredentialService,
|
||||
apiTemplateDataSupplier,
|
||||
this.examConfigurationValueService,
|
||||
this.cryptor,
|
||||
this.cacheManager);
|
||||
|
||||
return new LmsAPITemplateAdapter(
|
||||
this.asyncService,
|
||||
this.environment,
|
||||
|
|
|
@ -38,7 +38,7 @@ 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.SEBRestrictionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseAccess;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamResetEvent;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent;
|
||||
|
@ -402,7 +402,7 @@ class ExamUpdateHandler {
|
|||
log.debug("Found formerName quiz name: {}", exam.name);
|
||||
|
||||
// get the course name identifier
|
||||
final String shortname = MoodleCourseAccess.getShortname(quizId);
|
||||
final String shortname = MoodleUtils.getShortname(quizId);
|
||||
if (StringUtils.isNotBlank(shortname)) {
|
||||
|
||||
log.debug("Using short-name: {} for recovering", shortname);
|
||||
|
@ -412,7 +412,7 @@ class ExamUpdateHandler {
|
|||
.getOrThrow()
|
||||
.stream()
|
||||
.filter(quiz -> {
|
||||
final String qShortName = MoodleCourseAccess.getShortname(quiz.id);
|
||||
final String qShortName = MoodleUtils.getShortname(quiz.id);
|
||||
return qShortName != null && qShortName.equals(shortname);
|
||||
})
|
||||
.filter(quiz -> exam.name.equals(quiz.name))
|
||||
|
|
|
@ -51,6 +51,7 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
|||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SEBClientConfigDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientSessionService;
|
||||
|
@ -479,7 +480,7 @@ public class ExamAPI_V1_Controller {
|
|||
return;
|
||||
}
|
||||
this.examSessionService.getRunningExam(examId)
|
||||
.map(exam -> exam.getAdditionalAttribute(Exam.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK))
|
||||
.map(exam -> exam.getAdditionalAttribute(SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK))
|
||||
.onSuccess(bek -> response.setHeader(API.EXAM_API_EXAM_ALT_BEK, bek));
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,6 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
|
|||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat.OlatLmsAPITemplate;
|
||||
|
@ -42,8 +41,6 @@ public class OlatLmsAPITemplateTest extends AdministrationAPIIntegrationTester {
|
|||
@Autowired
|
||||
private ExamConfigurationValueService examConfigurationValueService;
|
||||
@Autowired
|
||||
private Cryptor cryptor;
|
||||
@Autowired
|
||||
private CacheManager cacheManager;
|
||||
|
||||
@Test
|
||||
|
@ -59,7 +56,6 @@ public class OlatLmsAPITemplateTest extends AdministrationAPIIntegrationTester {
|
|||
null,
|
||||
apiTemplateDataSupplier,
|
||||
this.examConfigurationValueService,
|
||||
this.cryptor,
|
||||
this.cacheManager);
|
||||
|
||||
Mockito.when(restTemplateMock.exchange(Mockito.any(), Mockito.any(), Mockito.any(),
|
||||
|
|
|
@ -29,8 +29,8 @@ import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
|||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult.ErrorType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplateImpl;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactoryImpl;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactoryImpl.MoodleAPIRestTemplateImpl;
|
||||
|
||||
public class MoodleCourseAccessTest {
|
||||
|
||||
|
@ -42,7 +42,7 @@ public class MoodleCourseAccessTest {
|
|||
@Test
|
||||
public void testGetExamineeAccountDetails() {
|
||||
|
||||
final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class);
|
||||
final MoodleRestTemplateFactoryImpl moodleRestTemplateFactory = mock(MoodleRestTemplateFactoryImpl.class);
|
||||
final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class);
|
||||
when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate));
|
||||
when(moodleAPIRestTemplate.callMoodleAPIFunction(
|
||||
|
@ -118,7 +118,7 @@ public class MoodleCourseAccessTest {
|
|||
|
||||
@Test
|
||||
public void testInitAPIAccessError1() {
|
||||
final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class);
|
||||
final MoodleRestTemplateFactoryImpl moodleRestTemplateFactory = mock(MoodleRestTemplateFactoryImpl.class);
|
||||
when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.ofRuntimeError("Error1"));
|
||||
when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE));
|
||||
|
||||
|
@ -138,7 +138,7 @@ public class MoodleCourseAccessTest {
|
|||
|
||||
@Test
|
||||
public void testInitAPIAccessError2() {
|
||||
final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class);
|
||||
final MoodleRestTemplateFactoryImpl moodleRestTemplateFactory = mock(MoodleRestTemplateFactoryImpl.class);
|
||||
final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class);
|
||||
when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate));
|
||||
doThrow(RuntimeException.class).when(moodleAPIRestTemplate).testAPIConnection(any());
|
||||
|
@ -160,7 +160,7 @@ public class MoodleCourseAccessTest {
|
|||
|
||||
@Test
|
||||
public void testInitAPIAccessOK() {
|
||||
final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class);
|
||||
final MoodleRestTemplateFactoryImpl moodleRestTemplateFactory = mock(MoodleRestTemplateFactoryImpl.class);
|
||||
final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class);
|
||||
when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate));
|
||||
when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE));
|
||||
|
|
Loading…
Reference in a new issue