Skip to content
Closed
Show file tree
Hide file tree
Changes from 10 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 @@ -5,11 +5,14 @@
import java.util.Set;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;

import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
Expand All @@ -26,6 +29,8 @@
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class LearnerProfile extends DomainObject {

public static final String ENTITY_NAME = "learnerProfile";

@JsonIgnoreProperties("learnerProfile")
@OneToOne(mappedBy = "learnerProfile", cascade = CascadeType.PERSIST)
private User user;
Expand All @@ -34,6 +39,26 @@ public class LearnerProfile extends DomainObject {
@JsonIgnoreProperties("learnerProfile")
private Set<CourseLearnerProfile> courseLearnerProfiles = new HashSet<>();

@Column(name = "feedback_practical_theoretical")
@Min(1)
@Max(5)
private int feedbackPracticalTheoretical;

@Column(name = "feedback_creative_guidance")
@Min(1)
@Max(5)
private int feedbackCreativeGuidance;

@Column(name = "feedback_followup_summary")
@Min(1)
@Max(5)
private int feedbackFollowupSummary;

@Column(name = "feedback_brief_detailed")
@Min(1)
@Max(5)
private int feedbackBriefDetailed;

public void setUser(User user) {
this.user = user;
}
Expand Down Expand Up @@ -61,4 +86,36 @@ public boolean addAllCourseLearnerProfiles(Collection<? extends CourseLearnerPro
public boolean removeCourseLearnerProfile(CourseLearnerProfile courseLearnerProfile) {
return this.courseLearnerProfiles.remove(courseLearnerProfile);
}

public int getFeedbackPracticalTheoretical() {
return feedbackPracticalTheoretical;
}

public void setFeedbackPracticalTheoretical(int feedbackPracticalTheoretical) {
this.feedbackPracticalTheoretical = feedbackPracticalTheoretical;
}

public int getFeedbackCreativeGuidance() {
return feedbackCreativeGuidance;
}

public void setFeedbackCreativeGuidance(int feedbackCreativeGuidance) {
this.feedbackCreativeGuidance = feedbackCreativeGuidance;
}

public int getFeedbackFollowupSummary() {
return feedbackFollowupSummary;
}

public void setFeedbackFollowupSummary(int feedbackFollowupSummary) {
this.feedbackFollowupSummary = feedbackFollowupSummary;
}

public int getFeedbackBriefDetailed() {
return feedbackBriefDetailed;
}

public void setFeedbackBriefDetailed(int feedbackBriefDetailed) {
this.feedbackBriefDetailed = feedbackBriefDetailed;
}
}
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.LearnerProfile;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record LearnerProfileDTO(long id, int feedbackPracticalTheoretical, int feedbackCreativeGuidance, int feedbackFollowupSummary, int feedbackBriefDetailed) {

/**
* Creates LearnerProfileDTO from given LearnerProfile.
*
* @param learnerProfile The given LearnerProfile
* @return LearnerProfile DTO for transfer
*/
public static LearnerProfileDTO of(LearnerProfile learnerProfile) {
return new LearnerProfileDTO(learnerProfile.getId(), learnerProfile.getFeedbackPracticalTheoretical(), learnerProfile.getFeedbackCreativeGuidance(),
learnerProfile.getFeedbackFollowupSummary(), learnerProfile.getFeedbackBriefDetailed());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package de.tum.cit.aet.artemis.atlas.web;

import java.util.Optional;

import org.springframework.context.annotation.Conditional;
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.config.AtlasEnabled;
import de.tum.cit.aet.artemis.atlas.domain.profile.LearnerProfile;
import de.tum.cit.aet.artemis.atlas.dto.LearnerProfileDTO;
import de.tum.cit.aet.artemis.atlas.repository.LearnerProfileRepository;
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;

@Conditional(AtlasEnabled.class)
@RestController
@RequestMapping("api/atlas/")
public class LearnerProfileResource {

private static final int MIN_PROFILE_VALUE = 1;

private static final int MAX_PROFILE_VALUE = 5;

private final UserRepository userRepository;

private final LearnerProfileRepository learnerProfileRepository;

public LearnerProfileResource(UserRepository userRepository, LearnerProfileRepository learnerProfileRepository) {
this.userRepository = userRepository;
this.learnerProfileRepository = learnerProfileRepository;
}

/**
* 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", LearnerProfile.ENTITY_NAME, fieldName.toLowerCase() + "OutOfBounds", true);
}
}

@GetMapping("learner-profiles")
@EnforceAtLeastStudent
public ResponseEntity<LearnerProfileDTO> getLearnerProfile() {
User user = userRepository.getUser();
LearnerProfile profile = learnerProfileRepository.findByUserElseThrow(user);
return ResponseEntity.ok(LearnerProfileDTO.of(profile));
}

/**
* PUT /learner-profiles/{learnerProfileId} : update fields in a {@link LearnerProfile}.
*
* @param learnerProfileId ID of the LearnerProfile
* @param learnerProfileDTO {@link LearnerProfileDTO} object from the request body.
* @return A ResponseEntity with a status matching the validity of the request containing the updated profile.
*/
@PutMapping(value = "learner-profiles/{learnerProfileId}")
@EnforceAtLeastStudent
public ResponseEntity<LearnerProfileDTO> updateLearnerProfile(@PathVariable long learnerProfileId, @RequestBody LearnerProfileDTO learnerProfileDTO) {
User user = userRepository.getUser();

if (learnerProfileDTO.id() != learnerProfileId) {
throw new BadRequestAlertException("Provided learnerProfileId does not match learnerProfile.", LearnerProfile.ENTITY_NAME, "objectDoesNotMatchId", true);
}

Optional<LearnerProfile> optionalLearnerProfile = learnerProfileRepository.findByUser(user);

if (optionalLearnerProfile.isEmpty()) {
throw new BadRequestAlertException("LearnerProfile not found.", LearnerProfile.ENTITY_NAME, "LearnerProfileNotFound", true);
}

validateProfileField(learnerProfileDTO.feedbackPracticalTheoretical(), "FeedbackPracticalTheoretical");
validateProfileField(learnerProfileDTO.feedbackCreativeGuidance(), "FeedbackCreativeGuidance");
validateProfileField(learnerProfileDTO.feedbackFollowupSummary(), "FeedbackFollowupSummary");
validateProfileField(learnerProfileDTO.feedbackBriefDetailed(), "FeedbackBriefDetailed");

LearnerProfile updateProfile = optionalLearnerProfile.get();
updateProfile.setFeedbackPracticalTheoretical(learnerProfileDTO.feedbackPracticalTheoretical());
updateProfile.setFeedbackCreativeGuidance(learnerProfileDTO.feedbackCreativeGuidance());
updateProfile.setFeedbackFollowupSummary(learnerProfileDTO.feedbackFollowupSummary());
updateProfile.setFeedbackBriefDetailed(learnerProfileDTO.feedbackBriefDetailed());

LearnerProfile result = learnerProfileRepository.save(updateProfile);
return ResponseEntity.ok(LearnerProfileDTO.of(result));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

<changeSet id="20250409191359" author="ahmetsenturk">
<addColumn tableName="learner_profile">
<column name="feedback_practical_theoretical" type="int" defaultValueNumeric="3">
<constraints nullable="false"/>
</column>
<column name="feedback_creative_guidance" type="int" defaultValueNumeric="3">
<constraints nullable="false"/>
</column>
<column name="feedback_followup_summary" type="int" defaultValueNumeric="3">
<constraints nullable="false"/>
</column>
<column name="feedback_brief_detailed" type="int" defaultValueNumeric="3">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>

</databaseChangeLog>
1 change: 1 addition & 0 deletions src/main/resources/config/liquibase/master.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
<include file="classpath:config/liquibase/changelog/20250329172100_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20250330140400_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20250402152800_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20250409191359_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20250404204728_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20250411202800_changelog.xml" relativeToChangelogFile="false"/>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<div class="d-flex justify-content-between">
<div>
<h1 jhiTranslate="artemisApp.learnerProfile.title"></h1>
<div class="d-flex userSettings-info">
<fa-icon class="ng-fa-icon" [icon]="faInfoCircle" />
<span class="ps-1" jhiTranslate="artemisApp.learnerProfile.description"></span>
</div>
</div>
@if (settingsChanged) {
<div class="d-flex align-items-center">
<button type="button" class="btn btn-primary" (click)="save()">
<fa-icon class="ng-fa-icon" [icon]="faSave" />
<span jhiTranslate="entity.action.save"></span>
</button>
</div>
}
</div>

<div class="list-group d-block">
<div class="list-group-item">
<h3 jhiTranslate="artemisApp.learnerProfile.feedback.title"></h3>
<div class="preference-section">
<dt jhiTranslate="artemisApp.learnerProfile.feedback.feedbackPracticalTheoretical.title"></dt>
<p class="text-muted" jhiTranslate="artemisApp.learnerProfile.feedback.feedbackPracticalTheoretical.description"></p>
<mat-slider min="1" max="5" step="1" [discrete]="true" showTickMarks [displayWith]="hideTooltip">
<input
matSliderThumb
[value]="learnerProfile.feedbackPracticalTheoretical"
(valueChange)="learnerProfile.feedbackPracticalTheoretical = $event"
(change)="onSliderChange()"
/>
</mat-slider>
<div class="labels">
<span jhiTranslate="artemisApp.learnerProfile.feedback.feedbackPracticalTheoretical.practical"></span>
<span jhiTranslate="artemisApp.learnerProfile.feedback.feedbackPracticalTheoretical.theoretical"></span>
</div>
</div>
<div class="preference-section">
<dt jhiTranslate="artemisApp.learnerProfile.feedback.feedbackCreativeGuidance.title"></dt>
<p class="text-muted" jhiTranslate="artemisApp.learnerProfile.feedback.feedbackCreativeGuidance.description"></p>
<mat-slider min="1" max="5" step="1" [discrete]="true" showTickMarks [displayWith]="hideTooltip">
<input matSliderThumb [(ngModel)]="learnerProfile.feedbackCreativeGuidance" (change)="onSliderChange()" />
</mat-slider>
<div class="labels">
<span jhiTranslate="artemisApp.learnerProfile.feedback.feedbackCreativeGuidance.creative"></span>
<span jhiTranslate="artemisApp.learnerProfile.feedback.feedbackCreativeGuidance.focused"></span>
</div>
</div>
<div class="preference-section">
<dt jhiTranslate="artemisApp.learnerProfile.feedback.feedbackFollowupSummary.title"></dt>
<p class="text-muted" jhiTranslate="artemisApp.learnerProfile.feedback.feedbackFollowupSummary.description"></p>
<mat-slider min="1" max="5" step="1" [discrete]="true" showTickMarks [displayWith]="hideTooltip">
<input matSliderThumb [(ngModel)]="learnerProfile.feedbackFollowupSummary" (change)="onSliderChange()" />
</mat-slider>
<div class="labels">
<span jhiTranslate="artemisApp.learnerProfile.feedback.feedbackFollowupSummary.followUp"></span>
<span jhiTranslate="artemisApp.learnerProfile.feedback.feedbackFollowupSummary.summary"></span>
</div>
</div>
<div class="preference-section">
<dt jhiTranslate="artemisApp.learnerProfile.feedback.feedbackBriefDetailed.title"></dt>
<p class="text-muted" jhiTranslate="artemisApp.learnerProfile.feedback.feedbackBriefDetailed.description"></p>
<mat-slider min="1" max="5" step="1" [discrete]="true" showTickMarks [displayWith]="hideTooltip">
<input matSliderThumb [(ngModel)]="learnerProfile.feedbackBriefDetailed" (change)="onSliderChange()" />
</mat-slider>
<div class="labels">
<span jhiTranslate="artemisApp.learnerProfile.feedback.feedbackBriefDetailed.brief"></span>
<span jhiTranslate="artemisApp.learnerProfile.feedback.feedbackBriefDetailed.detailed"></span>
</div>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
.preference-section {
margin-bottom: 32px;

.text-muted {
font-size: 0.9rem;
margin-bottom: 16px;
line-height: 1.4;
}

mat-slider {
width: 98%;
margin-bottom: 8px;

::ng-deep {

Check warning on line 14 in src/main/webapp/app/core/user/settings/learner-profile/learner-profile.component.scss

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/main/webapp/app/core/user/settings/learner-profile/learner-profile.component.scss#L14

Unexpected unknown pseudo-element selector "::ng-deep" (selector-pseudo-element-no-unknown)
.mdc-slider__thumb {
border-radius: 50% !important;
width: 24px !important;
height: 24px !important;
background-color: var(--bs-primary) !important;
top: 8px !important;
}

.mdc-slider__track {
height: 8px !important;
border-radius: 5px !important;
margin: 0 -12px !important;
}

.mdc-slider__track--inactive {
background-color: #e0e0e0 !important;
border-radius: 5px !important;
}

.mdc-slider__track--active {
background-color: var(--bs-primary) !important;
border-radius: 5px !important;
}

.mdc-slider__thumb-knob {
border-radius: 100% !important;
width: 24px !important;
height: 24px !important;
}
}
}

.labels {
display: flex;
justify-content: space-between;
color: #666;
font-size: 0.9rem;
}
}

.btn-primary {
padding: 8px 24px;
border-radius: 4px;
font-weight: 500;
transition: background-color 0.2s;

&:hover {
background-color: darken(#007bff, 5%);
}
}
Loading
Loading