diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java index cc2589f5321c..e26daca9ce8e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java @@ -785,6 +785,25 @@ public ResponseEntity getCourseWithExercises(@PathVariable Long courseId return ResponseEntity.ok(course); } + /** + * GET /courses/:courseId : get the "id" course. + * + * @param courseId the id of the course to retrieve + * @return the ResponseEntity with status 200 (OK) and with body the course, or with status 404 (Not Found) + */ + @GetMapping("courses/{courseId}/with-exercises-lectures-competencies") + @EnforceAtLeastTutor + public ResponseEntity getCourseWithExercisesAndLecturesAndCompetencies(@PathVariable Long courseId) { + log.debug("REST request to get course {} for tutors", courseId); + Optional courseOptional = courseRepository.findWithEagerExercisesAndLecturesAndLectureUnitsAndCompetenciesById(courseId); + if (courseOptional.isEmpty()) { + return ResponseEntity.notFound().build(); + } + Course course = courseOptional.get(); + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.TEACHING_ASSISTANT, course, null); + return ResponseEntity.ok(course); + } + /** * GET /courses/:courseId/with-organizations Get a course by id with eagerly loaded organizations * diff --git a/src/main/java/de/tum/cit/aet/artemis/lti/domain/LtiResourceLaunch.java b/src/main/java/de/tum/cit/aet/artemis/lti/domain/LtiResourceLaunch.java index 830f27870ad2..ccbbe71b8a8d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/lti/domain/LtiResourceLaunch.java +++ b/src/main/java/de/tum/cit/aet/artemis/lti/domain/LtiResourceLaunch.java @@ -35,7 +35,6 @@ public class LtiResourceLaunch extends DomainObject { @ManyToOne private User user; - @NotNull @ManyToOne private Exercise exercise; diff --git a/src/main/java/de/tum/cit/aet/artemis/lti/service/DeepLinkingType.java b/src/main/java/de/tum/cit/aet/artemis/lti/service/DeepLinkingType.java new file mode 100644 index 000000000000..dc51b95724f9 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/lti/service/DeepLinkingType.java @@ -0,0 +1,22 @@ +package de.tum.cit.aet.artemis.lti.service; + +public enum DeepLinkingType { + + EXERCISE, LECTURE, COMPETENCY, LEARNING_PATH, IRIS; + + /** + * Get the enum value from a string. + * + * @param type The string representation of the deep linking type. + * @return The corresponding DeepLinkingType. + * @throws IllegalArgumentException if the type does not match any enum value. + */ + public static DeepLinkingType fromString(String type) { + for (DeepLinkingType deepLinkingType : values()) { + if (deepLinkingType.name().equalsIgnoreCase(type)) { + return deepLinkingType; + } + } + throw new IllegalArgumentException("Invalid deep linking type: " + type); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/lti/service/Lti13Service.java b/src/main/java/de/tum/cit/aet/artemis/lti/service/Lti13Service.java index 8b645aa4aaef..3168cc1947d7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/lti/service/Lti13Service.java +++ b/src/main/java/de/tum/cit/aet/artemis/lti/service/Lti13Service.java @@ -45,6 +45,8 @@ import de.tum.cit.aet.artemis.exercise.domain.Exercise; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; import de.tum.cit.aet.artemis.exercise.repository.ExerciseRepository; +import de.tum.cit.aet.artemis.lecture.domain.Lecture; +import de.tum.cit.aet.artemis.lecture.repository.LectureRepository; import de.tum.cit.aet.artemis.lti.config.Lti13TokenRetriever; import de.tum.cit.aet.artemis.lti.domain.LtiPlatformConfiguration; import de.tum.cit.aet.artemis.lti.domain.LtiResourceLaunch; @@ -62,12 +64,24 @@ public class Lti13Service { private static final String EXERCISE_PATH_PATTERN = "/courses/{courseId}/exercises/{exerciseId}"; + private static final String LECTURE_PATH_PATTERN = "/courses/{courseId}/lectures/{lectureId}"; + + private static final String COMPETENCY_PATH_PATTERN = "/courses/{courseId}/competencies"; + + private static final String IRIS_PATH_PATTERN = "/about-iris"; + + private static final String LEARNING_PATH_PATH_PATTERN = "/courses/{courseId}/learning-path"; + + private static final String COURSE_PATH_PATTERN = "/courses/{courseId}"; + private static final Logger log = LoggerFactory.getLogger(Lti13Service.class); private final UserRepository userRepository; private final ExerciseRepository exerciseRepository; + private final LectureRepository lectureRepository; + private final CourseRepository courseRepository; private final Lti13ResourceLaunchRepository launchRepository; @@ -86,11 +100,13 @@ public class Lti13Service { private final RestTemplate restTemplate; - public Lti13Service(UserRepository userRepository, ExerciseRepository exerciseRepository, CourseRepository courseRepository, Lti13ResourceLaunchRepository launchRepository, - LtiService ltiService, ResultRepository resultRepository, Lti13TokenRetriever tokenRetriever, OnlineCourseConfigurationService onlineCourseConfigurationService, - RestTemplate restTemplate, ArtemisAuthenticationProvider artemisAuthenticationProvider, LtiPlatformConfigurationRepository ltiPlatformConfigurationRepository) { + public Lti13Service(UserRepository userRepository, ExerciseRepository exerciseRepository, LectureRepository lectureRepository, CourseRepository courseRepository, + Lti13ResourceLaunchRepository launchRepository, LtiService ltiService, ResultRepository resultRepository, Lti13TokenRetriever tokenRetriever, + OnlineCourseConfigurationService onlineCourseConfigurationService, RestTemplate restTemplate, ArtemisAuthenticationProvider artemisAuthenticationProvider, + LtiPlatformConfigurationRepository ltiPlatformConfigurationRepository) { this.userRepository = userRepository; this.exerciseRepository = exerciseRepository; + this.lectureRepository = lectureRepository; this.courseRepository = courseRepository; this.ltiService = ltiService; this.launchRepository = launchRepository; @@ -109,20 +125,23 @@ public Lti13Service(UserRepository userRepository, ExerciseRepository exerciseRe * @param ltiIdToken the id token for the user launching the request * @param clientRegistrationId the clientRegistrationId of the source LMS */ + // TODO implement Iris support public void performLaunch(OidcIdToken ltiIdToken, String clientRegistrationId) { String targetLinkUrl = ltiIdToken.getClaim(Claims.TARGET_LINK_URI); Optional targetExercise = getExerciseFromTargetLink(targetLinkUrl); - if (targetExercise.isEmpty()) { - String message = "No exercise to launch at " + targetLinkUrl; + Optional targetLecture = getLectureFromTargetLink(targetLinkUrl); + Optional targetCourse = getCourseFromTargetLink(targetLinkUrl); + + if (targetCourse.isEmpty()) { + String message = "No course to launch at " + targetLinkUrl; log.error(message); - throw new BadRequestAlertException("Exercise not found", "LTI", "ltiExerciseNotFound"); + throw new BadRequestAlertException("Course not found", "LTI", "ltiCourseNotFound"); } - Exercise exercise = targetExercise.get(); - Course course = courseRepository.findByIdWithEagerOnlineCourseConfigurationElseThrow(exercise.getCourseViaExerciseGroupOrCourseMember().getId()); - OnlineCourseConfiguration onlineCourseConfiguration = course.getOnlineCourseConfiguration(); + Course course = targetCourse.get(); + OnlineCourseConfiguration onlineCourseConfiguration = courseRepository.findWithEagerOnlineCourseConfigurationById(course.getId()).getOnlineCourseConfiguration(); if (onlineCourseConfiguration == null) { - String message = "Exercise is not related to course for target link url: " + targetLinkUrl; + String message = "LTI is not configured for course with target link URL: " + targetLinkUrl; log.error(message); throw new BadRequestAlertException("LTI is not configured for this course", "LTI", "ltiNotConfigured"); } @@ -133,18 +152,24 @@ public void performLaunch(OidcIdToken ltiIdToken, String clientRegistrationId) { if (!onlineCourseConfiguration.isRequireExistingUser() && optionalUsername.isEmpty()) { SecurityContextHolder.getContext().setAuthentication(ltiService.createNewUserFromLaunchRequest(ltiIdToken.getEmail(), createUsernameFromLaunchRequest(ltiIdToken, onlineCourseConfiguration), ltiIdToken.getGivenName(), ltiIdToken.getFamilyName())); - } String username = optionalUsername.orElseGet(() -> createUsernameFromLaunchRequest(ltiIdToken, onlineCourseConfiguration)); User user = userRepository.findOneWithGroupsAndAuthoritiesByLogin(username).orElseThrow(); - ltiService.onSuccessfulLtiAuthentication(user, targetExercise.get()); Lti13LaunchRequest launchRequest = launchRequestFrom(ltiIdToken, clientRegistrationId); - createOrUpdateResourceLaunch(launchRequest, user, targetExercise.get()); - - ltiService.authenticateLtiUser(ltiIdToken.getEmail(), createUsernameFromLaunchRequest(ltiIdToken, onlineCourseConfiguration), ltiIdToken.getGivenName(), - ltiIdToken.getFamilyName(), onlineCourseConfiguration.isRequireExistingUser()); + if (targetExercise.isPresent()) { + Exercise exercise = targetExercise.get(); + handleLaunchRequest(launchRequest, user, exercise, ltiIdToken, onlineCourseConfiguration); + } + else if (getCompetencyFromTargetLink(targetLinkUrl) || getLearningPathFromTargetLink(targetLinkUrl) || targetLecture.isPresent()) { + handleLaunchRequest(launchRequest, user, null, ltiIdToken, onlineCourseConfiguration); + } + else { + String message = "No exercise or competency to launch at " + targetLinkUrl; + log.error(message); + throw new BadRequestAlertException("Exercise not found", "LTI", "ltiExerciseNotFound"); + } } /** @@ -311,6 +336,141 @@ private Optional getExerciseFromTargetLink(String targetLinkUrl) { return exerciseOpt; } + /** + * Returns an Optional of a Lecture that was referenced by targetLinkUrl. + * + * @param targetLinkUrl to retrieve a Lecture + * @return the Lecture or nothing otherwise + */ + private Optional getLectureFromTargetLink(String targetLinkUrl) { + AntPathMatcher matcher = new AntPathMatcher(); + + String targetLinkPath; + try { + targetLinkPath = new URI(targetLinkUrl).getPath(); + } + catch (URISyntaxException ex) { + log.info("Malformed target link url: {}", targetLinkUrl); + return Optional.empty(); + } + + if (!matcher.match(LECTURE_PATH_PATTERN, targetLinkPath)) { + log.info("Could not extract lecture from target link: {}", targetLinkUrl); + return Optional.empty(); + } + + Map pathVariables = matcher.extractUriTemplateVariables(LECTURE_PATH_PATTERN, targetLinkPath); + + String lectureId = pathVariables.get("lectureId"); + + Optional lectureOpt = lectureRepository.findById(Long.valueOf(lectureId)); + + if (lectureOpt.isEmpty()) { + log.info("Could not find lecture for target link url: {}", targetLinkUrl); + return Optional.empty(); + } + + return lectureOpt; + } + + /** + * Returns an Optional of a Course that was referenced by targetLinkUrl. + * + * @param targetLinkUrl the target link URL to retrieve a Course + * @return the Course or nothing otherwise + */ + public Optional getCourseFromTargetLink(String targetLinkUrl) { + AntPathMatcher matcher = new AntPathMatcher(); + + String targetLinkPath; + try { + targetLinkPath = new URI(targetLinkUrl).getPath(); + } + catch (URISyntaxException ex) { + log.info("Malformed target link url: {}", targetLinkUrl); + return Optional.empty(); + } + + Map pathVariables = null; + if (matcher.match(COURSE_PATH_PATTERN, targetLinkPath)) { + pathVariables = matcher.extractUriTemplateVariables(COURSE_PATH_PATTERN, targetLinkPath); + } + else if (matcher.match(COMPETENCY_PATH_PATTERN, targetLinkPath)) { + pathVariables = matcher.extractUriTemplateVariables(COMPETENCY_PATH_PATTERN, targetLinkPath); + } + else if (matcher.match(EXERCISE_PATH_PATTERN, targetLinkPath)) { + pathVariables = matcher.extractUriTemplateVariables(EXERCISE_PATH_PATTERN, targetLinkPath); + } + else if (matcher.match(LEARNING_PATH_PATH_PATTERN, targetLinkPath)) { + pathVariables = matcher.extractUriTemplateVariables(LEARNING_PATH_PATH_PATTERN, targetLinkPath); + } + else if (matcher.match(LECTURE_PATH_PATTERN, targetLinkPath)) { + pathVariables = matcher.extractUriTemplateVariables(LECTURE_PATH_PATTERN, targetLinkPath); + } + + if (pathVariables == null || !pathVariables.containsKey("courseId")) { + log.info("Could not extract courseId from target link: {}", targetLinkUrl); + return Optional.empty(); + } + + String courseId = pathVariables.get("courseId"); + return courseRepository.findById(Long.valueOf(courseId)); + } + + /** + * Checks if the target link URL references a competency. + * + * @param targetLinkUrl the target link URL + * @return true if the target link URL references a competency, false otherwise + */ + private boolean getCompetencyFromTargetLink(String targetLinkUrl) { + AntPathMatcher matcher = new AntPathMatcher(); + + String targetLinkPath; + try { + targetLinkPath = new URI(targetLinkUrl).getPath(); + } + catch (URISyntaxException ex) { + log.info("Malformed target link url: {}", targetLinkUrl); + return false; + } + + if (!matcher.match(COMPETENCY_PATH_PATTERN, targetLinkPath)) { + log.info("Could not extract competency from target link: {}", targetLinkUrl); + return false; + } + + log.info("Competency link detected: {}", targetLinkUrl); + return true; + } + + /** + * Checks if the target link URL references a learning path. + * + * @param targetLinkUrl the target link URL + * @return true if the target link URL references a learning path, false otherwise + */ + private boolean getLearningPathFromTargetLink(String targetLinkUrl) { + AntPathMatcher matcher = new AntPathMatcher(); + + String targetLinkPath; + try { + targetLinkPath = new URI(targetLinkUrl).getPath(); + } + catch (URISyntaxException ex) { + log.info("Malformed target link URL: {}", targetLinkUrl); + return false; + } + + if (!matcher.match(LEARNING_PATH_PATH_PATTERN, targetLinkPath)) { + log.info("Could not extract learning path from target link: {}", targetLinkUrl); + return false; + } + + log.info("Learning path link detected: {}", targetLinkUrl); + return true; + } + private void createOrUpdateResourceLaunch(Lti13LaunchRequest launchRequest, User user, Exercise exercise) { Optional launchOpt = launchRepository.findByIssAndSubAndDeploymentIdAndResourceLinkId(launchRequest.iss(), launchRequest.sub(), launchRequest.deploymentId(), launchRequest.resourceLinkId()); @@ -332,6 +492,17 @@ private void createOrUpdateResourceLaunch(Lti13LaunchRequest launchRequest, User launchRepository.save(launch); } + private void handleLaunchRequest(Lti13LaunchRequest launchRequest, User user, Exercise exercise, OidcIdToken ltiIdToken, OnlineCourseConfiguration onlineCourseConfiguration) { + createOrUpdateResourceLaunch(launchRequest, user, exercise); + + ltiService.authenticateLtiUser(ltiIdToken.getEmail(), createUsernameFromLaunchRequest(ltiIdToken, onlineCourseConfiguration), ltiIdToken.getGivenName(), + ltiIdToken.getFamilyName(), onlineCourseConfiguration.isRequireExistingUser()); + + if (exercise != null) { + ltiService.onSuccessfulLtiAuthentication(user, exercise); + } + } + /** * Build the response for the LTI launch. * diff --git a/src/main/java/de/tum/cit/aet/artemis/lti/service/LtiDeepLinkingService.java b/src/main/java/de/tum/cit/aet/artemis/lti/service/LtiDeepLinkingService.java index 7bd84f9c49c2..014cea88c55f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/lti/service/LtiDeepLinkingService.java +++ b/src/main/java/de/tum/cit/aet/artemis/lti/service/LtiDeepLinkingService.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -15,10 +16,14 @@ import org.springframework.stereotype.Service; import org.springframework.web.util.UriComponentsBuilder; +import de.tum.cit.aet.artemis.atlas.domain.competency.Competency; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; +import de.tum.cit.aet.artemis.core.repository.CourseRepository; import de.tum.cit.aet.artemis.exercise.domain.Exercise; import de.tum.cit.aet.artemis.exercise.domain.IncludedInOverallScore; import de.tum.cit.aet.artemis.exercise.repository.ExerciseRepository; +import de.tum.cit.aet.artemis.lecture.domain.Lecture; +import de.tum.cit.aet.artemis.lecture.repository.LectureRepository; import de.tum.cit.aet.artemis.lti.config.Lti13TokenRetriever; import de.tum.cit.aet.artemis.lti.dto.Lti13DeepLinkingResponse; @@ -34,8 +39,12 @@ public class LtiDeepLinkingService { private static final double DEFAULT_SCORE_MAXIMUM = 100D; + private final CourseRepository courseRepository; + private final ExerciseRepository exerciseRepository; + private final LectureRepository lectureRepository; + private final Lti13TokenRetriever tokenRetriever; /** @@ -44,8 +53,11 @@ public class LtiDeepLinkingService { * @param exerciseRepository The repository for exercises. * @param tokenRetriever The LTI 1.3 token retriever. */ - public LtiDeepLinkingService(ExerciseRepository exerciseRepository, Lti13TokenRetriever tokenRetriever) { + public LtiDeepLinkingService(CourseRepository courseRepository, ExerciseRepository exerciseRepository, LectureRepository lectureRepository, + Lti13TokenRetriever tokenRetriever) { + this.courseRepository = courseRepository; this.exerciseRepository = exerciseRepository; + this.lectureRepository = lectureRepository; this.tokenRetriever = tokenRetriever; } @@ -55,15 +67,32 @@ public LtiDeepLinkingService(ExerciseRepository exerciseRepository, Lti13TokenRe * @param ltiIdToken OIDC ID token with the user's authentication claims. * @param clientRegistrationId Client registration ID for the LTI tool. * @param courseId ID of the course for deep linking. - * @param exerciseIds Set of IDs of the exercises for deep linking. + * @param unitIds Set of IDs of the exercises/lectures for deep linking. * @return Constructed deep linking response URL. * @throws BadRequestAlertException if there are issues with the OIDC ID token claims. */ - public String performDeepLinking(OidcIdToken ltiIdToken, String clientRegistrationId, Long courseId, Set exerciseIds) { + public String performDeepLinking(OidcIdToken ltiIdToken, String clientRegistrationId, Long courseId, Set unitIds, DeepLinkingType type) { // Initialize DeepLinkingResponse Lti13DeepLinkingResponse lti13DeepLinkingResponse = Lti13DeepLinkingResponse.from(ltiIdToken, clientRegistrationId); - // Fill selected exercise link into content items - ArrayList> contentItems = this.populateContentItems(String.valueOf(courseId), exerciseIds); + // Dynamically populate content items based on the type + ArrayList> contentItems = switch (type) { + case EXERCISE -> { + if (unitIds == null || unitIds.isEmpty()) { + throw new BadRequestAlertException("No exercise IDs provided for deep linking", "LTI", "noExerciseIds"); + } + yield populateExerciseContentItems(String.valueOf(courseId), unitIds); + } + case LECTURE -> { + if (unitIds == null || unitIds.isEmpty()) { + throw new BadRequestAlertException("No lecture IDs provided for deep linking", "LTI", "noLectureIds"); + } + yield populateLectureContentItems(String.valueOf(courseId), unitIds); + } + case COMPETENCY -> populateCompetencyContentItems(String.valueOf(courseId)); + case IRIS -> populateIrisContentItems(String.valueOf(courseId)); + case LEARNING_PATH -> populateLearningPathsContentItems(String.valueOf(courseId)); + default -> throw new BadRequestAlertException("Invalid deep linking type provided", "LTI", "invalidType"); + }; lti13DeepLinkingResponse = lti13DeepLinkingResponse.setContentItems(contentItems); // Prepare return url with jwt and id parameters @@ -98,23 +127,94 @@ private String buildLtiDeepLinkResponse(String clientRegistrationId, Lti13DeepLi * @param courseId The course ID. * @param exerciseIds The set of exercise IDs. */ - private ArrayList> populateContentItems(String courseId, Set exerciseIds) { + private ArrayList> populateExerciseContentItems(String courseId, Set exerciseIds) { ArrayList> contentItems = new ArrayList<>(); for (Long exerciseId : exerciseIds) { - Map item = setContentItem(courseId, String.valueOf(exerciseId)); + Map item = setExerciseContentItem(courseId, String.valueOf(exerciseId)); contentItems.add(item); } + return contentItems; + } + /** + * Populate content items for deep linking response with lectures. + * + * @param courseId The course ID. + * @param lectureIds The set of lecture IDs. + */ + private ArrayList> populateLectureContentItems(String courseId, Set lectureIds) { + ArrayList> contentItems = new ArrayList<>(); + for (Long lectureId : lectureIds) { + Map item = setLectureContentItem(courseId, String.valueOf(lectureId)); + contentItems.add(item); + + } return contentItems; } - private Map setContentItem(String courseId, String exerciseId) { + /** + * Populate content items for deep linking response with competencies. + * + * @param courseId The course ID. + */ + private ArrayList> populateCompetencyContentItems(String courseId) { + return new ArrayList<>(List.of(setCompetencyContentItem(courseId))); + } + + /** + * Populate content items for deep linking response with Iris. + * + * @param courseId The course ID. + */ + private ArrayList> populateIrisContentItems(String courseId) { + return new ArrayList<>(List.of(setIrisContentItem(courseId))); + } + + /** + * Populate content items for deep linking response with learning paths. + * + * @param courseId The course ID. + */ + private ArrayList> populateLearningPathsContentItems(String courseId) { + return new ArrayList<>(List.of(setLearningPathContentItem(courseId))); + } + + private Map setExerciseContentItem(String courseId, String exerciseId) { Optional exerciseOpt = exerciseRepository.findById(Long.valueOf(exerciseId)); String launchUrl = String.format(artemisServerUrl + "/courses/%s/exercises/%s", courseId, exerciseId); - return exerciseOpt.map(exercise -> createContentItem(exerciseOpt.get(), launchUrl)).orElse(null); + return exerciseOpt.map(exercise -> createExerciseContentItem(exerciseOpt.get(), launchUrl)).orElse(null); } - private Map createContentItem(Exercise exercise, String url) { + private Map setLectureContentItem(String courseId, String lectureId) { + Optional lectureOpt = lectureRepository.findById(Long.valueOf(lectureId)); + String launchUrl = String.format(artemisServerUrl + "/courses/%s/lectures/%s", courseId, lectureId); + return lectureOpt.map(lecture -> createLectureContentItem(lectureOpt.get(), launchUrl)).orElse(null); + } + + private Map setCompetencyContentItem(String courseId) { + Optional competencyOpt = courseRepository.findWithEagerCompetenciesAndPrerequisitesById(Long.parseLong(courseId)) + .flatMap(course -> course.getCompetencies().stream().findFirst()); + String launchUrl = String.format(artemisServerUrl + "/courses/%s/competencies", courseId); + return competencyOpt.map(competency -> createSingleUnitContentItem(launchUrl)).orElse(null); + } + + private Map setLearningPathContentItem(String courseId) { + boolean hasLearningPaths = courseRepository.findWithEagerLearningPathsAndLearningPathCompetenciesByIdElseThrow(Long.parseLong(courseId)).getLearningPathsEnabled(); + if (hasLearningPaths) { + String launchUrl = String.format(artemisServerUrl + "/courses/%s/learning-path", courseId); + return createSingleUnitContentItem(launchUrl); + } + else + return null; + } + + private Map setIrisContentItem(String courseId) { + // TODO Iris optional + URL + String launchUrl = String.format(artemisServerUrl + "/about-iris", courseId); + return createSingleUnitContentItem(launchUrl); + } + + private Map createExerciseContentItem(Exercise exercise, String url) { Map item = new HashMap<>(); item.put("type", "ltiResourceLink"); @@ -125,6 +225,26 @@ private Map createContentItem(Exercise exercise, String url) { return item; } + private Map createLectureContentItem(Lecture lecture, String url) { + + Map item = new HashMap<>(); + item.put("type", "ltiResourceLink"); + item.put("title", lecture.getTitle()); + item.put("url", url); + + return item; + } + + private Map createSingleUnitContentItem(String url) { + + Map item = new HashMap<>(); + item.put("type", "ltiResourceLink"); + item.put("title", "competency"); + item.put("url", url); + + return item; + } + private void validateDeepLinkingResponseSettings(String returnURL, String jwt, String deploymentId) { if (isEmptyString(jwt)) { throw new BadRequestAlertException("Deep linking response cannot be created", "LTI", "deepLinkingResponseFailed"); diff --git a/src/main/java/de/tum/cit/aet/artemis/lti/web/LtiResource.java b/src/main/java/de/tum/cit/aet/artemis/lti/web/LtiResource.java index cb27da1cf735..6b2f28b19a3a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/lti/web/LtiResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/lti/web/LtiResource.java @@ -39,6 +39,7 @@ import de.tum.cit.aet.artemis.lti.domain.LtiPlatformConfiguration; import de.tum.cit.aet.artemis.lti.domain.OnlineCourseConfiguration; import de.tum.cit.aet.artemis.lti.repository.LtiPlatformConfigurationRepository; +import de.tum.cit.aet.artemis.lti.service.DeepLinkingType; import de.tum.cit.aet.artemis.lti.service.LtiDeepLinkingService; import de.tum.cit.aet.artemis.lti.service.OnlineCourseConfigurationService; import tech.jhipster.web.util.PaginationUtil; @@ -131,10 +132,14 @@ public ResponseEntity updateOnlineCourseConfiguration * @param clientRegistrationId The identifier online of the course configuration. * @return A ResponseEntity containing a JSON object with the 'targetLinkUri' property set to the deep linking response target link. */ + // TODO Deep Linking 1 @PostMapping("lti13/deep-linking/{courseId}") @EnforceAtLeastInstructor - public ResponseEntity lti13DeepLinking(@PathVariable Long courseId, @RequestParam(name = "exerciseIds") Set exerciseIds, + public ResponseEntity lti13DeepLinking(@PathVariable Long courseId, @RequestParam(name = "exerciseIds", required = false) Set exerciseIds, + @RequestParam(name = "lectureIds", required = false) Set lectureIds, @RequestParam(name = "competency", required = false) boolean competency, + @RequestParam(name = "learningPath", required = false) boolean learningPath, @RequestParam(name = "iris", required = false) boolean iris, @RequestParam(name = "ltiIdToken") String ltiIdToken, @RequestParam(name = "clientRegistrationId") String clientRegistrationId) throws ParseException { + // TODO update message log.info("LTI 1.3 Deep Linking request received for course {} with exercises {} for registrationId {}", courseId, exerciseIds, clientRegistrationId); Course course = courseRepository.findByIdWithEagerOnlineCourseConfigurationElseThrow(courseId); @@ -146,7 +151,26 @@ public ResponseEntity lti13DeepLinking(@PathVariable Long courseId, @Req OidcIdToken idToken = new OidcIdToken(ltiIdToken, null, null, SignedJWT.parse(ltiIdToken).getJWTClaimsSet().getClaims()); - String targetLink = ltiDeepLinkingService.performDeepLinking(idToken, clientRegistrationId, courseId, exerciseIds); + String targetLink; + + if (exerciseIds != null) { + targetLink = ltiDeepLinkingService.performDeepLinking(idToken, clientRegistrationId, courseId, exerciseIds, DeepLinkingType.EXERCISE); + } + else if (lectureIds != null) { + targetLink = ltiDeepLinkingService.performDeepLinking(idToken, clientRegistrationId, courseId, lectureIds, DeepLinkingType.LECTURE); + } + else if (competency) { + targetLink = ltiDeepLinkingService.performDeepLinking(idToken, clientRegistrationId, courseId, null, DeepLinkingType.COMPETENCY); + } + else if (learningPath) { + targetLink = ltiDeepLinkingService.performDeepLinking(idToken, clientRegistrationId, courseId, null, DeepLinkingType.LEARNING_PATH); + } + else if (iris) { + targetLink = ltiDeepLinkingService.performDeepLinking(idToken, clientRegistrationId, courseId, null, DeepLinkingType.IRIS); + } + else { + throw new BadRequestAlertException("No valid deep linking type provided", "LTI", "invalidDeepLinkingType"); + } ObjectNode json = new ObjectMapper().createObjectNode(); json.put("targetLinkUri", targetLink); diff --git a/src/main/java/de/tum/cit/aet/artemis/lti/web/open/PublicLtiResource.java b/src/main/java/de/tum/cit/aet/artemis/lti/web/open/PublicLtiResource.java index c0594b5ba1ca..a94bd5ee6498 100644 --- a/src/main/java/de/tum/cit/aet/artemis/lti/web/open/PublicLtiResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/lti/web/open/PublicLtiResource.java @@ -54,6 +54,7 @@ public class PublicLtiResource { @EnforceNothing public ResponseEntity lti13LaunchRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException { String state = request.getParameter("state"); + if (state == null) { errorOnMissingParameter(response, "state"); return ResponseEntity.ok().build(); diff --git a/src/main/resources/config/liquibase/changelog/20250114140000_changelog.xml b/src/main/resources/config/liquibase/changelog/20250114140000_changelog.xml new file mode 100644 index 000000000000..22d2f6f7c756 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250114140000_changelog.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 02ac0209b9d1..3f3c84362612 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -45,6 +45,7 @@ + diff --git a/src/main/webapp/app/course/manage/course-management.service.ts b/src/main/webapp/app/course/manage/course-management.service.ts index 895d180db81a..2609e4150620 100644 --- a/src/main/webapp/app/course/manage/course-management.service.ts +++ b/src/main/webapp/app/course/manage/course-management.service.ts @@ -137,6 +137,16 @@ export class CourseManagementService { .pipe(map((res: EntityResponseType) => this.processCourseEntityResponseType(res))); } + /** + * finds the course with the provided unique identifier together with its exercises + * @param courseId - the id of the course to be found + */ + findWithExercisesAndLecturesAndCompetencies(courseId: number): Observable { + return this.http + .get(`${this.resourceUrl}/${courseId}/with-exercises-lectures-competencies`, { observe: 'response' }) + .pipe(map((res: EntityResponseType) => this.processCourseEntityResponseType(res))); + } + /** * finds a course with the given id and eagerly loaded organizations * @param courseId the id of the course to be found diff --git a/src/main/webapp/app/lti/lti13-deep-linking.component.html b/src/main/webapp/app/lti/lti13-deep-linking.component.html index a3964e4ac713..331bb0d0281a 100644 --- a/src/main/webapp/app/lti/lti13-deep-linking.component.html +++ b/src/main/webapp/app/lti/lti13-deep-linking.component.html @@ -8,74 +8,147 @@