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

Programming exercises: Add feedback discussion feature to feedback analysis table #9810

Merged
Changes from 44 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
2aef101
added tests and documentation
az108 Nov 10, 2024
862e9b0
fix
az108 Nov 10, 2024
0fdcfe9
Merge remote-tracking branch 'origin/develop' into feature/programmin…
az108 Nov 11, 2024
8fb7be1
johannes feedback
az108 Nov 11, 2024
ae13615
johannes feedback
az108 Nov 11, 2024
15d3fba
johannes feedback
az108 Nov 11, 2024
7f32c6f
tests fix
az108 Nov 11, 2024
3703ca0
johannes feedback
az108 Nov 11, 2024
3b32032
bilel feedback
az108 Nov 12, 2024
f0b99a3
florian feedback
az108 Nov 12, 2024
4d5a609
client test feedback
az108 Nov 12, 2024
cfd9442
client test feedback
az108 Nov 12, 2024
30ebcb7
client test feedback
az108 Nov 12, 2024
dcfd0a8
Merge branch 'develop' into feature/programming-exercises/add-affecte…
az108 Nov 12, 2024
72b482e
client test
az108 Nov 12, 2024
06c2462
Merge remote-tracking branch 'origin/feature/programming-exercises/ad…
az108 Nov 12, 2024
be28769
ramona feedback
az108 Nov 14, 2024
fe3cb3b
ramona feedback
az108 Nov 14, 2024
3dee150
Merge branch 'develop' into feature/programming-exercises/add-affecte…
az108 Nov 14, 2024
4594289
ramona feedback
az108 Nov 14, 2024
6fa20f9
Merge remote-tracking branch 'origin/feature/programming-exercises/ad…
az108 Nov 14, 2024
7d1b039
server style
az108 Nov 14, 2024
a8f17b7
communication feature added
az108 Nov 16, 2024
bee159f
Merge remote-tracking branch 'origin/develop' into feature/programmin…
az108 Nov 16, 2024
8795e5e
tests added
az108 Nov 17, 2024
60b0ff3
Merge remote-tracking branch 'origin/develop' into feature/programmin…
az108 Nov 17, 2024
45835c4
added translation field
az108 Nov 17, 2024
71a4cdb
server style
az108 Nov 17, 2024
31fa70e
removed functionality for non communication courses
az108 Nov 18, 2024
319b1ca
added missing translation and testfix
az108 Nov 18, 2024
7233d3c
flo feedback
az108 Nov 19, 2024
5d944d7
flo feedback
az108 Nov 19, 2024
bf0611e
flo feedback
az108 Nov 19, 2024
edd9ffd
Merge branch 'develop' into feature/programming-exercises/add-communi…
az108 Nov 19, 2024
ec4dc71
server style
az108 Nov 19, 2024
ffb4ded
Merge remote-tracking branch 'origin/feature/programming-exercises/ad…
az108 Nov 19, 2024
32cf023
markus feedback
az108 Nov 19, 2024
23988ca
Merge branch 'develop' into feature/programming-exercises/add-communi…
az108 Nov 19, 2024
fdbc64c
ramona feedback
az108 Nov 25, 2024
8f81d0a
Merge remote-tracking branch 'origin/feature/programming-exercises/ad…
az108 Nov 25, 2024
d24f50f
Merge branch 'develop' into feature/programming-exercises/add-communi…
az108 Nov 25, 2024
72e849e
translation
az108 Nov 25, 2024
4847f02
Merge remote-tracking branch 'origin/feature/programming-exercises/ad…
az108 Nov 25, 2024
4d47d71
fixed translation
az108 Nov 26, 2024
293b967
Merge branch 'develop' into feature/programming-exercises/add-communi…
az108 Nov 26, 2024
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
@@ -692,4 +692,15 @@ public void deleteLongFeedback(List<Feedback> feedbackList, Result result) {
List<Feedback> feedbacks = new ArrayList<>(feedbackList);
result.updateAllFeedbackItems(feedbacks, true);
}

/**
* Retrieves the number of students affected by a specific feedback detail text for a given exercise.
*
* @param exerciseId for which the affected student count is requested.
* @param detailText used to filter affected students.
* @return the total number of distinct students affected by the feedback detail text.
*/
public long getAffectedStudentCountByFeedbackDetailText(long exerciseId, String detailText) {
return studentParticipationRepository.countAffectedStudentsByFeedbackDetailText(exerciseId, detailText);
}
}
Original file line number Diff line number Diff line change
@@ -373,4 +373,18 @@ public ResponseEntity<Page<FeedbackAffectedStudentDTO>> getAffectedStudentsWithF

return ResponseEntity.ok(participation);
}

