From b767178d7582f6c0e6edba18cacd0d2749604aaf Mon Sep 17 00:00:00 2001 From: Carol Alexandru Date: Tue, 17 Aug 2021 06:06:58 +0200 Subject: [PATCH 1/2] Implement AnsLms integration --- .../lms/impl/ans/AnsLmsAPITemplate.java | 385 +++++++++++++----- .../servicelayer/lms/impl/ans/AnsLmsData.java | 98 +++++ .../lms/impl/ans/AnsPersonalRestTemplate.java | 44 ++ 3 files changed, 418 insertions(+), 109 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsData.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsPersonalRestTemplate.java diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java index c8e9fa16..5a40527c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java @@ -8,22 +8,50 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans; +import java.util.Optional; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; 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.stream.Collectors; +import java.util.Locale; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; 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.http.client.ClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; 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.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.gbl.api.APIMessage; 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.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 { - // 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 +128,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 +225,125 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms return super.protectedQuizRequest(id); } + private List collectAllQuizzes(final AnsPersonalRestTemplate restTemplate) { + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + final List 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(""), // TODO: what to show here? + startTime, + endTime, + // TODO: Waiting for Ans to implement supplying of the correct URL + String.format("not implemented by Ans"), + Map.of("assignment_id", String.valueOf(a.id))); + } + + private List getAssignments(final RestTemplate restTemplate) { + // TODO: 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>(){}); + } + + 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 getQuizzesByIds(final RestTemplate restTemplate, final Set 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> 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"); + List res = getRestTemplate() + .map(this::collectAllQuizzes) + .getOrThrow(); + super.putToCache(res); + return res; }; } @Override protected Supplier> quizzesSupplier(final Set 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 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 LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + final String url = String.format("/api/v2/users/%s", id); + UserData u = this.apiGet(restTemplate, url, UserData.class); + final Map 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 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 accountDetailsSupplier(final String id) { + return () -> getRestTemplate() + .map(t -> this.getExamineeById(t, id)) + .getOrThrow(); } @Override @@ -266,79 +355,157 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms @Override public Result getSEBClientRestriction(final Exam exam) { - @SuppressWarnings("unused") - final String quizId = exam.externalId; + return getRestTemplate() + .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()); + } - // TODO get the SEB client restrictions that are currently set on the remote LMS for - // the given quiz / course derived from the given exam + private SEBRestriction setRestrictionForAssignmentId(final RestTemplate restTemplate, String id, 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; + 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()); + } - 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()); } @Override public Result 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 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 String link; + public LinkRel rel; + public PageLink(String l, LinkRel r) { link = l; rel = r; } + } + private List parseLinks(String header) { + // Extracts the individual links from a header that looks like this: + // ; rel="first",; rel="last" + final Stream 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 getNextLink(List links) { + return links.stream().filter(l -> l.rel == LinkRel.NEXT).findFirst(); + } + + private List apiGetList(final RestTemplate restTemplate, String url, ParameterizedTypeReference> type) { + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + return apiGetListPages(restTemplate, lmsSetup.lmsApiUrl + url, type); + } + + private List apiGetListPages(final RestTemplate restTemplate, String link, ParameterizedTypeReference> 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> response = restTemplate.exchange( + link, + HttpMethod.GET, + new HttpEntity<>(requestHeaders), + type); + final List page = response.getBody(); + final HttpHeaders responseHeaders = response.getHeaders(); + final List links = parseLinks(responseHeaders.getFirst("link")); + final List nextPage = getNextLink(links).map( l -> { + return apiGetListPages(restTemplate, l.link, type); + }).orElse(new ArrayList()); + page.addAll(nextPage); + return page; + } + + private T apiGet(final RestTemplate restTemplate, String url, Class type) { + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + final HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.set("accept", "application/json"); + ResponseEntity res = restTemplate.exchange( + lmsSetup.lmsApiUrl + url, + HttpMethod.GET, + new HttpEntity<>(requestHeaders), + type); + return res.getBody(); + } + + private R apiPatch(final RestTemplate restTemplate, String url, P patch, Class

patchType, Class responseType) { + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + final HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.set("content-type", "application/json"); + HttpEntity

requestEntity = new HttpEntity<>(patch, requestHeaders); + final ResponseEntity res = restTemplate.exchange( + lmsSetup.lmsApiUrl + url, + HttpMethod.PATCH, + requestEntity, + responseType); + return res.getBody(); + } + + + private Result 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 LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + 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; - } - } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsData.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsData.java new file mode 100644 index 00000000..da6e59be --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsData.java @@ -0,0 +1,98 @@ +/* + * 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 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", + "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 + }, + "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 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; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsPersonalRestTemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsPersonalRestTemplate.java new file mode 100644 index 00000000..80555251 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsPersonalRestTemplate.java @@ -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; + } + }); + } +} + From 6e31ad5e0ec1e83cc09c8e8b49a42ecd4fa5b17b Mon Sep 17 00:00:00 2001 From: Carol Alexandru Date: Mon, 30 Aug 2021 03:54:31 +0200 Subject: [PATCH 2/2] Retrieve start_url from Ans Assignment --- .../servicelayer/lms/impl/ans/AnsLmsAPITemplate.java | 7 +++---- .../webservice/servicelayer/lms/impl/ans/AnsLmsData.java | 8 +++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java index 5a40527c..17634cf3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java @@ -259,16 +259,15 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms lmsSetup.id, lmsSetup.getLmsType(), String.format("%s", a.name), - String.format(""), // TODO: what to show here? + String.format(""), startTime, endTime, - // TODO: Waiting for Ans to implement supplying of the correct URL - String.format("not implemented by Ans"), + a.start_url, Map.of("assignment_id", String.valueOf(a.id))); } private List getAssignments(final RestTemplate restTemplate) { - // TODO: at the moment, seb_server_enabled cannot be set inside the Ans GUI, + // 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"; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsData.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsData.java index da6e59be..926e01a4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsData.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsData.java @@ -35,6 +35,8 @@ public final class AnsLmsData { "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, @@ -43,7 +45,10 @@ public final class AnsLmsData { "feedback": false, "forced_test_navigation": false, "cannot_reopen_question_groups": false, - "seb_server_enabled": true + "seb_server_enabled": true, + "config_keys": [ + "9dd14ac828617116a1230c71b9a1aa9e06f43b32d9fa7db67f4fa113a6896e83e" + ] }, "grades_settings": { "grade_calculation": "formula", @@ -64,6 +69,7 @@ public final class AnsLmsData { public String external_id; public String start_at; public String end_at; + public String start_url; public AccessibilitySettingsData accessibility_settings; }