Merge remote-tracking branch 'origin/dev-1.2' into development
This commit is contained in:
commit
03300923c0
8 changed files with 461 additions and 153 deletions
|
@ -61,7 +61,7 @@ public final class LmsSetup implements GrantEntity, Activatable {
|
|||
/** The Moodle binding features only the course access API so far */
|
||||
MOODLE(Features.COURSE_API /* , Features.SEB_RESTRICTION */),
|
||||
/** The Ans Delft binding is on the way */
|
||||
ANS_DELFT(),
|
||||
ANS_DELFT(Features.COURSE_API, Features.SEB_RESTRICTION),
|
||||
/** The OpenOLAT binding is on the way */
|
||||
OPEN_OLAT(Features.COURSE_API, Features.SEB_RESTRICTION);
|
||||
|
||||
|
|
|
@ -357,7 +357,7 @@ public class LmsSetupForm implements TemplateComposer {
|
|||
}
|
||||
|
||||
try {
|
||||
return this.resourceService.lmsTypeResources().get(0)._1;
|
||||
return LmsType.MOCKUP.name();
|
||||
} catch (final Exception e) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -293,14 +293,22 @@ public final class ViewContext {
|
|||
}
|
||||
|
||||
void setValuesToInputFields(final Collection<ConfigurationValue> values) {
|
||||
this.inputFieldMapping
|
||||
.values()
|
||||
.forEach(field -> {
|
||||
final ConfigurationValue initValue = field.initValue(values);
|
||||
if (initValue != null) {
|
||||
this.valueChangeListener.notifyGUI(this, field.getAttribute(), initValue);
|
||||
}
|
||||
});
|
||||
try {
|
||||
this.inputFieldMapping
|
||||
.values()
|
||||
.forEach(field -> {
|
||||
try {
|
||||
final ConfigurationValue initValue = field.initValue(values);
|
||||
if (initValue != null) {
|
||||
this.valueChangeListener.notifyGUI(this, field.getAttribute(), initValue);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to initialize SEB setting: {}", field.getAttribute(), e);
|
||||
}
|
||||
});
|
||||
} catch (final Exception e) {
|
||||
log.error("Unexpected error while initialize SEB settings: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Removes all registered InputFields with the given attribute ids
|
||||
|
|
|
@ -9,20 +9,33 @@
|
|||
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.joda.time.DateTime;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
|
||||
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
|
||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||
|
@ -36,7 +49,6 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
|||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.Features;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
||||
|
@ -47,20 +59,22 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier
|
|||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCourseAccess;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans.AnsLmsData.AccessibilitySettingsData;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans.AnsLmsData.AssignmentData;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans.AnsLmsData.UserData;
|
||||
|
||||
public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements LmsAPITemplate {
|
||||
|
||||
// TODO add needed dependencies here
|
||||
private static final Logger log = LoggerFactory.getLogger(AnsLmsAPITemplate.class);
|
||||
|
||||
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
|
||||
private final ClientCredentialService clientCredentialService;
|
||||
private final APITemplateDataSupplier apiTemplateDataSupplier;
|
||||
private final Long lmsSetupId;
|
||||
|
||||
private AnsPersonalRestTemplate cachedRestTemplate;
|
||||
|
||||
protected AnsLmsAPITemplate(
|
||||
|
||||
// TODO if you need more dependencies inject them here and set the reference
|
||||
|
||||
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
|
||||
final ClientCredentialService clientCredentialService,
|
||||
final APITemplateDataSupplier apiTemplateDataSupplier,
|
||||
|
@ -97,28 +111,19 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
|
|||
if (testLmsSetupSettings.hasAnyError()) {
|
||||
return testLmsSetupSettings;
|
||||
}
|
||||
|
||||
// TODO check if the course API of the remote LMS is available
|
||||
// if not, create corresponding LmsSetupTestResult error
|
||||
try {
|
||||
this.getRestTemplate().get();
|
||||
} catch (final RuntimeException e) {
|
||||
log.error("Failed to access Ans course API: ", e);
|
||||
return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.ANS_DELFT, e.getMessage());
|
||||
}
|
||||
|
||||
return LmsSetupTestResult.ofOkay(LmsType.ANS_DELFT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LmsSetupTestResult testCourseRestrictionAPI() {
|
||||
final LmsSetupTestResult testLmsSetupSettings = testLmsSetupSettings();
|
||||
if (testLmsSetupSettings.hasAnyError()) {
|
||||
return testLmsSetupSettings;
|
||||
}
|
||||
|
||||
if (LmsType.ANS_DELFT.features.contains(Features.SEB_RESTRICTION)) {
|
||||
|
||||
// TODO check if the course API of the remote LMS is available
|
||||
// if not, create corresponding LmsSetupTestResult error
|
||||
|
||||
}
|
||||
|
||||
return LmsSetupTestResult.ofOkay(LmsType.ANS_DELFT);
|
||||
return testCourseAccessAPI();
|
||||
}
|
||||
|
||||
private LmsSetupTestResult testLmsSetupSettings() {
|
||||
|
@ -203,58 +208,126 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
|
|||
return super.protectedQuizRequest(id);
|
||||
}
|
||||
|
||||
private List<QuizData> collectAllQuizzes(final AnsPersonalRestTemplate restTemplate) {
|
||||
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||
final List<QuizData> quizDatas = getAssignments(restTemplate)
|
||||
.stream()
|
||||
.map(a -> quizDataFromAnsData(lmsSetup, a))
|
||||
.collect(Collectors.toList());
|
||||
quizDatas.forEach(q -> super.putToCache(q));
|
||||
return quizDatas;
|
||||
}
|
||||
|
||||
private QuizData getQuizByAssignmentId(final RestTemplate restTemplate, final String id) {
|
||||
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||
final AssignmentData a = getAssignmentById(restTemplate, id);
|
||||
return quizDataFromAnsData(lmsSetup, a);
|
||||
}
|
||||
|
||||
private QuizData quizDataFromAnsData(final LmsSetup lmsSetup, final AssignmentData a) {
|
||||
// In Ans, one assignment can have multiple timeslots, but the SEB restriciton
|
||||
// is done at the assignment level, so timeslots don't really matter.
|
||||
// An assignment's start_at and end_at dates indicate when the first timeslot starts
|
||||
// and the last timeslot ends. If these are null, there are no timeslots, yet.
|
||||
// In that case, we create a placeholder timeslot to display in SEB server.
|
||||
if (a.start_at == null) {
|
||||
a.start_at = java.time.Instant.now().plus(365, java.time.temporal.ChronoUnit.DAYS).toString();
|
||||
a.end_at = java.time.Instant.now().plus(366, java.time.temporal.ChronoUnit.DAYS).toString();
|
||||
}
|
||||
final DateTime startTime = new DateTime(a.start_at);
|
||||
final DateTime endTime = new DateTime(a.end_at);
|
||||
final Map<String, String> attrs = new HashMap<>();
|
||||
attrs.put("assignment_id", String.valueOf(a.id));
|
||||
return new QuizData(
|
||||
String.valueOf(a.id),
|
||||
lmsSetup.getInstitutionId(),
|
||||
lmsSetup.id,
|
||||
lmsSetup.getLmsType(),
|
||||
String.format("%s", a.name),
|
||||
String.format(""),
|
||||
startTime,
|
||||
endTime,
|
||||
a.start_url,
|
||||
attrs);
|
||||
}
|
||||
|
||||
private List<AssignmentData> getAssignments(final RestTemplate restTemplate) {
|
||||
// NOTE: at the moment, seb_server_enabled cannot be set inside the Ans GUI,
|
||||
// only via the API, so we need to list all assignments. Maybe in the future,
|
||||
// we can only list those for which seb server has been enabled in Ans (like in OLAT):
|
||||
//final String url = "/api/v2/search/assignments?query=seb_server_enabled:true";
|
||||
final String url = "/api/v2/search/assignments";
|
||||
return this.apiGetList(restTemplate, url, new ParameterizedTypeReference<List<AssignmentData>>() {
|
||||
});
|
||||
}
|
||||
|
||||
private AssignmentData getAssignmentById(final RestTemplate restTemplate, final String id) {
|
||||
final String url = String.format("/api/v2/assignments/%s", id);
|
||||
return this.apiGet(restTemplate, url, AssignmentData.class);
|
||||
}
|
||||
|
||||
private List<QuizData> getQuizzesByIds(final RestTemplate restTemplate, final Set<String> ids) {
|
||||
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||
return ids.stream().map(id -> {
|
||||
final String url = String.format("/api/v2/assignments/%s", id);
|
||||
return this.apiGet(restTemplate, url, AssignmentData.class);
|
||||
}).map(a -> {
|
||||
final QuizData quizData = quizDataFromAnsData(lmsSetup, a);
|
||||
super.putToCache(quizData);
|
||||
return quizData;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
final String quizName = filterMap.getString(QuizData.FILTER_ATTR_QUIZ_NAME);
|
||||
@SuppressWarnings("unused")
|
||||
final DateTime quizFromTime = (filterMap != null) ? filterMap.getQuizFromTime() : null;
|
||||
|
||||
// We cannot filter by from-date or partial names using the Ans search API.
|
||||
// Only exact matches are permitted. So we're not implementing filtering
|
||||
// on the API level and always retrieve all assignments and let SEB server
|
||||
// do the filtering.
|
||||
return () -> {
|
||||
|
||||
// TODO get all course / quiz data from remote LMS that matches the filter criteria.
|
||||
// put loaded QuizData to the cache: super.putToCache(quizDataCollection);
|
||||
// before returning it.
|
||||
|
||||
throw new RuntimeException("TODO");
|
||||
final List<QuizData> res = getRestTemplate()
|
||||
.map(this::collectAllQuizzes)
|
||||
.getOrThrow();
|
||||
super.putToCache(res);
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Supplier<Collection<QuizData>> quizzesSupplier(final Set<String> ids) {
|
||||
return () -> {
|
||||
|
||||
// TODO get all quiz / course data for specified identifiers from remote LMS
|
||||
// and put it to the cache: super.putToCache(quizDataCollection);
|
||||
// before returning it.
|
||||
|
||||
throw new RuntimeException("TODO");
|
||||
};
|
||||
return () -> getRestTemplate()
|
||||
.map(t -> this.getQuizzesByIds(t, ids))
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Supplier<QuizData> quizSupplier(final String id) {
|
||||
return () -> {
|
||||
return () -> getRestTemplate()
|
||||
.map(t -> this.getQuizByAssignmentId(t, id))
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
// TODO get the specified quiz / course data for specified identifier from remote LMS
|
||||
// and put it to the cache: super.putToCache(quizDataCollection);
|
||||
// before returning it.
|
||||
|
||||
throw new RuntimeException("TODO");
|
||||
};
|
||||
private ExamineeAccountDetails getExamineeById(final RestTemplate restTemplate, final String id) {
|
||||
final String url = String.format("/api/v2/users/%s", id);
|
||||
final UserData u = this.apiGet(restTemplate, url, UserData.class);
|
||||
final Map<String, String> attrs = new HashMap<>();
|
||||
attrs.put("role", u.role);
|
||||
attrs.put("affiliation", u.affiliation);
|
||||
attrs.put("active", u.active ? "yes" : "no");
|
||||
return new ExamineeAccountDetails(
|
||||
String.valueOf(u.id),
|
||||
u.last_name + ", " + u.first_name,
|
||||
u.external_id,
|
||||
u.email_address,
|
||||
attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String examineeSessionId) {
|
||||
|
||||
return () -> {
|
||||
|
||||
// TODO get the examinee's account details by the given examineeSessionId from remote LMS.
|
||||
// Currently only the name is needed to display on monitoring view.
|
||||
|
||||
throw new RuntimeException("TODO");
|
||||
};
|
||||
protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String id) {
|
||||
return () -> getRestTemplate()
|
||||
.map(t -> this.getExamineeById(t, id))
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -266,79 +339,169 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
|
|||
|
||||
@Override
|
||||
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
|
||||
return getRestTemplate()
|
||||
.map(t -> this.getRestrictionForAssignmentId(t, exam.externalId));
|
||||
}
|
||||
|
||||
private SEBRestriction getRestrictionForAssignmentId(final RestTemplate restTemplate, final String id) {
|
||||
final String url = String.format("/api/v2/assignments/%s", id);
|
||||
final AssignmentData assignment = this.apiGet(restTemplate, url, AssignmentData.class);
|
||||
final AccessibilitySettingsData ts = assignment.accessibility_settings;
|
||||
return new SEBRestriction(Long.valueOf(id), ts.config_keys, null, new HashMap<String, String>());
|
||||
}
|
||||
|
||||
private SEBRestriction setRestrictionForAssignmentId(final RestTemplate restTemplate, final String id,
|
||||
final SEBRestriction restriction) {
|
||||
final String url = String.format("/api/v2/assignments/%s", id);
|
||||
final AssignmentData assignment = getAssignmentById(restTemplate, id);
|
||||
assignment.accessibility_settings.config_keys = new ArrayList<>(restriction.configKeys);
|
||||
assignment.accessibility_settings.seb_server_enabled = true;
|
||||
@SuppressWarnings("unused")
|
||||
final String quizId = exam.externalId;
|
||||
final AssignmentData r =
|
||||
this.apiPatch(restTemplate, url, assignment, AssignmentData.class, AssignmentData.class);
|
||||
final AccessibilitySettingsData ts = assignment.accessibility_settings;
|
||||
return new SEBRestriction(Long.valueOf(id), ts.config_keys, null, new HashMap<String, String>());
|
||||
}
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
// TODO get the SEB client restrictions that are currently set on the remote LMS for
|
||||
// the given quiz / course derived from the given exam
|
||||
|
||||
throw new RuntimeException("TODO");
|
||||
|
||||
});
|
||||
private SEBRestriction deleteRestrictionForAssignmentId(final RestTemplate restTemplate, final String id) {
|
||||
final String url = String.format("/api/v2/assignments/%s", id);
|
||||
final AssignmentData assignment = getAssignmentById(restTemplate, id);
|
||||
assignment.accessibility_settings.config_keys = null;
|
||||
assignment.accessibility_settings.seb_server_enabled = false;
|
||||
@SuppressWarnings("unused")
|
||||
final AssignmentData r =
|
||||
this.apiPatch(restTemplate, url, assignment, AssignmentData.class, AssignmentData.class);
|
||||
final AccessibilitySettingsData ts = assignment.accessibility_settings;
|
||||
return new SEBRestriction(Long.valueOf(id), ts.config_keys, null, new HashMap<String, String>());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<SEBRestriction> applySEBClientRestriction(
|
||||
final String externalExamId,
|
||||
final SEBRestriction sebRestrictionData) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
// TODO apply the given sebRestrictionData settings as current SEB client restriction setting
|
||||
// to the remote LMS for the given quiz / course.
|
||||
// Mainly SEBRestriction.configKeys and SEBRestriction.browserExamKeys
|
||||
|
||||
throw new RuntimeException("TODO");
|
||||
|
||||
});
|
||||
return getRestTemplate()
|
||||
.map(t -> this.setRestrictionForAssignmentId(t, externalExamId, sebRestrictionData));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
|
||||
@SuppressWarnings("unused")
|
||||
final String quizId = exam.externalId;
|
||||
return getRestTemplate()
|
||||
.map(t -> this.deleteRestrictionForAssignmentId(t, exam.externalId))
|
||||
.map(x -> exam);
|
||||
}
|
||||
|
||||
private enum LinkRel {
|
||||
FIRST, LAST, PREV, NEXT
|
||||
}
|
||||
|
||||
private class PageLink {
|
||||
public final String link;
|
||||
public final LinkRel rel;
|
||||
|
||||
public PageLink(final String l, final LinkRel r) {
|
||||
this.link = l;
|
||||
this.rel = r;
|
||||
}
|
||||
}
|
||||
|
||||
private List<PageLink> parseLinks(final String header) {
|
||||
// Extracts the individual links from a header that looks like this:
|
||||
// <https://staging.ans.app/api/v2/search/assignments?query=seb_server_enabled%3Atrue&page=1&items=20>; rel="first",<https://staging.ans.app/api/v2/search/assignments?query=seb_server_enabled%3Atrue&page=1&items=20>; rel="last"
|
||||
final Stream<String> links = Arrays.stream(header.split(","));
|
||||
return links
|
||||
.map(s -> {
|
||||
final String[] pair = s.split(";");
|
||||
final String link = pair[0].trim().substring(1).replaceFirst(".$", ""); // remove < >
|
||||
final String relName = pair[1].trim().substring(5).replaceFirst(".$", ""); // remove rel=" "
|
||||
return new PageLink(link, LinkRel.valueOf(relName.toUpperCase(Locale.ROOT)));
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private Optional<PageLink> getNextLink(final List<PageLink> links) {
|
||||
return links.stream().filter(l -> l.rel == LinkRel.NEXT).findFirst();
|
||||
}
|
||||
|
||||
private <T> List<T> apiGetList(final RestTemplate restTemplate, final String url,
|
||||
final ParameterizedTypeReference<List<T>> type) {
|
||||
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||
return apiGetListPages(restTemplate, lmsSetup.lmsApiUrl + url, type);
|
||||
}
|
||||
|
||||
private <T> List<T> apiGetListPages(final RestTemplate restTemplate, final String link,
|
||||
final ParameterizedTypeReference<List<T>> type) {
|
||||
// unlike the other api methods, this one takes an explicit link
|
||||
// instead of prepending lmsSetup.lmsApiUrl. This is done because Ans
|
||||
// provides absolute links for pagination. This method calls itself
|
||||
// recursively to retrieve multiple pages.
|
||||
final HttpHeaders requestHeaders = new HttpHeaders();
|
||||
requestHeaders.set("accept", "application/json");
|
||||
final ResponseEntity<List<T>> response = restTemplate.exchange(
|
||||
link,
|
||||
HttpMethod.GET,
|
||||
new HttpEntity<>(requestHeaders),
|
||||
type);
|
||||
final List<T> page = response.getBody();
|
||||
final HttpHeaders responseHeaders = response.getHeaders();
|
||||
final List<PageLink> links = parseLinks(responseHeaders.getFirst("link"));
|
||||
final List<T> nextPage = getNextLink(links).map(l -> {
|
||||
return apiGetListPages(restTemplate, l.link, type);
|
||||
}).orElse(new ArrayList<T>());
|
||||
page.addAll(nextPage);
|
||||
return page;
|
||||
}
|
||||
|
||||
private <T> T apiGet(final RestTemplate restTemplate, final String url, final Class<T> type) {
|
||||
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||
final HttpHeaders requestHeaders = new HttpHeaders();
|
||||
requestHeaders.set("accept", "application/json");
|
||||
final ResponseEntity<T> res = restTemplate.exchange(
|
||||
lmsSetup.lmsApiUrl + url,
|
||||
HttpMethod.GET,
|
||||
new HttpEntity<>(requestHeaders),
|
||||
type);
|
||||
return res.getBody();
|
||||
}
|
||||
|
||||
private <P, R> R apiPatch(final RestTemplate restTemplate, final String url, final P patch,
|
||||
final Class<P> patchType, final Class<R> responseType) {
|
||||
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||
final HttpHeaders requestHeaders = new HttpHeaders();
|
||||
requestHeaders.set("content-type", "application/json");
|
||||
final HttpEntity<P> requestEntity = new HttpEntity<>(patch, requestHeaders);
|
||||
final ResponseEntity<R> res = restTemplate.exchange(
|
||||
lmsSetup.lmsApiUrl + url,
|
||||
HttpMethod.PATCH,
|
||||
requestEntity,
|
||||
responseType);
|
||||
return res.getBody();
|
||||
}
|
||||
|
||||
private Result<AnsPersonalRestTemplate> getRestTemplate() {
|
||||
return Result.tryCatch(() -> {
|
||||
if (this.cachedRestTemplate != null) {
|
||||
return this.cachedRestTemplate;
|
||||
}
|
||||
|
||||
// TODO Release respectively delete all SEB client restrictions for the given
|
||||
// course / quize on the remote LMS.
|
||||
final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials();
|
||||
final ProxyData proxyData = this.apiTemplateDataSupplier.getProxyData();
|
||||
final CharSequence plainClientSecret = this.clientCredentialService
|
||||
.getPlainClientSecret(credentials)
|
||||
.getOrThrow();
|
||||
|
||||
throw new RuntimeException("TODO");
|
||||
final ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
|
||||
details.setClientSecret(plainClientSecret.toString());
|
||||
|
||||
final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService
|
||||
.getClientHttpRequestFactory(proxyData)
|
||||
.getOrThrow();
|
||||
|
||||
final AnsPersonalRestTemplate template = new AnsPersonalRestTemplate(details);
|
||||
template.setRequestFactory(clientHttpRequestFactory);
|
||||
|
||||
this.cachedRestTemplate = template;
|
||||
return this.cachedRestTemplate;
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: This is an example of how to create a RestTemplate for the service to access the LMS API
|
||||
// The example deals with a Http based API that is secured by an OAuth2 client-credential flow.
|
||||
// You might need some different template, then you have to adapt this code
|
||||
// To your needs.
|
||||
@SuppressWarnings("unused")
|
||||
private OAuth2RestTemplate createRestTemplate(final String accessTokenRequestPath) {
|
||||
|
||||
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||
final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials();
|
||||
final ProxyData proxyData = this.apiTemplateDataSupplier.getProxyData();
|
||||
|
||||
final CharSequence plainClientId = credentials.clientId;
|
||||
final CharSequence plainClientSecret = this.clientCredentialService
|
||||
.getPlainClientSecret(credentials)
|
||||
.getOrThrow();
|
||||
|
||||
final ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
|
||||
details.setAccessTokenUri(lmsSetup.lmsApiUrl + accessTokenRequestPath);
|
||||
details.setClientId(plainClientId.toString());
|
||||
details.setClientSecret(plainClientSecret.toString());
|
||||
|
||||
final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService
|
||||
.getClientHttpRequestFactory(proxyData)
|
||||
.getOrThrow();
|
||||
|
||||
final OAuth2RestTemplate template = new OAuth2RestTemplate(details);
|
||||
template.setRequestFactory(clientHttpRequestFactory);
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.ans;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
|
||||
public final class AnsLmsData {
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
static final class AccessibilitySettingsData {
|
||||
/* Ans API example: see nested in AssignmentData */
|
||||
public boolean seb_server_enabled;
|
||||
public List<String> config_keys;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
static final class AssignmentData {
|
||||
/*
|
||||
* Ans API example:
|
||||
* {
|
||||
* "id": 78711,
|
||||
* "course_id": 44412,
|
||||
* "name": "Digital test demo",
|
||||
* "summative": false,
|
||||
* "assignment_type": "Quiz",
|
||||
* "start_at": "2021-08-18T09:00:00.000+02:00",
|
||||
* "end_at": "2021-08-18T12:00:00.000+02:00",
|
||||
* "created_at": "2021-06-21T12:24:28.538+02:00",
|
||||
* "updated_at": "2021-08-17T03:41:56.747+02:00",
|
||||
* "trashed": false,
|
||||
* "start_url": "https://staging.ans.app/digital_test/assignments/78805/results/new",
|
||||
* "accessibility_settings": {
|
||||
* "attempts": 1,
|
||||
* "restricted_access_to_other_pages": false,
|
||||
* "notes": false,
|
||||
* "spellchecker": false,
|
||||
* "feedback": false,
|
||||
* "forced_test_navigation": false,
|
||||
* "cannot_reopen_question_groups": false,
|
||||
* "seb_server_enabled": true,
|
||||
* "config_keys": [
|
||||
* "9dd14ac828617116a1230c71b9a1aa9e06f43b32d9fa7db67f4fa113a6896e83e"
|
||||
* ]
|
||||
* },
|
||||
* "grades_settings": {
|
||||
* "grade_calculation": "formula",
|
||||
* "grade_formula": "1 + 9 * points / total",
|
||||
* "rounding": "decimal",
|
||||
* "grade_lower_bound": true,
|
||||
* "grade_lower_limit": "1.0",
|
||||
* "grade_upper_bound": true,
|
||||
* "grade_upper_limit": "10.0",
|
||||
* "guess_correction": false,
|
||||
* "passed_grade": "5.5"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
public long id;
|
||||
public long course_id;
|
||||
public String name;
|
||||
public String external_id;
|
||||
public String start_at;
|
||||
public String end_at;
|
||||
public String start_url;
|
||||
public AccessibilitySettingsData accessibility_settings;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
static final class UserData {
|
||||
/*
|
||||
* Ans API example:
|
||||
* {
|
||||
* "id": 726404,
|
||||
* "student_number": null,
|
||||
* "first_name": "John",
|
||||
* "middle_name": null,
|
||||
* "last_name": "Doe",
|
||||
* "external_id": null,
|
||||
* "created_at": "2021-06-21T12:07:11.668+02:00",
|
||||
* "updated_at": "2021-07-26T20:16:01.638+02:00",
|
||||
* "active": true,
|
||||
* "email_address": "person@example.org",
|
||||
* "affiliation": "employee",
|
||||
* "role": "owner"
|
||||
* }
|
||||
*/
|
||||
public long id;
|
||||
public String first_name;
|
||||
public String last_name;
|
||||
public String email_address;
|
||||
public String external_id;
|
||||
public String role;
|
||||
public String affiliation;
|
||||
public boolean active;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.ans;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
public class AnsPersonalRestTemplate extends RestTemplate {
|
||||
private static final Logger log = LoggerFactory.getLogger(AnsPersonalRestTemplate.class);
|
||||
public String token;
|
||||
|
||||
public AnsPersonalRestTemplate(final ClientCredentialsResourceDetails details) {
|
||||
super();
|
||||
this.token = details.getClientSecret();
|
||||
this.getInterceptors().add(new ClientHttpRequestInterceptor() {
|
||||
@Override
|
||||
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
|
||||
final ClientHttpRequestExecution execution) throws IOException {
|
||||
|
||||
request.getHeaders().set("Authorization", "Bearer " + AnsPersonalRestTemplate.this.token);
|
||||
//log.debug("Matching curl: curl -X GET {} -H 'accept: application/json' -H 'Authorization: Bearer {}'", request.getURI(), token);
|
||||
final ClientHttpResponse response = execution.execute(request, body);
|
||||
log.debug("Response Headers : {}", response.getHeaders());
|
||||
return response;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,17 +1,17 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8' />
|
||||
<link type='text/css' rel='stylesheet' href='https://source.zoom.us/1.9.0/css/bootstrap.css' />
|
||||
<link type='text/css' rel='stylesheet' href='https://source.zoom.us/1.9.0/css/react-select.css' />
|
||||
<link type='text/css' rel='stylesheet' href='https://source.zoom.us/1.9.1/css/bootstrap.css' />
|
||||
<link type='text/css' rel='stylesheet' href='https://source.zoom.us/1.9.1/css/react-select.css' />
|
||||
</head>
|
||||
<body>
|
||||
<script src='https://source.zoom.us/1.9.0/lib/vendor/react.min.js'></script>
|
||||
<script src='https://source.zoom.us/1.9.0/lib/vendor/react-dom.min.js'></script>
|
||||
<script src='https://source.zoom.us/1.9.0/lib/vendor/redux.min.js'></script>
|
||||
<script src='https://source.zoom.us/1.9.0/lib/vendor/redux-thunk.min.js'></script>
|
||||
<script src='https://source.zoom.us/1.9.0/lib/vendor/jquery.min.js'></script>
|
||||
<script src='https://source.zoom.us/1.9.0/lib/vendor/lodash.min.js'></script>
|
||||
<script src='https://source.zoom.us/zoom-meeting-1.9.0.min.js'></script>
|
||||
<script src='https://source.zoom.us/1.9.1/lib/vendor/react.min.js'></script>
|
||||
<script src='https://source.zoom.us/1.9.1/lib/vendor/react-dom.min.js'></script>
|
||||
<script src='https://source.zoom.us/1.9.1/lib/vendor/redux.min.js'></script>
|
||||
<script src='https://source.zoom.us/1.9.1/lib/vendor/redux-thunk.min.js'></script>
|
||||
<script src='https://source.zoom.us/1.9.1/lib/vendor/jquery.min.js'></script>
|
||||
<script src='https://source.zoom.us/1.9.1/lib/vendor/lodash.min.js'></script>
|
||||
<script src='https://source.zoom.us/zoom-meeting-1.9.1.min.js'></script>
|
||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9/crypto-js.min.js'></script>
|
||||
<script type='text/javascript'>
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
|||
console.log(JSON.stringify(ZoomMtg.checkSystemRequirements()));
|
||||
|
||||
console.log('Initializing Zoom...');
|
||||
ZoomMtg.setZoomJSLib('https://source.zoom.us/1.9.0/lib', '/av');
|
||||
ZoomMtg.setZoomJSLib('https://source.zoom.us/1.9.1/lib', '/av');
|
||||
ZoomMtg.preLoadWasm();
|
||||
ZoomMtg.prepareJssdk();
|
||||
|
||||
|
@ -89,11 +89,6 @@
|
|||
passWord: config.passWord,
|
||||
success(res) {
|
||||
console.log('JOIN SUCCESS')
|
||||
ZoomMtg.getAttendeeslist({
|
||||
success: function (res) {
|
||||
console.log(res, "get getAttendeeslist");
|
||||
}
|
||||
});
|
||||
},
|
||||
error(res) {
|
||||
console.warn('JOIN ERROR')
|
||||
|
|
|
@ -70,17 +70,17 @@ public class ZoomWindowScriptResolverTest {
|
|||
"<html>\r\n"
|
||||
+ " <head>\r\n"
|
||||
+ " <meta charset='utf-8' />\r\n"
|
||||
+ " <link type='text/css' rel='stylesheet' href='https://source.zoom.us/1.9.0/css/bootstrap.css' />\r\n"
|
||||
+ " <link type='text/css' rel='stylesheet' href='https://source.zoom.us/1.9.0/css/react-select.css' />\r\n"
|
||||
+ " <link type='text/css' rel='stylesheet' href='https://source.zoom.us/1.9.1/css/bootstrap.css' />\r\n"
|
||||
+ " <link type='text/css' rel='stylesheet' href='https://source.zoom.us/1.9.1/css/react-select.css' />\r\n"
|
||||
+ " </head>\r\n"
|
||||
+ " <body>\r\n"
|
||||
+ " <script src='https://source.zoom.us/1.9.0/lib/vendor/react.min.js'></script>\r\n"
|
||||
+ " <script src='https://source.zoom.us/1.9.0/lib/vendor/react-dom.min.js'></script>\r\n"
|
||||
+ " <script src='https://source.zoom.us/1.9.0/lib/vendor/redux.min.js'></script>\r\n"
|
||||
+ " <script src='https://source.zoom.us/1.9.0/lib/vendor/redux-thunk.min.js'></script>\r\n"
|
||||
+ " <script src='https://source.zoom.us/1.9.0/lib/vendor/jquery.min.js'></script>\r\n"
|
||||
+ " <script src='https://source.zoom.us/1.9.0/lib/vendor/lodash.min.js'></script>\r\n"
|
||||
+ " <script src='https://source.zoom.us/zoom-meeting-1.9.0.min.js'></script>\r\n"
|
||||
+ " <script src='https://source.zoom.us/1.9.1/lib/vendor/react.min.js'></script>\r\n"
|
||||
+ " <script src='https://source.zoom.us/1.9.1/lib/vendor/react-dom.min.js'></script>\r\n"
|
||||
+ " <script src='https://source.zoom.us/1.9.1/lib/vendor/redux.min.js'></script>\r\n"
|
||||
+ " <script src='https://source.zoom.us/1.9.1/lib/vendor/redux-thunk.min.js'></script>\r\n"
|
||||
+ " <script src='https://source.zoom.us/1.9.1/lib/vendor/jquery.min.js'></script>\r\n"
|
||||
+ " <script src='https://source.zoom.us/1.9.1/lib/vendor/lodash.min.js'></script>\r\n"
|
||||
+ " <script src='https://source.zoom.us/zoom-meeting-1.9.1.min.js'></script>\r\n"
|
||||
+ " <script src='https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9/crypto-js.min.js'></script>\r\n"
|
||||
+ " <script type='text/javascript'>\r\n"
|
||||
+ "\r\n"
|
||||
|
@ -88,7 +88,7 @@ public class ZoomWindowScriptResolverTest {
|
|||
+ " console.log(JSON.stringify(ZoomMtg.checkSystemRequirements()));\r\n"
|
||||
+ "\r\n"
|
||||
+ " console.log('Initializing Zoom...');\r\n"
|
||||
+ " ZoomMtg.setZoomJSLib('https://source.zoom.us/1.9.0/lib', '/av');\r\n"
|
||||
+ " ZoomMtg.setZoomJSLib('https://source.zoom.us/1.9.1/lib', '/av');\r\n"
|
||||
+ " ZoomMtg.preLoadWasm();\r\n"
|
||||
+ " ZoomMtg.prepareJssdk();\r\n"
|
||||
+ "\r\n"
|
||||
|
@ -158,11 +158,6 @@ public class ZoomWindowScriptResolverTest {
|
|||
+ " passWord: config.passWord,\r\n"
|
||||
+ " success(res) {\r\n"
|
||||
+ " console.log('JOIN SUCCESS')\r\n"
|
||||
+ " ZoomMtg.getAttendeeslist({\r\n"
|
||||
+ " success: function (res) {\r\n"
|
||||
+ " console.log(res, \"get getAttendeeslist\");\r\n"
|
||||
+ " }\r\n"
|
||||
+ " });\r\n"
|
||||
+ " },\r\n"
|
||||
+ " error(res) {\r\n"
|
||||
+ " console.warn('JOIN ERROR')\r\n"
|
||||
|
|
Loading…
Reference in a new issue