diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/domain/competency/LearningPath.java b/src/main/java/de/tum/cit/aet/artemis/atlas/domain/competency/LearningPath.java index 267efe722982..ab4c961be143 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/domain/competency/LearningPath.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/domain/competency/LearningPath.java @@ -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; @@ -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 + "}"; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/dto/LearningPathDTO.java b/src/main/java/de/tum/cit/aet/artemis/atlas/dto/LearningPathDTO.java new file mode 100644 index 000000000000..f61598b30cf4 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/dto/LearningPathDTO.java @@ -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()); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathService.java index 937123a30fe1..190565c5c35c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathService.java @@ -25,6 +25,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; @@ -39,6 +40,7 @@ import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO; import de.tum.cit.aet.artemis.core.dto.pageablesearch.SearchTermPageableSearchDTO; import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; +import de.tum.cit.aet.artemis.core.exception.ConflictException; import de.tum.cit.aet.artemis.core.repository.CourseRepository; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.core.util.PageUtil; @@ -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 ConflictException("Learning path already exists.", "LearningPath", "learningPathAlreadyExists"); + } + final var learningPath = generateLearningPathForUser(course, currentUser); + return LearningPathDTO.of(learningPath); + } + + /** + * Start the learning path for the current user + * + * @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 ConflictException("Learning path already started.", "LearningPath", "learningPathAlreadyStarted"); + } + learningPath.setStartedByStudent(true); + learningPathRepository.save(learningPath); + } + /** * Gets the health status of learning paths for the given course. * diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/web/LearningPathResource.java b/src/main/java/de/tum/cit/aet/artemis/atlas/web/LearningPathResource.java index 709ac913cdf9..d940e50acc51 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/web/LearningPathResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/web/LearningPathResource.java @@ -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; @@ -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; @@ -301,19 +303,31 @@ private ResponseEntity 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 getLearningPathId(@PathVariable long courseId) { - log.debug("REST request to get learning path id for course with id: {}", courseId); + public ResponseEntity 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 startLearningPathForCurrentUser(@PathVariable long learningPathId) { + log.debug("REST request to start learning path with id: {}", learningPathId); + learningPathService.startLearningPathForCurrentUser(learningPathId); + return ResponseEntity.noContent().build(); } /** @@ -324,20 +338,11 @@ public ResponseEntity getLearningPathId(@PathVariable long courseId) { */ @PostMapping("courses/{courseId}/learning-path") @EnforceAtLeastStudentInCourse - public ResponseEntity generateLearningPath(@PathVariable long courseId) throws URISyntaxException { - log.debug("REST request to generate learning path for user in course with id: {}", courseId); + public ResponseEntity 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); } /** diff --git a/src/main/resources/config/liquibase/changelog/20240924125742_changelog.xml b/src/main/resources/config/liquibase/changelog/20240924125742_changelog.xml new file mode 100644 index 000000000000..c74139c77c6d --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20240924125742_changelog.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 2c204094c0ff..64295fe02504 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -23,6 +23,7 @@ + diff --git a/src/main/webapp/app/admin/metrics/metrics.model.ts b/src/main/webapp/app/admin/metrics/metrics.model.ts index 1e006405c805..dbed33af6fc7 100644 --- a/src/main/webapp/app/admin/metrics/metrics.model.ts +++ b/src/main/webapp/app/admin/metrics/metrics.model.ts @@ -84,6 +84,7 @@ export enum HttpMethod { Post = 'POST', Get = 'GET', Delete = 'DELETE', + Patch = 'PATCH', } export interface ProcessMetrics { diff --git a/src/main/webapp/app/course/learning-paths/pages/learning-path-student-page/learning-path-student-page.component.html b/src/main/webapp/app/course/learning-paths/pages/learning-path-student-page/learning-path-student-page.component.html index b6a8f8255ef3..7e116bc6a169 100644 --- a/src/main/webapp/app/course/learning-paths/pages/learning-path-student-page/learning-path-student-page.component.html +++ b/src/main/webapp/app/course/learning-paths/pages/learning-path-student-page/learning-path-student-page.component.html @@ -1,12 +1,12 @@
- @if (isLearningPathIdLoading()) { + @if (isLearningPathLoading()) {
- } @else if (learningPathId()) { - + } @else if (learningPath() && learningPath()!.startedByStudent) { +
@if (currentLearningObject()?.type === LearningObjectType.LECTURE) { @@ -27,10 +27,10 @@

diff --git a/src/main/webapp/app/course/learning-paths/pages/learning-path-student-page/learning-path-student-page.component.ts b/src/main/webapp/app/course/learning-paths/pages/learning-path-student-page/learning-path-student-page.component.ts index 7df486af265c..a806c35f9119 100644 --- a/src/main/webapp/app/course/learning-paths/pages/learning-path-student-page/learning-path-student-page.component.ts +++ b/src/main/webapp/app/course/learning-paths/pages/learning-path-student-page/learning-path-student-page.component.ts @@ -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'; @@ -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(undefined); + readonly isLearningPathLoading = signal(false); + readonly learningPath = signal(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.loadLearningPath(this.courseId()), { allowSignalWrites: true }); } - private async loadLearningPathId(courseId: number): Promise { + private async loadLearningPath(courseId: number): Promise { 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 { + async startLearningPath(): Promise { 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); } } } diff --git a/src/main/webapp/app/course/learning-paths/services/base-api-http.service.ts b/src/main/webapp/app/course/learning-paths/services/base-api-http.service.ts index cb298f6ebf6c..8731eccd1c1e 100644 --- a/src/main/webapp/app/course/learning-paths/services/base-api-http.service.ts +++ b/src/main/webapp/app/course/learning-paths/services/base-api-http.service.ts @@ -31,7 +31,7 @@ export abstract class BaseApiHttpService { * @param url The endpoint URL excluding the base server url (/api). * @param options The HTTP options to send with the request. * - * @return An `Promise` of the response body of type `T`. + * @return A `Promise` of the response body of type `T`. */ private async request( method: HttpMethod, @@ -73,7 +73,7 @@ export abstract class BaseApiHttpService { * @param options The HTTP options to send with the request. * @protected * - * @return An `Promise` of type `Object` (T), + * @return A `Promise` of type `Object` (T), */ protected async get( url: string, @@ -102,7 +102,7 @@ export abstract class BaseApiHttpService { * @param options The HTTP options to send with the request. * @protected * - * @return An `Promise` of type `Object` (T), + * @return A `Promise` of type `Object` (T), */ protected async post( url: string, @@ -122,4 +122,34 @@ export abstract class BaseApiHttpService { ): Promise { return await this.request(HttpMethod.Post, url, { body: body, ...options }); } + + /** + * Constructs a `PATCH` 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 A `Promise` of type `Object` (T), + */ + protected async patch( + url: string, + body?: any, + options?: { + headers?: + | HttpHeaders + | { + [header: string]: string | string[]; + }; + params?: + | HttpParams + | { + [param: string]: string | number | boolean | ReadonlyArray; + }; + }, + ): Promise { + return await this.request(HttpMethod.Patch, url, { body: body, ...options }); + } } diff --git a/src/main/webapp/app/course/learning-paths/services/learning-path-api.service.ts b/src/main/webapp/app/course/learning-paths/services/learning-path-api.service.ts index 35dcf071e505..01a43398f1c8 100644 --- a/src/main/webapp/app/course/learning-paths/services/learning-path-api.service.ts +++ b/src/main/webapp/app/course/learning-paths/services/learning-path-api.service.ts @@ -3,6 +3,7 @@ import { CompetencyGraphDTO, LearningObjectType, LearningPathCompetencyDTO, + LearningPathDTO, LearningPathNavigationDTO, LearningPathNavigationObjectDTO, LearningPathNavigationOverviewDTO, @@ -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 { - return await this.get(`courses/${courseId}/learning-path-id`); + async getLearningPathForCurrentUser(courseId: number): Promise { + return await this.get(`courses/${courseId}/learning-path/me`); + } + + async startLearningPathForCurrentUser(learningPathId: number): Promise { + return await this.patch(`learning-path/${learningPathId}/start`); } async getLearningPathNavigation(learningPathId: number): Promise { @@ -35,8 +40,8 @@ export class LearningPathApiService extends BaseApiHttpService { return await this.get(`learning-path/${learningPathId}/relative-navigation`, { params: params }); } - async generateLearningPath(courseId: number): Promise { - return await this.post(`courses/${courseId}/learning-path`); + async generateLearningPathForCurrentUser(courseId: number): Promise { + return await this.post(`courses/${courseId}/learning-path`); } async getLearningPathNavigationOverview(learningPathId: number): Promise { diff --git a/src/main/webapp/app/entities/competency/learning-path.model.ts b/src/main/webapp/app/entities/competency/learning-path.model.ts index 3f5c0e7775de..2c55868d0af0 100644 --- a/src/main/webapp/app/entities/competency/learning-path.model.ts +++ b/src/main/webapp/app/entities/competency/learning-path.model.ts @@ -32,6 +32,12 @@ export interface LearningPathCompetencyDTO { masteryProgress: number; } +export interface LearningPathDTO { + id: number; + progress: number; + startedByStudent: boolean; +} + export interface LearningPathNavigationObjectDTO { id: number; completed: boolean; diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/learningpath/LearningPathIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/learningpath/LearningPathIntegrationTest.java index fb0bf6dcb286..239a2c443ac0 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/learningpath/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/learningpath/LearningPathIntegrationTest.java @@ -36,6 +36,7 @@ import de.tum.cit.aet.artemis.atlas.dto.CompetencyNameDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyWithTailRelationDTO; 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; @@ -121,7 +122,9 @@ class LearningPathIntegrationTest extends AbstractSpringIntegrationIndependentTe private static final int NUMBER_OF_STUDENTS = 5; - private static final String STUDENT_OF_COURSE = TEST_PREFIX + "student1"; + private static final String STUDENT1_OF_COURSE = TEST_PREFIX + "student1"; + + private static final String STUDENT2_OF_COURSE = TEST_PREFIX + "student2"; private static final String TUTOR_OF_COURSE = TEST_PREFIX + "tutor1"; @@ -158,7 +161,7 @@ void setupTestScenario() throws Exception { lecture.setCourse(course); lectureRepository.save(lecture); - final var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); textUnit = createAndLinkTextUnit(student, competencies[0], true); textExercise = createAndLinkTextExercise(competencies[1], false); @@ -216,10 +219,10 @@ private void deleteCompetencyRESTCall(Competency competency) throws Exception { } @Test - @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") void testAll_asStudent() throws Exception { this.testAllPreAuthorize(); - request.get("/api/courses/" + course.getId() + "/learning-path-id", HttpStatus.BAD_REQUEST, Long.class); + request.get("/api/courses/" + course.getId() + "/learning-path/me", HttpStatus.BAD_REQUEST, LearningPathDTO.class); } @Test @@ -315,7 +318,7 @@ void testGetLearningPathsOnPageForCourseLearningPathsDisabled() throws Exception @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") void testGetLearningPathsOnPageForCourseEmpty() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var search = pageableSearchUtilService.configureSearch(STUDENT_OF_COURSE + "SuffixThatAllowsTheResultToBeEmpty"); + final var search = pageableSearchUtilService.configureSearch(STUDENT1_OF_COURSE + "SuffixThatAllowsTheResultToBeEmpty"); final var result = request.getSearchResult("/api/courses/" + course.getId() + "/learning-paths", HttpStatus.OK, LearningPathInformationDTO.class, pageableSearchUtilService.searchMapping(search)); assertThat(result.getResultsOnPage()).isNullOrEmpty(); @@ -325,7 +328,7 @@ void testGetLearningPathsOnPageForCourseEmpty() throws Exception { @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") void testGetLearningPathsOnPageForCourseExactlyStudent() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var search = pageableSearchUtilService.configureSearch(STUDENT_OF_COURSE); + final var search = pageableSearchUtilService.configureSearch(STUDENT1_OF_COURSE); final var result = request.getSearchResult("/api/courses/" + course.getId() + "/learning-paths", HttpStatus.OK, LearningPathInformationDTO.class, pageableSearchUtilService.searchMapping(search)); assertThat(result.getResultsOnPage()).hasSize(1); @@ -359,7 +362,7 @@ void addCompetencyToLearningPaths(Function competencyProgressService.updateCompetencyProgress(competency.getId(), student)); @@ -488,10 +491,10 @@ void testGetLearningPathCompetencyGraph() throws Exception { @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") @EnumSource(LearningPathResource.NgxRequestType.class) - @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") void testGetLearningPathNgxForLearningPathsDisabled(LearningPathResource.NgxRequestType type) throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); course.setLearningPathsEnabled(false); courseRepository.save(course); @@ -503,7 +506,7 @@ void testGetLearningPathNgxForLearningPathsDisabled(LearningPathResource.NgxRequ @WithMockUser(username = TEST_PREFIX + "student2", roles = "USER") void testGetLearningPathNgxForOtherStudent(LearningPathResource.NgxRequestType type) throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); request.get("/api/learning-path/" + learningPath.getId() + "/" + type, HttpStatus.FORBIDDEN, NgxLearningPathDTO.class); } @@ -516,10 +519,10 @@ void testGetLearningPathNgxForOtherStudent(LearningPathResource.NgxRequestType t */ @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") @EnumSource(LearningPathResource.NgxRequestType.class) - @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") void testGetLearningPathNgxAsStudent(LearningPathResource.NgxRequestType type) throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); request.get("/api/learning-path/" + learningPath.getId() + "/" + type, HttpStatus.OK, NgxLearningPathDTO.class); } @@ -567,33 +570,33 @@ void testGetLearningPathNgxAsEditor(LearningPathResource.NgxRequestType type) th @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") void testGetLearningPathNgxAsInstructor(LearningPathResource.NgxRequestType type) throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); request.get("/api/learning-path/" + learningPath.getId() + "/" + type, HttpStatus.OK, NgxLearningPathDTO.class); } @Nested - class GetLearningPathId { + class GetLearningPath { @Test - @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") - void shouldReturnExistingId() throws Exception { + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") + void shouldReturnExisting() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); - final var result = request.get("/api/courses/" + course.getId() + "/learning-path-id", HttpStatus.OK, Long.class); - assertThat(result).isEqualTo(learningPath.getId()); + final var result = request.get("/api/courses/" + course.getId() + "/learning-path/me", HttpStatus.OK, LearningPathDTO.class); + assertThat(result).isEqualTo(LearningPathDTO.of(learningPath)); } @Test - @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") void shouldReturnNotFoundIfNotExists() throws Exception { course.setLearningPathsEnabled(true); course = courseRepository.save(course); - var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); student = userTestRepository.findWithLearningPathsByIdElseThrow(student.getId()); learningPathRepository.deleteAll(student.getLearningPaths()); - request.get("/api/courses/" + course.getId() + "/learning-path-id", HttpStatus.NOT_FOUND, Long.class); + request.get("/api/courses/" + course.getId() + "/learning-path/me", HttpStatus.NOT_FOUND, LearningPathDTO.class); } } @@ -601,42 +604,79 @@ void shouldReturnNotFoundIfNotExists() throws Exception { class GenerateLearningPath { @Test - @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") void shouldReturnForbiddenIfNotEnabled() throws Exception { - request.postWithResponseBody("/api/courses/" + course.getId() + "/learning-path", null, Long.class, HttpStatus.BAD_REQUEST); + request.postWithResponseBody("/api/courses/" + course.getId() + "/learning-path", null, LearningPathDTO.class, HttpStatus.BAD_REQUEST); } @Test - @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") void shouldReturnBadRequestIfAlreadyExists() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - request.postWithResponseBody("/api/courses/" + course.getId() + "/learning-path", null, Long.class, HttpStatus.BAD_REQUEST); + request.postWithResponseBody("/api/courses/" + course.getId() + "/learning-path", null, LearningPathDTO.class, HttpStatus.CONFLICT); } @Test - @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") void shouldGenerateLearningPath() throws Exception { course.setLearningPathsEnabled(true); course = courseRepository.save(course); - final var response = request.postWithResponseBody("/api/courses/" + course.getId() + "/learning-path", null, Long.class, HttpStatus.CREATED); + final var response = request.postWithResponseBody("/api/courses/" + course.getId() + "/learning-path", null, LearningPathDTO.class, HttpStatus.CREATED); assertThat(response).isNotNull(); - final var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); assertThat(learningPath).isNotNull(); } } + @Nested + class StartLearningPath { + + @BeforeEach + void setup() { + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + } + + @Test + @WithMockUser(username = STUDENT2_OF_COURSE, roles = "USER") + void shouldReturnForbiddenIfNotOwn() throws Exception { + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); + final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); + request.patch("/api/learning-path/" + learningPath.getId() + "/start", null, HttpStatus.FORBIDDEN); + } + + @Test + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") + void shouldReturnBadRequestIfAlreadyStarted() throws Exception { + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); + final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); + learningPath.setStartedByStudent(true); + learningPathRepository.save(learningPath); + request.patch("/api/learning-path/" + learningPath.getId() + "/start", null, HttpStatus.CONFLICT); + } + + @Test + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") + void shouldStartLearningPath() throws Exception { + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); + final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); + request.patch("/api/learning-path/" + learningPath.getId() + "/start", null, HttpStatus.NO_CONTENT); + final var updatedLearningPath = learningPathRepository.findByIdElseThrow(learningPath.getId()); + assertThat(updatedLearningPath.isStartedByStudent()).isTrue(); + } + } + @Test @WithMockUser(username = TEST_PREFIX + "student2", roles = "USER") void testGetCompetencyProgressForLearningPathByOtherStudent() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); request.get("/api/learning-path/" + learningPath.getId() + "/competency-progress", HttpStatus.FORBIDDEN, Set.class); } @Test - @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") void testGetCompetencyProgressForLearningPathByOwner() throws Exception { testGetCompetencyProgressForLearningPath(); } @@ -648,10 +688,10 @@ void testGetCompetencyProgressForLearningPathByInstructor() throws Exception { } @Test - @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") void testGetLearningPathNavigation() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); competencyProgressService.updateProgressByLearningObjectSync(textUnit, Set.of(student)); @@ -663,10 +703,10 @@ void testGetLearningPathNavigation() throws Exception { } @Test - @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") void testGetLearningPathNavigationEmptyCompetencies() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); textExercise.setCompetencies(Set.of()); @@ -688,10 +728,10 @@ void testGetLearningPathNavigationEmptyCompetencies() throws Exception { } @Test - @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") void testGetLearningPathNavigationDoesNotLeakUnreleasedLearningObjects() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); textExercise.setCompetencies(Set.of()); @@ -736,10 +776,10 @@ private void verifyNavigationObjectResult(LearningObject expectedObject, Learnin } @Test - @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") void testGetRelativeLearningPathNavigation() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); final var result = request.get("/api/learning-path/" + learningPath.getId() + "/relative-navigation?learningObjectId=" + textUnit.getId() + "&learningObjectType=" + LearningPathNavigationObjectDTO.LearningObjectType.LECTURE + "&competencyId=" + competencies[0].getId(), HttpStatus.OK, LearningPathNavigationDTO.class); @@ -753,16 +793,16 @@ void testGetRelativeLearningPathNavigation() throws Exception { @WithMockUser(username = TEST_PREFIX + "student1337", roles = "USER") void testGetLearningPathNavigationForOtherStudent() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); request.get("/api/learning-path/" + learningPath.getId() + "/navigation", HttpStatus.FORBIDDEN, LearningPathNavigationDTO.class); } @Test - @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") void testGetLearningPathNavigationOverview() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); final var result = request.get("/api/learning-path/" + learningPath.getId() + "/navigation-overview", HttpStatus.OK, LearningPathNavigationOverviewDTO.class); @@ -776,26 +816,26 @@ void testGetLearningPathNavigationOverview() throws Exception { @WithMockUser(username = TEST_PREFIX + "student1337", roles = "USER") void testGetLearningPathNavigationOverviewForOtherStudent() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); request.get("/api/learning-path/" + learningPath.getId() + "/navigation-overview", HttpStatus.FORBIDDEN, LearningPathNavigationOverviewDTO.class); } @Test - @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") void testGetCompetencyOrderForLearningPath() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); final var result = request.getList("/api/learning-path/" + learningPath.getId() + "/competencies", HttpStatus.OK, CompetencyNameDTO.class); assertThat(result).containsExactlyElementsOf(Arrays.stream(competencies).map(CompetencyNameDTO::of).toList()); } @Test - @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") void testGetLearningObjectsForCompetency() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); var result = request.getList("/api/learning-path/" + learningPath.getId() + "/competencies/" + competencies[0].getId() + "/learning-objects", HttpStatus.OK, LearningPathNavigationObjectDTO.class); @@ -809,10 +849,10 @@ void testGetLearningObjectsForCompetency() throws Exception { } @Test - @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") void testGetLearningObjectsForCompetencyMultipleObjects() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); List completedLectureUnits = List.of(createAndLinkTextUnit(student, competencies[4], true), createAndLinkTextUnit(student, competencies[4], true)); @@ -843,7 +883,7 @@ void testGetLearningObjectsForCompetencyMultipleObjects() throws Exception { void testGetCompetencyProgressForLearningPath() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); final var result = request.get("/api/learning-path/" + learningPath.getId() + "/competency-progress", HttpStatus.OK, Set.class); assertThat(result).hasSize(5); @@ -854,7 +894,7 @@ private TextExercise createAndLinkTextExercise(Competency competency, boolean wi Set gradingCriteria = exerciseUtilService.addGradingInstructionsToExercise(textExercise); gradingCriterionRepository.saveAll(gradingCriteria); if (withAssessment) { - var student = userTestRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); + var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); studentScoreUtilService.createStudentScore(textExercise, student, 100.0); } competencyUtilService.linkExerciseToCompetency(competency, textExercise); diff --git a/src/test/javascript/spec/component/learning-paths/pages/learning-path-student-page.component.spec.ts b/src/test/javascript/spec/component/learning-paths/pages/learning-path-student-page.component.spec.ts index c3609648d0d8..f5a5abbf3efc 100644 --- a/src/test/javascript/spec/component/learning-paths/pages/learning-path-student-page.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/pages/learning-path-student-page.component.spec.ts @@ -13,6 +13,7 @@ import { LearningPathApiService } from 'app/course/learning-paths/services/learn import { AlertService } from 'app/core/util/alert.service'; import { MockAlertService } from '../../../helpers/mocks/service/mock-alert.service'; import { EntityNotFoundError } from 'app/course/learning-paths/exceptions/entity-not-found.error'; +import { LearningPathDTO } from 'app/entities/competency/learning-path.model'; describe('LearningPathStudentPageComponent', () => { let component: LearningPathStudentPageComponent; @@ -20,7 +21,11 @@ describe('LearningPathStudentPageComponent', () => { let learningPathApiService: LearningPathApiService; let alertService: AlertService; - const learningPathId = 1; + const learningPath: LearningPathDTO = { + id: 1, + progress: 0, + startedByStudent: false, + }; const courseId = 2; beforeEach(async () => { @@ -42,6 +47,14 @@ describe('LearningPathStudentPageComponent', () => { }, { provide: TranslateService, useClass: MockTranslateService }, { provide: AlertService, useClass: MockAlertService }, + { + provide: LearningPathApiService, + useValue: { + getLearningPathForCurrentUser: jest.fn().mockResolvedValue(learningPath), + generateLearningPathForCurrentUser: jest.fn().mockResolvedValue(learningPath), + startLearningPathForCurrentUser: jest.fn().mockReturnValue(() => Promise.resolve()), + }, + }, ], }) .overrideComponent(LearningPathStudentPageComponent, { @@ -70,18 +83,23 @@ describe('LearningPathStudentPageComponent', () => { expect(component.courseId()).toBe(courseId); }); - it('should get learning path id', async () => { - const getLearningPathIdSpy = jest.spyOn(learningPathApiService, 'getLearningPathId').mockResolvedValue(learningPathId); + it('should get learning path', async () => { + const getLearningPathIdSpy = jest.spyOn(learningPathApiService, 'getLearningPathForCurrentUser').mockResolvedValue(learningPath); fixture.detectChanges(); await fixture.whenStable(); expect(getLearningPathIdSpy).toHaveBeenCalledWith(courseId); - expect(component.learningPathId()).toEqual(learningPathId); + expect(component.learningPath()).toEqual(learningPath); }); - it('should show navigation on successful load', async () => { - jest.spyOn(learningPathApiService, 'getLearningPathId').mockResolvedValue(learningPathId); + it('should show navigation when learning path has been started', async () => { + const learningPath: LearningPathDTO = { + id: 1, + progress: 0, + startedByStudent: true, + }; + jest.spyOn(learningPathApiService, 'getLearningPathForCurrentUser').mockResolvedValueOnce(learningPath); fixture.detectChanges(); await fixture.whenStable(); @@ -91,8 +109,8 @@ describe('LearningPathStudentPageComponent', () => { expect(navComponent).toBeTruthy(); }); - it('should show error when learning path id could not be loaded', async () => { - const getLearningPathIdSpy = jest.spyOn(learningPathApiService, 'getLearningPathId').mockRejectedValue(new Error()); + it('should show error when learning path could not be loaded', async () => { + const getLearningPathIdSpy = jest.spyOn(learningPathApiService, 'getLearningPathForCurrentUser').mockRejectedValue(new Error()); const alertServiceErrorSpy = jest.spyOn(alertService, 'error'); fixture.detectChanges(); @@ -103,8 +121,8 @@ describe('LearningPathStudentPageComponent', () => { }); it('should set isLoading correctly during learning path load', async () => { - jest.spyOn(learningPathApiService, 'getLearningPathId').mockResolvedValue(learningPathId); - const loadingSpy = jest.spyOn(component.isLearningPathIdLoading, 'set'); + jest.spyOn(learningPathApiService, 'getLearningPathForCurrentUser').mockResolvedValue(learningPath); + const loadingSpy = jest.spyOn(component.isLearningPathLoading, 'set'); fixture.detectChanges(); await fixture.whenStable(); @@ -113,28 +131,34 @@ describe('LearningPathStudentPageComponent', () => { expect(loadingSpy).toHaveBeenNthCalledWith(2, false); }); - it('should generate learning path on click', async () => { - jest.spyOn(learningPathApiService, 'getLearningPathId').mockRejectedValue(new EntityNotFoundError()); - const generateLearningPathSpy = jest.spyOn(learningPathApiService, 'generateLearningPath').mockResolvedValue(learningPathId); + it('should generate learning path on start when not found', async () => { + jest.spyOn(learningPathApiService, 'getLearningPathForCurrentUser').mockReturnValueOnce(Promise.reject(new EntityNotFoundError())); + const generateLearningPathSpy = jest.spyOn(learningPathApiService, 'generateLearningPathForCurrentUser').mockResolvedValue(learningPath); + const startSpy = jest.spyOn(learningPathApiService, 'startLearningPathForCurrentUser'); fixture.detectChanges(); await fixture.whenStable(); - fixture.detectChanges(); - const generateLearningPathButton = fixture.debugElement.query(By.css('#generate-learning-path-button')); - generateLearningPathButton.nativeElement.click(); + await component.startLearningPath(); + + expect(component.learningPath()).toEqual({ ...learningPath, startedByStudent: true }); + expect(generateLearningPathSpy).toHaveBeenCalledExactlyOnceWith(courseId); + expect(startSpy).toHaveBeenCalledExactlyOnceWith(learningPath.id); + }); + it('should set learning path to started', async () => { + const startedSpy = jest.spyOn(learningPathApiService, 'startLearningPathForCurrentUser'); fixture.detectChanges(); await fixture.whenStable(); - expect(component.learningPathId()).toEqual(learningPathId); - expect(generateLearningPathSpy).toHaveBeenCalledWith(courseId); + await component.startLearningPath(); + + expect(component.learningPath()).toEqual({ ...learningPath, startedByStudent: true }); + expect(startedSpy).toHaveBeenCalledExactlyOnceWith(learningPath.id); }); - it('should set isLoading correctly during learning path generation', async () => { - jest.spyOn(learningPathApiService, 'getLearningPathId').mockRejectedValue(new EntityNotFoundError()); - jest.spyOn(learningPathApiService, 'generateLearningPath').mockResolvedValue(learningPathId); - const loadingSpy = jest.spyOn(component.isLearningPathIdLoading, 'set'); + it('should set isLoading correctly during learning path start', async () => { + const loadingSpy = jest.spyOn(component.isLearningPathLoading, 'set'); fixture.detectChanges(); await fixture.whenStable(); diff --git a/src/test/javascript/spec/service/learning-path/learning-path-api.service.spec.ts b/src/test/javascript/spec/service/learning-path/learning-path-api.service.spec.ts index 57d2487cefdb..d7f34259716e 100644 --- a/src/test/javascript/spec/service/learning-path/learning-path-api.service.spec.ts +++ b/src/test/javascript/spec/service/learning-path/learning-path-api.service.spec.ts @@ -26,11 +26,11 @@ describe('LearningPathApiService', () => { httpClient.verify(); }); - it('should get learning path id', async () => { + it('should get learning path for current user', async () => { const courseId = 1; - const methodCall = learningPathApiService.getLearningPathId(courseId); - const response = httpClient.expectOne({ method: 'GET', url: `${baseUrl}/courses/${courseId}/learning-path-id` }); + const methodCall = learningPathApiService.getLearningPathForCurrentUser(courseId); + const response = httpClient.expectOne({ method: 'GET', url: `${baseUrl}/courses/${courseId}/learning-path/me` }); response.flush({}); await methodCall; }); @@ -55,6 +55,13 @@ describe('LearningPathApiService', () => { await methodCall; }); + it('should start learning path for current user', async () => { + const methodCall = learningPathApiService.startLearningPathForCurrentUser(learningPathId); + const response = httpClient.expectOne({ method: 'PATCH', url: `${baseUrl}/learning-path/${learningPathId}/start` }); + response.flush({}); + await methodCall; + }); + it('should get learning path navigation overview', async () => { const methodCall = learningPathApiService.getLearningPathNavigationOverview(learningPathId); const response = httpClient.expectOne({ method: 'GET', url: `${baseUrl}/learning-path/${learningPathId}/navigation-overview` }); @@ -62,8 +69,8 @@ describe('LearningPathApiService', () => { await methodCall; }); - it('should generate learning path', async () => { - const methodCall = learningPathApiService.generateLearningPath(courseId); + it('should generate learning path for current user', async () => { + const methodCall = learningPathApiService.generateLearningPathForCurrentUser(courseId); const response = httpClient.expectOne({ method: 'POST', url: `${baseUrl}/courses/${courseId}/learning-path` }); response.flush({}); await methodCall;