Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Development: Extend lti content selection table #9981

Draft
wants to merge 45 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
134b7b8
Mock test
raffifasaro Dec 9, 2024
2f27e4b
Mock test 2
raffifasaro Dec 9, 2024
afdf7f0
mock table test
raffifasaro Dec 9, 2024
9e8c5f1
mock table test fix
raffifasaro Dec 9, 2024
d19bc5d
mock table fix
raffifasaro Dec 9, 2024
358d5c7
mock table comment out dates
raffifasaro Dec 9, 2024
2bd644d
table fix
raffifasaro Dec 9, 2024
5927c0a
Merge branch 'develop' into chore/lti/extend-content-selection-table
raffifasaro Dec 14, 2024
8f2887d
expand table
raffifasaro Dec 14, 2024
2909b48
find lectures
raffifasaro Dec 14, 2024
31a2fcb
fix
raffifasaro Dec 14, 2024
2d0d385
Add working requests
raffifasaro Dec 15, 2024
916a378
fix
raffifasaro Dec 15, 2024
59ca782
Add translation support
raffifasaro Dec 15, 2024
a51cd68
fix
raffifasaro Dec 15, 2024
469a78b
Iris selector + info text for competencies
raffifasaro Dec 17, 2024
23476f5
info text complete
raffifasaro Dec 18, 2024
8c885d1
Fix selection
raffifasaro Dec 18, 2024
2816925
Update sendDeepLinkRequest() for use with new features
raffifasaro Dec 18, 2024
fa28ef2
Merge branch 'develop' into chore/lti/extend-content-selection-table
raffifasaro Dec 18, 2024
2774b1e
Merge branch 'develop' into chore/lti/extend-content-selection-table
raffifasaro Dec 19, 2024
0844708
Fix competency checkbox header
raffifasaro Dec 19, 2024
010c1bc
Merge remote-tracking branch 'origin/chore/lti/extend-content-selecti…
raffifasaro Dec 19, 2024
e7f94ef
Merge branch 'develop' into chore/lti/extend-content-selection-table
raffifasaro Dec 30, 2024
46ea417
Merge branch 'develop' into chore/lti/extend-content-selection-table
raffifasaro Jan 2, 2025
e36102e
Merge branch 'develop' into chore/lti/extend-content-selection-table
raffifasaro Jan 3, 2025
0ccd8d3
add competency support test
raffifasaro Jan 3, 2025
fc7b481
fix competency selection frontend
raffifasaro Jan 3, 2025
1812260
fix parameter requirements
raffifasaro Jan 3, 2025
3b802f8
fix exercise Id check
raffifasaro Jan 3, 2025
6c28428
launch routes
raffifasaro Jan 3, 2025
22d6f7e
log request body
raffifasaro Jan 3, 2025
f5685da
Merge remote-tracking branch 'origin/chore/lti/extend-content-selecti…
raffifasaro Jan 3, 2025
4e72689
log data
raffifasaro Jan 3, 2025
38e0fa3
log error detailed
raffifasaro Jan 4, 2025
d2d7984
Merge branch 'develop' into chore/lti/extend-content-selection-table
raffifasaro Jan 4, 2025
3f22b69
log error detailed 2
raffifasaro Jan 5, 2025
1f680e6
Merge branch 'develop' into chore/lti/extend-content-selection-table
raffifasaro Jan 5, 2025
803f7ad
Merge branch 'develop' into chore/lti/extend-content-selection-table
raffifasaro Jan 5, 2025
50282d5
Merge branch 'develop' into chore/lti/extend-content-selection-table
raffifasaro Jan 5, 2025
534328f
remove log
raffifasaro Jan 6, 2025
1cd6605
Merge remote-tracking branch 'origin/chore/lti/extend-content-selecti…
raffifasaro Jan 6, 2025
3594b05
Merge branch 'develop' into chore/lti/extend-content-selection-table
raffifasaro Jan 6, 2025
5d9635a
force manual exercise test
raffifasaro Jan 7, 2025
62e060b
test
raffifasaro Jan 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,25 @@ public ResponseEntity<Course> 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<Course> getCourseWithExercisesAndLecturesAndCompetencies(@PathVariable Long courseId) {
log.debug("REST request to get course {} for tutors", courseId);
Optional<Course> 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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ public Lti13Service(UserRepository userRepository, ExerciseRepository exerciseRe
*/
public void performLaunch(OidcIdToken ltiIdToken, String clientRegistrationId) {
String targetLinkUrl = ltiIdToken.getClaim(Claims.TARGET_LINK_URI);
Optional<Exercise> targetExercise = getExerciseFromTargetLink(targetLinkUrl);
// Optional<Exercise> targetExercise = getExerciseFromTargetLink(targetLinkUrl);
Optional<Exercise> targetExercise = exerciseRepository.findById(456L);
if (targetExercise.isEmpty()) {
String message = "No exercise to launch at " + targetLinkUrl;
log.error(message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,31 @@ public LtiDeepLinkingService(ExerciseRepository exerciseRepository, Lti13TokenRe
* @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<Long> exerciseIds) {
public String performExerciseDeepLinking(OidcIdToken ltiIdToken, String clientRegistrationId, Long courseId, Set<Long> exerciseIds) {
// Initialize DeepLinkingResponse
Lti13DeepLinkingResponse lti13DeepLinkingResponse = Lti13DeepLinkingResponse.from(ltiIdToken, clientRegistrationId);
// Fill selected exercise link into content items
ArrayList<Map<String, Object>> contentItems = this.populateContentItems(String.valueOf(courseId), exerciseIds);
ArrayList<Map<String, Object>> contentItems = this.populateContentItems(String.valueOf(courseId), exerciseIds, false);
lti13DeepLinkingResponse = lti13DeepLinkingResponse.setContentItems(contentItems);

// Prepare return url with jwt and id parameters
return this.buildLtiDeepLinkResponse(clientRegistrationId, lti13DeepLinkingResponse);
}

/**
* Constructs an LTI Deep Linking response URL with JWT for the competencies of the course.
*
* @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.
* @return Constructed deep linking response URL.
* @throws BadRequestAlertException if there are issues with the OIDC ID token claims.
*/
public String performCompetencyDeepLinking(OidcIdToken ltiIdToken, String clientRegistrationId, Long courseId) {
// Initialize DeepLinkingResponse
Lti13DeepLinkingResponse lti13DeepLinkingResponse = Lti13DeepLinkingResponse.from(ltiIdToken, clientRegistrationId);
// Fill selected exercise link into content items
ArrayList<Map<String, Object>> contentItems = this.populateContentItems(String.valueOf(courseId), null, true);
lti13DeepLinkingResponse = lti13DeepLinkingResponse.setContentItems(contentItems);

// Prepare return url with jwt and id parameters
Expand Down Expand Up @@ -98,23 +118,34 @@ private String buildLtiDeepLinkResponse(String clientRegistrationId, Lti13DeepLi
* @param courseId The course ID.
* @param exerciseIds The set of exercise IDs.
*/
private ArrayList<Map<String, Object>> populateContentItems(String courseId, Set<Long> exerciseIds) {
private ArrayList<Map<String, Object>> populateContentItems(String courseId, Set<Long> exerciseIds, boolean competency) {
ArrayList<Map<String, Object>> contentItems = new ArrayList<>();
for (Long exerciseId : exerciseIds) {
Map<String, Object> item = setContentItem(courseId, String.valueOf(exerciseId));
contentItems.add(item);
if (!(exerciseIds == null)) {
for (Long exerciseId : exerciseIds) {
Map<String, Object> item = setExerciseContentItem(courseId, String.valueOf(exerciseId));
contentItems.add(item);
}
}
else if (competency) {
contentItems.add(setCompetencyContentItem(courseId));
}

return contentItems;
}

private Map<String, Object> setContentItem(String courseId, String exerciseId) {
private Map<String, Object> setExerciseContentItem(String courseId, String exerciseId) {
Optional<Exercise> 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<String, Object> createContentItem(Exercise exercise, String url) {
private Map<String, Object> setCompetencyContentItem(String courseId) {
// TODO competency optional
String launchUrl = String.format(artemisServerUrl + "/courses/%s/competencies", courseId);
return createCompetencyContentItem(launchUrl);
}

private Map<String, Object> createExerciseContentItem(Exercise exercise, String url) {

Map<String, Object> item = new HashMap<>();
item.put("type", "ltiResourceLink");
Expand All @@ -125,6 +156,16 @@ private Map<String, Object> createContentItem(Exercise exercise, String url) {
return item;
}

private Map<String, Object> createCompetencyContentItem(String url) {

Map<String, Object> 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");
Expand Down
15 changes: 13 additions & 2 deletions src/main/java/de/tum/cit/aet/artemis/lti/web/LtiResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,14 @@ public ResponseEntity<OnlineCourseConfiguration> 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<String> lti13DeepLinking(@PathVariable Long courseId, @RequestParam(name = "exerciseIds") Set<Long> exerciseIds,
public ResponseEntity<String> lti13DeepLinking(@PathVariable Long courseId, @RequestParam(name = "exerciseIds", required = false) Set<Long> exerciseIds,
@RequestParam(name = "lectureIds", required = false) Set<Long> 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);
Expand All @@ -146,7 +150,14 @@ public ResponseEntity<String> 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.performExerciseDeepLinking(idToken, clientRegistrationId, courseId, exerciseIds);
}
else if (competency) {
targetLink = ltiDeepLinkingService.performCompetencyDeepLinking(idToken, clientRegistrationId, courseId);
}

ObjectNode json = new ObjectMapper().createObjectNode();
json.put("targetLinkUri", targetLink);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public class PublicLtiResource {
@EnforceNothing
public ResponseEntity<Void> lti13LaunchRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
String state = request.getParameter("state");

if (state == null) {
errorOnMissingParameter(response, "state");
return ResponseEntity.ok().build();
Expand Down
10 changes: 10 additions & 0 deletions src/main/webapp/app/course/manage/course-management.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,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<EntityResponseType> {
return this.http
.get<Course>(`${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
Expand Down
9 changes: 9 additions & 0 deletions src/main/webapp/app/lti/lti.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ export const ltiLaunchRoutes: Routes = [
},
canActivate: [UserRouteAccessService],
},
{
path: 'competencies',
component: Lti13DeepLinkingComponent,
data: {
authorities: [Authority.INSTRUCTOR, Authority.ADMIN],
pageTitle: 'artemisApp.lti13.deepLinking.title',
},
canActivate: [UserRouteAccessService],
},
];

const LTI_LAUNCH_ROUTES = [...ltiLaunchRoutes];
Expand Down
Loading
Loading