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

Learning paths: Add explanation view for learning path users #9391

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -31,6 +31,12 @@ public class LearningPath extends DomainObject {
@Column(name = "progress")
private int progress;

/**
* flag indicating if a student started the learning path
*/
@Column(name = "started_by_student")
private boolean startedByStudent = false;

@ManyToOne
@JoinColumn(name = "user_id")
private User user;
Expand Down Expand Up @@ -89,8 +95,16 @@ public void removeCompetency(CourseCompetency competency) {
this.competencies.remove(competency);
}

public boolean isStartedByStudent() {
return startedByStudent;
}

public void setStartedByStudent(boolean startedByStudent) {
this.startedByStudent = startedByStudent;
}

@Override
public String toString() {
return "LearningPath{" + "id=" + getId() + ", user=" + user + ", course=" + course + ", competencies=" + competencies + '}';
return "LearningPath{" + "id=" + getId() + ", user=" + user + ", course=" + course + ", competencies=" + competencies + ", startedByStudent=" + startedByStudent + "}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package de.tum.cit.aet.artemis.atlas.dto;

import com.fasterxml.jackson.annotation.JsonInclude;

import de.tum.cit.aet.artemis.atlas.domain.competency.LearningPath;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record LearningPathDTO(long id, boolean startedByStudent, int progress) {

public static LearningPathDTO of(LearningPath learningPath) {
return new LearningPathDTO(learningPath.getId(), learningPath.isStartedByStudent(), learningPath.getProgress());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.stream.Collectors;

import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.BadRequestException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -25,6 +26,7 @@
import de.tum.cit.aet.artemis.atlas.dto.CompetencyGraphEdgeDTO;
import de.tum.cit.aet.artemis.atlas.dto.CompetencyGraphNodeDTO;
import de.tum.cit.aet.artemis.atlas.dto.LearningPathCompetencyGraphDTO;
import de.tum.cit.aet.artemis.atlas.dto.LearningPathDTO;
import de.tum.cit.aet.artemis.atlas.dto.LearningPathHealthDTO;
import de.tum.cit.aet.artemis.atlas.dto.LearningPathInformationDTO;
import de.tum.cit.aet.artemis.atlas.dto.LearningPathNavigationOverviewDTO;
Expand Down Expand Up @@ -243,6 +245,52 @@ private void updateLearningPathProgress(@NotNull LearningPath learningPath) {
log.debug("Updated LearningPath (id={}) for user (id={})", learningPath.getId(), userId);
}

/**
* Get the learning path for the current user in the given course.
*
* @param courseId the id of the course
* @return the learning path of the current user
*/
public LearningPathDTO getLearningPathForCurrentUser(long courseId) {
final var currentUser = userRepository.getUser();
final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(courseId, currentUser.getId());
return LearningPathDTO.of(learningPath);
}

/**
* Generate a learning path for the current user in the given course.
*
* @param courseId the id of the course
* @return the generated learning path
*/
public LearningPathDTO generateLearningPathForCurrentUser(long courseId) {
final var currentUser = userRepository.getUser();
final var course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(courseId);
if (learningPathRepository.findByCourseIdAndUserId(courseId, currentUser.getId()).isPresent()) {
throw new BadRequestException("Learning path already exists.");
}
final var learningPath = generateLearningPathForUser(course, currentUser);
return LearningPathDTO.of(learningPath);
}

/**
* Start the learning path for the current user in the given course.
*
* @param learningPathId the id of the learning path
*/
public void startLearningPathForCurrentUser(long learningPathId) {
final var learningPath = learningPathRepository.findByIdElseThrow(learningPathId);
final var currentUser = userRepository.getUser();
if (!learningPath.getUser().equals(currentUser)) {
throw new AccessForbiddenException("You are not allowed to start this learning path.");
}
else if (learningPath.isStartedByStudent()) {
throw new BadRequestException("Learning path already started.");
}
learningPath.setStartedByStudent(true);
learningPathRepository.save(learningPath);
}

/**
* Gets the health status of learning paths for the given course.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
Expand All @@ -28,6 +29,7 @@
import de.tum.cit.aet.artemis.atlas.dto.CompetencyNameDTO;
import de.tum.cit.aet.artemis.atlas.dto.CompetencyProgressForLearningPathDTO;
import de.tum.cit.aet.artemis.atlas.dto.LearningPathCompetencyGraphDTO;
import de.tum.cit.aet.artemis.atlas.dto.LearningPathDTO;
import de.tum.cit.aet.artemis.atlas.dto.LearningPathHealthDTO;
import de.tum.cit.aet.artemis.atlas.dto.LearningPathInformationDTO;
import de.tum.cit.aet.artemis.atlas.dto.LearningPathNavigationDTO;
Expand Down Expand Up @@ -301,19 +303,31 @@ private ResponseEntity<NgxLearningPathDTO> getLearningPathNgx(@PathVariable long
}

/**
* GET courses/:courseId/learning-path-id : Gets the id of the learning path.
* GET courses/:courseId/learning-path/me : Gets the learning path of the current user in the course.
*
* @param courseId the id of the course from which the learning path id should be fetched
* @return the ResponseEntity with status 200 (OK) and with body the id of the learning path
* @param courseId the id of the course for which the learning path should be fetched
* @return the ResponseEntity with status 200 (OK) and with body the learning path
*/
@GetMapping("courses/{courseId}/learning-path-id")
@GetMapping("courses/{courseId}/learning-path/me")
@EnforceAtLeastStudentInCourse
public ResponseEntity<Long> getLearningPathId(@PathVariable long courseId) {
log.debug("REST request to get learning path id for course with id: {}", courseId);
public ResponseEntity<LearningPathDTO> getLearningPathForCurrentUser(@PathVariable long courseId) {
log.debug("REST request to get learning path of current user for course with id: {}", courseId);
courseService.checkLearningPathsEnabledElseThrow(courseId);
User user = userRepository.getUser();
final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(courseId, user.getId());
return ResponseEntity.ok(learningPath.getId());
return ResponseEntity.ok(learningPathService.getLearningPathForCurrentUser(courseId));
}

/**
* PATCH learning-path/:learningPathId/start : Starts the learning path for the current user.
*
* @param learningPathId the id of the learning path to start
* @return the ResponseEntity with status 204 (NO_CONTENT)
*/
@PatchMapping("learning-path/{learningPathId}/start")
@EnforceAtLeastStudent
public ResponseEntity<Void> startLearningPathForCurrentUser(@PathVariable long learningPathId) {
log.debug("REST request to start learning path with id: {}", learningPathId);
learningPathService.startLearningPathForCurrentUser(learningPathId);
return ResponseEntity.noContent().build();
}

/**
Expand All @@ -324,20 +338,11 @@ public ResponseEntity<Long> getLearningPathId(@PathVariable long courseId) {
*/
@PostMapping("courses/{courseId}/learning-path")
@EnforceAtLeastStudentInCourse
public ResponseEntity<Long> generateLearningPath(@PathVariable long courseId) throws URISyntaxException {
log.debug("REST request to generate learning path for user in course with id: {}", courseId);
public ResponseEntity<LearningPathDTO> generateLearningPathForCurrentUser(@PathVariable long courseId) throws URISyntaxException {
log.debug("REST request to generate learning path for current user in course with id: {}", courseId);
courseService.checkLearningPathsEnabledElseThrow(courseId);

User user = userRepository.getUser();
final var learningPathOptional = learningPathRepository.findByCourseIdAndUserId(courseId, user.getId());

if (learningPathOptional.isPresent()) {
throw new BadRequestException("Learning path already exists.");
}

final var course = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(courseId);
final var learningPath = learningPathService.generateLearningPathForUser(course, user);
return ResponseEntity.created(new URI("api/learning-path/" + learningPath.getId())).body(learningPath.getId());
final var learningPathDTO = learningPathService.generateLearningPathForCurrentUser(courseId);
return ResponseEntity.created(new URI("api/learning-path/" + learningPathDTO.id())).body(learningPathDTO);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="20240924125742" author="johannes_wt">
<addColumn tableName="learning_path">
<column name="started_by_student" defaultValueBoolean="false" type="boolean">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
2 changes: 1 addition & 1 deletion src/main/resources/config/liquibase/master.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<include file="classpath:config/liquibase/changelog/20240708144500_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20240802091201_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20240804144500_changelog.xml" relativeToChangelogFile="false"/>

<include file="classpath:config/liquibase/changelog/20240924125742_changelog.xml" relativeToChangelogFile="false"/>
<!-- NOTE: please use the format "YYYYMMDDhhmmss_changelog.xml", i.e. year month day hour minutes seconds and not something else! -->
<!-- we should also stay in a chronological order! -->
<!-- you can use the command 'date '+%Y%m%d%H%M%S'' to get the current date and time in the correct format -->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<div class="col learning-path-student-page">
@if (isLearningPathIdLoading()) {
@if (isLearningPathLoading()) {
<div class="row justify-content-center align-items-center h-100">
<div class="spinner-border text-primary" role="status">
<span class="sr-only" jhiTranslate="loading"></span>
</div>
</div>
} @else if (learningPathId()) {
<jhi-learning-path-student-nav [learningPathId]="learningPathId()!" />
} @else if (learningPath() && learningPath()!.startedByStudent) {
<jhi-learning-path-student-nav [learningPathId]="learningPath()!.id" />
<div class="learning-path-student-content">
@if (currentLearningObject()?.type === LearningObjectType.LECTURE) {
<jhi-learning-path-lecture-unit [lectureUnitId]="currentLearningObject()!.id" />
Expand All @@ -27,10 +27,10 @@ <h3 class="mb-3" jhiTranslate="artemisApp.learningPath.completion.title"></h3>
<h3 class="mb-3" jhiTranslate="artemisApp.learningPath.generation.title"></h3>
<div jhiTranslate="artemisApp.learningPath.generation.description"></div>
<button
(click)="generateLearningPath(this.courseId())"
(click)="startLearningPath()"
type="button"
class="mt-4 btn btn-primary"
id="generate-learning-path-button"
id="start-learning-path-button"
jhiTranslate="artemisApp.learningPath.generation.generateButton"
></button>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component, effect, inject, signal } from '@angular/core';
import { LearningObjectType } from 'app/entities/competency/learning-path.model';
import { LearningObjectType, LearningPathDTO } from 'app/entities/competency/learning-path.model';
import { map } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common';
Expand Down Expand Up @@ -32,45 +32,49 @@ import { ArtemisSharedModule } from 'app/shared/shared.module';
export class LearningPathStudentPageComponent {
protected readonly LearningObjectType = LearningObjectType;

private readonly learningApiService: LearningPathApiService = inject(LearningPathApiService);
private readonly learningApiService = inject(LearningPathApiService);
private readonly learningPathNavigationService = inject(LearningPathNavigationService);
private readonly alertService: AlertService = inject(AlertService);
private readonly activatedRoute: ActivatedRoute = inject(ActivatedRoute);
private readonly alertService = inject(AlertService);
private readonly activatedRoute = inject(ActivatedRoute);

readonly isLearningPathIdLoading = signal(false);
readonly learningPathId = signal<number | undefined>(undefined);
readonly isLearningPathLoading = signal(false);
readonly learningPath = signal<LearningPathDTO | undefined>(undefined);
readonly courseId = toSignal(this.activatedRoute.parent!.parent!.params.pipe(map((params) => Number(params.courseId))), { requireSync: true });
readonly currentLearningObject = this.learningPathNavigationService.currentLearningObject;
readonly isLearningPathNavigationLoading = this.learningPathNavigationService.isLoading;

constructor() {
effect(async () => await this.loadLearningPathId(this.courseId()), { allowSignalWrites: true });
effect(() => this.loadLearningPathId(this.courseId()), { allowSignalWrites: true });
}

private async loadLearningPathId(courseId: number): Promise<void> {
try {
this.isLearningPathIdLoading.set(true);
const learningPathId = await this.learningApiService.getLearningPathId(courseId);
this.learningPathId.set(learningPathId);
this.isLearningPathLoading.set(true);
const learningPath = await this.learningApiService.getLearningPathForCurrentUser(courseId);
this.learningPath.set(learningPath);
} catch (error) {
// If learning path does not exist (404) ignore the error
if (!(error instanceof EntityNotFoundError)) {
this.alertService.error(error);
}
} finally {
this.isLearningPathIdLoading.set(false);
this.isLearningPathLoading.set(false);
}
}

async generateLearningPath(courseId: number): Promise<void> {
async startLearningPath(): Promise<void> {
try {
this.isLearningPathIdLoading.set(true);
const learningPathId = await this.learningApiService.generateLearningPath(courseId);
this.learningPathId.set(learningPathId);
this.isLearningPathLoading.set(true);
if (!this.learningPath()) {
const learningPath = await this.learningApiService.generateLearningPathForCurrentUser(this.courseId());
this.learningPath.set(learningPath);
}
await this.learningApiService.startLearningPathForCurrentUser(this.learningPath()!.id);
this.learningPath.update((learningPath) => ({ ...learningPath!, startedByStudent: true }));
} catch (error) {
this.alertService.error(error);
} finally {
this.isLearningPathIdLoading.set(false);
this.isLearningPathLoading.set(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,34 @@ export abstract class BaseApiHttpService {
): Promise<T> {
return await this.request<T>(HttpMethod.Post, url, { body: body, ...options });
}

/**
* Constructs a `PUT` request that interprets the body as JSON and
* returns a Promise of an object of type `T`.
*
* @param url The endpoint URL excluding the base server url (/api).
* @param body The content to include in the body of the request.
* @param options The HTTP options to send with the request.
* @protected
*
* @return An `Promise` of type `Object` (T),
*/
protected async patch<T>(
url: string,
body?: any,
options?: {
headers?:
| HttpHeaders
| {
[header: string]: string | string[];
};
params?:
| HttpParams
| {
[param: string]: string | number | boolean | ReadonlyArray<string | number | boolean>;
};
},
): Promise<T> {
return await this.request<T>(HttpMethod.Patch, url, { body: body, ...options });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
CompetencyGraphDTO,
LearningObjectType,
LearningPathCompetencyDTO,
LearningPathDTO,
LearningPathNavigationDTO,
LearningPathNavigationObjectDTO,
LearningPathNavigationOverviewDTO,
Expand All @@ -14,8 +15,12 @@ import { BaseApiHttpService } from 'app/course/learning-paths/services/base-api-
providedIn: 'root',
})
export class LearningPathApiService extends BaseApiHttpService {
async getLearningPathId(courseId: number): Promise<number> {
return await this.get<number>(`courses/${courseId}/learning-path-id`);
async getLearningPathForCurrentUser(courseId: number): Promise<LearningPathDTO> {
return await this.get<LearningPathDTO>(`courses/${courseId}/learning-path/me`);
}

async startLearningPathForCurrentUser(learningPathId: number): Promise<void> {
return await this.patch<void>(`learning-path/${learningPathId}/start`);
}

async getLearningPathNavigation(learningPathId: number): Promise<LearningPathNavigationDTO> {
Expand All @@ -35,8 +40,8 @@ export class LearningPathApiService extends BaseApiHttpService {
return await this.get<LearningPathNavigationDTO>(`learning-path/${learningPathId}/relative-navigation`, { params: params });
}

async generateLearningPath(courseId: number): Promise<number> {
return await this.post<number>(`courses/${courseId}/learning-path`);
async generateLearningPathForCurrentUser(courseId: number): Promise<LearningPathDTO> {
return await this.post<LearningPathDTO>(`courses/${courseId}/learning-path`);
}

async getLearningPathNavigationOverview(learningPathId: number): Promise<LearningPathNavigationOverviewDTO> {
Expand Down
Loading
Loading