Skip to content

Commit 96a2285

Browse files
authored
General`: Support LTI learning activities for multiple lectures (#10688)
1 parent ce7ff10 commit 96a2285

File tree

11 files changed

+230
-162
lines changed

11 files changed

+230
-162
lines changed

src/main/java/de/tum/cit/aet/artemis/lti/service/DeepLinkingType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
public enum DeepLinkingType {
44

5-
EXERCISE, GROUPED_EXERCISE, LECTURE, COMPETENCY, LEARNING_PATH, IRIS;
5+
EXERCISE, GROUPED_EXERCISE, LECTURE, GROUPED_LECTURE, COMPETENCY, LEARNING_PATH, IRIS;
66

77
/**
88
* Get the enum value from a string.

src/main/java/de/tum/cit/aet/artemis/lti/service/LtiDeepLinkingService.java

Lines changed: 57 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
import java.net.URLEncoder;
66
import java.nio.charset.StandardCharsets;
7-
import java.util.ArrayList;
8-
import java.util.Arrays;
97
import java.util.List;
108
import java.util.Map;
119
import java.util.Optional;
@@ -34,6 +32,8 @@
3432

3533
/**
3634
* Service for handling LTI deep linking functionality.
35+
* This includes building and returning appropriate LTI launch URLs
36+
* for various Artemis content types such as exercises, lectures, competencies, etc.
3737
*/
3838
@Service
3939
@Profile(PROFILE_LTI)
@@ -78,6 +78,7 @@ public String performDeepLinking(OidcIdToken ltiIdToken, String clientRegistrati
7878
case EXERCISE -> populateExerciseContentItems(String.valueOf(courseId), unitIds);
7979
case GROUPED_EXERCISE -> List.of(populateGroupedExerciseContentItem(String.valueOf(courseId), unitIds));
8080
case LECTURE -> populateLectureContentItems(String.valueOf(courseId), unitIds);
81+
case GROUPED_LECTURE -> List.of(populateGroupedLectureContentItems(String.valueOf(courseId), unitIds));
8182
case COMPETENCY -> populateCompetencyContentItems(String.valueOf(courseId));
8283
case IRIS -> populateIrisContentItems(String.valueOf(courseId));
8384
case LEARNING_PATH -> populateLearningPathsContentItems(String.valueOf(courseId));
@@ -90,9 +91,11 @@ public String performDeepLinking(OidcIdToken ltiIdToken, String clientRegistrati
9091
}
9192

9293
/**
93-
* Build an LTI deep linking response URL.
94+
* Creates the deep linking launch URL that includes encoded JWT and parameters required by the LTI platform.
9495
*
95-
* @return The LTI deep link response URL.
96+
* @param clientRegistrationId Registration ID of the LTI client.
97+
* @param lti13DeepLinkingResponse Object holding the LTI claims and return URL.
98+
* @return Final URL to be sent back to the LTI platform for launching.
9699
*/
97100
private String buildLtiDeepLinkResponse(String clientRegistrationId, Lti13DeepLinkingResponse lti13DeepLinkingResponse) {
98101
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(this.artemisServerUrl + "/lti/select-content");
@@ -110,28 +113,39 @@ private String buildLtiDeepLinkResponse(String clientRegistrationId, Lti13DeepLi
110113
}
111114

112115
/**
113-
* Populate content items for deep linking response with exercises.
116+
* Maps each exercise ID to an individual LTI content item.
114117
*/
115118
private List<LtiContentItem> populateExerciseContentItems(String courseId, Set<Long> exerciseIds) {
116119
validateUnitIds(exerciseIds, DeepLinkingType.EXERCISE);
117120
return exerciseIds.stream().map(exerciseId -> setExerciseContentItem(courseId, String.valueOf(exerciseId))).toList();
118121
}
119122

123+
/**
124+
* Groups a set of exercises into one content item.
125+
*/
120126
private LtiContentItem populateGroupedExerciseContentItem(String courseId, Set<Long> exerciseIds) {
121127
validateUnitIds(exerciseIds, DeepLinkingType.GROUPED_EXERCISE);
122128
return setGroupedExerciseContentItem(courseId, exerciseIds);
123129
}
124130

125131
/**
126-
* Populate content items for deep linking response with lectures.
132+
* Maps each lecture ID to an individual LTI content item.
127133
*/
128134
private List<LtiContentItem> populateLectureContentItems(String courseId, Set<Long> lectureIds) {
129135
validateUnitIds(lectureIds, DeepLinkingType.LECTURE);
130136
return lectureIds.stream().map(lectureId -> setLectureContentItem(courseId, String.valueOf(lectureId))).toList();
131137
}
132138

133139
/**
134-
* Populate content items for deep linking response with competencies.
140+
* Groups a set of lectures into one content item.
141+
*/
142+
private LtiContentItem populateGroupedLectureContentItems(String courseId, Set<Long> lectureIds) {
143+
validateUnitIds(lectureIds, DeepLinkingType.GROUPED_LECTURE);
144+
return setGroupedLectureContentItem(courseId, lectureIds);
145+
}
146+
147+
/**
148+
* Prepares a content item pointing to the first available competency in the course.
135149
*/
136150
private List<LtiContentItem> populateCompetencyContentItems(String courseId) {
137151
Optional<Competency> competencyOpt = courseRepository.findWithEagerCompetenciesAndPrerequisitesById(Long.parseLong(courseId))
@@ -142,7 +156,7 @@ private List<LtiContentItem> populateCompetencyContentItems(String courseId) {
142156
}
143157

144158
/**
145-
* Populate content items for deep linking response with Iris.
159+
* Prepares a content item for launching the Iris analytics dashboard.
146160
*/
147161
private List<LtiContentItem> populateIrisContentItems(String courseId) {
148162
Optional<Course> courseOpt = courseRepository.findById(Long.parseLong(courseId));
@@ -156,7 +170,7 @@ private List<LtiContentItem> populateIrisContentItems(String courseId) {
156170
}
157171

158172
/**
159-
* Populate content items for deep linking response with learning paths.
173+
* Prepares a content item pointing to the learning path of the course.
160174
*/
161175
private List<LtiContentItem> populateLearningPathsContentItems(String courseId) {
162176
boolean hasLearningPaths = courseRepository.findWithEagerLearningPathsAndLearningPathCompetenciesByIdElseThrow(Long.parseLong(courseId)).getLearningPathsEnabled();
@@ -170,7 +184,7 @@ private List<LtiContentItem> populateLearningPathsContentItems(String courseId)
170184
}
171185

172186
/**
173-
* Set a content item for an exercise.
187+
* Create a content item for a specific exercise.
174188
*/
175189
private LtiContentItem setExerciseContentItem(String courseId, String exerciseId) {
176190
Optional<Exercise> exerciseOpt = exerciseRepository.findById(Long.valueOf(exerciseId));
@@ -179,22 +193,22 @@ private LtiContentItem setExerciseContentItem(String courseId, String exerciseId
179193
.orElseThrow(() -> new BadRequestAlertException("Exercise not found.", "LTI", "exerciseNotFound"));
180194
}
181195

196+
/**
197+
* Create a content item for a group of exercises.
198+
*/
182199
private LtiContentItem setGroupedExerciseContentItem(String courseId, Set<Long> exerciseIds) {
183-
List<Exercise> exercises = new ArrayList<>();
184200

185-
for (Long exerciseId : exerciseIds) {
186-
Optional<Exercise> exerciseOpt = exerciseRepository.findById(exerciseId);
187-
exerciseOpt.ifPresent(exercises::add);
188-
}
201+
List<Exercise> exercises = exerciseRepository.findAllById(exerciseIds);
202+
189203
if (exercises.isEmpty()) {
190204
throw new BadRequestAlertException("No exercises found.", "LTI", "exercisesNotFound");
191205
}
192-
String launchUrl = buildContentUrl(courseId, "groupedExercises", exercises.stream().map(Exercise::getId).map(String::valueOf).collect(Collectors.joining(",")));
206+
String launchUrl = buildGroupedResourceUrl(courseId, exercises.stream().map(Exercise::getId).collect(Collectors.toSet()), "exercises", "exerciseIDs", "noExerciseIds");
193207
return createGroupedExerciseContentItem(exercises, launchUrl);
194208
}
195209

196210
/**
197-
* Set a content item for a lecture.
211+
* Create a content item for a specific lecture.
198212
*/
199213
private LtiContentItem setLectureContentItem(String courseId, String lectureId) {
200214
String launchUrl = buildContentUrl(courseId, "lectures", lectureId);
@@ -203,8 +217,19 @@ private LtiContentItem setLectureContentItem(String courseId, String lectureId)
203217
}
204218

205219
/**
206-
* Create a content item for an exercise.
220+
* Create a content item for a group of lectures.
207221
*/
222+
private LtiContentItem setGroupedLectureContentItem(String courseId, Set<Long> lectureIds) {
223+
224+
List<Lecture> lectures = lectureRepository.findAllById(lectureIds);
225+
226+
if (lectures.isEmpty()) {
227+
throw new BadRequestAlertException("No lectures found.", "LTI", "lecturesNotFound");
228+
}
229+
String launchUrl = buildGroupedResourceUrl(courseId, lectures.stream().map(Lecture::getId).collect(Collectors.toSet()), "lectures", "lectureIDs", "noLectureIds");
230+
return createGroupedLectureContentItem(launchUrl);
231+
}
232+
208233
private LtiContentItem createExerciseContentItem(Exercise exercise, String url) {
209234
LineItem lineItem = exercise.getIncludedInOverallScore() != IncludedInOverallScore.NOT_INCLUDED ? new LineItem(DEFAULT_SCORE_MAXIMUM) : null;
210235
return new LtiContentItem("ltiResourceLink", exercise.getTitle(), url, lineItem);
@@ -217,13 +242,14 @@ private LtiContentItem createGroupedExerciseContentItem(List<Exercise> exercises
217242
return new LtiContentItem("ltiResourceLink", "Grouped Exercises", url, lineItem);
218243
}
219244

220-
/**
221-
* Create a content item for a lecture.
222-
*/
223245
private LtiContentItem createLectureContentItem(Lecture lecture, String url) {
224246
return new LtiContentItem("ltiResourceLink", lecture.getTitle(), url, null);
225247
}
226248

249+
private LtiContentItem createGroupedLectureContentItem(String url) {
250+
return new LtiContentItem("ltiResourceLink", "Grouped Lectures", url, null);
251+
}
252+
227253
/**
228254
* Create a content item for a single unit (e.g., competency, learning path, Iris).
229255
*/
@@ -244,28 +270,22 @@ private void validateUnitIds(Set<Long> unitIds, DeepLinkingType type) {
244270
* Build a content URL for deep linking.
245271
*/
246272
private String buildContentUrl(String courseId, String resourceType, String resourceId) {
247-
if ("groupedExercises".equals(resourceType)) {
248-
List<Long> exerciseIds = Arrays.stream(resourceId.split(",")).map(String::trim).map(Long::valueOf).toList();
249-
250-
// Take the smallest exercise ID for the base URL to establish it as "starting" exercise for LTI view
251-
Long smallestExerciseId = exerciseIds.stream().min(Long::compareTo).orElseThrow(() -> new BadRequestAlertException("No exercise IDs provided", "LTI", "noExerciseIds"));
252-
253-
String baseUrl = String.format("%s/courses/%s/exercises/%d", artemisServerUrl, courseId, smallestExerciseId);
254-
255-
// Include all exercise IDs in the query parameter for sidebar content
256-
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(baseUrl).queryParam("isMultiLaunch", true).queryParam("exerciseIDs", resourceId);
257-
258-
return uriBuilder.toUriString();
259-
}
260-
else {
261-
return String.format("%s/courses/%s/%s/%s", artemisServerUrl, courseId, resourceType, resourceId);
262-
}
273+
return String.format("%s/courses/%s/%s/%s", artemisServerUrl, courseId, resourceType, resourceId);
263274
}
264275

265276
private String buildContentUrl(String courseId, String resourceType) {
266277
return String.format("%s/courses/%s/%s", artemisServerUrl, courseId, resourceType);
267278
}
268279

280+
private String buildGroupedResourceUrl(String courseId, Set<Long> ids, String pathSegment, String queryParamKey, String alertKey) {
281+
Long smallestId = ids.stream().min(Long::compareTo).orElseThrow(() -> new BadRequestAlertException("No IDs provided", "LTI", alertKey));
282+
283+
String baseUrl = String.format("%s/courses/%s/%s/%d", artemisServerUrl, courseId, pathSegment, smallestId);
284+
String joinedIds = ids.stream().map(String::valueOf).collect(Collectors.joining(","));
285+
286+
return UriComponentsBuilder.fromUriString(baseUrl).queryParam("isMultiLaunch", true).queryParam(queryParamKey, joinedIds).toUriString();
287+
}
288+
269289
/**
270290
* Validate deep linking response settings.
271291
*/

src/main/java/de/tum/cit/aet/artemis/lti/web/LtiResource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ public ResponseEntity<String> lti13DeepLinking(@PathVariable Long courseId, @Req
158158
OidcIdToken idToken = new OidcIdToken(ltiIdToken, null, null, SignedJWT.parse(ltiIdToken).getJWTClaimsSet().getClaims());
159159

160160
String targetLink = switch (resourceType) {
161-
case EXERCISE, LECTURE, GROUPED_EXERCISE -> {
161+
case EXERCISE, LECTURE, GROUPED_EXERCISE, GROUPED_LECTURE -> {
162162
if (contentIds == null || contentIds.isEmpty()) {
163163
throw new BadRequestAlertException("Content IDs are required for resource type: " + resourceType, "LTI", "contentIdsRequired");
164164
}

0 commit comments

Comments
 (0)