Merge pull request #34 from sealexan/dev-lms-ans

Implement AnsLms integration
This commit is contained in:
Andreas Hefti 2021-08-30 10:45:12 +02:00 committed by GitHub
commit 3c72ed9738
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 423 additions and 109 deletions

View file

@ -8,22 +8,50 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans; package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans;
import java.util.Optional;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.Map;
import java.util.HashMap;
import java.util.Arrays;
import java.util.stream.Stream;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.Locale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.async.AsyncService; import ch.ethz.seb.sebserver.gbl.async.AsyncService;
@ -48,19 +76,22 @@ 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.impl.AbstractCachedCourseAccess; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCourseAccess;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans.AnsLmsData.AssignmentData;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans.AnsLmsData.UserData;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans.AnsLmsData.AccessibilitySettingsData;
public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements LmsAPITemplate { 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 ClientHttpRequestFactoryService clientHttpRequestFactoryService;
private final ClientCredentialService clientCredentialService; private final ClientCredentialService clientCredentialService;
private final APITemplateDataSupplier apiTemplateDataSupplier; private final APITemplateDataSupplier apiTemplateDataSupplier;
private final Long lmsSetupId; private final Long lmsSetupId;
private AnsPersonalRestTemplate cachedRestTemplate;
protected AnsLmsAPITemplate( protected AnsLmsAPITemplate(
// TODO if you need more dependencies inject them here and set the reference
final ClientHttpRequestFactoryService clientHttpRequestFactoryService, final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final ClientCredentialService clientCredentialService, final ClientCredentialService clientCredentialService,
final APITemplateDataSupplier apiTemplateDataSupplier, final APITemplateDataSupplier apiTemplateDataSupplier,
@ -97,28 +128,19 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
if (testLmsSetupSettings.hasAnyError()) { if (testLmsSetupSettings.hasAnyError()) {
return testLmsSetupSettings; return testLmsSetupSettings;
} }
try {
// TODO check if the course API of the remote LMS is available this.getRestTemplate().get();
// if not, create corresponding LmsSetupTestResult error } 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); return LmsSetupTestResult.ofOkay(LmsType.ANS_DELFT);
} }
@Override @Override
public LmsSetupTestResult testCourseRestrictionAPI() { public LmsSetupTestResult testCourseRestrictionAPI() {
final LmsSetupTestResult testLmsSetupSettings = testLmsSetupSettings(); return testCourseAccessAPI();
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);
} }
private LmsSetupTestResult testLmsSetupSettings() { private LmsSetupTestResult testLmsSetupSettings() {
@ -203,58 +225,124 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
return super.protectedQuizRequest(id); 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, String id) {
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final AssignmentData a = getAssignmentById(restTemplate, id);
return quizDataFromAnsData(lmsSetup, a);
}
private QuizData quizDataFromAnsData(LmsSetup lmsSetup, 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);
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,
Map.of("assignment_id", String.valueOf(a.id)));
}
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, 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 @Override
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) { protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) {
// We cannot filter by from-date or partial names using the Ans search API.
@SuppressWarnings("unused") // Only exact matches are permitted. So we're not implementing filtering
final String quizName = filterMap.getString(QuizData.FILTER_ATTR_QUIZ_NAME); // on the API level and always retrieve all assignments and let SEB server
@SuppressWarnings("unused") // do the filtering.
final DateTime quizFromTime = (filterMap != null) ? filterMap.getQuizFromTime() : null;
return () -> { return () -> {
List<QuizData> res = getRestTemplate()
// TODO get all course / quiz data from remote LMS that matches the filter criteria. .map(this::collectAllQuizzes)
// put loaded QuizData to the cache: super.putToCache(quizDataCollection); .getOrThrow();
// before returning it. super.putToCache(res);
return res;
throw new RuntimeException("TODO");
}; };
} }
@Override @Override
protected Supplier<Collection<QuizData>> quizzesSupplier(final Set<String> ids) { protected Supplier<Collection<QuizData>> quizzesSupplier(final Set<String> ids) {
return () -> { return () -> getRestTemplate()
.map(t -> this.getQuizzesByIds(t, ids))
// TODO get all quiz / course data for specified identifiers from remote LMS .getOrThrow();
// and put it to the cache: super.putToCache(quizDataCollection);
// before returning it.
throw new RuntimeException("TODO");
};
} }
@Override @Override
protected Supplier<QuizData> quizSupplier(final String id) { 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 private ExamineeAccountDetails getExamineeById(final RestTemplate restTemplate, final String id) {
// and put it to the cache: super.putToCache(quizDataCollection); final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
// before returning it. final String url = String.format("/api/v2/users/%s", id);
UserData u = this.apiGet(restTemplate, url, UserData.class);
throw new RuntimeException("TODO"); 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 @Override
protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String examineeSessionId) { protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String id) {
return () -> getRestTemplate()
return () -> { .map(t -> this.getExamineeById(t, id))
.getOrThrow();
// 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");
};
} }
@Override @Override
@ -266,79 +354,157 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
@Override @Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) { public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
@SuppressWarnings("unused") return getRestTemplate()
final String quizId = exam.externalId; .map(t -> this.getRestrictionForAssignmentId(t, exam.externalId));
}
return Result.tryCatch(() -> { private SEBRestriction getRestrictionForAssignmentId(final RestTemplate restTemplate, 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>());
}
// TODO get the SEB client restrictions that are currently set on the remote LMS for private SEBRestriction setRestrictionForAssignmentId(final RestTemplate restTemplate, String id, SEBRestriction restriction) {
// the given quiz / course derived from the given exam 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;
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>());
}
throw new RuntimeException("TODO"); private SEBRestriction deleteRestrictionForAssignmentId(final RestTemplate restTemplate, 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;
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 @Override
public Result<SEBRestriction> applySEBClientRestriction( public Result<SEBRestriction> applySEBClientRestriction(
final String externalExamId, final String externalExamId,
final SEBRestriction sebRestrictionData) { final SEBRestriction sebRestrictionData) {
return getRestTemplate()
return Result.tryCatch(() -> { .map(t -> this.setRestrictionForAssignmentId(t, externalExamId, sebRestrictionData));
// 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");
});
} }
@Override @Override
public Result<Exam> releaseSEBClientRestriction(final Exam exam) { public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
@SuppressWarnings("unused")
final String quizId = exam.externalId; 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 String link;
public LinkRel rel;
public PageLink(String l, LinkRel r) { link = l; rel = r; }
}
private List<PageLink> parseLinks(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 -> {
String[] pair = s.split(";");
String link = pair[0].trim().substring(1).replaceFirst(".$",""); // remove < >
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(List<PageLink> links) {
return links.stream().filter(l -> l.rel == LinkRel.NEXT).findFirst();
}
private <T> List<T> apiGetList(final RestTemplate restTemplate, String url, 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, String link, 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");
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, String url, Class<T> type) {
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.set("accept", "application/json");
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, String url, P patch, Class<P> patchType, Class<R> responseType) {
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.set("content-type", "application/json");
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(() -> { return Result.tryCatch(() -> {
if (this.cachedRestTemplate != null) { return this.cachedRestTemplate; }
// TODO Release respectively delete all SEB client restrictions for the given final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
// course / quize on the remote LMS. final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials();
final ProxyData proxyData = this.apiTemplateDataSupplier.getProxyData();
throw new RuntimeException("TODO"); final CharSequence plainClientId = credentials.clientId;
final CharSequence plainClientSecret = this.clientCredentialService
.getPlainClientSecret(credentials)
.getOrThrow();
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;
}
} }

View file

@ -0,0 +1,104 @@
/*
* 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;
import com.fasterxml.jackson.annotation.JsonProperty;
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;
}
}

View file

@ -0,0 +1,44 @@
/*
* 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.web.client.RestTemplate;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
public class AnsPersonalRestTemplate extends RestTemplate {
private static final Logger log = LoggerFactory.getLogger(AnsPersonalRestTemplate.class);
public String token;
public AnsPersonalRestTemplate(ClientCredentialsResourceDetails details) {
super();
token = details.getClientSecret();
this.getInterceptors().add(new ClientHttpRequestInterceptor(){
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
request.getHeaders().set("Authorization", "Bearer " + token);
//log.debug("Matching curl: curl -X GET {} -H 'accept: application/json' -H 'Authorization: Bearer {}'", request.getURI(), token);
ClientHttpResponse response = execution.execute(request, body);
log.debug("Response Headers : {}", response.getHeaders());
return response;
}
});
}
}