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

Adaptive learning: Add learner profile interface #10440

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
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 @@ -24,6 +24,8 @@
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class CourseLearnerProfile extends DomainObject {

public static final String ENTITY_NAME = "courseLearnerProfile";

@ManyToOne
@JoinColumn(name = "learner_profile_id")
private LearnerProfile learnerProfile;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package de.tum.cit.aet.artemis.atlas.dto;

import com.fasterxml.jackson.annotation.JsonInclude;

import de.tum.cit.aet.artemis.atlas.domain.profile.CourseLearnerProfile;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record CourseLearnerProfileDTO(long id, long courseId, int aimForGradeOrBonus, int timeInvestment, int repetitionIntensity) {

/**
* Creates CourseLearnerProfileDTO from given CourseLearnerProfile.
*
* @param courseLearnerProfile The given CourseLearnerProfile
* @return CourseLearnerProfile DTO for transfer
*/
public static CourseLearnerProfileDTO of(CourseLearnerProfile courseLearnerProfile) {
return new CourseLearnerProfileDTO(courseLearnerProfile.getId(), courseLearnerProfile.getCourse().getId(), courseLearnerProfile.getAimForGradeOrBonus(),
courseLearnerProfile.getTimeInvestment(), courseLearnerProfile.getRepetitionIntensity());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.util.Set;

import org.springframework.context.annotation.Profile;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
Expand Down Expand Up @@ -29,4 +31,11 @@ public interface CourseLearnerProfileRepository extends ArtemisJpaRepository<Cou
@Transactional // ok because of delete
@Modifying
void deleteAllByCourse(Course course);

@Query("""
SELECT clp
FROM CourseLearnerProfile clp, User u
WHERE u.login = :login AND u.learnerProfile = clp.learnerProfile
""")
Set<CourseLearnerProfile> findAllByLogin(@Param("login") String login);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package de.tum.cit.aet.artemis.atlas.web;

import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import de.tum.cit.aet.artemis.atlas.domain.profile.CourseLearnerProfile;
import de.tum.cit.aet.artemis.atlas.dto.CourseLearnerProfileDTO;
import de.tum.cit.aet.artemis.atlas.repository.CourseLearnerProfileRepository;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
import de.tum.cit.aet.artemis.core.repository.UserRepository;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent;

@Profile(PROFILE_CORE)
@RestController
@RequestMapping("api/atlas/")
public class LearnerProfileResource {

private static final int MIN_PROFILE_VALUE = 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what are min and max profile values? Can they be named in a more descriptive way?


private static final int MAX_PROFILE_VALUE = 5;

private static final Logger log = LoggerFactory.getLogger(LearnerProfileResource.class);

private final UserRepository userRepository;

private final CourseLearnerProfileRepository courseLearnerProfileRepository;

public LearnerProfileResource(UserRepository userRepository, CourseLearnerProfileRepository courseLearnerProfileRepository) {
this.userRepository = userRepository;
this.courseLearnerProfileRepository = courseLearnerProfileRepository;
}

/**
* GET /learner-profiles/course-learner-profiles : get a Map of a {@link de.tum.cit.aet.artemis.core.domain.Course} id
* to the corresponding {@link CourseLearnerProfile} of the logged-in user.
*
* @return The ResponseEntity with status 200 (OK) and with body containing a map of DTOs, wich contain per course profile data.
*/
@GetMapping("course-learner-profiles")
@EnforceAtLeastStudent
public ResponseEntity<Map<Long, CourseLearnerProfileDTO>> getCourseLearnerProfiles() {
User user = userRepository.getUser();
log.debug("REST request to get all CourseLearnerProfiles of user {}", user.getLogin());
Map<Long, CourseLearnerProfileDTO> result = courseLearnerProfileRepository.findAllByLogin(user.getLogin()).stream()
.collect(Collectors.toMap(profile -> profile.getCourse().getId(), CourseLearnerProfileDTO::of));
return ResponseEntity.ok(result);
}

/**
* Validates that fields are within {@link #MIN_PROFILE_VALUE} and {@link #MAX_PROFILE_VALUE}.
*
* @param value Value of the field
* @param fieldName Field name
*/
private void validateProfileField(int value, String fieldName) {
if (value < MIN_PROFILE_VALUE || value > MAX_PROFILE_VALUE) {
throw new BadRequestAlertException(fieldName + " field is outside valid bounds", CourseLearnerProfile.ENTITY_NAME, fieldName.toLowerCase() + "OutOfBounds", true);
}
}

/**
* PUT /learner-profiles/course-learner-profiles/{courseLearnerProfileId} : update fields in a {@link CourseLearnerProfile}.
*
* @param courseLearnerProfileId ID of the CourseLearnerProfile
* @param courseLearnerProfileDTO {@link CourseLearnerProfileDTO} object from the request body.
* @return A ResponseEntity with a status matching the validity of the request containing the updated profile.
*/
@PutMapping(value = "course-learner-profiles/{courseLearnerProfileId}")
@EnforceAtLeastStudent
public ResponseEntity<CourseLearnerProfileDTO> updateCourseLearnerProfile(@PathVariable long courseLearnerProfileId,
@RequestBody CourseLearnerProfileDTO courseLearnerProfileDTO) {
User user = userRepository.getUser();
log.debug("REST request to update CourseLearnerProfile {} of user {}", courseLearnerProfileId, user);

if (courseLearnerProfileDTO.id() != courseLearnerProfileId) {
throw new BadRequestAlertException("Provided courseLEarnerProfileId does not match CourseLearnerProfile.", CourseLearnerProfile.ENTITY_NAME, "objectDoesNotMatchId",
true);
}

Set<CourseLearnerProfile> clps = courseLearnerProfileRepository.findAllByLogin(user.getLogin());
Optional<CourseLearnerProfile> optionalCourseLearnerProfile = clps.stream()
.filter(clp -> clp.getId() == courseLearnerProfileId && clp.getCourse().getId() == courseLearnerProfileDTO.courseId()).findFirst();

if (optionalCourseLearnerProfile.isEmpty()) {
throw new BadRequestAlertException("CourseLearnerProfile not found.", CourseLearnerProfile.ENTITY_NAME, "courseLearnerProfileNotFound", true);
}

validateProfileField(courseLearnerProfileDTO.aimForGradeOrBonus(), "AimForGradeOrBonus");
validateProfileField(courseLearnerProfileDTO.timeInvestment(), "TimeInvestment");
validateProfileField(courseLearnerProfileDTO.repetitionIntensity(), "RepetitionIntensity");

CourseLearnerProfile updateProfile = optionalCourseLearnerProfile.get();
updateProfile.setAimForGradeOrBonus(courseLearnerProfileDTO.aimForGradeOrBonus());
updateProfile.setTimeInvestment(courseLearnerProfileDTO.timeInvestment());
updateProfile.setRepetitionIntensity(courseLearnerProfileDTO.repetitionIntensity());

CourseLearnerProfile result = courseLearnerProfileRepository.save(updateProfile);
return ResponseEntity.ok(CourseLearnerProfileDTO.of(result));
}
}
7 changes: 7 additions & 0 deletions src/main/webapp/app/entities/learner-profile.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class CourseLearnerProfileDTO {
public id: number;
public courseId: number;
public aimForGradeOrBonus: number;
public timeInvestment: number;
public repetitionIntensity: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Injectable } from '@angular/core';
import { BaseApiHttpService } from 'app/course/learning-paths/services/base-api-http.service';
import { CourseLearnerProfileDTO } from 'app/entities/learner-profile.model';

@Injectable({ providedIn: 'root' })
export class LearnerProfileApiService extends BaseApiHttpService {
async getCourseLearnerProfilesForCurrentUser(): Promise<Record<number, CourseLearnerProfileDTO>> {
return await this.get<Record<number, CourseLearnerProfileDTO>>('atlas/course-learner-profiles');
}

async putUpdatedCourseLearnerProfile(courseLearnerProfile: CourseLearnerProfileDTO): Promise<CourseLearnerProfileDTO> {
return this.put<CourseLearnerProfileDTO>(`atlas/course-learner-profiles/${courseLearnerProfile.id}`, courseLearnerProfile);
Copy link
Contributor

@tobias-lippert tobias-lippert Mar 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no await here, then you can also remove the async

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div #parentContainer class="sliders_control">
<input #initialSlider id="initialSlider" class="form-range" type="range" [value]="initialValue()" min="{{ min() }}" max="{{ max() }}" disabled />
<input #currentSlider id="currentSlider" class="form-range" type="range" value="{{ currentValue() }}" min="{{ min() }}" max="{{ max() }}" disabled />
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.sliders_control {
position: relative;
min-height: 25px;
}

.editing > input[type='range']::-webkit-slider-thumb {
pointer-events: all;
cursor: pointer;
}

.editing > input[type='range']::-moz-range-thumb {
pointer-events: all;
cursor: pointer;
}

.editing > input[type='range']::-ms-thumb {
pointer-events: all;
cursor: pointer;
}
input[type='range']#currentSlider::-webkit-slider-thumb {

Check notice on line 20 in src/main/webapp/app/shared/editable-slider/double-slider.component.scss

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/main/webapp/app/shared/editable-slider/double-slider.component.scss#L20

Expected empty line before rule (rule-empty-line-before)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

codacy

background-color: var(--primary);
}
input[type='range']#currentSlider::-moz-range-thumb {

Check notice on line 23 in src/main/webapp/app/shared/editable-slider/double-slider.component.scss

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/main/webapp/app/shared/editable-slider/double-slider.component.scss#L23

Expected empty line before rule (rule-empty-line-before)
background-color: var(--primary);
}
input[type='range']#currentSlider::-ms-thumb {

Check notice on line 26 in src/main/webapp/app/shared/editable-slider/double-slider.component.scss

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/main/webapp/app/shared/editable-slider/double-slider.component.scss#L26

Expected empty line before rule (rule-empty-line-before)
background-color: var(--primary);
}

input[type='number']::-webkit-inner-spin-button {
opacity: 1;
}
input[type='number']::-webkit-outer-spin-button {

Check notice on line 33 in src/main/webapp/app/shared/editable-slider/double-slider.component.scss

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/main/webapp/app/shared/editable-slider/double-slider.component.scss#L33

Expected empty line before rule (rule-empty-line-before)
opacity: 1;
}

input[type='range'] {
position: absolute;
pointer-events: none;
}

#currentSlider {
z-index: 1;
}

:not(.editing) > #currentSlider::-moz-range-track {
background-color: transparent;
}

