From 2ccea6e37c4f81fe7cbbc15bc17e9c82a237d4f5 Mon Sep 17 00:00:00 2001 From: Till Wanner Date: Wed, 22 Feb 2023 14:50:56 +0100 Subject: [PATCH 01/30] styling of the Session Page --- frontend/src/views/SessionPage.vue | 33 ++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/SessionPage.vue b/frontend/src/views/SessionPage.vue index 0b235e8f4..ab95cc703 100644 --- a/frontend/src/views/SessionPage.vue +++ b/frontend/src/views/SessionPage.vue @@ -48,7 +48,9 @@ - + @@ -134,6 +136,10 @@ +
+ + +
- + From 6397d8dc31085235a03e6062fab078465ba16c38 Mon Sep 17 00:00:00 2001 From: Till Wanner Date: Wed, 8 Mar 2023 11:34:22 +0100 Subject: [PATCH 02/30] voting as Admin is possible, Backend done, Frontend Restart Voting needs to be worked on --- .../backend/controller/RoutesController.java | 2 + .../controller/WebsocketController.java | 36 ++++++- .../java/io/diveni/backend/model/Session.java | 101 +++++++++++++++--- .../backend/service/WebSocketService.java | 38 +++++++ .../controller/RoutesControllerTest.java | 20 ++++ .../controller/WebsocketControllerTest.java | 66 +++++++++++- .../io/diveni/backend/model/SessionTest.java | 79 +++++++++++++- .../repository/SessionRepositoryTest.java | 4 + .../backend/service/WebSocketServiceTest.java | 42 ++++++++ frontend/src/components/SessionAdminCard.vue | 82 ++++++++++++++ .../components/actions/SessionStartButton.vue | 19 ++-- frontend/src/constants.ts | 8 ++ frontend/src/store/index.ts | 12 +++ frontend/src/types.ts | 2 + frontend/src/views/JoinPage.vue | 8 ++ frontend/src/views/MemberVotePage.vue | 12 +++ frontend/src/views/SessionPage.vue | 66 +++++++++++- 17 files changed, 567 insertions(+), 30 deletions(-) create mode 100644 frontend/src/components/SessionAdminCard.vue diff --git a/backend/src/main/java/io/diveni/backend/controller/RoutesController.java b/backend/src/main/java/io/diveni/backend/controller/RoutesController.java index bb709f69a..1d600b556 100644 --- a/backend/src/main/java/io/diveni/backend/controller/RoutesController.java +++ b/backend/src/main/java/io/diveni/backend/controller/RoutesController.java @@ -86,6 +86,8 @@ public ResponseEntity> createSession( SessionState.WAITING_FOR_MEMBERS, null, accessToken, + null, + false, null); databaseService.saveSession(session); val responseMap = Map.of("session", session, "adminCookie", session.getAdminCookie()); diff --git a/backend/src/main/java/io/diveni/backend/controller/WebsocketController.java b/backend/src/main/java/io/diveni/backend/controller/WebsocketController.java index 31dd0aad3..7e54ebafe 100644 --- a/backend/src/main/java/io/diveni/backend/controller/WebsocketController.java +++ b/backend/src/main/java/io/diveni/backend/controller/WebsocketController.java @@ -137,7 +137,7 @@ public void getMemberUpdate(AdminPrincipal principal) { } @MessageMapping("/startVoting") - public void startEstimation(AdminPrincipal principal) { + public void startEstimation(AdminPrincipal principal, @Payload boolean hostVoting) { LOGGER.debug("--> startEstimation()"); val session = ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()) @@ -145,6 +145,7 @@ public void startEstimation(AdminPrincipal principal) { .resetCurrentHighlights() .setTimerTimestamp(Utils.getTimestampISO8601(new Date())); databaseService.saveSession(session); + webSocketService.sendMembersHostVoting(session); webSocketService.sendSessionStateToMembers(session); webSocketService.sendTimerStartMessage(session, session.getTimerTimestamp()); LOGGER.debug("<-- startEstimation()"); @@ -159,11 +160,35 @@ public void votingFinished(AdminPrincipal principal) { .selectHighlightedMembers() .resetTimerTimestamp(); databaseService.saveSession(session); + if (session.getHostVoting()) { + webSocketService.sendMembersAdminVote(session); + } webSocketService.sendMembersUpdate(session); webSocketService.sendSessionStateToMembers(session); LOGGER.debug("<-- votingFinished()"); } + @MessageMapping("/hostVoting") + public void hostVotingChanged(AdminPrincipal principal, @Payload boolean stateOfHostVoting) { + LOGGER.debug("--> hostVotingChanged()"); + val session = ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()) + .setHostVoting(stateOfHostVoting); + databaseService.saveSession(session); + webSocketService.sendMembersHostVoting(session); + LOGGER.debug("<-- hostVotingChanged()"); + } + + @MessageMapping("/vote/admin") + public synchronized void processVoteAdmin(@Payload String vote, AdminPrincipal admin) { + LOGGER.debug("--> processVoteAdmin()"); + val session = + ControllerUtils.getSessionOrThrowResponse(databaseService, admin.getSessionID()) + .setHostEstimation(vote); + databaseService.saveSession(session); + LOGGER.debug("Ergebnis: " + session.getHostEstimation()); + LOGGER.debug("<-- processVoteAdmin()"); + } + @MessageMapping("/vote") public synchronized void processVote(@Payload String vote, MemberPrincipal member) { LOGGER.debug("--> processVote()"); @@ -173,7 +198,7 @@ public synchronized void processVote(@Payload String vote, MemberPrincipal membe webSocketService.sendMembersUpdate(session); databaseService.saveSession(session); - boolean votingCompleted = checkIfAllMembersVoted(session.getMembers()); + boolean votingCompleted = checkIfAllMembersVoted(session.getMembers(), session); if (votingCompleted) { votingFinished( new AdminPrincipal( @@ -183,8 +208,11 @@ public synchronized void processVote(@Payload String vote, MemberPrincipal membe LOGGER.debug("<-- processVote()"); } - private boolean checkIfAllMembersVoted(List members) { - return members.stream().filter(m -> m.getCurrentEstimation() == null).count() == 0; + private boolean checkIfAllMembersVoted(List members, Session session) { + if (session.getHostVoting() == false) { + return members.stream().filter(m -> m.getCurrentEstimation() == null).count() == 0; + } + return members.stream().filter(m -> m.getCurrentEstimation() == null).count() == 0 && session.getHostEstimation() != null; } @MessageMapping("/restart") diff --git a/backend/src/main/java/io/diveni/backend/model/Session.java b/backend/src/main/java/io/diveni/backend/model/Session.java index 188fce7a9..b861dda41 100644 --- a/backend/src/main/java/io/diveni/backend/model/Session.java +++ b/backend/src/main/java/io/diveni/backend/model/Session.java @@ -62,6 +62,10 @@ public class Session { private final String timerTimestamp; + private final boolean hostVoting; + + private final String hostEstimation; + static Comparator estimationByIndex(List set) { return Comparator.comparingInt((str) -> set.indexOf(str)); } @@ -89,7 +93,9 @@ public Session updateEstimation(String memberID, String vote) { sessionState, lastModified, accessToken, - timerTimestamp); + timerTimestamp, + hostVoting, + hostEstimation); } public Session selectHighlightedMembers() { @@ -120,7 +126,9 @@ public Session selectHighlightedMembers() { sessionState, lastModified, accessToken, - timerTimestamp); + timerTimestamp, + hostVoting, + hostEstimation); } val maxEstimationMembers = this.members.stream() @@ -193,7 +201,9 @@ public Session selectHighlightedMembers() { sessionState, lastModified, accessToken, - timerTimestamp); + timerTimestamp, + hostVoting, + hostEstimation); } public Session resetCurrentHighlights() { @@ -209,7 +219,9 @@ public Session resetCurrentHighlights() { sessionState, lastModified, accessToken, - timerTimestamp); + timerTimestamp, + hostVoting, + hostEstimation); } public Session updateUserStories(List userStories) { @@ -232,7 +244,9 @@ public Session updateUserStories(List userStories) { sessionState, lastModified, accessToken, - timerTimestamp); + timerTimestamp, + hostVoting, + hostEstimation); } public Session resetEstimations() { @@ -250,7 +264,9 @@ public Session resetEstimations() { sessionState, lastModified, accessToken, - timerTimestamp); + timerTimestamp, + hostVoting, + hostEstimation); } public Session updateMembers(List updatedMembers) { @@ -266,7 +282,9 @@ public Session updateMembers(List updatedMembers) { sessionState, lastModified, accessToken, - timerTimestamp); + timerTimestamp, + hostVoting, + hostEstimation); } public Session updateSessionState(SessionState updatedSessionState) { @@ -282,7 +300,9 @@ public Session updateSessionState(SessionState updatedSessionState) { updatedSessionState, lastModified, accessToken, - timerTimestamp); + timerTimestamp, + hostVoting, + hostEstimation); } public Session addMember(Member member) { @@ -300,7 +320,9 @@ public Session addMember(Member member) { sessionState, lastModified, accessToken, - timerTimestamp); + timerTimestamp, + hostVoting, + hostEstimation); } public Session removeMember(String memberID) { @@ -320,7 +342,9 @@ public Session removeMember(String memberID) { sessionState, lastModified, accessToken, - timerTimestamp); + timerTimestamp, + hostVoting, + hostEstimation); } public Session setTimerTimestamp(String timestamp) { @@ -336,7 +360,9 @@ public Session setTimerTimestamp(String timestamp) { sessionState, lastModified, accessToken, - timestamp); + timestamp, + hostVoting, + hostEstimation); } public Session resetTimerTimestamp() { @@ -352,7 +378,9 @@ public Session resetTimerTimestamp() { sessionState, lastModified, accessToken, - null); + null, + hostVoting, + hostEstimation); } public Session setLastModified(Date lastModified) { @@ -368,7 +396,9 @@ public Session setLastModified(Date lastModified) { sessionState, lastModified, accessToken, - timerTimestamp); + timerTimestamp, + hostVoting, + hostEstimation); } public Session setAccessToken(String token) { @@ -384,6 +414,49 @@ public Session setAccessToken(String token) { sessionState, lastModified, token, - timerTimestamp); + timerTimestamp, + hostVoting, + hostEstimation); + } + + public Session setHostVoting(boolean isHostVoting) { + return new Session( + databaseID, + sessionID, + adminID, + sessionConfig, + adminCookie, + members, + memberVoted, + currentHighlights, + sessionState, + lastModified, + accessToken, + timerTimestamp, + isHostVoting, + hostEstimation); + } + + public boolean getHostVoting() { + return hostVoting; + } + + public Session setHostEstimation(String vote) { + return new Session( + databaseID, + sessionID, + adminID, + sessionConfig, + adminCookie, + members, + memberVoted, + currentHighlights, + sessionState, + lastModified, + accessToken, + timerTimestamp, + hostVoting, + vote); } + } diff --git a/backend/src/main/java/io/diveni/backend/service/WebSocketService.java b/backend/src/main/java/io/diveni/backend/service/WebSocketService.java index 5cb1fe9e0..887f8cf3c 100644 --- a/backend/src/main/java/io/diveni/backend/service/WebSocketService.java +++ b/backend/src/main/java/io/diveni/backend/service/WebSocketService.java @@ -39,6 +39,10 @@ public class WebSocketService { public static String US_UPDATES_DESTINATION = "/updates/userStories"; + public static String MEMBER_UPDATES_HOSTVOTING = "/updates/hostVoting"; + + public static String ADMIN_UPDATED_ESTIMATION = "/updates/hostEstimation"; + public static String NOTIFICATIONS_DESTINATION = "/updates/notifications"; public static String START_TIMER_DESTINATION = "/updates/startTimer"; @@ -141,6 +145,21 @@ public synchronized void setAdminUser(AdminPrincipal principal) { LOGGER.debug("<-- setAdminUser()"); } + public void sendMembersHostVoting(Session session) { + LOGGER.debug("--> sendMembersHostVoting(), sessionID={}", session.getSessionID()); + getSessionPrincipals(session.getSessionID()) + .memberPrincipals() + .forEach(member -> sendUpdatedHostVotingToMember(session, member.getMemberID())); + LOGGER.debug("<-- sendMembersHostVoting()"); + } + + public void sendUpdatedHostVotingToMember(Session session, String memberID) { + LOGGER.debug("--> sendUpdatedHostVotingToMember(), sessionID={}, memberID={}", session.getSessionID(), memberID); + simpMessagingTemplate + .convertAndSendToUser(memberID, MEMBER_UPDATES_HOSTVOTING, session.getHostVoting()); + LOGGER.debug("<-- sendUpdatedHostVotingToMember()"); + } + public void sendMembersUpdate(Session session) { LOGGER.debug("--> sendMembersUpdate(), sessionID={}", session.getSessionID()); val sessionPrincipals = getSessionPrincipals(session.getSessionID()); @@ -167,6 +186,25 @@ public void sendMembersUpdateToMembers(Session session) { LOGGER.debug("<-- sendMembersUpdateToMembers()"); } + public void sendMembersAdminVote(Session session) { + LOGGER.debug("--> sendMembersAdminVote(), sessionID={}", session.getSessionID()); + getSessionPrincipals(session.getSessionID()) + .memberPrincipals() + .forEach( + member -> + simpMessagingTemplate.convertAndSendToUser( + member.getMemberID(), + ADMIN_UPDATED_ESTIMATION, + session.getHostEstimation() + ) + ); + LOGGER.debug("<-- sendMembersAdminVote()"); + } + + + + + public void sendSessionStateToMembers(Session session) { LOGGER.debug("--> sendSessionStateToMembers(), sessionID={}", session.getSessionID()); // TODO: Send highlighted with it diff --git a/backend/src/test/java/io/diveni/backend/controller/RoutesControllerTest.java b/backend/src/test/java/io/diveni/backend/controller/RoutesControllerTest.java index 969a81989..8e9775a66 100644 --- a/backend/src/test/java/io/diveni/backend/controller/RoutesControllerTest.java +++ b/backend/src/test/java/io/diveni/backend/controller/RoutesControllerTest.java @@ -105,6 +105,8 @@ public void joinMember_addsMemberToSession() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null)); // @formatter:off @@ -148,6 +150,8 @@ public void joinMember_addsMemberToProtectedSession() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null)); // @formatter:off @@ -194,6 +198,8 @@ public void joinMember_failsToAddMemberToProtectedSessionWrongPassword() throws SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null)); // @formatter:off var memberAsJson = @@ -240,6 +246,8 @@ public void joinMember_failsToAddMemberToProtectedSessionNullPassword() throws E SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null)); // @formatter:off @@ -284,6 +292,8 @@ public void joinMember_failsToAddMemberDueToFalseAvatarAnimal() throws Exception SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null)); // @formatter:off @@ -324,6 +334,8 @@ public void joinMember_failsToAddMemberDueToFalseAvatarAnimal2() throws Exceptio SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null)); // @formatter:off @@ -364,6 +376,8 @@ public void joinMember_failsToAddMemberDueToFalseEstimation() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null)); // @formatter:off @@ -431,6 +445,8 @@ public void joinMember_addsMemberNotIfAlreadyExisting() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null)); // @formatter:off @@ -480,6 +496,8 @@ public void getSession_returnsSession() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null)); this.mockMvc .perform(get("/sessions/{sessionID}", sessionUUID)) @@ -504,6 +522,8 @@ public void getSession_failsWrongID() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null)); this.mockMvc diff --git a/backend/src/test/java/io/diveni/backend/controller/WebsocketControllerTest.java b/backend/src/test/java/io/diveni/backend/controller/WebsocketControllerTest.java index 0890953ec..c8710c65e 100644 --- a/backend/src/test/java/io/diveni/backend/controller/WebsocketControllerTest.java +++ b/backend/src/test/java/io/diveni/backend/controller/WebsocketControllerTest.java @@ -6,6 +6,7 @@ package io.diveni.backend.controller; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.lang.reflect.Type; import java.util.ArrayList; @@ -77,6 +78,10 @@ public class WebsocketControllerTest { private static final String ADMIN_MEMBER_UPDATES = "/users/updates/membersUpdated"; + private static final String MEMBER_LISTEN_HOSTVOTING = "/users/updates/hostVoting"; + + private static final String ADMIN_SENDS_HOSTVOTING = "/ws/hostVoting"; + private static final ObjectMapper objectMapper = new ObjectMapper(); @Autowired SessionRepository sessionRepo; @@ -175,6 +180,8 @@ public void registerAdminPrincipal_isRegistered() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null)); val adminPrincipal = new AdminPrincipal(sessionID, adminID); StompSession session = getAdminSession(sessionID, adminID); @@ -211,6 +218,8 @@ public void registerMemberPrincipal_isRegistered() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null)); webSocketService.setAdminUser(adminPrincipal); val memberPrincipal = new MemberPrincipal(sessionID, memberID); @@ -255,6 +264,8 @@ public void registerMemberPrincipal_sendsMembersUpdates() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null)); webSocketService.setAdminUser(adminPrincipal); StompSession session = getMemberSession(sessionID, memberID); @@ -312,6 +323,8 @@ public void unregisterAdminPrincipal_isUnregistered() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null)); val adminPrincipal = new AdminPrincipal(sessionID, adminID); webSocketService.setAdminUser(adminPrincipal); @@ -344,6 +357,8 @@ public void unregisterMemberPrincipal_isUnregistered() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null)); val adminPrincipal = new AdminPrincipal(sessionID, adminID); val memberPrincipal = new MemberPrincipal(sessionID, memberID); @@ -383,6 +398,8 @@ public void vote_setsVote() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null)); webSocketService.setAdminUser(adminPrincipal); StompSession session = getMemberSession(sessionID, memberID); @@ -420,6 +437,8 @@ public void vote_sendsUpdate() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null)); webSocketService.setAdminUser(adminPrincipal); StompSession session = getMemberSession(sessionID, memberID); @@ -458,12 +477,14 @@ public void startVoting_updatesState() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null); sessionRepo.save(oldSession); webSocketService.setAdminUser(adminPrincipal); StompSession adminSession = getAdminSession(sessionID, adminID); - adminSession.send(START_VOTING, null); + adminSession.send(START_VOTING, false); // Wait for server-side handling TimeUnit.MILLISECONDS.sleep(TIMEOUT); @@ -493,6 +514,8 @@ public void restartVoting_resetsEstimations() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null); sessionRepo.save(oldSession); webSocketService.setAdminUser(adminPrincipal); @@ -505,4 +528,45 @@ public void restartVoting_resetsEstimations() throws Exception { val newMembers = sessionRepo.findBySessionID(oldSession.getSessionID()).getMembers(); Assertions.assertTrue(newMembers.stream().allMatch(m -> m.getCurrentEstimation() == null)); } + + @Test + public void hostVotingChanged_changesHostVoting() throws Exception { + val dbID = new ObjectId(); + val sessionID = Utils.generateRandomID(); + val adminID = Utils.generateRandomID(); + val memberID = Utils.generateRandomID(); + val memberList = + List.of( + new Member(memberID, null, null, null, null), + new Member(Utils.generateRandomID(), null, null, null, null)); + val adminPrincipal = new AdminPrincipal(sessionID, adminID); + sessionRepo.save( + new Session( + dbID, + sessionID, + adminID, + new SessionConfig(new ArrayList<>(), List.of(), 10, "US_MANUALLY", null), + null, + memberList, + new HashMap<>(), + List.of("asdf", "bsdf"), + SessionState.WAITING_FOR_MEMBERS, + null, + null, + null, + false, + null)); + webSocketService.setAdminUser(adminPrincipal); + StompSession session = getMemberSession(sessionID, memberID); + StompSession adminSession = getAdminSession(sessionID, adminID); + + session.subscribe(MEMBER_LISTEN_HOSTVOTING, stompFrameHandler); + adminSession.send(ADMIN_SENDS_HOSTVOTING, true); + + // Wait for server-side handling + TimeUnit.MILLISECONDS.sleep(TIMEOUT); + + Session result = sessionRepo.findBySessionID(sessionID); + assertTrue(result.getHostVoting()); + } } diff --git a/backend/src/test/java/io/diveni/backend/model/SessionTest.java b/backend/src/test/java/io/diveni/backend/model/SessionTest.java index 8773528d1..8d77bbfa5 100644 --- a/backend/src/test/java/io/diveni/backend/model/SessionTest.java +++ b/backend/src/test/java/io/diveni/backend/model/SessionTest.java @@ -43,6 +43,8 @@ public void equal_works() { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null); val sameSession = new Session( @@ -57,6 +59,8 @@ public void equal_works() { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null); val otherSession = new Session( @@ -71,6 +75,8 @@ public void equal_works() { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null); assertEquals(session, sameSession); @@ -100,6 +106,8 @@ public void updateEstimation_works() { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null); val result = session.updateEstimation(member1.getMemberID(), vote); @@ -132,6 +140,8 @@ public void resetEstimations_works() { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null); val result = session.resetEstimations(); @@ -157,6 +167,8 @@ public void updateSessionState_works() { oldSessionState, null, null, + null, + false, null); val result = session.updateSessionState(newSessionState); @@ -178,6 +190,8 @@ public void setLastModified_works() { null, null, null, + null, + false, null); val date = new Date(); @@ -204,6 +218,8 @@ public void addMember_works() { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null); val memberID2 = Utils.generateRandomID(); val member2 = new Member(memberID2, null, null, null, "5"); @@ -234,6 +250,8 @@ public void removeMember_works() { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null); val result = session.removeMember(memberID1); @@ -261,6 +279,8 @@ public void selectHighlightedMembers_works() { null, null, null, + null, + false, null); val result = session.selectHighlightedMembers(); @@ -292,6 +312,8 @@ public void selectHighlightedMembers_correctOrder() { null, null, null, + null, + false, null); val result = session.selectHighlightedMembers(); @@ -325,6 +347,8 @@ public void selectHighlightedMembersPrioritized_works() { null, null, null, + null, + false, null); val result = session.selectHighlightedMembers(); @@ -351,6 +375,8 @@ public void resetHighlightedMembers_works() { null, null, null, + null, + false, null); val result = session.resetCurrentHighlights(); @@ -373,6 +399,8 @@ public void setTimerTimestamp_works() { null, null, null, + null, + false, null); val timestamp = Utils.getTimestampISO8601(new Date()); @@ -396,10 +424,59 @@ public void resetTimerTimestamp_works() { null, null, null, - Utils.getTimestampISO8601(new Date())); + Utils.getTimestampISO8601(new Date()), + false, + null); val result = session.resetTimerTimestamp(); assertNull(result.getTimerTimestamp()); } + + @Test + public void setHostVoting_works() { + val session = + new Session( + null, + null, + null, + null, + null, + new ArrayList<>(), + new HashMap<>(), + new ArrayList<>(), + null, + null, + null, + Utils.getTimestampISO8601(new Date()), + false, + null); + + Session result = session.setHostVoting(true); + + assertTrue(result.getHostVoting()); + } + + public void setHostEstimation_works() { + val session = + new Session( + null, + null, + null, + null, + null, + new ArrayList<>(), + new HashMap<>(), + new ArrayList<>(), + null, + null, + null, + Utils.getTimestampISO8601(new Date()), + false, + null); + + val result = session.setHostEstimation("10"); + + assertEquals("10", result.getHostEstimation()); + } } diff --git a/backend/src/test/java/io/diveni/backend/repository/SessionRepositoryTest.java b/backend/src/test/java/io/diveni/backend/repository/SessionRepositoryTest.java index 9df8c678f..415539985 100644 --- a/backend/src/test/java/io/diveni/backend/repository/SessionRepositoryTest.java +++ b/backend/src/test/java/io/diveni/backend/repository/SessionRepositoryTest.java @@ -47,6 +47,8 @@ public void saveSession_returnsSession() { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null); assertEquals(session, sessionRepo.save(session)); @@ -72,6 +74,8 @@ public void addMemberToSession_addsMember() { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null); assertEquals(session, sessionRepo.save(session)); diff --git a/backend/src/test/java/io/diveni/backend/service/WebSocketServiceTest.java b/backend/src/test/java/io/diveni/backend/service/WebSocketServiceTest.java index 36303d725..7fb370119 100644 --- a/backend/src/test/java/io/diveni/backend/service/WebSocketServiceTest.java +++ b/backend/src/test/java/io/diveni/backend/service/WebSocketServiceTest.java @@ -169,6 +169,8 @@ public void sendMembersUpdate_sendsUpdate() throws Exception { null, null, null, + null, + false, null); webSocketService.sendMembersUpdate(session); @@ -196,6 +198,8 @@ public void sendSessionState_sendsState() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null); webSocketService.sendSessionStateToMember(session, defaultMemberPrincipal.getMemberID()); @@ -227,6 +231,8 @@ public void sendSessionStates_sendsToAll() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null); webSocketService.sendSessionStateToMembers(session); @@ -263,6 +269,8 @@ public void sendUpdatedUserStoriesToMembers_sendsToAll() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null); webSocketService.sendUpdatedUserStoriesToMembers(session); @@ -299,6 +307,8 @@ public void adminLeft_sendsNotification() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null); val notification = new Notification(NotificationType.ADMIN_LEFT, null); @@ -327,10 +337,42 @@ public void removeSession_isRemoved() throws Exception { SessionState.WAITING_FOR_MEMBERS, null, null, + null, + false, null); webSocketService.removeSession(session); assertTrue(webSocketService.getSessionPrincipalList().isEmpty()); } + + @Test + public void sendMembersHostVoting_sendsHostVotingUpdate() throws Exception { + setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal)); + val session = + new Session( + new ObjectId(), + defaultAdminPrincipal.getSessionID(), + defaultAdminPrincipal.getAdminID(), + null, + null, + List.of(new Member(defaultMemberPrincipal.getMemberID(), null, null, null, null)), + new HashMap<>(), + new ArrayList<>(), + null, + null, + null, + null, + false, + null); + + webSocketService.sendMembersHostVoting(session); + + verify(simpMessagingTemplateMock, times(1)) + .convertAndSendToUser( + defaultMemberPrincipal.getMemberID(), + WebSocketService.MEMBER_UPDATES_HOSTVOTING, + session.getHostVoting()); + } + } diff --git a/frontend/src/components/SessionAdminCard.vue b/frontend/src/components/SessionAdminCard.vue new file mode 100644 index 000000000..9d3624223 --- /dev/null +++ b/frontend/src/components/SessionAdminCard.vue @@ -0,0 +1,82 @@ + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/actions/SessionStartButton.vue b/frontend/src/components/actions/SessionStartButton.vue index 52bd132af..0e77ce04e 100644 --- a/frontend/src/components/actions/SessionStartButton.vue +++ b/frontend/src/components/actions/SessionStartButton.vue @@ -11,20 +11,25 @@ + \ No newline at end of file diff --git a/frontend/src/constants.ts b/frontend/src/constants.ts index 4c072372f..446a11d3e 100644 --- a/frontend/src/constants.ts +++ b/frontend/src/constants.ts @@ -21,6 +21,10 @@ class Constants { webSocketCloseSessionRoute = "/ws/closeSession"; + webSocketHostVotingRoute = "/ws/hostVoting"; + + webSocketMemberListenHostVotingRoute = "/users/updates/hostVoting"; + webSocketGetMemberUpdateRoute = "/ws/memberUpdate"; webSocketMembersUpdatedRoute = "/users/updates/membersUpdated"; @@ -33,6 +37,10 @@ class Constants { webSocketVoteRoute = "/ws/vote"; + webSocketVoteRouteAdmin = "/ws/vote/admin"; + + webSocketMembersUpdatedHostEstimation = "/users/updates/hostEstimation"; + webSocketAdminUpdatedUserStoriesRoute = "/ws/adminUpdatedUserStories"; webSocketMemberListenUserStoriesRoute = "/users/updates/userStories"; diff --git a/frontend/src/store/index.ts b/frontend/src/store/index.ts index 16d47442d..ae49e070b 100644 --- a/frontend/src/store/index.ts +++ b/frontend/src/store/index.ts @@ -19,6 +19,8 @@ export default new Vuex.Store({ tokenId: undefined, projects: [], selectedProject: undefined, + hostVoting: false, + hostEstimation: undefined, }, mutations: { setMembers(state, members) { @@ -58,6 +60,16 @@ export default new Vuex.Store({ state.highlightedMembers = JSON.parse(frame.body).highlightedMembers; }); }, + subscribeOnBackendWSHostVoting(state) { + state.stompClient?.subscribe(Constants.webSocketMemberListenHostVotingRoute, (frame) => { + state.hostVoting = JSON.parse(frame.body); + }) + }, + subscribeOnBackendWSHostEstimation(state) { + state.stompClient?.subscribe(Constants.webSocketMembersUpdatedHostEstimation, (frame) => { + state.hostEstimation = JSON.parse(frame.body); + }) + }, subscribeOnBackendWSTimerStart(state) { state.stompClient?.subscribe(Constants.webSocketTimerStartRoute, (frame) => { console.log(`Got timer start ${frame.body}`); diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 0f1981f12..6eba71d77 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -13,6 +13,8 @@ export interface StoreState { tokenId: string | undefined; projects: Record[]; selectedProject: Project | undefined; + hostVoting: boolean; + hostEstimation: string | undefined; } export interface JiraRequestTokenDto { diff --git a/frontend/src/views/JoinPage.vue b/frontend/src/views/JoinPage.vue index 4b510884d..9e11c1d96 100644 --- a/frontend/src/views/JoinPage.vue +++ b/frontend/src/views/JoinPage.vue @@ -52,6 +52,8 @@ export default Vue.extend({ this.subscribeWSMemberUpdated(); this.subscribeOnTimerStart(); this.subscribeWSNotification(); + this.subscribeWSMemberHostVotingUpdate(); + this.subscribeWSMemberHostEstimation(); this.goToEstimationPage(); } }, @@ -109,6 +111,12 @@ export default Vue.extend({ const endPoint = Constants.webSocketRegisterMemberRoute; this.$store.commit("sendViaBackendWS", { endPoint }); }, + subscribeWSMemberHostVotingUpdate() { + this.$store.commit("subscribeOnBackendWSHostVoting"); + }, + subscribeWSMemberHostEstimation() { + this.$store.commit("subscribeOnBackendWSHostEstimation"); + }, subscribeWSMemberUpdates() { this.$store.commit("subscribeOnBackendWSMemberUpdates"); }, diff --git a/frontend/src/views/MemberVotePage.vue b/frontend/src/views/MemberVotePage.vue index 4f5b137bd..bf2123894 100644 --- a/frontend/src/views/MemberVotePage.vue +++ b/frontend/src/views/MemberVotePage.vue @@ -61,6 +61,7 @@ :disabled="pauseSession" @sentVote="onSendVote" /> + @@ -105,6 +106,9 @@ highlightedMembers.includes(member.memberID) || highlightedMembers.length === 0, }" /> + @@ -165,6 +169,7 @@ import MobileStoryList from "../components/MobileStoryList.vue"; import MobileStoryTitle from "../components/MobileStoryTitle.vue"; import UserStorySumComponent from "@/components/UserStorySum.vue"; import SessionLeaveButton from "@/components/actions/SessionLeaveButton.vue"; +import SessionAdminCard from "@/components/SessionAdminCard.vue"; export default Vue.extend({ name: "MemberVotePage", @@ -174,6 +179,7 @@ export default Vue.extend({ MemberVoteCard, EstimateTimer, SessionMemberCard, + SessionAdminCard, NotifyMemberComponent, UserStories, UserStoryDescriptions, @@ -234,6 +240,12 @@ export default Vue.extend({ notifications() { return this.$store.state.notifications; }, + hostVoting() { + return this.$store.state.hostVoting; + }, + hostEstimation() { + return this.$store.state.hostEstimation; + }, getMember() { return { hexColor: this.hexColor, diff --git a/frontend/src/views/SessionPage.vue b/frontend/src/views/SessionPage.vue index ab95cc703..f27accc92 100644 --- a/frontend/src/views/SessionPage.vue +++ b/frontend/src/views/SessionPage.vue @@ -85,7 +85,13 @@
+ {{ (estimateFinished = true) }} +
+
{{ (estimateFinished = true) }} @@ -107,11 +113,23 @@ />
-

+

{{ $t("page.session.during.estimation.message.finished") }} {{ membersEstimated.length }} / {{ membersPending.length + membersEstimated.length }}

+

+
+ {{ $t("page.session.during.estimation.message.finished") }} + {{ membersEstimated.length }} / + {{ membersPending.length + membersEstimated.length + 1}} +
+
+ {{ $t("page.session.during.estimation.message.finished") }} + {{ membersEstimated.length + 1}} / + {{ membersPending.length + membersEstimated.length + 1}} +
+

+ +
+
+
+

+ Your Estimation +

+
+
+ + {{ item }} + +
+ + +
@@ -137,7 +181,7 @@
- +
@@ -540,5 +597,8 @@ export default Vue.extend({ padding-left: 10px; margin-top: 2%; } +.newVotes { + text-align: center; +} From 6d1fb9bc65602944fe7086752cc76426c1ec8211 Mon Sep 17 00:00:00 2001 From: Till Wanner Date: Thu, 9 Mar 2023 11:26:09 +0100 Subject: [PATCH 03/30] styled the session admin card, fixxed a voting Problem, restart is now possible --- .../backend/controller/WebsocketController.java | 12 +++++++++--- .../java/io/diveni/backend/model/Session.java | 2 +- .../diveni/backend/service/WebSocketService.java | 4 ---- .../io/diveni/backend/model/SessionTest.java | 3 ++- frontend/src/assets/crown.png | Bin 0 -> 9989 bytes frontend/src/components/SessionAdminCard.vue | 13 +++++-------- .../components/actions/SessionStartButton.vue | 3 +-- frontend/src/views/MemberVotePage.vue | 2 +- frontend/src/views/SessionPage.vue | 4 ++-- 9 files changed, 21 insertions(+), 22 deletions(-) create mode 100644 frontend/src/assets/crown.png diff --git a/backend/src/main/java/io/diveni/backend/controller/WebsocketController.java b/backend/src/main/java/io/diveni/backend/controller/WebsocketController.java index 7e54ebafe..dffd2dae7 100644 --- a/backend/src/main/java/io/diveni/backend/controller/WebsocketController.java +++ b/backend/src/main/java/io/diveni/backend/controller/WebsocketController.java @@ -137,7 +137,7 @@ public void getMemberUpdate(AdminPrincipal principal) { } @MessageMapping("/startVoting") - public void startEstimation(AdminPrincipal principal, @Payload boolean hostVoting) { + public void startEstimation(AdminPrincipal principal) { LOGGER.debug("--> startEstimation()"); val session = ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()) @@ -184,8 +184,14 @@ public synchronized void processVoteAdmin(@Payload String vote, AdminPrincipal a val session = ControllerUtils.getSessionOrThrowResponse(databaseService, admin.getSessionID()) .setHostEstimation(vote); - databaseService.saveSession(session); - LOGGER.debug("Ergebnis: " + session.getHostEstimation()); + //webSocketService.sendMembersUpdate(session); + databaseService.saveSession(session); + if (checkIfAllMembersVoted(session.getMembers(), session)) { + votingFinished( + new AdminPrincipal( + admin.getSessionID(), + admin.getAdminID())); //databaseService.getSessionByID(admin.getSessionID()).get().getAdminID() + } LOGGER.debug("<-- processVoteAdmin()"); } diff --git a/backend/src/main/java/io/diveni/backend/model/Session.java b/backend/src/main/java/io/diveni/backend/model/Session.java index b861dda41..71ae88b28 100644 --- a/backend/src/main/java/io/diveni/backend/model/Session.java +++ b/backend/src/main/java/io/diveni/backend/model/Session.java @@ -266,7 +266,7 @@ public Session resetEstimations() { accessToken, timerTimestamp, hostVoting, - hostEstimation); + null); } public Session updateMembers(List updatedMembers) { diff --git a/backend/src/main/java/io/diveni/backend/service/WebSocketService.java b/backend/src/main/java/io/diveni/backend/service/WebSocketService.java index 887f8cf3c..d1d566e26 100644 --- a/backend/src/main/java/io/diveni/backend/service/WebSocketService.java +++ b/backend/src/main/java/io/diveni/backend/service/WebSocketService.java @@ -201,10 +201,6 @@ public void sendMembersAdminVote(Session session) { LOGGER.debug("<-- sendMembersAdminVote()"); } - - - - public void sendSessionStateToMembers(Session session) { LOGGER.debug("--> sendSessionStateToMembers(), sessionID={}", session.getSessionID()); // TODO: Send highlighted with it diff --git a/backend/src/test/java/io/diveni/backend/model/SessionTest.java b/backend/src/test/java/io/diveni/backend/model/SessionTest.java index 8d77bbfa5..d8af79545 100644 --- a/backend/src/test/java/io/diveni/backend/model/SessionTest.java +++ b/backend/src/test/java/io/diveni/backend/model/SessionTest.java @@ -142,11 +142,12 @@ public void resetEstimations_works() { null, null, false, - null); + "10"); val result = session.resetEstimations(); assertTrue(result.getMembers().stream().allMatch(m -> m.getCurrentEstimation() == null)); + assertTrue(result.getHostEstimation() == null); } @Test diff --git a/frontend/src/assets/crown.png b/frontend/src/assets/crown.png new file mode 100644 index 0000000000000000000000000000000000000000..d60f0b0752709c8c9b5c64d1545b2a83966838e5 GIT binary patch literal 9989 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�C;YA zib9qYrv#^`n$r%Ub0E3;f?gRBwSr{-b-4L_lijNa`YSY;J;rv;>0sec{X(BC<}D|m zd$VIASW!nU@i13_R7M?fh&jg)j?A$bsq8Rji#P?Y^sULN3@cd!{L_yLnK4m)+i^0uxNdOeOFCDR%V-$-ooh%PmV!TW%rHvyQ^T- z=!MzlQE~&nzUHi`hlN7_%K3NgZtSN{Fo`CPJpRHQnSuvKZRX)dlC;z4xUV&lF~3X( zmhPh4=-Ddc?EcrTg3U!F#L)DMA1nj;U>?7S3!_8EqG5!bhUyD`RS{^vmyhcJ zW0j5{hV8`}%{@t6^{nfkf)HX-Vbt_rafpcE`<_*(C0XYehAZL{V@*^< zn_KFlFCVi5AQoZ=!vqfR&zZI2BU|n^tjDN|M zk(}E)x9y;kTnSrM>-dkMItw8$+~=68c(cyR@lUoH7zE>W9P9~L_-SR5`efJcYZ@16 zt~e*xihsu7-*;0Azl-wW7RHdS!oc(Ax*)<(o|8AP?y8bpkp=51VqWSltq$+1 zA*!iW2z`h#Hr4i>NN4UjDHay~=YL#){js2CTk1C`ig1IZj~WasXF6v-qXRQisWe`Q zcd!C3*ic(_d;X`lA|wn!0mhZ2BEp`dpFMD}sz}Hr)x_UWb|q?Gy1cAu!R{L&{QppQ z+pT{Hzk*U%5*`s3JDdUo^P_~l|1Sf8)n+ghHv=}U_jd+Sd zSP6)$Gu96aey$190J@DfOw?|Rb;Xa0d+V%tN-?|?wpfc)7z%ct@E)Ymw-Dz=0#^G{ z_>*|_T;G6P#06yK34qrQs*)@opSZ|@F*sp~q&Pzx&RyPYSMx7>9tfY7MA=Ap-F^EflZPCOR^&;p zyRvVdo7#S@*j}r6;@H%G=C7dVPX2v3CQH<~6Ns%1&8vHN#(STOkSH5)kWf)?!$CUH zoVOEcsRfY&SE7nDx_57sJsj@8)}hHLFGhs2$)0lmHPRYKM@~;nnc|g#twVl(l*4Pm zQ)W}cK08`(XTCBK-FvSu>p{HXcbqxZvi8HlY56lS39C%Sc~B|AuZ*)T1y3;lyV1wc z4?jC{ri?h&qCZvJwazK6v^J^8dBpl>{10$K+Q}v|EzO^iH_0BhPh2DY&j&@R*itUW$%}xY=C18-(NT`96-m^#H>6`dB z6gN8Uokm`h9qn&qpybLul2=>r>S~4is~R@iifVs1NF_J&PvGs&{W0XZzopu!>sja@>J9QhsA9M)Yb2#M`tSm~H{w);6-la_ zF9j2QmT`s(wNEu@E$U)PUeA8JCnSxMphbBo=@SgKdB-zE>b!gpV({Nj&`8BgU^c@p+6vP6klkwXEG&0f$)i=c#}ovY z?By-PG=AO}c*|#$9IM@Zhtvy3j>=<{=Ic(l37{XtK*=VwKMn#G?IvD~=uRCO5>5R- zg##>9EC~Wh@hw`X9=nGe&Ii^56JA;|NFkNt864bv1>OT5Y~A)C_UnDOk@~;>t`C~i zD81x}w{3Fu8L3l{il01qbEmf3SYB(agYVdU-PPb{HHbqG3ZD$xA|4btnBBFcrI5WE z>m9zq6+mFRCKK>EBa^_)f_C!e^1fiX1M1O+Y5h{L_(fy+rSUIWNSFA9WF>` zt*|cNbA1l7Wwcc5EJRlg=}$g1@K(tqK(YdutP0Y^FZgF7`;+jTlpvrkB1HkH%JVx4NFs1 LPo-MPCiMRR!W;7E literal 0 HcmV?d00001 diff --git a/frontend/src/components/SessionAdminCard.vue b/frontend/src/components/SessionAdminCard.vue index 9d3624223..5b61d91c7 100644 --- a/frontend/src/components/SessionAdminCard.vue +++ b/frontend/src/components/SessionAdminCard.vue @@ -1,6 +1,6 @@ @@ -75,5 +77,10 @@ font-weight: 400; text-overflow: ellipsis; } + +.greyOut { + opacity: 0.5; + transform: scale(0.8); +} \ No newline at end of file diff --git a/frontend/src/views/MemberVotePage.vue b/frontend/src/views/MemberVotePage.vue index 5462ceca8..b898095ee 100644 --- a/frontend/src/views/MemberVotePage.vue +++ b/frontend/src/views/MemberVotePage.vue @@ -107,9 +107,10 @@ }" />
- + :estimateFinished="votingFinished" + :highlight="isAdminHighlighted()"/>
@@ -311,6 +312,27 @@ export default Vue.extend({ this.sendUnregisterCommand(); }, methods: { + isAdminHighlighted() { + if (this.highlightedMembers.length === 0) { + return true; + } + let highlightedMap = new Map(); + this.highlightedMembers.forEach(highlightedMemberID => { + let isMemberID = false; + this.members.forEach(member => { + if (member.memberID === highlightedMemberID) { + isMemberID = true; + } + }) + highlightedMap.set(highlightedMemberID, isMemberID); + }); + for (let value of highlightedMap.values()) { + if (value === false) { + return true; + } + } + return false; + }, onSelectedStory($event) { this.index = $event; }, diff --git a/frontend/src/views/SessionPage.vue b/frontend/src/views/SessionPage.vue index b841e6d1a..97e532665 100644 --- a/frontend/src/views/SessionPage.vue +++ b/frontend/src/views/SessionPage.vue @@ -147,6 +147,7 @@
From 94f6c8e2621a3ae77cbc125d81596ce5bd3831d1 Mon Sep 17 00:00:00 2001 From: Till Wanner Date: Mon, 20 Mar 2023 15:25:49 +0100 Subject: [PATCH 06/30] fixxed the problem where the admin vote didnt reset --- .../controller/WebsocketController.java | 2 +- .../io/diveni/backend/model/AdminVote.java | 16 +++++++ .../java/io/diveni/backend/model/Session.java | 24 +++++----- .../io/diveni/backend/model/SessionTest.java | 4 +- frontend/src/components/SessionAdminCard.vue | 2 +- frontend/src/model/AdminVote.ts | 5 +++ frontend/src/views/MemberVotePage.vue | 44 ++++++++++++++----- frontend/src/views/SessionPage.vue | 36 ++++++++++++--- 8 files changed, 103 insertions(+), 30 deletions(-) create mode 100644 backend/src/main/java/io/diveni/backend/model/AdminVote.java create mode 100644 frontend/src/model/AdminVote.ts diff --git a/backend/src/main/java/io/diveni/backend/controller/WebsocketController.java b/backend/src/main/java/io/diveni/backend/controller/WebsocketController.java index cde4e69dc..cba68f45b 100644 --- a/backend/src/main/java/io/diveni/backend/controller/WebsocketController.java +++ b/backend/src/main/java/io/diveni/backend/controller/WebsocketController.java @@ -218,7 +218,7 @@ private boolean checkIfAllMembersVoted(List members, Session session) { if (session.getHostVoting() == false) { return members.stream().filter(m -> m.getCurrentEstimation() == null).count() == 0; } - return members.stream().filter(m -> m.getCurrentEstimation() == null).count() == 0 && !session.getHostEstimation().equals(""); + return members.stream().filter(m -> m.getCurrentEstimation() == null).count() == 0 && null != session.getHostEstimation() && !"".equals(session.getHostEstimation().getHostEstimation()); } @MessageMapping("/restart") diff --git a/backend/src/main/java/io/diveni/backend/model/AdminVote.java b/backend/src/main/java/io/diveni/backend/model/AdminVote.java new file mode 100644 index 000000000..5b5538520 --- /dev/null +++ b/backend/src/main/java/io/diveni/backend/model/AdminVote.java @@ -0,0 +1,16 @@ +package io.diveni.backend.model; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@EqualsAndHashCode +@AllArgsConstructor +public class AdminVote { + + private String hostEstimation; + +} diff --git a/backend/src/main/java/io/diveni/backend/model/Session.java b/backend/src/main/java/io/diveni/backend/model/Session.java index 3a2b7e8de..7a48b57f5 100644 --- a/backend/src/main/java/io/diveni/backend/model/Session.java +++ b/backend/src/main/java/io/diveni/backend/model/Session.java @@ -66,7 +66,7 @@ public class Session { private final boolean hostVoting; - private final String hostEstimation; + private final AdminVote hostEstimation; static Comparator estimationByIndex(List set) { return Comparator.comparingInt((str) -> set.indexOf(str)); @@ -116,7 +116,7 @@ public Session selectHighlightedMembers() { maxEstimation = getFilteredEstimationStream(this.members).max(estimationByIndex(filteredSet)); } else { Stream filteredEstimationsMember = getFilteredEstimationStream(this.members); - Stream allEstimations = Stream.concat(filteredEstimationsMember, Stream.of(this.hostEstimation)); + Stream allEstimations = Stream.concat(filteredEstimationsMember, Stream.of(this.hostEstimation.getHostEstimation())); maxEstimation = allEstimations.max(estimationByIndex(filteredSet)); } if (!this.hostVoting || this.hostEstimation.equals("")) { @@ -124,7 +124,7 @@ public Session selectHighlightedMembers() { getFilteredEstimationStream(this.members).min(estimationByIndex(filteredSet)); } else { Stream filteredEstimationsMember = getFilteredEstimationStream(this.members); - Stream allEstimations = Stream.concat(filteredEstimationsMember, Stream.of(this.hostEstimation)); + Stream allEstimations = Stream.concat(filteredEstimationsMember, Stream.of(this.hostEstimation.getHostEstimation())); minEstimation = allEstimations.min(estimationByIndex(filteredSet)); } @@ -161,8 +161,10 @@ public Session selectHighlightedMembers() { .anyMatch((member) -> member.getMemberID().equals(entry.getKey()))) .collect(Collectors.toList()); - if (maxEstimation.get().equals(this.hostEstimation)) { - maxOptions.add(new AbstractMap.SimpleEntry(this.adminID, 0)); + if (hostVoting == true) { + if (maxEstimation.get().equals(this.hostEstimation.getHostEstimation())) { + maxOptions.add(new AbstractMap.SimpleEntry(this.adminID, 0)); + } } val maxMemberID = @@ -184,8 +186,10 @@ public Session selectHighlightedMembers() { .anyMatch((member) -> member.getMemberID().equals(entry.getKey()))) .collect(Collectors.toList()); - if (minEstimation.get().equals(this.hostEstimation)) { - minOptions.add(new AbstractMap.SimpleEntry(this.adminID, 0)); + if (hostVoting == true) { + if (minEstimation.get().equals(this.hostEstimation.getHostEstimation())) { + minOptions.add(new AbstractMap.SimpleEntry(this.adminID, 0)); + } } val minMemberID = @@ -293,7 +297,7 @@ public Session resetEstimations() { accessToken, timerTimestamp, hostVoting, - ""); + new AdminVote("")); } public Session updateMembers(List updatedMembers) { @@ -483,7 +487,7 @@ public Session setHostEstimation(String vote) { accessToken, timerTimestamp, hostVoting, - vote); + new AdminVote(vote)); } - + } diff --git a/backend/src/test/java/io/diveni/backend/model/SessionTest.java b/backend/src/test/java/io/diveni/backend/model/SessionTest.java index 4076183d1..d50ac2edb 100644 --- a/backend/src/test/java/io/diveni/backend/model/SessionTest.java +++ b/backend/src/test/java/io/diveni/backend/model/SessionTest.java @@ -142,12 +142,12 @@ public void resetEstimations_works() { null, null, false, - "10"); + new AdminVote("10")); val result = session.resetEstimations(); assertTrue(result.getMembers().stream().allMatch(m -> m.getCurrentEstimation() == null)); - assertTrue(result.getHostEstimation() == ""); + assertTrue(result.getHostEstimation().getHostEstimation() == ""); } @Test diff --git a/frontend/src/components/SessionAdminCard.vue b/frontend/src/components/SessionAdminCard.vue index 172bb2ae9..fb10df465 100644 --- a/frontend/src/components/SessionAdminCard.vue +++ b/frontend/src/components/SessionAdminCard.vue @@ -6,7 +6,7 @@ >

? - - + - {{ currentEstimation }} diff --git a/frontend/src/model/AdminVote.ts b/frontend/src/model/AdminVote.ts new file mode 100644 index 000000000..07c15e8ae --- /dev/null +++ b/frontend/src/model/AdminVote.ts @@ -0,0 +1,5 @@ +interface AdminVote { + hostEstimation: string; + } + + export default AdminVote; \ No newline at end of file diff --git a/frontend/src/views/MemberVotePage.vue b/frontend/src/views/MemberVotePage.vue index b898095ee..5ad636842 100644 --- a/frontend/src/views/MemberVotePage.vue +++ b/frontend/src/views/MemberVotePage.vue @@ -92,10 +92,16 @@

+
+ +
-
- -
-
+ + + +
+ +
+
@@ -208,7 +230,7 @@ export default Vue.extend({ triggerTimer: 0, estimateFinished: false, pauseSession: false, - safedHostEstimation: "", + safedHostEstimation: null, }; }, computed: { @@ -263,7 +285,7 @@ export default Vue.extend({ if (updates.at(-1) === Constants.memberUpdateCommandStartVoting) { this.draggedVote = null; this.estimateFinished = false; - this.safedHostEstimation = ""; + this.safedHostEstimation = null; this.triggerTimer = (this.triggerTimer + 1) % 5; } else if (updates.at(-1) === Constants.memberUpdateCommandVotingFinished) { this.estimateFinished = true; @@ -280,7 +302,7 @@ export default Vue.extend({ }); } if (this.hostVoting) { - this.safedHostEstimation = this.hostEstimation; + this.safedHostEstimation = this.hostEstimation.hostEstimation; } }, notifications(notifications) { diff --git a/frontend/src/views/SessionPage.vue b/frontend/src/views/SessionPage.vue index 97e532665..9548e5322 100644 --- a/frontend/src/views/SessionPage.vue +++ b/frontend/src/views/SessionPage.vue @@ -132,7 +132,13 @@ + - + + + -
+

@@ -284,6 +307,7 @@ export default Vue.extend({ session: {}, hostVoting: false, hostEstimation: "", + safedHostVoting: false, }; }, computed: { @@ -563,6 +587,7 @@ export default Vue.extend({ sendRestartMessage() { this.estimateFinished = false; this.hostEstimation = ''; + this.safedHostVoting = this.hostVoting; const endPoint = Constants.webSocketRestartPlanningRoute; this.$store.commit("sendViaBackendWS", { endPoint }); }, @@ -571,6 +596,7 @@ export default Vue.extend({ }, onPlanningStarted() { this.planningStart = true; + this.safedHostVoting = this.hostVoting; }, vote(vote: string) { const endPoint = `${Constants.webSocketVoteRouteAdmin}`; From 27ae44f50db9379f5ca189c6c800d8e42f955ff9 Mon Sep 17 00:00:00 2001 From: SponsoredByPuma <92574150+SponsoredByPuma@users.noreply.github.com> Date: Fri, 24 Mar 2023 17:24:05 +0100 Subject: [PATCH 07/30] Issue now works as inteded only Tests missing --- .../controller/WebsocketController.java | 113 +++++----- .../java/io/diveni/backend/model/Session.java | 161 ++++++-------- .../components/actions/SessionStartButton.vue | 13 +- frontend/src/constants.ts | 4 +- frontend/src/views/MemberVotePage.vue | 175 +++++---------- frontend/src/views/SessionPage.vue | 209 +++++++----------- 6 files changed, 264 insertions(+), 411 deletions(-) diff --git a/backend/src/main/java/io/diveni/backend/controller/WebsocketController.java b/backend/src/main/java/io/diveni/backend/controller/WebsocketController.java index cba68f45b..9e798b7ad 100644 --- a/backend/src/main/java/io/diveni/backend/controller/WebsocketController.java +++ b/backend/src/main/java/io/diveni/backend/controller/WebsocketController.java @@ -34,14 +34,15 @@ public class WebsocketController { private static final Logger LOGGER = LoggerFactory.getLogger(WebsocketController.class); - @Autowired DatabaseService databaseService; - @Autowired private WebSocketService webSocketService; + @Autowired + DatabaseService databaseService; + @Autowired + private WebSocketService webSocketService; @MessageMapping("/registerAdminUser") public void registerAdminUser(AdminPrincipal principal) { LOGGER.debug("--> registerAdminUser()"); - Session session = - ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()); + Session session = ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()); webSocketService.setAdminUser(principal); if (session.getTimerTimestamp() != null) { session = session.setTimerTimestamp(Utils.getTimestampISO8601(new Date())); @@ -60,8 +61,7 @@ public void registerAdminUser(AdminPrincipal principal) { @MessageMapping("/registerMember") public void joinMember(MemberPrincipal principal) { LOGGER.debug("--> joinMember()"); - val session = - ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()); + val session = ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()); webSocketService.addMemberIfNew(principal); webSocketService.sendMembersUpdate(session); webSocketService.sendSessionStateToMember(session, principal.getName()); @@ -69,6 +69,10 @@ public void joinMember(MemberPrincipal principal) { webSocketService.sendTimerStartMessageToUser( session, session.getTimerTimestamp(), principal.getMemberID()); } + webSocketService.sendUpdatedHostVotingToMember(session, principal.getMemberID()); + if (session.getHostVoting() && session.getSessionState().equals(SessionState.VOTING_FINISHED)) { + webSocketService.sendMembersAdminVote(session); + } webSocketService.sendNotification( session, new Notification( @@ -81,10 +85,9 @@ public void removeMember(Principal principal) { LOGGER.debug("--> removeMember()"); if (principal instanceof MemberPrincipal) { webSocketService.removeMember((MemberPrincipal) principal); - val session = - ControllerUtils.getSessionByMemberIDOrThrowResponse( - databaseService, ((MemberPrincipal) principal).getMemberID()) - .removeMember(((MemberPrincipal) principal).getMemberID()); + val session = ControllerUtils.getSessionByMemberIDOrThrowResponse( + databaseService, ((MemberPrincipal) principal).getMemberID()) + .removeMember(((MemberPrincipal) principal).getMemberID()); databaseService.saveSession(session); webSocketService.sendMembersUpdate(session); webSocketService.sendNotification( @@ -93,9 +96,8 @@ public void removeMember(Principal principal) { NotificationType.MEMBER_LEFT, new MemberPayload(((MemberPrincipal) principal).getMemberID()))); } else { - val session = - ControllerUtils.getSessionOrThrowResponse( - databaseService, ((AdminPrincipal) principal).getSessionID()); + val session = ControllerUtils.getSessionOrThrowResponse( + databaseService, ((AdminPrincipal) principal).getSessionID()); webSocketService.sendNotification( session, new Notification(NotificationType.ADMIN_LEFT, null)); webSocketService.removeAdmin((AdminPrincipal) principal); @@ -105,9 +107,8 @@ public void removeMember(Principal principal) { @MessageMapping("/kick-member") public void kickMember(AdminPrincipal principal, @Payload String memberID) { - val session = - ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()) - .removeMember(memberID); + val session = ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()) + .removeMember(memberID); databaseService.saveSession(session); webSocketService.sendMembersUpdate(session); webSocketService.sendNotification( @@ -118,8 +119,7 @@ public void kickMember(AdminPrincipal principal, @Payload String memberID) { @MessageMapping("/closeSession") public void closeSession(AdminPrincipal principal) { LOGGER.debug("--> closeSession()"); - val session = - ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()); + val session = ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()); webSocketService.sendSessionStateToMembers( session.updateSessionState(SessionState.SESSION_CLOSED)); webSocketService.removeSession(session); @@ -130,20 +130,19 @@ public void closeSession(AdminPrincipal principal) { @MessageMapping("/memberUpdate") public void getMemberUpdate(AdminPrincipal principal) { LOGGER.debug("--> getMemberUpdate()"); - val session = - ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()); + val session = ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()); webSocketService.sendMembersUpdate(session); LOGGER.debug("<-- getMemberUpdate()"); } @MessageMapping("/startVoting") - public void startEstimation(AdminPrincipal principal) { + public void startEstimation(AdminPrincipal principal, @Payload Boolean stateOfHostVoting) { LOGGER.debug("--> startEstimation()"); - val session = - ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()) - .updateSessionState(SessionState.START_VOTING) - .resetCurrentHighlights() - .setTimerTimestamp(Utils.getTimestampISO8601(new Date())); + val session = ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()) + .updateSessionState(SessionState.START_VOTING) + .resetCurrentHighlights() + .setHostVoting(stateOfHostVoting) + .setTimerTimestamp(Utils.getTimestampISO8601(new Date())); databaseService.saveSession(session); webSocketService.sendMembersHostVoting(session); webSocketService.sendSessionStateToMembers(session); @@ -154,11 +153,10 @@ public void startEstimation(AdminPrincipal principal) { @MessageMapping("/votingFinished") public void votingFinished(AdminPrincipal principal) { LOGGER.debug("--> votingFinished()"); - val session = - ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()) - .updateSessionState(SessionState.VOTING_FINISHED) - .selectHighlightedMembers() - .resetTimerTimestamp(); + val session = ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()) + .updateSessionState(SessionState.VOTING_FINISHED) + .selectHighlightedMembers() + .resetTimerTimestamp(); databaseService.saveSession(session); if (session.getHostVoting()) { webSocketService.sendMembersAdminVote(session); @@ -168,39 +166,27 @@ public void votingFinished(AdminPrincipal principal) { LOGGER.debug("<-- votingFinished()"); } - @MessageMapping("/hostVoting") - public void hostVotingChanged(AdminPrincipal principal, @Payload boolean stateOfHostVoting) { - LOGGER.debug("--> hostVotingChanged()"); - val session = ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()) - .setHostVoting(stateOfHostVoting); - databaseService.saveSession(session); - webSocketService.sendMembersHostVoting(session); - LOGGER.debug("<-- hostVotingChanged()"); - } - @MessageMapping("/vote/admin") public synchronized void processVoteAdmin(@Payload String vote, AdminPrincipal admin) { LOGGER.debug("--> processVoteAdmin()"); - val session = - ControllerUtils.getSessionOrThrowResponse(databaseService, admin.getSessionID()) - .setHostEstimation(vote); - //webSocketService.sendMembersUpdate(session); + val session = ControllerUtils.getSessionOrThrowResponse(databaseService, admin.getSessionID()) + .setHostEstimation(vote); + // webSocketService.sendMembersUpdate(session); databaseService.saveSession(session); if (checkIfAllMembersVoted(session.getMembers(), session)) { votingFinished( new AdminPrincipal( admin.getSessionID(), - admin.getAdminID())); //databaseService.getSessionByID(admin.getSessionID()).get().getAdminID() - } + admin.getAdminID())); + } LOGGER.debug("<-- processVoteAdmin()"); } @MessageMapping("/vote") public synchronized void processVote(@Payload String vote, MemberPrincipal member) { LOGGER.debug("--> processVote()"); - val session = - ControllerUtils.getSessionByMemberIDOrThrowResponse(databaseService, member.getMemberID()) - .updateEstimation(member.getMemberID(), vote); + val session = ControllerUtils.getSessionByMemberIDOrThrowResponse(databaseService, member.getMemberID()) + .updateEstimation(member.getMemberID(), vote); webSocketService.sendMembersUpdate(session); databaseService.saveSession(session); @@ -215,35 +201,36 @@ public synchronized void processVote(@Payload String vote, MemberPrincipal membe } private boolean checkIfAllMembersVoted(List members, Session session) { - if (session.getHostVoting() == false) { + if (session.getHostVoting() == false) { return members.stream().filter(m -> m.getCurrentEstimation() == null).count() == 0; } - return members.stream().filter(m -> m.getCurrentEstimation() == null).count() == 0 && null != session.getHostEstimation() && !"".equals(session.getHostEstimation().getHostEstimation()); + return members.stream().filter(m -> m.getCurrentEstimation() == null).count() == 0 + && null != session.getHostEstimation() && !"".equals(session.getHostEstimation().getHostEstimation()); } @MessageMapping("/restart") - public synchronized void restartVote(AdminPrincipal principal) { + public synchronized void restartVote(AdminPrincipal principal, @Payload Boolean stateOfHostVoting) { LOGGER.debug("--> restartVote()"); - val session = - ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()) - .updateSessionState(SessionState.START_VOTING) - .resetEstimations() - .setTimerTimestamp(Utils.getTimestampISO8601(new Date())); + val session = ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()) + .updateSessionState(SessionState.START_VOTING) + .resetEstimations() + .setHostVoting(stateOfHostVoting) + .setTimerTimestamp(Utils.getTimestampISO8601(new Date())); databaseService.saveSession(session); webSocketService.sendMembersUpdate(session); + webSocketService.sendMembersHostVoting(session); webSocketService.sendSessionStateToMembers(session); webSocketService.sendTimerStartMessage(session, session.getTimerTimestamp()); - //webSocketService.sendMembersAdminVote(session); + webSocketService.sendMembersAdminVote(session); LOGGER.debug("<-- restartVote()"); - } + } @MessageMapping("/adminUpdatedUserStories") public synchronized void adminUpdatedUserStories( AdminPrincipal principal, @Payload List userStories) { LOGGER.debug("--> adminUpdatedUserStories()"); - val session = - ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()) - .updateUserStories(userStories); + val session = ControllerUtils.getSessionOrThrowResponse(databaseService, principal.getSessionID()) + .updateUserStories(userStories); databaseService.saveSession(session); webSocketService.sendUpdatedUserStoriesToMembers(session); LOGGER.debug("<-- adminUpdatedUserStories()"); diff --git a/backend/src/main/java/io/diveni/backend/model/Session.java b/backend/src/main/java/io/diveni/backend/model/Session.java index 7a48b57f5..f535cc748 100644 --- a/backend/src/main/java/io/diveni/backend/model/Session.java +++ b/backend/src/main/java/io/diveni/backend/model/Session.java @@ -35,7 +35,8 @@ @Document("sessions") public class Session { - @Id private final ObjectId databaseID; + @Id + private final ObjectId databaseID; private final String sessionID; @@ -79,10 +80,9 @@ static Stream getFilteredEstimationStream(List members) { } public Session updateEstimation(String memberID, String vote) { - val updatedMembers = - members.stream() - .map(m -> m.getMemberID().equals(memberID) ? m.updateEstimation(vote) : m) - .collect(Collectors.toList()); + val updatedMembers = members.stream() + .map(m -> m.getMemberID().equals(memberID) ? m.updateEstimation(vote) : m) + .collect(Collectors.toList()); return new Session( databaseID, sessionID, @@ -105,26 +105,26 @@ public Session selectHighlightedMembers() { memberVoted.putIfAbsent(member.getMemberID(), 0); } - val filteredSet = - sessionConfig.getSet().stream() - .filter((string) -> !string.equals("?")) - .collect(Collectors.toList()); + val filteredSet = sessionConfig.getSet().stream() + .filter((string) -> !string.equals("?")) + .collect(Collectors.toList()); Optional maxEstimation; Optional minEstimation; - if (!this.hostVoting || this.hostEstimation.equals("")) { + if (!this.hostVoting || this.hostEstimation.getHostEstimation().equals("")) { maxEstimation = getFilteredEstimationStream(this.members).max(estimationByIndex(filteredSet)); } else { Stream filteredEstimationsMember = getFilteredEstimationStream(this.members); - Stream allEstimations = Stream.concat(filteredEstimationsMember, Stream.of(this.hostEstimation.getHostEstimation())); + Stream allEstimations = Stream.concat(filteredEstimationsMember, + Stream.of(this.hostEstimation.getHostEstimation())); maxEstimation = allEstimations.max(estimationByIndex(filteredSet)); } - if (!this.hostVoting || this.hostEstimation.equals("")) { - minEstimation = - getFilteredEstimationStream(this.members).min(estimationByIndex(filteredSet)); + if (!this.hostVoting || this.hostEstimation.getHostEstimation().equals("")) { + minEstimation = getFilteredEstimationStream(this.members).min(estimationByIndex(filteredSet)); } else { Stream filteredEstimationsMember = getFilteredEstimationStream(this.members); - Stream allEstimations = Stream.concat(filteredEstimationsMember, Stream.of(this.hostEstimation.getHostEstimation())); + Stream allEstimations = Stream.concat(filteredEstimationsMember, + Stream.of(this.hostEstimation.getHostEstimation())); minEstimation = allEstimations.min(estimationByIndex(filteredSet)); } @@ -145,21 +145,17 @@ public Session selectHighlightedMembers() { hostVoting, hostEstimation); } - val maxEstimationMembers = - this.members.stream() - .filter( - (member) -> - member.getCurrentEstimation() != null - && member.getCurrentEstimation().equals(maxEstimation.get())) - .collect(Collectors.toList()); - - List> maxOptions = - memberVoted.entrySet().stream() - .filter( - (entry) -> - maxEstimationMembers.stream() - .anyMatch((member) -> member.getMemberID().equals(entry.getKey()))) - .collect(Collectors.toList()); + val maxEstimationMembers = this.members.stream() + .filter( + (member) -> member.getCurrentEstimation() != null + && member.getCurrentEstimation().equals(maxEstimation.get())) + .collect(Collectors.toList()); + + List> maxOptions = memberVoted.entrySet().stream() + .filter( + (entry) -> maxEstimationMembers.stream() + .anyMatch((member) -> member.getMemberID().equals(entry.getKey()))) + .collect(Collectors.toList()); if (hostVoting == true) { if (maxEstimation.get().equals(this.hostEstimation.getHostEstimation())) { @@ -167,59 +163,51 @@ public Session selectHighlightedMembers() { } } - val maxMemberID = - Collections.max(maxOptions, Comparator.comparingInt(Map.Entry::getValue)).getKey(); - - val minEstimationMembers = - this.members.stream() - .filter( - (member) -> - member.getCurrentEstimation() != null - && member.getCurrentEstimation().equals(minEstimation.get())) - .collect(Collectors.toList()); - - List> minOptions = - memberVoted.entrySet().stream() - .filter( - (entry) -> - minEstimationMembers.stream() - .anyMatch((member) -> member.getMemberID().equals(entry.getKey()))) - .collect(Collectors.toList()); - + val maxMemberID = Collections.max(maxOptions, Comparator.comparingInt(Map.Entry::getValue)).getKey(); + + val minEstimationMembers = this.members.stream() + .filter( + (member) -> member.getCurrentEstimation() != null + && member.getCurrentEstimation().equals(minEstimation.get())) + .collect(Collectors.toList()); + + List> minOptions = memberVoted.entrySet().stream() + .filter( + (entry) -> minEstimationMembers.stream() + .anyMatch((member) -> member.getMemberID().equals(entry.getKey()))) + .collect(Collectors.toList()); + if (hostVoting == true) { if (minEstimation.get().equals(this.hostEstimation.getHostEstimation())) { minOptions.add(new AbstractMap.SimpleEntry(this.adminID, 0)); } } - val minMemberID = - Collections.max(minOptions, Comparator.comparingInt(Map.Entry::getValue)).getKey(); - - val newVoted = - memberVoted.entrySet().stream() - .map( - (entry) -> { - if (entry.getKey().equals(maxMemberID) || entry.getKey().equals(minMemberID)) { - return Map.entry(entry.getKey(), 0); - } else { - return Map.entry(entry.getKey(), entry.getValue() + 1); - } - }) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + val minMemberID = Collections.max(minOptions, Comparator.comparingInt(Map.Entry::getValue)).getKey(); + + val newVoted = memberVoted.entrySet().stream() + .map( + (entry) -> { + if (entry.getKey().equals(maxMemberID) || entry.getKey().equals(minMemberID)) { + return Map.entry(entry.getKey(), 0); + } else { + return Map.entry(entry.getKey(), entry.getValue() + 1); + } + }) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); val newHighlighted = List.of(minMemberID, maxMemberID); - val sortedMembers = - members.stream() - .sorted( - (Member m1, Member m2) -> { - boolean b1 = newHighlighted.contains(m1.getMemberID()); - boolean b2 = newHighlighted.contains(m2.getMemberID()); - if (b1 == b2) { - return 0; - } - return b1 ? -1 : 1; - }) - .collect(Collectors.toList()); + val sortedMembers = members.stream() + .sorted( + (Member m1, Member m2) -> { + boolean b1 = newHighlighted.contains(m1.getMemberID()); + boolean b2 = newHighlighted.contains(m2.getMemberID()); + if (b1 == b2) { + return 0; + } + return b1 ? -1 : 1; + }) + .collect(Collectors.toList()); return new Session( databaseID, sessionID, @@ -256,13 +244,12 @@ public Session resetCurrentHighlights() { } public Session updateUserStories(List userStories) { - val updatedSessionConfig = - new SessionConfig( - sessionConfig.getSet(), - userStories, - sessionConfig.getTimerSeconds().orElse(null), - sessionConfig.getUserStoryMode(), - sessionConfig.getPassword()); + val updatedSessionConfig = new SessionConfig( + sessionConfig.getSet(), + userStories, + sessionConfig.getTimerSeconds().orElse(null), + sessionConfig.getUserStoryMode(), + sessionConfig.getPassword()); return new Session( databaseID, sessionID, @@ -281,8 +268,7 @@ public Session updateUserStories(List userStories) { } public Session resetEstimations() { - val updatedMembers = - members.stream().map(m -> m.resetEstimation()).collect(Collectors.toList()); + val updatedMembers = members.stream().map(m -> m.resetEstimation()).collect(Collectors.toList()); return new Session( databaseID, sessionID, @@ -357,10 +343,9 @@ public Session addMember(Member member) { } public Session removeMember(String memberID) { - val updatedMembers = - members.stream() - .filter(m -> !m.getMemberID().equals(memberID)) - .collect(Collectors.toList()); + val updatedMembers = members.stream() + .filter(m -> !m.getMemberID().equals(memberID)) + .collect(Collectors.toList()); return new Session( databaseID, sessionID, diff --git a/frontend/src/components/actions/SessionStartButton.vue b/frontend/src/components/actions/SessionStartButton.vue index 91cdb1444..2a82530f4 100644 --- a/frontend/src/components/actions/SessionStartButton.vue +++ b/frontend/src/components/actions/SessionStartButton.vue @@ -1,9 +1,5 @@ @@ -20,14 +16,15 @@ export default Vue.extend({ required: false, default: () => [] as Array, }, + hostVoting: { type: Boolean, required: true } }, methods: { sendStartEstimationMessages() { const endPoint = Constants.webSocketStartPlanningRoute; - this.$store.commit("sendViaBackendWS", { - endPoint + this.$store.commit("sendViaBackendWS", { + endPoint, data: this.hostVoting }); - this.$emit("clicked"); + this.$emit("clicked"); }, }, }); diff --git a/frontend/src/constants.ts b/frontend/src/constants.ts index 446a11d3e..47c6a8848 100644 --- a/frontend/src/constants.ts +++ b/frontend/src/constants.ts @@ -21,9 +21,7 @@ class Constants { webSocketCloseSessionRoute = "/ws/closeSession"; - webSocketHostVotingRoute = "/ws/hostVoting"; - - webSocketMemberListenHostVotingRoute = "/users/updates/hostVoting"; + webSocketMemberListenHostVotingRoute = "/users/updates/hostVoting"; webSocketGetMemberUpdateRoute = "/ws/memberUpdate"; diff --git a/frontend/src/views/MemberVotePage.vue b/frontend/src/views/MemberVotePage.vue index 5ad636842..0b38dd425 100644 --- a/frontend/src/views/MemberVotePage.vue +++ b/frontend/src/views/MemberVotePage.vue @@ -17,70 +17,37 @@ - + - +
- - - + + +
- +
@@ -91,50 +58,36 @@

- -
- + +
+
+ +
- -
- - -
- + + + + +
+
+
- +
+
@@ -143,33 +96,19 @@
- +
- +
- +
@@ -231,6 +170,7 @@ export default Vue.extend({ estimateFinished: false, pauseSession: false, safedHostEstimation: null, + test: "", }; }, computed: { @@ -245,7 +185,7 @@ export default Vue.extend({ memberUpdates() { return this.$store.state.memberUpdates; }, - isStartVoting(): boolean { + isStartVoting(): boolean { return this.memberUpdates.at(-1) === Constants.memberUpdateCommandStartVoting; }, votingFinished(): boolean { @@ -266,7 +206,7 @@ export default Vue.extend({ notifications() { return this.$store.state.notifications; }, - hostVoting() { + hostVoting(): boolean { return this.$store.state.hostVoting; }, hostEstimation() { @@ -391,6 +331,7 @@ export default Vue.extend({ /* overflow:visible; Add when fix is clear how to stay responsiv*/ width: 100%; } + .overlayText { font-size: 2em; margin: 0.67em 0; diff --git a/frontend/src/views/SessionPage.vue b/frontend/src/views/SessionPage.vue index 9548e5322..dba441230 100644 --- a/frontend/src/views/SessionPage.vue +++ b/frontend/src/views/SessionPage.vue @@ -5,32 +5,22 @@

{{ planningStart - ? $t("page.session.during.estimation.title") - : $t("page.session.before.title") + ? $t("page.session.during.estimation.title") + : $t("page.session.before.title") }}

- + - +
- +

{{ $t("page.session.before.text.waiting") }} @@ -38,18 +28,12 @@

- + - +
@@ -67,12 +51,8 @@ - + @@ -83,16 +63,13 @@
-
+
{{ (estimateFinished = true) }}
+ style="display: none"> {{ (estimateFinished = true) }}
@@ -103,75 +80,49 @@
- +

{{ $t("page.session.during.estimation.message.finished") }} {{ membersEstimated.length }} / - {{ membersPending.length + membersEstimated.length }} + {{ members.length }}

{{ $t("page.session.during.estimation.message.finished") }} {{ membersEstimated.length }} / - {{ membersPending.length + membersEstimated.length + 1}} + {{ members.length + 1 }}
{{ $t("page.session.during.estimation.message.finished") }} - {{ membersEstimated.length + 1}} / - {{ membersPending.length + membersEstimated.length + 1}} + {{ membersEstimated.length + 1 }} / + {{ members.length + 1 }}

- - - + + + + + - - - - + }" /> +
@@ -181,26 +132,20 @@
- - {{ item }} + + {{ item }}
- - + +

- +

@@ -214,27 +159,17 @@
- +
- +
- +
@@ -308,6 +243,7 @@ export default Vue.extend({ hostVoting: false, hostEstimation: "", safedHostVoting: false, + numberOfEstimatesInThisRound: 0, }; }, computed: { @@ -588,8 +524,13 @@ export default Vue.extend({ this.estimateFinished = false; this.hostEstimation = ''; this.safedHostVoting = this.hostVoting; + if (this.hostVoting) { + this.numberOfEstimatesInThisRound = this.members.length + 1; + } else { + this.numberOfEstimatesInThisRound = this.members.length; + } const endPoint = Constants.webSocketRestartPlanningRoute; - this.$store.commit("sendViaBackendWS", { endPoint }); + this.$store.commit("sendViaBackendWS", { endPoint, data: this.hostVoting }); }, goToLandingPage() { this.$router.push({ name: "LandingPage" }); @@ -597,45 +538,49 @@ export default Vue.extend({ onPlanningStarted() { this.planningStart = true; this.safedHostVoting = this.hostVoting; + if (this.hostVoting) { + this.numberOfEstimatesInThisRound = this.members.length + 1; + } else { + this.numberOfEstimatesInThisRound = this.members.length; + } }, vote(vote: string) { const endPoint = `${Constants.webSocketVoteRouteAdmin}`; this.$store.commit("sendViaBackendWS", { endPoint, data: vote }); this.hostEstimation = vote; }, - sendHostVoting() { - this.hostVoting = !this.hostVoting; - const endPoint = `${Constants.webSocketHostVotingRoute}`; - this.$store.commit("sendViaBackendWS", { endPoint, data: this.hostVoting}); - }, }, }); From b1523d157bc838384104305ee255a6b8ee006b97 Mon Sep 17 00:00:00 2001 From: SponsoredByPuma <92574150+SponsoredByPuma@users.noreply.github.com> Date: Mon, 27 Mar 2023 11:55:46 +0200 Subject: [PATCH 08/30] Finished the backend tests --- .../controller/WebsocketControllerTest.java | 162 ++--- .../diveni/backend/model/AdminVoteTest.java | 19 + .../io/diveni/backend/model/SessionTest.java | 575 ++++++++------- .../backend/service/WebSocketServiceTest.java | 683 +++++++++--------- 4 files changed, 744 insertions(+), 695 deletions(-) create mode 100644 backend/src/test/java/io/diveni/backend/model/AdminVoteTest.java diff --git a/backend/src/test/java/io/diveni/backend/controller/WebsocketControllerTest.java b/backend/src/test/java/io/diveni/backend/controller/WebsocketControllerTest.java index c8710c65e..638b6bedd 100644 --- a/backend/src/test/java/io/diveni/backend/controller/WebsocketControllerTest.java +++ b/backend/src/test/java/io/diveni/backend/controller/WebsocketControllerTest.java @@ -6,7 +6,6 @@ package io.diveni.backend.controller; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import java.lang.reflect.Type; import java.util.ArrayList; @@ -22,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.diveni.backend.Utils; +import io.diveni.backend.model.AdminVote; import io.diveni.backend.model.Member; import io.diveni.backend.model.MemberUpdate; import io.diveni.backend.model.Session; @@ -74,43 +74,43 @@ public class WebsocketControllerTest { private static final String VOTE = "/ws/vote"; + private static final String ADMIN_VOTE = "/ws/vote/admin"; + private static final String UNREGISTER = "/ws/unregister"; private static final String ADMIN_MEMBER_UPDATES = "/users/updates/membersUpdated"; - private static final String MEMBER_LISTEN_HOSTVOTING = "/users/updates/hostVoting"; - - private static final String ADMIN_SENDS_HOSTVOTING = "/ws/hostVoting"; - private static final ObjectMapper objectMapper = new ObjectMapper(); - @Autowired SessionRepository sessionRepo; + @Autowired + SessionRepository sessionRepo; - @Autowired private WebSocketService webSocketService; + @Autowired + private WebSocketService webSocketService; - @LocalServerPort private Integer port; + @LocalServerPort + private Integer port; private WebSocketStompClient webSocketStompClient; private BlockingQueue blockingQueue; - private final StompFrameHandler stompFrameHandler = - new StompFrameHandler() { - @Override - public Type getPayloadType(StompHeaders stompHeaders) { - return List.class; - } - - @Override - public void handleFrame(StompHeaders stompHeaders, Object o) { - try { - blockingQueue.offer(objectMapper.writeValueAsString(o)); - } catch (JsonProcessingException e) { - throw new RuntimeException("Failed to handle frame", e); - } - } - }; - private final List transports = - Collections.singletonList(new WebSocketTransport(new StandardWebSocketClient())); + private final StompFrameHandler stompFrameHandler = new StompFrameHandler() { + @Override + public Type getPayloadType(StompHeaders stompHeaders) { + return List.class; + } + + @Override + public void handleFrame(StompHeaders stompHeaders, Object o) { + try { + blockingQueue.offer(objectMapper.writeValueAsString(o)); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to handle frame", e); + } + } + }; + private final List transports = Collections + .singletonList(new WebSocketTransport(new StandardWebSocketClient())); @BeforeAll public static void init() { @@ -142,7 +142,8 @@ public void handleException( private StompSession getMemberSession(String sessionID, String memberID) throws Exception { return webSocketStompClient .connect( - getWsPath(WS_MEMBER_PATH, sessionID, memberID), new StompSessionHandlerAdapter() {}) + getWsPath(WS_MEMBER_PATH, sessionID, memberID), new StompSessionHandlerAdapter() { + }) .get(); } @@ -246,10 +247,9 @@ public void registerMemberPrincipal_sendsMembersUpdates() throws Exception { val sessionID = Utils.generateRandomID(); val adminID = Utils.generateRandomID(); val memberID = Utils.generateRandomID(); - val memberList = - List.of( - new Member(memberID, null, null, null, null), - new Member(Utils.generateRandomID(), null, null, null, null)); + val memberList = List.of( + new Member(memberID, null, null, null, null), + new Member(Utils.generateRandomID(), null, null, null, null)); val adminPrincipal = new AdminPrincipal(sessionID, adminID); sessionRepo.save( new Session( @@ -464,32 +464,32 @@ public void startVoting_updatesState() throws Exception { val member = new Member(memberID, null, null, null, null); val memberList = List.of(member); val adminPrincipal = new AdminPrincipal(sessionID, adminID); - val oldSession = - new Session( - dbID, - sessionID, - adminID, - new SessionConfig(new ArrayList<>(), List.of(), 10, "US_MANUALLY", null), - null, - memberList, - new HashMap<>(), - new ArrayList<>(), - SessionState.WAITING_FOR_MEMBERS, - null, - null, - null, - false, - null); + val oldSession = new Session( + dbID, + sessionID, + adminID, + new SessionConfig(new ArrayList<>(), List.of(), 10, "US_MANUALLY", null), + null, + memberList, + new HashMap<>(), + new ArrayList<>(), + SessionState.WAITING_FOR_MEMBERS, + null, + null, + null, + false, + null); sessionRepo.save(oldSession); webSocketService.setAdminUser(adminPrincipal); StompSession adminSession = getAdminSession(sessionID, adminID); - adminSession.send(START_VOTING, false); + adminSession.send(START_VOTING, true); // Wait for server-side handling TimeUnit.MILLISECONDS.sleep(TIMEOUT); val newSession = sessionRepo.findBySessionID(oldSession.getSessionID()); Assertions.assertEquals(SessionState.START_VOTING, newSession.getSessionState()); + Assertions.assertTrue(newSession.getHostVoting()); } @Test @@ -501,45 +501,47 @@ public void restartVoting_resetsEstimations() throws Exception { val member = new Member(memberID, null, null, null, "5"); val memberList = List.of(member); val adminPrincipal = new AdminPrincipal(sessionID, adminID); - val oldSession = - new Session( - dbID, - sessionID, - adminID, - new SessionConfig(new ArrayList<>(), List.of(), 10, "US_MANUALLY", null), - null, - memberList, - new HashMap<>(), - new ArrayList<>(), - SessionState.WAITING_FOR_MEMBERS, - null, - null, - null, - false, - null); + val adminVote = new AdminVote("XL"); + val oldSession = new Session( + dbID, + sessionID, + adminID, + new SessionConfig(new ArrayList<>(), List.of(), 10, "US_MANUALLY", null), + null, + memberList, + new HashMap<>(), + new ArrayList<>(), + SessionState.WAITING_FOR_MEMBERS, + null, + null, + null, + false, + adminVote); sessionRepo.save(oldSession); webSocketService.setAdminUser(adminPrincipal); StompSession adminSession = getAdminSession(sessionID, adminID); - adminSession.send(RESTART, null); + adminSession.send(RESTART, true); // Wait for server-side handling TimeUnit.MILLISECONDS.sleep(TIMEOUT); val newMembers = sessionRepo.findBySessionID(oldSession.getSessionID()).getMembers(); + val newHostVoting = sessionRepo.findBySessionID(oldSession.getSessionID()).getHostVoting(); + val newHostEstimation = sessionRepo.findBySessionID(oldSession.getSessionID()).getHostEstimation(); Assertions.assertTrue(newMembers.stream().allMatch(m -> m.getCurrentEstimation() == null)); + Assertions.assertTrue(newHostVoting); + Assertions.assertTrue(newHostEstimation.getHostEstimation().equals("")); } - @Test - public void hostVotingChanged_changesHostVoting() throws Exception { + public void adminVote_setsHostEstimation() throws Exception { val dbID = new ObjectId(); val sessionID = Utils.generateRandomID(); val adminID = Utils.generateRandomID(); val memberID = Utils.generateRandomID(); - val memberList = - List.of( - new Member(memberID, null, null, null, null), - new Member(Utils.generateRandomID(), null, null, null, null)); + val member = new Member(memberID, null, null, null, null); + val memberList = List.of(member); val adminPrincipal = new AdminPrincipal(sessionID, adminID); + val adminVote = new AdminVote(""); sessionRepo.save( new Session( dbID, @@ -549,24 +551,22 @@ public void hostVotingChanged_changesHostVoting() throws Exception { null, memberList, new HashMap<>(), - List.of("asdf", "bsdf"), + new ArrayList<>(), SessionState.WAITING_FOR_MEMBERS, null, null, null, - false, - null)); + true, + adminVote)); webSocketService.setAdminUser(adminPrincipal); - StompSession session = getMemberSession(sessionID, memberID); - StompSession adminSession = getAdminSession(sessionID, adminID); - - session.subscribe(MEMBER_LISTEN_HOSTVOTING, stompFrameHandler); - adminSession.send(ADMIN_SENDS_HOSTVOTING, true); + StompSession session = getAdminSession(sessionID, adminID); + val vote = "5"; + session.send(ADMIN_VOTE, vote); // Wait for server-side handling TimeUnit.MILLISECONDS.sleep(TIMEOUT); - Session result = sessionRepo.findBySessionID(sessionID); - assertTrue(result.getHostVoting()); + val newSession = sessionRepo.findBySessionID(sessionID); + assertEquals(newSession.getHostEstimation().getHostEstimation(), vote); } } diff --git a/backend/src/test/java/io/diveni/backend/model/AdminVoteTest.java b/backend/src/test/java/io/diveni/backend/model/AdminVoteTest.java new file mode 100644 index 000000000..87e034509 --- /dev/null +++ b/backend/src/test/java/io/diveni/backend/model/AdminVoteTest.java @@ -0,0 +1,19 @@ +package io.diveni.backend.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import lombok.val; + +public class AdminVoteTest { + + @Test + public void setHostEstimation_works() { + val adminVote = new AdminVote(""); + adminVote.setHostEstimation("XL"); + + assertEquals(adminVote.getHostEstimation(), "XL"); + } + +} diff --git a/backend/src/test/java/io/diveni/backend/model/SessionTest.java b/backend/src/test/java/io/diveni/backend/model/SessionTest.java index d50ac2edb..024d662ad 100644 --- a/backend/src/test/java/io/diveni/backend/model/SessionTest.java +++ b/backend/src/test/java/io/diveni/backend/model/SessionTest.java @@ -30,54 +30,51 @@ public void equal_works() { val sessionIdBefore = Utils.generateRandomID(); val adminIdBefore = Utils.generateRandomID(); val dbId = new ObjectId(); - val session = - new Session( - dbId, - sessionIdBefore, - adminIdBefore, - null, - null, - new ArrayList<>(), - new HashMap<>(), - new ArrayList<>(), - SessionState.WAITING_FOR_MEMBERS, - null, - null, - null, - false, - null); - val sameSession = - new Session( - dbId, - sessionIdBefore, - adminIdBefore, - null, - null, - new ArrayList<>(), - new HashMap<>(), - new ArrayList<>(), - SessionState.WAITING_FOR_MEMBERS, - null, - null, - null, - false, - null); - val otherSession = - new Session( - new ObjectId(), - sessionIdBefore, - adminIdBefore, - null, - null, - new ArrayList<>(), - new HashMap<>(), - new ArrayList<>(), - SessionState.WAITING_FOR_MEMBERS, - null, - null, - null, - false, - null); + val session = new Session( + dbId, + sessionIdBefore, + adminIdBefore, + null, + null, + new ArrayList<>(), + new HashMap<>(), + new ArrayList<>(), + SessionState.WAITING_FOR_MEMBERS, + null, + null, + null, + false, + null); + val sameSession = new Session( + dbId, + sessionIdBefore, + adminIdBefore, + null, + null, + new ArrayList<>(), + new HashMap<>(), + new ArrayList<>(), + SessionState.WAITING_FOR_MEMBERS, + null, + null, + null, + false, + null); + val otherSession = new Session( + new ObjectId(), + sessionIdBefore, + adminIdBefore, + null, + null, + new ArrayList<>(), + new HashMap<>(), + new ArrayList<>(), + SessionState.WAITING_FOR_MEMBERS, + null, + null, + null, + false, + null); assertEquals(session, sameSession); assertNotEquals(session, otherSession); @@ -93,29 +90,27 @@ public void updateEstimation_works() { val members = Arrays.asList(member1, member2); val vote = "5"; - val session = - new Session( - null, - null, - null, - null, - null, - members, - new HashMap<>(), - new ArrayList<>(), - SessionState.WAITING_FOR_MEMBERS, - null, - null, - null, - false, - null); + val session = new Session( + null, + null, + null, + null, + null, + members, + new HashMap<>(), + new ArrayList<>(), + SessionState.WAITING_FOR_MEMBERS, + null, + null, + null, + false, + null); val result = session.updateEstimation(member1.getMemberID(), vote); - val resultMember = - result.getMembers().stream() - .filter(m -> m.getMemberID().equals(member1.getMemberID())) - .findFirst() - .get(); + val resultMember = result.getMembers().stream() + .filter(m -> m.getMemberID().equals(member1.getMemberID())) + .findFirst() + .get(); assertTrue(resultMember.getCurrentEstimation() != null); assertEquals(resultMember.getCurrentEstimation(), vote); } @@ -127,22 +122,21 @@ public void resetEstimations_works() { val member1 = new Member(memberID1, null, null, null, "3"); val member2 = new Member(memberID2, null, null, null, "5"); val members = Arrays.asList(member1, member2); - val session = - new Session( - null, - null, - null, - null, - null, - members, - new HashMap<>(), - new ArrayList<>(), - SessionState.WAITING_FOR_MEMBERS, - null, - null, - null, - false, - new AdminVote("10")); + val session = new Session( + null, + null, + null, + null, + null, + members, + new HashMap<>(), + new ArrayList<>(), + SessionState.WAITING_FOR_MEMBERS, + null, + null, + null, + false, + new AdminVote("10")); val result = session.resetEstimations(); @@ -155,22 +149,21 @@ public void updateSessionState_works() { val oldSessionState = SessionState.WAITING_FOR_MEMBERS; val newSessionState = SessionState.START_VOTING; - val session = - new Session( - null, - null, - null, - null, - null, - new ArrayList(), - new HashMap<>(), - new ArrayList<>(), - oldSessionState, - null, - null, - null, - false, - null); + val session = new Session( + null, + null, + null, + null, + null, + new ArrayList(), + new HashMap<>(), + new ArrayList<>(), + oldSessionState, + null, + null, + null, + false, + null); val result = session.updateSessionState(newSessionState); assertEquals(result.getSessionState(), newSessionState); @@ -178,22 +171,21 @@ public void updateSessionState_works() { @Test public void setLastModified_works() { - val session = - new Session( - null, - null, - null, - null, - null, - new ArrayList(), - new HashMap<>(), - new ArrayList<>(), - null, - null, - null, - null, - false, - null); + val session = new Session( + null, + null, + null, + null, + null, + new ArrayList(), + new HashMap<>(), + new ArrayList<>(), + null, + null, + null, + null, + false, + null); val date = new Date(); val result = session.setLastModified(date); @@ -206,22 +198,21 @@ public void addMember_works() { val memberID1 = Utils.generateRandomID(); val member1 = new Member(memberID1, null, null, null, "3"); val members = Arrays.asList(member1); - val session = - new Session( - null, - null, - null, - null, - null, - members, - new HashMap<>(), - new ArrayList<>(), - SessionState.WAITING_FOR_MEMBERS, - null, - null, - null, - false, - null); + val session = new Session( + null, + null, + null, + null, + null, + members, + new HashMap<>(), + new ArrayList<>(), + SessionState.WAITING_FOR_MEMBERS, + null, + null, + null, + false, + null); val memberID2 = Utils.generateRandomID(); val member2 = new Member(memberID2, null, null, null, "5"); @@ -238,22 +229,21 @@ public void removeMember_works() { val member1 = new Member(memberID1, null, null, null, "3"); val member2 = new Member(memberID2, null, null, null, "5"); val members = Arrays.asList(member1, member2); - val session = - new Session( - null, - null, - null, - null, - null, - members, - new HashMap<>(), - new ArrayList<>(), - SessionState.WAITING_FOR_MEMBERS, - null, - null, - null, - false, - null); + val session = new Session( + null, + null, + null, + null, + null, + members, + new HashMap<>(), + new ArrayList<>(), + SessionState.WAITING_FOR_MEMBERS, + null, + null, + null, + false, + null); val result = session.removeMember(memberID1); @@ -267,22 +257,21 @@ public void selectHighlightedMembers_works() { val member1 = new Member(Utils.generateRandomID(), null, null, null, "S"); val member2 = new Member(Utils.generateRandomID(), null, null, null, "L"); val member3 = new Member(Utils.generateRandomID(), null, null, null, "XS"); - val session = - new Session( - null, - null, - null, - new SessionConfig(set, List.of(), 30, "US_MANUALLY", null), - null, - List.of(member1, member2, member3), - new HashMap<>(), - new ArrayList<>(), - null, - null, - null, - null, - false, - null); + val session = new Session( + null, + null, + null, + new SessionConfig(set, List.of(), 30, "US_MANUALLY", null), + null, + List.of(member1, member2, member3), + new HashMap<>(), + new ArrayList<>(), + null, + null, + null, + null, + true, + new AdminVote("M")); val result = session.selectHighlightedMembers(); @@ -293,6 +282,37 @@ public void selectHighlightedMembers_works() { assertEquals(0, result.getMemberVoted().get(member3.getMemberID())); } + @Test + public void selectHighlightedMembers_adminGetsSelected() { + List set = List.of("XS", "S", "M", "L", "XL"); + val member1 = new Member(Utils.generateRandomID(), null, null, null, "S"); + val member2 = new Member(Utils.generateRandomID(), null, null, null, "L"); + val member3 = new Member(Utils.generateRandomID(), null, null, null, "M"); + val session = new Session( + null, + null, + "ADMINID", + new SessionConfig(set, List.of(), 30, "US_MANUALLY", null), + null, + List.of(member1, member2, member3), + new HashMap<>(), + new ArrayList<>(), + null, + null, + null, + null, + true, + new AdminVote("XS")); + + val result = session.selectHighlightedMembers(); + + val expectedHighlights = List.of(session.getAdminID(), member2.getMemberID()); + assertEquals(expectedHighlights, result.getCurrentHighlights()); + assertEquals(1, result.getMemberVoted().get(member1.getMemberID())); + assertEquals(0, result.getMemberVoted().get(member2.getMemberID())); + assertEquals(1, result.getMemberVoted().get(member3.getMemberID())); + } + @Test public void selectHighlightedMembers_correctOrder() { List set = List.of("1", "2", "3", "4", "5"); @@ -300,22 +320,21 @@ public void selectHighlightedMembers_correctOrder() { val member2 = new Member(Utils.generateRandomID(), null, null, null, "1"); val member3 = new Member(Utils.generateRandomID(), null, null, null, "3"); val member4 = new Member(Utils.generateRandomID(), null, null, null, "5"); - val session = - new Session( - null, - null, - null, - new SessionConfig(set, List.of(), 30, null, null), - null, - List.of(member1, member2, member3, member4), - new HashMap<>(), - new ArrayList<>(), - null, - null, - null, - null, - false, - null); + val session = new Session( + null, + null, + null, + new SessionConfig(set, List.of(), 30, null, null), + null, + List.of(member1, member2, member3, member4), + new HashMap<>(), + new ArrayList<>(), + null, + null, + null, + null, + false, + null); val result = session.selectHighlightedMembers(); @@ -335,22 +354,21 @@ public void selectHighlightedMembersPrioritized_works() { map.put(member1.getMemberID(), 1); map.put(member2.getMemberID(), 0); map.put(member3.getMemberID(), 0); - val session = - new Session( - null, - null, - null, - new SessionConfig(set, List.of(), 30, "US_MANUALLY", null), - null, - List.of(member1, member2, member3), - map, - new ArrayList<>(), - null, - null, - null, - null, - false, - null); + val session = new Session( + null, + null, + null, + new SessionConfig(set, List.of(), 30, "US_MANUALLY", null), + null, + List.of(member1, member2, member3), + map, + new ArrayList<>(), + null, + null, + null, + null, + false, + null); val result = session.selectHighlightedMembers(); @@ -363,22 +381,21 @@ public void selectHighlightedMembersPrioritized_works() { @Test public void resetHighlightedMembers_works() { - val session = - new Session( - null, - null, - null, - null, - null, - new ArrayList<>(), - new HashMap<>(), - List.of("highlighted1", "highlighted2"), - null, - null, - null, - null, - false, - null); + val session = new Session( + null, + null, + null, + null, + null, + new ArrayList<>(), + new HashMap<>(), + List.of("highlighted1", "highlighted2"), + null, + null, + null, + null, + false, + null); val result = session.resetCurrentHighlights(); @@ -387,22 +404,21 @@ public void resetHighlightedMembers_works() { @Test public void setTimerTimestamp_works() { - val session = - new Session( - null, - null, - null, - null, - null, - new ArrayList<>(), - new HashMap<>(), - new ArrayList<>(), - null, - null, - null, - null, - false, - null); + val session = new Session( + null, + null, + null, + null, + null, + new ArrayList<>(), + new HashMap<>(), + new ArrayList<>(), + null, + null, + null, + null, + false, + null); val timestamp = Utils.getTimestampISO8601(new Date()); val result = session.setTimerTimestamp(timestamp); @@ -412,22 +428,21 @@ public void setTimerTimestamp_works() { @Test public void resetTimerTimestamp_works() { - val session = - new Session( - null, - null, - null, - null, - null, - new ArrayList<>(), - new HashMap<>(), - new ArrayList<>(), - null, - null, - null, - Utils.getTimestampISO8601(new Date()), - false, - null); + val session = new Session( + null, + null, + null, + null, + null, + new ArrayList<>(), + new HashMap<>(), + new ArrayList<>(), + null, + null, + null, + Utils.getTimestampISO8601(new Date()), + false, + null); val result = session.resetTimerTimestamp(); @@ -436,22 +451,21 @@ public void resetTimerTimestamp_works() { @Test public void setHostVoting_works() { - val session = - new Session( - null, - null, - null, - null, - null, - new ArrayList<>(), - new HashMap<>(), - new ArrayList<>(), - null, - null, - null, - Utils.getTimestampISO8601(new Date()), - false, - null); + val session = new Session( + null, + null, + null, + null, + null, + new ArrayList<>(), + new HashMap<>(), + new ArrayList<>(), + null, + null, + null, + Utils.getTimestampISO8601(new Date()), + false, + null); Session result = session.setHostVoting(true); @@ -459,25 +473,24 @@ public void setHostVoting_works() { } public void setHostEstimation_works() { - val session = - new Session( - null, - null, - null, - null, - null, - new ArrayList<>(), - new HashMap<>(), - new ArrayList<>(), - null, - null, - null, - Utils.getTimestampISO8601(new Date()), - false, - null); - - val result = session.setHostEstimation("10"); - - assertEquals("10", result.getHostEstimation()); + val session = new Session( + null, + null, + null, + null, + null, + new ArrayList<>(), + new HashMap<>(), + new ArrayList<>(), + null, + null, + null, + Utils.getTimestampISO8601(new Date()), + false, + null); + + val result = session.setHostEstimation("10"); + + assertEquals("10", result.getHostEstimation().getHostEstimation()); } } diff --git a/backend/src/test/java/io/diveni/backend/service/WebSocketServiceTest.java b/backend/src/test/java/io/diveni/backend/service/WebSocketServiceTest.java index 7fb370119..a59334e23 100644 --- a/backend/src/test/java/io/diveni/backend/service/WebSocketServiceTest.java +++ b/backend/src/test/java/io/diveni/backend/service/WebSocketServiceTest.java @@ -17,6 +17,7 @@ import java.util.Set; import io.diveni.backend.Utils; +import io.diveni.backend.model.AdminVote; import io.diveni.backend.model.Member; import io.diveni.backend.model.MemberUpdate; import io.diveni.backend.model.Session; @@ -41,338 +42,354 @@ public class WebSocketServiceTest { - @Mock SimpMessagingTemplate simpMessagingTemplateMock; - - @InjectMocks private WebSocketService webSocketService; - - private final AdminPrincipal defaultAdminPrincipal = - new AdminPrincipal(Utils.generateRandomID(), Utils.generateRandomID()); - - private final MemberPrincipal defaultMemberPrincipal = - new MemberPrincipal(defaultAdminPrincipal.getSessionID(), Utils.generateRandomID()); - - @BeforeEach - public void initEach() { - MockitoAnnotations.openMocks(this); - } - - void setDefaultAdminPrincipal(Set members) throws Exception { - val sessionPrincipalsField = WebSocketService.class.getDeclaredField("sessionPrincipalList"); - sessionPrincipalsField.setAccessible(true); - val sessionPrincipals = - List.of( - new SessionPrincipals( - defaultAdminPrincipal.getSessionID(), defaultAdminPrincipal, members)); - sessionPrincipalsField.set(webSocketService, sessionPrincipals); - } - - @Test - public void setAdmin_isAdded() throws Exception { - val adminPrincipal = new AdminPrincipal(Utils.generateRandomID(), Utils.generateRandomID()); - - webSocketService.setAdminUser(adminPrincipal); - - assertTrue( - webSocketService.getSessionPrincipalList().stream() - .anyMatch(p -> p.adminPrincipal() == adminPrincipal)); - } - - @Test - public void setExistingAdmin_isOverwritten() throws Exception { - setDefaultAdminPrincipal(new HashSet<>()); - val adminPrincipal = - new AdminPrincipal( - defaultAdminPrincipal.getSessionID(), defaultAdminPrincipal.getAdminID()); - - webSocketService.setAdminUser(adminPrincipal); - - assertTrue( - webSocketService.getSessionPrincipalList().stream() - .anyMatch(p -> p.adminPrincipal() == adminPrincipal)); - } - - @Test - public void getSessionPrincipals_isCorrect() throws Exception { - setDefaultAdminPrincipal(new HashSet<>()); - - val sessionPrincipals = - webSocketService.getSessionPrincipals(defaultAdminPrincipal.getSessionID()); - - Assertions.assertEquals( - new SessionPrincipals( - defaultAdminPrincipal.getSessionID(), defaultAdminPrincipal, Set.of()), - sessionPrincipals); - } - - @Test - public void getMissingSessionEntry_isError() throws Exception { - assertThrows( - ResponseStatusException.class, - () -> webSocketService.getSessionPrincipals(defaultAdminPrincipal.getSessionID())); - } - - @Test - public void addMember_isAdded() throws Exception { - setDefaultAdminPrincipal(new HashSet<>()); - val memberPrincipal = - new MemberPrincipal(defaultAdminPrincipal.getSessionID(), Utils.generateRandomID()); - - webSocketService.addMemberIfNew(memberPrincipal); - - Assertions.assertTrue( - webSocketService - .getSessionPrincipals(defaultAdminPrincipal.getSessionID()) - .memberPrincipals() - .contains(memberPrincipal)); - } - - @Test - public void addExistingMember_notDuplicate() throws Exception { - setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal)); - - webSocketService.addMemberIfNew(defaultMemberPrincipal); - - Assertions.assertEquals( - 1, - webSocketService - .getSessionPrincipals(defaultAdminPrincipal.getSessionID()) - .memberPrincipals() - .size()); - } - - @Test - public void removeMember_isRemoved() throws Exception { - setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal)); - - webSocketService.removeMember(defaultMemberPrincipal); - - Assertions.assertTrue( - webSocketService - .getSessionPrincipals(defaultAdminPrincipal.getSessionID()) - .memberPrincipals() - .isEmpty()); - } - - @Test - public void sendMembersUpdate_sendsUpdate() throws Exception { - setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal)); - val session = - new Session( - new ObjectId(), - defaultAdminPrincipal.getSessionID(), - defaultAdminPrincipal.getAdminID(), - null, - null, - List.of(new Member(defaultMemberPrincipal.getMemberID(), null, null, null, null)), - new HashMap<>(), - new ArrayList<>(), - null, - null, - null, - null, - false, - null); - - webSocketService.sendMembersUpdate(session); - - verify(simpMessagingTemplateMock, times(1)) - .convertAndSendToUser( - defaultAdminPrincipal.getName(), - WebSocketService.MEMBERS_UPDATED_DESTINATION, - new MemberUpdate(session.getMembers(), session.getCurrentHighlights())); - } - - @Test - public void sendSessionState_sendsState() throws Exception { - setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal)); - val session = - new Session( - new ObjectId(), - defaultAdminPrincipal.getSessionID(), - defaultAdminPrincipal.getAdminID(), - null, - null, - List.of(new Member(defaultMemberPrincipal.getMemberID(), null, null, null, null)), - new HashMap<>(), - new ArrayList<>(), - SessionState.WAITING_FOR_MEMBERS, - null, - null, - null, - false, - null); - - webSocketService.sendSessionStateToMember(session, defaultMemberPrincipal.getMemberID()); - - verify(simpMessagingTemplateMock, times(1)) - .convertAndSendToUser( - defaultMemberPrincipal.getMemberID(), - WebSocketService.MEMBER_UPDATES_DESTINATION, - session.getSessionState().toString()); - } - - @Test - public void sendSessionStates_sendsToAll() throws Exception { - val memberPrincipal = - new MemberPrincipal(defaultAdminPrincipal.getSessionID(), Utils.generateRandomID()); - setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal, memberPrincipal)); - val session = - new Session( - new ObjectId(), - defaultAdminPrincipal.getSessionID(), - defaultAdminPrincipal.getAdminID(), - null, - null, - List.of( - new Member(defaultMemberPrincipal.getMemberID(), null, null, null, null), - new Member(memberPrincipal.getMemberID(), null, null, null, null)), - new HashMap<>(), - new ArrayList<>(), - SessionState.WAITING_FOR_MEMBERS, - null, - null, - null, - false, - null); - - webSocketService.sendSessionStateToMembers(session); - - verify(simpMessagingTemplateMock, times(1)) - .convertAndSendToUser( - defaultMemberPrincipal.getMemberID(), - WebSocketService.MEMBER_UPDATES_DESTINATION, - session.getSessionState().toString()); - verify(simpMessagingTemplateMock, times(1)) - .convertAndSendToUser( - memberPrincipal.getMemberID(), - WebSocketService.MEMBER_UPDATES_DESTINATION, - session.getSessionState().toString()); - } - - @Test - public void sendUpdatedUserStoriesToMembers_sendsToAll() throws Exception { - val memberPrincipal = - new MemberPrincipal(defaultAdminPrincipal.getSessionID(), Utils.generateRandomID()); - setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal, memberPrincipal)); - val session = - new Session( - new ObjectId(), - defaultAdminPrincipal.getSessionID(), - defaultAdminPrincipal.getAdminID(), - new SessionConfig(List.of(), List.of(), null, "US_MANUALLY", "password"), - null, - List.of( - new Member(defaultMemberPrincipal.getMemberID(), null, null, null, null), - new Member(memberPrincipal.getMemberID(), null, null, null, null)), - new HashMap<>(), - new ArrayList<>(), - SessionState.WAITING_FOR_MEMBERS, - null, - null, - null, - false, - null); - - webSocketService.sendUpdatedUserStoriesToMembers(session); - - verify(simpMessagingTemplateMock, times(1)) - .convertAndSendToUser( - defaultMemberPrincipal.getMemberID(), - WebSocketService.US_UPDATES_DESTINATION, - session.getSessionConfig().getUserStories()); - verify(simpMessagingTemplateMock, times(1)) - .convertAndSendToUser( - defaultMemberPrincipal.getMemberID(), - WebSocketService.US_UPDATES_DESTINATION, - session.getSessionConfig().getUserStories()); - } - - @Test - public void adminLeft_sendsNotification() throws Exception { - val memberPrincipal = - new MemberPrincipal(defaultAdminPrincipal.getSessionID(), Utils.generateRandomID()); - setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal, memberPrincipal)); - val session = - new Session( - new ObjectId(), - defaultAdminPrincipal.getSessionID(), - defaultAdminPrincipal.getAdminID(), - new SessionConfig(List.of(), List.of(), null, "US_MANUALLY", "password"), - null, - List.of( - new Member(defaultMemberPrincipal.getMemberID(), null, null, null, null), - new Member(memberPrincipal.getMemberID(), null, null, null, null)), - new HashMap<>(), - new ArrayList<>(), - SessionState.WAITING_FOR_MEMBERS, - null, - null, - null, - false, - null); - val notification = new Notification(NotificationType.ADMIN_LEFT, null); - - webSocketService.sendNotification(session, notification); - - verify(simpMessagingTemplateMock, times(1)) - .convertAndSendToUser( - defaultMemberPrincipal.getMemberID(), - WebSocketService.NOTIFICATIONS_DESTINATION, - notification); - } - - @Test - public void removeSession_isRemoved() throws Exception { - setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal)); - val session = - new Session( - new ObjectId(), - defaultAdminPrincipal.getSessionID(), - defaultAdminPrincipal.getAdminID(), - null, - null, - List.of(new Member(defaultMemberPrincipal.getMemberID(), null, null, null, null)), - new HashMap<>(), - new ArrayList<>(), - SessionState.WAITING_FOR_MEMBERS, - null, - null, - null, - false, - null); - - webSocketService.removeSession(session); - - assertTrue(webSocketService.getSessionPrincipalList().isEmpty()); - } - - @Test - public void sendMembersHostVoting_sendsHostVotingUpdate() throws Exception { - setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal)); - val session = - new Session( - new ObjectId(), - defaultAdminPrincipal.getSessionID(), - defaultAdminPrincipal.getAdminID(), - null, - null, - List.of(new Member(defaultMemberPrincipal.getMemberID(), null, null, null, null)), - new HashMap<>(), - new ArrayList<>(), - null, - null, - null, - null, - false, - null); - - webSocketService.sendMembersHostVoting(session); - - verify(simpMessagingTemplateMock, times(1)) - .convertAndSendToUser( - defaultMemberPrincipal.getMemberID(), - WebSocketService.MEMBER_UPDATES_HOSTVOTING, - session.getHostVoting()); - } + @Mock + SimpMessagingTemplate simpMessagingTemplateMock; + + @InjectMocks + private WebSocketService webSocketService; + + private final AdminPrincipal defaultAdminPrincipal = new AdminPrincipal(Utils.generateRandomID(), + Utils.generateRandomID()); + + private final MemberPrincipal defaultMemberPrincipal = new MemberPrincipal(defaultAdminPrincipal.getSessionID(), + Utils.generateRandomID()); + + @BeforeEach + public void initEach() { + MockitoAnnotations.openMocks(this); + } + + void setDefaultAdminPrincipal(Set members) throws Exception { + val sessionPrincipalsField = WebSocketService.class.getDeclaredField("sessionPrincipalList"); + sessionPrincipalsField.setAccessible(true); + val sessionPrincipals = List.of( + new SessionPrincipals( + defaultAdminPrincipal.getSessionID(), defaultAdminPrincipal, members)); + sessionPrincipalsField.set(webSocketService, sessionPrincipals); + } + + @Test + public void setAdmin_isAdded() throws Exception { + val adminPrincipal = new AdminPrincipal(Utils.generateRandomID(), Utils.generateRandomID()); + + webSocketService.setAdminUser(adminPrincipal); + + assertTrue( + webSocketService.getSessionPrincipalList().stream() + .anyMatch(p -> p.adminPrincipal() == adminPrincipal)); + } + + @Test + public void setExistingAdmin_isOverwritten() throws Exception { + setDefaultAdminPrincipal(new HashSet<>()); + val adminPrincipal = new AdminPrincipal( + defaultAdminPrincipal.getSessionID(), defaultAdminPrincipal.getAdminID()); + + webSocketService.setAdminUser(adminPrincipal); + + assertTrue( + webSocketService.getSessionPrincipalList().stream() + .anyMatch(p -> p.adminPrincipal() == adminPrincipal)); + } + + @Test + public void getSessionPrincipals_isCorrect() throws Exception { + setDefaultAdminPrincipal(new HashSet<>()); + + val sessionPrincipals = webSocketService.getSessionPrincipals(defaultAdminPrincipal.getSessionID()); + + Assertions.assertEquals( + new SessionPrincipals( + defaultAdminPrincipal.getSessionID(), defaultAdminPrincipal, Set.of()), + sessionPrincipals); + } + + @Test + public void getMissingSessionEntry_isError() throws Exception { + assertThrows( + ResponseStatusException.class, + () -> webSocketService.getSessionPrincipals(defaultAdminPrincipal.getSessionID())); + } + + @Test + public void addMember_isAdded() throws Exception { + setDefaultAdminPrincipal(new HashSet<>()); + val memberPrincipal = new MemberPrincipal(defaultAdminPrincipal.getSessionID(), Utils.generateRandomID()); + + webSocketService.addMemberIfNew(memberPrincipal); + + Assertions.assertTrue( + webSocketService + .getSessionPrincipals(defaultAdminPrincipal.getSessionID()) + .memberPrincipals() + .contains(memberPrincipal)); + } + + @Test + public void addExistingMember_notDuplicate() throws Exception { + setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal)); + + webSocketService.addMemberIfNew(defaultMemberPrincipal); + + Assertions.assertEquals( + 1, + webSocketService + .getSessionPrincipals(defaultAdminPrincipal.getSessionID()) + .memberPrincipals() + .size()); + } + + @Test + public void removeMember_isRemoved() throws Exception { + setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal)); + + webSocketService.removeMember(defaultMemberPrincipal); + + Assertions.assertTrue( + webSocketService + .getSessionPrincipals(defaultAdminPrincipal.getSessionID()) + .memberPrincipals() + .isEmpty()); + } + + @Test + public void sendMembersUpdate_sendsUpdate() throws Exception { + setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal)); + val session = new Session( + new ObjectId(), + defaultAdminPrincipal.getSessionID(), + defaultAdminPrincipal.getAdminID(), + null, + null, + List.of(new Member(defaultMemberPrincipal.getMemberID(), null, null, null, null)), + new HashMap<>(), + new ArrayList<>(), + null, + null, + null, + null, + false, + null); + + webSocketService.sendMembersUpdate(session); + + verify(simpMessagingTemplateMock, times(1)) + .convertAndSendToUser( + defaultAdminPrincipal.getName(), + WebSocketService.MEMBERS_UPDATED_DESTINATION, + new MemberUpdate(session.getMembers(), session.getCurrentHighlights())); + } + + @Test + public void sendSessionState_sendsState() throws Exception { + setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal)); + val session = new Session( + new ObjectId(), + defaultAdminPrincipal.getSessionID(), + defaultAdminPrincipal.getAdminID(), + null, + null, + List.of(new Member(defaultMemberPrincipal.getMemberID(), null, null, null, null)), + new HashMap<>(), + new ArrayList<>(), + SessionState.WAITING_FOR_MEMBERS, + null, + null, + null, + false, + null); + + webSocketService.sendSessionStateToMember(session, defaultMemberPrincipal.getMemberID()); + + verify(simpMessagingTemplateMock, times(1)) + .convertAndSendToUser( + defaultMemberPrincipal.getMemberID(), + WebSocketService.MEMBER_UPDATES_DESTINATION, + session.getSessionState().toString()); + } + + @Test + public void sendSessionStates_sendsToAll() throws Exception { + val memberPrincipal = new MemberPrincipal(defaultAdminPrincipal.getSessionID(), Utils.generateRandomID()); + setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal, memberPrincipal)); + val session = new Session( + new ObjectId(), + defaultAdminPrincipal.getSessionID(), + defaultAdminPrincipal.getAdminID(), + null, + null, + List.of( + new Member(defaultMemberPrincipal.getMemberID(), null, null, null, null), + new Member(memberPrincipal.getMemberID(), null, null, null, null)), + new HashMap<>(), + new ArrayList<>(), + SessionState.WAITING_FOR_MEMBERS, + null, + null, + null, + false, + null); + + webSocketService.sendSessionStateToMembers(session); + + verify(simpMessagingTemplateMock, times(1)) + .convertAndSendToUser( + defaultMemberPrincipal.getMemberID(), + WebSocketService.MEMBER_UPDATES_DESTINATION, + session.getSessionState().toString()); + verify(simpMessagingTemplateMock, times(1)) + .convertAndSendToUser( + memberPrincipal.getMemberID(), + WebSocketService.MEMBER_UPDATES_DESTINATION, + session.getSessionState().toString()); + } + + @Test + public void sendUpdatedUserStoriesToMembers_sendsToAll() throws Exception { + val memberPrincipal = new MemberPrincipal(defaultAdminPrincipal.getSessionID(), Utils.generateRandomID()); + setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal, memberPrincipal)); + val session = new Session( + new ObjectId(), + defaultAdminPrincipal.getSessionID(), + defaultAdminPrincipal.getAdminID(), + new SessionConfig(List.of(), List.of(), null, "US_MANUALLY", "password"), + null, + List.of( + new Member(defaultMemberPrincipal.getMemberID(), null, null, null, null), + new Member(memberPrincipal.getMemberID(), null, null, null, null)), + new HashMap<>(), + new ArrayList<>(), + SessionState.WAITING_FOR_MEMBERS, + null, + null, + null, + false, + null); + + webSocketService.sendUpdatedUserStoriesToMembers(session); + + verify(simpMessagingTemplateMock, times(1)) + .convertAndSendToUser( + defaultMemberPrincipal.getMemberID(), + WebSocketService.US_UPDATES_DESTINATION, + session.getSessionConfig().getUserStories()); + verify(simpMessagingTemplateMock, times(1)) + .convertAndSendToUser( + defaultMemberPrincipal.getMemberID(), + WebSocketService.US_UPDATES_DESTINATION, + session.getSessionConfig().getUserStories()); + } + + @Test + public void adminLeft_sendsNotification() throws Exception { + val memberPrincipal = new MemberPrincipal(defaultAdminPrincipal.getSessionID(), Utils.generateRandomID()); + setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal, memberPrincipal)); + val session = new Session( + new ObjectId(), + defaultAdminPrincipal.getSessionID(), + defaultAdminPrincipal.getAdminID(), + new SessionConfig(List.of(), List.of(), null, "US_MANUALLY", "password"), + null, + List.of( + new Member(defaultMemberPrincipal.getMemberID(), null, null, null, null), + new Member(memberPrincipal.getMemberID(), null, null, null, null)), + new HashMap<>(), + new ArrayList<>(), + SessionState.WAITING_FOR_MEMBERS, + null, + null, + null, + false, + null); + val notification = new Notification(NotificationType.ADMIN_LEFT, null); + + webSocketService.sendNotification(session, notification); + + verify(simpMessagingTemplateMock, times(1)) + .convertAndSendToUser( + defaultMemberPrincipal.getMemberID(), + WebSocketService.NOTIFICATIONS_DESTINATION, + notification); + } + + @Test + public void removeSession_isRemoved() throws Exception { + setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal)); + val session = new Session( + new ObjectId(), + defaultAdminPrincipal.getSessionID(), + defaultAdminPrincipal.getAdminID(), + null, + null, + List.of(new Member(defaultMemberPrincipal.getMemberID(), null, null, null, null)), + new HashMap<>(), + new ArrayList<>(), + SessionState.WAITING_FOR_MEMBERS, + null, + null, + null, + false, + null); + + webSocketService.removeSession(session); + + assertTrue(webSocketService.getSessionPrincipalList().isEmpty()); + } + + @Test + public void sendMembersHostVoting_sendsHostVotingUpdate() throws Exception { + setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal)); + val session = new Session( + new ObjectId(), + defaultAdminPrincipal.getSessionID(), + defaultAdminPrincipal.getAdminID(), + null, + null, + List.of(new Member(defaultMemberPrincipal.getMemberID(), null, null, null, null)), + new HashMap<>(), + new ArrayList<>(), + null, + null, + null, + null, + false, + null); + + webSocketService.sendMembersHostVoting(session); + + verify(simpMessagingTemplateMock, times(1)) + .convertAndSendToUser( + defaultMemberPrincipal.getMemberID(), + WebSocketService.MEMBER_UPDATES_HOSTVOTING, + session.getHostVoting()); + } + + @Test + public void sendMembersHostEstimation_sendsHostEstimationUpdate() throws Exception { + setDefaultAdminPrincipal(Set.of(defaultMemberPrincipal)); + val session = new Session( + new ObjectId(), + defaultAdminPrincipal.getSessionID(), + defaultAdminPrincipal.getAdminID(), + null, + null, + List.of(new Member(defaultMemberPrincipal.getMemberID(), null, null, null, null)), + new HashMap<>(), + new ArrayList<>(), + null, + null, + null, + null, + false, + new AdminVote("XL")); + + webSocketService.sendMembersAdminVote(session); + + verify(simpMessagingTemplateMock, times(1)) + .convertAndSendToUser( + defaultMemberPrincipal.getMemberID(), + WebSocketService.ADMIN_UPDATED_ESTIMATION, + session.getHostEstimation()); + } } From 17386406b1c49554628931b54d7df81775ca30d0 Mon Sep 17 00:00:00 2001 From: SponsoredByPuma <92574150+SponsoredByPuma@users.noreply.github.com> Date: Fri, 31 Mar 2023 11:46:39 +0200 Subject: [PATCH 09/30] add the feature to the documentation --- docs/.vuepress/public/img/host_voting.png | Bin 0 -> 19855 bytes docs/guide/user.md | 7 +++++++ 2 files changed, 7 insertions(+) create mode 100644 docs/.vuepress/public/img/host_voting.png diff --git a/docs/.vuepress/public/img/host_voting.png b/docs/.vuepress/public/img/host_voting.png new file mode 100644 index 0000000000000000000000000000000000000000..e770e7b028298dc7b6f86b419f034532420684a0 GIT binary patch literal 19855 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 literal 0 HcmV?d00001 diff --git a/docs/guide/user.md b/docs/guide/user.md index 6acbb378c..9b4c4e408 100644 --- a/docs/guide/user.md +++ b/docs/guide/user.md @@ -107,6 +107,13 @@ cards once you've started the session. ## Start Session +Once everybody of your team has joined your session, you can start the session. While you are waiting for your team members to join, you can change some settings for your session. + + - The admin of the session can vote + - As the admin of the session you can choose to vote aswell or to only let your members vote on the userstories. + Host Voting + - While the Estimation is running, you cannot change the voting option. + ## Start voting of User Story From 5f31011a3f98e7f175fe31f20c953a9c558f389b Mon Sep 17 00:00:00 2001 From: SponsoredByPuma <92574150+SponsoredByPuma@users.noreply.github.com> Date: Fri, 21 Apr 2023 08:33:57 +0200 Subject: [PATCH 10/30] add button on the prepare Page, changed the design on the button --- frontend/src/locales/de.json | 13 ++- frontend/src/locales/en.json | 13 ++- frontend/src/locales/es.json | 13 ++- frontend/src/locales/fr.json | 13 ++- frontend/src/locales/it.json | 13 ++- frontend/src/locales/pl.json | 13 ++- frontend/src/locales/pt.json | 13 ++- frontend/src/locales/uk.json | 13 ++- frontend/src/model/Session.ts | 2 + frontend/src/views/LandingPage.vue | 25 ++--- frontend/src/views/MemberVotePage.vue | 1 - frontend/src/views/PrepareSessionPage.vue | 64 +++++------ frontend/src/views/SessionPage.vue | 125 ++++++++++------------ 13 files changed, 174 insertions(+), 147 deletions(-) diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 5bb28557f..2c0e2d811 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -50,7 +50,9 @@ "buttons": { "new": "Neu", "result": "Ergebnisse anzeigen", - "finish": "Beende die Session" + "finish": "Beende die Session", + "addHostVoting": "Host darf mitabstimmen", + "removeHostVoting": "Host darf nicht mitabstimmen" } }, "modal": { @@ -234,8 +236,13 @@ "time": { "title": "3. Bestimme die Zeit pro Schätzrunde" }, + "hostVoting": { + "title": "4. Soll der Host auch abstimmen", + "hostVotingOn": "Ja", + "hostVotingOff": "Nein" + }, "password": { - "title": "4. Wähle ein Passwort, falls gewünscht", + "title": "5. Wähle ein Passwort, falls gewünscht", "placeholder": "Passwort (leer lassen für eine offene Session)" } } @@ -273,4 +280,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 6751a3147..00858f937 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -50,7 +50,9 @@ "buttons": { "new": "New", "result": "Show result", - "finish": "Finish session" + "finish": "Finish session", + "addHostVoting": "Enable host voting", + "removeHostVoting": "Disable host voting" } }, "modal": { @@ -234,8 +236,13 @@ "time": { "title": "3. Adjust the time per estimation" }, + "hostVoting": { + "title": "4. Should the host vote aswell", + "hostVotingOn": "Yes", + "hostVotingOff": "No" + }, "password": { - "title": "4. Choose a password if needed", + "title": "5. Choose a password if needed", "placeholder": "Password (leave empty for unprotected session)" } } @@ -273,4 +280,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json index baf84eee1..aa460d05a 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -50,7 +50,9 @@ "buttons": { "new": "Nuevo", "result": "Mostrar resultado", - "finish": "Terminar sesión" + "finish": "Terminar sesión", + "addHostVoting": "Enable host voting", + "removeHostVoting": "Disable host voting" } }, "modal": { @@ -234,8 +236,13 @@ "time": { "title": "3. Ajuste el tiempo por estimación" }, + "hostVoting": { + "title": "4. Soll der Host auch abstimmen", + "hostVotingOn": "Yes", + "hostVotingOff": "No" + }, "password": { - "title": "4. Elija una contraseña si es necesario", + "title": "5. Elija una contraseña si es necesario", "placeholder": "Contraseña (dejar vacío para la sesión sin protección)" } } @@ -273,4 +280,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json index d3d0a0afc..a8d2c247a 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -50,7 +50,9 @@ "buttons": { "new": "Nouveau", "result": "Afficher le résultat", - "finish": "Terminer la session" + "finish": "Terminer la session", + "addHostVoting": "Enable host voting", + "removeHostVoting": "Disable host voting" } }, "modal": { @@ -234,8 +236,13 @@ "time": { "title": "3. Ajuster le temps par estimation" }, + "hostVoting": { + "title": "4. Soll der Host auch abstimmen", + "hostVotingOn": "Yes", + "hostVotingOff": "No" + }, "password": { - "title": "4. Choisissez un mot de passe si nécessaire", + "title": "5. Choisissez un mot de passe si nécessaire", "placeholder": "Mot de passe (laisser vide pour une session non protégée)" } } @@ -273,4 +280,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/locales/it.json b/frontend/src/locales/it.json index 4aa09c657..61bc13bd6 100644 --- a/frontend/src/locales/it.json +++ b/frontend/src/locales/it.json @@ -50,7 +50,9 @@ "buttons": { "new": "Nuovo", "result": "Mostra risultato", - "finish": "Termina sessione" + "finish": "Termina sessione", + "addHostVoting": "Enable host voting", + "removeHostVoting": "Disable host voting" } }, "modal": { @@ -234,8 +236,13 @@ "time": { "title": "3. Regolare il tempo per stima" }, + "hostVoting": { + "title": "4. Soll der Host auch abstimmen", + "hostVotingOn": "Yes", + "hostVotingOff": "No" + }, "password": { - "title": "4. Scegliere una password se necessario", + "title": "5. Scegliere una password se necessario", "placeholder": "Password (lasciare vuota per una sessione non protetta)" } } @@ -273,4 +280,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/locales/pl.json b/frontend/src/locales/pl.json index 96a8f15b9..9ae6813d8 100644 --- a/frontend/src/locales/pl.json +++ b/frontend/src/locales/pl.json @@ -89,7 +89,9 @@ "description": "Przygotuj nową sesję, zaproś członków zespołu i zacznij szacować", "buttons": { "start": { - "label": "Start ..." + "label": "Start ...", + "addHostVoting": "Enable host voting", + "removeHostVoting": "Disable host voting" } } }, @@ -234,8 +236,13 @@ "time": { "title": "3. Dostosuj czas na oszacowanie" }, + "hostVoting": { + "title": "4. Soll der Host auch abstimmen", + "hostVotingOn": "Yes", + "hostVotingOff": "No" + }, "password": { - "title": "4. W razie potrzeby wybierz hasło.", + "title": "5. W razie potrzeby wybierz hasło.", "placeholder": "Hasło (pozostaw puste dla sesji bez zabezpieczenia)" } } @@ -273,4 +280,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/locales/pt.json b/frontend/src/locales/pt.json index b098e172e..25a0aa2fb 100644 --- a/frontend/src/locales/pt.json +++ b/frontend/src/locales/pt.json @@ -50,7 +50,9 @@ "buttons": { "new": "Novidades", "result": "Mostrar resultado", - "finish": "Finalizar a sessão" + "finish": "Finalizar a sessão", + "addHostVoting": "Enable host voting", + "removeHostVoting": "Disable host voting" } }, "modal": { @@ -234,8 +236,13 @@ "time": { "title": "3. Ajuste o tempo por estimativa" }, + "hostVoting": { + "title": "4. Soll der Host auch abstimmen", + "hostVotingOn": "Yes", + "hostVotingOff": "No" + }, "password": { - "title": "4. Escolha uma senha, se necessário", + "title": "5. Escolha uma senha, se necessário", "placeholder": "Senha (deixe em branco para sessão desprotegida)" } } @@ -273,4 +280,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/locales/uk.json b/frontend/src/locales/uk.json index 15994a9be..c81b46445 100644 --- a/frontend/src/locales/uk.json +++ b/frontend/src/locales/uk.json @@ -50,7 +50,9 @@ "buttons": { "new": "Створити", "result": "Показати результат", - "finish": "Завершити сеанс" + "finish": "Завершити сеанс", + "addHostVoting": "Enable host voting", + "removeHostVoting": "Disable host voting" } }, "modal": { @@ -234,8 +236,13 @@ "time": { "title": "3. Відрегулюйте час за оцінку" }, + "hostVoting": { + "title": "4. Soll der Host auch abstimmen", + "hostVotingOn": "Yes", + "hostVotingOff": "No" + }, "password": { - "title": "4. Виберіть пароль, якщо необхідно", + "title": "5. Виберіть пароль, якщо необхідно", "placeholder": "Пароль (залиште порожнім для незахищеної сесії)" } } @@ -273,4 +280,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/src/model/Session.ts b/frontend/src/model/Session.ts index 37dc7caba..fed7f30ee 100644 --- a/frontend/src/model/Session.ts +++ b/frontend/src/model/Session.ts @@ -8,6 +8,8 @@ interface Session { sessionConfig: SessionConfig; sessionState: string; + + hostVoting: string; } export default Session; diff --git a/frontend/src/views/LandingPage.vue b/frontend/src/views/LandingPage.vue index 23cd07fba..c177df688 100644 --- a/frontend/src/views/LandingPage.vue +++ b/frontend/src/views/LandingPage.vue @@ -7,25 +7,15 @@ - - + - + + :button-text="$t('page.landing.meeting.reconnect.buttons.start.label')" :on-click="goToSessionPage" /> @@ -126,6 +116,7 @@ export default Vue.extend({ userStoryMode: string; }; sessionState: string; + hostVoting: string; }; this.sessionWrapper = { session }; } catch (e) { @@ -154,6 +145,7 @@ export default Vue.extend({ timerSecondsString: this.sessionWrapper.session.sessionConfig.timerSeconds.toString(), startNewSessionOnMountedString: this.startNewSessionOnMounted.toString(), userStoryMode: this.sessionWrapper.session.sessionConfig.userStoryMode, + hostVoting: this.sessionWrapper.session.hostVoting, }, }); }, @@ -168,6 +160,7 @@ export default Vue.extend({ background-size: cover; background-repeat: no-repeat; } + .jumbotron { background-color: rgba(255, 255, 255, 0.5); } diff --git a/frontend/src/views/MemberVotePage.vue b/frontend/src/views/MemberVotePage.vue index 0b38dd425..2b63cfe05 100644 --- a/frontend/src/views/MemberVotePage.vue +++ b/frontend/src/views/MemberVotePage.vue @@ -170,7 +170,6 @@ export default Vue.extend({ estimateFinished: false, pauseSession: false, safedHostEstimation: null, - test: "", }; }, computed: { diff --git a/frontend/src/views/PrepareSessionPage.vue b/frontend/src/views/PrepareSessionPage.vue index 23b519422..db41acc58 100644 --- a/frontend/src/views/PrepareSessionPage.vue +++ b/frontend/src/views/PrepareSessionPage.vue @@ -7,45 +7,27 @@ {{ $t("session.prepare.step.selection.mode.title") }} - + - + - + {{ $t("session.prepare.step.selection.mode.description.withUS.importButton") }} - +

{{ $t("session.prepare.step.selection.cardSet.title") }}

- +

{{ $t("session.prepare.step.selection.time.title") }}

@@ -62,6 +44,19 @@ +
+

+ {{ $t("session.prepare.step.selection.hostVoting.title") }} +

+ + + + {{ $t("session.prepare.step.selection.hostVoting.hostVotingOn") }} + + + + {{ $t("session.prepare.step.selection.hostVoting.hostVotingOff") }} + +

{{ $t("session.prepare.step.selection.password.title") }}

@@ -69,21 +64,13 @@ - +
- + {{ $t("session.prepare.button.start") }} @@ -117,6 +104,7 @@ export default Vue.extend({ warningWhenUnderZero: "", tabIndex: 0, isJiraEnabled: constants.isJiraEnabled, + hostVoting: false, }; }, computed: { @@ -185,6 +173,7 @@ export default Vue.extend({ sessionState: string; }; adminCookie: string; + hostVoting: string; }; window.localStorage.setItem("adminCookie", response.adminCookie); this.goToSessionPage(response.session as Session); @@ -202,6 +191,7 @@ export default Vue.extend({ voteSetJson: JSON.stringify(session.sessionConfig.set), sessionState: session.sessionState, userStoryMode: session.sessionConfig.userStoryMode, + hostVoting: this.hostVoting.toString(), }, }); }, diff --git a/frontend/src/views/SessionPage.vue b/frontend/src/views/SessionPage.vue index dba441230..c49a3dc71 100644 --- a/frontend/src/views/SessionPage.vue +++ b/frontend/src/views/SessionPage.vue @@ -1,7 +1,7 @@