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
Show file tree
Hide file tree
Changes from 30 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
Expand Up @@ -692,4 +692,19 @@ 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.
* <p>
* This method queries the repository to count the distinct students whose submissions were impacted
* by feedback entries matching the provided detail text within the specified exercise.
* </p>
*
* @param exerciseId the ID of the exercise for which the affected student count is requested.
* @param detailText the feedback detail text used to filter affected students.
* @return the total number of distinct students affected by the feedback detail text.
*/
az108 marked this conversation as resolved.
Show resolved Hide resolved
public long getAffectedStudentCountByFeedbackDetailText(long exerciseId, String detailText) {
return studentParticipationRepository.countAffectedStudentsByFeedbackDetailText(exerciseId, detailText);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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)
Expand All @@ -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
}

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

/**
* Creates a feedback-specific channel for an exercise within a course.
* <p>
* This method sets up a new channel and adds affected students based on the provided feedback detail text.
* It validates the channel name, creates the channel, and registers affected students as members. Additionally,
* it sends notifications to the added users about the new channel.
* </p>
*
* @param course the course in which the channel is being created.
* @param exerciseId the ID of the exercise associated with the feedback channel.
* @param channelDTO the DTO containing the properties of the channel to be created, such as name, description, and visibility.
* @param feedbackDetailText the feedback detail text used to identify the students affected by the feedback.
* @param requestingUser the user 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) {
az108 marked this conversation as resolved.
Show resolved Hide resolved
var channelToCreate = new Channel();
az108 marked this conversation as resolved.
Show resolved Hide resolved
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");
}

var createdChannel = createChannel(course, channelToCreate, Optional.of(requestingUser));
az108 marked this conversation as resolved.
Show resolved Hide resolved

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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
az108 marked this conversation as resolved.
Show resolved Hide resolved
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -50,7 +51,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)
Expand Down Expand Up @@ -80,10 +83,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;
Expand All @@ -95,6 +101,7 @@ public ChannelResource(ConversationParticipantRepository conversationParticipant
this.tutorialGroupChannelManagementService = tutorialGroupChannelManagementService;
this.singleUserNotificationService = singleUserNotificationService;
this.conversationParticipantRepository = conversationParticipantRepository;
this.studentParticipationRepository = studentParticipationRepository;
}

/**
Expand Down Expand Up @@ -460,6 +467,38 @@ 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.
* <p>
* This endpoint allows authorized users to create a new channel within a course that is specifically designed for discussions
* around a particular exercise's feedback. The channel is populated with all affected students based on the provided feedback detail text.
* </p>
*
* @param courseId the ID of the course where the channel is being created.
* @param exerciseId the ID of the exercise for which the feedback channel is being created.
* @param channelDTO the DTO containing the properties of the channel to be created, such as name, description, and visibility.
* @param feedbackDetailText a string representing 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., "$").
*/
az108 marked this conversation as resolved.
Show resolved Hide resolved
az108 marked this conversation as resolved.
Show resolved Hide resolved
@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 ChannelDTO channelDTO,
@RequestHeader("feedback-detail-text") String feedbackDetailText) throws URISyntaxException {
az108 marked this conversation as resolved.
Show resolved Hide resolved
log.debug("REST request to create feedback channel in course {} with properties: {}", courseId, channelDTO);

var requestingUser = userRepository.getUserWithGroupsAndAuthorities();
var course = courseRepository.findByIdElseThrow(courseId);

checkCommunicationEnabledElseThrow(course);
channelAuthorizationService.isAllowedToCreateChannel(course, requestingUser);
az108 marked this conversation as resolved.
Show resolved Hide resolved

var createdChannel = channelService.createFeedbackChannel(course, exerciseId, channelDTO, feedbackDetailText, requestingUser);
az108 marked this conversation as resolved.
Show resolved Hide resolved

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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1361,4 +1361,48 @@ 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
LEFT JOIN p.submissions s
LEFT JOIN s.results r
LEFT JOIN r.feedbacks f
az108 marked this conversation as resolved.
Show resolved Hide resolved
WHERE p.exercise.id = :exerciseId
AND f.detailText = :detailText
AND p.testRun = FALSE
AND f.positive = FALSE
az108 marked this conversation as resolved.
Show resolved Hide resolved
""")
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
LEFT JOIN p.submissions s
LEFT JOIN s.results r
LEFT JOIN r.feedbacks f
az108 marked this conversation as resolved.
Show resolved Hide resolved
WHERE p.exercise.id = :exerciseId
AND f.detailText = :detailText
AND p.testRun = FALSE
AND f.positive = FALSE
az108 marked this conversation as resolved.
Show resolved Hide resolved
""")
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,24 @@
import { Component, 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 {
affectedStudentsCount = input.required<number>();
readonly TRANSLATION_BASE = 'artemisApp.programmingExercise.configureGrading.feedbackAnalysis.feedbackDetailChannel.confirmationModal';
az108 marked this conversation as resolved.
Show resolved Hide resolved

constructor(private activeModal: NgbActiveModal) {}
az108 marked this conversation as resolved.
Show resolved Hide resolved

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

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