Skip to content

Commit

Permalink
Merge branch 'develop' into feature/add-auxiliary-repository-view
Browse files Browse the repository at this point in the history
  • Loading branch information
SimonEntholzer authored Oct 18, 2024
2 parents e453fe5 + 96284c7 commit ecd4138
Show file tree
Hide file tree
Showing 44 changed files with 1,734 additions and 167 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
public record CompetencyGraphNodeDTO(String id, String label, ZonedDateTime softDueDate, Double value, CompetencyNodeValueType valueType) {

public enum CompetencyNodeValueType {
MASTERY_PROGRESS
MASTERY_PROGRESS, AVERAGE_MASTERY_PROGRESS,
}

public static CompetencyGraphNodeDTO of(@NotNull CourseCompetency competency, Double value, CompetencyNodeValueType valueType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ public LearningPathHealthDTO(Set<HealthStatus> status) {
}

public enum HealthStatus {
OK, DISABLED, MISSING, NO_COMPETENCIES, NO_RELATIONS
MISSING, NO_COMPETENCIES, NO_RELATIONS
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,16 @@ SELECT COUNT(cp)
AND c = :competency
""")
Set<CompetencyProgress> findAllPriorByCompetencyId(@Param("competency") CourseCompetency competency, @Param("user") User userId);

@Query("""
SELECT COALESCE(GREATEST(0.0, LEAST(1.0, AVG(cp.progress * cp.confidence / com.masteryThreshold))), 0.0)
FROM CompetencyProgress cp
LEFT JOIN cp.competency com
LEFT JOIN com.course c
LEFT JOIN cp.user u
WHERE com.id = :competencyId
AND cp.progress > 0
AND c.studentGroupName MEMBER OF u.groups
""")
double findAverageOfAllNonZeroStudentProgressByCompetencyId(@Param("competencyId") long competencyId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import de.tum.cit.aet.artemis.atlas.repository.CompetencyProgressRepository;
import de.tum.cit.aet.artemis.atlas.repository.CompetencyRelationRepository;
import de.tum.cit.aet.artemis.atlas.repository.CompetencyRepository;
import de.tum.cit.aet.artemis.atlas.repository.CourseCompetencyRepository;
import de.tum.cit.aet.artemis.atlas.repository.LearningPathRepository;
import de.tum.cit.aet.artemis.atlas.service.competency.CompetencyProgressService;
import de.tum.cit.aet.artemis.core.domain.Course;
Expand Down Expand Up @@ -89,10 +90,13 @@ public class LearningPathService {

private final StudentParticipationRepository studentParticipationRepository;

private final CourseCompetencyRepository courseCompetencyRepository;

public LearningPathService(UserRepository userRepository, LearningPathRepository learningPathRepository, CompetencyProgressRepository competencyProgressRepository,
LearningPathNavigationService learningPathNavigationService, CourseRepository courseRepository, CompetencyRepository competencyRepository,
CompetencyRelationRepository competencyRelationRepository, LearningPathNgxService learningPathNgxService,
LectureUnitCompletionRepository lectureUnitCompletionRepository, StudentParticipationRepository studentParticipationRepository) {
LectureUnitCompletionRepository lectureUnitCompletionRepository, StudentParticipationRepository studentParticipationRepository,
CourseCompetencyRepository courseCompetencyRepository) {
this.userRepository = userRepository;
this.learningPathRepository = learningPathRepository;
this.competencyProgressRepository = competencyProgressRepository;
Expand All @@ -103,6 +107,7 @@ public LearningPathService(UserRepository userRepository, LearningPathRepository
this.learningPathNgxService = learningPathNgxService;
this.lectureUnitCompletionRepository = lectureUnitCompletionRepository;
this.studentParticipationRepository = studentParticipationRepository;
this.courseCompetencyRepository = courseCompetencyRepository;
}

/**
Expand Down Expand Up @@ -298,20 +303,11 @@ else if (learningPath.isStartedByStudent()) {
* @return dto containing the health status and additional information (missing learning paths) if needed
*/
public LearningPathHealthDTO getHealthStatusForCourse(@NotNull Course course) {
if (!course.getLearningPathsEnabled()) {
return new LearningPathHealthDTO(Set.of(LearningPathHealthDTO.HealthStatus.DISABLED));
}

Set<LearningPathHealthDTO.HealthStatus> status = new HashSet<>();
Long numberOfMissingLearningPaths = checkMissingLearningPaths(course, status);
checkNoCompetencies(course, status);
checkNoRelations(course, status);

// if no issues where found, add OK status
if (status.isEmpty()) {
status.add(LearningPathHealthDTO.HealthStatus.OK);
}

return new LearningPathHealthDTO(status, numberOfMissingLearningPaths);
}

Expand Down Expand Up @@ -366,6 +362,25 @@ public LearningPathCompetencyGraphDTO generateLearningPathCompetencyGraph(@NotNu
return new LearningPathCompetencyGraphDTO(progressDTOs, relationDTOs);
}

/**
* Generates the graph of competencies with the student's progress for the given learning path.
*
* @param courseId the id of the course for which the graph should be generated
* @return dto containing the competencies and relations of the learning path
*/
public LearningPathCompetencyGraphDTO generateLearningPathCompetencyInstructorGraph(long courseId) {
List<CourseCompetency> competencies = courseCompetencyRepository.findByCourseIdOrderById(courseId);
Set<CompetencyGraphNodeDTO> progressDTOs = competencies.stream().map(competency -> {
double averageMasteryProgress = competencyProgressRepository.findAverageOfAllNonZeroStudentProgressByCompetencyId(competency.getId());
return CompetencyGraphNodeDTO.of(competency, averageMasteryProgress, CompetencyGraphNodeDTO.CompetencyNodeValueType.AVERAGE_MASTERY_PROGRESS);
}).collect(Collectors.toSet());

Set<CompetencyRelation> relations = competencyRelationRepository.findAllWithHeadAndTailByCourseId(courseId);
Set<CompetencyGraphEdgeDTO> relationDTOs = relations.stream().map(CompetencyGraphEdgeDTO::of).collect(Collectors.toSet());

return new LearningPathCompetencyGraphDTO(progressDTOs, relationDTOs);
}

/**
* Generates Ngx graph representation of the learning path graph.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,21 @@ public ResponseEntity<LearningPathCompetencyGraphDTO> getLearningPathCompetencyG
return ResponseEntity.ok(learningPathService.generateLearningPathCompetencyGraph(learningPath, user));
}

/**
* GET courses/{courseId}/learning-path/competency-instructor-graph : Gets the competency instructor graph
*
* @param courseId the id of the course for which the graph should be fetched
* @return the ResponseEntity with status 200 (OK) and with body the graph
*/
@GetMapping("courses/{courseId}/learning-path/competency-instructor-graph")
@FeatureToggle(Feature.LearningPaths)
@EnforceAtLeastInstructorInCourse
public ResponseEntity<LearningPathCompetencyGraphDTO> getLearningPathCompetencyInstructorGraph(@PathVariable long courseId) {
log.debug("REST request to get competency instructor graph for learning path with id: {}", courseId);

return ResponseEntity.ok(learningPathService.generateLearningPathCompetencyInstructorGraph(courseId));
}

/**
* 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 @@ -322,6 +322,14 @@ GROUP BY SUBSTRING(CAST(s.submissionDate AS string), 1, 10), p.student.login
""")
List<Course> findAllNotEndedCoursesByManagementGroupNames(@Param("now") ZonedDateTime now, @Param("userGroups") List<String> userGroups);

@Query("""
SELECT COUNT(DISTINCT ug.userId)
FROM Course c
JOIN UserGroup ug ON c.studentGroupName = ug.group
WHERE c.id = :courseId
""")
int countCourseStudents(@Param("courseId") long courseId);

/**
* Counts the number of members of a course, i.e. users that are a member of the course's student, tutor, editor or instructor group.
* Users that are part of multiple groups are NOT counted multiple times.
Expand Down
1 change: 1 addition & 0 deletions src/main/webapp/app/admin/metrics/metrics.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export interface Services {
export enum HttpMethod {
Post = 'POST',
Get = 'GET',
Put = 'PUT',
Delete = 'DELETE',
Patch = 'PATCH',
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component, effect, inject, input, signal } from '@angular/core';
import { FontAwesomeModule, IconDefinition } from '@fortawesome/angular-fontawesome';
import { faXmark } from '@fortawesome/free-solid-svg-icons';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { CompetencyGraphComponent } from 'app/course/learning-paths/components/competency-graph/competency-graph.component';
import { ArtemisSharedModule } from 'app/shared/shared.module';
import { LearningPathApiService } from 'app/course/learning-paths/services/learning-path-api.service';
Expand Down Expand Up @@ -46,4 +46,13 @@ export class CompetencyGraphModalComponent {
closeModal(): void {
this.activeModal.close();
}

static openCompetencyGraphModal(modalService: NgbModal, learningPathId: number): void {
const modalRef = modalService.open(CompetencyGraphModalComponent, {
size: 'xl',
backdrop: 'static',
windowClass: 'competency-graph-modal',
});
modalRef.componentInstance.learningPathId = signal<number>(learningPathId);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<div class="d-inline-block competency-node">
<div class="progress-container d-inline-block" [ngClass]="{ 'green-competency': isGreen(), 'yellow-competency': isYellow(), 'gray-competency': isGray() }">
<small class="font-bold"
<small
><strong>
@if (valueType() === CompetencyGraphNodeValueType.MASTERY_PROGRESS) {
@if (valueType() === CompetencyGraphNodeValueType.MASTERY_PROGRESS || valueType() === CompetencyGraphNodeValueType.AVERAGE_MASTERY_PROGRESS) {
{{ value() }} %
} @else {
{{ value() }}
}
</strong></small
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class CompetencyNodeComponent implements AfterViewInit {
isYellow(): boolean {
switch (this.valueType()) {
case CompetencyGraphNodeValueType.MASTERY_PROGRESS:
case CompetencyGraphNodeValueType.AVERAGE_MASTERY_PROGRESS:
return this.value() > 0 && this.value() < 100;
default:
return false;
Expand All @@ -55,6 +56,7 @@ export class CompetencyNodeComponent implements AfterViewInit {
isGray(): boolean {
switch (this.valueType()) {
case CompetencyGraphNodeValueType.MASTERY_PROGRESS:
case CompetencyGraphNodeValueType.AVERAGE_MASTERY_PROGRESS:
return this.value() === 0;
default:
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,6 @@ export class LearningPathNavOverviewComponent {
}

openCompetencyGraph(): void {
const modalRef = this.modalService.open(CompetencyGraphModalComponent, {
size: 'xl',
backdrop: 'static',
windowClass: 'competency-graph-modal',
});
modalRef.componentInstance.learningPathId = this.learningPathId;
CompetencyGraphModalComponent.openCompetencyGraphModal(this.modalService, this.learningPathId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<div class="learning-paths-analytics-container">
<h5 class="m-0" jhiTranslate="artemisApp.learningPathManagement.learningPathsAnalytics.title"></h5>
<hr class="my-2" />
<div class="row h-100 m-0 gap-3">
<div class="col-2 p-0 learning-paths-analytics-graph-selection-container">
<ng-container [ngTemplateOutlet]="radioTemplate" [ngTemplateOutletContext]="{ $implicit: CompetencyGraphNodeValueType.AVERAGE_MASTERY_PROGRESS }"></ng-container>
</div>
<div class="col p-0">
@if (isLoading()) {
<div class="row justify-content-center p-2">
<div class="spinner-border text-primary" role="status">
<span class="sr-only" jhiTranslate="loading"></span>
</div>
</div>
} @else if (instructorCompetencyGraph()) {
<jhi-competency-graph [competencyGraph]="instructorCompetencyGraph()!" />
}
</div>
</div>
</div>

<ng-template #radioTemplate let-competencyNodeValueType>
<div class="row m-0 align-items-center">
<input type="radio" class="col-md-auto" [checked]="valueSelection() === competencyNodeValueType" />
<label class="col-md-auto pe-0 text-break" [jhiTranslate]="'artemisApp.learningPathManagement.learningPathsAnalytics.graphSelection.' + competencyNodeValueType"></label>
</div>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.learning-paths-analytics-container {
height: 500px;

.learning-paths-analytics-graph-selection-container {
border-right: var(--bs-border-width) solid var(--border-color);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Component, effect, inject, input, signal } from '@angular/core';
import { LearningPathApiService } from 'app/course/learning-paths/services/learning-path-api.service';
import { CompetencyGraphDTO, CompetencyGraphNodeValueType } from 'app/entities/competency/learning-path.model';
import { AlertService } from 'app/core/util/alert.service';
import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module';
import { CompetencyGraphComponent } from 'app/course/learning-paths/components/competency-graph/competency-graph.component';
import { onError } from 'app/shared/util/global.utils';

@Component({
selector: 'jhi-learning-paths-analytics',
standalone: true,
imports: [ArtemisSharedCommonModule, CompetencyGraphComponent],
templateUrl: './learning-paths-analytics.component.html',
styleUrl: './learning-paths-analytics.component.scss',
})
export class LearningPathsAnalyticsComponent {
protected readonly CompetencyGraphNodeValueType = CompetencyGraphNodeValueType;

private readonly learningPathApiService = inject(LearningPathApiService);
private readonly alertService = inject(AlertService);

readonly courseId = input.required<number>();

readonly isLoading = signal<boolean>(false);
readonly instructorCompetencyGraph = signal<CompetencyGraphDTO | undefined>(undefined);

readonly valueSelection = signal<CompetencyGraphNodeValueType>(CompetencyGraphNodeValueType.AVERAGE_MASTERY_PROGRESS);

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

private async loadInstructionCompetencyGraph(courseId: number): Promise<void> {
try {
this.isLoading.set(true);
const instructorCompetencyGraph = await this.learningPathApiService.getLearningPathInstructorCompetencyGraph(courseId);
this.instructorCompetencyGraph.set(instructorCompetencyGraph);
} catch (error) {
onError(this.alertService, error);
} finally {
this.isLoading.set(false);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<div>
<div class="row m-0 align-items-center justify-content-between">
<h5 class="m-0 col-md-auto p-0" jhiTranslate="artemisApp.learningPathManagement.learningPathsConfiguration.title"></h5>
@if (isEditMode()) {
<button id="save-learning-paths-configuration-button" class="btn btn-primary btn-sm col-md-auto" (click)="saveLearningPathsConfiguration()">
@if (isSaving()) {
<fa-icon [icon]="faSpinner" animation="spin" />
}
<span jhiTranslate="artemisApp.learningPathManagement.learningPathsConfiguration.saveButtonLabel"></span>
</button>
} @else {
<button
id="edit-learning-paths-configuration-button"
class="btn btn-secondary btn-sm col-md-auto"
(click)="enableEditMode()"
jhiTranslate="artemisApp.learningPathManagement.learningPathsConfiguration.editButtonLabel"
></button>
}
</div>
<hr class="my-2" />
<div class="row align-items-center m-0 learning-paths-management-container">
@if (isConfigLoading()) {
<div class="row justify-content-center p-2">
<div class="spinner-border text-primary" role="status">
<span class="sr-only" jhiTranslate="loading"></span>
</div>
</div>
} @else {
<input
id="include-all-graded-exercises-checkbox"
type="checkbox"
class="col-md-auto"
(change)="toggleIncludeAllGradedExercises()"
[checked]="includeAllGradedExercisesEnabled()"
[disabled]="!isEditMode()"
/>
<label class="col-md-auto" jhiTranslate="artemisApp.learningPathManagement.learningPathsConfiguration.configuration.includeAllGradedExercises"></label>
<jhi-help-icon class="col-md-auto p-0" text="artemisApp.learningPathManagement.learningPathsConfiguration.configuration.includeAllGradedExercisesToolTip" />
}
</div>
</div>
Loading

0 comments on commit ecd4138

Please sign in to comment.