diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/conversation/Channel.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/conversation/Channel.java index e37beff7d4b7..4b69920ee2d1 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/conversation/Channel.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/conversation/Channel.java @@ -57,16 +57,14 @@ public class Channel extends Conversation { * A channel is either public or private. Users need an invitation to join a private channel. Every user can join a public channel. */ @Column(name = "is_public") - @NotNull - private Boolean isPublic; + private boolean isPublic = false; /** * An announcement channel is a special type of channel where only channel moderators and instructors can start new posts. * Answer posts are still possible so that students can ask questions concerning the announcement. */ @Column(name = "is_announcement") - @NotNull - private Boolean isAnnouncementChannel; + private boolean isAnnouncementChannel = false; /** * A channel that is no longer needed can be archived or deleted. @@ -74,8 +72,7 @@ public class Channel extends Conversation { * The channel can be unarchived at any time. */ @Column(name = "is_archived") - @NotNull - private Boolean isArchived; + private boolean isArchived = false; /** * Channels, that are meant to be seen by all course members by default, even if they haven't joined the channel yet, can be flagged with is_course_wide=true. @@ -101,7 +98,7 @@ public class Channel extends Conversation { private Exam exam; public Channel(Long id, User creator, Set conversationParticipants, Set posts, Course course, ZonedDateTime creationDate, - ZonedDateTime lastMessageDate, String name, @Nullable String description, @Nullable String topic, Boolean isPublic, Boolean isAnnouncementChannel, Boolean isArchived, + ZonedDateTime lastMessageDate, String name, @Nullable String description, @Nullable String topic, boolean isPublic, boolean isAnnouncementChannel, boolean isArchived, boolean isCourseWide, Lecture lecture, Exercise exercise, Exam exam) { super(id, creator, conversationParticipants, posts, course, creationDate, lastMessageDate); this.name = name; @@ -138,11 +135,11 @@ public void setDescription(@Nullable String description) { } @Nullable - public Boolean getIsPublic() { + public boolean getIsPublic() { return isPublic; } - public void setIsPublic(@Nullable Boolean isPublic) { + public void setIsPublic(boolean isPublic) { this.isPublic = isPublic; } @@ -155,19 +152,19 @@ public void setTopic(@Nullable String topic) { this.topic = topic; } - public Boolean getIsArchived() { + public boolean getIsArchived() { return isArchived; } - public void setIsArchived(Boolean archived) { + public void setIsArchived(boolean archived) { isArchived = archived; } - public Boolean getIsAnnouncementChannel() { + public boolean getIsAnnouncementChannel() { return isAnnouncementChannel; } - public void setIsAnnouncementChannel(Boolean announcementChannel) { + public void setIsAnnouncementChannel(boolean announcementChannel) { isAnnouncementChannel = announcementChannel; } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/dto/ChannelDTO.java b/src/main/java/de/tum/cit/aet/artemis/communication/dto/ChannelDTO.java index 338a29b2b421..6e5738d3904a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/dto/ChannelDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/dto/ChannelDTO.java @@ -6,6 +6,7 @@ import de.tum.cit.aet.artemis.communication.domain.conversation.ChannelSubType; @JsonInclude(JsonInclude.Include.NON_EMPTY) +// TODO: use record in the future public class ChannelDTO extends ConversationDTO { private String name; @@ -14,25 +15,25 @@ public class ChannelDTO extends ConversationDTO { private String topic; - private Boolean isPublic; + private boolean isPublic = false; // default value - private Boolean isAnnouncementChannel; + private boolean isAnnouncementChannel = false; // default value - private Boolean isArchived; + private boolean isArchived = false; // default value - private Boolean isCourseWide; + private boolean isCourseWide = false; // default value // property not taken from entity /** * A course instructor has channel moderation rights but is not necessarily a moderator of the channel */ - private Boolean hasChannelModerationRights; + private boolean hasChannelModerationRights = false; // default value // property not taken from entity /** * Member of the channel that is also a moderator of the channel */ - private Boolean isChannelModerator; + private boolean isChannelModerator = false; // default value // property not taken from entity /** @@ -98,43 +99,43 @@ public void setTopic(String topic) { this.topic = topic; } - public Boolean getIsPublic() { + public boolean getIsPublic() { return isPublic; } - public void setIsPublic(Boolean isPublic) { + public void setIsPublic(boolean isPublic) { this.isPublic = isPublic; } - public Boolean getIsAnnouncementChannel() { + public boolean getIsAnnouncementChannel() { return isAnnouncementChannel; } - public void setIsAnnouncementChannel(Boolean announcementChannel) { + public void setIsAnnouncementChannel(boolean announcementChannel) { isAnnouncementChannel = announcementChannel; } - public Boolean getIsArchived() { + public boolean getIsArchived() { return isArchived; } - public void setIsArchived(Boolean archived) { + public void setIsArchived(boolean archived) { isArchived = archived; } - public Boolean getHasChannelModerationRights() { + public boolean getHasChannelModerationRights() { return hasChannelModerationRights; } - public void setHasChannelModerationRights(Boolean hasChannelModerationRights) { + public void setHasChannelModerationRights(boolean hasChannelModerationRights) { this.hasChannelModerationRights = hasChannelModerationRights; } - public Boolean getIsChannelModerator() { + public boolean getIsChannelModerator() { return isChannelModerator; } - public void setIsChannelModerator(Boolean isChannelModerator) { + public void setIsChannelModerator(boolean isChannelModerator) { this.isChannelModerator = isChannelModerator; } @@ -162,11 +163,11 @@ public Long getSubTypeReferenceId() { return subTypeReferenceId; } - public Boolean getIsCourseWide() { + public boolean getIsCourseWide() { return isCourseWide; } - public void setIsCourseWide(Boolean courseWide) { + public void setIsCourseWide(boolean courseWide) { isCourseWide = courseWide; } @@ -195,4 +196,16 @@ else if (channel.getExam() != null) { this.subType = ChannelSubType.GENERAL; } } + + public Channel toChannel() { + Channel channel = new Channel(); + channel.setName(this.name); + channel.setDescription(this.description); + channel.setTopic(this.topic); + channel.setIsPublic(this.isPublic); + channel.setIsArchived(this.isArchived); + channel.setIsAnnouncementChannel(this.isAnnouncementChannel); + channel.setIsCourseWide(this.isCourseWide); + return channel; + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/dto/ConversationDTO.java b/src/main/java/de/tum/cit/aet/artemis/communication/dto/ConversationDTO.java index 8ea2d6450ce9..e6500c781cf5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/dto/ConversationDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/dto/ConversationDTO.java @@ -39,6 +39,8 @@ public class ConversationDTO { // property not taken from entity private Long unreadMessagesCount; + // TODO: use boolean where possible + // property not taken from entity private Boolean isFavorite; diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationParticipantRepository.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationParticipantRepository.java index aaf733332105..b56981186c0d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationParticipantRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/ConversationParticipantRepository.java @@ -3,6 +3,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.time.ZonedDateTime; +import java.util.List; import java.util.Optional; import java.util.Set; @@ -60,8 +61,27 @@ public interface ConversationParticipantRepository extends ArtemisJpaRepository< """) void updateLastReadAsync(@Param("userId") Long userId, @Param("conversationId") Long conversationId, @Param("now") ZonedDateTime now); + @Async + @Transactional // ok because of modifying query + @Modifying + @Query(""" + UPDATE ConversationParticipant p + SET p.lastRead = :now, p.unreadMessagesCount = 0 + WHERE p.user.id = :userId + AND p.conversation.id IN :conversationIds + """) + void updateMultipleLastReadAsync(@Param("userId") Long userId, @Param("conversationIds") List conversationIds, @Param("now") ZonedDateTime now); + boolean existsByConversationIdAndUserId(Long conversationId, Long userId); + @Query(""" + SELECT DISTINCT conversationParticipant.conversation.id + FROM ConversationParticipant conversationParticipant + WHERE conversationParticipant.user.id = :userId + AND conversationParticipant.conversation.course.id = :courseId + """) + List findConversationIdsByUserIdAndCourseId(@Param("userId") Long userId, @Param("courseId") Long courseId); + Optional findConversationParticipantByConversationIdAndUserId(Long conversationId, Long userId); @Query(""" diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/conversation/ConversationRepository.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/conversation/ConversationRepository.java index df3782a95a22..e62965d22674 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/conversation/ConversationRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/conversation/ConversationRepository.java @@ -14,6 +14,7 @@ import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; +import de.tum.cit.aet.artemis.communication.domain.conversation.Channel; import de.tum.cit.aet.artemis.communication.domain.conversation.Conversation; import de.tum.cit.aet.artemis.communication.dto.GeneralConversationInfo; import de.tum.cit.aet.artemis.communication.dto.UserConversationInfo; @@ -90,11 +91,17 @@ SELECT COUNT(p.id) > 0 """) boolean userHasUnreadMessageInCourse(@Param("courseId") Long courseId, @Param("userId") Long userId); - /** - * Retrieves a list of conversations for the given course - * - * @param courseId the course id - * @return a list of conversations for the given course - */ - List findAllByCourseId(Long courseId); + @Query(""" + SELECT DISTINCT c + FROM Conversation c + WHERE c.course.id = :courseId + AND TYPE(c) = Channel + AND c.isCourseWide = TRUE + AND c.id NOT IN ( + SELECT cp.conversation.id + FROM ConversationParticipant cp + WHERE cp.user.id = :userId + ) + """) + List findAllCourseWideChannelsByUserIdAndCourseIdWithoutConversationParticipant(@Param("courseId") Long courseId, @Param("userId") Long userId); } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/ChannelService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/ChannelService.java index d16a17430138..8ce8c73a9364 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/ChannelService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/ChannelService.java @@ -256,14 +256,12 @@ public void unarchiveChannel(Long channelId) { * * @param lecture the lecture to create the channel for * @param channelName the name of the channel - * @return the created channel */ - public Channel createLectureChannel(Lecture lecture, Optional channelName) { + public void createLectureChannel(Lecture lecture, Optional channelName) { Channel channelToCreate = createDefaultChannel(channelName, "lecture-", lecture.getTitle()); channelToCreate.setLecture(lecture); Channel createdChannel = createChannel(lecture.getCourse(), channelToCreate, Optional.of(userRepository.getUserWithGroupsAndAuthorities())); lecture.setChannelName(createdChannel.getName()); - return createdChannel; } /** @@ -287,14 +285,12 @@ public Channel createExerciseChannel(Exercise exercise, Optional channel * * @param exam the exam to create the channel for * @param channelName the name of the channel - * @return the created channel */ - public Channel createExamChannel(Exam exam, Optional channelName) { + public void createExamChannel(Exam exam, Optional channelName) { Channel channelToCreate = createDefaultChannel(channelName, "exam-", exam.getTitle()); channelToCreate.setExam(exam); Channel createdChannel = createChannel(exam.getCourse(), channelToCreate, Optional.of(userRepository.getUserWithGroupsAndAuthorities())); exam.setChannelName(createdChannel.getName()); - return createdChannel; } /** @@ -302,17 +298,16 @@ public Channel createExamChannel(Exam exam, Optional channelName) { * * @param originalLecture the original lecture * @param channelName the new channel name - * @return the updated channel */ - public Channel updateLectureChannel(Lecture originalLecture, String channelName) { + public void updateLectureChannel(Lecture originalLecture, String channelName) { if (channelName == null) { - return null; + return; } Channel channel = channelRepository.findChannelByLectureId(originalLecture.getId()); if (channel == null) { - return null; + return; } - return updateChannelName(channel, channelName); + updateChannelName(channel, channelName); } /** @@ -428,18 +423,11 @@ private static String generateChannelNameFromTitle(@NotNull String prefix, Optio * @throws BadRequestAlertException if the channel name starts with an invalid prefix (e.g., "$"). */ public Channel createFeedbackChannel(Course course, Long exerciseId, ChannelDTO channelDTO, List feedbackDetailTexts, String testCaseName, 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()); - - if (channelToCreate.getName() != null && channelToCreate.getName().trim().startsWith("$")) { + if (channelDTO.getName() != null && channelDTO.getName().trim().startsWith("$")) { throw new BadRequestAlertException("User generated channels cannot start with $", "channel", "channelNameInvalid"); } - Channel createdChannel = createChannel(course, channelToCreate, Optional.of(requestingUser)); + Channel createdChannel = createChannel(course, channelDTO.toChannel(), Optional.of(requestingUser)); List userLogins = studentParticipationRepository.findAffectedLoginsByFeedbackDetailText(exerciseId, feedbackDetailTexts, testCaseName); @@ -451,14 +439,4 @@ public Channel createFeedbackChannel(Course course, Long exerciseId, ChannelDTO return createdChannel; } - - /** - * Marks all channels of a course as read for the requesting user. - * - * @param course the course for which all channels should be marked as read. - * @param requestingUser the user requesting the marking of all channels as read. - */ - public void markAllChannelsOfCourseAsRead(Course course, User requestingUser) { - conversationService.markAllConversationOfAUserAsRead(course.getId(), requestingUser); - } } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/ConversationService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/ConversationService.java index 56afa4a5c497..6dd36e5e9bac 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/ConversationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/ConversationService.java @@ -14,6 +14,8 @@ import jakarta.validation.constraints.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -44,11 +46,14 @@ import de.tum.cit.aet.artemis.core.repository.CourseRepository; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; +import de.tum.cit.aet.artemis.core.util.TimeLogUtil; @Profile(PROFILE_CORE) @Service public class ConversationService { + private static final Logger log = LoggerFactory.getLogger(ConversationService.class); + private static final String METIS_WEBSOCKET_CHANNEL_PREFIX = "/topic/metis/"; private final ConversationDTOService conversationDTOService; @@ -111,18 +116,6 @@ public boolean isMember(Long conversationId, Long userId) { return conversationParticipantRepository.existsByConversationIdAndUserId(conversationId, userId); } - /** - * Checks if a user is a member of a conversation and therefore can access it else throws an exception - * - * @param conversationId the id of the conversation - * @param userId the id of the user - */ - public void isMemberElseThrow(Long conversationId, Long userId) { - if (!isMember(conversationId, userId)) { - throw new AccessForbiddenException("User not allowed to access this conversation!"); - } - } - /** * Checks whether the user is a member of the conversation. *

@@ -284,17 +277,6 @@ public void notifyAllConversationMembersAboutUpdate(Conversation conversation) { broadcastOnConversationMembershipChannel(conversation.getCourse(), MetisCrudAction.UPDATE, conversation, usersToContact); } - /** - * Notify all members of a conversation about a new message in the conversation - * - * @param course the course in which the conversation takes place - * @param conversation conversation which members to notify about the new message (except the author) - * @param recipients users to which the notification should be sent - */ - public void notifyAllConversationMembersAboutNewMessage(Course course, Conversation conversation, Set recipients) { - broadcastOnConversationMembershipChannel(course, MetisCrudAction.NEW_MESSAGE, conversation, recipients); - } - /** * Removes users from a conversation * @@ -335,6 +317,7 @@ public void deleteConversation(Conversation conversation) { * @param conversation the conversation that was affected * @param recipients the users to be messaged */ + // TODO: this should be Async public void broadcastOnConversationMembershipChannel(Course course, MetisCrudAction metisCrudAction, Conversation conversation, Set recipients) { String conversationParticipantTopicName = getConversationParticipantTopicName(course.getId()); recipients.forEach(user -> sendToConversationMembershipChannel(metisCrudAction, conversation, user, conversationParticipantTopicName)); @@ -445,27 +428,36 @@ public void setIsMuted(Long conversationId, User requestingUser, boolean isMuted } /** - * Mark all conversation of a user as read + * Mark all conversation of a user as read in the given course * * @param courseId the id of the course * @param requestingUser the user that wants to mark the conversation as read */ public void markAllConversationOfAUserAsRead(Long courseId, User requestingUser) { - List conversations = conversationRepository.findAllByCourseId(courseId); + long start = System.nanoTime(); + // First, update all existing conversation participants with only two database queries ZonedDateTime now = ZonedDateTime.now(); + var userId = requestingUser.getId(); + List conversationIds = conversationParticipantRepository.findConversationIdsByUserIdAndCourseId(userId, courseId); + conversationParticipantRepository.updateMultipleLastReadAsync(userId, conversationIds, now); + + log.debug("Marking all conversations with existing participants as read took {} ms", TimeLogUtil.formatDurationFrom(start)); + start = System.nanoTime(); + + // Then, find all course-wide channels that the user has not yet accessed and create conversation participants for them + List courseWideChannelsWithoutParticipants = conversationRepository.findAllCourseWideChannelsByUserIdAndCourseIdWithoutConversationParticipant(courseId, userId); List participants = new ArrayList<>(); - for (Conversation conversation : conversations) { - boolean userCanBePartOfConversation = conversationParticipantRepository - .findConversationParticipantByConversationIdAndUserId(conversation.getId(), requestingUser.getId()).isPresent() - || (conversation instanceof Channel channel && channel.getIsCourseWide()); - if (userCanBePartOfConversation) { - ConversationParticipant conversationParticipant = getOrCreateConversationParticipant(conversation.getId(), requestingUser); - conversationParticipant.setLastRead(now); - conversationParticipant.setUnreadMessagesCount(0L); - participants.add(conversationParticipant); - } + for (Channel channel : courseWideChannelsWithoutParticipants) { + var newParticipant = ConversationParticipant.createWithDefaultValues(requestingUser, channel); + newParticipant.setUnreadMessagesCount(0L); + newParticipant.setLastRead(now); + participants.add(newParticipant); + } + // save all new conversation participants (i.e. for course-wide channels that the user has not yet accessed) + if (!participants.isEmpty()) { conversationParticipantRepository.saveAll(participants); } + log.debug("Marking all conversations without participants (i.e. creating new ones) as read took {} ms", TimeLogUtil.formatDurationFrom(start)); } /** @@ -482,7 +474,7 @@ public enum ConversationMemberSearchFilters { * @param findAllStudents if true, result includes all users with the student role in the course * @param findAllTutors if true, result includes all users with the tutor role in the course * @param findAllInstructors if true, result includes all users with the instructor role in the course - * @return the list of users found + * @return the set of users found in the database */ public Set findUsersInDatabase(Course course, boolean findAllStudents, boolean findAllTutors, boolean findAllInstructors) { Set users = new HashSet<>(); diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/auth/ChannelAuthorizationService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/auth/ChannelAuthorizationService.java index 5069a67dd5f4..1dd64366240d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/auth/ChannelAuthorizationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/auth/ChannelAuthorizationService.java @@ -54,7 +54,7 @@ public void isAllowedToCreateChannel(@NotNull Course course, @NotNull User user) * @param user the user that wants to create the message */ public void isAllowedToCreateNewAnswerPostInChannel(@NotNull Channel channel, @NotNull User user) { - var isArchivedChannel = channel.getIsArchived() != null && channel.getIsArchived(); + var isArchivedChannel = channel.getIsArchived(); var userToCheck = getUserIfNecessary(user); if (isArchivedChannel) { throw new AccessForbiddenException("You are not allowed to create a new answer post in an archived channel."); @@ -72,8 +72,8 @@ public void isAllowedToCreateNewAnswerPostInChannel(@NotNull Channel channel, @N * @param user the user that wants to create answer the message */ public void isAllowedToCreateNewPostInChannel(@NotNull Channel channel, @NotNull User user) { - var isAnnouncementChannel = channel.getIsAnnouncementChannel() != null && channel.getIsAnnouncementChannel(); - var isArchivedChannel = channel.getIsArchived() != null && channel.getIsArchived(); + var isAnnouncementChannel = channel.getIsAnnouncementChannel(); + var isArchivedChannel = channel.getIsArchived(); if (isArchivedChannel) { throw new AccessForbiddenException("You are not allowed to create a new post in an archived channel."); } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java index cf8b1095f38f..4456f377be1e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java @@ -391,6 +391,7 @@ public record NewReplyNotificationSubject(AnswerPost answerPost, User user, User * @param responsibleUser the responsibleUser that has registered/removed the user for the conversation * @param notificationType the type of notification to be sent */ + // TODO: this should be Async public void notifyClientAboutConversationCreationOrDeletion(Conversation conversation, User user, User responsibleUser, NotificationType notificationType) { notifyRecipientWithNotificationType(new ConversationNotificationSubject(conversation, user, responsibleUser), notificationType, null, null); } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/conversation/ChannelResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/conversation/ChannelResource.java index ed1b4e45d478..f9fcd4a58986 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/conversation/ChannelResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/conversation/ChannelResource.java @@ -54,7 +54,6 @@ 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) @@ -84,13 +83,10 @@ 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, - StudentParticipationRepository studentParticipationRepository) { + ConversationService conversationService, TutorialGroupChannelManagementService tutorialGroupChannelManagementService) { super(courseRepository); this.channelService = channelService; this.channelRepository = channelRepository; @@ -102,7 +98,6 @@ public ChannelResource(ConversationParticipantRepository conversationParticipant this.tutorialGroupChannelManagementService = tutorialGroupChannelManagementService; this.singleUserNotificationService = singleUserNotificationService; this.conversationParticipantRepository = conversationParticipantRepository; - this.studentParticipationRepository = studentParticipationRepository; } /** @@ -226,18 +221,11 @@ public ResponseEntity createChannel(@PathVariable Long courseId, @Re checkCommunicationEnabledElseThrow(course); channelAuthorizationService.isAllowedToCreateChannel(course, requestingUser); - var channelToCreate = new Channel(); - channelToCreate.setName(channelDTO.getName()); - channelToCreate.setIsPublic(channelDTO.getIsPublic()); - channelToCreate.setIsAnnouncementChannel(channelDTO.getIsAnnouncementChannel()); - channelToCreate.setIsArchived(false); - channelToCreate.setDescription(channelDTO.getDescription()); - - if (channelToCreate.getName() != null && channelToCreate.getName().trim().startsWith("$")) { + if (channelDTO.getName() != null && channelDTO.getName().trim().startsWith("$")) { throw new BadRequestAlertException("User generated channels cannot start with $", "channel", "channelNameInvalid"); } - var createdChannel = channelService.createChannel(course, channelToCreate, Optional.of(userRepository.getUserWithGroupsAndAuthorities())); + var createdChannel = channelService.createChannel(course, channelDTO.toChannel(), Optional.of(userRepository.getUserWithGroupsAndAuthorities())); return ResponseEntity.created(new URI("/api/channels/" + createdChannel.getId())).body(conversationDTOService.convertChannelToDTO(requestingUser, createdChannel)); } @@ -498,12 +486,12 @@ public ResponseEntity createFeedbackChannel(@PathVariable Long cours } /** - * PUT /api/courses/:courseId/channels/mark-as-read: Marks all channels of a course as read for the current user. + * POST /api/courses/:courseId/channels/mark-as-read: Marks all channels of a course as read for the current user. * * @param courseId the id of the course. * @return ResponseEntity with status 200 (Ok). */ - @PutMapping("{courseId}/channels/mark-as-read") + @PostMapping("{courseId}/channels/mark-as-read") @EnforceAtLeastStudent public ResponseEntity markAllChannelsOfCourseAsRead(@PathVariable Long courseId) { log.debug("REST request to mark all channels of course {} as read", courseId); @@ -511,7 +499,7 @@ public ResponseEntity markAllChannelsOfCourseAsRead(@PathVariable Lo var course = courseRepository.findByIdElseThrow(courseId); checkCommunicationEnabledElseThrow(course); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, requestingUser); - channelService.markAllChannelsOfCourseAsRead(course, requestingUser); + conversationService.markAllConversationOfAUserAsRead(course.getId(), requestingUser); return ResponseEntity.ok().build(); } diff --git a/src/main/webapp/app/entities/metis/conversation/channel.model.ts b/src/main/webapp/app/entities/metis/conversation/channel.model.ts index dbcc9bb3aac1..541e4f411973 100644 --- a/src/main/webapp/app/entities/metis/conversation/channel.model.ts +++ b/src/main/webapp/app/entities/metis/conversation/channel.model.ts @@ -19,10 +19,10 @@ export class Channel extends Conversation { public name?: string; // max 30 characters public description?: string; // max 250 characters public topic?: string; // max 250 characters; - public isPublic?: boolean; - public isAnnouncementChannel?: boolean; - public isArchived?: boolean; - public isCourseWide?: boolean; + public isPublic = false; // default value + public isAnnouncementChannel = false; // default value + public isArchived = false; // default value + public isCourseWide = false; // default value public exercise?: Exercise; public lecture?: Lecture; @@ -42,12 +42,12 @@ export class ChannelDTO extends ConversationDTO { public name?: string; public description?: string; public topic?: string; - public isPublic?: boolean; - public isAnnouncementChannel?: boolean; - public isArchived?: boolean; - public isChannelModerator?: boolean; - public hasChannelModerationRights?: boolean; - public isCourseWide?: boolean; + public isPublic = false; // default value + public isAnnouncementChannel = false; // default value + public isArchived = false; // default value + public isChannelModerator = false; // default value + public hasChannelModerationRights = false; // default value + public isCourseWide = false; // default value public tutorialGroupId?: number; diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-detail-channel-modal.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-detail-channel-modal.component.ts index 12f65c9710f2..5e9577ea889d 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-detail-channel-modal.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-detail-channel-modal.component.ts @@ -40,8 +40,8 @@ export class FeedbackDetailChannelModalComponent { const channelDTO = new ChannelDTO(); channelDTO.name = this.form.get('name')?.value; channelDTO.description = this.form.get('description')?.value; - channelDTO.isPublic = this.form.get('isPublic')?.value; - channelDTO.isAnnouncementChannel = this.form.get('isAnnouncementChannel')?.value; + channelDTO.isPublic = this.form.get('isPublic')?.value || false; + channelDTO.isAnnouncementChannel = this.form.get('isAnnouncementChannel')?.value || false; this.formSubmitted.emit({ channelDto: channelDTO, navigate }); this.closeModal(); diff --git a/src/main/webapp/app/overview/course-conversations/dialogs/channels-create-dialog/channels-create-dialog.component.ts b/src/main/webapp/app/overview/course-conversations/dialogs/channels-create-dialog/channels-create-dialog.component.ts index 28d5f0f710d4..35c4de065757 100644 --- a/src/main/webapp/app/overview/course-conversations/dialogs/channels-create-dialog/channels-create-dialog.component.ts +++ b/src/main/webapp/app/overview/course-conversations/dialogs/channels-create-dialog/channels-create-dialog.component.ts @@ -35,8 +35,8 @@ export class ChannelsCreateDialogComponent extends AbstractDialogComponent { const { name, description, isPublic, isAnnouncementChannel } = formData; this.channelToCreate.name = name ? name.trim() : undefined; this.channelToCreate.description = description ? description.trim() : undefined; - this.channelToCreate.isPublic = isPublic; - this.channelToCreate.isAnnouncementChannel = isAnnouncementChannel; + this.channelToCreate.isPublic = isPublic ?? false; + this.channelToCreate.isAnnouncementChannel = isAnnouncementChannel ?? false; this.close(this.channelToCreate); } } diff --git a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.ts b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.ts index 3824645cfab9..9112db6a5021 100644 --- a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.ts +++ b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.ts @@ -30,7 +30,7 @@ import { catchError } from 'rxjs/operators'; export class ConversationMemberRowComponent implements OnInit, OnDestroy { private ngUnsubscribe = new Subject(); - activeConversation = input(); + activeConversation = input.required(); course = input(); changePerformed = output(); conversationMember = input(); @@ -90,13 +90,13 @@ export class ConversationMemberRowComponent implements OnInit, OnDestroy { this.setUserAuthorityIconAndTooltip(); // the creator of a channel can not be removed from the channel this.canBeRemovedFromConversation = !this.isCurrentUser && this.canRemoveUsersFromConversation(this.activeConversation()!); - if (isChannelDTO(this.activeConversation()!)) { + if (isChannelDTO(this.activeConversation())) { // the creator of a channel can not be removed from the channel - this.canBeRemovedFromConversation = this.canBeRemovedFromConversation && !this.isCreator && !(this.activeConversation() as ChannelDTO)!.isCourseWide; - this.canBeGrantedChannelModeratorRole = this.canGrantChannelModeratorRole(this.activeConversation()!) && !this.conversationMember()?.isChannelModerator; + const channelDTO = this.activeConversation() as ChannelDTO; + this.canBeRemovedFromConversation = this.canBeRemovedFromConversation && !this.isCreator && !channelDTO.isCourseWide; + this.canBeGrantedChannelModeratorRole = this.canGrantChannelModeratorRole(channelDTO) && !this.conversationMember()?.isChannelModerator; // the creator of a channel cannot be revoked the channel moderator role - this.canBeRevokedChannelModeratorRole = - this.canRevokeChannelModeratorRole(this.activeConversation()!) && !this.isCreator && !!this.conversationMember()?.isChannelModerator; + this.canBeRevokedChannelModeratorRole = this.canRevokeChannelModeratorRole(channelDTO) && !this.isCreator && !!this.conversationMember()?.isChannelModerator; } }); } diff --git a/src/main/webapp/app/shared/metis/conversations/channel.service.ts b/src/main/webapp/app/shared/metis/conversations/channel.service.ts index 61105215ca1c..a893c9386f3b 100644 --- a/src/main/webapp/app/shared/metis/conversations/channel.service.ts +++ b/src/main/webapp/app/shared/metis/conversations/channel.service.ts @@ -2,16 +2,15 @@ import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { ChannelDTO, ChannelIdAndNameDTO } from 'app/entities/metis/conversation/channel.model'; -import { ConversationService } from 'app/shared/metis/conversations/conversation.service'; import { map } from 'rxjs/operators'; import { AccountService } from 'app/core/auth/account.service'; +import { convertDateFromServer } from 'app/utils/date.utils'; @Injectable({ providedIn: 'root' }) export class ChannelService { public resourceUrl = '/api/courses/'; private http = inject(HttpClient); - private conversationService = inject(ConversationService); private accountService = inject(AccountService); getChannelsOfCourse(courseId: number): Observable> { @@ -51,13 +50,11 @@ export class ChannelService { } create(courseId: number, channelDTO: ChannelDTO): Observable> { - return this.http.post(`${this.resourceUrl}${courseId}/channels`, channelDTO, { observe: 'response' }).pipe(map(this.conversationService.convertDateFromServer)); + return this.http.post(`${this.resourceUrl}${courseId}/channels`, channelDTO, { observe: 'response' }).pipe(map(this.convertDateFromServer)); } update(courseId: number, channelId: number, channelDTO: ChannelDTO): Observable> { - return this.http - .put(`${this.resourceUrl}${courseId}/channels/${channelId}`, channelDTO, { observe: 'response' }) - .pipe(map(this.conversationService.convertDateFromServer)); + return this.http.put(`${this.resourceUrl}${courseId}/channels/${channelId}`, channelDTO, { observe: 'response' }).pipe(map(this.convertDateFromServer)); } deregisterUsersFromChannel(courseId: number, channelId: number, logins?: string[]): Observable> { // if no explicit login is give we assume self deregistration @@ -101,4 +98,18 @@ export class ChannelService { const userLogins = logins ? logins : [this.accountService.userIdentity?.login]; return this.http.post(`${this.resourceUrl}${courseId}/channels/${channelId}/revoke-channel-moderator`, userLogins, { observe: 'response' }); } + + public convertDateFromServer = (res: HttpResponse): HttpResponse => { + if (res.body) { + this.convertServerDates(res.body); + } + return res; + }; + + public convertServerDates(conversation: ChannelDTO) { + conversation.creationDate = convertDateFromServer(conversation.creationDate); + conversation.lastMessageDate = convertDateFromServer(conversation.lastMessageDate); + conversation.lastReadDate = convertDateFromServer(conversation.lastReadDate); + return conversation; + } } diff --git a/src/main/webapp/app/shared/metis/conversations/conversation.service.ts b/src/main/webapp/app/shared/metis/conversations/conversation.service.ts index 28cc8e95267d..1705ddd7f168 100644 --- a/src/main/webapp/app/shared/metis/conversations/conversation.service.ts +++ b/src/main/webapp/app/shared/metis/conversations/conversation.service.ts @@ -2,7 +2,7 @@ import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { Conversation, ConversationDTO } from 'app/entities/metis/conversation/conversation.model'; +import { ConversationDTO } from 'app/entities/metis/conversation/conversation.model'; import { TranslateService } from '@ngx-translate/core'; import { AccountService } from 'app/core/auth/account.service'; import { User } from 'app/core/user/user.model'; @@ -11,7 +11,7 @@ import { isGroupChatDTO } from 'app/entities/metis/conversation/group-chat.model import { ConversationUserDTO } from 'app/entities/metis/conversation/conversation-user-dto.model'; import { isOneToOneChatDTO } from 'app/entities/metis/conversation/one-to-one-chat.model'; import { getUserLabel } from 'app/overview/course-conversations/other/conversation.util'; -import { convertDateFromClient, convertDateFromServer } from 'app/utils/date.utils'; +import { convertDateFromServer } from 'app/utils/date.utils'; type EntityArrayResponseType = HttpResponse; @@ -141,12 +141,6 @@ export class ConversationService { return this.http.get(`${this.resourceUrl}${courseId}/code-of-conduct/responsible-users`, { observe: 'response' }); } - public convertDateFromClient = (conversation: Conversation) => ({ - ...conversation, - creationDate: convertDateFromClient(conversation.creationDate), - lastMessageDate: convertDateFromClient(conversation.lastMessageDate), - }); - public convertDateFromServer = (res: HttpResponse): HttpResponse => { if (res.body) { this.convertServerDates(res.body); @@ -184,6 +178,6 @@ export class ConversationService { }; markAllChannelsAsRead(courseId: number) { - return this.http.put(`${this.resourceUrl}${courseId}/channels/mark-as-read`, { observe: 'response' }); + return this.http.post(`${this.resourceUrl}${courseId}/channels/mark-as-read`, { observe: 'response' }); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/ChannelIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/ChannelIntegrationTest.java index fcdaed430ebd..067b1c4626df 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/ChannelIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/ChannelIntegrationTest.java @@ -2,12 +2,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Fail.fail; +import static org.awaitility.Awaitility.await; import java.time.ZonedDateTime; import java.util.Arrays; import java.util.HashSet; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -22,13 +22,11 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.cit.aet.artemis.communication.domain.ConversationParticipant; 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.dto.MetisCrudAction; -import de.tum.cit.aet.artemis.communication.service.conversation.ChannelService; import de.tum.cit.aet.artemis.communication.service.conversation.ConversationService; import de.tum.cit.aet.artemis.communication.util.ConversationUtilService; import de.tum.cit.aet.artemis.core.domain.Course; @@ -78,9 +76,6 @@ class ChannelIntegrationTest extends AbstractConversationTest { @Autowired private ProgrammingExerciseUtilService programmingExerciseUtilService; - @Autowired - private ChannelService channelService; - @BeforeEach @Override void setupTestScenario() throws Exception { @@ -982,9 +977,9 @@ void createFeedbackChannel_asInstructor_shouldCreateChannel() { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void markAllChannelsAsRead() throws Exception { - // ensure there exist atleast two channel with unread messages in the course - ChannelDTO newChannel1 = createChannel(true, "channel1"); - ChannelDTO newChannel2 = createChannel(true, "channel2"); + // ensure there exist at least two channel with unread messages in the course + createChannel(true, "channel1"); + createChannel(true, "channel2"); List channels = channelRepository.findChannelsByCourseId(exampleCourseId); channels.forEach(channel -> { addUsersToConversation(channel.getId(), "instructor1"); @@ -994,13 +989,12 @@ void markAllChannelsAsRead() throws Exception { }); }); - User requestingUser = userTestRepository.getUser(); - request.put("/api/courses/" + exampleCourseId + "/channels/mark-as-read", null, HttpStatus.OK); + User instructor1 = userTestRepository.getUser(); + request.postWithoutLocation("/api/courses/" + exampleCourseId + "/channels/mark-as-read", null, HttpStatus.OK, null); List updatedChannels = channelRepository.findChannelsByCourseId(exampleCourseId); updatedChannels.forEach(channel -> { - Optional conversationParticipant = conversationParticipantRepository.findConversationParticipantByConversationIdAndUserId(channel.getId(), - requestingUser.getId()); - assertThat(conversationParticipant.get().getUnreadMessagesCount()).isEqualTo(0L); + var conversationParticipant = conversationParticipantRepository.findConversationParticipantByConversationIdAndUserId(channel.getId(), instructor1.getId()); + await().untilAsserted(() -> assertThat(conversationParticipant.get().getUnreadMessagesCount()).isZero()); // async db call, so we need to wait }); } diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/ConversationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/ConversationIntegrationTest.java index ef488a032399..8a592cb503e0 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/ConversationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/ConversationIntegrationTest.java @@ -42,28 +42,23 @@ class ConversationIntegrationTest extends AbstractConversationTest { private static final String TEST_PREFIX = "cvtest"; - private final TextExerciseUtilService textExerciseUtilService; + @Autowired + private TextExerciseUtilService textExerciseUtilService; - private final ExerciseUtilService exerciseUtilService; + @Autowired + private ExerciseUtilService exerciseUtilService; - private final ExamUtilService examUtilService; + @Autowired + private ExamUtilService examUtilService; - private final LectureUtilService lectureUtilService; + @Autowired + private LectureUtilService lectureUtilService; - private final ConversationUtilService conversationUtilService; + @Autowired + private ConversationUtilService conversationUtilService; private List users = List.of(); - @Autowired - public ConversationIntegrationTest(TextExerciseUtilService textExerciseUtilService, ExerciseUtilService exerciseUtilService, ExamUtilService examUtilService, - LectureUtilService lectureUtilService, ConversationUtilService conversationUtilService) { - this.textExerciseUtilService = textExerciseUtilService; - this.exerciseUtilService = exerciseUtilService; - this.examUtilService = examUtilService; - this.lectureUtilService = lectureUtilService; - this.conversationUtilService = conversationUtilService; - } - @BeforeEach @Override void setupTestScenario() throws Exception { @@ -212,9 +207,7 @@ void getConversationsOfUser_onlyChannelsIfMessagingDisabled() throws Exception { setCourseInformationSharingConfiguration(CourseInformationSharingConfiguration.COMMUNICATION_ONLY); List channels = request.getList("/api/courses/" + exampleCourseId + "/conversations", HttpStatus.OK, ConversationDTO.class); - assertThat(channels).allSatisfy(ch -> { - assertThat(ch).isInstanceOf(ChannelDTO.class); - }); + assertThat(channels).allSatisfy(ch -> assertThat(ch).isInstanceOf(ChannelDTO.class)); // cleanup conversationMessageRepository.deleteById(post.getId()); diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/test_repository/ConversationTestRepository.java b/src/test/java/de/tum/cit/aet/artemis/communication/test_repository/ConversationTestRepository.java index 7beac73ef014..0256a4dbff90 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/test_repository/ConversationTestRepository.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/test_repository/ConversationTestRepository.java @@ -12,5 +12,11 @@ @Primary public interface ConversationTestRepository extends ConversationRepository { + /** + * Retrieves a list of conversations for the given course + * + * @param courseId the course id + * @return a list of conversations for the given course + */ List findAllByCourseId(long courseId); } diff --git a/src/test/javascript/spec/component/overview/course-conversations/course-conversations.component.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/course-conversations.component.spec.ts index 7add13f9c64f..a702982bdd36 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/course-conversations.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/course-conversations.component.spec.ts @@ -1,6 +1,7 @@ import { CourseConversationsComponent } from 'app/overview/course-conversations/course-conversations.component'; import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; import { ConversationDTO } from 'app/entities/metis/conversation/conversation.model'; +import { OneToOneChatDTO } from '../../../../../../main/webapp/app/entities/metis/conversation/one-to-one-chat.model'; import { generateExampleChannelDTO, generateExampleGroupChatDTO, generateOneToOneChatDTO } from './helpers/conversationExampleModels'; import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; import { MetisConversationService } from 'app/shared/metis/metis-conversation.service'; @@ -44,7 +45,12 @@ import { LayoutService } from 'app/shared/breakpoints/layout.service'; import { CustomBreakpointNames } from 'app/shared/breakpoints/breakpoints.service'; import { Posting, PostingType, SavedPostStatus, SavedPostStatusMap } from 'app/entities/metis/posting.model'; -const examples: (ConversationDTO | undefined)[] = [undefined, generateOneToOneChatDTO({}), generateExampleGroupChatDTO({}), generateExampleChannelDTO({})]; +const examples: (ConversationDTO | undefined)[] = [ + undefined, + generateOneToOneChatDTO({} as OneToOneChatDTO), + generateExampleGroupChatDTO({} as GroupChatDTO), + generateExampleChannelDTO({} as ChannelDTO), +]; examples.forEach((activeConversation) => { describe('CourseConversationComponent with ' + (activeConversation?.type || 'no active conversation'), () => { diff --git a/src/test/javascript/spec/component/overview/course-conversations/dialogs/channels-create-dialog/channels-create-dialog.component.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/dialogs/channels-create-dialog/channels-create-dialog.component.spec.ts index d8e311cd0703..0d74ed850884 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/dialogs/channels-create-dialog/channels-create-dialog.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/dialogs/channels-create-dialog/channels-create-dialog.component.spec.ts @@ -7,7 +7,7 @@ import { Course } from 'app/entities/course.model'; import { Component, EventEmitter, Output } from '@angular/core'; import { ChannelFormData, ChannelType } from 'app/overview/course-conversations/dialogs/channels-create-dialog/channel-form/channel-form.component'; import { By } from '@angular/platform-browser'; -import { Channel } from 'app/entities/metis/conversation/channel.model'; +import { ChannelDTO } from 'app/entities/metis/conversation/channel.model'; import { initializeDialog } from '../dialog-test-helpers'; @Component({ @@ -81,10 +81,10 @@ describe('ChannelsCreateDialogComponent', () => { }; form.formSubmitted.emit(formData); - const expectedChannel = new Channel(); + const expectedChannel = new ChannelDTO(); expectedChannel.name = formData.name; expectedChannel.description = formData.description; - expectedChannel.isPublic = formData.isPublic; + expectedChannel.isPublic = formData.isPublic!; expect(closeSpy).toHaveBeenCalledOnce(); expect(closeSpy).toHaveBeenCalledWith(expectedChannel); diff --git a/src/test/javascript/spec/component/overview/course-conversations/dialogs/channels-overview-dialog/channel-item/channel-item.component.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/dialogs/channels-overview-dialog/channel-item/channel-item.component.spec.ts index 2a2616bfb0d2..1090b60899b2 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/dialogs/channels-overview-dialog/channel-item/channel-item.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/dialogs/channels-overview-dialog/channel-item/channel-item.component.spec.ts @@ -3,6 +3,7 @@ import { ChannelItemComponent } from 'app/overview/course-conversations/dialogs/ import { MockComponent, MockPipe } from 'ng-mocks'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { ChannelIconComponent } from 'app/overview/course-conversations/other/channel-icon/channel-icon.component'; +import { ChannelDTO } from '../../../../../../../../../main/webapp/app/entities/metis/conversation/channel.model'; import { generateExampleChannelDTO } from '../../../helpers/conversationExampleModels'; describe('ChannelItemComponent', () => { @@ -10,7 +11,7 @@ describe('ChannelItemComponent', () => { let fixture: ComponentFixture; const canJoinChannel = jest.fn(); const canLeaveConversation = jest.fn(); - const channel = generateExampleChannelDTO({ id: 1 }); + const channel = generateExampleChannelDTO({ id: 1 } as ChannelDTO); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ChannelItemComponent, MockPipe(ArtemisTranslatePipe), MockComponent(ChannelIconComponent)] }).compileComponents(); @@ -53,7 +54,7 @@ describe('ChannelItemComponent', () => { expect(fixture.nativeElement.querySelector('#deregister' + channel.id)).toBeFalsy(); // change dto to one where not is member - component.channel = generateExampleChannelDTO({ id: 2, isMember: false }); + component.channel = generateExampleChannelDTO({ id: 2, isMember: false } as ChannelDTO); fixture.detectChanges(); expect(fixture.nativeElement.querySelector('#view' + channel.id)).toBeFalsy(); expect(fixture.nativeElement.querySelector('#register' + channel.id)).toBeFalsy(); diff --git a/src/test/javascript/spec/component/overview/course-conversations/dialogs/channels-overview-dialog/channels-overview-dialog.component.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/dialogs/channels-overview-dialog/channels-overview-dialog.component.spec.ts index 3cc001fce612..f44fea3aea36 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/dialogs/channels-overview-dialog/channels-overview-dialog.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/dialogs/channels-overview-dialog/channels-overview-dialog.component.spec.ts @@ -23,17 +23,15 @@ import { NgbCollapseMocksModule } from '../../../../../helpers/mocks/directive/n template: '', }) class ChannelItemStubComponent { - @Output() - channelAction = new EventEmitter(); - @Input() - channel: ChannelDTO; + @Output() channelAction = new EventEmitter(); + @Input() channel: ChannelDTO; } const examples: ChannelDTO[] = [ - generateExampleChannelDTO({}), - generateExampleChannelDTO({ subType: ChannelSubType.EXERCISE }), - generateExampleChannelDTO({ subType: ChannelSubType.LECTURE }), - generateExampleChannelDTO({ subType: ChannelSubType.EXAM }), + generateExampleChannelDTO({} as ChannelDTO), + generateExampleChannelDTO({ subType: ChannelSubType.EXERCISE } as ChannelDTO), + generateExampleChannelDTO({ subType: ChannelSubType.LECTURE } as ChannelDTO), + generateExampleChannelDTO({ subType: ChannelSubType.EXAM } as ChannelDTO), ]; examples.forEach((exampleChannel) => { @@ -76,8 +74,8 @@ examples.forEach((exampleChannel) => { beforeEach(() => { fixture = TestBed.createComponent(ChannelsOverviewDialogComponent); component = fixture.componentInstance; - channelOne = generateExampleChannelDTO({ id: 1, name: 'one', subType: exampleChannel.subType }); - channelTwo = generateExampleChannelDTO({ id: 2, name: 'two', subType: exampleChannel.subType }); + channelOne = generateExampleChannelDTO({ id: 1, name: 'one', subType: exampleChannel.subType } as ChannelDTO); + channelTwo = generateExampleChannelDTO({ id: 2, name: 'two', subType: exampleChannel.subType } as ChannelDTO); channelService = TestBed.inject(ChannelService); getChannelsOfCourseSpy = jest.spyOn(channelService, 'getChannelsOfCourse').mockReturnValue( of( diff --git a/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-add-users-dialog/add-users-form/conversation-add-users-form.component.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-add-users-dialog/add-users-form/conversation-add-users-form.component.spec.ts index 5dba62cd4934..cf12ecb80732 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-add-users-dialog/add-users-form/conversation-add-users-form.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-add-users-dialog/add-users-form/conversation-add-users-form.component.spec.ts @@ -8,14 +8,15 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MockComponent, MockDirective, MockPipe } from 'ng-mocks'; import { CourseUsersSelectorComponent } from 'app/shared/course-users-selector/course-users-selector.component'; import { ConversationDTO } from 'app/entities/metis/conversation/conversation.model'; +import { GroupChatDTO } from '../../../../../../../../../main/webapp/app/entities/metis/conversation/group-chat.model'; import { generateExampleChannelDTO, generateExampleGroupChatDTO } from '../../../helpers/conversationExampleModels'; import { Course } from 'app/entities/course.model'; -import { isChannelDTO } from 'app/entities/metis/conversation/channel.model'; +import { ChannelDTO, isChannelDTO } from 'app/entities/metis/conversation/channel.model'; import { By } from '@angular/platform-browser'; import { UserPublicInfoDTO } from 'app/core/user/user.model'; import { TranslateDirective } from 'app/shared/language/translate.directive'; -const examples: ConversationDTO[] = [generateExampleGroupChatDTO({}), generateExampleChannelDTO({})]; +const examples: ConversationDTO[] = [generateExampleGroupChatDTO({} as GroupChatDTO), generateExampleChannelDTO({} as ChannelDTO)]; examples.forEach((activeConversation) => { describe('ConversationAddUsersFormComponent with ' + activeConversation.type, () => { let component: ConversationAddUsersFormComponent; @@ -164,23 +165,22 @@ examples.forEach((activeConversation) => { expect(component.form.valid).toBeTrue(); expect(component.isSubmitPossible).toBeTrue(); } - const clickSubmitButton = (expectSubmitEvent: boolean, expectedFormData?: AddUsersFormData) => { + const clickSubmitButton = async (expectSubmitEvent: boolean, expectedFormData?: AddUsersFormData) => { const submitFormSpy = jest.spyOn(component, 'submitForm'); const submitFormEventSpy = jest.spyOn(component.formSubmitted, 'emit'); const submitButton = fixture.debugElement.nativeElement.querySelector('#submitButton'); submitButton.click(); - return fixture.whenStable().then(() => { - if (expectSubmitEvent) { - expect(submitFormSpy).toHaveBeenCalledOnce(); - expect(submitFormEventSpy).toHaveBeenCalledOnce(); - expect(submitFormEventSpy).toHaveBeenCalledWith(expectedFormData); - } else { - expect(submitFormSpy).not.toHaveBeenCalled(); - expect(submitFormEventSpy).not.toHaveBeenCalled(); - } - }); + await fixture.whenStable(); + if (expectSubmitEvent) { + expect(submitFormSpy).toHaveBeenCalledOnce(); + expect(submitFormEventSpy).toHaveBeenCalledOnce(); + expect(submitFormEventSpy).toHaveBeenCalledWith(expectedFormData); + } else { + expect(submitFormSpy).not.toHaveBeenCalled(); + expect(submitFormEventSpy).not.toHaveBeenCalled(); + } }; }); }); diff --git a/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-add-users-dialog/conversation-add-users-dialog.component.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-add-users-dialog/conversation-add-users-dialog.component.spec.ts index f2886be487e9..ce48067e8a2e 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-add-users-dialog/conversation-add-users-dialog.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-add-users-dialog/conversation-add-users-dialog.component.spec.ts @@ -16,8 +16,8 @@ import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { ChannelIconComponent } from 'app/overview/course-conversations/other/channel-icon/channel-icon.component'; import { UserPublicInfoDTO } from 'app/core/user/user.model'; import { By } from '@angular/platform-browser'; -import { isChannelDTO } from 'app/entities/metis/conversation/channel.model'; -import { isGroupChatDTO } from 'app/entities/metis/conversation/group-chat.model'; +import { ChannelDTO, isChannelDTO } from 'app/entities/metis/conversation/channel.model'; +import { GroupChatDTO, isGroupChatDTO } from 'app/entities/metis/conversation/group-chat.model'; import { of } from 'rxjs'; import { HttpResponse } from '@angular/common/http'; @Component({ @@ -34,7 +34,7 @@ class ConversationAddUsersFormStubComponent { @Input() activeConversation: ConversationDTO; } -const examples: ConversationDTO[] = [generateExampleGroupChatDTO({}), generateExampleChannelDTO({})]; +const examples: ConversationDTO[] = [generateExampleGroupChatDTO({} as GroupChatDTO), generateExampleChannelDTO({} as ChannelDTO)]; examples.forEach((activeConversation) => { describe('ConversationAddUsersDialogComponent with ' + activeConversation.type, () => { let component: ConversationAddUsersDialogComponent; diff --git a/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/conversation-detail-dialog.component.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/conversation-detail-dialog.component.spec.ts index dc0c8f2325a3..93acf886bdd3 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/conversation-detail-dialog.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/conversation-detail-dialog.component.spec.ts @@ -12,6 +12,7 @@ import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; import { ChannelIconComponent } from 'app/overview/course-conversations/other/channel-icon/channel-icon.component'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { ConversationService } from 'app/shared/metis/conversations/conversation.service'; +import { ChannelDTO } from '../../../../../../../../main/webapp/app/entities/metis/conversation/channel.model'; import { generateExampleChannelDTO, generateExampleGroupChatDTO, generateOneToOneChatDTO } from '../../helpers/conversationExampleModels'; import { initializeDialog } from '../dialog-test-helpers'; import { isOneToOneChatDTO } from 'app/entities/metis/conversation/one-to-one-chat.model'; @@ -65,7 +66,7 @@ class ConversationInfoStubComponent { changesPerformed = new EventEmitter(); } -const examples: ConversationDTO[] = [generateOneToOneChatDTO({}), generateExampleGroupChatDTO({}), generateExampleChannelDTO({})]; +const examples: ConversationDTO[] = [generateOneToOneChatDTO({}), generateExampleGroupChatDTO({}), generateExampleChannelDTO({} as ChannelDTO)]; examples.forEach((activeConversation) => { describe('ConversationDetailDialogComponent with ' + activeConversation.type, () => { diff --git a/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-info/conversation-info.component.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-info/conversation-info.component.spec.ts index 6f8c5379103d..4ac0f2a9d16a 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-info/conversation-info.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-info/conversation-info.component.spec.ts @@ -20,7 +20,7 @@ import { GenericUpdateTextPropertyDialogComponent } from 'app/overview/course-co import { defaultSecondLayerDialogOptions } from 'app/overview/course-conversations/other/conversation.util'; import { input } from '@angular/core'; -const examples: ConversationDTO[] = [generateOneToOneChatDTO({}), generateExampleGroupChatDTO({}), generateExampleChannelDTO({})]; +const examples: ConversationDTO[] = [generateOneToOneChatDTO({}), generateExampleGroupChatDTO({}), generateExampleChannelDTO({} as ChannelDTO)]; examples.forEach((activeConversation) => { describe('ConversationInfoComponent with ' + activeConversation.type, () => { @@ -36,7 +36,7 @@ examples.forEach((activeConversation) => { name: 'updated', description: 'updated', topic: 'updated', - }); + } as ChannelDTO); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ diff --git a/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.spec.ts index 59d135ef102f..453e946afb84 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.spec.ts @@ -42,8 +42,8 @@ const currentUserTemplate = { id: 3, login: 'login3', firstName: 'Kaddl3', lastN const examples: ConversationDTO[] = [ generateOneToOneChatDTO({}), generateExampleGroupChatDTO({}), - generateExampleChannelDTO({}), - generateExampleChannelDTO({ isCourseWide: true }), + generateExampleChannelDTO({} as ChannelDTO), + generateExampleChannelDTO({ isCourseWide: true } as ChannelDTO), ]; examples.forEach((activeConversation) => { diff --git a/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-members.component.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-members.component.spec.ts index 0ea2089fa489..7936f973d076 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-members.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-members.component.spec.ts @@ -11,6 +11,7 @@ import { ItemCountComponent } from 'app/shared/pagination/item-count.component'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { ConversationMemberSearchFilter, ConversationService } from 'app/shared/metis/conversations/conversation.service'; import { AlertService } from 'app/core/util/alert.service'; +import { ChannelDTO } from '../../../../../../../../../../main/webapp/app/entities/metis/conversation/channel.model'; import { generateExampleChannelDTO, generateExampleGroupChatDTO, generateOneToOneChatDTO } from '../../../../helpers/conversationExampleModels'; import { HttpHeaders, HttpResponse } from '@angular/common/http'; import { of } from 'rxjs'; @@ -34,7 +35,7 @@ class ConversationMemberRowStubComponent { @Input() conversationMember: ConversationUserDTO; } -const examples: ConversationDTO[] = [generateOneToOneChatDTO({}), generateExampleGroupChatDTO({}), generateExampleChannelDTO({})]; +const examples: ConversationDTO[] = [generateOneToOneChatDTO({}), generateExampleGroupChatDTO({}), generateExampleChannelDTO({} as ChannelDTO)]; examples.forEach((activeConversation) => { describe('ConversationMembersComponent with ' + activeConversation.type, () => { diff --git a/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-settings/conversation-settings.component.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-settings/conversation-settings.component.spec.ts index 85057eddc05d..075c9c1eede4 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-settings/conversation-settings.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-settings/conversation-settings.component.spec.ts @@ -20,7 +20,7 @@ import { defaultSecondLayerDialogOptions } from 'app/overview/course-conversatio import * as ConversationPermissionUtils from 'app/shared/metis/conversations/conversation-permissions.utils'; import { input, runInInjectionContext } from '@angular/core'; -const examples: ConversationDTO[] = [generateExampleGroupChatDTO({}), generateExampleChannelDTO({})]; +const examples: ConversationDTO[] = [generateExampleGroupChatDTO({}), generateExampleChannelDTO({} as ChannelDTO)]; examples.forEach((activeConversation) => { describe('ConversationSettingsComponent with ' + activeConversation.type, () => { diff --git a/src/test/javascript/spec/component/overview/course-conversations/layout/conversation-header/conversation-header.component.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/layout/conversation-header/conversation-header.component.spec.ts index 32644d2b20b5..ae68ef156219 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/layout/conversation-header/conversation-header.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/layout/conversation-header/conversation-header.component.spec.ts @@ -32,10 +32,10 @@ import { ProfilePictureComponent } from '../../../../../../../../main/webapp/app const examples: ConversationDTO[] = [ generateOneToOneChatDTO({}), generateExampleGroupChatDTO({}), - generateExampleChannelDTO({}), - generateExampleChannelDTO({ subType: ChannelSubType.EXERCISE, subTypeReferenceId: 1 }), - generateExampleChannelDTO({ subType: ChannelSubType.LECTURE, subTypeReferenceId: 1 }), - generateExampleChannelDTO({ subType: ChannelSubType.EXAM, subTypeReferenceId: 1 }), + generateExampleChannelDTO({} as ChannelDTO), + generateExampleChannelDTO({ subType: ChannelSubType.EXERCISE, subTypeReferenceId: 1 } as ChannelDTO), + generateExampleChannelDTO({ subType: ChannelSubType.LECTURE, subTypeReferenceId: 1 } as ChannelDTO), + generateExampleChannelDTO({ subType: ChannelSubType.EXAM, subTypeReferenceId: 1 } as ChannelDTO), ]; examples.forEach((activeConversation) => { describe('ConversationHeaderComponent with' + +(activeConversation instanceof ChannelDTO ? activeConversation.subType + ' ' : '') + activeConversation.type, () => { diff --git a/src/test/javascript/spec/component/overview/course-conversations/layout/conversation-messages/conversation-messages.component.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/layout/conversation-messages/conversation-messages.component.spec.ts index 815c5d9a4501..eadb4d81e8e1 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/layout/conversation-messages/conversation-messages.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/layout/conversation-messages/conversation-messages.component.spec.ts @@ -17,15 +17,15 @@ import { generateExampleChannelDTO, generateExampleGroupChatDTO, generateOneToOn import { Directive, EventEmitter, Input, Output, QueryList } from '@angular/core'; import { By } from '@angular/platform-browser'; import { Course } from 'app/entities/course.model'; -import { getAsChannelDTO } from 'app/entities/metis/conversation/channel.model'; +import { ChannelDTO, getAsChannelDTO } from 'app/entities/metis/conversation/channel.model'; import { PostCreateEditModalComponent } from 'app/shared/metis/posting-create-edit-modal/post-create-edit-modal/post-create-edit-modal.component'; import dayjs from 'dayjs'; const examples: ConversationDTO[] = [ generateOneToOneChatDTO({}), generateExampleGroupChatDTO({}), - generateExampleChannelDTO({}), - generateExampleChannelDTO({ isAnnouncementChannel: true }), + generateExampleChannelDTO({} as ChannelDTO), + generateExampleChannelDTO({ isAnnouncementChannel: true } as ChannelDTO), ]; @Directive({ diff --git a/src/test/javascript/spec/component/overview/course-conversations/services/channel.service.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/services/channel.service.spec.ts index a3ab41a7c960..a9f2f7ab8df2 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/services/channel.service.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/services/channel.service.spec.ts @@ -31,7 +31,7 @@ describe('ChannelService', () => { service = TestBed.inject(ChannelService); httpMock = TestBed.inject(HttpTestingController); - elemDefault = generateExampleChannelDTO({}); + elemDefault = generateExampleChannelDTO({} as ChannelDTO); }); afterEach(() => { diff --git a/src/test/javascript/spec/component/overview/course-conversations/services/conversation-permissions.util.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/services/conversation-permissions.util.spec.ts index 4fb64728679a..1141d65c67c5 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/services/conversation-permissions.util.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/services/conversation-permissions.util.spec.ts @@ -19,39 +19,41 @@ import { ChannelDTO } from 'app/entities/metis/conversation/channel.model'; describe('ConversationPermissionUtils', () => { describe('channels', () => { describe('canCreateNewMessageInConversation', () => { - const channelsWhereNewMessageCanBeCreated = generateExampleChannelDTO({ isMember: true }); + const channelsWhereNewMessageCanBeCreated = generateExampleChannelDTO({ isMember: true } as ChannelDTO); it('can create new message in channel where user is member', () => { expect(canCreateNewMessageInConversation(channelsWhereNewMessageCanBeCreated)).toBeTrue(); }); it('can not create new message in channel where user is not member', () => { - expect(canCreateNewMessageInConversation(generateExampleChannelDTO({ isMember: false }))).toBeFalse(); + expect(canCreateNewMessageInConversation(generateExampleChannelDTO({ isMember: false } as ChannelDTO))).toBeFalse(); }); it('can not create new message in an archived channel', () => { - expect(canCreateNewMessageInConversation(generateExampleChannelDTO({ isArchived: true }))).toBeFalse(); + expect(canCreateNewMessageInConversation(generateExampleChannelDTO({ isArchived: true } as ChannelDTO))).toBeFalse(); }); it('can not create new message in a an announcement channel where user is not moderator', () => { - expect(canCreateNewMessageInConversation(generateExampleChannelDTO({ isMember: false, isAnnouncementChannel: true }))).toBeFalse(); + expect(canCreateNewMessageInConversation(generateExampleChannelDTO({ isMember: false, isAnnouncementChannel: true } as ChannelDTO))).toBeFalse(); }); it('can create new message in a an announcement channel where user is moderator', () => { - expect(canCreateNewMessageInConversation(generateExampleChannelDTO({ isMember: true, isAnnouncementChannel: true, isChannelModerator: true }))).toBeTrue(); + expect( + canCreateNewMessageInConversation(generateExampleChannelDTO({ isMember: true, isAnnouncementChannel: true, isChannelModerator: true } as ChannelDTO)), + ).toBeTrue(); }); it('can create a new message in an announcement channel where user is not moderator but has moderation rights', () => { expect( canCreateNewMessageInConversation( - generateExampleChannelDTO({ isMember: true, isAnnouncementChannel: true, isChannelModerator: false, hasChannelModerationRights: true }), + generateExampleChannelDTO({ isMember: true, isAnnouncementChannel: true, isChannelModerator: false, hasChannelModerationRights: true } as ChannelDTO), ), ).toBeTrue(); }); }); describe('canLeaveConversation', () => { - const channelsThatCanBeLeft = generateExampleChannelDTO({ isMember: true, isCreator: false }); + const channelsThatCanBeLeft = generateExampleChannelDTO({ isMember: true, isCreator: false } as ChannelDTO); it('can leave channel', () => { expect(canLeaveConversation(channelsThatCanBeLeft)).toBeTrue(); }); @@ -70,7 +72,7 @@ describe('ConversationPermissionUtils', () => { }); describe('addUsersToConversation', () => { - const channelWhereUsersCanBeAdded = generateExampleChannelDTO({ hasChannelModerationRights: true, isArchived: false }); + const channelWhereUsersCanBeAdded = generateExampleChannelDTO({ hasChannelModerationRights: true, isArchived: false } as ChannelDTO); it('can add users to channel', () => { expect(canAddUsersToConversation(channelWhereUsersCanBeAdded)).toBeTrue(); @@ -96,7 +98,7 @@ describe('ConversationPermissionUtils', () => { }); describe('removeUsersFromConversation', () => { - const channelsWhereUsersCanBeRemoved = generateExampleChannelDTO({ hasChannelModerationRights: true, isArchived: false, isPublic: false }); + const channelsWhereUsersCanBeRemoved = generateExampleChannelDTO({ hasChannelModerationRights: true, isArchived: false, isPublic: false } as ChannelDTO); it('can remove users to channel', () => { expect(canRemoveUsersFromConversation(channelsWhereUsersCanBeRemoved)).toBeTrue(); @@ -128,7 +130,7 @@ describe('ConversationPermissionUtils', () => { }); describe('canDeleteChannel', () => { const courseWithCorrectRights = { isAtLeastInstructor: true } as Course; - const channelWhereNoModerator = generateExampleChannelDTO({ hasChannelModerationRights: false, isChannelModerator: false, isCreator: false }); + const channelWhereNoModerator = generateExampleChannelDTO({ hasChannelModerationRights: false, isChannelModerator: false, isCreator: false } as ChannelDTO); it('can delete any channel as instructor', () => { expect(canDeleteChannel(courseWithCorrectRights, channelWhereNoModerator)).toBeTrue(); @@ -138,7 +140,7 @@ describe('ConversationPermissionUtils', () => { expect(canDeleteChannel({ isAtLeastInstructor: false, isAtLeastTutor: true } as Course, channelWhereNoModerator)).toBeFalse(); }); - const channelWhereModerator = generateExampleChannelDTO({ hasChannelModerationRights: true, isChannelModerator: true, isCreator: true }); + const channelWhereModerator = generateExampleChannelDTO({ hasChannelModerationRights: true, isChannelModerator: true, isCreator: true } as ChannelDTO); it('can delete self created channel as tutor', () => { expect(canDeleteChannel({ isAtLeastInstructor: false, isAtLeastTutor: true } as Course, channelWhereModerator)).toBeTrue(); }); @@ -149,14 +151,14 @@ describe('ConversationPermissionUtils', () => { isCreator: false, tutorialGroupId: 1, tutorialGroupTitle: 'test', - }); + } as ChannelDTO); it('can not delete tutorial group channel', () => { expect(canDeleteChannel(courseWithCorrectRights, tutorialGroupChannel)).toBeFalse(); }); }); describe('can grant channel moderator role', () => { - const channelWhereRoleCanBeGranted = generateExampleChannelDTO({ hasChannelModerationRights: true }); + const channelWhereRoleCanBeGranted = generateExampleChannelDTO({ hasChannelModerationRights: true } as ChannelDTO); it('can grant moderator role', () => { expect(canGrantChannelModeratorRole(channelWhereRoleCanBeGranted)).toBeTrue(); @@ -168,7 +170,7 @@ describe('ConversationPermissionUtils', () => { }); describe('can revoke channel moderator role', () => { - const channelWhereRoleCanBeRevoked = generateExampleChannelDTO({ hasChannelModerationRights: true }); + const channelWhereRoleCanBeRevoked = generateExampleChannelDTO({ hasChannelModerationRights: true } as ChannelDTO); it('can revoke moderator role', () => { expect(canRevokeChannelModeratorRole(channelWhereRoleCanBeRevoked)).toBeTrue(); @@ -180,7 +182,7 @@ describe('ConversationPermissionUtils', () => { }); describe('can change channel archival state', () => { - const channelThatCanBeArchived = generateExampleChannelDTO({ hasChannelModerationRights: true }); + const channelThatCanBeArchived = generateExampleChannelDTO({ hasChannelModerationRights: true } as ChannelDTO); it('can archive channel', () => { expect(canChangeChannelArchivalState(channelThatCanBeArchived)).toBeTrue(); @@ -192,7 +194,7 @@ describe('ConversationPermissionUtils', () => { }); describe('can change channel properties', () => { - const channelThatCanBeChanged = generateExampleChannelDTO({ hasChannelModerationRights: true, isArchived: false }); + const channelThatCanBeChanged = generateExampleChannelDTO({ hasChannelModerationRights: true, isArchived: false } as ChannelDTO); it('can change channel properties', () => { expect(canChangeChannelProperties(channelThatCanBeChanged)).toBeTrue(); @@ -214,7 +216,7 @@ describe('ConversationPermissionUtils', () => { isArchived: false, hasChannelModerationRights: false, isChannelModerator: false, - }); + } as ChannelDTO); it('can join channel', () => { expect(canJoinChannel(channelThatCanBeJoined)).toBeTrue(); diff --git a/src/test/javascript/spec/component/overview/course-conversations/services/conversation.service.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/services/conversation.service.spec.ts index dc0096d3303e..571a82d5431c 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/services/conversation.service.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/services/conversation.service.spec.ts @@ -2,6 +2,7 @@ import { HttpTestingController, provideHttpClientTesting } from '@angular/common import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { map, take } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; +import { ChannelDTO } from '../../../../../../../main/webapp/app/entities/metis/conversation/channel.model'; import { MockAccountService } from '../../../../helpers/mocks/service/mock-account.service'; import { MockTranslateService } from '../../../../helpers/mocks/service/mock-translate.service'; import { AccountService } from 'app/core/auth/account.service'; @@ -147,7 +148,7 @@ describe('ConversationService', () => { // undefined expect(service.getConversationName(undefined)).toBe(''); // channel - const channel = generateExampleChannelDTO({}); + const channel = generateExampleChannelDTO({} as ChannelDTO); expect(service.getConversationName(channel)).toBe(channel.name); // one to one chat const oneToOneChat = generateOneToOneChatDTO({}); diff --git a/src/test/javascript/spec/component/overview/course-conversations/services/metis-conversation.service.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/services/metis-conversation.service.spec.ts index 1eb01c92d9cb..d707c727fbe7 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/services/metis-conversation.service.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/services/metis-conversation.service.spec.ts @@ -12,6 +12,7 @@ import { ChannelService } from 'app/shared/metis/conversations/channel.service'; import { AccountService } from 'app/core/auth/account.service'; import { HttpResponse } from '@angular/common/http'; import { Subject, forkJoin, of } from 'rxjs'; +import { ConversationDTO } from '../../../../../../../main/webapp/app/entities/metis/conversation/conversation.model'; import { generateExampleChannelDTO, generateExampleGroupChatDTO, generateOneToOneChatDTO } from '../helpers/conversationExampleModels'; import { GroupChatDTO } from 'app/entities/metis/conversation/group-chat.model'; import { OneToOneChatDTO } from 'app/entities/metis/conversation/one-to-one-chat.model'; @@ -60,7 +61,7 @@ describe('MetisConversationService', () => { }); groupChat = generateExampleGroupChatDTO({ id: 1 }); oneToOneChat = generateOneToOneChatDTO({ id: 2 }); - channel = generateExampleChannelDTO({ id: 3 }); + channel = generateExampleChannelDTO({ id: 3 } as ChannelDTO); notificationService = TestBed.inject(NotificationService); newOrUpdatedMessageSubject = new Subject(); @@ -195,7 +196,7 @@ describe('MetisConversationService', () => { return new Promise((done) => { metisConversationService.setUpConversationService(course).subscribe({ complete: () => { - const newChannel = generateExampleChannelDTO({ id: 99 }); + const newChannel = generateExampleChannelDTO({ id: 99 } as ChannelDTO); const createChannelSpy = jest.spyOn(channelService, 'create').mockReturnValue(of(new HttpResponse({ body: newChannel }))); const getConversationSpy = jest .spyOn(conversationService, 'getConversationsOfUser') @@ -301,7 +302,7 @@ describe('MetisConversationService', () => { complete: () => { const websocketDTO = new ConversationWebsocketDTO(); websocketDTO.action = MetisPostAction.CREATE; - websocketDTO.conversation = generateExampleChannelDTO({ id: 99 }); + websocketDTO.conversation = generateExampleChannelDTO({ id: 99 } as ChannelDTO); receiveMockSubject.next(websocketDTO); metisConversationService.conversationsOfUser$.subscribe((conversationsOfUser) => { @@ -421,14 +422,14 @@ describe('MetisConversationService', () => { action: MetisPostAction.CREATE, notification: { title: 'title' }, }; - metisConversationService['conversationsOfUser'] = [{ id: 1, unreadMessageCount: 0 } as ChannelDTO]; + metisConversationService['conversationsOfUser'] = [{ id: 1, unreadMessageCount: 0 } as ConversationDTO]; newOrUpdatedMessageSubject.next(postDTO); expect(metisConversationService['conversationsOfUser'][0].unreadMessagesCount).toBe(1); })); it('should mark messages as read', () => { - metisConversationService['conversationsOfUser'] = [{ id: 1, unreadMessageCount: 1 } as ChannelDTO, { id: 2, unreadMessageCount: 1 } as ChannelDTO]; + metisConversationService['conversationsOfUser'] = [{ id: 1, unreadMessageCount: 1 } as ConversationDTO, { id: 2, unreadMessageCount: 1 } as ConversationDTO]; metisConversationService.markAsRead(2); expect(metisConversationService['conversationsOfUser'][1].unreadMessagesCount).toBe(0); }); diff --git a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.service.spec.ts b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.service.spec.ts index 54c6d6d5a5e3..7823bdaa24f0 100644 --- a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.service.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.service.spec.ts @@ -3,6 +3,7 @@ import { HttpTestingController, provideHttpClientTesting } from '@angular/common import { FeedbackAnalysisService, FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; import { provideHttpClient } from '@angular/common/http'; import { SortingOrder } from 'app/shared/table/pageable-table'; +import { ChannelDTO } from '../../../../../../main/webapp/app/entities/metis/conversation/channel.model'; describe('FeedbackAnalysisService', () => { let service: FeedbackAnalysisService; @@ -152,7 +153,7 @@ describe('FeedbackAnalysisService', () => { description: 'Discussion channel for feedback', isPublic: true, isAnnouncementChannel: false, - }; + } as ChannelDTO; const feedbackChannelRequestMock = { channel: channelDtoMock, diff --git a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-detail-channel-modal.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-detail-channel-modal.component.spec.ts index 1db978174340..c1f78dee4499 100644 --- a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-detail-channel-modal.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-detail-channel-modal.component.spec.ts @@ -71,13 +71,13 @@ describe('FeedbackDetailChannelModalComponent', () => { creationDate: undefined, creator: undefined, description: 'channelDescription', - hasChannelModerationRights: undefined, + hasChannelModerationRights: false, hasUnreadMessage: undefined, id: undefined, isAnnouncementChannel: false, - isArchived: undefined, - isChannelModerator: undefined, - isCourseWide: undefined, + isArchived: false, + isChannelModerator: false, + isCourseWide: false, isCreator: undefined, isFavorite: undefined, isHidden: undefined, diff --git a/src/test/javascript/spec/component/shared/sidebar/conversation-options.component.spec.ts b/src/test/javascript/spec/component/shared/sidebar/conversation-options.component.spec.ts index e36a883298a8..de008e5098bc 100644 --- a/src/test/javascript/spec/component/shared/sidebar/conversation-options.component.spec.ts +++ b/src/test/javascript/spec/component/shared/sidebar/conversation-options.component.spec.ts @@ -31,10 +31,10 @@ import { provideRouter } from '@angular/router'; const examples: (() => ConversationDTO)[] = [ () => generateOneToOneChatDTO({}), () => generateExampleGroupChatDTO({}), - () => generateExampleChannelDTO({}), - () => generateExampleChannelDTO({ subType: ChannelSubType.EXERCISE, subTypeReferenceId: 1 }), - () => generateExampleChannelDTO({ subType: ChannelSubType.LECTURE, subTypeReferenceId: 1 }), - () => generateExampleChannelDTO({ subType: ChannelSubType.EXAM, subTypeReferenceId: 1 }), + () => generateExampleChannelDTO({} as ChannelDTO), + () => generateExampleChannelDTO({ subType: ChannelSubType.EXERCISE, subTypeReferenceId: 1 } as ChannelDTO), + () => generateExampleChannelDTO({ subType: ChannelSubType.LECTURE, subTypeReferenceId: 1 } as ChannelDTO), + () => generateExampleChannelDTO({ subType: ChannelSubType.EXAM, subTypeReferenceId: 1 } as ChannelDTO), ]; examples.forEach((conversation) => { diff --git a/src/test/javascript/spec/service/notification.service.spec.ts b/src/test/javascript/spec/service/notification.service.spec.ts index 19c5d0600c33..8ad4713b1a11 100644 --- a/src/test/javascript/spec/service/notification.service.spec.ts +++ b/src/test/javascript/spec/service/notification.service.spec.ts @@ -515,9 +515,9 @@ describe('Notification Service', () => { const notification = { author: { id: 2 }, target: 'target', notificationDate: dayjs() } as Notification; const postDTO: MetisPostDTO = { post: { - author: { id: 2 }, + author: { id: 2 } as User, conversation: { type: ConversationType.CHANNEL, isCourseWide: true } as Channel, - answers: [{ author: { id: 1 } }, { author: { id: 2 } }], + answers: [{ author: { id: 1 } as User }, { author: { id: 2 } as User }], } as Post, action: MetisPostAction.UPDATE, notification,