/**
* GET /exercises/{exerciseId}/feedback-detail/affected-students : Retrieves the count of students affected by a specific feedback detail text.
*
* @param exerciseId The ID of the exercise for which affected students are counted.
* @param detailText The feedback detail text to filter by.
* @return A {@link ResponseEntity} containing the count of affected students.
*/
@GetMapping("exercises/{exerciseId}/feedback-detail/affected-students")
@EnforceAtLeastEditorInExercise
public ResponseEntity<Long> countAffectedStudentsByFeedbackDetailText(@PathVariable long exerciseId, @RequestParam("detailText") String detailText) {
long affectedStudentCount = resultService.getAffectedStudentCountByFeedbackDetailText(exerciseId, detailText);
return ResponseEntity.ok(affectedStudentCount);
}
az108 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package de.tum.cit.aet.artemis.communication.dto;

import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record FeedbackChannelRequestDTO(ChannelDTO channel, String feedbackDetailText) {
}
Original file line number Diff line number Diff line change
@@ -17,18 +17,21 @@
import org.springframework.util.StringUtils;

import de.tum.cit.aet.artemis.communication.domain.ConversationParticipant;
import de.tum.cit.aet.artemis.communication.domain.NotificationType;
import de.tum.cit.aet.artemis.communication.domain.conversation.Channel;
import de.tum.cit.aet.artemis.communication.dto.ChannelDTO;
import de.tum.cit.aet.artemis.communication.dto.MetisCrudAction;
import de.tum.cit.aet.artemis.communication.repository.ConversationParticipantRepository;
import de.tum.cit.aet.artemis.communication.repository.conversation.ChannelRepository;
import de.tum.cit.aet.artemis.communication.service.conversation.errors.ChannelNameDuplicateException;
import de.tum.cit.aet.artemis.communication.service.notifications.SingleUserNotificationService;
import de.tum.cit.aet.artemis.core.domain.Course;
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.exam.domain.Exam;
import de.tum.cit.aet.artemis.exercise.domain.Exercise;
import de.tum.cit.aet.artemis.exercise.repository.StudentParticipationRepository;
import de.tum.cit.aet.artemis.lecture.domain.Lecture;

