diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java index 223a53f3..7603eabc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java @@ -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); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/LmsSetupForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/LmsSetupForm.java index 573fd16a..b2a3c32c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/LmsSetupForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/LmsSetupForm.java @@ -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; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/ViewContext.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/ViewContext.java index 93650f57..dd34d845 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/ViewContext.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/ViewContext.java @@ -293,14 +293,22 @@ public final class ViewContext { } void setValuesToInputFields(final Collection 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 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..078227ac 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 @@ -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 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, 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 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 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>() { + }); + } + + 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 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"); + final 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 String url = String.format("/api/v2/users/%s", id); + final 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 +339,169 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms @Override public Result 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()); + } + + 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()); + } - 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()); } @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 final String link; + public final LinkRel rel; + + public PageLink(final String l, final LinkRel r) { + this.link = l; + this.rel = r; + } + } + + private List parseLinks(final 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 -> { + 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 getNextLink(final List links) { + return links.stream().filter(l -> l.rel == LinkRel.NEXT).findFirst(); + } + + private List apiGetList(final RestTemplate restTemplate, final String url, + final ParameterizedTypeReference> type) { + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + return apiGetListPages(restTemplate, lmsSetup.lmsApiUrl + url, type); + } + + private List apiGetListPages(final RestTemplate restTemplate, final String link, + final 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"); + final 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, final String url, final Class type) { + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + final HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.set("accept", "application/json"); + final ResponseEntity res = restTemplate.exchange( + lmsSetup.lmsApiUrl + url, + HttpMethod.GET, + new HttpEntity<>(requestHeaders), + type); + return res.getBody(); + } + + private R apiPatch(final RestTemplate restTemplate, final String url, final P patch, + final Class

patchType, final Class responseType) { + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + final HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.set("content-type", "application/json"); + final 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 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; - } - } 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..b0a17dce --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsData.java @@ -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 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; + } + +} 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..374ac4b6 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsPersonalRestTemplate.java @@ -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; + } + }); + } +} diff --git a/src/main/resources/ch/ethz/seb/sebserver/gui/service/session/proctoring/zoomWindow.html b/src/main/resources/ch/ethz/seb/sebserver/gui/service/session/proctoring/zoomWindow.html index 3a5dbf72..6f4a7798 100644 --- a/src/main/resources/ch/ethz/seb/sebserver/gui/service/session/proctoring/zoomWindow.html +++ b/src/main/resources/ch/ethz/seb/sebserver/gui/service/session/proctoring/zoomWindow.html @@ -1,17 +1,17 @@ - - + + - - - - - - - + + + + + + + \r\n" - + " \r\n" - + " \r\n" - + " \r\n" - + " \r\n" - + " \r\n" - + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + " \r\n" + "