.editing > #initialSlider {
display: none;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Component, ElementRef, OnChanges, SimpleChanges, input, model, viewChild } from '@angular/core';
import { EditStateTransition } from 'app/shared/editable-slider/edit-process.component';

@Component({
selector: 'jhi-double-slider',
templateUrl: './double-slider.component.html',
styleUrls: ['./double-slider.component.scss'],
})
export class DoubleSliderComponent implements OnChanges {
editStateTransition = input.required<EditStateTransition>();
currentValue = input.required<number>();
min = input.required<number>();
max = input.required<number>();

currentVal: number;
initialVal: number;

initialValue = model.required<number>();

currentSlider = viewChild.required<ElementRef>('currentSlider');
parentDiv = viewChild.required<ElementRef>('parentContainer');

onEdit() {
this.currentSlider().nativeElement.disabled = false;
this.parentDiv().nativeElement.classList.add('editing');
//ensure, that current val is up-to-date.
this.currentVal = this.currentValue();
this.initialVal = this.initialValue();
}

onAbort() {
this.currentSlider().nativeElement.disabled = true;
this.initialValue.set(this.initialVal);
this.currentSlider().nativeElement.value = this.currentVal;
this.parentDiv().nativeElement.classList.remove('editing');
}

onTrySave() {
this.currentSlider().nativeElement.disabled = true;
this.initialValue.set(this.currentSlider().nativeElement.value);
}

onSaved() {
this.currentVal = this.initialValue();
this.parentDiv().nativeElement.classList.remove('editing');
}

ngOnChanges(changes: SimpleChanges): void {
if (changes.editStateTransition) {
switch (changes.editStateTransition.currentValue) {
case EditStateTransition.Edit:
this.onEdit();
break;
case EditStateTransition.Abort:
this.onAbort();
break;
case EditStateTransition.TrySave:
this.onTrySave();
break;
case EditStateTransition.Saved:
this.onSaved();
break;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div class="position-relative">
<div [ngClass]="{ invisible: !editing }">
<span><fa-icon class="text-success mx-2" [icon]="faCheck" (click)="onSave()" /><fa-icon class="text-danger mx-2" [icon]="faXmark" (click)="onAbort()" /></span>
</div>
<div [ngClass]="{ invisible: editing, 'disabled text-secondary': disabled() }" class="position-absolute top-0 end-0">
<fa-icon class="mx-2" [icon]="faPencil" (click)="onEdit()" />
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
fa-icon {
cursor: pointer;
}

.disabled > fa-icon {
cursor: default;
pointer-events: none;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { NgClass } from '@angular/common';
import { Component, OnChanges, SimpleChanges, input, model } from '@angular/core';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';

import { faCheck, faPencil, faXmark } from '@fortawesome/free-solid-svg-icons';

export enum EditStateTransition {
Edit,
TrySave,
Saved,
Abort,
}

@Component({
selector: 'jhi-edit-process',
templateUrl: './edit-process.component.html',
styleUrls: ['./edit-process.component.scss'],
standalone: true,
imports: [FontAwesomeModule, NgClass],
})
export class EditProcessComponent implements OnChanges {
editStateTransition = model<EditStateTransition>();
disabled = input<boolean>(false);

protected readonly faXmark = faXmark;
protected readonly faPencil = faPencil;
protected readonly faCheck = faCheck;

protected editing: boolean = false;

ngOnChanges(changes: SimpleChanges): void {
if (changes.editStateTransition) {
switch (changes.editStateTransition.currentValue) {
case EditStateTransition.Edit:
this.editing = true;
break;
case EditStateTransition.TrySave:
case EditStateTransition.Abort:
default:
this.editing = false;
break;
}
}
}

onEdit() {
if (!this.disabled()) {
this.editStateTransition.set(EditStateTransition.Edit);
}
}

onSave() {
if (!this.disabled()) {
this.editStateTransition.set(EditStateTransition.TrySave);
}
}

onAbort() {
if (!this.disabled()) {
this.editStateTransition.set(EditStateTransition.Abort);
}
}
}
Loading
Loading