@Profile(PROFILE_CORE)
@@ -47,12 +50,18 @@ public class ChannelService {

private final UserRepository userRepository;

private final StudentParticipationRepository studentParticipationRepository;

private final SingleUserNotificationService singleUserNotificationService;

public ChannelService(ConversationParticipantRepository conversationParticipantRepository, ChannelRepository channelRepository, ConversationService conversationService,
UserRepository userRepository) {
UserRepository userRepository, StudentParticipationRepository studentParticipationRepository, SingleUserNotificationService singleUserNotificationService) {
this.conversationParticipantRepository = conversationParticipantRepository;
this.channelRepository = channelRepository;
this.conversationService = conversationService;
this.userRepository = userRepository;
this.studentParticipationRepository = studentParticipationRepository;
this.singleUserNotificationService = singleUserNotificationService;
az108 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
@@ -405,4 +414,40 @@ private static String generateChannelNameFromTitle(@NotNull String prefix, Optio
}
return channelName;
}

/**
* Creates a feedback-specific channel for an exercise within a course.
*
* @param course in which the channel is being created.
* @param exerciseId of the exercise associated with the feedback channel.
* @param channelDTO containing the properties of the channel to be created, such as name, description, and visibility.
* @param feedbackDetailText used to identify the students affected by the feedback.
* @param requestingUser initiating the channel creation request.
* @return the created {@link Channel} object with its properties.
* @throws BadRequestAlertException if the channel name starts with an invalid prefix (e.g., "$").
*/
public Channel createFeedbackChannel(Course course, Long exerciseId, ChannelDTO channelDTO, String feedbackDetailText, User requestingUser) {
Channel channelToCreate = new Channel();
channelToCreate.setName(channelDTO.getName());
channelToCreate.setIsPublic(channelDTO.getIsPublic());
channelToCreate.setIsAnnouncementChannel(channelDTO.getIsAnnouncementChannel());
channelToCreate.setIsArchived(false);
channelToCreate.setDescription(channelDTO.getDescription());

az108 marked this conversation as resolved.
Show resolved Hide resolved
if (channelToCreate.getName() != null && channelToCreate.getName().trim().startsWith("$")) {
throw new BadRequestAlertException("User generated channels cannot start with $", "channel", "channelNameInvalid");
}

Channel createdChannel = createChannel(course, channelToCreate, Optional.of(requestingUser));

List<String> userLogins = studentParticipationRepository.findAffectedLoginsByFeedbackDetailText(exerciseId, feedbackDetailText);

if (userLogins != null && !userLogins.isEmpty()) {
var registeredUsers = registerUsersToChannel(false, false, false, userLogins, course, createdChannel);
registeredUsers.forEach(user -> singleUserNotificationService.notifyClientAboutConversationCreationOrDeletion(createdChannel, user, requestingUser,
NotificationType.CONVERSATION_ADD_USER_CHANNEL));
}

return createdChannel;
}
az108 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -35,13 +35,15 @@
import de.tum.cit.aet.artemis.communication.domain.conversation.Channel;
import de.tum.cit.aet.artemis.communication.dto.ChannelDTO;
import de.tum.cit.aet.artemis.communication.dto.ChannelIdAndNameDTO;
import de.tum.cit.aet.artemis.communication.dto.FeedbackChannelRequestDTO;
import de.tum.cit.aet.artemis.communication.repository.ConversationParticipantRepository;
import de.tum.cit.aet.artemis.communication.repository.conversation.ChannelRepository;
import de.tum.cit.aet.artemis.communication.service.conversation.ChannelService;
import de.tum.cit.aet.artemis.communication.service.conversation.ConversationDTOService;
import de.tum.cit.aet.artemis.communication.service.conversation.ConversationService;
import de.tum.cit.aet.artemis.communication.service.conversation.auth.ChannelAuthorizationService;
import de.tum.cit.aet.artemis.communication.service.notifications.SingleUserNotificationService;
import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.core.exception.AccessForbiddenAlertException;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
@@ -50,7 +52,9 @@
import de.tum.cit.aet.artemis.core.repository.UserRepository;
import de.tum.cit.aet.artemis.core.security.Role;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent;
import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastEditorInCourse;
import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService;
import de.tum.cit.aet.artemis.exercise.repository.StudentParticipationRepository;
import de.tum.cit.aet.artemis.tutorialgroup.service.TutorialGroupChannelManagementService;

@Profile(PROFILE_CORE)
@@ -80,10 +84,13 @@ public class ChannelResource extends ConversationManagementResource {

private final ConversationParticipantRepository conversationParticipantRepository;

private final StudentParticipationRepository studentParticipationRepository;

public ChannelResource(ConversationParticipantRepository conversationParticipantRepository, SingleUserNotificationService singleUserNotificationService,
ChannelService channelService, ChannelRepository channelRepository, ChannelAuthorizationService channelAuthorizationService,
AuthorizationCheckService authorizationCheckService, ConversationDTOService conversationDTOService, CourseRepository courseRepository, UserRepository userRepository,
ConversationService conversationService, TutorialGroupChannelManagementService tutorialGroupChannelManagementService) {
ConversationService conversationService, TutorialGroupChannelManagementService tutorialGroupChannelManagementService,
StudentParticipationRepository studentParticipationRepository) {
super(courseRepository);
this.channelService = channelService;
this.channelRepository = channelRepository;
@@ -95,6 +102,7 @@ public ChannelResource(ConversationParticipantRepository conversationParticipant
this.tutorialGroupChannelManagementService = tutorialGroupChannelManagementService;
this.singleUserNotificationService = singleUserNotificationService;
this.conversationParticipantRepository = conversationParticipantRepository;
this.studentParticipationRepository = studentParticipationRepository;
}

/**
@@ -460,6 +468,34 @@ public ResponseEntity<Void> deregisterUsers(@PathVariable Long courseId, @PathVa
return ResponseEntity.ok().build();
}

/**
* POST /api/courses/:courseId/channels/: Creates a new feedback-specific channel in a course.
*
* @param courseId where the channel is being created.
* @param exerciseId for which the feedback channel is being created.
* @param feedbackChannelRequest containing a DTO with the properties of the channel (e.g., name, description, visibility)
* and the feedback detail text used to determine the affected students to be added to the channel.
* @return ResponseEntity with status 201 (Created) and the body containing the details of the created channel.
* @throws URISyntaxException if the URI for the created resource cannot be constructed.
* @throws BadRequestAlertException if the channel name starts with an invalid prefix (e.g., "$").
*/
@PostMapping("{courseId}/{exerciseId}/feedback-channel")
az108 marked this conversation as resolved.
Show resolved Hide resolved
@EnforceAtLeastEditorInCourse
public ResponseEntity<ChannelDTO> createFeedbackChannel(@PathVariable Long courseId, @PathVariable Long exerciseId,
@RequestBody FeedbackChannelRequestDTO feedbackChannelRequest) throws URISyntaxException {
log.debug("REST request to create feedback channel for course {} and exercise {} with properties: {}", courseId, exerciseId, feedbackChannelRequest);

ChannelDTO channelDTO = feedbackChannelRequest.channel();
String feedbackDetailText = feedbackChannelRequest.feedbackDetailText();

az108 marked this conversation as resolved.
Show resolved Hide resolved
User requestingUser = userRepository.getUserWithGroupsAndAuthorities();
Course course = courseRepository.findByIdElseThrow(courseId);
checkCommunicationEnabledElseThrow(course);
Channel createdChannel = channelService.createFeedbackChannel(course, exerciseId, channelDTO, feedbackDetailText, requestingUser);

return ResponseEntity.created(new URI("/api/channels/" + createdChannel.getId())).body(conversationDTOService.convertChannelToDTO(requestingUser, createdChannel));
az108 marked this conversation as resolved.
Show resolved Hide resolved
}

private void checkEntityIdMatchesPathIds(Channel channel, Optional<Long> courseId, Optional<Long> conversationId) {
courseId.ifPresent(courseIdValue -> {
if (!channel.getCourse().getId().equals(courseIdValue)) {
Original file line number Diff line number Diff line change
@@ -1361,4 +1361,54 @@ SELECT MAX(pr.id)
ORDER BY p.student.firstName ASC
""")
Page<FeedbackAffectedStudentDTO> findAffectedStudentsByFeedbackId(@Param("exerciseId") long exerciseId, @Param("feedbackIds") List<Long> feedbackIds, Pageable pageable);

/**
* Retrieves the logins of students affected by a specific feedback detail text in a given exercise.
*
* @param exerciseId The ID of the exercise for which affected students are requested.
* @param detailText The feedback detail text to filter by.
* @return A list of student logins affected by the given feedback detail text in the specified exercise.
*/
@Query("""
SELECT DISTINCT p.student.login
FROM ProgrammingExerciseStudentParticipation p
INNER JOIN p.submissions s
INNER JOIN s.results r ON r.id = (
SELECT MAX(pr.id)
FROM s.results pr
WHERE pr.participation.id = p.id
)
INNER JOIN r.feedbacks f
WHERE p.exercise.id = :exerciseId
AND f.detailText = :detailText
AND p.testRun = FALSE
""")
List<String> findAffectedLoginsByFeedbackDetailText(@Param("exerciseId") long exerciseId, @Param("detailText") String detailText);

/**
* Counts the number of distinct students affected by a specific feedback detail text for a given programming exercise.
* <p>
* This query identifies students whose submissions were impacted by feedback entries matching the provided detail text
* within the specified exercise. Only students with non-test run submissions and negative feedback entries are considered.
* </p>
*
* @param exerciseId the ID of the programming exercise for which the count is calculated.
* @param detailText the feedback detail text used to filter the affected students.
* @return the total number of distinct students affected by the feedback detail text.
*/
@Query("""
SELECT COUNT(DISTINCT p.student.id)
FROM ProgrammingExerciseStudentParticipation p
INNER JOIN p.submissions s
INNER JOIN s.results r ON r.id = (
SELECT MAX(pr.id)
FROM s.results pr
WHERE pr.participation.id = p.id
)
INNER JOIN r.feedbacks f
WHERE p.exercise.id = :exerciseId
AND f.detailText = :detailText
AND p.testRun = FALSE
""")
long countAffectedStudentsByFeedbackDetailText(@Param("exerciseId") long exerciseId, @Param("detailText") String detailText);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<div class="modal-header">
<h4 class="modal-title" [jhiTranslate]="TRANSLATION_BASE + '.header'"></h4>
<button type="button" class="btn-close" aria-label="Close" (click)="dismiss()"></button>
</div>
<div class="modal-body">
<p [jhiTranslate]="TRANSLATION_BASE + '.confirmationMessage'" [translateValues]="{ count: affectedStudentsCount }"></p>
</div>
az108 marked this conversation as resolved.
Show resolved Hide resolved
<div class="modal-footer">
<button type="button" class="btn btn-secondary" (click)="dismiss()" [jhiTranslate]="TRANSLATION_BASE + '.cancel'"></button>
<button type="button" class="btn btn-primary" (click)="confirm()" [jhiTranslate]="TRANSLATION_BASE + '.confirm'"></button>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Component, inject, input } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module';

@Component({
selector: 'jhi-confirm-feedback-channel-creation-modal',
templateUrl: './confirm-feedback-channel-creation-modal.component.html',
imports: [ArtemisSharedCommonModule],
standalone: true,
})
az108 marked this conversation as resolved.
Show resolved Hide resolved
export class ConfirmFeedbackChannelCreationModalComponent {
protected readonly TRANSLATION_BASE = 'artemisApp.programmingExercise.configureGrading.feedbackAnalysis.feedbackDetailChannel.confirmationModal';
affectedStudentsCount = input.required<number>();
private activeModal = inject(NgbActiveModal);

confirm(): void {
this.activeModal.close(true);
}

dismiss(): void {
this.activeModal.dismiss();
}
}
Loading
Loading