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";
|
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 */
|
/** 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";
|
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 {
|
public enum ExamStatus {
|
||||||
UP_COMING,
|
UP_COMING,
|
||||||
|
|
|
@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.gbl.model.exam;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
|
@ -211,6 +212,23 @@ public final class QuizData implements GrantEntity {
|
||||||
return this.additionalAttributes;
|
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
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
final StringBuilder builder = new StringBuilder();
|
final StringBuilder builder = new StringBuilder();
|
||||||
|
|
|
@ -10,6 +10,9 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.exam;
|
||||||
|
|
||||||
public interface ExamConfigurationValueService {
|
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
|
/** Get the actual SEB settings attribute value for the exam configuration mapped as default configuration
|
||||||
* to the given exam
|
* to the given exam
|
||||||
*
|
*
|
||||||
|
@ -18,4 +21,8 @@ public interface ExamConfigurationValueService {
|
||||||
* @return The current value of the above SEB settings attribute and given exam. */
|
* @return The current value of the above SEB settings attribute and given exam. */
|
||||||
String getMappedDefaultConfigAttributeValue(Long examId, String configAttributeName);
|
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(
|
this.additionalAttributesDAO.saveAdditionalAttribute(
|
||||||
EntityType.EXAM,
|
EntityType.EXAM,
|
||||||
exam.id,
|
exam.id,
|
||||||
Exam.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK,
|
SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK,
|
||||||
moodleBEK).getOrThrow();
|
moodleBEK).getOrThrow();
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
log.error("Failed to create additional moodle SEB BEK attribute: ", 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;
|
package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
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.ConfigurationAttributeDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO;
|
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.ConfigurationValueDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
|
||||||
|
|
||||||
|
@Lazy
|
||||||
@Service
|
@Service
|
||||||
|
@WebServiceProfile
|
||||||
public class ExamConfigurationValueServiceImpl implements ExamConfigurationValueService {
|
public class ExamConfigurationValueServiceImpl implements ExamConfigurationValueService {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(ExamConfigurationValueServiceImpl.class);
|
private static final Logger log = LoggerFactory.getLogger(ExamConfigurationValueServiceImpl.class);
|
||||||
|
@ -27,17 +33,20 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue
|
||||||
private final ConfigurationDAO configurationDAO;
|
private final ConfigurationDAO configurationDAO;
|
||||||
private final ConfigurationAttributeDAO configurationAttributeDAO;
|
private final ConfigurationAttributeDAO configurationAttributeDAO;
|
||||||
private final ConfigurationValueDAO configurationValueDAO;
|
private final ConfigurationValueDAO configurationValueDAO;
|
||||||
|
private final Cryptor cryptor;
|
||||||
|
|
||||||
public ExamConfigurationValueServiceImpl(
|
public ExamConfigurationValueServiceImpl(
|
||||||
final ExamConfigurationMapDAO examConfigurationMapDAO,
|
final ExamConfigurationMapDAO examConfigurationMapDAO,
|
||||||
final ConfigurationDAO configurationDAO,
|
final ConfigurationDAO configurationDAO,
|
||||||
final ConfigurationAttributeDAO configurationAttributeDAO,
|
final ConfigurationAttributeDAO configurationAttributeDAO,
|
||||||
final ConfigurationValueDAO configurationValueDAO) {
|
final ConfigurationValueDAO configurationValueDAO,
|
||||||
|
final Cryptor cryptor) {
|
||||||
|
|
||||||
this.examConfigurationMapDAO = examConfigurationMapDAO;
|
this.examConfigurationMapDAO = examConfigurationMapDAO;
|
||||||
this.configurationDAO = configurationDAO;
|
this.configurationDAO = configurationDAO;
|
||||||
this.configurationAttributeDAO = configurationAttributeDAO;
|
this.configurationAttributeDAO = configurationAttributeDAO;
|
||||||
this.configurationValueDAO = configurationValueDAO;
|
this.configurationValueDAO = configurationValueDAO;
|
||||||
|
this.cryptor = cryptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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;
|
package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
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. */
|
* @return Result referencing to the Chapters model for the given course or to an error when happened. */
|
||||||
Result<Chapters> getCourseChapters(String courseId);
|
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 {
|
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;
|
public boolean finished = false;
|
||||||
|
/** Indicates if the fetch is been canceled. Set this to true to cancel the asynchronous process */
|
||||||
public boolean canceled = false;
|
public boolean canceled = false;
|
||||||
|
/** Reference to an error when the asynchronous fetch stopped with an error */
|
||||||
public Exception error = null;
|
public Exception error = null;
|
||||||
|
|
||||||
public void finish() {
|
public void finish() {
|
||||||
|
|
|
@ -18,6 +18,9 @@ public interface SEBRestrictionService {
|
||||||
String SEB_RESTRICTION_ADDITIONAL_PROPERTY_NAME_PREFIX = "sebRestrictionProp_";
|
String SEB_RESTRICTION_ADDITIONAL_PROPERTY_NAME_PREFIX = "sebRestrictionProp_";
|
||||||
|
|
||||||
String SEB_RESTRICTION_ADDITIONAL_PROPERTY_CONFIG_KEY = "config_key";
|
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 */
|
/** Get the LmsAPIService that is used by the SEBRestrictionService */
|
||||||
LmsAPIService getLmsAPIService();
|
LmsAPIService getLmsAPIService();
|
||||||
|
|
|
@ -98,9 +98,14 @@ public abstract class AbstractCachedCourseAccess {
|
||||||
|
|
||||||
/** Put all QuizData to short time cache.
|
/** Put all QuizData to short time cache.
|
||||||
*
|
*
|
||||||
* @param quizData Collection of QuizData */
|
* @param quizData Collection of QuizData
|
||||||
protected void putToCache(final Collection<QuizData> 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));
|
quizData.stream().forEach(q -> this.cache.put(createCacheKey(q.id), q));
|
||||||
|
return quizData;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void evict(final String id) {
|
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)
|
* Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
|
||||||
*
|
*
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
* 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
|
* 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/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
|
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.Set;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||||
import java.util.Collections;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
import java.util.HashMap;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
public interface MoodleRestTemplateFactory {
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
LmsSetupTestResult test();
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.Collectors;
|
APITemplateDataSupplier getApiTemplateDataSupplier();
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
Set<String> getKnownTokenAccessPaths();
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
Result<MoodleAPIRestTemplate> createRestTemplate();
|
||||||
import org.springframework.http.HttpEntity;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -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.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.apache.commons.lang3.BooleanUtils;
|
import org.apache.commons.lang3.BooleanUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
import org.joda.time.DateTimeZone;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.core.env.Environment;
|
import org.springframework.core.env.Environment;
|
||||||
import org.springframework.util.LinkedMultiValueMap;
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
import org.springframework.util.MultiValueMap;
|
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.JsonParseException;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
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.CourseAccessAPI;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate;
|
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.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.CourseDataShort;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseDataAsyncLoader.CourseQuizShort;
|
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";
|
static final String MOODLE_COURSE_API_SEARCH_PAGE_SIZE = "perpage";
|
||||||
|
|
||||||
private final JSONMapper jsonMapper;
|
private final JSONMapper jsonMapper;
|
||||||
private final MoodleRestTemplateFactory moodleRestTemplateFactory;
|
private final MoodleRestTemplateFactory restTemplateFactory;
|
||||||
private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader;
|
private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader;
|
||||||
private final boolean prependShortCourseName;
|
private final boolean prependShortCourseName;
|
||||||
private final CircuitBreaker<String> protectedMoodlePageCall;
|
private final CircuitBreaker<String> protectedMoodlePageCall;
|
||||||
|
@ -110,13 +109,13 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
||||||
public MoodleCourseAccess(
|
public MoodleCourseAccess(
|
||||||
final JSONMapper jsonMapper,
|
final JSONMapper jsonMapper,
|
||||||
final AsyncService asyncService,
|
final AsyncService asyncService,
|
||||||
final MoodleRestTemplateFactory moodleRestTemplateFactory,
|
final MoodleRestTemplateFactory restTemplateFactory,
|
||||||
final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader,
|
final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader,
|
||||||
final Environment environment) {
|
final Environment environment) {
|
||||||
|
|
||||||
this.jsonMapper = jsonMapper;
|
this.jsonMapper = jsonMapper;
|
||||||
this.moodleCourseDataAsyncLoader = moodleCourseDataAsyncLoader;
|
this.moodleCourseDataAsyncLoader = moodleCourseDataAsyncLoader;
|
||||||
this.moodleRestTemplateFactory = moodleRestTemplateFactory;
|
this.restTemplateFactory = restTemplateFactory;
|
||||||
|
|
||||||
this.prependShortCourseName = BooleanUtils.toBoolean(environment.getProperty(
|
this.prependShortCourseName = BooleanUtils.toBoolean(environment.getProperty(
|
||||||
"sebserver.webservice.lms.moodle.prependShortCourseName",
|
"sebserver.webservice.lms.moodle.prependShortCourseName",
|
||||||
|
@ -142,12 +141,12 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
APITemplateDataSupplier getApiTemplateDataSupplier() {
|
APITemplateDataSupplier getApiTemplateDataSupplier() {
|
||||||
return this.moodleRestTemplateFactory.apiTemplateDataSupplier;
|
return this.restTemplateFactory.getApiTemplateDataSupplier();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LmsSetupTestResult testCourseAccessAPI() {
|
public LmsSetupTestResult testCourseAccessAPI() {
|
||||||
final LmsSetupTestResult attributesCheck = this.moodleRestTemplateFactory.test();
|
final LmsSetupTestResult attributesCheck = this.restTemplateFactory.test();
|
||||||
if (!attributesCheck.isOk()) {
|
if (!attributesCheck.isOk()) {
|
||||||
return attributesCheck;
|
return attributesCheck;
|
||||||
}
|
}
|
||||||
|
@ -155,7 +154,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
||||||
final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate();
|
final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate();
|
||||||
if (restTemplateRequest.hasError()) {
|
if (restTemplateRequest.hasError()) {
|
||||||
final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " +
|
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());
|
log.error(message + " cause: {}", restTemplateRequest.getError().getMessage());
|
||||||
return LmsSetupTestResult.ofTokenRequestError(LmsType.MOODLE, message);
|
return LmsSetupTestResult.ofTokenRequestError(LmsType.MOODLE, message);
|
||||||
}
|
}
|
||||||
|
@ -189,12 +188,12 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
||||||
try {
|
try {
|
||||||
int page = 0;
|
int page = 0;
|
||||||
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
|
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
|
||||||
|
final MoodleAPIRestTemplate restTemplate = getRestTemplate().getOrThrow();
|
||||||
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
|
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
|
||||||
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
|
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
|
||||||
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
|
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
|
||||||
|
|
||||||
while (!asyncQuizFetchBuffer.finished && !asyncQuizFetchBuffer.canceled) {
|
while (!asyncQuizFetchBuffer.finished && !asyncQuizFetchBuffer.canceled) {
|
||||||
final MoodleAPIRestTemplate restTemplate = getRestTemplate().getOrThrow();
|
|
||||||
// first get courses from Moodle for page
|
// first get courses from Moodle for page
|
||||||
final Map<String, CourseData> courseData = new HashMap<>();
|
final Map<String, CourseData> courseData = new HashMap<>();
|
||||||
final Collection<CourseData> coursesPage = getCoursesPage(restTemplate, page, this.pageSize);
|
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()) {
|
if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) {
|
||||||
log.warn(
|
MoodleUtils.logMoodleWarning(
|
||||||
"There are warnings from Moodle response: Moodle: {} request: {} warnings: {} warning sample: {}",
|
courseQuizData.warnings,
|
||||||
lmsSetup.name,
|
lmsSetup.name,
|
||||||
MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) {
|
if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) {
|
||||||
|
@ -255,7 +249,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
||||||
|
|
||||||
courseQuizData.quizzes
|
courseQuizData.quizzes
|
||||||
.stream()
|
.stream()
|
||||||
.filter(getQuizFilter())
|
.filter(MoodleUtils.getQuizFilter())
|
||||||
.forEach(quiz -> {
|
.forEach(quiz -> {
|
||||||
final CourseData data = courseData.get(quiz.course);
|
final CourseData data = courseData.get(quiz.course);
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
|
@ -266,10 +260,16 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
||||||
courseData.values().stream()
|
courseData.values().stream()
|
||||||
.filter(c -> !c.quizzes.isEmpty())
|
.filter(c -> !c.quizzes.isEmpty())
|
||||||
.forEach(c -> asyncQuizFetchBuffer.buffer.addAll(
|
.forEach(c -> asyncQuizFetchBuffer.buffer.addAll(
|
||||||
quizDataOf(lmsSetup, c, urlPrefix).stream()
|
MoodleUtils.quizDataOf(lmsSetup, c, urlPrefix, this.prependShortCourseName)
|
||||||
|
.stream()
|
||||||
.filter(LmsAPIService.quizFilterPredicate(filterMap))
|
.filter(LmsAPIService.quizFilterPredicate(filterMap))
|
||||||
.collect(Collectors.toList())));
|
.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++;
|
page++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -298,8 +298,8 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
||||||
final Map<String, CourseDataShort> cachedCourseData = this.moodleCourseDataAsyncLoader
|
final Map<String, CourseDataShort> cachedCourseData = this.moodleCourseDataAsyncLoader
|
||||||
.getCachedCourseData();
|
.getCachedCourseData();
|
||||||
|
|
||||||
final String courseId = getCourseId(id);
|
final String courseId = MoodleUtils.getCourseId(id);
|
||||||
final String quizId = getQuizId(id);
|
final String quizId = MoodleUtils.getQuizId(id);
|
||||||
if (cachedCourseData.containsKey(courseId)) {
|
if (cachedCourseData.containsKey(courseId)) {
|
||||||
final CourseDataShort courseData = cachedCourseData.get(courseId);
|
final CourseDataShort courseData = cachedCourseData.get(courseId);
|
||||||
final CourseQuizShort quiz = courseData.quizzes
|
final CourseQuizShort quiz = courseData.quizzes
|
||||||
|
@ -352,7 +352,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
||||||
MOODLE_USER_PROFILE_API_FUNCTION_NAME,
|
MOODLE_USER_PROFILE_API_FUNCTION_NAME,
|
||||||
queryAttributes);
|
queryAttributes);
|
||||||
|
|
||||||
if (checkAccessDeniedError(userDetailsJSON)) {
|
if (MoodleUtils.checkAccessDeniedError(userDetailsJSON)) {
|
||||||
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
|
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
|
||||||
log.error("Get access denied error from Moodle: {} for API call: {}, response: {}",
|
log.error("Get access denied error from Moodle: {} for API call: {}, response: {}",
|
||||||
lmsSetup,
|
lmsSetup,
|
||||||
|
@ -486,7 +486,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
||||||
final Map<String, CourseData> courseData = getCoursesForIds(
|
final Map<String, CourseData> courseData = getCoursesForIds(
|
||||||
restTemplate,
|
restTemplate,
|
||||||
quizIds.stream()
|
quizIds.stream()
|
||||||
.map(MoodleCourseAccess::getCourseId)
|
.map(MoodleUtils::getCourseId)
|
||||||
.collect(Collectors.toSet()))
|
.collect(Collectors.toSet()))
|
||||||
.stream()
|
.stream()
|
||||||
.collect(Collectors.toMap(cd -> cd.id, Function.identity()));
|
.collect(Collectors.toMap(cd -> cd.id, Function.identity()));
|
||||||
|
@ -518,7 +518,12 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
||||||
return Collections.emptyList();
|
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()) {
|
if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) {
|
||||||
log.error("No quizzes found for ids: {} on LMS; {}", quizIds, lmsSetup.name);
|
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;
|
final Map<String, CourseData> finalCourseDataRef = courseData;
|
||||||
courseQuizData.quizzes
|
courseQuizData.quizzes
|
||||||
.stream()
|
.stream()
|
||||||
.forEach(quiz -> fillSelectedQuizzes(quizIds, finalCourseDataRef, quiz));
|
.forEach(quiz -> MoodleUtils.fillSelectedQuizzes(quizIds, finalCourseDataRef, quiz));
|
||||||
|
|
||||||
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
|
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
|
||||||
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
|
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
|
||||||
|
@ -537,7 +542,11 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
||||||
return courseData.values()
|
return courseData.values()
|
||||||
.stream()
|
.stream()
|
||||||
.filter(c -> !c.quizzes.isEmpty())
|
.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());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
} catch (final Exception e) {
|
} 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(
|
private Collection<CourseData> getCoursesForIds(
|
||||||
final MoodleAPIRestTemplate restTemplate,
|
final MoodleAPIRestTemplate restTemplate,
|
||||||
final Set<String> ids) {
|
final Set<String> ids) {
|
||||||
|
@ -596,7 +584,12 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
||||||
return Collections.emptyList();
|
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()) {
|
if (courses.courses == null || courses.courses.isEmpty()) {
|
||||||
log.error("No courses found for ids: {} on LMS: {}", ids, lmsSetup.name);
|
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(
|
private List<QuizData> quizDataOf(
|
||||||
final LmsSetup lmsSetup,
|
final LmsSetup lmsSetup,
|
||||||
final CourseDataShort courseData,
|
final CourseDataShort courseData,
|
||||||
|
@ -682,7 +630,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
||||||
|
|
||||||
final String startURI = uriPrefix + courseQuizData.course_module;
|
final String startURI = uriPrefix + courseQuizData.course_module;
|
||||||
return new QuizData(
|
return new QuizData(
|
||||||
getInternalQuizId(
|
MoodleUtils.getInternalQuizId(
|
||||||
courseQuizData.course_module,
|
courseQuizData.course_module,
|
||||||
courseData.id,
|
courseData.id,
|
||||||
courseData.short_name,
|
courseData.short_name,
|
||||||
|
@ -706,7 +654,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
||||||
|
|
||||||
private Result<MoodleAPIRestTemplate> getRestTemplate() {
|
private Result<MoodleAPIRestTemplate> getRestTemplate() {
|
||||||
if (this.restTemplate == null) {
|
if (this.restTemplate == null) {
|
||||||
final Result<MoodleAPIRestTemplate> templateRequest = this.moodleRestTemplateFactory
|
final Result<MoodleAPIRestTemplate> templateRequest = this.restTemplateFactory
|
||||||
.createRestTemplate();
|
.createRestTemplate();
|
||||||
if (templateRequest.hasError()) {
|
if (templateRequest.hasError()) {
|
||||||
return templateRequest;
|
return templateRequest;
|
||||||
|
@ -718,95 +666,6 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
||||||
return Result.of(this.restTemplate);
|
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(
|
private Collection<CourseData> getCoursesPage(
|
||||||
final MoodleAPIRestTemplate restTemplate,
|
final MoodleAPIRestTemplate restTemplate,
|
||||||
final int page,
|
final int page,
|
||||||
|
@ -866,7 +725,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
|
||||||
|
|
||||||
final Collection<CourseData> result = getCoursesForIds(restTemplate, ids)
|
final Collection<CourseData> result = getCoursesForIds(restTemplate, ids)
|
||||||
.stream()
|
.stream()
|
||||||
.filter(getCourseFilter())
|
.filter(MoodleUtils.getCourseFilter())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
if (log.isDebugEnabled()) {
|
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.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;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate.Warning;
|
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
|
@Lazy
|
||||||
@Component
|
@Component
|
||||||
|
|
|
@ -8,408 +8,34 @@
|
||||||
|
|
||||||
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy;
|
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.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.exam.SEBRestriction;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
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.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:
|
/** Dummy Implementation */
|
||||||
* 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 */
|
|
||||||
public class MoodleCourseRestriction implements SEBRestrictionAPI {
|
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
|
@Override
|
||||||
public LmsSetupTestResult testCourseRestrictionAPI() {
|
public LmsSetupTestResult testCourseRestrictionAPI() {
|
||||||
// try to call the SEB Restrictions API
|
return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.MOODLE, "SEB restriction not supported");
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
|
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
|
||||||
return Result.tryCatch(() -> {
|
return Result.ofError(new UnsupportedOperationException("SEB restriction not supported"));
|
||||||
return getSEBRestriction(
|
|
||||||
MoodleCourseAccess.getQuizId(exam.externalId),
|
|
||||||
MoodleCourseAccess.getShortname(exam.externalId),
|
|
||||||
MoodleCourseAccess.getIdnumber(exam.externalId))
|
|
||||||
.map(restriction -> SEBRestriction.from(exam.id, restriction))
|
|
||||||
.getOrThrow();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<SEBRestriction> applySEBClientRestriction(
|
public Result<SEBRestriction> applySEBClientRestriction(final Exam exam, final SEBRestriction sebRestrictionData) {
|
||||||
final Exam exam,
|
return Result.ofError(new UnsupportedOperationException("SEB restriction not supported"));
|
||||||
final SEBRestriction sebRestrictionData) {
|
|
||||||
|
|
||||||
return this.updateSEBRestriction(
|
|
||||||
exam.externalId,
|
|
||||||
MoodleSEBRestriction.from(sebRestrictionData))
|
|
||||||
.map(result -> sebRestrictionData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
|
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
|
||||||
return this.deleteSEBRestriction(exam.externalId)
|
return Result.ofError(new UnsupportedOperationException("SEB restriction not supported"));
|
||||||
.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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.model.institution.LmsSetup.LmsType;
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
|
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.LmsAPITemplate;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
|
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.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.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.MoodlePluginCourseAccess;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseRestriction;
|
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 Environment environment;
|
||||||
private final ClientCredentialService clientCredentialService;
|
private final ClientCredentialService clientCredentialService;
|
||||||
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
|
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
|
||||||
|
private final ExamConfigurationValueService examConfigurationValueService;
|
||||||
private final ApplicationContext applicationContext;
|
private final ApplicationContext applicationContext;
|
||||||
private final String[] alternativeTokenRequestPaths;
|
private final String[] alternativeTokenRequestPaths;
|
||||||
|
|
||||||
|
@ -56,6 +59,7 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
||||||
final AsyncService asyncService,
|
final AsyncService asyncService,
|
||||||
final Environment environment,
|
final Environment environment,
|
||||||
final ClientCredentialService clientCredentialService,
|
final ClientCredentialService clientCredentialService,
|
||||||
|
final ExamConfigurationValueService examConfigurationValueService,
|
||||||
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
|
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
|
||||||
final ApplicationContext applicationContext,
|
final ApplicationContext applicationContext,
|
||||||
@Value("${sebserver.webservice.lms.moodle.api.token.request.paths:}") final String alternativeTokenRequestPaths) {
|
@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.asyncService = asyncService;
|
||||||
this.environment = environment;
|
this.environment = environment;
|
||||||
this.clientCredentialService = clientCredentialService;
|
this.clientCredentialService = clientCredentialService;
|
||||||
|
this.examConfigurationValueService = examConfigurationValueService;
|
||||||
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
|
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
|
||||||
this.applicationContext = applicationContext;
|
this.applicationContext = applicationContext;
|
||||||
this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
|
this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
|
||||||
|
@ -88,20 +93,26 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
||||||
.getBean(MoodleCourseDataAsyncLoader.class);
|
.getBean(MoodleCourseDataAsyncLoader.class);
|
||||||
asyncLoaderPrototype.init(lmsSetup.getModelId());
|
asyncLoaderPrototype.init(lmsSetup.getModelId());
|
||||||
|
|
||||||
final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory(
|
final MoodleRestTemplateFactory restTemplateFactory = new MoodleRestTemplateFactoryImpl(
|
||||||
this.jsonMapper,
|
this.jsonMapper,
|
||||||
apiTemplateDataSupplier,
|
apiTemplateDataSupplier,
|
||||||
this.clientCredentialService,
|
this.clientCredentialService,
|
||||||
this.clientHttpRequestFactoryService,
|
this.clientHttpRequestFactoryService,
|
||||||
this.alternativeTokenRequestPaths);
|
this.alternativeTokenRequestPaths);
|
||||||
|
|
||||||
if (this.moodlePluginCheck.checkPluginAvailable(lmsSetup)) {
|
if (this.moodlePluginCheck.checkPluginAvailable(restTemplateFactory)) {
|
||||||
|
|
||||||
final MoodlePluginCourseAccess moodlePluginCourseAccess = new MoodlePluginCourseAccess(
|
final MoodlePluginCourseAccess moodlePluginCourseAccess = new MoodlePluginCourseAccess(
|
||||||
this.jsonMapper,
|
this.jsonMapper,
|
||||||
moodleRestTemplateFactory,
|
this.asyncService,
|
||||||
this.cacheManager);
|
restTemplateFactory,
|
||||||
final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction();
|
this.cacheManager,
|
||||||
|
this.environment);
|
||||||
|
|
||||||
|
final MoodlePluginCourseRestriction moodlePluginCourseRestriction = new MoodlePluginCourseRestriction(
|
||||||
|
this.jsonMapper,
|
||||||
|
restTemplateFactory,
|
||||||
|
this.examConfigurationValueService);
|
||||||
|
|
||||||
return new LmsAPITemplateAdapter(
|
return new LmsAPITemplateAdapter(
|
||||||
this.asyncService,
|
this.asyncService,
|
||||||
|
@ -115,20 +126,16 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
||||||
final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess(
|
final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess(
|
||||||
this.jsonMapper,
|
this.jsonMapper,
|
||||||
this.asyncService,
|
this.asyncService,
|
||||||
moodleRestTemplateFactory,
|
restTemplateFactory,
|
||||||
asyncLoaderPrototype,
|
asyncLoaderPrototype,
|
||||||
this.environment);
|
this.environment);
|
||||||
|
|
||||||
final MoodleCourseRestriction moodleCourseRestriction = new MoodleCourseRestriction(
|
|
||||||
this.jsonMapper,
|
|
||||||
moodleRestTemplateFactory);
|
|
||||||
|
|
||||||
return new LmsAPITemplateAdapter(
|
return new LmsAPITemplateAdapter(
|
||||||
this.asyncService,
|
this.asyncService,
|
||||||
this.environment,
|
this.environment,
|
||||||
apiTemplateDataSupplier,
|
apiTemplateDataSupplier,
|
||||||
moodleCourseAccess,
|
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;
|
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.cache.CacheManager;
|
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.core.JsonParseException;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
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.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.Chapters;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
|
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.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.AbstractCachedCourseAccess;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate;
|
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.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 {
|
public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess implements CourseAccessAPI {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(MoodlePluginCourseAccess.class);
|
private static final Logger log = LoggerFactory.getLogger(MoodlePluginCourseAccess.class);
|
||||||
|
|
||||||
static final String COURSES_API_FUNCTION_NAME = "local_sebserver_get_courses";
|
public static final String MOODLE_QUIZ_START_URL_PATH = "mod/quiz/view.php?id=";
|
||||||
static final String QUIZZES_BY_COURSES_API_FUNCTION_NAME = "local_sebserver_get_quizzes_by_courses";
|
public static final String COURSES_API_FUNCTION_NAME = "quizaccess_sebserver_get_courses";
|
||||||
static final String USERS_API_FUNCTION_NAME = "local_sebserver_get_users";
|
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";
|
public static final String ATTR_FIELD = "field";
|
||||||
static final String CRITERIA_TO_DATE = "to_date";
|
public static final String CRITERIA_COURSE_IDS = "ids";
|
||||||
static final String CRITERIA_LIMIT_FROM = "limitfrom";
|
public static final String CRITERIA_FROM_DATE = "from_date";
|
||||||
static final String CRITERIA_LIMIT_NUM = "limitnum";
|
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 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;
|
private MoodleAPIRestTemplate restTemplate;
|
||||||
|
|
||||||
public MoodlePluginCourseAccess(
|
public MoodlePluginCourseAccess(
|
||||||
final JSONMapper jsonMapper,
|
final JSONMapper jsonMapper,
|
||||||
final MoodleRestTemplateFactory moodleRestTemplateFactory,
|
final AsyncService asyncService,
|
||||||
final CacheManager cacheManager) {
|
final MoodleRestTemplateFactory restTemplateFactory,
|
||||||
|
final CacheManager cacheManager,
|
||||||
|
final Environment environment) {
|
||||||
|
|
||||||
super(cacheManager);
|
super(cacheManager);
|
||||||
this.jsonMapper = jsonMapper;
|
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
|
@Override
|
||||||
public LmsSetupTestResult testCourseAccessAPI() {
|
public LmsSetupTestResult testCourseAccessAPI() {
|
||||||
final LmsSetupTestResult attributesCheck = this.moodleRestTemplateFactory.test();
|
final LmsSetupTestResult attributesCheck = this.restTemplateFactory.test();
|
||||||
if (!attributesCheck.isOk()) {
|
if (!attributesCheck.isOk()) {
|
||||||
return attributesCheck;
|
return attributesCheck;
|
||||||
}
|
}
|
||||||
|
@ -73,78 +136,417 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
|
||||||
final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate();
|
final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate();
|
||||||
if (restTemplateRequest.hasError()) {
|
if (restTemplateRequest.hasError()) {
|
||||||
final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " +
|
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());
|
log.error(message + " cause: {}", restTemplateRequest.getError().getMessage());
|
||||||
return LmsSetupTestResult.ofTokenRequestError(LmsType.MOODLE_PLUGIN, message);
|
return LmsSetupTestResult.ofTokenRequestError(LmsType.MOODLE_PLUGIN, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
final MoodleAPIRestTemplate restTemplate = restTemplateRequest.get();
|
final MoodleAPIRestTemplate restTemplate = restTemplateRequest.get();
|
||||||
|
|
||||||
// try {
|
try {
|
||||||
// restTemplate.testAPIConnection(
|
|
||||||
// COURSES_API_FUNCTION_NAME,
|
restTemplate.testAPIConnection(
|
||||||
// QUIZZES_BY_COURSES_API_FUNCTION_NAME,
|
COURSES_API_FUNCTION_NAME,
|
||||||
// USERS_API_FUNCTION_NAME);
|
QUIZZES_BY_COURSES_API_FUNCTION_NAME,
|
||||||
// } catch (final RuntimeException e) {
|
USERS_API_FUNCTION_NAME);
|
||||||
// log.error("Failed to access Moodle course API: ", e);
|
|
||||||
// return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.MOODLE_PLUGIN, e.getMessage());
|
} 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);
|
return LmsSetupTestResult.ofOkay(LmsType.MOODLE_PLUGIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
|
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
|
||||||
System.out.println("***************** filterMap: " + filterMap);
|
return Result.ofError(new UnsupportedOperationException());
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void fetchQuizzes(final FilterMap filterMap, final AsyncQuizFetchBuffer asyncQuizFetchBuffer) {
|
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
|
@Override
|
||||||
public Result<QuizData> getQuiz(final String id) {
|
public Result<QuizData> getQuiz(final String id) {
|
||||||
// TODO Auto-generated method stub
|
return Result.tryCatch(() -> {
|
||||||
return null;
|
|
||||||
|
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
|
@Override
|
||||||
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
|
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
|
||||||
// TODO Auto-generated method stub
|
return Result.tryCatch(() -> {
|
||||||
return null;
|
|
||||||
|
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
|
@Override
|
||||||
public String getExamineeName(final String examineeUserId) {
|
public String getExamineeName(final String examineeUserId) {
|
||||||
// TODO Auto-generated method stub
|
return getExamineeAccountDetails(examineeUserId)
|
||||||
return null;
|
.map(ExamineeAccountDetails::getDisplayName)
|
||||||
|
.onError(error -> log.warn("Failed to request user-name for ID: {}", error.getMessage(), error))
|
||||||
|
.getOr(examineeUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<Chapters> getCourseChapters(final String courseId) {
|
public Result<Chapters> getCourseChapters(final String courseId) {
|
||||||
// TODO Auto-generated method stub
|
return Result.of(new Chapters(Collections.emptyList()));
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private String getLmsSetupName() {
|
||||||
protected Long getLmsSetupId() {
|
return this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup().name;
|
||||||
// TODO Auto-generated method stub
|
}
|
||||||
return null;
|
|
||||||
|
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() {
|
private Result<MoodleAPIRestTemplate> getRestTemplate() {
|
||||||
if (this.restTemplate == null) {
|
if (this.restTemplate == null) {
|
||||||
final Result<MoodleAPIRestTemplate> templateRequest = this.moodleRestTemplateFactory
|
final Result<MoodleAPIRestTemplate> templateRequest = this.restTemplateFactory
|
||||||
.createRestTemplate();
|
.createRestTemplate();
|
||||||
if (templateRequest.hasError()) {
|
if (templateRequest.hasError()) {
|
||||||
return templateRequest;
|
return templateRequest;
|
||||||
|
@ -156,99 +558,4 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
|
||||||
return Result.of(this.restTemplate);
|
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;
|
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.Exam;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
|
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.LmsSetup.LmsType;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
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.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 {
|
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
|
@Override
|
||||||
public LmsSetupTestResult testCourseRestrictionAPI() {
|
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);
|
return LmsSetupTestResult.ofOkay(LmsType.MOODLE_PLUGIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
|
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
|
||||||
// TODO Auto-generated method stub
|
return getRestTemplate().map(restTemplate -> {
|
||||||
return null;
|
|
||||||
|
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
|
@Override
|
||||||
|
@ -34,14 +125,113 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
|
||||||
final Exam exam,
|
final Exam exam,
|
||||||
final SEBRestriction sebRestrictionData) {
|
final SEBRestriction sebRestrictionData) {
|
||||||
|
|
||||||
// TODO Auto-generated method stub
|
return Result.tryCatch(() -> {
|
||||||
return null;
|
|
||||||
|
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
|
@Override
|
||||||
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
|
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
|
||||||
// TODO Auto-generated method stub
|
return Result.tryCatch(() -> {
|
||||||
return null;
|
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.model.institution.LmsSetup.LmsType;
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
|
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.LmsAPITemplate;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
|
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.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;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
|
@ -41,6 +44,7 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
|
||||||
private final AsyncService asyncService;
|
private final AsyncService asyncService;
|
||||||
private final Environment environment;
|
private final Environment environment;
|
||||||
private final ClientCredentialService clientCredentialService;
|
private final ClientCredentialService clientCredentialService;
|
||||||
|
private final ExamConfigurationValueService examConfigurationValueService;
|
||||||
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
|
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
|
||||||
private final ApplicationContext applicationContext;
|
private final ApplicationContext applicationContext;
|
||||||
private final String[] alternativeTokenRequestPaths;
|
private final String[] alternativeTokenRequestPaths;
|
||||||
|
@ -52,6 +56,7 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
|
||||||
final AsyncService asyncService,
|
final AsyncService asyncService,
|
||||||
final Environment environment,
|
final Environment environment,
|
||||||
final ClientCredentialService clientCredentialService,
|
final ClientCredentialService clientCredentialService,
|
||||||
|
final ExamConfigurationValueService examConfigurationValueService,
|
||||||
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
|
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
|
||||||
final ApplicationContext applicationContext,
|
final ApplicationContext applicationContext,
|
||||||
@Value("${sebserver.webservice.lms.moodle.api.token.request.paths:}") final String alternativeTokenRequestPaths) {
|
@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.asyncService = asyncService;
|
||||||
this.environment = environment;
|
this.environment = environment;
|
||||||
this.clientCredentialService = clientCredentialService;
|
this.clientCredentialService = clientCredentialService;
|
||||||
|
this.examConfigurationValueService = examConfigurationValueService;
|
||||||
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
|
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
|
||||||
this.applicationContext = applicationContext;
|
this.applicationContext = applicationContext;
|
||||||
this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
|
this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
|
||||||
|
@ -78,19 +84,27 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory
|
||||||
public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
|
public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
|
|
||||||
final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory(
|
// final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactoryImpl(
|
||||||
this.jsonMapper,
|
// this.jsonMapper,
|
||||||
apiTemplateDataSupplier,
|
// apiTemplateDataSupplier,
|
||||||
this.clientCredentialService,
|
// this.clientCredentialService,
|
||||||
this.clientHttpRequestFactoryService,
|
// this.clientHttpRequestFactoryService,
|
||||||
this.alternativeTokenRequestPaths);
|
// this.alternativeTokenRequestPaths);
|
||||||
|
|
||||||
|
final MoodleRestTemplateFactory moodleRestTemplateFactory =
|
||||||
|
new MockupRestTemplateFactory(apiTemplateDataSupplier);
|
||||||
|
|
||||||
final MoodlePluginCourseAccess moodlePluginCourseAccess = new MoodlePluginCourseAccess(
|
final MoodlePluginCourseAccess moodlePluginCourseAccess = new MoodlePluginCourseAccess(
|
||||||
this.jsonMapper,
|
this.jsonMapper,
|
||||||
|
this.asyncService,
|
||||||
moodleRestTemplateFactory,
|
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(
|
return new LmsAPITemplateAdapter(
|
||||||
this.asyncService,
|
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.LmsSetup.LmsType;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
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.Result;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
|
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_LINK = "ADDITIONAL_ATTR_QUIT_LINK";
|
||||||
private static final String ADDITIONAL_ATTR_QUIT_SECRET = "ADDITIONAL_ATTR_QUIT_SECRET";
|
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 ClientHttpRequestFactoryService clientHttpRequestFactoryService;
|
||||||
private final ClientCredentialService clientCredentialService;
|
private final ClientCredentialService clientCredentialService;
|
||||||
private final APITemplateDataSupplier apiTemplateDataSupplier;
|
private final APITemplateDataSupplier apiTemplateDataSupplier;
|
||||||
private final ExamConfigurationValueService examConfigurationValueService;
|
private final ExamConfigurationValueService examConfigurationValueService;
|
||||||
private final Cryptor cryptor;
|
|
||||||
private final Long lmsSetupId;
|
private final Long lmsSetupId;
|
||||||
|
|
||||||
private OlatLmsRestTemplate cachedRestTemplate;
|
private OlatLmsRestTemplate cachedRestTemplate;
|
||||||
|
@ -85,7 +80,6 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
|
||||||
final ClientCredentialService clientCredentialService,
|
final ClientCredentialService clientCredentialService,
|
||||||
final APITemplateDataSupplier apiTemplateDataSupplier,
|
final APITemplateDataSupplier apiTemplateDataSupplier,
|
||||||
final ExamConfigurationValueService examConfigurationValueService,
|
final ExamConfigurationValueService examConfigurationValueService,
|
||||||
final Cryptor cryptor,
|
|
||||||
final CacheManager cacheManager) {
|
final CacheManager cacheManager) {
|
||||||
|
|
||||||
super(cacheManager);
|
super(cacheManager);
|
||||||
|
@ -94,7 +88,6 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
|
||||||
this.clientCredentialService = clientCredentialService;
|
this.clientCredentialService = clientCredentialService;
|
||||||
this.apiTemplateDataSupplier = apiTemplateDataSupplier;
|
this.apiTemplateDataSupplier = apiTemplateDataSupplier;
|
||||||
this.examConfigurationValueService = examConfigurationValueService;
|
this.examConfigurationValueService = examConfigurationValueService;
|
||||||
this.cryptor = cryptor;
|
|
||||||
this.lmsSetupId = apiTemplateDataSupplier.getLmsSetup().id;
|
this.lmsSetupId = apiTemplateDataSupplier.getLmsSetup().id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -357,8 +350,8 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
|
||||||
final RestrictionDataPost post = new RestrictionDataPost();
|
final RestrictionDataPost post = new RestrictionDataPost();
|
||||||
post.browserExamKeys = new ArrayList<>(restriction.browserExamKeys);
|
post.browserExamKeys = new ArrayList<>(restriction.browserExamKeys);
|
||||||
post.configKeys = new ArrayList<>(restriction.configKeys);
|
post.configKeys = new ArrayList<>(restriction.configKeys);
|
||||||
post.quitLink = this.getQuitLink(restriction.examId);
|
post.quitLink = this.examConfigurationValueService.getQuitLink(restriction.examId);
|
||||||
post.quitSecret = this.getQuitSecret(restriction.examId);
|
post.quitSecret = this.examConfigurationValueService.getQuitSecret(restriction.examId);
|
||||||
final RestrictionData r =
|
final RestrictionData r =
|
||||||
this.apiPost(restTemplate, url, post, RestrictionDataPost.class, RestrictionData.class);
|
this.apiPost(restTemplate, url, post, RestrictionDataPost.class, RestrictionData.class);
|
||||||
return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, new HashMap<String, String>());
|
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.client.ClientCredentialService;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
|
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
|
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.APITemplateDataSupplier;
|
||||||
|
@ -44,7 +43,6 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
||||||
private final AsyncService asyncService;
|
private final AsyncService asyncService;
|
||||||
private final Environment environment;
|
private final Environment environment;
|
||||||
private final CacheManager cacheManager;
|
private final CacheManager cacheManager;
|
||||||
private final Cryptor cryptor;
|
|
||||||
|
|
||||||
public OlatLmsAPITemplateFactory(
|
public OlatLmsAPITemplateFactory(
|
||||||
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
|
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
|
||||||
|
@ -52,8 +50,7 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
||||||
final ExamConfigurationValueService examConfigurationValueService,
|
final ExamConfigurationValueService examConfigurationValueService,
|
||||||
final AsyncService asyncService,
|
final AsyncService asyncService,
|
||||||
final Environment environment,
|
final Environment environment,
|
||||||
final CacheManager cacheManager,
|
final CacheManager cacheManager) {
|
||||||
final Cryptor cryptor) {
|
|
||||||
|
|
||||||
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
|
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
|
||||||
this.clientCredentialService = clientCredentialService;
|
this.clientCredentialService = clientCredentialService;
|
||||||
|
@ -61,7 +58,6 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
||||||
this.asyncService = asyncService;
|
this.asyncService = asyncService;
|
||||||
this.environment = environment;
|
this.environment = environment;
|
||||||
this.cacheManager = cacheManager;
|
this.cacheManager = cacheManager;
|
||||||
this.cryptor = cryptor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -72,13 +68,14 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
||||||
@Override
|
@Override
|
||||||
public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
|
public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
|
|
||||||
final OlatLmsAPITemplate olatLmsAPITemplate = new OlatLmsAPITemplate(
|
final OlatLmsAPITemplate olatLmsAPITemplate = new OlatLmsAPITemplate(
|
||||||
this.clientHttpRequestFactoryService,
|
this.clientHttpRequestFactoryService,
|
||||||
this.clientCredentialService,
|
this.clientCredentialService,
|
||||||
apiTemplateDataSupplier,
|
apiTemplateDataSupplier,
|
||||||
this.examConfigurationValueService,
|
this.examConfigurationValueService,
|
||||||
this.cryptor,
|
|
||||||
this.cacheManager);
|
this.cacheManager);
|
||||||
|
|
||||||
return new LmsAPITemplateAdapter(
|
return new LmsAPITemplateAdapter(
|
||||||
this.asyncService,
|
this.asyncService,
|
||||||
this.environment,
|
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.LmsAPIService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.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.ExamFinishedEvent;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamResetEvent;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamResetEvent;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent;
|
||||||
|
@ -402,7 +402,7 @@ class ExamUpdateHandler {
|
||||||
log.debug("Found formerName quiz name: {}", exam.name);
|
log.debug("Found formerName quiz name: {}", exam.name);
|
||||||
|
|
||||||
// get the course name identifier
|
// get the course name identifier
|
||||||
final String shortname = MoodleCourseAccess.getShortname(quizId);
|
final String shortname = MoodleUtils.getShortname(quizId);
|
||||||
if (StringUtils.isNotBlank(shortname)) {
|
if (StringUtils.isNotBlank(shortname)) {
|
||||||
|
|
||||||
log.debug("Using short-name: {} for recovering", shortname);
|
log.debug("Using short-name: {} for recovering", shortname);
|
||||||
|
@ -412,7 +412,7 @@ class ExamUpdateHandler {
|
||||||
.getOrThrow()
|
.getOrThrow()
|
||||||
.stream()
|
.stream()
|
||||||
.filter(quiz -> {
|
.filter(quiz -> {
|
||||||
final String qShortName = MoodleCourseAccess.getShortname(quiz.id);
|
final String qShortName = MoodleUtils.getShortname(quiz.id);
|
||||||
return qShortName != null && qShortName.equals(shortname);
|
return qShortName != null && qShortName.equals(shortname);
|
||||||
})
|
})
|
||||||
.filter(quiz -> exam.name.equals(quiz.name))
|
.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.gbl.util.Utils;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO;
|
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.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.ExamSessionService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientSessionService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientSessionService;
|
||||||
|
@ -479,7 +480,7 @@ public class ExamAPI_V1_Controller {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.examSessionService.getRunningExam(examId)
|
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));
|
.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.Exam.ExamType;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
|
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
|
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
|
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.APITemplateDataSupplier;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat.OlatLmsAPITemplate;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat.OlatLmsAPITemplate;
|
||||||
|
@ -42,8 +41,6 @@ public class OlatLmsAPITemplateTest extends AdministrationAPIIntegrationTester {
|
||||||
@Autowired
|
@Autowired
|
||||||
private ExamConfigurationValueService examConfigurationValueService;
|
private ExamConfigurationValueService examConfigurationValueService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private Cryptor cryptor;
|
|
||||||
@Autowired
|
|
||||||
private CacheManager cacheManager;
|
private CacheManager cacheManager;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -59,7 +56,6 @@ public class OlatLmsAPITemplateTest extends AdministrationAPIIntegrationTester {
|
||||||
null,
|
null,
|
||||||
apiTemplateDataSupplier,
|
apiTemplateDataSupplier,
|
||||||
this.examConfigurationValueService,
|
this.examConfigurationValueService,
|
||||||
this.cryptor,
|
|
||||||
this.cacheManager);
|
this.cacheManager);
|
||||||
|
|
||||||
Mockito.when(restTemplateMock.exchange(Mockito.any(), Mockito.any(), Mockito.any(),
|
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.institution.LmsSetupTestResult.ErrorType;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
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.MoodleRestTemplateFactoryImpl;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplateImpl;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactoryImpl.MoodleAPIRestTemplateImpl;
|
||||||
|
|
||||||
public class MoodleCourseAccessTest {
|
public class MoodleCourseAccessTest {
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ public class MoodleCourseAccessTest {
|
||||||
@Test
|
@Test
|
||||||
public void testGetExamineeAccountDetails() {
|
public void testGetExamineeAccountDetails() {
|
||||||
|
|
||||||
final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class);
|
final MoodleRestTemplateFactoryImpl moodleRestTemplateFactory = mock(MoodleRestTemplateFactoryImpl.class);
|
||||||
final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class);
|
final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class);
|
||||||
when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate));
|
when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate));
|
||||||
when(moodleAPIRestTemplate.callMoodleAPIFunction(
|
when(moodleAPIRestTemplate.callMoodleAPIFunction(
|
||||||
|
@ -118,7 +118,7 @@ public class MoodleCourseAccessTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testInitAPIAccessError1() {
|
public void testInitAPIAccessError1() {
|
||||||
final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class);
|
final MoodleRestTemplateFactoryImpl moodleRestTemplateFactory = mock(MoodleRestTemplateFactoryImpl.class);
|
||||||
when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.ofRuntimeError("Error1"));
|
when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.ofRuntimeError("Error1"));
|
||||||
when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE));
|
when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE));
|
||||||
|
|
||||||
|
@ -138,7 +138,7 @@ public class MoodleCourseAccessTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testInitAPIAccessError2() {
|
public void testInitAPIAccessError2() {
|
||||||
final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class);
|
final MoodleRestTemplateFactoryImpl moodleRestTemplateFactory = mock(MoodleRestTemplateFactoryImpl.class);
|
||||||
final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class);
|
final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class);
|
||||||
when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate));
|
when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate));
|
||||||
doThrow(RuntimeException.class).when(moodleAPIRestTemplate).testAPIConnection(any());
|
doThrow(RuntimeException.class).when(moodleAPIRestTemplate).testAPIConnection(any());
|
||||||
|
@ -160,7 +160,7 @@ public class MoodleCourseAccessTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testInitAPIAccessOK() {
|
public void testInitAPIAccessOK() {
|
||||||
final MoodleRestTemplateFactory moodleRestTemplateFactory = mock(MoodleRestTemplateFactory.class);
|
final MoodleRestTemplateFactoryImpl moodleRestTemplateFactory = mock(MoodleRestTemplateFactoryImpl.class);
|
||||||
final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class);
|
final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class);
|
||||||
when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate));
|
when(moodleRestTemplateFactory.createRestTemplate()).thenReturn(Result.of(moodleAPIRestTemplate));
|
||||||
when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE));
|
when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE));
|
||||||
|
|
Loading…
Reference in a new issue