Skip to content

Commit

Permalink
Merge branch 'develop' into refactor/iris-settings-rework
Browse files Browse the repository at this point in the history
  • Loading branch information
Hialus authored Oct 20, 2023
2 parents b7a02ba + ecc348c commit 6971eb6
Show file tree
Hide file tree
Showing 38 changed files with 793 additions and 400 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ default LearningPath findByCourseIdAndUserIdElseThrow(long courseId, long userId
return findByCourseIdAndUserId(courseId, userId).orElseThrow(() -> new EntityNotFoundException("LearningPath"));
}

@EntityGraph(type = LOAD, attributePaths = { "user" })
Optional<LearningPath> findWithEagerUserById(long learningPathId);

default LearningPath findWithEagerUserByIdElseThrow(long learningPathId) {
return findWithEagerUserById(learningPathId).orElseThrow(() -> new EntityNotFoundException("LearningPath"));
}

@EntityGraph(type = LOAD, attributePaths = { "competencies" })
Optional<LearningPath> findWithEagerCompetenciesByCourseIdAndUserId(long courseId, long userId);

Expand All @@ -45,20 +52,31 @@ SELECT COUNT (learningPath)
""")
long countLearningPathsOfEnrolledStudentsInCourse(@Param("courseId") long courseId);

/**
* Gets a learning path with eagerly fetched competencies, linked lecture units and exercises, and the corresponding domain objects storing the progress.
* <p>
* The query only fetches data related to the owner of the learning path. participations and progress for other users are not included.
* IMPORTANT: JPA doesn't support JOIN-FETCH-ON statements. To fetch the relevant data we utilize the entity graph annotation.
* Moving the ON clauses to the WHERE clause would result in significantly different and faulty output.
*
* @param learningPathId the id of the learning path to fetch
* @return the learning path with fetched data
*/
@Query("""
SELECT learningPath
FROM LearningPath learningPath
LEFT JOIN FETCH learningPath.competencies competencies
LEFT JOIN FETCH competencies.userProgress progress
LEFT JOIN competencies.userProgress progress
ON competencies.id = progress.learningGoal.id AND progress.user.id = learningPath.user.id
LEFT JOIN FETCH competencies.lectureUnits lectureUnits
LEFT JOIN FETCH lectureUnits.completedUsers completedUsers
LEFT JOIN lectureUnits.completedUsers completedUsers
ON lectureUnits.id = completedUsers.lectureUnit.id AND completedUsers.user.id = learningPath.user.id
LEFT JOIN FETCH competencies.exercises exercises
LEFT JOIN FETCH exercises.studentParticipations studentParticipations
LEFT JOIN exercises.studentParticipations studentParticipations
ON exercises.id = studentParticipations.exercise.id AND studentParticipations.student.id = learningPath.user.id
WHERE learningPath.id = :learningPathId
AND (progress IS NULL OR progress.user.id = learningPath.user.id)
AND (completedUsers IS NULL OR completedUsers.user.id = learningPath.user.id)
AND (studentParticipations IS NULL OR studentParticipations.student.id = learningPath.user.id)
""")
@EntityGraph(type = LOAD, attributePaths = { "competencies.userProgress", "competencies.lectureUnits.completedUsers", "competencies.exercises.studentParticipations" })
Optional<LearningPath> findWithEagerCompetenciesAndProgressAndLearningObjectsAndCompletedUsersById(@Param("learningPathId") long learningPathId);

default LearningPath findWithEagerCompetenciesAndProgressAndLearningObjectsAndCompletedUsersByIdElseThrow(long learningPathId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO;
import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO;
import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathHealthDTO;
import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathPageableSearchDTO;
import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathInformationDTO;
import de.tum.in.www1.artemis.web.rest.dto.competency.NgxLearningPathDTO;
import de.tum.in.www1.artemis.web.rest.util.PageUtil;

Expand Down Expand Up @@ -106,11 +106,11 @@ public LearningPath generateLearningPathForUser(@NotNull Course course, @NotNull
* @param course the course the learning paths are linked to
* @return A wrapper object containing a list of all found learning paths and the total number of pages
*/
public SearchResultPageDTO<LearningPathPageableSearchDTO> getAllOfCourseOnPageWithSize(@NotNull PageableSearchDTO<String> search, @NotNull Course course) {
public SearchResultPageDTO<LearningPathInformationDTO> getAllOfCourseOnPageWithSize(@NotNull PageableSearchDTO<String> search, @NotNull Course course) {
final var pageable = PageUtil.createLearningPathPageRequest(search);
final var searchTerm = search.getSearchTerm();
final Page<LearningPath> learningPathPage = learningPathRepository.findByLoginOrNameInCourse(searchTerm, course.getId(), pageable);
final List<LearningPathPageableSearchDTO> contentDTOs = learningPathPage.getContent().stream().map(LearningPathPageableSearchDTO::new).toList();
final List<LearningPathInformationDTO> contentDTOs = learningPathPage.getContent().stream().map(LearningPathInformationDTO::new).toList();
return new SearchResultPageDTO<>(contentDTOs, learningPathPage.getTotalPages());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO;
import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO;
import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathHealthDTO;
import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathPageableSearchDTO;
import de.tum.in.www1.artemis.web.rest.dto.competency.LearningPathInformationDTO;
import de.tum.in.www1.artemis.web.rest.dto.competency.NgxLearningPathDTO;
import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException;
import de.tum.in.www1.artemis.web.rest.errors.ConflictException;
Expand Down Expand Up @@ -107,7 +107,7 @@ public ResponseEntity<Void> generateMissingLearningPathsForCourse(@PathVariable
@GetMapping("courses/{courseId}/learning-paths")
@FeatureToggle(Feature.LearningPaths)
@EnforceAtLeastInstructor
public ResponseEntity<SearchResultPageDTO<LearningPathPageableSearchDTO>> getLearningPathsOnPage(@PathVariable long courseId, PageableSearchDTO<String> search) {
public ResponseEntity<SearchResultPageDTO<LearningPathInformationDTO>> getLearningPathsOnPage(@PathVariable long courseId, PageableSearchDTO<String> search) {
log.debug("REST request to get learning paths for course with id: {}", courseId);
Course course = courseRepository.findByIdElseThrow(courseId);
authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null);
Expand Down Expand Up @@ -138,6 +138,25 @@ public ResponseEntity<LearningPathHealthDTO> getHealthStatusForCourse(@PathVaria
return ResponseEntity.ok(learningPathService.getHealthStatusForCourse(course));
}

/**
* GET learning-path/:learningPathId : Gets the learning path information.
*
* @param learningPathId the id of the learning path that should be fetched
* @return the ResponseEntity with status 200 (OK) and with body the learning path
*/
@GetMapping("learning-path/{learningPathId}")
@FeatureToggle(Feature.LearningPaths)
@EnforceAtLeastStudent
public ResponseEntity<LearningPathInformationDTO> getLearningPath(@PathVariable long learningPathId) {
log.debug("REST request to get learning path with id: {}", learningPathId);
final var learningPath = learningPathRepository.findWithEagerUserByIdElseThrow(learningPathId);
final var user = userRepository.getUser();
if (!user.getId().equals(learningPath.getUser().getId())) {
throw new AccessForbiddenException("You are not the owner of the learning path.");
}
return ResponseEntity.ok(new LearningPathInformationDTO(learningPath));
}

/**
* GET /learning-path/:learningPathId/graph : Gets the ngx representation of the learning path as a graph.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import de.tum.in.www1.artemis.domain.competency.LearningPath;
import de.tum.in.www1.artemis.web.rest.dto.user.UserNameAndLoginDTO;

public record LearningPathPageableSearchDTO(long id, UserNameAndLoginDTO user, int progress) {
public record LearningPathInformationDTO(long id, UserNameAndLoginDTO user, int progress) {

public LearningPathPageableSearchDTO(LearningPath learningPath) {
public LearningPathInformationDTO(LearningPath learningPath) {
this(learningPath.getId(), new UserNameAndLoginDTO(learningPath.getUser()), learningPath.getProgress());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,21 @@
</div>
<ng-template #noTask>
<div
class="node-icon-container"
class="node-s node-icon-container"
*ngIf="node.type === NodeType.COMPETENCY_START || node.type === NodeType.COMPETENCY_END; else noCompetency"
[jhiStickyPopover]="popContentCompetency"
placement="right"
triggers="manual"
>
<fa-icon id="generic" [icon]="faCircle"></fa-icon>
<fa-icon *ngIf="node.type === NodeType.COMPETENCY_START" id="generic" [icon]="faFlag"></fa-icon>
<fa-icon *ngIf="node.type === NodeType.COMPETENCY_END" id="generic" [icon]="faFlagCheckered"></fa-icon>
</div>
</ng-template>

<ng-template #noCompetency>
<div class="node-icon-container">
<fa-icon id="match" [icon]="faCircle"></fa-icon>
<div class="node-xs node-icon-container">
<fa-icon *ngIf="node.type === NodeType.MATCH_START" id="match" [icon]="faSignsPost"></fa-icon>
<fa-icon *ngIf="node.type === NodeType.MATCH_END" id="match" [icon]="faCircle"></fa-icon>
</div>
</ng-template>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component, Input } from '@angular/core';
import { faCheckCircle, faCircle, faPlayCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
import { faCheckCircle, faCircle, faFlag, faFlagCheckered, faPlayCircle, faQuestionCircle, faSignsPost } from '@fortawesome/free-solid-svg-icons';
import { NgxLearningPathNode, NodeType } from 'app/entities/competency/learning-path.model';
import { Competency, CompetencyProgress } from 'app/entities/competency.model';
import { Exercise } from 'app/entities/exercise.model';
Expand All @@ -26,6 +26,9 @@ export class LearningPathGraphNodeComponent {
faPlayCircle = faPlayCircle;
faQuestionCircle = faQuestionCircle;
faCircle = faCircle;
faSignsPost = faSignsPost;
faFlagCheckered = faFlagCheckered;
faFlag = faFlag;

nodeDetailsData = new NodeDetailsData();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@
<ng-template #nodeTemplate let-node>
<svg:g class="node" [attr.width]="node.dimension.width" [attr.height]="node.dimension.height">
<svg:foreignObject [attr.width]="node.dimension.width" [attr.height]="node.dimension.height">
<jhi-learning-path-graph-node class="node" [courseId]="courseId" [node]="node" (click)="nodeClicked.emit(node)"></jhi-learning-path-graph-node>
<jhi-learning-path-graph-node
class="node"
[class.current]="node.id === highlightedNode?.id"
[courseId]="courseId"
[node]="node"
(click)="nodeClicked.emit(node)"
></jhi-learning-path-graph-node>
</svg:foreignObject>
</svg:g>
</ng-template>
Expand All @@ -37,4 +43,8 @@
</svg:g>
</ng-template>
</ngx-graph>

<div *ngIf="viewMode === PATH" class="floating-icon-button" (click)="viewProgress()">
<fa-icon [icon]="faEye" ngbTooltip="{{ 'artemisApp.learningPath.participate.viewProgress' | artemisTranslate }}"></fa-icon>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
.graph-container {
display: block;
display: flex;
width: 100%;
height: 100%;
overflow: hidden;

.ngx-graph {
width: auto;
}

.node {
display: flex;
width: 100%;
Expand All @@ -23,6 +19,15 @@
stroke: var(--body-color) !important;
marker-end: url(#arrow);
}

.floating-icon-button {
position: absolute;
right: 0;
padding: 0.25em;
}
.floating-icon-button:hover {
cursor: pointer;
}
}

jhi-learning-path-graph-node:hover {
Expand All @@ -35,6 +40,7 @@ jhi-learning-path-graph-node:hover {

fa-icon {
width: 100%;
display: flex;

svg {
margin: 10%;
Expand All @@ -44,10 +50,24 @@ jhi-learning-path-graph-node:hover {
}
}

.node-xs {
width: 40%;
margin: 30%;
}

.node-s {
width: 60%;
margin: 20%;
}

.completed {
color: var(--bs-success);
}

.current {
color: var(--bs-warning);
}

.node-details {
display: block;
max-width: 40vw;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import * as shape from 'd3-shape';
import { Subject } from 'rxjs';
import { LearningPathService } from 'app/course/learning-paths/learning-path.service';
import { NgxLearningPathDTO, NgxLearningPathNode } from 'app/entities/competency/learning-path.model';
import { ExerciseEntry, LectureUnitEntry } from 'app/course/learning-paths/participate/learning-path-storage.service';
import { faEye } from '@fortawesome/free-solid-svg-icons';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { LearningPathProgressModalComponent } from 'app/course/learning-paths/progress-modal/learning-path-progress-modal.component';

export enum LearningPathViewMode {
GRAPH = 'GRAPH',
Expand All @@ -27,22 +31,28 @@ export class LearningPathGraphComponent implements OnInit {
ngxLearningPath: NgxLearningPathDTO;
ngxGraph?: NgxLearningPathDTO;
ngxPath?: NgxLearningPathDTO;
highlightedNode?: NgxLearningPathNode;

layout: string | Layout = 'dagreCluster';
curve = shape.curveBundle;

draggingEnabled = false;
panningEnabled = true;
zoomEnabled = true;
panOnZoom = true;
private _draggingEnabled = false;
private _panningEnabled = false;
private _zoomEnabled = false;
private _panOnZoom = false;

update$: Subject<boolean> = new Subject<boolean>();
center$: Subject<boolean> = new Subject<boolean>();
zoomToFit$: Subject<boolean> = new Subject<boolean>();

faEye = faEye;

protected readonly PATH = LearningPathViewMode.PATH;

constructor(
private activatedRoute: ActivatedRoute,
private learningPathService: LearningPathService,
private modalService: NgbModal,
) {}

ngOnInit() {
Expand All @@ -51,6 +61,39 @@ export class LearningPathGraphComponent implements OnInit {
}
}

@Input() set draggingEnabled(value) {
this._draggingEnabled = value || false;
}

get draggingEnabled() {
return this._draggingEnabled;
}

@Input() set panningEnabled(value) {
this._panningEnabled = value || false;
}

get panningEnabled() {
return this._panningEnabled;
}

@Input() set zoomEnabled(value) {
this._zoomEnabled = value || false;
}

get zoomEnabled() {
return this._zoomEnabled;
}

@Input('panOnZoom')
get panOnZoom() {
return this._panOnZoom;
}

set panOnZoom(value) {
this._panOnZoom = value || false;
}

refreshData() {
if (this.ngxGraph) {
this.loadGraphRepresentation(this.viewMode === LearningPathViewMode.GRAPH);
Expand Down Expand Up @@ -122,4 +165,42 @@ export class LearningPathGraphComponent implements OnInit {
this.loadDataIfNecessary();
this.update$.next(true);
}

highlightNode(learningObject: LectureUnitEntry | ExerciseEntry) {
if (this.viewMode === LearningPathViewMode.GRAPH) {
this.highlightedNode = this.findNode(learningObject, this.ngxGraph!);
} else {
this.highlightedNode = this.findNode(learningObject, this.ngxPath!);
}
this.update$.next(true);
}

clearHighlighting() {
this.highlightedNode = undefined;
this.update$.next(true);
}

private findNode(learningObject: LectureUnitEntry | ExerciseEntry, ngx: NgxLearningPathDTO) {
if (learningObject instanceof LectureUnitEntry) {
return ngx.nodes.find((node) => {
return node.linkedResource === learningObject.lectureUnitId && node.linkedResourceParent === learningObject.lectureId;
});
} else {
return ngx.nodes.find((node) => {
return node.linkedResource === learningObject.exerciseId && !node.linkedResourceParent;
});
}
}

viewProgress() {
this.learningPathService.getLearningPath(this.learningPathId).subscribe((learningPathResponse) => {
const modalRef = this.modalService.open(LearningPathProgressModalComponent, {
size: 'xl',
backdrop: 'static',
windowClass: 'learning-path-modal',
});
modalRef.componentInstance.courseId = this.courseId;
modalRef.componentInstance.learningPath = learningPathResponse.body!;
});
}
}
Loading

0 comments on commit 6971eb6

Please sign in to comment.