Skip to content

Commit

Permalink
Merge branch 'develop' into feature/programming-exercises/feedback-su…
Browse files Browse the repository at this point in the history
…ggestions-server
  • Loading branch information
pal03377 authored Nov 23, 2023
2 parents 01cb04e + 73a7b57 commit a85364a
Show file tree
Hide file tree
Showing 36 changed files with 405 additions and 178 deletions.
5 changes: 4 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,10 @@ dependencies {
testImplementation "org.gradle:gradle-tooling-api:8.5-rc-3"
testImplementation "org.apache.maven.surefire:surefire-report-parser:3.2.2"
testImplementation "com.opencsv:opencsv:5.8"
testImplementation "io.zonky.test:embedded-database-spring-test:2.3.0"
testImplementation("io.zonky.test:embedded-database-spring-test:2.3.0") {
exclude group: 'org.testcontainers', module: 'mariadb'
exclude group: 'org.testcontainers', module: 'mssqlserver'
}
testImplementation "com.tngtech.archunit:archunit:1.2.0"
testImplementation "org.skyscreamer:jsonassert:1.5.1"
testImplementation ("net.bytebuddy:byte-buddy") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ public interface ChannelRepository extends JpaRepository<Channel, Long> {
""")
List<Channel> findChannelsOfUser(@Param("courseId") Long courseId, @Param("userId") Long userId);

@Query("""
SELECT DISTINCT channel
FROM Channel channel
WHERE channel.course.id = :courseId
AND channel.isCourseWide IS true
ORDER BY channel.name
""")
List<Channel> findCourseWideChannelsInCourse(@Param("courseId") long courseId);

@Query("""
SELECT DISTINCT channel
FROM Channel channel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ public AnswerPost createAnswerMessage(Long courseId, AnswerPost answerMessage) {
answerMessage.setResolvesPost(false);
AnswerPost savedAnswerMessage = answerPostRepository.save(answerMessage);
savedAnswerMessage.getPost().setConversation(conversation);
setAuthorRoleForPosting(savedAnswerMessage, course);
this.preparePostAndBroadcast(savedAnswerMessage, course);
this.singleUserNotificationService.notifyInvolvedUsersAboutNewMessageReply(post, mentionedUsers, savedAnswerMessage, author);
return savedAnswerMessage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import de.tum.in.www1.artemis.domain.*;
import de.tum.in.www1.artemis.domain.ConversationNotificationRecipientSummary;
import de.tum.in.www1.artemis.domain.Course;
import de.tum.in.www1.artemis.domain.Exercise;
import de.tum.in.www1.artemis.domain.User;
import de.tum.in.www1.artemis.domain.enumeration.DisplayPriority;
import de.tum.in.www1.artemis.domain.metis.Post;
import de.tum.in.www1.artemis.domain.metis.conversation.Channel;
Expand Down Expand Up @@ -120,34 +123,49 @@ public Post createMessage(Long courseId, Post newMessage) {
conversationParticipantRepository.updateLastReadAsync(author.getId(), conversation.getId(), ZonedDateTime.now());

var createdMessage = conversationMessageRepository.save(newMessage);
log.debug(" conversationMessageRepository.save DONE");
// set the conversation again, because it might have been lost during save
createdMessage.setConversation(conversation);
// reduce the payload of the response / websocket message: this is important to avoid overloading the involved subsystems
if (createdMessage.getConversation() != null) {
createdMessage.getConversation().hideDetails();
}
log.debug(" conversationMessageRepository.save DONE");
setAuthorRoleForPosting(createdMessage, course);

// TODO: we should consider invoking the following method async to avoid that authors wait for the message creation if many notifications are sent
notifyAboutMessageCreation(author, savedConversation, course, createdMessage, mentionedUsers);
log.debug(" notifyAboutMessageCreation DONE");
return createdMessage;
}

private void notifyAboutMessageCreation(User author, Conversation conversation, Course course, Post createdMessage, Set<User> mentionedUsers) {
Set<ConversationNotificationRecipientSummary> webSocketRecipients = getWebSocketRecipients(conversation).collect(Collectors.toSet());
log.debug(" getWebSocketRecipients DONE");
Set<User> broadcastRecipients = webSocketRecipients.stream()
.map(summary -> new User(summary.userId(), summary.userLogin(), summary.firstName(), summary.lastName(), summary.userLangKey(), summary.userEmail()))
.collect(Collectors.toSet());
// Websocket notification 1: this notifies everyone including the author that there is a new message
Set<ConversationNotificationRecipientSummary> webSocketRecipients;
Set<User> broadcastRecipients;
if (conversation instanceof Channel channel && channel.getIsCourseWide()) {
// We don't need the list of participants for course-wide channels. We can delay the db query and send the WS messages first
broadcastForPost(new PostDTO(createdMessage, MetisCrudAction.CREATE), course, null);
log.debug(" broadcastForPost DONE");

webSocketRecipients = getWebSocketRecipients(conversation).collect(Collectors.toSet());
log.debug(" getWebSocketRecipients DONE");
broadcastRecipients = mapToUsers(webSocketRecipients);
}
else {
// In all other cases we need the list of participants to send the WS messages to the correct topics. Hence, the db query has to be made before sending WS messages
webSocketRecipients = getWebSocketRecipients(conversation).collect(Collectors.toSet());
log.debug(" getWebSocketRecipients DONE");
broadcastRecipients = mapToUsers(webSocketRecipients);

broadcastForPost(new PostDTO(createdMessage, MetisCrudAction.CREATE), course, broadcastRecipients);
log.debug(" broadcastForPost DONE");
}

// Add all mentioned users, including the author (if mentioned). Since working with sets, there are no duplicate user entries
mentionedUsers = mentionedUsers.stream().map(user -> new User(user.getId(), user.getLogin(), user.getFirstName(), user.getLastName(), user.getLangKey(), user.getEmail()))
.collect(Collectors.toSet());
broadcastRecipients.addAll(mentionedUsers);

// Websocket notification 1: this notifies everyone including the author that there is a new message
broadcastForPost(new PostDTO(createdMessage, MetisCrudAction.CREATE), course, broadcastRecipients);
log.debug(" broadcastForPost DONE");

if (conversation instanceof OneToOneChat) {
var getNumberOfPosts = conversationMessageRepository.countByConversationId(conversation.getId());
if (getNumberOfPosts == 1) { // first message in one to one chat --> notify all participants that a conversation with them has been created
Expand Down Expand Up @@ -176,6 +194,18 @@ private void notifyAboutMessageCreation(User author, Conversation conversation,
}
}

/**
* Maps a set of {@link ConversationNotificationRecipientSummary} to a set of {@link User}
*
* @param webSocketRecipients Set of recipient summaries
* @return Set of users meant to receive WebSocket messages
*/
private static Set<User> mapToUsers(Set<ConversationNotificationRecipientSummary> webSocketRecipients) {
return webSocketRecipients.stream()
.map(summary -> new User(summary.userId(), summary.userLogin(), summary.firstName(), summary.lastName(), summary.userLangKey(), summary.userEmail()))
.collect(Collectors.toSet());
}

/**
* Filters the given list of recipients for users that should receive a notification about a new message.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,9 @@ public Channel updateLectureChannel(Lecture originalLecture, String channelName)
return null;
}
Channel channel = channelRepository.findChannelByLectureId(originalLecture.getId());
if (channel == null) {
return null;
}
return updateChannelName(channel, channelName);
}

Expand Down Expand Up @@ -330,11 +333,13 @@ public Channel updateExamChannel(Exam originalExam, Exam updatedExam) {
return null;
}
Channel channel = channelRepository.findChannelByExamId(originalExam.getId());
if (channel == null) {
return null;
}
return updateChannelName(channel, updatedExam.getChannelName());
}

private Channel updateChannelName(Channel channel, String newChannelName) {

// Update channel name if necessary
if (!newChannelName.equals(channel.getName())) {
channel.setName(newChannelName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,19 +144,26 @@ public Optional<Conversation> isMemberOrCreateForCourseWideElseThrow(Long conver
/**
* Gets the conversation in a course for which the user is a member
*
* @param courseId the id of the course
* @param course the course
* @param requestingUser the user for which the conversations are requested
* @return the conversation in the course for which the user is a member
*/
public List<ConversationDTO> getConversationsOfUser(Long courseId, User requestingUser) {
var oneToOneChatsOfUser = oneToOneChatRepository.findActiveOneToOneChatsOfUserWithParticipantsAndUserGroups(courseId, requestingUser.getId());
var channelsOfUser = channelRepository.findChannelsOfUser(courseId, requestingUser.getId());
var groupChatsOfUser = groupChatRepository.findGroupChatsOfUserWithParticipantsAndUserGroups(courseId, requestingUser.getId());

public List<ConversationDTO> getConversationsOfUser(Course course, User requestingUser) {
var conversationsOfUser = new ArrayList<Conversation>();
conversationsOfUser.addAll(oneToOneChatsOfUser);
conversationsOfUser.addAll(groupChatsOfUser);
Course course = courseRepository.findByIdElseThrow(courseId);
List<Channel> channelsOfUser;
if (course.getCourseInformationSharingConfiguration().isMessagingEnabled()) {
var oneToOneChatsOfUser = oneToOneChatRepository.findActiveOneToOneChatsOfUserWithParticipantsAndUserGroups(course.getId(), requestingUser.getId());
conversationsOfUser.addAll(oneToOneChatsOfUser);

var groupChatsOfUser = groupChatRepository.findGroupChatsOfUserWithParticipantsAndUserGroups(course.getId(), requestingUser.getId());
conversationsOfUser.addAll(groupChatsOfUser);

channelsOfUser = channelRepository.findChannelsOfUser(course.getId(), requestingUser.getId());
}
else {
channelsOfUser = channelRepository.findCourseWideChannelsInCourse(course.getId());
}

// if the user is only a student in the course, we filter out all channels that are not yet open
var isOnlyStudent = authorizationCheckService.isOnlyStudentInCourse(course, requestingUser);
var filteredChannels = isOnlyStudent ? filterVisibleChannelsForStudents(channelsOfUser.stream()).toList() : channelsOfUser;
Expand All @@ -172,7 +179,7 @@ public List<ConversationDTO> getConversationsOfUser(Long courseId, User requesti
for (Channel channel : filteredChannels) {
if (channel.getIsCourseWide()) {
if (numberOfCourseMembers == null) {
numberOfCourseMembers = courseRepository.countCourseMembers(courseId);
numberOfCourseMembers = courseRepository.countCourseMembers(course.getId());
}
generalConversationInfos.get(channel.getId()).setNumberOfParticipants(numberOfCourseMembers);
}
Expand All @@ -195,6 +202,10 @@ public boolean userHasUnreadMessages(Long courseId, User requestingUser) {
return conversationRepository.userHasUnreadMessageInCourse(courseId, requestingUser.getId());
}

public void markAsRead(Long conversationId, Long userId) {
conversationParticipantRepository.updateLastReadAsync(userId, conversationId, ZonedDateTime.now());
}

/**
* Updates a conversation
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ public ProgrammingPlagiarismDetectionService(ProgrammingExerciseRepository progr
* @param programmingExerciseId the id of the programming exercises which should be checked
* @param similarityThreshold ignore comparisons whose similarity is below this threshold (in % between 0 and 100)
* @param minimumScore consider only submissions whose score is greater or equal to this value
* @param minimumSize consider only submissions whose number of lines in diff to template is greater or equal to this value
* @return the text plagiarism result container with up to 500 comparisons with the highest similarity values
* @throws ExitException is thrown if JPlag exits unexpectedly
* @throws IOException is thrown for file handling errors
Expand Down Expand Up @@ -145,6 +146,7 @@ public TextPlagiarismResult checkPlagiarism(long programmingExerciseId, float si
* @param programmingExerciseId the id of the programming exercises which should be checked
* @param similarityThreshold ignore comparisons whose similarity is below this threshold (in % between 0 and 100)
* @param minimumScore consider only submissions whose score is greater or equal to this value
* @param minimumSize consider only submissions whose number of lines in diff to template is greater or equal to this value
* @return a zip file that can be returned to the client
*/
public File checkPlagiarismWithJPlagReport(long programmingExerciseId, float similarityThreshold, int minimumScore, int minimumSize) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ public ResponseEntity<ChannelDTO> createChannel(@PathVariable Long courseId, @Re
@EnforceAtLeastStudent
public ResponseEntity<ChannelDTO> updateChannel(@PathVariable Long courseId, @PathVariable Long channelId, @RequestBody ChannelDTO channelDTO) {
log.debug("REST request to update channel {} with properties : {}", channelId, channelDTO);
checkMessagingEnabledElseThrow(courseId);
checkMessagingOrCommunicationEnabledElseThrow(courseId);

var originalChannel = channelRepository.findByIdElseThrow(channelId);
var requestingUser = userRepository.getUserWithGroupsAndAuthorities();
Expand Down Expand Up @@ -230,7 +230,7 @@ public ResponseEntity<ChannelDTO> updateChannel(@PathVariable Long courseId, @Pa
@EnforceAtLeastStudent
public ResponseEntity<Void> deleteChannel(@PathVariable Long courseId, @PathVariable Long channelId) {
log.debug("REST request to delete channel {}", channelId);
checkMessagingEnabledElseThrow(courseId);
checkMessagingOrCommunicationEnabledElseThrow(courseId);
var channel = channelRepository.findByIdElseThrow(channelId);
if (!channel.getCourse().getId().equals(courseId)) {
throw new BadRequestAlertException("The channel does not belong to the course", CHANNEL_ENTITY_NAME, "channel.course.mismatch");
Expand Down Expand Up @@ -261,7 +261,7 @@ public ResponseEntity<Void> deleteChannel(@PathVariable Long courseId, @PathVari
@EnforceAtLeastStudent
public ResponseEntity<Void> archiveChannel(@PathVariable Long courseId, @PathVariable Long channelId) {
log.debug("REST request to archive channel : {}", channelId);
checkMessagingEnabledElseThrow(courseId);
checkMessagingOrCommunicationEnabledElseThrow(courseId);
var channelFromDatabase = channelRepository.findByIdElseThrow(channelId);
checkEntityIdMatchesPathIds(channelFromDatabase, Optional.of(courseId), Optional.of(channelId));
channelAuthorizationService.isAllowedToArchiveChannel(channelFromDatabase, userRepository.getUserWithGroupsAndAuthorities());
Expand All @@ -280,7 +280,7 @@ public ResponseEntity<Void> archiveChannel(@PathVariable Long courseId, @PathVar
@EnforceAtLeastStudent
public ResponseEntity<Void> unArchiveChannel(@PathVariable Long courseId, @PathVariable Long channelId) {
log.debug("REST request to unarchive channel : {}", channelId);
checkMessagingEnabledElseThrow(courseId);
checkMessagingOrCommunicationEnabledElseThrow(courseId);
var channelFromDatabase = channelRepository.findByIdElseThrow(channelId);
checkEntityIdMatchesPathIds(channelFromDatabase, Optional.of(courseId), Optional.of(channelId));
channelAuthorizationService.isAllowedToUnArchiveChannel(channelFromDatabase, userRepository.getUserWithGroupsAndAuthorities());
Expand All @@ -300,7 +300,7 @@ public ResponseEntity<Void> unArchiveChannel(@PathVariable Long courseId, @PathV
@EnforceAtLeastStudent
public ResponseEntity<Void> grantChannelModeratorRole(@PathVariable Long courseId, @PathVariable Long channelId, @RequestBody List<String> userLogins) {
log.debug("REST request to grant channel moderator role to users {} in channel {}", userLogins.toString(), channelId);
checkMessagingEnabledElseThrow(courseId);
checkMessagingOrCommunicationEnabledElseThrow(courseId);
var channel = channelRepository.findByIdElseThrow(channelId);
if (!channel.getCourse().getId().equals(courseId)) {
throw new BadRequestAlertException("The channel does not belong to the course", CHANNEL_ENTITY_NAME, "channel.course.mismatch");
Expand All @@ -323,7 +323,7 @@ public ResponseEntity<Void> grantChannelModeratorRole(@PathVariable Long courseI
@EnforceAtLeastStudent
public ResponseEntity<Void> revokeChannelModeratorRole(@PathVariable Long courseId, @PathVariable Long channelId, @RequestBody List<String> userLogins) {
log.debug("REST request to revoke channel moderator role from users {} in channel {}", userLogins.toString(), channelId);
checkMessagingEnabledElseThrow(courseId);
checkMessagingOrCommunicationEnabledElseThrow(courseId);
var channel = channelRepository.findByIdElseThrow(channelId);
if (!channel.getCourse().getId().equals(courseId)) {
throw new BadRequestAlertException("The channel does not belong to the course", CHANNEL_ENTITY_NAME, "channel.course.mismatch");
Expand Down
Loading

0 comments on commit a85364a

Please sign in to comment.