Merge remote-tracking branch 'origin/dev-1.2' into development

This commit is contained in:
anhefti 2021-08-30 13:14:16 +02:00
commit 03300923c0
8 changed files with 461 additions and 153 deletions

View file

@ -61,7 +61,7 @@ public final class LmsSetup implements GrantEntity, Activatable {
/** The Moodle binding features only the course access API so far */ /** The Moodle binding features only the course access API so far */
MOODLE(Features.COURSE_API /* , Features.SEB_RESTRICTION */), MOODLE(Features.COURSE_API /* , Features.SEB_RESTRICTION */),
/** The Ans Delft binding is on the way */ /** 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 */ /** The OpenOLAT binding is on the way */
OPEN_OLAT(Features.COURSE_API, Features.SEB_RESTRICTION); OPEN_OLAT(Features.COURSE_API, Features.SEB_RESTRICTION);

View file

@ -357,7 +357,7 @@ public class LmsSetupForm implements TemplateComposer {
} }
try { try {
return this.resourceService.lmsTypeResources().get(0)._1; return LmsType.MOCKUP.name();
} catch (final Exception e) { } catch (final Exception e) {
return null; return null;
} }

View file

@ -293,14 +293,22 @@ public final class ViewContext {
} }
void setValuesToInputFields(final Collection<ConfigurationValue> values) { void setValuesToInputFields(final Collection<ConfigurationValue> values) {
this.inputFieldMapping try {
.values() this.inputFieldMapping
.forEach(field -> { .values()
final ConfigurationValue initValue = field.initValue(values); .forEach(field -> {
if (initValue != null) { try {
this.valueChangeListener.notifyGUI(this, field.getAttribute(), initValue); 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 /** Removes all registered InputFields with the given attribute ids

View file

@ -9,20 +9,33 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans; package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.env.Environment; 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.http.client.ClientHttpRequestFactory;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; 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.ClientHttpRequestFactoryService;
import ch.ethz.seb.sebserver.gbl.api.APIMessage; 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.QuizData;
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.model.institution.LmsSetup.Features;
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;
@ -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.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.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 { 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 +111,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 +208,126 @@ 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, 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 @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 () -> {
final 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 String url = String.format("/api/v2/users/%s", id);
// before returning it. final UserData u = this.apiGet(restTemplate, url, UserData.class);
final Map<String, String> attrs = new HashMap<>();
throw new RuntimeException("TODO"); 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 +339,169 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
@Override @Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) { 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") @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(() -> { private SEBRestriction deleteRestrictionForAssignmentId(final RestTemplate restTemplate, final String id) {
final String url = String.format("/api/v2/assignments/%s", id);
// TODO get the SEB client restrictions that are currently set on the remote LMS for final AssignmentData assignment = getAssignmentById(restTemplate, id);
// the given quiz / course derived from the given exam assignment.accessibility_settings.config_keys = null;
assignment.accessibility_settings.seb_server_enabled = false;
throw new RuntimeException("TODO"); @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 @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") return getRestTemplate()
final String quizId = exam.externalId; .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(() -> { return Result.tryCatch(() -> {
if (this.cachedRestTemplate != null) {
return this.cachedRestTemplate;
}
// TODO Release respectively delete all SEB client restrictions for the given final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials();
// course / quize on the remote LMS. 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;
}
} }

View file

@ -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;
}
}

View file

@ -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;
}
});
}
}

View file

@ -1,17 +1,17 @@
<html> <html>
<head> <head>
<meta charset='utf-8' /> <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.1/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/react-select.css' />
</head> </head>
<body> <body>
<script src='https://source.zoom.us/1.9.0/lib/vendor/react.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.0/lib/vendor/react-dom.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.0/lib/vendor/redux.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.0/lib/vendor/redux-thunk.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.0/lib/vendor/jquery.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.0/lib/vendor/lodash.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.0.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 src='https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9/crypto-js.min.js'></script>
<script type='text/javascript'> <script type='text/javascript'>
@ -19,7 +19,7 @@
console.log(JSON.stringify(ZoomMtg.checkSystemRequirements())); console.log(JSON.stringify(ZoomMtg.checkSystemRequirements()));
console.log('Initializing Zoom...'); 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.preLoadWasm();
ZoomMtg.prepareJssdk(); ZoomMtg.prepareJssdk();
@ -89,11 +89,6 @@
passWord: config.passWord, passWord: config.passWord,
success(res) { success(res) {
console.log('JOIN SUCCESS') console.log('JOIN SUCCESS')
ZoomMtg.getAttendeeslist({
success: function (res) {
console.log(res, "get getAttendeeslist");
}
});
}, },
error(res) { error(res) {
console.warn('JOIN ERROR') console.warn('JOIN ERROR')

View file

@ -70,17 +70,17 @@ public class ZoomWindowScriptResolverTest {
"<html>\r\n" "<html>\r\n"
+ " <head>\r\n" + " <head>\r\n"
+ " <meta charset='utf-8' />\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.1/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/react-select.css' />\r\n"
+ " </head>\r\n" + " </head>\r\n"
+ " <body>\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.1/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.1/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.1/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.1/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.1/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/1.9.1/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/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 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" + " <script type='text/javascript'>\r\n"
+ "\r\n" + "\r\n"
@ -88,7 +88,7 @@ public class ZoomWindowScriptResolverTest {
+ " console.log(JSON.stringify(ZoomMtg.checkSystemRequirements()));\r\n" + " console.log(JSON.stringify(ZoomMtg.checkSystemRequirements()));\r\n"
+ "\r\n" + "\r\n"
+ " console.log('Initializing Zoom...');\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.preLoadWasm();\r\n"
+ " ZoomMtg.prepareJssdk();\r\n" + " ZoomMtg.prepareJssdk();\r\n"
+ "\r\n" + "\r\n"
@ -158,11 +158,6 @@ public class ZoomWindowScriptResolverTest {
+ " passWord: config.passWord,\r\n" + " passWord: config.passWord,\r\n"
+ " success(res) {\r\n" + " success(res) {\r\n"
+ " console.log('JOIN SUCCESS')\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" + " },\r\n"
+ " error(res) {\r\n" + " error(res) {\r\n"
+ " console.warn('JOIN ERROR')\r\n" + " console.warn('JOIN ERROR')\r\n"