SEBSERV-91 implementation

This commit is contained in:
anhefti 2020-04-21 16:26:08 +02:00
parent 422147dd7f
commit f442b9885f
8 changed files with 152 additions and 15 deletions

View file

@ -119,6 +119,7 @@ public final class API {
public static final String EXAM_ADMINISTRATION_SEB_RESTRICTION_PATH_SEGMENT = "/seb-restriction";
public static final String EXAM_ADMINISTRATION_CHECK_RESTRICTION_PATH_SEGMENT = "/check-seb-restriction";
public static final String EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT = "/check-imported";
public static final String EXAM_ADMINISTRATION_SEB_RESTRICTION_CHAPTERS_PATH_SEGMENT = "/chapters";
public static final String EXAM_INDICATOR_ENDPOINT = "/indicator";

View file

@ -49,7 +49,7 @@ public final class Chapters {
public final String id;
@JsonCreator
protected Chapter(
public Chapter(
@JsonProperty(ATTR_NAME) final String name,
@JsonProperty(ATTR_ID) final String id) {

View file

@ -15,6 +15,7 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
@ -23,9 +24,11 @@ import org.eclipse.swt.widgets.Composite;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSebRestriction;
import ch.ethz.seb.sebserver.gbl.model.exam.SebRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.util.Tuple;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.form.Form;
import ch.ethz.seb.sebserver.gui.form.FormBuilder;
@ -40,6 +43,7 @@ import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.ActivateSebRestriction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeactivateSebRestriction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetCourseChapters;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetSebRestriction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveSebRestriction;
@ -190,6 +194,13 @@ public class ExamSebRestrictionSettings {
.call()
.getOrThrow();
final Chapters chapters = restService
.getBuilder(GetCourseChapters.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.onError(t -> t.printStackTrace())
.getOr(null);
final PageContext formContext = this.pageContext
.copyOf(content)
.clearEntityKeys();
@ -231,16 +242,7 @@ public class ExamSebRestrictionSettings {
resourceService::sebRestrictionWhiteListResources))
.addFieldIf(
() -> lmsType == LmsType.OPEN_EDX,
() -> FormBuilder.multiCheckboxSelection(
OpenEdxSebRestriction.ATTR_PERMISSION_COMPONENTS,
SEB_RESTRICTION_FORM_EDX_PERMISSIONS,
sebRestriction.getAdditionalProperties()
.get(OpenEdxSebRestriction.ATTR_PERMISSION_COMPONENTS),
resourceService::sebRestrictionPermissionResources))
.addFieldIf(
() -> lmsType == LmsType.OPEN_EDX,
() -> chapters == null && lmsType == LmsType.OPEN_EDX,
() -> FormBuilder.text(
OpenEdxSebRestriction.ATTR_BLACKLIST_CHAPTERS,
SEB_RESTRICTION_FORM_EDX_BLACKLIST_CHAPTERS,
@ -250,6 +252,28 @@ public class ExamSebRestrictionSettings {
.get(OpenEdxSebRestriction.ATTR_BLACKLIST_CHAPTERS)))
.asArea())
.addFieldIf(
() -> chapters != null && lmsType == LmsType.OPEN_EDX,
() -> FormBuilder.multiCheckboxSelection(
OpenEdxSebRestriction.ATTR_BLACKLIST_CHAPTERS,
SEB_RESTRICTION_FORM_EDX_BLACKLIST_CHAPTERS,
sebRestriction
.getAdditionalProperties()
.get(OpenEdxSebRestriction.ATTR_BLACKLIST_CHAPTERS),
() -> chapters.chapters
.stream()
.map(chapter -> new Tuple<>(chapter.id, chapter.name))
.collect(Collectors.toList())))
.addFieldIf(
() -> lmsType == LmsType.OPEN_EDX,
() -> FormBuilder.multiCheckboxSelection(
OpenEdxSebRestriction.ATTR_PERMISSION_COMPONENTS,
SEB_RESTRICTION_FORM_EDX_PERMISSIONS,
sebRestriction.getAdditionalProperties()
.get(OpenEdxSebRestriction.ATTR_PERMISSION_COMPONENTS),
resourceService::sebRestrictionPermissionResources))
.addFieldIf(
() -> lmsType == LmsType.OPEN_EDX,
() -> FormBuilder.checkbox(

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2020 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.gui.service.remote.webservice.api.exam;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class GetCourseChapters extends RestCall<Chapters> {
public GetCourseChapters() {
super(new TypeKey<>(
CallType.UNDEFINED,
EntityType.EXAM,
new TypeReference<Chapters>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_ADMINISTRATION_ENDPOINT +
API.MODEL_ID_VAR_PATH_SEGMENT +
API.EXAM_ADMINISTRATION_SEB_RESTRICTION_CHAPTERS_PATH_SEGMENT);
}
}

View file

@ -109,6 +109,15 @@ public interface LmsAPITemplate {
// examinee identifier received by on SEB-Client connection.
//Result<ExamineeAccountDetails> getExamineeAccountDetails(String examineeUserId);
/** Used to get a list of chapters (display name and chapter-identifier) that can be used to
* apply chapter-based SEB restriction for a specified course.
*
* The availability of this depends on the type of LMS and on installed plugins that supports this feature.
* If this is not supported by the underling LMS a UnsupportedOperationException will be presented
* within the Result.
*
* @param courseId The course identifier
* @return Result referencing to the Chapters model for the given course or to an error when happened. */
Result<Chapters> getCourseChapters(String courseId);
/** Get SEB restriction data form LMS within a SebRestrictionData instance if available

View file

@ -89,7 +89,7 @@ public abstract class CourseAccess {
}
protected Result<Chapters> getCourseChapters(final String courseId) {
return null;
return this.chaptersRequest.protectedRun(getCourseChaptersSupplier(courseId));
}
protected abstract Supplier<List<QuizData>> allQuizzesSupplier();

View file

@ -10,11 +10,11 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
@ -30,6 +30,8 @@ import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.http.AccessTokenRequiredException;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@ -40,6 +42,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.CourseAccess;
@ -53,6 +56,7 @@ final class OpenEdxCourseAccess extends CourseAccess {
private static final String OPEN_EDX_DEFAULT_COURSE_ENDPOINT = "/api/courses/v1/courses/";
private static final String OPEN_EDX_DEFAULT_BLOCKS_ENDPOINT =
"/api/courses/v1/blocks/?depth=1&all_blocks=true&course_id=";
private static final String OPEN_EDX_DEFAULT_BLOCKS_TYPE_CHAPTER = "chapter";
private static final String OPEN_EDX_DEFAULT_COURSE_START_URL_PREFIX = "/courses/";
private final LmsSetup lmsSetup;
@ -116,7 +120,18 @@ final class OpenEdxCourseAccess extends CourseAccess {
@Override
protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) {
throw new UnsupportedOperationException("not available yet");
return () -> {
final String uri =
this.lmsSetup.lmsApiUrl +
OPEN_EDX_DEFAULT_BLOCKS_ENDPOINT +
Utils.encodeFormURL_UTF_8(courseId);
return new Chapters(getCourseBlocks(uri)
.getBody().blocks.values()
.stream()
.filter(block -> OPEN_EDX_DEFAULT_BLOCKS_TYPE_CHAPTER.equals(block.type))
.map(block -> new Chapters.Chapter(block.display_name, block.block_id))
.collect(Collectors.toList()));
};
}
private ArrayList<QuizData> collectAllQuizzes(final OAuth2RestTemplate restTemplate) {
@ -186,6 +201,17 @@ final class OpenEdxCourseAccess extends CourseAccess {
EdXPage.class);
}
private ResponseEntity<Blocks> getCourseBlocks(final String uri) {
final HttpHeaders httpHeaders = new HttpHeaders();
return getRestTemplateNoEncoding()
.getOrThrow()
.exchange(
uri,
HttpMethod.GET,
new HttpEntity<>(httpHeaders),
Blocks.class);
}
private static QuizData quizDataOf(
final LmsSetup lmsSetup,
final CourseData courseData,
@ -230,13 +256,14 @@ final class OpenEdxCourseAccess extends CourseAccess {
@JsonIgnoreProperties(ignoreUnknown = true)
static final class Blocks {
public String root;
public Collection<Block> blocks;
public Map<String, Block> blocks;
}
@JsonIgnoreProperties(ignoreUnknown = true)
static final class Block {
public String block_id;
public String display_name;
public String type;
}
private static final class EdxOAuth2RequestAuthenticator implements OAuth2RequestAuthenticator {
@ -257,6 +284,17 @@ final class OpenEdxCourseAccess extends CourseAccess {
}
private Result<OAuth2RestTemplate> getRestTemplateNoEncoding() {
return this.openEdxRestTemplateFactory
.createOAuthRestTemplate()
.map(tempalte -> {
final DefaultUriBuilderFactory builderFactory = new DefaultUriBuilderFactory();
builderFactory.setEncodingMode(EncodingMode.NONE);
tempalte.setUriTemplateHandler(builderFactory);
return tempalte;
});
}
private Result<OAuth2RestTemplate> getRestTemplate() {
if (this.restTemplate == null) {
final Result<OAuth2RestTemplate> templateRequest = this.openEdxRestTemplateFactory

View file

@ -48,6 +48,7 @@ import ch.ethz.seb.sebserver.gbl.model.Domain.EXAM;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.PageSortOrder;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
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;
@ -351,6 +352,28 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
.getOrThrow();
}
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_SEB_RESTRICTION_CHAPTERS_PATH_SEGMENT,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Chapters getChapters(
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
@PathVariable(API.PARAM_MODEL_ID) final Long examlId) {
checkReadPrivilege(institutionId);
return this.entityDAO.byPK(examlId)
.flatMap(this.authorization::checkRead)
.flatMap(exam -> this.lmsAPIService
.getLmsAPITemplate(exam.lmsSetupId)
.getOrThrow()
.getCourseChapters(exam.externalId))
.getOrThrow();
}
// **** SEB Restriction
// ****************************************************************************