From 0c12da84201a7df762b16792a0f28cab05fe9d1b Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Thu, 6 Feb 2025 11:01:45 -0600 Subject: [PATCH 01/62] Moved slack functionality into a new slack directory. --- .../PulseResponseController.java | 87 ++------------ .../PulseSlackCommand.java | 2 +- .../SlackPulseResponseConverter.java | 16 +-- .../SlackSignatureVerifier.java | 2 +- .../slack/SlackSubmissionHandler.java | 110 ++++++++++++++++++ 5 files changed, 129 insertions(+), 88 deletions(-) rename server/src/main/java/com/objectcomputing/checkins/services/{pulseresponse => slack}/PulseSlackCommand.java (97%) rename server/src/main/java/com/objectcomputing/checkins/services/{pulseresponse => slack}/SlackPulseResponseConverter.java (89%) rename server/src/main/java/com/objectcomputing/checkins/services/{pulseresponse => slack}/SlackSignatureVerifier.java (97%) create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java diff --git a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseController.java b/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseController.java index f14b56e87..a7b52bcdd 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseController.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseController.java @@ -1,7 +1,7 @@ package com.objectcomputing.checkins.services.pulseresponse; import com.objectcomputing.checkins.exceptions.NotFoundException; -import com.objectcomputing.checkins.util.form.FormUrlEncodedDecoder; +import com.objectcomputing.checkins.services.slack.SlackSubmissionHandler; import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; import io.micronaut.http.MediaType; @@ -29,8 +29,6 @@ import java.time.LocalDate; import java.util.Set; import java.util.UUID; -import java.util.Map; -import java.nio.charset.StandardCharsets; @Controller("/services/pulse-responses") @ExecuteOn(TaskExecutors.BLOCKING) @@ -38,20 +36,14 @@ public class PulseResponseController { private final PulseResponseService pulseResponseServices; private final MemberProfileServices memberProfileServices; - private final SlackSignatureVerifier slackSignatureVerifier; - private final PulseSlackCommand pulseSlackCommand; - private final SlackPulseResponseConverter slackPulseResponseConverter; + private final SlackSubmissionHandler slackSubmissionHandler; public PulseResponseController(PulseResponseService pulseResponseServices, MemberProfileServices memberProfileServices, - SlackSignatureVerifier slackSignatureVerifier, - PulseSlackCommand pulseSlackCommand, - SlackPulseResponseConverter slackPulseResponseConverter) { + SlackSubmissionHandler slackSubmissionHandler) { this.pulseResponseServices = pulseResponseServices; this.memberProfileServices = memberProfileServices; - this.slackSignatureVerifier = slackSignatureVerifier; - this.pulseSlackCommand = pulseSlackCommand; - this.slackPulseResponseConverter = slackPulseResponseConverter; + this.slackSubmissionHandler = slackSubmissionHandler; } /** @@ -120,25 +112,8 @@ public HttpResponse commandPulseResponse( @Header("X-Slack-Signature") String signature, @Header("X-Slack-Request-Timestamp") String timestamp, @Body String requestBody) { - // Validate the request - if (slackSignatureVerifier.verifyRequest(signature, - timestamp, requestBody)) { - // Convert the request body to a map of values. - FormUrlEncodedDecoder formUrlEncodedDecoder = new FormUrlEncodedDecoder(); - Map body = - formUrlEncodedDecoder.decode(requestBody, - StandardCharsets.UTF_8); - - // Respond to the slack command. - String triggerId = (String)body.get("trigger_id"); - if (pulseSlackCommand.send(triggerId)) { - return HttpResponse.ok(); - } else { - return HttpResponse.status(HttpStatus.INTERNAL_SERVER_ERROR); - } - } else { - return HttpResponse.unauthorized(); - } + return slackSubmissionHandler.commandResponse(signature, + timestamp, requestBody); } @Secured(SecurityRule.IS_ANONYMOUS) @@ -148,53 +123,7 @@ public HttpResponse externalPulseResponse( @Header("X-Slack-Request-Timestamp") String timestamp, @Body String requestBody, HttpRequest request) { - // Validate the request - if (slackSignatureVerifier.verifyRequest(signature, - timestamp, requestBody)) { - // Convert the request body to a map of values. - FormUrlEncodedDecoder formUrlEncodedDecoder = - new FormUrlEncodedDecoder(); - Map body = - formUrlEncodedDecoder.decode(requestBody, - StandardCharsets.UTF_8); - - final String key = "payload"; - if (body.containsKey(key)) { - PulseResponseCreateDTO pulseResponseDTO = - slackPulseResponseConverter.get(memberProfileServices, - (String)body.get(key)); - - // If we receive a null DTO, that means that this is not the - // actual submission of the form. We can just return 200 so - // that Slack knows to continue without error. - if (pulseResponseDTO == null) { - return HttpResponse.ok(); - } - - // Create the pulse response - PulseResponse pulseResponse = - pulseResponseServices.unsecureSave( - new PulseResponse( - pulseResponseDTO.getInternalScore(), - pulseResponseDTO.getExternalScore(), - pulseResponseDTO.getSubmissionDate(), - pulseResponseDTO.getTeamMemberId(), - pulseResponseDTO.getInternalFeelings(), - pulseResponseDTO.getExternalFeelings() - ) - ); - - if (pulseResponse == null) { - return HttpResponse.status(HttpStatus.CONFLICT, - "Already submitted today"); - } else { - return HttpResponse.ok(); - } - } else { - return HttpResponse.unprocessableEntity(); - } - } else { - return HttpResponse.unauthorized(); - } + return slackSubmissionHandler.externalResponse(signature, timestamp, + requestBody, request); } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseSlackCommand.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/PulseSlackCommand.java similarity index 97% rename from server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseSlackCommand.java rename to server/src/main/java/com/objectcomputing/checkins/services/slack/PulseSlackCommand.java index 1c48eb30b..c76afe86a 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseSlackCommand.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/PulseSlackCommand.java @@ -1,4 +1,4 @@ -package com.objectcomputing.checkins.services.pulseresponse; +package com.objectcomputing.checkins.services.slack; import com.objectcomputing.checkins.configuration.CheckInsConfiguration; diff --git a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/SlackPulseResponseConverter.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackPulseResponseConverter.java similarity index 89% rename from server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/SlackPulseResponseConverter.java rename to server/src/main/java/com/objectcomputing/checkins/services/slack/SlackPulseResponseConverter.java index 4e00287cb..c0703e11b 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/SlackPulseResponseConverter.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackPulseResponseConverter.java @@ -1,9 +1,10 @@ -package com.objectcomputing.checkins.services.pulseresponse; +package com.objectcomputing.checkins.services.slack; import com.objectcomputing.checkins.exceptions.BadArgException; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; import com.objectcomputing.checkins.notifications.social_media.SlackSearch; +import com.objectcomputing.checkins.services.pulseresponse.PulseResponseCreateDTO; import jakarta.inject.Singleton; @@ -23,13 +24,15 @@ public class SlackPulseResponseConverter { private static final Logger LOG = LoggerFactory.getLogger(SlackPulseResponseConverter.class); private final SlackSearch slackSearch; + private final MemberProfileServices memberProfileServices; - public SlackPulseResponseConverter(SlackSearch slackSearch) { + public SlackPulseResponseConverter(SlackSearch slackSearch, + MemberProfileServices memberProfileServices) { this.slackSearch = slackSearch; + this.memberProfileServices = memberProfileServices; } - public PulseResponseCreateDTO get( - MemberProfileServices memberProfileServices, String body) { + public PulseResponseCreateDTO get(String body) { try { // Get the map of values from the string body final ObjectMapper mapper = new ObjectMapper(); @@ -47,7 +50,7 @@ public PulseResponseCreateDTO get( // Create the pulse DTO and fill in the values. PulseResponseCreateDTO response = new PulseResponseCreateDTO(); - response.setTeamMemberId(lookupUser(memberProfileServices, map)); + response.setTeamMemberId(lookupUser(map)); response.setSubmissionDate(LocalDate.now()); // Internal Score @@ -111,8 +114,7 @@ private String getMappedValue(Map map, String key1, } } - private UUID lookupUser(MemberProfileServices memberProfileServices, - Map map) { + private UUID lookupUser(Map map) { // Get the user's profile map. Map user = (Map)map.get("user"); diff --git a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/SlackSignatureVerifier.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSignatureVerifier.java similarity index 97% rename from server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/SlackSignatureVerifier.java rename to server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSignatureVerifier.java index 2d95c33bd..6ff2575da 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/SlackSignatureVerifier.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSignatureVerifier.java @@ -1,4 +1,4 @@ -package com.objectcomputing.checkins.services.pulseresponse; +package com.objectcomputing.checkins.services.slack; import com.objectcomputing.checkins.configuration.CheckInsConfiguration; diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java new file mode 100644 index 000000000..bb621cc55 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java @@ -0,0 +1,110 @@ +package com.objectcomputing.checkins.services.slack; + +import com.objectcomputing.checkins.util.form.FormUrlEncodedDecoder; +import com.objectcomputing.checkins.services.pulseresponse.PulseResponse; +import com.objectcomputing.checkins.services.pulseresponse.PulseResponseService; +import com.objectcomputing.checkins.services.pulseresponse.PulseResponseCreateDTO; + +import io.micronaut.http.HttpStatus; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; + +import jakarta.inject.Singleton; + +import java.util.Map; +import java.nio.charset.StandardCharsets; + +@Singleton +public class SlackSubmissionHandler { + private final PulseResponseService pulseResponseServices; + private final SlackSignatureVerifier slackSignatureVerifier; + private final PulseSlackCommand pulseSlackCommand; + private final SlackPulseResponseConverter slackPulseResponseConverter; + + public SlackSubmissionHandler(PulseResponseService pulseResponseServices, + SlackSignatureVerifier slackSignatureVerifier, + PulseSlackCommand pulseSlackCommand, + SlackPulseResponseConverter slackPulseResponseConverter) { + this.pulseResponseServices = pulseResponseServices; + this.slackSignatureVerifier = slackSignatureVerifier; + this.pulseSlackCommand = pulseSlackCommand; + this.slackPulseResponseConverter = slackPulseResponseConverter; + } + + public HttpResponse commandResponse(String signature, + String timestamp, + String requestBody) { + // Validate the request + if (slackSignatureVerifier.verifyRequest(signature, + timestamp, requestBody)) { + // Convert the request body to a map of values. + FormUrlEncodedDecoder formUrlEncodedDecoder = new FormUrlEncodedDecoder(); + Map body = + formUrlEncodedDecoder.decode(requestBody, + StandardCharsets.UTF_8); + + // Respond to the slack command. + String triggerId = (String)body.get("trigger_id"); + if (pulseSlackCommand.send(triggerId)) { + return HttpResponse.ok(); + } else { + return HttpResponse.status(HttpStatus.INTERNAL_SERVER_ERROR); + } + } else { + return HttpResponse.unauthorized(); + } + } + + public HttpResponse externalResponse(String signature, + String timestamp, + String requestBody, + HttpRequest request) { + // Validate the request + if (slackSignatureVerifier.verifyRequest(signature, + timestamp, requestBody)) { + // Convert the request body to a map of values. + FormUrlEncodedDecoder formUrlEncodedDecoder = + new FormUrlEncodedDecoder(); + Map body = + formUrlEncodedDecoder.decode(requestBody, + StandardCharsets.UTF_8); + + final String key = "payload"; + if (body.containsKey(key)) { + PulseResponseCreateDTO pulseResponseDTO = + slackPulseResponseConverter.get((String)body.get(key)); + + // If we receive a null DTO, that means that this is not the + // actual submission of the form. We can just return 200 so + // that Slack knows to continue without error. + if (pulseResponseDTO == null) { + return HttpResponse.ok(); + } + + // Create the pulse response + PulseResponse pulseResponse = + pulseResponseServices.unsecureSave( + new PulseResponse( + pulseResponseDTO.getInternalScore(), + pulseResponseDTO.getExternalScore(), + pulseResponseDTO.getSubmissionDate(), + pulseResponseDTO.getTeamMemberId(), + pulseResponseDTO.getInternalFeelings(), + pulseResponseDTO.getExternalFeelings() + ) + ); + + if (pulseResponse == null) { + return HttpResponse.status(HttpStatus.CONFLICT, + "Already submitted today"); + } else { + return HttpResponse.ok(); + } + } else { + return HttpResponse.unprocessableEntity(); + } + } else { + return HttpResponse.unauthorized(); + } + } +} From 4ede442ed48c46a9e3358ab750aea75f6158afcd Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 7 Feb 2025 12:25:00 -0600 Subject: [PATCH 02/62] Moved handling of Slack submissions, added code to read slack messages, persist automated kudos. --- .../configuration/CheckInsConfiguration.java | 3 + .../social_media/SlackSender.java | 71 +++++++++ .../services/kudos/KudosServices.java | 2 + .../services/kudos/KudosServicesImpl.java | 67 +++++--- .../checkins/services/slack/SlackReader.java | 66 ++++++++ .../slack/SlackSubmissionHandler.java | 100 ++++++++---- .../services/slack/kudos/AutomatedKudos.java | 78 ++++++++++ .../slack/kudos/AutomatedKudosDTO.java | 31 ++++ .../slack/kudos/AutomatedKudosRepository.java | 22 +++ .../slack/kudos/KudosChannelReader.java | 55 +++++++ .../slack/kudos/SlackKudosCreator.java | 146 ++++++++++++++++++ .../kudos/SlackKudosResponseHandler.java | 56 +++++++ .../PulseSlackCommand.java | 6 +- .../SlackPulseResponseConverter.java | 18 +-- server/src/main/resources/application.yml | 1 + .../db/common/V121__automated_kudos_table.sql | 11 ++ .../resources/slack/kudos_slack_blocks.json | 43 ++++++ .../resources/slack/pulse_slack_blocks.json | 1 + .../src/components/kudos/PublicKudosCard.jsx | 8 +- 19 files changed, 711 insertions(+), 74 deletions(-) create mode 100644 server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSender.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/slack/SlackReader.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/AutomatedKudos.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/AutomatedKudosDTO.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/AutomatedKudosRepository.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosResponseHandler.java rename server/src/main/java/com/objectcomputing/checkins/services/slack/{ => pulseresponse}/PulseSlackCommand.java (90%) rename server/src/main/java/com/objectcomputing/checkins/services/slack/{ => pulseresponse}/SlackPulseResponseConverter.java (87%) create mode 100644 server/src/main/resources/db/common/V121__automated_kudos_table.sql create mode 100644 server/src/main/resources/slack/kudos_slack_blocks.json diff --git a/server/src/main/java/com/objectcomputing/checkins/configuration/CheckInsConfiguration.java b/server/src/main/java/com/objectcomputing/checkins/configuration/CheckInsConfiguration.java index d29dedbe8..3e8327af4 100644 --- a/server/src/main/java/com/objectcomputing/checkins/configuration/CheckInsConfiguration.java +++ b/server/src/main/java/com/objectcomputing/checkins/configuration/CheckInsConfiguration.java @@ -82,6 +82,9 @@ public static class SlackConfig { @NotBlank private String signingSecret; + + @NotBlank + private String kudosChannel; } } } diff --git a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSender.java b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSender.java new file mode 100644 index 000000000..b4c7b0875 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSender.java @@ -0,0 +1,71 @@ +package com.objectcomputing.checkins.notifications.social_media; + +import com.objectcomputing.checkins.configuration.CheckInsConfiguration; + +import com.slack.api.Slack; +import com.slack.api.methods.MethodsClient; +import com.slack.api.methods.request.chat.ChatPostMessageRequest; +import com.slack.api.methods.response.chat.ChatPostMessageResponse; +import com.slack.api.methods.request.conversations.ConversationsOpenRequest; +import com.slack.api.methods.response.conversations.ConversationsOpenResponse; + +import jakarta.inject.Singleton; +import jakarta.inject.Inject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +@Singleton +public class SlackSender { + private static final Logger LOG = LoggerFactory.getLogger(SlackSender.class); + + @Inject + private CheckInsConfiguration configuration; + + public boolean send(List userIds, String slackBlocks) { + // See if we have a token. + String token = configuration.getApplication() + .getSlack().getBotToken(); + if (token != null && !slackBlocks.isEmpty()) { + MethodsClient client = Slack.getInstance().methods(token); + + try { + ConversationsOpenResponse openResponse = + client.conversationsOpen(ConversationsOpenRequest.builder() + .users(userIds) + .returnIm(true) + .build()); + if (!openResponse.isOk()) { + LOG.error("Unable to open the conversation"); + return false; + } + + ChatPostMessageRequest request = ChatPostMessageRequest + .builder() + .channel(openResponse.getChannel().getId()) + .blocksAsString(slackBlocks) + .text("This is a test") + .build(); + + // Send it to Slack + ChatPostMessageResponse response = client.chatPostMessage(request); + + if (!response.isOk()) { + LOG.error("Unable to send the chat message: " + + response.getError()); + } + + return response.isOk(); + } catch(Exception ex) { + LOG.error("SlackSender.send: " + ex.toString()); + return false; + } + } else { + LOG.error("Missing token or missing slack blocks"); + return false; + } + } +} + diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServices.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServices.java index 72cbf4f96..8b22af16b 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServices.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServices.java @@ -11,6 +11,8 @@ public interface KudosServices { Kudos approve(Kudos kudos); + Kudos savePreapproved(KudosCreateDTO kudos); + List getRecent(); KudosResponseDTO getById(UUID id); diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java index 27de524e4..a59070784 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java @@ -93,31 +93,7 @@ private enum NotificationType { @Transactional @RequiredPermission(Permission.CAN_CREATE_KUDOS) public Kudos save(KudosCreateDTO kudosDTO) { - UUID senderId = kudosDTO.getSenderId(); - if (memberProfileRetrievalServices.getById(senderId).isEmpty()) { - throw new BadArgException("Kudos sender %s does not exist".formatted(senderId)); - } - - if (kudosDTO.getTeamId() != null) { - UUID teamId = kudosDTO.getTeamId(); - if (teamRepository.findById(teamId).isEmpty()) { - throw new BadArgException("Team %s does not exist".formatted(teamId)); - } - } - - if (kudosDTO.getRecipientMembers() == null || kudosDTO.getRecipientMembers().isEmpty()) { - throw new BadArgException("Kudos must contain at least one recipient"); - } - - Kudos kudos = new Kudos(kudosDTO); - - Kudos savedKudos = kudosRepository.save(kudos); - - for (MemberProfile recipient : kudosDTO.getRecipientMembers()) { - KudosRecipient kudosRecipient = new KudosRecipient(savedKudos.getId(), recipient.getId()); - kudosRecipientServices.save(kudosRecipient); - } - + Kudos savedKudos = saveCommon(kudosDTO, true); sendNotification(savedKudos, NotificationType.creation); return savedKudos; } @@ -140,6 +116,13 @@ public Kudos approve(Kudos kudos) { return updated; } + @Override + public Kudos savePreapproved(KudosCreateDTO kudos) { + Kudos savedKudos = saveCommon(kudos, false); + savedKudos.setDateApproved(LocalDate.now()); + return kudosRepository.update(savedKudos); + } + @Override public KudosResponseDTO getById(UUID id) { @@ -387,4 +370,38 @@ private void slackApprovedKudos(Kudos kudos) { private boolean hasAdministerKudosPermission() { return currentUserServices.hasPermission(Permission.CAN_ADMINISTER_KUDOS); } + + private Kudos saveCommon(KudosCreateDTO kudosDTO, boolean verifyAndNotify) { + UUID senderId = kudosDTO.getSenderId(); + if (memberProfileRetrievalServices.getById(senderId).isEmpty()) { + throw new BadArgException("Kudos sender %s does not exist".formatted(senderId)); + } + + if (kudosDTO.getTeamId() != null) { + UUID teamId = kudosDTO.getTeamId(); + if (teamRepository.findById(teamId).isEmpty()) { + throw new BadArgException("Team %s does not exist".formatted(teamId)); + } + } + + if (kudosDTO.getRecipientMembers() == null || kudosDTO.getRecipientMembers().isEmpty()) { + throw new BadArgException("Kudos must contain at least one recipient"); + } + + Kudos savedKudos = kudosRepository.save(new Kudos(kudosDTO)); + + for (MemberProfile recipient : kudosDTO.getRecipientMembers()) { + KudosRecipient kudosRecipient = new KudosRecipient(savedKudos.getId(), recipient.getId()); + if (verifyAndNotify) { + // Going through the service verifies the sender and recipient + // and sends email notification after saving. + kudosRecipientServices.save(kudosRecipient); + } else { + // This does none of that and just stores it in the database. + kudosRecipientRepository.save(kudosRecipient); + } + } + + return savedKudos; + } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackReader.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackReader.java new file mode 100644 index 000000000..a880ed9af --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackReader.java @@ -0,0 +1,66 @@ +package com.objectcomputing.checkins.services.slack; + +import com.objectcomputing.checkins.configuration.CheckInsConfiguration; + +import com.slack.api.Slack; +import com.slack.api.model.Message; +import com.slack.api.model.Conversation; +import com.slack.api.methods.MethodsClient; +import com.slack.api.methods.SlackApiException; +import com.slack.api.methods.request.conversations.ConversationsHistoryRequest; +import com.slack.api.methods.response.conversations.ConversationsHistoryResponse; + +import jakarta.inject.Singleton; +import jakarta.inject.Inject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.ArrayList; +import java.io.IOException; +import java.time.ZoneOffset; +import java.time.LocalDateTime; +import java.time.ZoneId; + +@Singleton +public class SlackReader { + private static final Logger LOG = LoggerFactory.getLogger(SlackReader.class); + + @Inject + private CheckInsConfiguration configuration; + + public List read(String channelId, LocalDateTime last) { + String token = configuration.getApplication().getSlack().getBotToken(); + if (token != null) { + try { + long ts = last.atZone(ZoneId.systemDefault()) + .toInstant().getEpochSecond(); + String timestamp = String.valueOf(ts); + MethodsClient client = Slack.getInstance().methods(token); + ConversationsHistoryResponse response = + client.conversationsHistory( + ConversationsHistoryRequest.builder() + .channel(channelId) + .oldest(timestamp) + .inclusive(true) + .build()); + + if (response.isOk()) { + return response.getMessages(); + } else { + LOG.error("Slack Response: " + response.getError() + + " - " + response.getNeeded()); + } + } catch(IOException e) { + LOG.error("SlackReader.read: " + e.toString()); + } catch(SlackApiException e) { + LOG.error("SlackReader.read: " + e.toString()); + } + } else { + LOG.error("Slack Token not available"); + } + return new ArrayList(); + } +} + diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java index bb621cc55..984dc3320 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java @@ -1,5 +1,8 @@ package com.objectcomputing.checkins.services.slack; +import com.objectcomputing.checkins.services.slack.pulseresponse.PulseSlackCommand; +import com.objectcomputing.checkins.services.slack.pulseresponse.SlackPulseResponseConverter; + import com.objectcomputing.checkins.util.form.FormUrlEncodedDecoder; import com.objectcomputing.checkins.services.pulseresponse.PulseResponse; import com.objectcomputing.checkins.services.pulseresponse.PulseResponseService; @@ -11,6 +14,10 @@ import jakarta.inject.Singleton; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.core.JsonProcessingException; + import java.util.Map; import java.nio.charset.StandardCharsets; @@ -71,40 +78,73 @@ public HttpResponse externalResponse(String signature, final String key = "payload"; if (body.containsKey(key)) { - PulseResponseCreateDTO pulseResponseDTO = - slackPulseResponseConverter.get((String)body.get(key)); - - // If we receive a null DTO, that means that this is not the - // actual submission of the form. We can just return 200 so - // that Slack knows to continue without error. - if (pulseResponseDTO == null) { - return HttpResponse.ok(); + try { + final ObjectMapper mapper = new ObjectMapper(); + final Map map = + mapper.readValue((String)body.get(key), + new TypeReference<>() {}); + if (isPulseSubmission(map)) { + return completePulse(map); + } + } catch(JsonProcessingException ex) { + // Fall through to the bottom... } - - // Create the pulse response - PulseResponse pulseResponse = - pulseResponseServices.unsecureSave( - new PulseResponse( - pulseResponseDTO.getInternalScore(), - pulseResponseDTO.getExternalScore(), - pulseResponseDTO.getSubmissionDate(), - pulseResponseDTO.getTeamMemberId(), - pulseResponseDTO.getInternalFeelings(), - pulseResponseDTO.getExternalFeelings() - ) - ); - - if (pulseResponse == null) { - return HttpResponse.status(HttpStatus.CONFLICT, - "Already submitted today"); - } else { - return HttpResponse.ok(); - } - } else { - return HttpResponse.unprocessableEntity(); } } else { return HttpResponse.unauthorized(); } + + return HttpResponse.unprocessableEntity(); + } + + private boolean isPulseSubmission(Map map) { + final String typeKey = "type"; + if (map.containsKey(typeKey)) { + final String type = (String)map.get(typeKey); + if (type.equals("view_submission")) { + final String viewKey = "view"; + if (map.containsKey(viewKey)) { + final Map view = + (Map)map.get(viewKey); + final String callbackKey = "callback_id"; + if (view.containsKey(callbackKey)) { + return "pulseSubmission".equals(view.get(callbackKey)); + } + } + } + } + return false; + } + + private HttpResponse completePulse(Map map) { + PulseResponseCreateDTO pulseResponseDTO = + slackPulseResponseConverter.get(map); + + // If we receive a null DTO, that means that this is not the actual + // submission of the form. We can just return 200 so that Slack knows + // to continue without error. Realy, this should not happen. But, just + // in case... + if (pulseResponseDTO != null) { + // Create the pulse response + PulseResponse pulseResponse = + pulseResponseServices.unsecureSave( + new PulseResponse( + pulseResponseDTO.getInternalScore(), + pulseResponseDTO.getExternalScore(), + pulseResponseDTO.getSubmissionDate(), + pulseResponseDTO.getTeamMemberId(), + pulseResponseDTO.getInternalFeelings(), + pulseResponseDTO.getExternalFeelings() + ) + ); + + if (pulseResponse == null) { + // If pulse response is null, that means that this user has + // already submitted a response today. + return HttpResponse.status(HttpStatus.CONFLICT, + "Already submitted today"); + } + } + return HttpResponse.ok(); } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/AutomatedKudos.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/AutomatedKudos.java new file mode 100644 index 000000000..a21d79341 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/AutomatedKudos.java @@ -0,0 +1,78 @@ +package com.objectcomputing.checkins.services.slack.kudos; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.data.annotation.AutoPopulated; +import io.micronaut.data.annotation.TypeDef; +import io.micronaut.data.annotation.sql.ColumnTransformer; +import io.micronaut.data.model.DataType; +import io.swagger.v3.oas.annotations.media.Schema; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@Entity +@Getter +@Setter +@Introspected +@Table(name = "automated_kudos") +public class AutomatedKudos { + @Id + @Column(name = "id") + @AutoPopulated + @TypeDef(type = DataType.STRING) + @Schema(description = "the id of the kudos") + private UUID id; + + @Column(name = "requested") + @NotNull + @Schema(description = "Has permission been requested of the poster") + private Boolean requested; + + @NotBlank + @Column(name = "message") + @ColumnTransformer(read = "pgp_sym_decrypt(message::bytea, '${aes.key}')", write = "pgp_sym_encrypt(?, '${aes.key}')") + @Schema(description = "message describing the kudos") + private String message; + + @NotBlank + @Column(name = "externalid") + @ColumnTransformer(read = "pgp_sym_decrypt(message::bytea, '${aes.key}')", write = "pgp_sym_encrypt(?, '${aes.key}')") + @Schema(description = "the external id of the sender") + private String externalId; + + @NotNull + @Column(name = "senderid") + @TypeDef(type = DataType.STRING) + @Schema(description = "id of the user who gave the kudos") + private UUID senderId; + + @Column(name = "recipientids") + @TypeDef(type = DataType.STRING_ARRAY) + @Schema(description = "UUIDs of the recipients") + private List recipientIds; + + // This is necessary for Micronaut to persist instances of this class. + AutomatedKudos() {} + + AutomatedKudos(AutomatedKudosDTO automatedKudosDTO) { + this.requested = false; + this.message = automatedKudosDTO.getMessage(); + this.externalId = automatedKudosDTO.getExternalId(); + this.senderId = automatedKudosDTO.getSenderId(); + this.recipientIds = automatedKudosDTO.getRecipientIds() + .stream() + .map(UUID::toString) + .collect(Collectors.toList()); + } +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/AutomatedKudosDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/AutomatedKudosDTO.java new file mode 100644 index 000000000..d8b80d068 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/AutomatedKudosDTO.java @@ -0,0 +1,31 @@ +package com.objectcomputing.checkins.services.slack.kudos; + +import io.micronaut.core.annotation.Introspected; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; +import java.util.UUID; + +@Getter +@Setter +@AllArgsConstructor +@Introspected +public class AutomatedKudosDTO { + + @NotBlank + private String message; + + @NotNull + private String externalId; + + @NotNull + private UUID senderId; + + @NotNull + private List recipientIds; +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/AutomatedKudosRepository.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/AutomatedKudosRepository.java new file mode 100644 index 000000000..224e0e936 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/AutomatedKudosRepository.java @@ -0,0 +1,22 @@ +package com.objectcomputing.checkins.services.slack.kudos; + +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.repository.CrudRepository; +import io.micronaut.data.annotation.Query; + +import java.util.List; +import java.util.UUID; + +@JdbcRepository(dialect = Dialect.POSTGRES) +public interface AutomatedKudosRepository extends CrudRepository { + @Query(value = """ + SELECT + id, requested, + PGP_SYM_DECRYPT(cast(message as bytea), '${aes.key}') as message, + PGP_SYM_DECRYPT(cast(externalid as bytea), '${aes.key}') as externalid, + senderid, recipientids + FROM automated_kudos + WHERE requested IS FALSE""", nativeQuery = true) + List getUnrequested(); +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java new file mode 100644 index 000000000..c7c53849b --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java @@ -0,0 +1,55 @@ +package com.objectcomputing.checkins.services.slack.kudos; + +import com.objectcomputing.checkins.services.slack.SlackReader; +import com.objectcomputing.checkins.configuration.CheckInsConfiguration; + +import com.slack.api.model.Message; + +import io.micronaut.scheduling.annotation.Scheduled; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.inject.Singleton; +import jakarta.inject.Inject; + +import java.util.List; +import java.time.LocalDateTime; + +@Singleton +public class KudosChannelReader { + private static final Logger LOG = LoggerFactory.getLogger(KudosChannelReader.class); + + private static LocalDateTime lastImport = null; + + @Inject + private CheckInsConfiguration configuration; + + @Inject + private SlackReader slackReader; + + @Inject + private SlackKudosCreator slackKudosCreator; + + @Scheduled(fixedDelay = "1m") + public void readChannel() { + if (lastImport == null) { + lastImport = LocalDateTime.now(); + } + + String channelId = configuration.getApplication() + .getSlack().getKudosChannel(); + List messages = slackReader.read(channelId, lastImport); + updateLastImportTime(); + + slackKudosCreator.store(messages); + } + + private LocalDateTime getLastImportTime() { + return lastImport; + } + + private void updateLastImportTime() { + lastImport = LocalDateTime.now(); + } +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java new file mode 100644 index 000000000..3b927759a --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java @@ -0,0 +1,146 @@ +package com.objectcomputing.checkins.services.slack.kudos; + +import com.objectcomputing.checkins.notifications.social_media.SlackSearch; +import com.objectcomputing.checkins.notifications.social_media.SlackSender; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileUtils; +import com.objectcomputing.checkins.services.memberprofile.MemberProfile; +import com.objectcomputing.checkins.exceptions.NotFoundException; + +import org.apache.commons.lang3.StringEscapeUtils; + +import com.slack.api.model.Message; + +import io.micronaut.context.annotation.Value; +import io.micronaut.core.io.Readable; +import io.micronaut.core.io.IOUtils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.inject.Singleton; +import jakarta.inject.Inject; + +import java.util.UUID; +import java.util.List; +import java.util.ArrayList; +import java.util.regex.Pattern; +import java.util.regex.Matcher; +import java.lang.StringBuffer; +import java.io.BufferedReader; + +@Singleton +public class SlackKudosCreator { + private static final Logger LOG = LoggerFactory.getLogger(SlackKudosCreator.class); + + @Inject + private SlackSearch slackSearch; + + @Inject + private SlackSender slackSender; + + @Inject + private AutomatedKudosRepository automatedKudosRepository; + + @Inject + private MemberProfileServices memberProfileServices; + + @Value("classpath:slack/kudos_slack_blocks.json") + private Readable kudosSlackBlocks; + + public void store(List messages) { + for (Message message : messages) { + if (message.getSubtype() == null && + message.getText().toLowerCase().contains("kudos")) { + try { + AutomatedKudosDTO kudosDTO = createFromMessage(message); + if (kudosDTO.getRecipientIds().size() == 0) { + LOG.warn("Unable to extract recipients from message"); + LOG.warn(message.getText()); + } else { + automatedKudosRepository.save( + new AutomatedKudos(kudosDTO)); + } + } catch (Exception ex) { + LOG.error("store: " + ex.toString()); + } + } + } + + requestAction(); + } + + private AutomatedKudosDTO createFromMessage(Message message) { + String userId = message.getUser(); + MemberProfile sender = lookupUser(userId); + List recipients = new ArrayList<>(); + String text = processText(message.getText(), recipients); + return new AutomatedKudosDTO(text, userId, sender.getId(), recipients); + } + + private MemberProfile lookupUser(String userId) { + if (userId == null) { + throw new NotFoundException("User Id is not present"); + } + + String email = slackSearch.findUserEmail(userId); + if (email == null) { + throw new NotFoundException( + "Could not find an email address for " + userId); + } + return memberProfileServices.findByWorkEmail(email); + } + + private String processText(String text, List recipients) { + StringBuffer buffer = new StringBuffer(text.length()); + Pattern userRef = Pattern.compile("<@([^>]+)>"); + Matcher action = userRef.matcher(StringEscapeUtils.unescapeHtml4(text)); + while (action.find()) { + // Pull out the recipient user id, get the profile and add it to + // the list of recipients. + String userId = action.group(1); + MemberProfile profile = lookupUser(userId); + recipients.add(profile.getId()); + + // Replace the user reference with their full name. + action.appendReplacement(buffer, Matcher.quoteReplacement( + MemberProfileUtils.getFullName(profile))); + } + action.appendTail(buffer); + return buffer.toString(); + } + + private void requestAction() { + for (AutomatedKudos kudos : automatedKudosRepository.getUnrequested()) { + try { + // Create the slack blocks, inserting the kudos UUID as the + // block id. + String blocks = getSlackBlocks(kudos.getId().toString(), + kudos.getMessage()); + + // Send the message to the sender of the kudos + List userIds = new ArrayList<>(); + userIds.add(kudos.getExternalId()); + if (slackSender.send(userIds, blocks)) { + // If the message was sent, set the requested flag and + // update the repository. + kudos.setRequested(true); + automatedKudosRepository.update(kudos); + } + } catch (Exception ex) { + LOG.error("requestAction: " + ex.toString()); + } + } + } + + private String getSlackBlocks(String kudosUUID, String contents) { + try { + return String.format(IOUtils.readText( + new BufferedReader(kudosSlackBlocks.asReader())), + kudosUUID, contents); + } catch(Exception ex) { + LOG.error("SlackKudosCreator.getSlackBlocks: " + ex.toString()); + return ""; + } + } +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosResponseHandler.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosResponseHandler.java new file mode 100644 index 000000000..ca48c4eb6 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosResponseHandler.java @@ -0,0 +1,56 @@ +package com.objectcomputing.checkins.services.slack.kudos; + +import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; +import com.objectcomputing.checkins.services.memberprofile.MemberProfile; +import com.objectcomputing.checkins.services.kudos.KudosCreateDTO; +import com.objectcomputing.checkins.services.kudos.KudosServices; + +import jakarta.inject.Singleton; +import jakarta.inject.Inject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.UUID; +import java.util.List; +import java.util.ArrayList; +import java.util.Optional; + +@Singleton +public class SlackKudosResponseHandler { + private static final Logger LOG = LoggerFactory.getLogger(SlackKudosResponseHandler.class); + + @Inject + private KudosServices kudosServices; + + @Inject + private AutomatedKudosRepository automatedKudosRepository; + + @Inject + private MemberProfileServices memberProfileServices; + + public void store(UUID automatedKudosId) { + Optional found = + automatedKudosRepository.findById(automatedKudosId); + if (found.isPresent()) { + AutomatedKudos kudos = found.get(); + List recipients = new ArrayList<>(); + for (String recipientId : kudos.getRecipientIds()) { + recipients.add(memberProfileServices.getById( + UUID.fromString(recipientId))); + } + KudosCreateDTO dto = + new KudosCreateDTO(kudos.getMessage(), kudos.getSenderId(), + null, true, recipients); + kudosServices.savePreapproved(dto); + automatedKudosRepository.deleteById(automatedKudosId); + } else { + LOG.error("Unable to find automated kudos: " + + automatedKudosId.toString()); + } + } + + public void remove(UUID automatedKudosId) { + automatedKudosRepository.deleteById(automatedKudosId); + } +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/PulseSlackCommand.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/pulseresponse/PulseSlackCommand.java similarity index 90% rename from server/src/main/java/com/objectcomputing/checkins/services/slack/PulseSlackCommand.java rename to server/src/main/java/com/objectcomputing/checkins/services/slack/pulseresponse/PulseSlackCommand.java index c76afe86a..85f9d79fa 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/slack/PulseSlackCommand.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/pulseresponse/PulseSlackCommand.java @@ -1,4 +1,4 @@ -package com.objectcomputing.checkins.services.slack; +package com.objectcomputing.checkins.services.slack.pulseresponse; import com.objectcomputing.checkins.configuration.CheckInsConfiguration; @@ -54,7 +54,7 @@ public boolean send(String triggerId) { return response.isOk(); } catch(Exception ex) { - LOG.error(ex.toString()); + LOG.error("PulseSlackCommand.send: " + ex.toString()); return false; } } else { @@ -68,7 +68,7 @@ private String getSlackBlocks() { return IOUtils.readText( new BufferedReader(pulseSlackBlocks.asReader())); } catch(Exception ex) { - LOG.error(ex.toString()); + LOG.error("PulseSlackCommand.getSlackBlocks: " + ex.toString()); return ""; } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackPulseResponseConverter.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/pulseresponse/SlackPulseResponseConverter.java similarity index 87% rename from server/src/main/java/com/objectcomputing/checkins/services/slack/SlackPulseResponseConverter.java rename to server/src/main/java/com/objectcomputing/checkins/services/slack/pulseresponse/SlackPulseResponseConverter.java index c0703e11b..54508287d 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackPulseResponseConverter.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/pulseresponse/SlackPulseResponseConverter.java @@ -1,4 +1,4 @@ -package com.objectcomputing.checkins.services.slack; +package com.objectcomputing.checkins.services.slack.pulseresponse; import com.objectcomputing.checkins.exceptions.BadArgException; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; @@ -11,10 +11,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.core.JsonProcessingException; - import java.util.Map; import java.util.UUID; import java.time.LocalDate; @@ -32,14 +28,9 @@ public SlackPulseResponseConverter(SlackSearch slackSearch, this.memberProfileServices = memberProfileServices; } - public PulseResponseCreateDTO get(String body) { + public PulseResponseCreateDTO get(Map map) { try { - // Get the map of values from the string body - final ObjectMapper mapper = new ObjectMapper(); - final Map map = - mapper.readValue(body, new TypeReference<>() {}); final String type = (String)map.get("type"); - if (type.equals("view_submission")) { final Map view = (Map)map.get("view"); @@ -81,11 +72,8 @@ public PulseResponseCreateDTO get(String body) { // response. return null; } - } catch(JsonProcessingException ex) { - LOG.error(ex.getMessage()); - throw new BadArgException(ex.getMessage()); } catch(NumberFormatException ex) { - LOG.error(ex.getMessage()); + LOG.error("SlackPulseResponseConverter.get: " + ex.getMessage()); throw new BadArgException("Pulse scores must be integers"); } } diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml index 469d84a58..23ea93a92 100755 --- a/server/src/main/resources/application.yml +++ b/server/src/main/resources/application.yml @@ -101,6 +101,7 @@ check-ins: webhook-url: ${ SLACK_WEBHOOK_URL } bot-token: ${ SLACK_BOT_TOKEN } signing-secret: ${ SLACK_SIGNING_SECRET } + kudos-channel: ${ SLACK_KUDOS_CHANNEL_ID } web-address: ${ WEB_ADDRESS } --- flyway: diff --git a/server/src/main/resources/db/common/V121__automated_kudos_table.sql b/server/src/main/resources/db/common/V121__automated_kudos_table.sql new file mode 100644 index 000000000..5d8943ad2 --- /dev/null +++ b/server/src/main/resources/db/common/V121__automated_kudos_table.sql @@ -0,0 +1,11 @@ +DROP TABLE IF EXISTS automated_kudos; + +CREATE TABLE automated_kudos +( + id varchar PRIMARY KEY, + requested boolean, + message varchar, + externalid varchar, + senderid varchar REFERENCES member_profile (id), + recipientids varchar[] +); diff --git a/server/src/main/resources/slack/kudos_slack_blocks.json b/server/src/main/resources/slack/kudos_slack_blocks.json new file mode 100644 index 000000000..19558fc32 --- /dev/null +++ b/server/src/main/resources/slack/kudos_slack_blocks.json @@ -0,0 +1,43 @@ +[ + { + "type": "section", + "block_id": "%s", + "text": { + "type": "plain_text", + "text": "Would you like to automatically add this Kudos to Check-Ins?" + } + }, + { + "type": "section", + "text": { + "type": "plain_text", + "text": "%s" + } + }, + { + "type": "divider" + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "action_id": "yes_button", + "text": { + "type": "plain_text", + "text": "Yes" + }, + "value": "yes" + }, + { + "type": "button", + "action_id": "no_button", + "text": { + "type": "plain_text", + "text": "No" + }, + "value": "no" + } + ] + } +] diff --git a/server/src/main/resources/slack/pulse_slack_blocks.json b/server/src/main/resources/slack/pulse_slack_blocks.json index ad0f7342a..6ef6b9d43 100644 --- a/server/src/main/resources/slack/pulse_slack_blocks.json +++ b/server/src/main/resources/slack/pulse_slack_blocks.json @@ -1,5 +1,6 @@ { "type": "modal", + "callback_id": "pulseSubmission", "title": { "type": "plain_text", "text": "Check-Ins Pulse", diff --git a/web-ui/src/components/kudos/PublicKudosCard.jsx b/web-ui/src/components/kudos/PublicKudosCard.jsx index dc554602d..0ba037348 100644 --- a/web-ui/src/components/kudos/PublicKudosCard.jsx +++ b/web-ui/src/components/kudos/PublicKudosCard.jsx @@ -98,7 +98,13 @@ const KudosCard = ({ kudos }) => { subheaderTypographyProps={{variant:"subtitle1"}} /> - {kudos.message} +
+ {kudos.message.split('\n').map((line, index) => ( + + {line} + + ))} +
{kudos.recipientTeam && ( {kudos.recipientMembers.map((member) => ( From 6ada3135edfb178d937f333ac11f5a8b2bbc8ecd Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 7 Feb 2025 14:05:00 -0600 Subject: [PATCH 03/62] Process the response from users prompt asking to post kudos in Check-Ins. --- .../currentuser/CurrentUserServicesImpl.java | 13 +++++-- .../slack/SlackSubmissionHandler.java | 31 ++++++++++++++- .../slack/kudos/KudosChannelReader.java | 2 +- .../kudos/SlackKudosResponseHandler.java | 38 ++++++++++++++++--- 4 files changed, 73 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServicesImpl.java index 84af6e3cb..75784e5a7 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServicesImpl.java @@ -57,7 +57,7 @@ public boolean hasRole(RoleType role) { @Override public boolean hasPermission(Permission permission) { - MemberProfile currentUser = getCurrentUser(); + MemberProfile currentUser = getCurrentUserImpl(); if (currentUser == null) { return false; } @@ -72,7 +72,7 @@ public boolean isAdmin() { return hasRole(RoleType.ADMIN); } - public MemberProfile getCurrentUser() { + private MemberProfile getCurrentUserImpl() { if (securityService != null) { Optional auth = securityService.getAuthentication(); if (auth.isPresent() && auth.get().getAttributes().get("email") != null) { @@ -80,8 +80,15 @@ public MemberProfile getCurrentUser() { return memberProfileRepo.findByWorkEmail(workEmail).orElse(null); } } + return null; + } - throw new NotFoundException("No active members in the system"); + public MemberProfile getCurrentUser() { + MemberProfile profile = getCurrentUserImpl(); + if (profile == null) { + throw new NotFoundException("No active members in the system"); + } + return profile; } private MemberProfile saveNewUser(String firstName, String lastName, String workEmail) { diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java index 984dc3320..fc34b3a22 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java @@ -2,6 +2,7 @@ import com.objectcomputing.checkins.services.slack.pulseresponse.PulseSlackCommand; import com.objectcomputing.checkins.services.slack.pulseresponse.SlackPulseResponseConverter; +import com.objectcomputing.checkins.services.slack.kudos.SlackKudosResponseHandler; import com.objectcomputing.checkins.util.form.FormUrlEncodedDecoder; import com.objectcomputing.checkins.services.pulseresponse.PulseResponse; @@ -23,19 +24,24 @@ @Singleton public class SlackSubmissionHandler { + private final String typeKey = "type"; + private final PulseResponseService pulseResponseServices; private final SlackSignatureVerifier slackSignatureVerifier; private final PulseSlackCommand pulseSlackCommand; private final SlackPulseResponseConverter slackPulseResponseConverter; + private final SlackKudosResponseHandler slackKudosResponseHandler; public SlackSubmissionHandler(PulseResponseService pulseResponseServices, SlackSignatureVerifier slackSignatureVerifier, PulseSlackCommand pulseSlackCommand, - SlackPulseResponseConverter slackPulseResponseConverter) { + SlackPulseResponseConverter slackPulseResponseConverter, + SlackKudosResponseHandler slackKudosResponseHandler) { this.pulseResponseServices = pulseResponseServices; this.slackSignatureVerifier = slackSignatureVerifier; this.pulseSlackCommand = pulseSlackCommand; this.slackPulseResponseConverter = slackPulseResponseConverter; + this.slackKudosResponseHandler = slackKudosResponseHandler; } public HttpResponse commandResponse(String signature, @@ -85,6 +91,8 @@ public HttpResponse externalResponse(String signature, new TypeReference<>() {}); if (isPulseSubmission(map)) { return completePulse(map); + } else if (isKudosSubmission(map)) { + return completeKudos(map); } } catch(JsonProcessingException ex) { // Fall through to the bottom... @@ -98,7 +106,6 @@ public HttpResponse externalResponse(String signature, } private boolean isPulseSubmission(Map map) { - final String typeKey = "type"; if (map.containsKey(typeKey)) { final String type = (String)map.get(typeKey); if (type.equals("view_submission")) { @@ -147,4 +154,24 @@ private HttpResponse completePulse(Map map) { } return HttpResponse.ok(); } + + private boolean isKudosSubmission(Map map) { + if (map.containsKey(typeKey)) { + final String type = (String)map.get(typeKey); + if (type.equals("block_actions")) { + final String actionKey = "actions"; + return map.containsKey(actionKey); + } + } + return false; + } + + private HttpResponse completeKudos(Map map) { + if (slackKudosResponseHandler.handle(map)) { + return HttpResponse.ok(); + } else { + // Something was wrong and we were not able to handle this. + return HttpResponse.unprocessableEntity(); + } + } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java index c7c53849b..d0419f5e3 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java @@ -31,7 +31,7 @@ public class KudosChannelReader { @Inject private SlackKudosCreator slackKudosCreator; - @Scheduled(fixedDelay = "1m") + @Scheduled(fixedDelay = "10m") public void readChannel() { if (lastImport == null) { lastImport = LocalDateTime.now(); diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosResponseHandler.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosResponseHandler.java index ca48c4eb6..179a969f6 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosResponseHandler.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosResponseHandler.java @@ -11,6 +11,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Map; import java.util.UUID; import java.util.List; import java.util.ArrayList; @@ -29,7 +30,38 @@ public class SlackKudosResponseHandler { @Inject private MemberProfileServices memberProfileServices; - public void store(UUID automatedKudosId) { + public boolean handle(Map map) { + try { + // Get the blocks out of the message so that we can grab the + // automated kudos id. + Map message = + (Map)map.get("message"); + List blocks = (List)message.get("blocks"); + if (blocks.size() > 0) { + Map first = (Map)blocks.get(0); + String id = (String)first.get("block_id"); + UUID uuid = UUID.fromString(id); + + List actions = (List)map.get("actions"); + if (actions.size() > 0) { + Map entry = + (Map)actions.get(0); + String actionId = (String)entry.get("action_id"); + if (actionId.equals("yes_button")) { + store(uuid); + } else { + automatedKudosRepository.deleteById(uuid); + } + return true; + } + } + } catch (Exception ex) { + LOG.error("SlackKudosResponseHandler.handle: " + ex.toString()); + } + return false; + } + + private void store(UUID automatedKudosId) { Optional found = automatedKudosRepository.findById(automatedKudosId); if (found.isPresent()) { @@ -49,8 +81,4 @@ public void store(UUID automatedKudosId) { automatedKudosId.toString()); } } - - public void remove(UUID automatedKudosId) { - automatedKudosRepository.deleteById(automatedKudosId); - } } From 2885adc6440a392efabe30e6bb98cdf801aa01bf Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 7 Feb 2025 15:04:11 -0600 Subject: [PATCH 04/62] Translate channel referencs to channel names and escape the kudos content for json when sending it to the poster. --- .../social_media/SlackSearch.java | 25 +++++++++++++++++++ .../slack/kudos/SlackKudosCreator.java | 22 +++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java index e7fbb6fed..936cacd33 100644 --- a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java +++ b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java @@ -58,6 +58,31 @@ public String findChannelId(String channelName) { return null; } + public String findChannelName(String channelId) { + String token = configuration.getApplication().getSlack().getBotToken(); + if (token != null) { + try { + MethodsClient client = Slack.getInstance().methods(token); + ConversationsListResponse response = client.conversationsList( + ConversationsListRequest.builder().build() + ); + + if (response.isOk()) { + for (Conversation conversation: response.getChannels()) { + if (conversation.getId().equals(channelId)) { + return conversation.getName(); + } + } + } + } catch(IOException e) { + LOG.error("SlackSearch.findChannelName: " + e.toString()); + } catch(SlackApiException e) { + LOG.error("SlackSearch.findChannelName: " + e.toString()); + } + } + return null; + } + public String findUserId(String userEmail) { String token = configuration.getApplication().getSlack().getBotToken(); if (token != null) { diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java index 3b927759a..f1ec5c119 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java @@ -92,6 +92,7 @@ private MemberProfile lookupUser(String userId) { } private String processText(String text, List recipients) { + // First, process user references. StringBuffer buffer = new StringBuffer(text.length()); Pattern userRef = Pattern.compile("<@([^>]+)>"); Matcher action = userRef.matcher(StringEscapeUtils.unescapeHtml4(text)); @@ -107,6 +108,25 @@ private String processText(String text, List recipients) { MemberProfileUtils.getFullName(profile))); } action.appendTail(buffer); + text = buffer.toString(); + + // Next, translate channel references to channel names. + Pattern channelRef = Pattern.compile("<#([^>]+)\\|>"); + buffer = new StringBuffer(text.length()); + action = channelRef.matcher(text); + while (action.find()) { + // Get the name of the channel. + String channelId = action.group(1); + String name = slackSearch.findChannelName(channelId); + if (name == null) { + name = "unknown_channel"; + } + name = "#" + name; + + // Replace the channel reference with the channel name. + action.appendReplacement(buffer, Matcher.quoteReplacement(name)); + } + action.appendTail(buffer); return buffer.toString(); } @@ -137,7 +157,7 @@ private String getSlackBlocks(String kudosUUID, String contents) { try { return String.format(IOUtils.readText( new BufferedReader(kudosSlackBlocks.asReader())), - kudosUUID, contents); + kudosUUID, StringEscapeUtils.escapeJson(contents)); } catch(Exception ex) { LOG.error("SlackKudosCreator.getSlackBlocks: " + ex.toString()); return ""; From 3874759e5b751b3d234881700fe61065310490f0 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 10 Feb 2025 09:31:42 -0600 Subject: [PATCH 05/62] Persist the last import timestamp so that we can get messages posted if the server is not running. --- .../slack/kudos/KudosChannelReadTime.java | 42 +++++++++++++++++++ .../kudos/KudosChannelReadTimeStore.java | 9 ++++ .../slack/kudos/KudosChannelReader.java | 28 +++++++------ .../slack/kudos/SlackKudosCreator.java | 5 ++- .../db/common/V121__automated_kudos_table.sql | 8 ++++ 5 files changed, 77 insertions(+), 15 deletions(-) create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReadTime.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReadTimeStore.java diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReadTime.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReadTime.java new file mode 100644 index 000000000..33307243b --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReadTime.java @@ -0,0 +1,42 @@ +package com.objectcomputing.checkins.services.slack.kudos; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.data.annotation.TypeDef; +import io.micronaut.data.model.DataType; +import io.swagger.v3.oas.annotations.media.Schema; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; + +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@Introspected +@Table(name = "automated_kudos_read_time") +public class KudosChannelReadTime { + static final public String key = "Singleton"; + + @Id + @Column(name = "id") + @Schema(description = "the id of the kudos channel read time") + private String id; + + @NotNull + @Column(name = "readtime") + @TypeDef(type = DataType.TIMESTAMP) + @Schema(description = "date the kudos were created") + private LocalDateTime readTime; + + public KudosChannelReadTime() { + id = key; + readTime = LocalDateTime.now(); + } +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReadTimeStore.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReadTimeStore.java new file mode 100644 index 000000000..e2adbec19 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReadTimeStore.java @@ -0,0 +1,9 @@ +package com.objectcomputing.checkins.services.slack.kudos; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.repository.CrudRepository; + +@JdbcRepository(dialect = Dialect.POSTGRES) +public interface KudosChannelReadTimeStore extends CrudRepository { +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java index d0419f5e3..1983b0c65 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java @@ -14,13 +14,15 @@ import jakarta.inject.Inject; import java.util.List; +import java.util.Optional; import java.time.LocalDateTime; @Singleton public class KudosChannelReader { private static final Logger LOG = LoggerFactory.getLogger(KudosChannelReader.class); - private static LocalDateTime lastImport = null; + @Inject + private KudosChannelReadTimeStore kudosChannelReadTimeStore; @Inject private CheckInsConfiguration configuration; @@ -33,23 +35,23 @@ public class KudosChannelReader { @Scheduled(fixedDelay = "10m") public void readChannel() { - if (lastImport == null) { - lastImport = LocalDateTime.now(); - } + Optional readTime = + kudosChannelReadTimeStore.findById(KudosChannelReadTime.key); + boolean present = readTime.isPresent(); + LocalDateTime lastImport = present ? readTime.get().getReadTime() + : LocalDateTime.now(); String channelId = configuration.getApplication() .getSlack().getKudosChannel(); + LOG.info("Reading messages from " + channelId + + " as of " + lastImport.toString()); List messages = slackReader.read(channelId, lastImport); - updateLastImportTime(); + if (present) { + kudosChannelReadTimeStore.update(new KudosChannelReadTime()); + } else { + kudosChannelReadTimeStore.save(new KudosChannelReadTime()); + } slackKudosCreator.store(messages); } - - private LocalDateTime getLastImportTime() { - return lastImport; - } - - private void updateLastImportTime() { - lastImport = LocalDateTime.now(); - } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java index f1ec5c119..20b4a0ae1 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java @@ -50,8 +50,7 @@ public class SlackKudosCreator { public void store(List messages) { for (Message message : messages) { - if (message.getSubtype() == null && - message.getText().toLowerCase().contains("kudos")) { + if (message.getSubtype() == null) { try { AutomatedKudosDTO kudosDTO = createFromMessage(message); if (kudosDTO.getRecipientIds().size() == 0) { @@ -64,6 +63,8 @@ public void store(List messages) { } catch (Exception ex) { LOG.error("store: " + ex.toString()); } + } else { + LOG.info("Skipping message: " + message.getText()); } } diff --git a/server/src/main/resources/db/common/V121__automated_kudos_table.sql b/server/src/main/resources/db/common/V121__automated_kudos_table.sql index 5d8943ad2..a29c362bb 100644 --- a/server/src/main/resources/db/common/V121__automated_kudos_table.sql +++ b/server/src/main/resources/db/common/V121__automated_kudos_table.sql @@ -9,3 +9,11 @@ CREATE TABLE automated_kudos senderid varchar REFERENCES member_profile (id), recipientids varchar[] ); + +DROP TABLE IF EXISTS automated_kudos_read_time; + +CREATE TABLE automated_kudos_read_time +( + id varchar PRIMARY KEY, + readtime timestamp +); From 173f55228282002523da329a3c61eaccdb058bca Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 10 Feb 2025 09:45:26 -0600 Subject: [PATCH 06/62] Moved SlackSearch into the slack services directory. --- .../objectcomputing/checkins/services/kudos/KudosConverter.java | 2 +- .../social_media => services/slack}/SlackSearch.java | 2 +- .../checkins/services/slack/kudos/SlackKudosCreator.java | 2 +- .../slack/pulseresponse/SlackPulseResponseConverter.java | 2 +- .../checkins/services/SlackSearchReplacement.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename server/src/main/java/com/objectcomputing/checkins/{notifications/social_media => services/slack}/SlackSearch.java (98%) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java index 3587243bb..65c2ee1d9 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java @@ -1,6 +1,6 @@ package com.objectcomputing.checkins.services.kudos; -import com.objectcomputing.checkins.notifications.social_media.SlackSearch; +import com.objectcomputing.checkins.services.slack.SlackSearch; import com.objectcomputing.checkins.services.kudos.kudos_recipient.KudosRecipientServices; import com.objectcomputing.checkins.services.kudos.kudos_recipient.KudosRecipient; import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; diff --git a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSearch.java similarity index 98% rename from server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java rename to server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSearch.java index 936cacd33..c0af370cb 100644 --- a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSearch.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSearch.java @@ -1,4 +1,4 @@ -package com.objectcomputing.checkins.notifications.social_media; +package com.objectcomputing.checkins.services.slack; import com.objectcomputing.checkins.configuration.CheckInsConfiguration; import com.slack.api.model.block.LayoutBlock; diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java index 20b4a0ae1..796a75332 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java @@ -1,6 +1,6 @@ package com.objectcomputing.checkins.services.slack.kudos; -import com.objectcomputing.checkins.notifications.social_media.SlackSearch; +import com.objectcomputing.checkins.services.slack.SlackSearch; import com.objectcomputing.checkins.notifications.social_media.SlackSender; import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; import com.objectcomputing.checkins.services.memberprofile.MemberProfileUtils; diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/pulseresponse/SlackPulseResponseConverter.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/pulseresponse/SlackPulseResponseConverter.java index 54508287d..3205b6c66 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/slack/pulseresponse/SlackPulseResponseConverter.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/pulseresponse/SlackPulseResponseConverter.java @@ -3,7 +3,7 @@ import com.objectcomputing.checkins.exceptions.BadArgException; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; -import com.objectcomputing.checkins.notifications.social_media.SlackSearch; +import com.objectcomputing.checkins.services.slack.SlackSearch; import com.objectcomputing.checkins.services.pulseresponse.PulseResponseCreateDTO; import jakarta.inject.Singleton; diff --git a/server/src/test/java/com/objectcomputing/checkins/services/SlackSearchReplacement.java b/server/src/test/java/com/objectcomputing/checkins/services/SlackSearchReplacement.java index c239da699..8c10f4b77 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/SlackSearchReplacement.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/SlackSearchReplacement.java @@ -1,6 +1,6 @@ package com.objectcomputing.checkins.services; -import com.objectcomputing.checkins.notifications.social_media.SlackSearch; +import com.objectcomputing.checkins.services.slack.SlackSearch; import com.objectcomputing.checkins.configuration.CheckInsConfiguration; import io.micronaut.context.annotation.Replaces; From 8d2607df9d809a69512b8c1373776a7df5d0b877 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 10 Feb 2025 11:15:20 -0600 Subject: [PATCH 07/62] Fixed test errors. --- .../slack/SlackSubmissionHandler.java | 7 ++- .../services/SlackSearchReplacement.java | 14 ++++- .../PulseResponseControllerTest.java | 55 ++++++++++++++++++- .../src/test/resources/application-test.yml | 1 + 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java index fc34b3a22..20d9a4b3d 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java @@ -19,12 +19,16 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.JsonProcessingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.Map; import java.nio.charset.StandardCharsets; @Singleton public class SlackSubmissionHandler { - private final String typeKey = "type"; + private static final Logger LOG = LoggerFactory.getLogger(SlackSubmissionHandler.class); + private static final String typeKey = "type"; private final PulseResponseService pulseResponseServices; private final SlackSignatureVerifier slackSignatureVerifier; @@ -96,6 +100,7 @@ public HttpResponse externalResponse(String signature, } } catch(JsonProcessingException ex) { // Fall through to the bottom... + LOG.error("externalResponse: " + ex.toString()); } } } else { diff --git a/server/src/test/java/com/objectcomputing/checkins/services/SlackSearchReplacement.java b/server/src/test/java/com/objectcomputing/checkins/services/SlackSearchReplacement.java index 8c10f4b77..db0d49347 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/SlackSearchReplacement.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/SlackSearchReplacement.java @@ -25,10 +25,20 @@ public SlackSearchReplacement(CheckInsConfiguration checkInsConfiguration) { super(checkInsConfiguration); } + @Override + public String findChannelName(String channelId) { + return channels.containsKey(channelId) ? + channels.get(channelId) : null; + } + @Override public String findChannelId(String channelName) { - return channels.containsKey(channelName) ? - channels.get(channelName) : null; + for (Map.Entry entry : channels.entrySet()) { + if (entry.getValue().equals(channelName)) { + return entry.getKey(); + } + } + return null; } @Override diff --git a/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java index c4d94aa70..e776d6757 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java @@ -37,6 +37,7 @@ import java.nio.charset.StandardCharsets; import java.util.Base64; import java.time.Instant; +import java.net.URLEncoder; import static com.objectcomputing.checkins.services.role.RoleType.Constants.ADMIN_ROLE; import static com.objectcomputing.checkins.services.role.RoleType.Constants.MEMBER_ROLE; @@ -536,9 +537,10 @@ void testUpdateInvalidDatePulseResponse() { @Test void testCreateAPulseResponseFromSlack() { MemberProfile memberProfile = createADefaultMemberProfile(); - slackSearch.users.put("SLACK_ID_HI", memberProfile.getWorkEmail()); + String slackId = "SLACK_ID_HI"; + slackSearch.users.put(slackId, memberProfile.getWorkEmail()); - final String rawBody = "payload=%7B%22type%22%3A+%22view_submission%22%2C+%22user%22%3A+%7B%22id%22%3A+%22SLACK_ID_HI%22%7D%2C+%22view%22%3A+%7B%22id%22%3A+%22VNHU13V36%22%2C+%22type%22%3A+%22modal%22%2C+%22state%22%3A+%7B%22values%22%3A+%7B%22internalNumber%22%3A+%7B%22internalScore%22%3A+%7B%22selected_option%22%3A+%7B%22type%22%3A+%22radio_buttons%22%2C+%22value%22%3A+%224%22%7D%7D%7D%2C+%22internalText%22%3A+%7B%22internalFeelings%22%3A+%7B%22type%22%3A+%22plain_text_input%22%2C+%22value%22%3A+%22I+am+a+robot.%22%7D%7D%2C+%22externalNumber%22%3A+%7B%22externalScore%22%3A+%7B%22selected_option%22%3A+%7B%22type%22%3A+%22radio_buttons%22%2C+%22value%22%3A+%225%22%7D%7D%7D%2C+%22externalText%22%3A+%7B%22externalFeelings%22%3A+%7B%22type%22%3A+%22plain_text_input%22%2C+%22value%22%3A+%22You+are+a+robot.%22%7D%7D%7D%7D%7D%7D"; + final String rawBody = getSlackPulsePayload(slackId); long currentTime = Instant.now().getEpochSecond(); String timestamp = String.valueOf(currentTime); @@ -603,4 +605,53 @@ private MemberProfile profile(String key) { private UUID id(String key) { return profile(key).getId(); } + + private String getSlackPulsePayload(String slackId) { + return "payload=" + + URLEncoder.encode(String.format(""" +{ + "type": "view_submission", + "user": { + "id": "%s" + }, + "view": { + "id": "VNHU13V36", + "type": "modal", + "callback_id": "pulseSubmission", + "state": { + "values": { + "internalNumber": { + "internalScore": { + "selected_option": { + "type": "radio_buttons", + "value": "4" + } + } + }, + "internalText": { + "internalFeelings": { + "type": "plain_text_input", + "value": "I am a robot." + } + }, + "externalNumber": { + "externalScore": { + "selected_option": { + "type": "radio_buttons", + "value": "5" + } + } + }, + "externalText": { + "externalFeelings": { + "type": "plain_text_input", + "value": "You are a robot." + } + } + } + } + } +} +""", slackId)); + } } diff --git a/server/src/test/resources/application-test.yml b/server/src/test/resources/application-test.yml index 132a9b1a6..0eb4040fd 100644 --- a/server/src/test/resources/application-test.yml +++ b/server/src/test/resources/application-test.yml @@ -45,6 +45,7 @@ check-ins: webhook-url: https://bogus.objectcomputing.com/slack bot-token: BOGUS_TOKEN signing-secret: BOGUS_SIGNING_SECRET + kudos-channel: SLACK_KUDOS_CHANNEL_ID --- aes: key: BOGUS_TEST_KEY From f72b174726212264af615d14bf5fb81f6d9eb4ff Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 11 Feb 2025 08:12:42 -0600 Subject: [PATCH 08/62] Fixed incorrect column reference. --- .../checkins/services/slack/kudos/AutomatedKudos.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/AutomatedKudos.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/AutomatedKudos.java index a21d79341..e08dfc941 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/AutomatedKudos.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/AutomatedKudos.java @@ -47,7 +47,7 @@ public class AutomatedKudos { @NotBlank @Column(name = "externalid") - @ColumnTransformer(read = "pgp_sym_decrypt(message::bytea, '${aes.key}')", write = "pgp_sym_encrypt(?, '${aes.key}')") + @ColumnTransformer(read = "pgp_sym_decrypt(externalid::bytea, '${aes.key}')", write = "pgp_sym_encrypt(?, '${aes.key}')") @Schema(description = "the external id of the sender") private String externalId; From b4a0c912e0529b2add87efe6e45c9f802513fffe Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 11 Feb 2025 08:13:18 -0600 Subject: [PATCH 09/62] Added a test for the slack kudos. --- .../social_media/SlackSender.java | 1 - ...atureVerifier.java => SlackSignature.java} | 33 ++- .../slack/SlackSubmissionHandler.java | 12 +- .../services/SlackReaderReplacement.java | 58 ++++++ .../services/SlackSenderReplacement.java | 37 ++++ .../fixture/AutomatedKudosFixture.java | 15 ++ .../services/fixture/RepositoryFixture.java | 5 + .../services/kudos/SlackKudosTest.java | 188 ++++++++++++++++++ .../PulseResponseControllerTest.java | 30 +-- 9 files changed, 343 insertions(+), 36 deletions(-) rename server/src/main/java/com/objectcomputing/checkins/services/slack/{SlackSignatureVerifier.java => SlackSignature.java} (64%) create mode 100644 server/src/test/java/com/objectcomputing/checkins/services/SlackReaderReplacement.java create mode 100644 server/src/test/java/com/objectcomputing/checkins/services/SlackSenderReplacement.java create mode 100644 server/src/test/java/com/objectcomputing/checkins/services/fixture/AutomatedKudosFixture.java create mode 100644 server/src/test/java/com/objectcomputing/checkins/services/kudos/SlackKudosTest.java diff --git a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSender.java b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSender.java index b4c7b0875..d351f5ccd 100644 --- a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSender.java +++ b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSender.java @@ -46,7 +46,6 @@ public boolean send(List userIds, String slackBlocks) { .builder() .channel(openResponse.getChannel().getId()) .blocksAsString(slackBlocks) - .text("This is a test") .build(); // Send it to Slack diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSignatureVerifier.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSignature.java similarity index 64% rename from server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSignatureVerifier.java rename to server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSignature.java index 6ff2575da..22f36c202 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSignatureVerifier.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSignature.java @@ -1,5 +1,6 @@ package com.objectcomputing.checkins.services.slack; +import com.objectcomputing.checkins.exceptions.BadArgException; import com.objectcomputing.checkins.configuration.CheckInsConfiguration; import javax.crypto.Mac; @@ -12,11 +13,11 @@ import jakarta.inject.Singleton; @Singleton -public class SlackSignatureVerifier { +public class SlackSignature { @Inject private CheckInsConfiguration configuration; - public SlackSignatureVerifier() {} + public SlackSignature() {} public boolean verifyRequest(String slackSignature, String timestamp, String requestBody) { try { @@ -35,7 +36,7 @@ public boolean verifyRequest(String slackSignature, String timestamp, String req // Compare the computed signature with Slack's signature return computedSignature.equals(slackSignature); - } catch (Exception e) { + } catch (Exception ex) { return false; } } @@ -64,4 +65,30 @@ private String hmacSha256(String secret, String message) throws Exception { } return hexString.toString(); } + + public String generate(String timestamp, String rawBody) { + String baseString = "v0:" + timestamp + ":" + rawBody; + String secret = configuration.getApplication() + .getSlack().getSigningSecret(); + + try { + // Generate HMAC SHA-256 signature + Mac mac = Mac.getInstance("HmacSHA256"); + SecretKeySpec secretKeySpec = + new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), + "HmacSHA256"); + mac.init(secretKeySpec); + byte[] hash = mac.doFinal( + baseString.getBytes(StandardCharsets.UTF_8)); + + // Convert hash to hex + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + hexString.append(String.format("%02x", b)); + } + return "v0=" + hexString.toString(); + } catch (Exception ex) { + throw new BadArgException(ex.toString()); + } + } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java index 20d9a4b3d..5c21ece4d 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/SlackSubmissionHandler.java @@ -31,18 +31,18 @@ public class SlackSubmissionHandler { private static final String typeKey = "type"; private final PulseResponseService pulseResponseServices; - private final SlackSignatureVerifier slackSignatureVerifier; + private final SlackSignature slackSignature; private final PulseSlackCommand pulseSlackCommand; private final SlackPulseResponseConverter slackPulseResponseConverter; private final SlackKudosResponseHandler slackKudosResponseHandler; public SlackSubmissionHandler(PulseResponseService pulseResponseServices, - SlackSignatureVerifier slackSignatureVerifier, + SlackSignature slackSignature, PulseSlackCommand pulseSlackCommand, SlackPulseResponseConverter slackPulseResponseConverter, SlackKudosResponseHandler slackKudosResponseHandler) { this.pulseResponseServices = pulseResponseServices; - this.slackSignatureVerifier = slackSignatureVerifier; + this.slackSignature = slackSignature; this.pulseSlackCommand = pulseSlackCommand; this.slackPulseResponseConverter = slackPulseResponseConverter; this.slackKudosResponseHandler = slackKudosResponseHandler; @@ -52,8 +52,7 @@ public HttpResponse commandResponse(String signature, String timestamp, String requestBody) { // Validate the request - if (slackSignatureVerifier.verifyRequest(signature, - timestamp, requestBody)) { + if (slackSignature.verifyRequest(signature, timestamp, requestBody)) { // Convert the request body to a map of values. FormUrlEncodedDecoder formUrlEncodedDecoder = new FormUrlEncodedDecoder(); Map body = @@ -77,8 +76,7 @@ public HttpResponse externalResponse(String signature, String requestBody, HttpRequest request) { // Validate the request - if (slackSignatureVerifier.verifyRequest(signature, - timestamp, requestBody)) { + if (slackSignature.verifyRequest(signature, timestamp, requestBody)) { // Convert the request body to a map of values. FormUrlEncodedDecoder formUrlEncodedDecoder = new FormUrlEncodedDecoder(); diff --git a/server/src/test/java/com/objectcomputing/checkins/services/SlackReaderReplacement.java b/server/src/test/java/com/objectcomputing/checkins/services/SlackReaderReplacement.java new file mode 100644 index 000000000..6ac2035d4 --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/SlackReaderReplacement.java @@ -0,0 +1,58 @@ +package com.objectcomputing.checkins.services; + +import com.objectcomputing.checkins.services.slack.SlackReader; +import com.objectcomputing.checkins.configuration.CheckInsConfiguration; + +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; + +import com.slack.api.model.Message; + +import jakarta.inject.Singleton; + +import java.util.List; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.time.LocalDateTime; +import java.time.ZoneId; + +@Singleton +@Replaces(SlackReader.class) +@Requires(property = "replace.slackreader", value = StringUtils.TRUE) +public class SlackReaderReplacement extends SlackReader { + public final Map> channelMessages = new HashMap<>(); + + @Override + public List read(String channelId, LocalDateTime last) { + List messages = new ArrayList<>(); + if (channelMessages.containsKey(channelId)) { + long ts = last.atZone(ZoneId.systemDefault()) + .toInstant().getEpochSecond(); + for (Message message : channelMessages.get(channelId)) { + long messageTime = Long.parseLong(message.getTs()); + if (messageTime >= ts) { + messages.add(message); + } + } + } + return messages; + } + + public void addMessage(String channelId, String userId, + String text, LocalDateTime sendTime) { + Message message = new Message(); + message.setTs(String.valueOf(sendTime.atZone(ZoneId.systemDefault()) + .toInstant().getEpochSecond())); + message.setText(text); + message.setUser(userId); + + if (!channelMessages.containsKey(channelId)) { + channelMessages.put(channelId, new ArrayList()); + } + channelMessages.get(channelId).add(message); + } +} diff --git a/server/src/test/java/com/objectcomputing/checkins/services/SlackSenderReplacement.java b/server/src/test/java/com/objectcomputing/checkins/services/SlackSenderReplacement.java new file mode 100644 index 000000000..b64c4aef1 --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/SlackSenderReplacement.java @@ -0,0 +1,37 @@ +package com.objectcomputing.checkins.services; + +import com.objectcomputing.checkins.notifications.social_media.SlackSender; + +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.util.StringUtils; + +import jakarta.inject.Singleton; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +@Singleton +@Replaces(SlackSender.class) +@Requires(property = "replace.slacksender", value = StringUtils.TRUE) +public class SlackSenderReplacement extends SlackSender { + public final Map> sent = new HashMap<>(); + + public void reset() { + sent.clear(); + } + + @Override + public boolean send(List userIds, String slackBlocks) { + for (String userId : userIds) { + if (!sent.containsKey(userId)) { + sent.put(userId, new ArrayList()); + } + sent.get(userId).add(slackBlocks); + } + return true; + } +} + diff --git a/server/src/test/java/com/objectcomputing/checkins/services/fixture/AutomatedKudosFixture.java b/server/src/test/java/com/objectcomputing/checkins/services/fixture/AutomatedKudosFixture.java new file mode 100644 index 000000000..a8c0c2a80 --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/fixture/AutomatedKudosFixture.java @@ -0,0 +1,15 @@ +package com.objectcomputing.checkins.services.fixture; + +import com.objectcomputing.checkins.services.slack.kudos.AutomatedKudos; +import com.objectcomputing.checkins.services.slack.kudos.AutomatedKudosRepository; + +import java.util.ArrayList; +import java.util.List; + +public interface AutomatedKudosFixture extends RepositoryFixture { + default List getAutomatedKudos() { + List list = new ArrayList<>(); + getAutomatedKudosRepository().findAll().forEach(list::add); + return list; + } +} diff --git a/server/src/test/java/com/objectcomputing/checkins/services/fixture/RepositoryFixture.java b/server/src/test/java/com/objectcomputing/checkins/services/fixture/RepositoryFixture.java index eba62d2b6..648728e1d 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/fixture/RepositoryFixture.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/fixture/RepositoryFixture.java @@ -43,6 +43,7 @@ import com.objectcomputing.checkins.services.volunteering.VolunteeringRelationshipRepository; import io.micronaut.runtime.server.EmbeddedServer; import com.objectcomputing.checkins.services.employee_hours.EmployeeHoursRepository; +import com.objectcomputing.checkins.services.slack.kudos.AutomatedKudosRepository; public interface RepositoryFixture { EmbeddedServer getEmbeddedServer(); @@ -213,4 +214,8 @@ default DocumentRepository getDocumentRepository() { default RoleDocumentationRepository getRoleDocumentationRepository() { return getEmbeddedServer().getApplicationContext().getBean(RoleDocumentationRepository.class); } + + default AutomatedKudosRepository getAutomatedKudosRepository() { + return getEmbeddedServer().getApplicationContext().getBean(AutomatedKudosRepository.class); + } } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/kudos/SlackKudosTest.java b/server/src/test/java/com/objectcomputing/checkins/services/kudos/SlackKudosTest.java new file mode 100644 index 000000000..893b9cf36 --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/kudos/SlackKudosTest.java @@ -0,0 +1,188 @@ +package com.objectcomputing.checkins.services.kudos; + +import com.objectcomputing.checkins.services.slack.SlackSignature; +import com.objectcomputing.checkins.services.slack.kudos.KudosChannelReader; +import com.objectcomputing.checkins.services.slack.kudos.AutomatedKudos; +import com.objectcomputing.checkins.services.memberprofile.MemberProfile; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileUtils; +import com.objectcomputing.checkins.services.kudos.KudosResponseDTO; +import com.objectcomputing.checkins.services.role.RoleType; + +import com.objectcomputing.checkins.services.TestContainersSuite; +import com.objectcomputing.checkins.services.SlackSenderReplacement; +import com.objectcomputing.checkins.services.SlackReaderReplacement; +import com.objectcomputing.checkins.services.SlackSearchReplacement; +import com.objectcomputing.checkins.services.fixture.MemberProfileFixture; +import com.objectcomputing.checkins.services.fixture.AutomatedKudosFixture; +import com.objectcomputing.checkins.services.fixture.RoleFixture; + +import io.micronaut.core.type.Argument; +import io.micronaut.core.util.StringUtils; +import io.micronaut.context.annotation.Property; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.core.JsonProcessingException; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.time.Instant; +import java.net.URLEncoder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Property(name = "replace.slacksearch", value = StringUtils.TRUE) +@Property(name = "replace.slackreader", value = StringUtils.TRUE) +@Property(name = "replace.slacksender", value = StringUtils.TRUE) +class SlackKudosTest extends TestContainersSuite implements MemberProfileFixture, AutomatedKudosFixture, RoleFixture { + @Inject + private SlackReaderReplacement slackReader; + + @Inject + private SlackSearchReplacement slackSearch; + + @Inject + private SlackSenderReplacement slackSender; + + @Inject + private KudosChannelReader kudosChannelReader; + + @Inject + private SlackSignature slackSignature; + + @Inject + @Client("/services") + protected HttpClient client; + + MemberProfile sender; + MemberProfile recipient; + + @BeforeEach + void setUp() { + createAndAssignRoles(); + sender = createADefaultMemberProfile(); + recipient = createASecondDefaultMemberProfile(); + } + + @Test + void testCreateKudosFromSlackMessage() throws JsonProcessingException { + String slackSenderId = "senderId"; + slackSearch.users.put(slackSenderId, sender.getWorkEmail()); + String slackRecipient = "recipientId"; + slackSearch.users.put(slackRecipient, recipient.getWorkEmail()); + + // Post to "Slack" + final String beginning = "Kudos to "; + slackReader.addMessage("SLACK_KUDOS_CHANNEL_ID", slackSenderId, + beginning + "<@" + slackRecipient + ">", + LocalDateTime.now()); + + final String messageWithName = beginning + + MemberProfileUtils.getFullName(recipient); + + // Manually tell the reader to load messages from slack. This normally + // happens on an interval. + kudosChannelReader.readChannel(); + + // Get the automated kudos from the repository and validate. + List generatedKudos = getAutomatedKudos(); + assertEquals(1, generatedKudos.size()); + AutomatedKudos kudos = generatedKudos.get(0); + assertEquals(messageWithName, kudos.getMessage()); + assertEquals(slackSenderId, kudos.getExternalId()); + assertEquals(sender.getId(), kudos.getSenderId()); + assertEquals(1, kudos.getRecipientIds().size()); + assertEquals(recipient.getId().toString(), + kudos.getRecipientIds().get(0)); + + // A slack message should have been sent to the sender as well... + assertTrue(slackSender.sent.containsKey(slackSenderId)); + List messages = slackSender.sent.get(slackSenderId); + assertEquals(1, messages.size()); + + ObjectMapper mapper = new ObjectMapper(); + JsonNode sent = mapper.readTree(messages.get(0)); + assertEquals(JsonNodeType.ARRAY, sent.getNodeType()); + + var iter = sent.elements(); + assertTrue(iter.hasNext()); + JsonNode section = iter.next(); + + assertEquals(JsonNodeType.OBJECT, section.getNodeType()); + assertTrue(section.has("block_id")); + UUID automatedKudosUUID = + UUID.fromString(section.get("block_id").asText()); + + // Post to /external to say "yes, we want it in Check-Ins". + final String rawBody = getSlackPostPayload( + automatedKudosUUID.toString()); + long currentTime = Instant.now().getEpochSecond(); + String timestamp = String.valueOf(currentTime); + + HttpRequest request = + HttpRequest.POST("/pulse-responses/external", rawBody) + .header("Content-Type", "application/x-www-form-urlencoded") + .header("X-Slack-Signature", slackSignature.generate(timestamp, rawBody)) + .header("X-Slack-Request-Timestamp", timestamp); + + HttpResponse response = client.toBlocking().exchange(request); + assertEquals(HttpStatus.OK, response.getStatus()); + + // Get list of kudos from kudos controller and verify. + request = HttpRequest.GET("/kudos/recent") + .basicAuth(sender.getWorkEmail(), + RoleType.Constants.MEMBER_ROLE); + HttpResponse> list = + client.toBlocking() + .exchange(request, Argument.listOf(KudosResponseDTO.class)); + assertEquals(HttpStatus.OK, list.getStatus()); + assertEquals(1, list.body().size()); + KudosResponseDTO element = list.body().getFirst(); + assertEquals(messageWithName, element.getMessage()); + assertEquals(sender.getId(), element.getSenderId()); + assertTrue(element.getPubliclyVisible()); + assertEquals(element.getRecipientMembers(), List.of(recipient)); + } + + @Test + void testNoSlackMessages() { + kudosChannelReader.readChannel(); + List generatedKudos = getAutomatedKudos(); + assertEquals(0, generatedKudos.size()); + } + + String getSlackPostPayload(String kudosId) { + return "payload=" + + URLEncoder.encode(String.format(""" +{ + "type": "block_actions", + "message": { + "blocks": [ + { + "block_id": "%s" + } + ] + }, + "actions": [ + { + "action_id": "yes_button" + } + ] +} +""", kudosId)); + } +} diff --git a/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java index e776d6757..a4308af5b 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java @@ -9,6 +9,7 @@ import com.objectcomputing.checkins.util.Util; import com.objectcomputing.checkins.configuration.CheckInsConfiguration; import com.objectcomputing.checkins.services.SlackSearchReplacement; +import com.objectcomputing.checkins.services.slack.SlackSignature; import io.micronaut.core.util.StringUtils; import io.micronaut.core.type.Argument; @@ -35,7 +36,6 @@ import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; -import java.util.Base64; import java.time.Instant; import java.net.URLEncoder; @@ -58,6 +58,9 @@ class PulseResponseControllerTest extends TestContainersSuite implements MemberP @Inject private SlackSearchReplacement slackSearch; + @Inject + private SlackSignature slackSignature; + private Map hierarchy; @BeforeEach @@ -547,7 +550,7 @@ void testCreateAPulseResponseFromSlack() { final HttpRequest request = HttpRequest.POST("/external", rawBody) .header("Content-Type", "application/x-www-form-urlencoded") - .header("X-Slack-Signature", slackSignature(timestamp, rawBody)) + .header("X-Slack-Signature", slackSignature.generate(timestamp, rawBody)) .header("X-Slack-Request-Timestamp", timestamp); final HttpResponse response = client.toBlocking().exchange(request); @@ -555,29 +558,6 @@ void testCreateAPulseResponseFromSlack() { assertEquals(HttpStatus.OK, response.getStatus()); } - private String slackSignature(String timestamp, String rawBody) { - String baseString = "v0:" + timestamp + ":" + rawBody; - String secret = configuration.getApplication() - .getSlack().getSigningSecret(); - - try { - // Generate HMAC SHA-256 signature - Mac mac = Mac.getInstance("HmacSHA256"); - SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); - mac.init(secretKeySpec); - byte[] hash = mac.doFinal(baseString.getBytes(StandardCharsets.UTF_8)); - - // Convert hash to hex - StringBuilder hexString = new StringBuilder(); - for (byte b : hash) { - hexString.append(String.format("%02x", b)); - } - return "v0=" + hexString.toString(); - } catch (Exception e) { - return null; - } - } - private static PulseResponseCreateDTO createPulseResponseCreateDTO() { return createPulseResponseCreateDTO(UUID.randomUUID()); } From 0e2b322a5f871e591a3ff4f047cc522d0193aa84 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 11 Feb 2025 08:30:15 -0600 Subject: [PATCH 10/62] For testing purposes, the send date could be equal. --- .../checkins/services/email/EmailControllerTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/email/EmailControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/email/EmailControllerTest.java index eea14ecce..e1bd32cf2 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/email/EmailControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/email/EmailControllerTest.java @@ -138,14 +138,16 @@ void testSendAndSaveTextEmail() { assertEquals(email.get("content"), firstEmailRes.getContents()); assertEquals(admin.getId(), firstEmailRes.getSentBy()); assertEquals(recipient1.getId(), firstEmailRes.getRecipient()); - assertTrue(firstEmailRes.getTransmissionDate().isAfter(firstEmailRes.getSendDate())); + assertTrue(firstEmailRes.getTransmissionDate().isAfter(firstEmailRes.getSendDate()) || + firstEmailRes.getTransmissionDate().isEqual(firstEmailRes.getSendDate())); Email secondEmailRes = response.getBody().get().get(1); assertEquals(email.get("subject"), secondEmailRes.getSubject()); assertEquals(email.get("content"), secondEmailRes.getContents()); assertEquals(admin.getId(), secondEmailRes.getSentBy()); assertEquals(recipient2.getId(), secondEmailRes.getRecipient()); - assertTrue(secondEmailRes.getTransmissionDate().isAfter(secondEmailRes.getSendDate())); + assertTrue(secondEmailRes.getTransmissionDate().isAfter(secondEmailRes.getSendDate()) || + secondEmailRes.getTransmissionDate().isEqual(secondEmailRes.getSendDate())); assertEquals(1, textEmailSender.events.size()); assertEquals( From 69f065507153fa25ec9645f1d9dc4e595e5c6eb1 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 11 Feb 2025 08:57:27 -0600 Subject: [PATCH 11/62] Added a comment about slack message subtype and removed unused imports. --- .../checkins/services/slack/kudos/SlackKudosCreator.java | 1 + .../services/pulseresponse/PulseResponseControllerTest.java | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java index 796a75332..84c3183e4 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java @@ -50,6 +50,7 @@ public class SlackKudosCreator { public void store(List messages) { for (Message message : messages) { + // User messages do not have a sub-type. if (message.getSubtype() == null) { try { AutomatedKudosDTO kudosDTO = createFromMessage(message); diff --git a/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java index a4308af5b..7f4985c5b 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java @@ -33,9 +33,6 @@ import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.nio.charset.StandardCharsets; import java.time.Instant; import java.net.URLEncoder; From 29634b93b929c15c1a256d222dd76aee6bc52ed1 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 11 Feb 2025 11:04:10 -0600 Subject: [PATCH 12/62] Automatically approve private kudos. --- .../checkins/services/kudos/KudosServicesImpl.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java index 27de524e4..52f3990e5 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java @@ -118,6 +118,12 @@ public Kudos save(KudosCreateDTO kudosDTO) { kudosRecipientServices.save(kudosRecipient); } + if (!savedKudos.getPubliclyVisible()) { + // Private kudos do not need to be approved by another party. + savedKudos.setDateApproved(LocalDate.now()); + savedKudos = kudosRepository.update(savedKudos); + } + sendNotification(savedKudos, NotificationType.creation); return savedKudos; } From c445b87d23819a6e62683c684edcbc3f77f65789 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 11 Feb 2025 11:14:40 -0600 Subject: [PATCH 13/62] Only newly created publicly visible kudos do not have a approve date. --- .../checkins/services/kudos/KudosControllerTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java index e16695484..deab31e63 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java @@ -133,7 +133,9 @@ void testCreateKudos(boolean supplyTeam, boolean publiclyVisible) { assertEquals(senderId, kudos.getSenderId()); assertEquals(supplyTeam ? teamId : null, kudos.getTeamId()); assertEquals(LocalDate.now(), kudos.getDateCreated()); - assertNull(kudos.getDateApproved()); + if (publiclyVisible) { + assertNull(kudos.getDateApproved()); + } List kudosRecipients = findKudosRecipientByKudosId(kudos.getId()); assertEquals(1, kudosRecipients.size()); From a445f23710b07bf145ab693698f4f7dcbe087840 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Wed, 12 Feb 2025 08:46:51 -0600 Subject: [PATCH 14/62] Initial changes to implement Kudos modification. --- .../services/kudos/KudosController.java | 5 + .../services/kudos/KudosServices.java | 2 + .../services/kudos/KudosServicesImpl.java | 93 ++++++++- web-ui/src/api/kudos.js | 17 +- .../src/components/kudos_card/KudosCard.jsx | 177 ++++++++++++++---- web-ui/src/pages/KudosPage.jsx | 3 +- web-ui/src/pages/ManageKudosPage.jsx | 1 + 7 files changed, 262 insertions(+), 36 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosController.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosController.java index 72eead812..7db280cbe 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosController.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosController.java @@ -39,6 +39,11 @@ public Kudos create(@Body @Valid KudosCreateDTO kudos) { } @Put + public Kudos update(@Body @Valid KudosResponseDTO kudos) { + return kudosServices.update(kudos); + } + + @Put("/approve") public Kudos approve(@Body @Valid Kudos kudos) { return kudosServices.approve(kudos); } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServices.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServices.java index 72cbf4f96..85e31970e 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServices.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServices.java @@ -9,6 +9,8 @@ public interface KudosServices { Kudos save(KudosCreateDTO kudos); + Kudos update(KudosResponseDTO kudos); + Kudos approve(Kudos kudos); List getRecent(); diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java index 52f3990e5..e21eb1af7 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java @@ -1,5 +1,6 @@ package com.objectcomputing.checkins.services.kudos; +import com.objectcomputing.checkins.exceptions.PermissionException; import com.objectcomputing.checkins.services.permissions.Permission; import com.objectcomputing.checkins.services.permissions.RequiredPermission; import com.objectcomputing.checkins.configuration.CheckInsConfiguration; @@ -37,6 +38,8 @@ import java.util.List; import java.util.UUID; import java.util.Set; +import java.util.HashSet; +import java.util.stream.Collectors; import static com.objectcomputing.checkins.services.validate.PermissionsValidation.NOT_AUTHORIZED_MSG; @@ -146,6 +149,67 @@ public Kudos approve(Kudos kudos) { return updated; } + @Override + public Kudos update(KudosResponseDTO kudos) { + // Find the corresponding kudos and make sure we have permission. + final UUID kudosId = kudos.getId(); + final Kudos existingKudos = + kudosRepository.findById(kudosId).orElseThrow(() -> + new BadArgException(KUDOS_DOES_NOT_EXIST_MSG.formatted(kudosId))); + + final MemberProfile currentUser = currentUserServices.getCurrentUser(); + if (!currentUser.getId().equals(existingKudos.getSenderId()) && + !hasAdministerKudosPermission()) { + throw new PermissionException(NOT_AUTHORIZED_MSG); + } + + // Begin modifying the existing kudos to reflect desired changes. + existingKudos.setMessage(kudos.getMessage()); + + boolean existingPublic = existingKudos.getPubliclyVisible(); + boolean proposedPublic = kudos.getPubliclyVisible(); + if (existingPublic && !proposedPublic) { + // TODO: Somehow find and remove the Slack Kudos that the Check-Ins + // Integration posted. + } else if (!existingPublic && proposedPublic) { + // Clear the date approved when going from private to public. + existingKudos.setDateApproved(null); + } + + existingKudos.setPubliclyVisible(kudos.getPubliclyVisible()); + + List recipients = kudosRecipientRepository + .findByKudosId(kudosId); + Set proposed = kudos.getRecipientMembers() + .stream() + .map(r -> r.getId()) + .collect(Collectors.toSet()); + boolean different = (recipients.size() != proposed.size()); + if (!different) { + Set existing = recipients.stream() + .map(r -> r.getMemberId()) + .collect(Collectors.toSet()); + different = !existing.equals(proposed); + } + + // First, update the Kudos so that we only change recipients if they + // are different and we were able to update the Kudos. + final Kudos updated = kudosRepository.update(existingKudos); + + // Change recipients, if necessary. + if (different) { + updateRecipients(updated, recipients, proposed); + } + + // The kudos has been updated. Send notification to admin, if going + // from private to public. + if (!existingPublic && proposedPublic) { + sendNotification(updated, NotificationType.creation); + } + + return updated; + } + @Override public KudosResponseDTO getById(UUID id) { @@ -178,11 +242,16 @@ public KudosResponseDTO getById(UUID id) { } @Override - @RequiredPermission(Permission.CAN_ADMINISTER_KUDOS) public void delete(UUID id) { Kudos kudos = kudosRepository.findById(id).orElseThrow(() -> new NotFoundException(KUDOS_DOES_NOT_EXIST_MSG.formatted(id))); + MemberProfile currentUser = currentUserServices.getCurrentUser(); + if (!currentUser.getId().equals(kudos.getSenderId()) && + !hasAdministerKudosPermission()) { + throw new PermissionException(NOT_AUTHORIZED_MSG); + } + // Delete all KudosRecipients associated with this kudos List recipients = kudosRecipientServices.getAllByKudosId(kudos.getId()); kudosRecipientRepository.deleteAll(recipients); @@ -393,4 +462,26 @@ private void slackApprovedKudos(Kudos kudos) { private boolean hasAdministerKudosPermission() { return currentUserServices.hasPermission(Permission.CAN_ADMINISTER_KUDOS); } + + private void updateRecipients(Kudos updated, + List recipients, + Set proposed) { + // Add the new recipients. + Set existing = recipients.stream() + .map(r -> r.getMemberId()) + .collect(Collectors.toSet()); + for (UUID id : proposed) { + if (!existing.contains(id)) { + kudosRecipientServices.save( + new KudosRecipient(updated.getId(), id)); + } + } + + // Remove any that are no longer designated as recipients. + for (KudosRecipient recipient : recipients) { + if (!proposed.contains(recipient.getMemberId())) { + kudosRecipientRepository.delete(recipient); + } + } + } } diff --git a/web-ui/src/api/kudos.js b/web-ui/src/api/kudos.js index ee17acf42..a9a960123 100644 --- a/web-ui/src/api/kudos.js +++ b/web-ui/src/api/kudos.js @@ -57,7 +57,7 @@ export const getAllKudos = async (cookie, isPending) => { }); }; -export const approveKudos = async (kudos, cookie) => { +export const updateKudos = async (kudos, cookie) => { return resolve({ method: "put", url: kudosUrl, @@ -70,6 +70,19 @@ export const approveKudos = async (kudos, cookie) => { }); }; +export const approveKudos = async (kudos, cookie) => { + return resolve({ + method: "put", + url: `${kudosUrl}/approve`, + data: kudos, + responseType: "json", + headers: { + "X-CSRF-Header": cookie, + Accept: 'application/json', + 'Content-Type': 'application/json;charset=UTF-8'} + }); +}; + export const deleteKudos = async (kudosId, cookie) => { return resolve({ method: "delete", @@ -77,4 +90,4 @@ export const deleteKudos = async (kudosId, cookie) => { responseType: "json", headers: { "X-CSRF-Header": cookie, Accept: 'application/json' } }); -}; \ No newline at end of file +}; diff --git a/web-ui/src/components/kudos_card/KudosCard.jsx b/web-ui/src/components/kudos_card/KudosCard.jsx index 168413127..1903b9e22 100644 --- a/web-ui/src/components/kudos_card/KudosCard.jsx +++ b/web-ui/src/components/kudos_card/KudosCard.jsx @@ -16,17 +16,22 @@ import { DialogContentText, DialogActions, TextField, + FormGroup, + FormControlLabel, + Checkbox, } from "@mui/material"; import { selectCsrfToken, selectProfile } from "../../context/selectors"; +import MemberSelector from '../member_selector/MemberSelector'; import { AppContext } from "../../context/AppContext"; import { getAvatarURL } from "../../api/api"; import DateFnsUtils from "@date-io/date-fns"; import CheckIcon from "@mui/icons-material/Check"; import CloseIcon from "@mui/icons-material/Close"; +import EditIcon from "@mui/icons-material/Edit"; import TeamIcon from "@mui/icons-material/Groups"; import "./KudosCard.css"; -import { approveKudos, deleteKudos } from "../../api/kudos"; +import { approveKudos, deleteKudos, updateKudos } from "../../api/kudos"; import { UPDATE_TOAST } from "../../context/actions"; const dateUtils = new DateFnsUtils(); @@ -42,16 +47,21 @@ const propTypes = { recipientMembers: PropTypes.array, }).isRequired, includeActions: PropTypes.bool, + includeEdit: PropTypes.bool, onKudosAction: PropTypes.func, }; -const KudosCard = ({ kudos, includeActions, onKudosAction }) => { +const KudosCard = ({ kudos, includeActions, includeEdit, onKudosAction }) => { const { state, dispatch } = useContext(AppContext); const csrf = selectCsrfToken(state); const [expanded, setExpanded] = useState(true); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - // const [deleteReason, setDeleteReason] = useState(""); // TODO: setup optional reason for deleting a kudos + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [kudosPublic, setKudosPublic] = useState(kudos.publiclyVisible); + const [kudosMessage, setKudosMessage] = useState(kudos.message); + const [memberSelectorOpen, setMemberSelectorOpen] = useState(false); + const [kudosRecipientMembers, setKudosRecipientMembers] = useState(kudos.recipientMembers); const sender = selectProfile(state, kudos.senderId); @@ -104,7 +114,7 @@ const KudosCard = ({ kudos, includeActions, onKudosAction }) => { toast: "Kudos approved", }, }); - onKudosAction(); + onKudosAction && onKudosAction(); } else { dispatch({ type: UPDATE_TOAST, @@ -129,7 +139,7 @@ const KudosCard = ({ kudos, includeActions, onKudosAction }) => { toast: "Pending kudos deleted", }, }); - onKudosAction(); + onKudosAction && onKudosAction(); } else { dispatch({ type: UPDATE_TOAST, @@ -141,16 +151,47 @@ const KudosCard = ({ kudos, includeActions, onKudosAction }) => { } }, [kudos, csrf, dispatch, onKudosAction]); + const updateKudosCallback = useCallback(async () => { + // Close the dialog. + setEditDialogOpen(false); + + // Update the modifiable parts. + kudos.message = kudosMessage; + kudos.publiclyVisible = kudosPublic; + kudos.recipientMembers = kudosRecipientMembers; + + // Update on the server. + const res = await updateKudos(kudos, csrf); + if (!res.error) { + dispatch({ + type: UPDATE_TOAST, + payload: { + severity: "success", + toast: "Kudos Updated", + }, + }); + onKudosAction && onKudosAction(); + } else { + dispatch({ + type: UPDATE_TOAST, + payload: { + severity: "error", + toast: "Failed to update kudos", + }, + }); + } + }, [kudos, kudosMessage, kudosPublic, kudosRecipientMembers, csrf, dispatch, onKudosAction]); + const getStatusComponent = useCallback(() => { const dateApproved = kudos.dateApproved ? new Date(kudos.dateApproved.join("/")) : null; - const dateCreated = new Date(kudos.dateCreated.join("/")); + const info = []; + const actions = []; if (includeActions) { - return ( -
- + actions.push( + - + ); + } else if (dateApproved) { + info.push( + + Received{" "} + {dateApproved ? dateUtils.format(dateApproved, "MM/dd/yyyy") : ""} + + ); + } else { + const dateCreated = new Date(kudos.dateCreated.join("/")); + info.push( + + Pending + + ); + info.push( + + Created {dateUtils.format(dateCreated, "MM/dd/yyyy")} + + ); + } + if (includeEdit) { + actions.push( + + + + ); + } + if (includeActions || includeEdit) { + actions.push( + -
- ); - } else if (dateApproved) { - return ( - <> - - Received{" "} - {dateApproved ? dateUtils.format(dateApproved, "MM/dd/yyyy") : ""} - - ); } + return <> + {info.length > 0 &&
+ {info} +
} + {actions.length > 0 &&
+ {actions} +
} + ; + }, [kudos, includeActions, includeEdit, approveKudosCallback]); - return ( - <> - - Pending - - - Created {dateUtils.format(dateCreated, "MM/dd/yyyy")} - - - ); - }, [kudos, includeActions, approveKudosCallback]); + const reloadKudosValues = () => { + setKudosMessage(kudos.message); + setKudosPublic(kudos.publiclyVisible); + setKudosRecipientMembers(kudos.recipientMembers); + }; const dateApproved = kudos.dateApproved ? new Date(kudos.dateApproved.join("/")) @@ -208,7 +282,7 @@ const KudosCard = ({ kudos, includeActions, onKudosAction }) => { Delete Kudos - Are you sure you want to deny approval for these kudos? The kudos + Are you sure you want to complete this action? The kudos will be deleted. { + + Edit Kudos + + + + } + label="Public" + onChange={e => { + setKudosPublic(e.target.checked); + }} + /> + + { + setKudosMessage(event.target.value); + }} + rows={5} + style={{ marginTop: "2rem" }} + variant="outlined" + /> + + + + + +
{ ) : sentKudos.length > 0 ? (
{sentKudos.map((k) => ( - + ))}
) : ( diff --git a/web-ui/src/pages/ManageKudosPage.jsx b/web-ui/src/pages/ManageKudosPage.jsx index 00b45497b..1c7b8050b 100644 --- a/web-ui/src/pages/ManageKudosPage.jsx +++ b/web-ui/src/pages/ManageKudosPage.jsx @@ -209,6 +209,7 @@ const ManageKudosPage = () => { key={k.id} kudos={k} includeActions + includeEdit onKudosAction={() => { const updatedKudos = pendingKudos.filter(pk => pk.id !== k.id); setPendingKudos(updatedKudos); From 70a25c531a7679bd21b458a1280f9e84d8a94873 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Wed, 12 Feb 2025 08:58:26 -0600 Subject: [PATCH 15/62] Fixed tests. --- .../services/kudos/KudosControllerTest.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java index deab31e63..ac73d1e75 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java @@ -79,6 +79,7 @@ class KudosControllerTest extends TestContainersSuite implements KudosFixture, T private String message; private MemberProfile sender; + private MemberProfile recipient; private MemberProfile admin; private UUID senderId; private String senderWorkEmail; @@ -94,7 +95,7 @@ void setUp() { senderId = sender.getId(); senderWorkEmail = sender.getWorkEmail(); - MemberProfile recipient = createASecondDefaultMemberProfile(); + recipient = createASecondDefaultMemberProfile(); recipientMembers = List.of(recipient); admin = createAThirdDefaultMemberProfile(); @@ -230,7 +231,7 @@ void testApproveKudos() throws JsonProcessingException { assertNull(kudos.getDateApproved()); KudosRecipient recipient = createKudosRecipient(kudos.getId(), recipientMembers.getFirst().getId()); - final HttpRequest request = HttpRequest.PUT("", kudos).basicAuth(admin.getWorkEmail(), ADMIN_ROLE); + final HttpRequest request = HttpRequest.PUT("/approve", kudos).basicAuth(admin.getWorkEmail(), ADMIN_ROLE); final HttpResponse response = client.exchange(request, Kudos.class); assertEquals(HttpStatus.OK, response.getStatus()); @@ -300,7 +301,7 @@ void testApproveNonExistentKudos() { UUID nonExistentKudosId = UUID.randomUUID(); kudos.setId(nonExistentKudosId); - final HttpRequest request = HttpRequest.PUT("", kudos).basicAuth(admin.getWorkEmail(), ADMIN_ROLE); + final HttpRequest request = HttpRequest.PUT("/approve", kudos).basicAuth(admin.getWorkEmail(), ADMIN_ROLE); HttpClientResponseException responseException = assertThrows(HttpClientResponseException.class, () -> client.exchange(request, Kudos.class)); assertEquals(HttpStatus.BAD_REQUEST, responseException.getStatus()); @@ -312,7 +313,7 @@ void testApproveNonExistentKudos() { void testApproveAlreadyApprovedKudos() { Kudos kudos = createApprovedKudos(senderId); - final HttpRequest request = HttpRequest.PUT("", kudos).basicAuth(admin.getWorkEmail(), ADMIN_ROLE); + final HttpRequest request = HttpRequest.PUT("/approve", kudos).basicAuth(admin.getWorkEmail(), ADMIN_ROLE); HttpClientResponseException responseException = assertThrows(HttpClientResponseException.class, () -> client.exchange(request, Kudos.class)); assertEquals(HttpStatus.BAD_REQUEST, responseException.getStatus()); @@ -324,11 +325,10 @@ void testApproveAlreadyApprovedKudos() { void testApproveKudosWithoutAdministerPermission() { Kudos kudos = createADefaultKudos(senderId); - final HttpRequest request = HttpRequest.PUT("", kudos).basicAuth(senderWorkEmail, MEMBER_ROLE); + final HttpRequest request = HttpRequest.PUT("/approve", kudos).basicAuth(senderWorkEmail, MEMBER_ROLE); HttpClientResponseException responseException = assertThrows(HttpClientResponseException.class, () -> client.exchange(request, Kudos.class)); assertEquals(HttpStatus.FORBIDDEN, responseException.getStatus()); - assertEquals("Forbidden", responseException.getMessage()); assertEquals(0, emailSender.events.size()); } @@ -564,10 +564,9 @@ void testDeleteKudosWithNonExistentKudosId() { void testDeleteKudosWithoutAdministerPermission() { Kudos kudos = createADefaultKudos(senderId); - HttpRequest request = HttpRequest.DELETE(String.format("/%s", kudos.getId())).basicAuth(senderWorkEmail, MEMBER_ROLE); + HttpRequest request = HttpRequest.DELETE(String.format("/%s", kudos.getId())).basicAuth(recipient.getWorkEmail(), MEMBER_ROLE); HttpClientResponseException responseException = assertThrows(HttpClientResponseException.class, () -> client.exchange(request)); assertEquals(HttpStatus.FORBIDDEN, responseException.getStatus()); - assertEquals("Forbidden", responseException.getMessage()); } } From 2457660cfad5a97deaf196d786b2100cbd5c4499 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Wed, 12 Feb 2025 10:26:44 -0600 Subject: [PATCH 16/62] Added the kudos update DTO and a test for updating kudos. --- .../services/kudos/KudosController.java | 2 +- .../services/kudos/KudosServices.java | 2 +- .../services/kudos/KudosServicesImpl.java | 3 +- .../services/kudos/KudosUpdateDTO.java | 31 +++++++++ .../services/kudos/KudosControllerTest.java | 69 ++++++++++++++++++- .../src/components/kudos_card/KudosCard.jsx | 23 ++++--- 6 files changed, 115 insertions(+), 15 deletions(-) create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosUpdateDTO.java diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosController.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosController.java index 7db280cbe..2e8119011 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosController.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosController.java @@ -39,7 +39,7 @@ public Kudos create(@Body @Valid KudosCreateDTO kudos) { } @Put - public Kudos update(@Body @Valid KudosResponseDTO kudos) { + public Kudos update(@Body @Valid KudosUpdateDTO kudos) { return kudosServices.update(kudos); } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServices.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServices.java index 85e31970e..9f736055c 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServices.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServices.java @@ -9,7 +9,7 @@ public interface KudosServices { Kudos save(KudosCreateDTO kudos); - Kudos update(KudosResponseDTO kudos); + Kudos update(KudosUpdateDTO kudos); Kudos approve(Kudos kudos); diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java index e21eb1af7..cc07209f1 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java @@ -150,7 +150,7 @@ public Kudos approve(Kudos kudos) { } @Override - public Kudos update(KudosResponseDTO kudos) { + public Kudos update(KudosUpdateDTO kudos) { // Find the corresponding kudos and make sure we have permission. final UUID kudosId = kudos.getId(); final Kudos existingKudos = @@ -171,6 +171,7 @@ public Kudos update(KudosResponseDTO kudos) { if (existingPublic && !proposedPublic) { // TODO: Somehow find and remove the Slack Kudos that the Check-Ins // Integration posted. + existingKudos.setDateApproved(LocalDate.now()); } else if (!existingPublic && proposedPublic) { // Clear the date approved when going from private to public. existingKudos.setDateApproved(null); diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosUpdateDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosUpdateDTO.java new file mode 100644 index 000000000..38ea0a0ed --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosUpdateDTO.java @@ -0,0 +1,31 @@ +package com.objectcomputing.checkins.services.kudos; + +import com.objectcomputing.checkins.services.memberprofile.MemberProfile; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.Nullable; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; +import java.util.UUID; + +@Getter +@Setter +@AllArgsConstructor +@Introspected +public class KudosUpdateDTO { + @NotNull + private UUID id; + + @NotBlank + private String message; + + @Nullable + private Boolean publiclyVisible; + + @NotNull + private List recipientMembers; +} diff --git a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java index ac73d1e75..fdd23cdd5 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java @@ -41,6 +41,7 @@ import java.time.LocalDate; import java.util.Collections; import java.util.List; +import java.util.ArrayList; import java.util.Optional; import java.util.UUID; @@ -80,6 +81,7 @@ class KudosControllerTest extends TestContainersSuite implements KudosFixture, T private String message; private MemberProfile sender; private MemberProfile recipient; + private MemberProfile other; private MemberProfile admin; private UUID senderId; private String senderWorkEmail; @@ -101,6 +103,8 @@ void setUp() { admin = createAThirdDefaultMemberProfile(); assignAdminRole(admin); + other = createAnotherSupervisor(); + Team team = createDefaultTeam(); teamId = team.getId(); @@ -116,7 +120,7 @@ void setUp() { "true, false", "true, true" }) - void testCreateKudos(boolean supplyTeam, boolean publiclyVisible) { + Kudos testCreateKudos(boolean supplyTeam, boolean publiclyVisible) { KudosCreateDTO kudosCreateDTO = new KudosCreateDTO( message, senderId, @@ -128,7 +132,7 @@ void testCreateKudos(boolean supplyTeam, boolean publiclyVisible) { HttpRequest request = HttpRequest.POST("/", kudosCreateDTO).basicAuth(senderWorkEmail, MEMBER_ROLE); HttpResponse httpResponse = client.exchange(request, Kudos.class); - Kudos kudos = httpResponse.body(); + final Kudos kudos = httpResponse.body(); assertEquals(message, kudos.getMessage()); assertEquals(publiclyVisible, kudos.getPubliclyVisible()); assertEquals(senderId, kudos.getSenderId()); @@ -167,6 +171,7 @@ void testCreateKudos(boolean supplyTeam, boolean publiclyVisible) { emailSender.events.getFirst() ); } + return kudos; } @Test @@ -569,4 +574,64 @@ void testDeleteKudosWithoutAdministerPermission() { assertEquals(HttpStatus.FORBIDDEN, responseException.getStatus()); } + + @ParameterizedTest + @CsvSource({ + "false, false", + "false, true", + "true, false", + "true, true" + }) + void testUpdateKudos(boolean supplyTeam, boolean publiclyVisible) { + // Create a kudos + final Kudos kudos = testCreateKudos(supplyTeam, publiclyVisible); + + // Set of changes to make. + final String message = "New kudos message"; + final boolean visible = !publiclyVisible; + final List members = new ArrayList<>(); + members.add(other); + if (!supplyTeam || publiclyVisible) { + // On some tests, retain the original recipient. + members.add(recipient); + } + + // Create the DTO + KudosUpdateDTO proposed = new KudosUpdateDTO(kudos.getId(), message, + visible, members); + + // Make the call + final HttpRequest request = + HttpRequest.PUT("", proposed) + .basicAuth(senderWorkEmail, MEMBER_ROLE); + final HttpResponse response = client.exchange(request, + Kudos.class); + assertEquals(HttpStatus.OK, response.getStatus()); + + final Kudos updated = response.body(); + assertEquals(message, updated.getMessage()); + assertEquals(visible, updated.getPubliclyVisible()); + + if (visible) { + // Public kudos should not be approved. + assertNull(updated.getDateApproved()); + } else { + // Private kudos should be approved. + assertNotNull(updated.getDateApproved()); + } + + final List kudosRecipients = + findKudosRecipientByKudosId(updated.getId()); + assertEquals(members.size(), kudosRecipients.size()); + for (MemberProfile member : members) { + boolean found = false; + for (KudosRecipient recipient : kudosRecipients) { + if (recipient.getMemberId().equals(member.getId())) { + found = true; + } + } + assertTrue(found); + } + } + } diff --git a/web-ui/src/components/kudos_card/KudosCard.jsx b/web-ui/src/components/kudos_card/KudosCard.jsx index 1903b9e22..d22df1bb7 100644 --- a/web-ui/src/components/kudos_card/KudosCard.jsx +++ b/web-ui/src/components/kudos_card/KudosCard.jsx @@ -156,29 +156,32 @@ const KudosCard = ({ kudos, includeActions, includeEdit, onKudosAction }) => { setEditDialogOpen(false); // Update the modifiable parts. - kudos.message = kudosMessage; - kudos.publiclyVisible = kudosPublic; - kudos.recipientMembers = kudosRecipientMembers; + const proposed = { + id: kudos.id, + message: kudosMessage, + publiclyVisible: kudosPublic, + recipientMembers: kudosRecipientMembers, + }; // Update on the server. - const res = await updateKudos(kudos, csrf); - if (!res.error) { + const res = await updateKudos(proposed, csrf); + if (res.error) { dispatch({ type: UPDATE_TOAST, payload: { - severity: "success", - toast: "Kudos Updated", + severity: "error", + toast: "Failed to update kudos", }, }); - onKudosAction && onKudosAction(); } else { dispatch({ type: UPDATE_TOAST, payload: { - severity: "error", - toast: "Failed to update kudos", + severity: "success", + toast: "Kudos Updated", }, }); + onKudosAction && onKudosAction(); } }, [kudos, kudosMessage, kudosPublic, kudosRecipientMembers, csrf, dispatch, onKudosAction]); From 5a753993f629017bf00da928d2e32051ea65707d Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Wed, 12 Feb 2025 11:04:41 -0600 Subject: [PATCH 17/62] Added a check for at least one recipient and more tests. --- .../services/kudos/KudosServicesImpl.java | 6 +++ .../services/kudos/KudosControllerTest.java | 52 +++++++++++++++++++ .../src/components/kudos_card/KudosCard.jsx | 4 +- 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java index cc07209f1..1376dd5fe 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java @@ -163,6 +163,12 @@ public Kudos update(KudosUpdateDTO kudos) { throw new PermissionException(NOT_AUTHORIZED_MSG); } + if (kudos.getRecipientMembers() == null || + kudos.getRecipientMembers().isEmpty()) { + throw new BadArgException( + "Kudos must contain at least one recipient"); + } + // Begin modifying the existing kudos to reflect desired changes. existingKudos.setMessage(kudos.getMessage()); diff --git a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java index fdd23cdd5..70379a7ec 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java @@ -634,4 +634,56 @@ void testUpdateKudos(boolean supplyTeam, boolean publiclyVisible) { } } + @Test + void testUpdateKudosNoPermission() { + // Create a kudos + final Kudos kudos = testCreateKudos(false, true); + + KudosUpdateDTO proposed = new KudosUpdateDTO(kudos.getId(), + kudos.getMessage(), + false, recipientMembers); + final HttpRequest request = + HttpRequest.PUT("", proposed).basicAuth(other.getWorkEmail(), + MEMBER_ROLE); + final HttpClientResponseException responseException = + assertThrows(HttpClientResponseException.class, + () -> client.exchange(request, Kudos.class)); + + assertEquals(HttpStatus.FORBIDDEN, responseException.getStatus()); + } + + @Test + void testUpdateKudosAdminPermission() { + // Create a kudos + final Kudos kudos = testCreateKudos(false, true); + + KudosUpdateDTO proposed = new KudosUpdateDTO(kudos.getId(), + kudos.getMessage(), + false, recipientMembers); + final HttpRequest request = + HttpRequest.PUT("", proposed).basicAuth(admin.getWorkEmail(), + ADMIN_ROLE); + final HttpResponse response = client.exchange(request, + Kudos.class); + assertEquals(HttpStatus.OK, response.getStatus()); + } + + @Test + void testUpdateKudosNoMembers() { + // Create a kudos + final Kudos kudos = testCreateKudos(false, true); + + final List members = new ArrayList<>(); + KudosUpdateDTO proposed = new KudosUpdateDTO(kudos.getId(), + kudos.getMessage(), + false, members); + final HttpRequest request = + HttpRequest.PUT("", proposed).basicAuth(senderWorkEmail, + MEMBER_ROLE); + final HttpClientResponseException responseException = + assertThrows(HttpClientResponseException.class, + () -> client.exchange(request, Kudos.class)); + + assertEquals(HttpStatus.BAD_REQUEST, responseException.getStatus()); + } } diff --git a/web-ui/src/components/kudos_card/KudosCard.jsx b/web-ui/src/components/kudos_card/KudosCard.jsx index d22df1bb7..c6a9a6ab2 100644 --- a/web-ui/src/components/kudos_card/KudosCard.jsx +++ b/web-ui/src/components/kudos_card/KudosCard.jsx @@ -344,7 +344,9 @@ const KudosCard = ({ kudos, includeActions, includeEdit, onKudosAction }) => { Cancel From c2c3a0d12d55c122921138d044e37b9dfde26438 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Wed, 12 Feb 2025 11:22:08 -0600 Subject: [PATCH 18/62] Fixed lack of update after updating sent kudos. --- web-ui/src/pages/KudosPage.jsx | 47 +++++++++++++++++----------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/web-ui/src/pages/KudosPage.jsx b/web-ui/src/pages/KudosPage.jsx index bb381b42b..db7ecf672 100644 --- a/web-ui/src/pages/KudosPage.jsx +++ b/web-ui/src/pages/KudosPage.jsx @@ -71,25 +71,29 @@ const KudosPage = () => { } }, [csrf, dispatch, currentUser.id]); + const loadAndSetReceivedKudos = () => { + loadReceivedKudos().then((data) => { + if (data) { + const filtered = data.filter((kudo) => + kudo.recipientMembers.some((member) => member.id === currentUser.id) + ); + setReceivedKudos(filtered); + } + }); + }; + + const loadAndSetSentKudos = () => { + loadSentKudos().then((data) => { + if (data) { + setSentKudos(data.filter((kudo) => kudo.senderId === currentUser.id)); + } + }); + }; + useEffect(() => { if (csrf && currentUser && currentUser.id) { - loadReceivedKudos().then((data) => { - if (data) { - let filtered = data.filter((kudo) => - kudo.recipientMembers.some((member) => member.id === currentUser.id) - ); - setReceivedKudos(filtered); - } - }); - - loadSentKudos().then((data) => { - if (data) { - let filtered = data.filter( - (kudo) => kudo.senderId === currentUser.id - ); - setSentKudos(filtered); - } - }); + loadAndSetReceivedKudos(); + loadAndSetSentKudos(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [csrf, currentUser, kudosTab]); @@ -161,12 +165,7 @@ const KudosPage = () => { { - const updatedKudos = receivedKudos.filter( - (pk) => pk.id !== k.id - ); - setReceivedKudos(updatedKudos); - }} + onKudosAction={loadAndSetReceivedKudos} /> ))} @@ -187,7 +186,7 @@ const KudosPage = () => {
{sentKudos.map((k) => ( + onKudosAction={loadAndSetSentKudos}/> ))}
) : ( From 4d70e1f7d499db5aa8fea271d8ff5d404d78f4b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Feb 2025 20:40:33 +0000 Subject: [PATCH 19/62] Bump koa from 2.15.3 to 2.15.4 in /web-ui Bumps [koa](https://github.com/koajs/koa) from 2.15.3 to 2.15.4. - [Release notes](https://github.com/koajs/koa/releases) - [Changelog](https://github.com/koajs/koa/blob/2.15.4/History.md) - [Commits](https://github.com/koajs/koa/compare/2.15.3...2.15.4) --- updated-dependencies: - dependency-name: koa dependency-type: indirect ... Signed-off-by: dependabot[bot] --- web-ui/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web-ui/yarn.lock b/web-ui/yarn.lock index d063a83b1..84f375764 100644 --- a/web-ui/yarn.lock +++ b/web-ui/yarn.lock @@ -4917,9 +4917,9 @@ koa-convert@^2.0.0: koa-compose "^4.1.0" koa@^2.15.3: - version "2.15.3" - resolved "https://registry.yarnpkg.com/koa/-/koa-2.15.3.tgz#062809266ee75ce0c75f6510a005b0e38f8c519a" - integrity sha512-j/8tY9j5t+GVMLeioLaxweJiKUayFhlGqNTzf2ZGwL0ZCQijd2RLHK0SLW5Tsko8YyyqCZC2cojIb0/s62qTAg== + version "2.15.4" + resolved "https://registry.yarnpkg.com/koa/-/koa-2.15.4.tgz#7000b3d8354558671adb1ba1b1c09bedb5f8da75" + integrity sha512-7fNBIdrU2PEgLljXoPWoyY4r1e+ToWCmzS/wwMPbUNs7X+5MMET1ObhJBlUkF5uZG9B6QhM2zS1TsH6adegkiQ== dependencies: accepts "^1.3.5" cache-content-type "^1.0.0" From 544b5ec5d174d687e5719c964fac6b20b8df2c35 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Thu, 13 Feb 2025 07:45:51 -0600 Subject: [PATCH 20/62] Clear approval for public kudos if the text changes. --- .../checkins/services/kudos/KudosServicesImpl.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java index 1376dd5fe..605d82053 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java @@ -170,6 +170,7 @@ public Kudos update(KudosUpdateDTO kudos) { } // Begin modifying the existing kudos to reflect desired changes. + final String originalMessage = existingKudos.getMessage(); existingKudos.setMessage(kudos.getMessage()); boolean existingPublic = existingKudos.getPubliclyVisible(); @@ -182,6 +183,11 @@ public Kudos update(KudosUpdateDTO kudos) { // Clear the date approved when going from private to public. existingKudos.setDateApproved(null); } + if (proposedPublic && + !originalMessage.equals(existingKudos.getMessage())) { + // If public and the text changed, require approval again. + existingKudos.setDateApproved(null); + } existingKudos.setPubliclyVisible(kudos.getPubliclyVisible()); From 32877512e306657dffe7ad56cc4dec7385e2639d Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Thu, 13 Feb 2025 07:47:39 -0600 Subject: [PATCH 21/62] Remove the unused reason reason text field. --- web-ui/src/components/kudos_card/KudosCard.jsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/web-ui/src/components/kudos_card/KudosCard.jsx b/web-ui/src/components/kudos_card/KudosCard.jsx index c6a9a6ab2..87bfeb7d1 100644 --- a/web-ui/src/components/kudos_card/KudosCard.jsx +++ b/web-ui/src/components/kudos_card/KudosCard.jsx @@ -288,15 +288,6 @@ const KudosCard = ({ kudos, includeActions, includeEdit, onKudosAction }) => { Are you sure you want to complete this action? The kudos will be deleted. - - + + + ); } else if (dateApproved) { info.push( - - Received{" "} - {dateApproved ? dateUtils.format(dateApproved, "MM/dd/yyyy") : ""} - + + Received{" "} + {dateApproved ? dateUtils.format(dateApproved, "MM/dd/yyyy") : ""} + ); } else { const dateCreated = new Date(kudos.dateCreated.join("/")); - info.push( + if (kudos.publiclyVisible) { + info.push( Pending - ); + ); + } info.push( - - Created {dateUtils.format(dateCreated, "MM/dd/yyyy")} - + + Created {dateUtils.format(dateCreated, "MM/dd/yyyy")} + ); } if (includeEdit) { actions.push( - - - + + + ); } if (includeActions || includeEdit) { actions.push( - - - + + + ); } return <> From 7d905d12172d5df9d3b28cca0f358c1481dad1c0 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Thu, 13 Feb 2025 11:00:55 -0600 Subject: [PATCH 23/62] Added a filter for kudos, similar to the feedback filter. --- web-ui/src/pages/KudosPage.css | 1 + web-ui/src/pages/KudosPage.jsx | 61 +++++++++++++++++++++++++++++++--- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/web-ui/src/pages/KudosPage.css b/web-ui/src/pages/KudosPage.css index ed9bab05d..f6dd9dd14 100644 --- a/web-ui/src/pages/KudosPage.css +++ b/web-ui/src/pages/KudosPage.css @@ -18,6 +18,7 @@ .kudos-page .kudos-page-header { display: flex; + gap: .5em; justify-content: flex-end; } diff --git a/web-ui/src/pages/KudosPage.jsx b/web-ui/src/pages/KudosPage.jsx index db7ecf672..287963147 100644 --- a/web-ui/src/pages/KudosPage.jsx +++ b/web-ui/src/pages/KudosPage.jsx @@ -1,6 +1,9 @@ import React, { useCallback, useContext, useEffect, useState } from "react"; import { styled } from "@mui/material/styles"; import { Button, Tab, Typography } from "@mui/material"; +import MenuItem from '@mui/material/MenuItem'; +import FormControl from '@mui/material/FormControl'; +import TextField from '@mui/material/TextField'; import { TabContext, TabList, TabPanel } from "@mui/lab"; import { AppContext } from "../context/AppContext"; import { @@ -40,6 +43,13 @@ const Root = styled("div")({ }, }); +const DateRange = { + THREE_MONTHS: '3mo', + SIX_MONTHS: '6mo', + ONE_YEAR: '1yr', + ALL_TIME: 'all' +}; + const KudosPage = () => { const { state, dispatch } = useContext(AppContext); const csrf = selectCsrfToken(state); @@ -52,24 +62,48 @@ const KudosPage = () => { const [receivedKudosLoading, setReceivedKudosLoading] = useState(true); const [sentKudos, setSentKudos] = useState([]); const [sentKudosLoading, setSentKudosLoading] = useState(true); + const [dateRange, setDateRange] = useState(DateRange.THREE_MONTHS); + + const isInRange = (requestDate) => { + const oldestDate = new Date(); + switch (dateRange) { + case DateRange.SIX_MONTHS: + oldestDate.setMonth(oldestDate.getMonth() - 6); + break; + case DateRange.ONE_YEAR: + oldestDate.setFullYear(oldestDate.getFullYear() - 1); + break; + case DateRange.ALL_TIME: + return true; + case DateRange.THREE_MONTHS: + default: + oldestDate.setMonth(oldestDate.getMonth() - 3); + } + + if (Array.isArray(requestDate)) { + requestDate = new Date(requestDate.join('/')); + // have to do for Safari + } + return requestDate >= oldestDate; + }; const loadReceivedKudos = useCallback(async () => { setReceivedKudosLoading(true); const res = await getReceivedKudos(currentUser.id, csrf); if (res?.payload?.data && !res.error) { setReceivedKudosLoading(false); - return res.payload.data; + return res.payload.data.filter((k) => isInRange(k.dateCreated)); } - }, [csrf, dispatch, currentUser.id]); + }, [csrf, dispatch, currentUser.id, dateRange]); const loadSentKudos = useCallback(async () => { setSentKudosLoading(true); const res = await getSentKudos(currentUser.id, csrf); if (res?.payload?.data && !res.error) { setSentKudosLoading(false); - return res.payload.data; + return res.payload.data.filter((k) => isInRange(k.dateCreated)); } - }, [csrf, dispatch, currentUser.id]); + }, [csrf, dispatch, currentUser.id, dateRange]); const loadAndSetReceivedKudos = () => { loadReceivedKudos().then((data) => { @@ -96,7 +130,7 @@ const KudosPage = () => { loadAndSetSentKudos(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [csrf, currentUser, kudosTab]); + }, [csrf, currentUser, kudosTab, dateRange]); const handleTabChange = useCallback( (event, newTab) => { @@ -124,6 +158,23 @@ const KudosPage = () => { open={kudosDialogOpen} onClose={() => setKudosDialogOpen(false)} /> + + setDateRange(e.target.value)} + value={dateRange} + variant="outlined" + > + Past 3 months + Past 6 months + Past year + All time + + {selectHasCreateKudosPermission(state) && + + + + + +
+
+
+
+
+

+ Brock and Brockman did a great job helping Clark, Jimmy Olsen, Jimmy T. Olsen, and Johnson +

+
+

+ Members: +

+
+
+ +
+ + Jimmy Johnson + +
+
+
+ +
+ + Jimmy Olsen + +
+
+
+ +
+ + Clark Kent + +
+
+
+ +
+ + Brock Smith + +
+
+
+ +
+ + Kent Brockman + +
+
+
+ +
+ + Jimmy Olsen + +
+
+
+
+
+
+ + +`; From f7a6851fe759d0c8bb404b239fdf1c9d87f4d7db Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 17 Feb 2025 10:32:51 -0600 Subject: [PATCH 37/62] Updated the KudosCard test to include an active and an inactive sender. --- .../components/kudos_card/KudosCard.test.jsx | 34 +++- .../__snapshots__/KudosCard.test.jsx.snap | 157 +++++++++++++++++- 2 files changed, 187 insertions(+), 4 deletions(-) diff --git a/web-ui/src/components/kudos_card/KudosCard.test.jsx b/web-ui/src/components/kudos_card/KudosCard.test.jsx index 44589a747..f411e0441 100644 --- a/web-ui/src/components/kudos_card/KudosCard.test.jsx +++ b/web-ui/src/components/kudos_card/KudosCard.test.jsx @@ -69,8 +69,8 @@ const initialState = { } }; -const kudos = { - id: 'test-kudos', +const terminated = { + id: 'test-terminated-kudos', message: "Brock and Brockman did a great job helping Clark, Jimmy Olsen, Jimmy T. Olsen, and Johnson", senderId: "5", dateCreated: [ 2025, 2, 14 ], @@ -115,7 +115,35 @@ const kudos = { ], }; -it('renders correctly', () => { +const kudos = { + id: 'test-kudos', + message: "Jimmy is awesome!", + senderId: "1", + dateCreated: [ 2025, 2, 17 ], + recipientMembers: [ + { + id: "2", + firstName: 'Jimmy', + lastName: 'Olsen', + role: ['MEMBER'], + }, + ], +}; + +it('inactive renders correctly', () => { + snapshot( + + {}} + /> + + ); +}); + +it('active renders correctly', () => { snapshot( +
+
+
+
+
+ +
+ + Jimmy Olsen + +
+

+ received kudos from +

+
+
+ +
+ + Jimmy Johnson + +
+
+
+
+ + + +
+
+
+
+
+
+
+
+

+ Jimmy is awesome! +

+
+
+
+
+
+ +`; + +exports[`inactive renders correctly 1`] = `
Date: Mon, 17 Feb 2025 11:20:43 -0600 Subject: [PATCH 38/62] If a supervisor is terminated, this would blow up on the check for supervisor id. --- web-ui/src/components/reviews/TeamReviews.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-ui/src/components/reviews/TeamReviews.jsx b/web-ui/src/components/reviews/TeamReviews.jsx index ef70a54d5..87a30b468 100644 --- a/web-ui/src/components/reviews/TeamReviews.jsx +++ b/web-ui/src/components/reviews/TeamReviews.jsx @@ -172,7 +172,7 @@ const TeamReviews = ({ onBack, periodId }) => { useEffect(() => { const myId = currentUser?.id; const supervisors = selectSupervisors(state); - const isManager = supervisors.some(s => s.id === myId); + const isManager = supervisors.some(s => s?.id === myId); const period = selectReviewPeriod(state, periodId); if (period) { setApprovalState(period.reviewStatus === ReviewStatus.AWAITING_APPROVAL); From 1b5145dc30655af7fd6b1d50d6603d3342f1e0b3 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 17 Feb 2025 11:21:19 -0600 Subject: [PATCH 39/62] Allow a user to view their self-review for closed review periods. --- web-ui/src/components/reviews/periods/ReviewPeriods.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web-ui/src/components/reviews/periods/ReviewPeriods.jsx b/web-ui/src/components/reviews/periods/ReviewPeriods.jsx index b62d2e980..cedd2d151 100644 --- a/web-ui/src/components/reviews/periods/ReviewPeriods.jsx +++ b/web-ui/src/components/reviews/periods/ReviewPeriods.jsx @@ -327,8 +327,13 @@ const ReviewPeriods = ({ onPeriodSelected, mode }) => { case ReviewStatus.OPEN: onPeriodSelected(id); break; + case ReviewStatus.CLOSED: + if (mode === selfReviewMode) { + onPeriodSelected(id); + } + break; default: - // We do nothing if the status is CLOSED or UNKNOWN. + // We do nothing if the status is UNKNOWN. break; } }, From 918a1d080c6165d66d13190932ae56982f6ca727 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 17 Feb 2025 13:17:43 -0600 Subject: [PATCH 40/62] No need to call the kudos creator if the message list is empty. --- .../checkins/services/slack/kudos/KudosChannelReader.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java index 1983b0c65..a589d80bc 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java @@ -52,6 +52,8 @@ public void readChannel() { kudosChannelReadTimeStore.save(new KudosChannelReadTime()); } - slackKudosCreator.store(messages); + if (!messages.isEmpty()) { + slackKudosCreator.store(messages); + } } } From 67c8df8cd9d7965f31dbc05c3af72eb47db210a1 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 17 Feb 2025 13:28:47 -0600 Subject: [PATCH 41/62] Switch from a scheduled task to being triggered when the check services runs. --- .../services/request_notifications/CheckServicesImpl.java | 8 +++++++- .../checkins/services/slack/kudos/KudosChannelReader.java | 3 --- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImpl.java index baba523e3..70f76665a 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImpl.java @@ -5,6 +5,8 @@ import com.objectcomputing.checkins.services.feedback_request.FeedbackRequestServicesImpl; import com.objectcomputing.checkins.services.reviews.ReviewPeriodServices; import com.objectcomputing.checkins.services.pulse.PulseServices; +import com.objectcomputing.checkins.services.slack.kudos.KudosChannelReader; + import jakarta.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,15 +22,18 @@ public class CheckServicesImpl implements CheckServices { private final FeedbackRequestRepository feedbackRequestRepository; private final PulseServices pulseServices; private final ReviewPeriodServices reviewPeriodServices; + private final KudosChannelReader kudosChannelReader; public CheckServicesImpl(FeedbackRequestServicesImpl feedbackRequestServices, FeedbackRequestRepository feedbackRequestRepository, PulseServices pulseServices, - ReviewPeriodServices reviewPeriodServices) { + ReviewPeriodServices reviewPeriodServices, + KudosChannelReader kudosChannelReader) { this.feedbackRequestServices = feedbackRequestServices; this.feedbackRequestRepository = feedbackRequestRepository; this.pulseServices = pulseServices; this.reviewPeriodServices = reviewPeriodServices; + this.kudosChannelReader = kudosChannelReader; } @Override @@ -43,6 +48,7 @@ public boolean sendScheduledEmails() { } pulseServices.notifyUsers(today); reviewPeriodServices.sendNotifications(today); + kudosChannelReader.readChannel(); return true; } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java index a589d80bc..faa62ebfb 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/KudosChannelReader.java @@ -5,8 +5,6 @@ import com.slack.api.model.Message; -import io.micronaut.scheduling.annotation.Scheduled; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,7 +31,6 @@ public class KudosChannelReader { @Inject private SlackKudosCreator slackKudosCreator; - @Scheduled(fixedDelay = "10m") public void readChannel() { Optional readTime = kudosChannelReadTimeStore.findById(KudosChannelReadTime.key); From 00cc1ddb10bd85846e1b9199da362933a81fc19a Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 17 Feb 2025 14:06:30 -0600 Subject: [PATCH 42/62] Use the SlackReaderReplacement for the native test. --- .../request_notifications/CheckServicesImplTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImplTest.java b/server/src/test/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImplTest.java index ae589d972..6ca02c27f 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImplTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImplTest.java @@ -13,6 +13,7 @@ import com.objectcomputing.checkins.services.fixture.MemberProfileFixture; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import com.objectcomputing.checkins.services.CurrentUserServicesReplacement; +import com.objectcomputing.checkins.services.SlackReaderReplacement; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -30,11 +31,15 @@ @Property(name = "replace.mailjet.factory", value = StringUtils.TRUE) @Property(name = "replace.currentuserservices", value = StringUtils.TRUE) +@Property(name = "replace.slackreader", value = StringUtils.TRUE) class CheckServicesImplTest extends TestContainersSuite implements FeedbackTemplateFixture, FeedbackRequestFixture, MemberProfileFixture, RoleFixture { @Inject CurrentUserServicesReplacement currentUserServices; + @Inject + private SlackReaderReplacement slackReader; + @Inject @Named(MailJetFactory.MJML_FORMAT) private MailJetFactoryReplacement.MockEmailSender emailSender; From 7a371a0c34344b6fdeaae872ee21d5053ef01cf7 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 17 Feb 2025 14:11:36 -0600 Subject: [PATCH 43/62] Skip messages posted by bot users too. --- .../checkins/services/slack/kudos/SlackKudosCreator.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java index 84c3183e4..254df1cad 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/SlackKudosCreator.java @@ -50,8 +50,10 @@ public class SlackKudosCreator { public void store(List messages) { for (Message message : messages) { - // User messages do not have a sub-type. - if (message.getSubtype() == null) { + // User messages do not have a sub-type. A bot user can send + // messages. They will not have a subtype, but they will have a bot + // id. We want to skip those too. + if (message.getSubtype() == null && message.getBotId() == null) { try { AutomatedKudosDTO kudosDTO = createFromMessage(message); if (kudosDTO.getRecipientIds().size() == 0) { @@ -74,7 +76,7 @@ public void store(List messages) { private AutomatedKudosDTO createFromMessage(Message message) { String userId = message.getUser(); - MemberProfile sender = lookupUser(userId); + MemberProfile sender = lookupUser(userId); List recipients = new ArrayList<>(); String text = processText(message.getText(), recipients); return new AutomatedKudosDTO(text, userId, sender.getId(), recipients); From 772a7114d3dc8bb34262241839f3e5fa645d65ab Mon Sep 17 00:00:00 2001 From: Michael Kimberlin Date: Mon, 17 Feb 2025 15:57:16 -0600 Subject: [PATCH 44/62] Converted selectActiveOrInactiveProfile to a standard selector --- web-ui/src/context/selectors.js | 17 +++---- web-ui/src/context/selectors.test.js | 75 +++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 11 deletions(-) diff --git a/web-ui/src/context/selectors.js b/web-ui/src/context/selectors.js index e7ad32f64..4c046af60 100644 --- a/web-ui/src/context/selectors.js +++ b/web-ui/src/context/selectors.js @@ -1,7 +1,7 @@ import { createSelector } from 'reselect'; export const selectMemberProfiles = state => state.memberProfiles || []; -export const selectTerminatedMembers = state => state.terminatedMembers; +export const selectTerminatedMembers = state => state.terminatedMembers || []; export const selectMemberSkills = state => state.memberSkills || []; export const selectSkills = state => state.skills || []; export const selectTeamMembers = state => state.teamMembers; @@ -327,15 +327,12 @@ export const selectProfile = createSelector( (profileMap, profileId) => profileMap[profileId] ); -export const selectActiveOrInactiveProfile = (state, profileId) => { - // See if the profile is active, return it if so. - const sender = selectProfile(state, profileId); - if (sender) return sender; - - // The profile is inactive or does not exist. Check terminated members. - const terminatedMap = selectProfileMapForTerminatedMembers(state); - return terminatedMap[profileId]; -}; +export const selectActiveOrInactiveProfile = createSelector( + selectProfileMap, + selectProfileMapForTerminatedMembers, + (state, profileId) => profileId, + (profileMap, termedProfileMap, profileId) => profileMap[profileId] || termedProfileMap[profileId] +); export const selectSkill = createSelector( selectSkills, diff --git a/web-ui/src/context/selectors.test.js b/web-ui/src/context/selectors.test.js index 0ba8e98cd..3b8b0d25f 100644 --- a/web-ui/src/context/selectors.test.js +++ b/web-ui/src/context/selectors.test.js @@ -20,7 +20,8 @@ import { selectSupervisorHierarchyIds, selectSubordinates, selectIsSubordinateOfCurrentUser, - selectHasReportPermission + selectHasReportPermission, + selectActiveOrInactiveProfile } from './selectors'; describe('Selectors', () => { @@ -1525,4 +1526,76 @@ describe('Selectors', () => { expect(selectHasReportPermission(testState)).toBe(false); }); + + it('selectActiveOrInactiveProfile should a profile if active or inactive', () => { + const activeTestMember = { + id: 1, + bioText: 'foo', + employeeId: 11, + name: 'A Person', + firstName: 'A', + lastName: 'PersonA', + location: 'St Louis', + title: 'engineer', + workEmail: 'employee@sample.com', + pdlId: 9, + startDate: [2012, 9, 29], + }; + const inactiveTestMember = { + id: 2, + bioText: 'foo', + employeeId: 12, + name: 'B Person', + firstName: 'B', + lastName: 'PersonB', + location: 'St Louis', + title: 'engineer', + workEmail: 'employee@sample.com', + pdlId: 9, + startDate: [2012, 9, 29], + terminationDate: [2013, 9, 29], + }; + /** @type MemberProfile[] */ + const testActiveMemberProfiles = [ + activeTestMember, + { + id: 3, + bioText: 'foo', + employeeId: 13, + name: 'C Person', + firstName: 'C', + lastName: 'PersonC', + location: 'St Louis', + title: 'engineer', + workEmail: 'employee@sample.com', + pdlId: 9, + startDate: [2012, 9, 29], + } + ]; + /** @type MemberProfile[] */ + const testInactiveMemberProfiles = [ + inactiveTestMember, + { + id: 4, + bioText: 'foo', + employeeId: 13, + name: 'D Person', + firstName: 'D', + lastName: 'PersonD', + location: 'St Louis', + title: 'engineer', + workEmail: 'employee@sample.com', + pdlId: 9, + startDate: [2012, 9, 29], + terminationDate: [2013, 9, 29], + } + ]; + const testState = { + memberProfiles: testActiveMemberProfiles, + terminatedMembers: testInactiveMemberProfiles, + }; + + expect(selectActiveOrInactiveProfile(testState, activeTestMember.id)).toEqual(activeTestMember); + expect(selectActiveOrInactiveProfile(testState, inactiveTestMember.id)).toEqual(inactiveTestMember); + }); }); From 652deda02334558418ae0017ace101a0873780ba Mon Sep 17 00:00:00 2001 From: Michael Kimberlin Date: Mon, 17 Feb 2025 16:44:10 -0600 Subject: [PATCH 45/62] Added permission for view terminated profile and adjusted app context to respect it --- .../services/permissions/Permission.java | 1 + web-ui/src/context/AppContext.jsx | 4 +- web-ui/src/context/selectors.js | 8 ++ web-ui/src/context/selectors.test.js | 83 ++++++++++++++++++- 4 files changed, 93 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java b/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java index ba203ca9a..00ddde2fb 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java @@ -17,6 +17,7 @@ public enum Permission { CAN_ADMINISTER_FEEDBACK_ANSWER("Administer feedback answers", "Feedback"), CAN_ADMINISTER_FEEDBACK_TEMPLATES("Administer feedback templates", "Feedback"), CAN_SEND_EMAIL("Send email", "Feedback"), + CAN_VIEW_TERMINATED_MEMBERS("Can view the profiles of terminated members", "User Management"), CAN_EDIT_ALL_ORGANIZATION_MEMBERS("Edit all member profiles", "User Management"), CAN_DELETE_ORGANIZATION_MEMBERS("Delete organization members", "User Management"), CAN_CREATE_ORGANIZATION_MEMBERS("Create organization members", "User Management"), diff --git a/web-ui/src/context/AppContext.jsx b/web-ui/src/context/AppContext.jsx index 5dd175260..78481c04e 100644 --- a/web-ui/src/context/AppContext.jsx +++ b/web-ui/src/context/AppContext.jsx @@ -23,7 +23,7 @@ import { } from '../api/member'; import { selectCanViewCheckinsPermission, - selectCanEditAllOrganizationMembers, + selectCanViewTerminatedMembers, } from './selectors'; import { getAllRoles, getAllUserRoles } from '../api/roles'; import { getMemberSkills } from '../api/memberskill'; @@ -191,7 +191,7 @@ const AppContextProvider = props => { if (csrf && userProfile && !memberProfiles) { dispatch({ type: UPDATE_PEOPLE_LOADING, payload: true }); getMemberProfiles(); - if (selectCanEditAllOrganizationMembers(state)) { + if (selectCanViewTerminatedMembers(state)) { getTerminatedMembers(); } } diff --git a/web-ui/src/context/selectors.js b/web-ui/src/context/selectors.js index 4c046af60..27d53c1c1 100644 --- a/web-ui/src/context/selectors.js +++ b/web-ui/src/context/selectors.js @@ -242,6 +242,14 @@ export const selectCanEditAllOrganizationMembers = hasPermission( 'CAN_EDIT_ALL_ORGANIZATION_MEMBERS', ); +export const selectCanViewTerminatedMembers = createSelector( + selectCanEditAllOrganizationMembers, + hasPermission( + 'CAN_VIEW_TERMINATED_MEMBERS' + ), + (canEdit, canView) => canEdit || canView +); + export const selectIsPDL = createSelector( selectUserProfile, userProfile => diff --git a/web-ui/src/context/selectors.test.js b/web-ui/src/context/selectors.test.js index 3b8b0d25f..8f6007702 100644 --- a/web-ui/src/context/selectors.test.js +++ b/web-ui/src/context/selectors.test.js @@ -21,7 +21,9 @@ import { selectSubordinates, selectIsSubordinateOfCurrentUser, selectHasReportPermission, - selectActiveOrInactiveProfile + selectActiveOrInactiveProfile, + selectCanEditAllOrganizationMembers, + selectCanViewTerminatedMembers, } from './selectors'; describe('Selectors', () => { @@ -1527,6 +1529,85 @@ describe('Selectors', () => { expect(selectHasReportPermission(testState)).toBe(false); }); + it("selectCanEditAllOrganizationMembers should return false when user does not have 'CAN_EDIT_ALL_ORGANIZATION_MEMBERS' permission", () => { + const testState = { + userProfile: { + firstName: 'Huey', + lastName: 'Emmerich', + role: 'MEMBER', + permissions: [ + { permission: 'CAN_VIEW_FEEDBACK_REQUEST' }, + { permission: 'CAN_VIEW_FEEDBACK_ANSWER' }, + ] + } + }; + + expect(selectCanEditAllOrganizationMembers(testState)).toBe(false); + }); + + it("selectCanEditAllOrganizationMembers should return true when user has 'CAN_EDIT_ALL_ORGANIZATION_MEMBERS' permission", () => { + const testState = { + userProfile: { + firstName: 'Huey', + lastName: 'Emmerich', + role: 'MEMBER', + permissions: [ + { permission: 'CAN_VIEW_FEEDBACK_REQUEST' }, + { permission: 'CAN_EDIT_ALL_ORGANIZATION_MEMBERS' }, + { permission: 'CAN_VIEW_FEEDBACK_ANSWER' }, + ] + } + }; + + expect(selectCanEditAllOrganizationMembers(testState)).toBe(true); + }); + + it("selectCanViewTerminatedMembers should return false when user does not have 'CAN_EDIT_ALL_ORGANIZATION_MEMBERS' or 'CAN_VIEW_TERMINATED_MEMBERS' permission", () => { + const testState = { + userProfile: { + firstName: 'Huey', + lastName: 'Emmerich', + role: 'MEMBER', + permissions: [ + { permission: 'CAN_VIEW_FEEDBACK_REQUEST' }, + { permission: 'CAN_VIEW_FEEDBACK_ANSWER' }, + ] + } + }; + + expect(selectCanViewTerminatedMembers(testState)).toBe(false); + }); + + it("selectCanViewTerminatedMembers should return true when user has 'CAN_EDIT_ALL_ORGANIZATION_MEMBERS' or 'CAN_VIEW_TERMINATED_MEMBERS' permissions", () => { + const testState = { + userProfile: { + firstName: 'Huey', + lastName: 'Emmerich', + role: 'MEMBER', + permissions: [ + { permission: 'CAN_VIEW_FEEDBACK_REQUEST' }, + { permission: 'CAN_EDIT_ALL_ORGANIZATION_MEMBERS' }, + { permission: 'CAN_VIEW_FEEDBACK_ANSWER' }, + ] + } + }; + const otherTestState = { + userProfile: { + firstName: 'Huey', + lastName: 'Emmerich', + role: 'MEMBER', + permissions: [ + { permission: 'CAN_VIEW_FEEDBACK_REQUEST' }, + { permission: 'CAN_VIEW_TERMINATED_MEMBERS' }, + { permission: 'CAN_VIEW_FEEDBACK_ANSWER' }, + ] + } + }; + + expect(selectCanViewTerminatedMembers(testState)).toBe(true); + expect(selectCanViewTerminatedMembers(otherTestState)).toBe(true); + }); + it('selectActiveOrInactiveProfile should a profile if active or inactive', () => { const activeTestMember = { id: 1, From 6a736550f079cb2a78557f6513f3f4da572f70bf Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 18 Feb 2025 07:47:45 -0600 Subject: [PATCH 46/62] Not all kudos being deleted are pending. --- web-ui/src/components/kudos_card/KudosCard.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-ui/src/components/kudos_card/KudosCard.jsx b/web-ui/src/components/kudos_card/KudosCard.jsx index 798f0d946..d94451073 100644 --- a/web-ui/src/components/kudos_card/KudosCard.jsx +++ b/web-ui/src/components/kudos_card/KudosCard.jsx @@ -136,7 +136,7 @@ const KudosCard = ({ kudos, includeActions, includeEdit, onKudosAction }) => { type: UPDATE_TOAST, payload: { severity: "success", - toast: "Pending kudos deleted", + toast: "Kudos deleted", }, }); onKudosAction && onKudosAction(); From e5b8a96368b236accd9b40634ede3d7729daccfd Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 18 Feb 2025 08:33:25 -0600 Subject: [PATCH 47/62] Allow admins to edit approved kudos from the management screen. --- web-ui/src/pages/ManageKudosPage.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web-ui/src/pages/ManageKudosPage.jsx b/web-ui/src/pages/ManageKudosPage.jsx index 15b55694f..d1df8e577 100644 --- a/web-ui/src/pages/ManageKudosPage.jsx +++ b/web-ui/src/pages/ManageKudosPage.jsx @@ -234,7 +234,12 @@ const ManageKudosPage = () => { : (
{approvedKudos.filter(filterApprovedKudos).map(k => - + )}
) From f4aed833da08f2c4de5d2dd639b1a3e73884cc70 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 18 Feb 2025 09:51:05 -0600 Subject: [PATCH 48/62] Remove slack messages sent by our bot in the event that a user changes a kudos from public to private or deletes a public kudos. --- .../social_media/SlackSender.java | 56 +++++++++++++++++- .../services/kudos/KudosConverter.java | 7 +-- .../services/kudos/KudosServicesImpl.java | 57 ++++++++++++++----- .../slack/kudos/BotSentKudosLocator.java | 55 ++++++++++++++++++ 4 files changed, 154 insertions(+), 21 deletions(-) create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/BotSentKudosLocator.java diff --git a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSender.java b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSender.java index d351f5ccd..1555638f2 100644 --- a/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSender.java +++ b/server/src/main/java/com/objectcomputing/checkins/notifications/social_media/SlackSender.java @@ -6,6 +6,8 @@ import com.slack.api.methods.MethodsClient; import com.slack.api.methods.request.chat.ChatPostMessageRequest; import com.slack.api.methods.response.chat.ChatPostMessageResponse; +import com.slack.api.methods.request.chat.ChatDeleteRequest; +import com.slack.api.methods.response.chat.ChatDeleteResponse; import com.slack.api.methods.request.conversations.ConversationsOpenRequest; import com.slack.api.methods.response.conversations.ConversationsOpenResponse; @@ -42,9 +44,28 @@ public boolean send(List userIds, String slackBlocks) { return false; } + return send(openResponse.getChannel().getId(), slackBlocks); + } catch(Exception ex) { + LOG.error("SlackSender.send: " + ex.toString()); + return false; + } + } else { + LOG.error("Missing token or missing slack blocks"); + return false; + } + } + + public boolean send(String channelId, String slackBlocks) { + // See if we have a token. + String token = configuration.getApplication() + .getSlack().getBotToken(); + if (token != null && !slackBlocks.isEmpty()) { + MethodsClient client = Slack.getInstance().methods(token); + + try { ChatPostMessageRequest request = ChatPostMessageRequest .builder() - .channel(openResponse.getChannel().getId()) + .channel(channelId) .blocksAsString(slackBlocks) .build(); @@ -66,5 +87,38 @@ public boolean send(List userIds, String slackBlocks) { return false; } } + + public boolean delete(String channel, String ts) { + // See if we have a token. + String token = configuration.getApplication() + .getSlack().getBotToken(); + if (token != null) { + MethodsClient client = Slack.getInstance().methods(token); + + try { + ChatDeleteRequest request = ChatDeleteRequest + .builder() + .channel(channel) + .ts(ts) + .build(); + + // Send it to Slack + ChatDeleteResponse response = client.chatDelete(request); + + if (!response.isOk()) { + LOG.error("Unable to delete the chat message: " + + response.getError()); + } + + return response.isOk(); + } catch(Exception ex) { + LOG.error("SlackSender.delete: " + ex.toString()); + return false; + } + } else { + LOG.error("Missing token or missing slack blocks"); + return false; + } + } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java index 65c2ee1d9..8e4f969b9 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosConverter.java @@ -22,10 +22,6 @@ @Singleton public class KudosConverter { - private record InternalBlock( - List blocks - ) {} - private final MemberProfileServices memberProfileServices; private final KudosRecipientServices kudosRecipientServices; private final SlackSearch slackSearch; @@ -61,9 +57,8 @@ public String toSlackBlock(Kudos kudos) { .elements(content).build(); RichTextBlock richTextBlock = RichTextBlock.builder() .elements(List.of(element)).build(); - InternalBlock block = new InternalBlock(List.of(richTextBlock)); Gson mapper = GsonFactory.createSnakeCase(); - return mapper.toJson(block); + return mapper.toJson(List.of(richTextBlock)); } private RichTextSectionElement.TextStyle boldItalic() { diff --git a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java index b18bca9bd..1489a1bf9 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/kudos/KudosServicesImpl.java @@ -6,7 +6,9 @@ import com.objectcomputing.checkins.configuration.CheckInsConfiguration; import com.objectcomputing.checkins.notifications.email.EmailSender; import com.objectcomputing.checkins.notifications.email.MailJetFactory; -import com.objectcomputing.checkins.notifications.social_media.SlackPoster; +import com.objectcomputing.checkins.notifications.social_media.SlackSender; +import com.objectcomputing.checkins.services.slack.SlackReader; +import com.objectcomputing.checkins.services.slack.kudos.BotSentKudosLocator; import com.objectcomputing.checkins.exceptions.BadArgException; import com.objectcomputing.checkins.exceptions.NotFoundException; import com.objectcomputing.checkins.exceptions.PermissionException; @@ -23,13 +25,15 @@ import com.objectcomputing.checkins.services.team.Team; import com.objectcomputing.checkins.services.team.TeamRepository; import com.objectcomputing.checkins.util.Util; +import com.objectcomputing.checkins.configuration.CheckInsConfiguration; + import io.micronaut.core.annotation.Nullable; import io.micronaut.transaction.annotation.Transactional; -import io.micronaut.http.HttpResponse; -import io.micronaut.http.HttpStatus; import jakarta.inject.Named; import jakarta.inject.Singleton; +import jakarta.inject.Inject; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,13 +62,17 @@ class KudosServicesImpl implements KudosServices { private final CheckInsConfiguration checkInsConfiguration; private final RoleServices roleServices; private final MemberProfileServices memberProfileServices; - private final SlackPoster slackPoster; + private final SlackSender slackSender; private final KudosConverter converter; + private final BotSentKudosLocator botSentKudosLocator; private enum NotificationType { creation, approval } + @Inject + private CheckInsConfiguration configuration; + KudosServicesImpl(KudosRepository kudosRepository, KudosRecipientServices kudosRecipientServices, KudosRecipientRepository kudosRecipientRepository, @@ -75,8 +83,9 @@ private enum NotificationType { MemberProfileServices memberProfileServices, @Named(MailJetFactory.HTML_FORMAT) EmailSender emailSender, CheckInsConfiguration checkInsConfiguration, - SlackPoster slackPoster, - KudosConverter converter + SlackSender slackSender, + KudosConverter converter, + BotSentKudosLocator botSentKudosLocator ) { this.kudosRepository = kudosRepository; this.kudosRecipientServices = kudosRecipientServices; @@ -88,8 +97,9 @@ private enum NotificationType { this.currentUserServices = currentUserServices; this.emailSender = emailSender; this.checkInsConfiguration = checkInsConfiguration; - this.slackPoster = slackPoster; + this.slackSender = slackSender; this.converter = converter; + this.botSentKudosLocator = botSentKudosLocator; } @Override @@ -152,9 +162,9 @@ public Kudos update(KudosUpdateDTO kudos) { boolean existingPublic = existingKudos.getPubliclyVisible(); boolean proposedPublic = kudos.getPubliclyVisible(); + boolean removePublicSlack = false; if (existingPublic && !proposedPublic) { - // TODO: Search for and remove the Slack Kudos that the Check-Ins - // Integration posted. + removePublicSlack = true; existingKudos.setDateApproved(null); } else if ((!existingPublic && proposedPublic) || (proposedPublic && @@ -195,6 +205,12 @@ public Kudos update(KudosUpdateDTO kudos) { sendNotification(updated, NotificationType.creation); } + if (removePublicSlack) { + // Search for and remove the Slack Kudos that the Check-Ins + // Integration posted. + removeSlackMessage(existingKudos); + } + return updated; } @@ -245,6 +261,12 @@ public void delete(UUID id) { kudosRecipientRepository.deleteAll(recipients); kudosRepository.deleteById(id); + + if (kudos.getPubliclyVisible()) { + // Search for and remove the Slack Kudos that the Check-Ins + // Integration posted. + removeSlackMessage(kudos); + } } @Override @@ -441,11 +463,9 @@ private void sendNotification(Kudos kudos, NotificationType notificationType) { } private void slackApprovedKudos(Kudos kudos) { - HttpResponse httpResponse = - slackPoster.post(converter.toSlackBlock(kudos)); - if (httpResponse.status() != HttpStatus.OK) { - LOG.error("Unable to POST to Slack: " + httpResponse.reason()); - } + slackSender.send(configuration.getApplication() + .getSlack().getKudosChannel(), + converter.toSlackBlock(kudos)); } private boolean hasAdministerKudosPermission() { @@ -507,4 +527,13 @@ private void updateRecipients(Kudos updated, } } } + + private void removeSlackMessage(Kudos kudos) { + String ts = botSentKudosLocator.find(kudos); + if (ts != null) { + slackSender.delete(configuration.getApplication() + .getSlack().getKudosChannel(), + ts); + } + } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/BotSentKudosLocator.java b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/BotSentKudosLocator.java new file mode 100644 index 000000000..0df297df9 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/slack/kudos/BotSentKudosLocator.java @@ -0,0 +1,55 @@ +package com.objectcomputing.checkins.services.slack.kudos; + +import com.objectcomputing.checkins.services.kudos.Kudos; +import com.objectcomputing.checkins.services.slack.SlackReader; +import com.objectcomputing.checkins.configuration.CheckInsConfiguration; + +import com.slack.api.model.Message; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.inject.Singleton; +import jakarta.inject.Inject; + +import java.util.List; +import java.time.LocalDateTime; + +@Singleton +public class BotSentKudosLocator { + private static final Logger LOG = LoggerFactory.getLogger(BotSentKudosLocator.class); + + @Inject + private CheckInsConfiguration configuration; + + @Inject + private SlackReader slackReader; + + // The identifiers needed to identify a message is the channel id and the + // time stamp. We are always looking at a specific channel. So if we find + // a message, we will return the timestamp as a string. Otherwise, we will + // return null. + public String find(Kudos kudos) { + String channelId = configuration.getApplication() + .getSlack().getKudosChannel(); + List messages = + slackReader.read(channelId, kudos.getDateCreated().atStartOfDay()); + + String kudosText = kudos.getMessage().trim(); + for (Message message : messages) { + // We only care about messages sent by our bot. + if (message.getBotId() != null) { + // The first line is the "kudos from" line and is not part of + // the kudos message. + int cut = message.getText().indexOf("\n"); + if (cut >= 0) { + String actual = message.getText().substring(cut + 1).trim(); + if (actual.equals(kudosText)) { + return message.getTs(); + } + } + } + } + return null; + } +} From 515e5132c1d9a3ee33b416f91703662ce85f367b Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 18 Feb 2025 10:24:18 -0600 Subject: [PATCH 49/62] Updated the test to match how slack messages are now sent. --- .../services/SlackSenderReplacement.java | 9 ++++++++ .../services/kudos/KudosControllerTest.java | 21 ++++++++++--------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/SlackSenderReplacement.java b/server/src/test/java/com/objectcomputing/checkins/services/SlackSenderReplacement.java index b64c4aef1..84fba2984 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/SlackSenderReplacement.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/SlackSenderReplacement.java @@ -33,5 +33,14 @@ public boolean send(List userIds, String slackBlocks) { } return true; } + + @Override + public boolean send(String channelId, String slackBlocks) { + if (!sent.containsKey(channelId)) { + sent.put(channelId, new ArrayList()); + } + sent.get(channelId).add(slackBlocks); + return true; + } } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java index 839cf574f..6e42c684b 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/kudos/KudosControllerTest.java @@ -3,7 +3,7 @@ import com.objectcomputing.checkins.configuration.CheckInsConfiguration; import com.objectcomputing.checkins.notifications.email.MailJetFactory; import com.objectcomputing.checkins.services.MailJetFactoryReplacement; -import com.objectcomputing.checkins.services.SlackPosterReplacement; +import com.objectcomputing.checkins.services.SlackSenderReplacement; import com.objectcomputing.checkins.services.TestContainersSuite; import com.objectcomputing.checkins.services.fixture.KudosFixture; import com.objectcomputing.checkins.services.fixture.TeamFixture; @@ -60,14 +60,14 @@ // when attempting to post public Kudos to Slack. @DisabledInNativeImage @Property(name = "replace.mailjet.factory", value = StringUtils.TRUE) -@Property(name = "replace.slackposter", value = StringUtils.TRUE) +@Property(name = "replace.slacksender", value = StringUtils.TRUE) class KudosControllerTest extends TestContainersSuite implements KudosFixture, TeamFixture, RoleFixture { @Inject @Named(MailJetFactory.HTML_FORMAT) private MailJetFactoryReplacement.MockEmailSender emailSender; @Inject - private SlackPosterReplacement slackPoster; + private SlackSenderReplacement slackSender; @Inject @Client("/services/kudos") @@ -110,7 +110,7 @@ void setUp() { message = "Kudos!"; emailSender.reset(); - slackPoster.reset(); + slackSender.reset(); } @ParameterizedTest @@ -252,15 +252,16 @@ void testApproveKudos() throws JsonProcessingException { ); // Check the posted slack block - assertEquals(1, slackPoster.posted.size()); + assertEquals(1, slackSender.sent.size()); ObjectMapper mapper = new ObjectMapper(); - JsonNode posted = mapper.readTree(slackPoster.posted.get(0)); + String channelId = checkInsConfiguration.getApplication() + .getSlack().getKudosChannel(); + List sent = slackSender.sent.get(channelId); + JsonNode posted = mapper.readTree(sent.get(0)); - assertEquals(JsonNodeType.OBJECT, posted.getNodeType()); - JsonNode blocks = posted.get("blocks"); - assertEquals(JsonNodeType.ARRAY, blocks.getNodeType()); + assertEquals(JsonNodeType.ARRAY, posted.getNodeType()); - var iter = blocks.elements(); + var iter = posted.elements(); assertTrue(iter.hasNext()); JsonNode block = iter.next(); From 1c994601444b44ae7b9e5eace8d664c3e3d714ce Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 18 Feb 2025 12:46:08 -0600 Subject: [PATCH 50/62] Removed the Run Search button and run the search whenever the skills change. --- web-ui/src/pages/SkillReportPage.jsx | 68 +++++++++++----------------- 1 file changed, 27 insertions(+), 41 deletions(-) diff --git a/web-ui/src/pages/SkillReportPage.jsx b/web-ui/src/pages/SkillReportPage.jsx index e2728e0c9..9a2f713c5 100644 --- a/web-ui/src/pages/SkillReportPage.jsx +++ b/web-ui/src/pages/SkillReportPage.jsx @@ -1,4 +1,4 @@ -import React, { useContext, useRef, useState } from 'react'; +import React, { useContext, useRef, useState, useEffect } from 'react'; import { Button, TextField } from '@mui/material'; import Autocomplete from '@mui/material/Autocomplete'; @@ -54,26 +54,32 @@ const SkillReportPage = props => { processedQPs ); - const handleSearch = async searchRequestDTO => { - let res = await reportSkills(searchRequestDTO, csrf); - let memberSkillsFound; - if (res && res.payload) { - memberSkillsFound = - !res.error && res.payload.data.teamMembers - ? res.payload.data.teamMembers - : undefined; - } - // Filter out skills of terminated members - memberSkillsFound = memberSkillsFound?.filter(memberSkill => - memberIds.includes(memberSkill.id) - ); - if (memberSkillsFound && memberIds) { - let newSort = sortMembersBySkill(memberSkillsFound); - setSearchResults(newSort); - } else { - setSearchResults([]); - } - }; + useEffect(() => { + const handleSearch = async () => { + let memberSkillsFound = []; + + if (searchSkills.length > 0) { + const searchRequestDTO = createRequestDTO(editedSearchRequest); + const res = await reportSkills(searchRequestDTO, csrf); + if (res && res.payload) { + memberSkillsFound = + !res.error && res.payload.data?.teamMembers + ? res.payload.data.teamMembers + : []; + } + // Filter out skills of terminated members + memberSkillsFound = memberSkillsFound.filter(memberSkill => + memberIds?.includes(memberSkill.id) + ); + memberSkillsFound = sortMembersBySkill(memberSkillsFound); + } + + setSearchResults(memberSkillsFound); + }; + + handleSearch(); + }, [searchSkills]); + function skillsToSkillLevelDTO(skills) { return skills.map((skill, index) => { @@ -124,26 +130,6 @@ const SkillReportPage = props => { /> )} /> -
- -
From cf8cce05f86c9655e6c8ff0e35c54261cbbdbdbc Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 18 Feb 2025 12:56:35 -0600 Subject: [PATCH 51/62] Updated the test snapshot. --- .../__snapshots__/SkillReportPage.test.jsx.snap | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/web-ui/src/pages/__snapshots__/SkillReportPage.test.jsx.snap b/web-ui/src/pages/__snapshots__/SkillReportPage.test.jsx.snap index 3917370ce..6275d6ddb 100644 --- a/web-ui/src/pages/__snapshots__/SkillReportPage.test.jsx.snap +++ b/web-ui/src/pages/__snapshots__/SkillReportPage.test.jsx.snap @@ -91,20 +91,6 @@ exports[`renders correctly 1`] = ` -
- -
Date: Tue, 18 Feb 2025 13:15:56 -0600 Subject: [PATCH 52/62] Updated github actions workflows to provide SLACK_KUDOS_CHANNEL_ID value --- .github/workflows/gradle-build-production.yml | 1 + .github/workflows/gradle-deploy-develop.yml | 1 + .github/workflows/gradle-deploy-native-develop.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/gradle-build-production.yml b/.github/workflows/gradle-build-production.yml index 02e3b4236..a6a3002f0 100644 --- a/.github/workflows/gradle-build-production.yml +++ b/.github/workflows/gradle-build-production.yml @@ -89,6 +89,7 @@ jobs: --set-env-vars "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" \ --set-env-vars "SLACK_PULSE_SIGNING_SECRET=${{ secrets.SLACK_PULSE_SIGNING_SECRET }}" \ --set-env-vars "SLACK_PULSE_BOT_TOKEN=${{ secrets.SLACK_PULSE_BOT_TOKEN }}" \ + --set-env-vars "SLACK_KUDOS_CHANNEL_ID=${{ secrets.SLACK_KUDOS_CHANNEL_ID }}" \ --platform "managed" \ --max-instances 8 \ --allow-unauthenticated diff --git a/.github/workflows/gradle-deploy-develop.yml b/.github/workflows/gradle-deploy-develop.yml index 1994e1886..a30f87d57 100644 --- a/.github/workflows/gradle-deploy-develop.yml +++ b/.github/workflows/gradle-deploy-develop.yml @@ -113,6 +113,7 @@ jobs: --set-env-vars "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL }}" \ --set-env-vars "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" \ --set-env-vars "SLACK_SIGNING_SECRET=${{ secrets.SLACK_PULSE_SIGNING_SECRET }}" \ + --set-env-vars "SLACK_KUDOS_CHANNEL_ID=${{ secrets.SLACK_KUDOS_CHANNEL_ID }}" \ --platform "managed" \ --max-instances 2 \ --allow-unauthenticated diff --git a/.github/workflows/gradle-deploy-native-develop.yml b/.github/workflows/gradle-deploy-native-develop.yml index 802d2431b..abddf7dc8 100644 --- a/.github/workflows/gradle-deploy-native-develop.yml +++ b/.github/workflows/gradle-deploy-native-develop.yml @@ -113,6 +113,7 @@ jobs: --set-env-vars "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" \ --set-env-vars "SLACK_PULSE_SIGNING_SECRET=${{ secrets.SLACK_PULSE_SIGNING_SECRET }}" \ --set-env-vars "SLACK_PULSE_BOT_TOKEN=${{ secrets.SLACK_PULSE_BOT_TOKEN }}" \ + --set-env-vars "SLACK_KUDOS_CHANNEL_ID=${{ secrets.SLACK_KUDOS_CHANNEL_ID }}" \ --platform "managed" \ --max-instances 2 \ --allow-unauthenticated From 37def8edb48477efe09a2cc665d7b579dd2df606 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 18 Feb 2025 14:05:17 -0600 Subject: [PATCH 53/62] Remove the Run Search button from the Team Skills report. --- web-ui/src/pages/TeamSkillReportPage.jsx | 79 ++++++++----------- .../TeamSkillReportPage.test.jsx.snap | 10 --- 2 files changed, 33 insertions(+), 56 deletions(-) diff --git a/web-ui/src/pages/TeamSkillReportPage.jsx b/web-ui/src/pages/TeamSkillReportPage.jsx index 2b32a25fc..979c82367 100644 --- a/web-ui/src/pages/TeamSkillReportPage.jsx +++ b/web-ui/src/pages/TeamSkillReportPage.jsx @@ -1,4 +1,4 @@ -import React, { useContext, useRef, useState } from 'react'; +import React, { useContext, useRef, useState, useEffect } from 'react'; import { Autocomplete, Button, TextField, Typography } from '@mui/material'; @@ -72,33 +72,38 @@ const TeamSkillReportPage = () => { processedQPs ); - const handleSearch = async searchRequestDTO => { - let res = await reportSkills(searchRequestDTO, csrf); - let memberSkillsFound; - if (res && res.payload) { - memberSkillsFound = - !res.error && res.payload.data.teamMembers - ? res.payload.data.teamMembers - : undefined; - } - if (memberSkillsFound && memberProfiles) { - // Filter the member skill down to only members that are not terminated. - memberSkillsFound = memberSkillsFound.filter( - mSkill => memberProfiles.find(member => member.id == mSkill.id) - ); - - setAllSearchResults(memberSkillsFound); - let membersSelected = memberSkillsFound.filter(mSkill => - selectedMembers.some(member => member.id === mSkill.id) - ); - let newSort = sortMembersBySkill(membersSelected); - setSearchResults(newSort); - } else { - setSearchResults([]); - setAllSearchResults([]); - } - setShowRadar(true); - }; + useEffect(() => { + const handleSearch = async () => { + if (searchSkills.length > 0) { + const searchRequestDTO = createRequest(editedSearchRequest); + const res = await reportSkills(searchRequestDTO, csrf); + let memberSkillsFound; + if (res && res.payload) { + memberSkillsFound = + !res.error && res.payload.data.teamMembers + ? res.payload.data.teamMembers + : []; + } + + // Filter the member skill down to only members that are not terminated. + memberSkillsFound = memberSkillsFound.filter( + mSkill => memberProfiles.find(member => member.id == mSkill.id) + ); + + setAllSearchResults(memberSkillsFound); + const membersSelected = memberSkillsFound.filter(mSkill => + selectedMembers.some(member => member.id === mSkill.id) + ); + setSearchResults(sortMembersBySkill(membersSelected)); + setShowRadar(true); + } else { + setSearchResults([]); + setAllSearchResults([]); + setShowRadar(false); + } + }; + handleSearch(); + }, [selectedMembers, searchSkills]); function skillsToSkillLevel(skills) { return skills.map(skill => { @@ -194,24 +199,6 @@ const TeamSkillReportPage = () => { /> )} /> -
{showRadar && (
diff --git a/web-ui/src/pages/__snapshots__/TeamSkillReportPage.test.jsx.snap b/web-ui/src/pages/__snapshots__/TeamSkillReportPage.test.jsx.snap index b7b15e400..bea67ab24 100644 --- a/web-ui/src/pages/__snapshots__/TeamSkillReportPage.test.jsx.snap +++ b/web-ui/src/pages/__snapshots__/TeamSkillReportPage.test.jsx.snap @@ -155,16 +155,6 @@ exports[`renders correctly 1`] = `
- From 5e47a290ebdd560078ec2712e73b3dba367e6bdc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Feb 2025 12:54:47 +0000 Subject: [PATCH 54/62] Bump nokogiri from 1.16.5 to 1.18.3 in /docs Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.16.5 to 1.18.3. - [Release notes](https://github.com/sparklemotion/nokogiri/releases) - [Changelog](https://github.com/sparklemotion/nokogiri/blob/v1.18.3/CHANGELOG.md) - [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.16.5...v1.18.3) --- updated-dependencies: - dependency-name: nokogiri dependency-type: indirect ... Signed-off-by: dependabot[bot] --- docs/Gemfile.lock | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 5b49fd379..2a16e5e58 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -220,6 +220,7 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) mercenary (0.3.6) + mini_portile2 (2.8.8) minima (2.5.1) jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) @@ -228,17 +229,18 @@ GEM mutex_m (0.2.0) net-http (0.4.1) uri - nokogiri (1.16.5-aarch64-linux) + nokogiri (1.18.3) + mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.16.5-arm-linux) + nokogiri (1.18.3-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.16.5-arm64-darwin) + nokogiri (1.18.3-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.16.5-x86-linux) + nokogiri (1.18.3-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.5-x86_64-darwin) + nokogiri (1.18.3-x86_64-darwin) racc (~> 1.4) - nokogiri (1.16.5-x86_64-linux) + nokogiri (1.18.3-x86_64-linux-gnu) racc (~> 1.4) octokit (4.25.1) faraday (>= 1, < 3) @@ -246,7 +248,7 @@ GEM pathutil (0.16.2) forwardable-extended (~> 2.6) public_suffix (5.0.4) - racc (1.7.3) + racc (1.8.1) rake (13.1.0) rb-fsevent (0.11.2) rb-inotify (0.10.1) From 1da833fdcdf7cd45e2e9565bc5b81bdb9d198cd8 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Wed, 19 Feb 2025 10:19:24 -0600 Subject: [PATCH 55/62] Added a custom query to the MemberSkillRepository to filter out skills from terminated members. --- .../services/member_skill/MemberSkillRepository.java | 10 +++++++++- .../skillsreport/SkillsReportServicesImpl.java | 3 ++- web-ui/src/pages/SkillReportPage.jsx | 4 ---- web-ui/src/pages/TeamSkillReportPage.jsx | 5 ----- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/member_skill/MemberSkillRepository.java b/server/src/main/java/com/objectcomputing/checkins/services/member_skill/MemberSkillRepository.java index 08c9a6314..175e97630 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/member_skill/MemberSkillRepository.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/member_skill/MemberSkillRepository.java @@ -1,5 +1,6 @@ package com.objectcomputing.checkins.services.member_skill; +import io.micronaut.data.annotation.Query; import io.micronaut.data.jdbc.annotation.JdbcRepository; import io.micronaut.data.model.query.builder.sql.Dialect; import io.micronaut.data.repository.CrudRepository; @@ -19,6 +20,13 @@ public interface MemberSkillRepository extends CrudRepository List findBySkillid(UUID skillid); - Optional findByMemberidAndSkillid(UUID memberId,UUID skillid ); + Optional findByMemberidAndSkillid(UUID memberId, UUID skillid); + @Query(value = "SELECT member_skills.* FROM member_skills " + + "INNER JOIN member_profile " + + "ON member_skills.memberid = member_profile.id " + + "WHERE :targetSkillId = member_skills.skillid " + + "AND member_profile.terminationdate IS NULL", + nativeQuery = true) + List activeMemberSkills(String targetSkillId); } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/member_skill/skillsreport/SkillsReportServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/member_skill/skillsreport/SkillsReportServicesImpl.java index ef2c12ac6..f6321f0fa 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/member_skill/skillsreport/SkillsReportServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/member_skill/skillsreport/SkillsReportServicesImpl.java @@ -92,7 +92,8 @@ private List getPotentialQualifyingMembers(List temp = memberSkillRepo.findBySkillid(skill.getId()); + final List temp = + memberSkillRepo.activeMemberSkills(skill.getId().toString()); if (skill.getLevel() != null && !temp.isEmpty()) { for (MemberSkill memSkill : temp) { if (memSkill.getSkilllevel() != null && isSkillLevelSatisfied(memSkill.getSkilllevel(), skill.getLevel())) { diff --git a/web-ui/src/pages/SkillReportPage.jsx b/web-ui/src/pages/SkillReportPage.jsx index 9a2f713c5..6d9abfe7b 100644 --- a/web-ui/src/pages/SkillReportPage.jsx +++ b/web-ui/src/pages/SkillReportPage.jsx @@ -67,10 +67,6 @@ const SkillReportPage = props => { ? res.payload.data.teamMembers : []; } - // Filter out skills of terminated members - memberSkillsFound = memberSkillsFound.filter(memberSkill => - memberIds?.includes(memberSkill.id) - ); memberSkillsFound = sortMembersBySkill(memberSkillsFound); } diff --git a/web-ui/src/pages/TeamSkillReportPage.jsx b/web-ui/src/pages/TeamSkillReportPage.jsx index 979c82367..a79d4866f 100644 --- a/web-ui/src/pages/TeamSkillReportPage.jsx +++ b/web-ui/src/pages/TeamSkillReportPage.jsx @@ -85,11 +85,6 @@ const TeamSkillReportPage = () => { : []; } - // Filter the member skill down to only members that are not terminated. - memberSkillsFound = memberSkillsFound.filter( - mSkill => memberProfiles.find(member => member.id == mSkill.id) - ); - setAllSearchResults(memberSkillsFound); const membersSelected = memberSkillsFound.filter(mSkill => selectedMembers.some(member => member.id === mSkill.id) From f9dc4338eeb13775850c5f224fccfb0a7f9e1609 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Wed, 19 Feb 2025 11:10:54 -0600 Subject: [PATCH 56/62] Added a terminated member to one of the tests. --- .../skillsreport/SkillsReportServicesImplTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/member_skill/skillsreport/SkillsReportServicesImplTest.java b/server/src/test/java/com/objectcomputing/checkins/services/member_skill/skillsreport/SkillsReportServicesImplTest.java index b815befe0..fd2d6b0d6 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/member_skill/skillsreport/SkillsReportServicesImplTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/member_skill/skillsreport/SkillsReportServicesImplTest.java @@ -108,6 +108,7 @@ void testReport() { MemberProfile member2 = createASecondDefaultMemberProfile(); MemberProfile member3 = createAThirdDefaultMemberProfile(); MemberProfile member4 = createADefaultMemberProfileForPdl(member1); + MemberProfile member5 = createAPastTerminatedMemberProfile(); final UUID skillId1 = skill1.getId(); final UUID skillId2 = skill2.getId(); @@ -128,6 +129,12 @@ void testReport() { final MemberSkill ms8 = createMemberSkill(member4, skill2, SkillLevel.INTERMEDIATE_LEVEL, LocalDate.now()); final MemberSkill ms9 = createMemberSkill(member4, skill4, SkillLevel.EXPERT_LEVEL, LocalDate.now()); + // Skills for the terminated member + createMemberSkill(member5, skill1, SkillLevel.ADVANCED_LEVEL, LocalDate.now()); + createMemberSkill(member5, skill2, SkillLevel.ADVANCED_LEVEL, LocalDate.now()); + createMemberSkill(member5, skill3, SkillLevel.ADVANCED_LEVEL, LocalDate.now()); + createMemberSkill(member5, skill4, SkillLevel.ADVANCED_LEVEL, LocalDate.now()); + // List of skills required in first request final SkillLevelDTO dto1 = new SkillLevelDTO(); final SkillLevelDTO dto2 = new SkillLevelDTO(); From e96787406d9a819983e498c1439458205b0f2ac8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 22:15:17 +0000 Subject: [PATCH 57/62] Bump uri from 0.13.0 to 0.13.2 in /docs Bumps [uri](https://github.com/ruby/uri) from 0.13.0 to 0.13.2. - [Release notes](https://github.com/ruby/uri/releases) - [Commits](https://github.com/ruby/uri/compare/v0.13.0...v0.13.2) --- updated-dependencies: - dependency-name: uri dependency-type: indirect ... Signed-off-by: dependabot[bot] --- docs/Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 2a16e5e58..1e38a6480 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -277,7 +277,7 @@ GEM unf_ext unf_ext (0.0.9.1) unicode-display_width (1.8.0) - uri (0.13.0) + uri (0.13.2) webrick (1.8.2) PLATFORMS From 4cc8836a69e0de90018eb7a8f6e0023fedc0e068 Mon Sep 17 00:00:00 2001 From: Michael Kimberlin Date: Wed, 5 Mar 2025 00:13:51 -0600 Subject: [PATCH 58/62] added flag for ignoring birthday celebrations --- .../services/memberprofile/MemberProfile.java | 20 +++++-- .../MemberProfileController.java | 5 +- .../memberprofile/MemberProfileCreateDTO.java | 9 +++- .../MemberProfileRepository.java | 16 +++--- .../MemberProfileResponseDTO.java | 5 ++ .../memberprofile/MemberProfileUpdateDTO.java | 4 ++ .../birthday/BirthDayServicesImpl.java | 6 +-- .../currentuser/CurrentUserServicesImpl.java | 2 +- .../db/common/V122__add_disable_bday_flag.sql | 1 + .../fixture/FeedbackRequestFixture.java | 4 +- .../fixture/MemberProfileFixture.java | 54 +++++++++++-------- .../memberprofile/MemberProfileTest.java | 24 ++++----- .../memberprofile/MemberProfileTestUtil.java | 2 +- .../birthday/BirthDayControllerTest.java | 19 +++++++ 14 files changed, 113 insertions(+), 58 deletions(-) create mode 100644 server/src/main/resources/db/common/V122__add_disable_bday_flag.sql diff --git a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfile.java b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfile.java index e1fc5efdb..89f615726 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfile.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfile.java @@ -155,6 +155,11 @@ public class MemberProfile { @TypeDef(type = DataType.DATE, converter = LocalDateConverter.class) private LocalDate lastSeen; + @Column(name="ignorebirthday") + @Schema(description = "flag indicating the member would like to have their birthday ignored") + @Nullable + private Boolean ignoreBirthday; + public MemberProfile(@NotBlank String firstName, @Nullable String middleName, @NotBlank String lastName, @@ -171,9 +176,10 @@ public MemberProfile(@NotBlank String firstName, @Nullable LocalDate birthDate, @Nullable Boolean voluntary, @Nullable Boolean excluded, - @Nullable LocalDate lastSeen) { + @Nullable LocalDate lastSeen, + @Nullable Boolean ignoreBirthday) { this(null, firstName, middleName, lastName, suffix, title, pdlId, location, workEmail, - employeeId, startDate, bioText, supervisorid, terminationDate,birthDate, voluntary, excluded, lastSeen); + employeeId, startDate, bioText, supervisorid, terminationDate,birthDate, voluntary, excluded, lastSeen, ignoreBirthday); } public MemberProfile(UUID id, @@ -193,7 +199,8 @@ public MemberProfile(UUID id, @Nullable LocalDate birthDate, @Nullable Boolean voluntary, @Nullable Boolean excluded, - @Nullable LocalDate lastSeen) { + @Nullable LocalDate lastSeen, + @Nullable Boolean ignoreBirthday) { this.id = id; this.firstName = firstName; this.middleName = middleName; @@ -212,6 +219,7 @@ public MemberProfile(UUID id, this.voluntary = voluntary; this.excluded = excluded; this.lastSeen = lastSeen; + this.ignoreBirthday = ignoreBirthday; } public MemberProfile() { @@ -246,14 +254,15 @@ public boolean equals(Object o) { Objects.equals(birthDate, that.birthDate) && Objects.equals(voluntary, that.voluntary) && Objects.equals(excluded, that.excluded) && - Objects.equals(lastSeen, that.lastSeen); + Objects.equals(lastSeen, that.lastSeen) && + Objects.equals(ignoreBirthday, that.ignoreBirthday); } @Override public int hashCode() { return Objects.hash(id, firstName, middleName, lastName, suffix, title, pdlId, location, workEmail, employeeId, startDate, bioText, supervisorid, terminationDate,birthDate, - voluntary, excluded, lastSeen); + voluntary, excluded, lastSeen, ignoreBirthday); } @Override @@ -274,6 +283,7 @@ public String toString() { ", voluntary=" + voluntary + ", excluded=" + excluded + ", lastSeen=" + lastSeen + + ", ignoreBirthday=" + ignoreBirthday + '}'; } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileController.java b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileController.java index c79727b6b..0afea95cb 100755 --- a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileController.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileController.java @@ -147,6 +147,7 @@ private MemberProfileResponseDTO fromEntity(MemberProfile entity) { dto.setTerminationDate(entity.getTerminationDate()); dto.setBirthDay(entity.getBirthDate()); dto.setLastSeen(entity.getLastSeen()); + dto.setIgnoreBirthday(entity.getIgnoreBirthday()); return dto; } @@ -154,13 +155,13 @@ private MemberProfile fromDTO(MemberProfileUpdateDTO dto) { return new MemberProfile(dto.getId(), dto.getFirstName(), dto.getMiddleName(), dto.getLastName(), dto.getSuffix(), dto.getTitle(), dto.getPdlId(), dto.getLocation(), dto.getWorkEmail(), dto.getEmployeeId(), dto.getStartDate(), dto.getBioText(), dto.getSupervisorid(), - dto.getTerminationDate(), dto.getBirthDay(), dto.getVoluntary(), dto.getExcluded(), dto.getLastSeen()); + dto.getTerminationDate(), dto.getBirthDay(), dto.getVoluntary(), dto.getExcluded(), dto.getLastSeen(), dto.getIgnoreBirthday()); } private MemberProfile fromDTO(MemberProfileCreateDTO dto) { return new MemberProfile(dto.getFirstName(), dto.getMiddleName(), dto.getLastName(), dto.getSuffix(), dto.getTitle(), dto.getPdlId(), dto.getLocation(), dto.getWorkEmail(), dto.getEmployeeId(), dto.getStartDate(), dto.getBioText(), dto.getSupervisorid(), dto.getTerminationDate(), dto.getBirthDay(), - dto.getVoluntary(), dto.getExcluded(), dto.getLastSeen()); + dto.getVoluntary(), dto.getExcluded(), dto.getLastSeen(), dto.getIgnoreBirthday()); } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileCreateDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileCreateDTO.java index 4cc079033..8c5900221 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileCreateDTO.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileCreateDTO.java @@ -85,6 +85,10 @@ public class MemberProfileCreateDTO { @Schema(description = "Last date employee logged in") private LocalDate lastSeen; + @Nullable + @Schema(description = "The employee would like their birthday to not be celebrated", nullable = true) + private Boolean ignoreBirthday; + @Override public boolean equals(Object o) { if (this == o) return true; @@ -106,7 +110,8 @@ public boolean equals(Object o) { Objects.equals(birthDay, that.birthDay) && Objects.equals(voluntary, that.voluntary) && Objects.equals(excluded, that.excluded) && - Objects.equals(lastSeen, that.lastSeen); + Objects.equals(lastSeen, that.lastSeen) && + Objects.equals(ignoreBirthday, that.ignoreBirthday); } @@ -114,6 +119,6 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(firstName, middleName, lastName, suffix, title, pdlId, location, workEmail, employeeId, startDate, bioText, supervisorid, terminationDate, birthDay, - voluntary, excluded, lastSeen); + voluntary, excluded, lastSeen, ignoreBirthday); } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileRepository.java b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileRepository.java index f9f448d29..79eddb96e 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileRepository.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileRepository.java @@ -27,7 +27,7 @@ public interface MemberProfileRepository extends CrudRepository findWorkEmailByIdIn(Set ids); @Query(value = "WITH RECURSIVE subordinate AS (SELECT " + - "id, firstname, middlename, lastname, suffix, title, pdlid, location, workemail, employeeid, startdate, biotext, supervisorid, terminationdate, birthdate, voluntary, excluded, last_seen, 0 as level " + + "id, firstname, middlename, lastname, suffix, title, pdlid, location, workemail, employeeid, startdate, biotext, supervisorid, terminationdate, birthdate, voluntary, excluded, last_seen, ignoreBirthday, 0 as level " + "FROM member_profile " + "WHERE id = :id and terminationdate is NULL " + " UNION ALL " + "SELECT " + - "e.id, e.firstname, e.middlename, e.lastname, e.suffix, e.title, e.pdlid, e.location, e.workemail, e.employeeid, e.startdate, e.biotext, e.supervisorid, e.terminationdate, e.birthdate, e.voluntary, e.excluded, e.last_seen, level + 1 " + + "e.id, e.firstname, e.middlename, e.lastname, e.suffix, e.title, e.pdlid, e.location, e.workemail, e.employeeid, e.startdate, e.biotext, e.supervisorid, e.terminationdate, e.birthdate, e.voluntary, e.excluded, e.last_seen, e.ignoreBirthday, level + 1 " + "FROM member_profile e " + "JOIN subordinate s " + "ON s.supervisorid = e.id " + @@ -96,7 +96,7 @@ WHERE mp.id IN (:ids)""", "PGP_SYM_DECRYPT(cast(s.workemail as bytea), '${aes.key}') as workemail, " + "s.employeeid, s.startdate, " + "PGP_SYM_DECRYPT(cast(s.biotext as bytea), '${aes.key}') as biotext, " + - "s.supervisorid, s.terminationdate, s.birthdate, s.voluntary, s.excluded, s.last_seen, " + + "s.supervisorid, s.terminationdate, s.birthdate, s.voluntary, s.excluded, s.last_seen, s.ignoreBirthday, " + "s.level " + "FROM subordinate s " + "WHERE s.id <> :id " + @@ -106,11 +106,11 @@ WHERE mp.id IN (:ids)""", @Query( value = """ WITH RECURSIVE subordinate AS ( - SELECT id, firstname, middlename, lastname, suffix, title, pdlid, location, workemail, employeeid, startdate, biotext, supervisorid, terminationdate, birthdate, voluntary, excluded, last_seen, 0 as level + SELECT id, firstname, middlename, lastname, suffix, title, pdlid, location, workemail, employeeid, startdate, biotext, supervisorid, terminationdate, birthdate, voluntary, excluded, last_seen, ignoreBirthday, 0 as level FROM member_profile WHERE id = :id and terminationdate is NULL UNION ALL - SELECT e.id, e.firstname, e.middlename, e.lastname, e.suffix, e.title, e.pdlid, e.location, e.workemail, e.employeeid, e.startdate, e.biotext, e.supervisorid, e.terminationdate, e.birthdate, e.voluntary, e.excluded, e.last_seen, level + 1 + SELECT e.id, e.firstname, e.middlename, e.lastname, e.suffix, e.title, e.pdlid, e.location, e.workemail, e.employeeid, e.startdate, e.biotext, e.supervisorid, e.terminationdate, e.birthdate, e.voluntary, e.excluded, e.last_seen, e.ignoreBirthday, level + 1 FROM member_profile AS e JOIN subordinate AS s ON s.id = e.supervisorid WHERE e.terminationdate is NULL ) @@ -126,7 +126,7 @@ WITH RECURSIVE subordinate AS ( PGP_SYM_DECRYPT(cast(s.workemail as bytea), '${aes.key}') as workemail, s.employeeid, s.startdate, PGP_SYM_DECRYPT(cast(s.biotext as bytea), '${aes.key}') as biotext, - s.supervisorid, s.terminationdate, s.birthdate, s.voluntary, s.excluded, s.last_seen, + s.supervisorid, s.terminationdate, s.birthdate, s.voluntary, s.excluded, s.last_seen, s.ignoreBirthday, s.level FROM subordinate s WHERE s.id <> :id diff --git a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileResponseDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileResponseDTO.java index 40fe0bd67..36a657074 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileResponseDTO.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileResponseDTO.java @@ -92,6 +92,10 @@ public class MemberProfileResponseDTO { @Schema(description = "Last date employee logged in") private LocalDate lastSeen; + @Nullable + @Schema(description = "The employee would like their birthday to not be celebrated", nullable = true) + private Boolean ignoreBirthday; + @Override public String toString() { return "MemberProfileResponseDTO{" + @@ -114,6 +118,7 @@ public String toString() { ", voluntary=" + voluntary + ", excluded=" + excluded + ", lastSeen=" + lastSeen + + ", ignoreBirthday=" + ignoreBirthday + '}'; } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileUpdateDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileUpdateDTO.java index 4f17f003b..95d74e45c 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileUpdateDTO.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileUpdateDTO.java @@ -87,4 +87,8 @@ public class MemberProfileUpdateDTO { @Nullable @Schema(description = "Last date employee logged in", nullable = true) private LocalDate lastSeen; + + @Nullable + @Schema(description = "The employee would like their birthday to not be celebrated", nullable = true) + private Boolean ignoreBirthday; } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/birthday/BirthDayServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/birthday/BirthDayServicesImpl.java index 015bb8855..a71f7c9cf 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/birthday/BirthDayServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/birthday/BirthDayServicesImpl.java @@ -38,7 +38,7 @@ public List findByValue(String[] months, Integer[] daysOfMo if (month != null) { memberProfileAll = memberProfileAll .stream() - .filter(member -> member.getBirthDate() != null && month.equalsIgnoreCase(member.getBirthDate().getMonth().name()) && member.getTerminationDate() == null) + .filter(member -> member.getBirthDate() != null && month.equalsIgnoreCase(member.getBirthDate().getMonth().name()) && member.getTerminationDate() == null && (member.getIgnoreBirthday() == null || member.getIgnoreBirthday() == Boolean.FALSE)) .toList(); } } @@ -48,7 +48,7 @@ public List findByValue(String[] months, Integer[] daysOfMo if (day != null) { memberProfileAll = memberProfiles .stream() - .filter(member -> member.getBirthDate() != null && day.equals(member.getBirthDate().getDayOfMonth()) && member.getTerminationDate() == null) + .filter(member -> member.getBirthDate() != null && day.equals(member.getBirthDate().getDayOfMonth()) && member.getTerminationDate() == null && (member.getIgnoreBirthday() == null || member.getIgnoreBirthday() == Boolean.FALSE)) .toList(); } } @@ -63,7 +63,7 @@ public List getTodaysBirthdays() { LocalDate today = LocalDate.now(); List results = memberProfiles .stream() - .filter(member -> member.getBirthDate() != null && today.getMonthValue() == member.getBirthDate().getMonthValue()) + .filter(member -> member.getBirthDate() != null && today.getMonthValue() == member.getBirthDate().getMonthValue() && (member.getIgnoreBirthday() == null || member.getIgnoreBirthday() == Boolean.FALSE)) .toList(); return profileToBirthDateResponseDto(results); } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServicesImpl.java index 75784e5a7..2914e6f27 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServicesImpl.java @@ -98,7 +98,7 @@ private MemberProfile saveNewUser(String firstName, String lastName, String work } LocalDate lastSeen = LocalDate.now(); MemberProfile createdMember = memberProfileRepo.save(new MemberProfile(firstName, null, lastName, null, "", null, - "", workEmail, "", null, "", null, null, null, null, null, lastSeen)); + "", workEmail, "", null, "", null, null, null, null, null, lastSeen, false)); Optional role = roleServices.findByRole("MEMBER"); if(role.isPresent()){ diff --git a/server/src/main/resources/db/common/V122__add_disable_bday_flag.sql b/server/src/main/resources/db/common/V122__add_disable_bday_flag.sql new file mode 100644 index 000000000..7158e914c --- /dev/null +++ b/server/src/main/resources/db/common/V122__add_disable_bday_flag.sql @@ -0,0 +1 @@ +ALTER TABLE member_profile ADD column ignoreBirthday boolean; diff --git a/server/src/test/java/com/objectcomputing/checkins/services/fixture/FeedbackRequestFixture.java b/server/src/test/java/com/objectcomputing/checkins/services/fixture/FeedbackRequestFixture.java index 29c427f94..9ccf96094 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/fixture/FeedbackRequestFixture.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/fixture/FeedbackRequestFixture.java @@ -103,7 +103,7 @@ default MemberProfile createADefaultRecipient() { null, "Parks Director", null, "Pawnee, Indiana", "ron@objectcomputing.com", "mr-ron-swanson", LocalDate.now(), "enjoys woodworking, breakfast meats, and saxophone jazz", - null, null, null, false, false, null)); + null, null, null, false, false, null, false)); } default MemberProfile createASecondDefaultRecipient() { @@ -111,7 +111,7 @@ default MemberProfile createASecondDefaultRecipient() { null, "Parks Deputy Director", null, "Pawnee, Indiana", "leslie@objectcomputing.com", "ms-leslie-knope", LocalDate.now(), "proud member of numerous action committees", - null, null, null, false, false, null)); + null, null, null, false, false, null, false)); } default FeedbackRequest createFeedbackRequest(MemberProfile creator, MemberProfile requestee, MemberProfile recipient) { diff --git a/server/src/test/java/com/objectcomputing/checkins/services/fixture/MemberProfileFixture.java b/server/src/test/java/com/objectcomputing/checkins/services/fixture/MemberProfileFixture.java index b45f82ebf..20258206f 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/fixture/MemberProfileFixture.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/fixture/MemberProfileFixture.java @@ -12,7 +12,7 @@ default MemberProfile createADefaultMemberProfile() { null, "Comedic Relief", null, "New York, New York", "billm@objectcomputing.com", "mr-bill-employee", LocalDate.now().minusDays(3).minusYears(5), "is a clay figurine clown star of a parody of children's clay animation shows", - null, null, null, null, null, LocalDate.now())); + null, null, null, null, null, LocalDate.now(), false)); } default MemberProfile createADefaultMemberProfileWithBirthDayToday() { @@ -21,7 +21,7 @@ default MemberProfile createADefaultMemberProfileWithBirthDayToday() { null, "Comedic Relief", null, "New York, New York", "billm@objectcomputing.com", "mr-billy-employee-today", LocalDate.now().minusDays(3).minusMonths(1).minusYears(5), "is a clay figurine clown star of a parody of children's clay animation shows", - null, null,today.minusYears(30), null, null, LocalDate.now())); + null, null,today.minusYears(30), null, null, LocalDate.now(), false)); } default MemberProfile createADefaultMemberProfileWithBirthDayNotToday() { @@ -30,7 +30,7 @@ default MemberProfile createADefaultMemberProfileWithBirthDayNotToday() { null, "Comedic Relief", null, "New York, New York", "billm@objectcomputing.com", "mr-billy-employee-not-today", LocalDate.now().minusDays(3).minusYears(5), "is a clay figurine clown star of a parody of children's clay animation shows", - null, null, today.minusYears(30).minusDays(3), null, null, LocalDate.now())); + null, null, today.minusYears(30).minusDays(3), null, null, LocalDate.now(), false)); } default MemberProfile createADefaultMemberProfileWithAnniversaryToday() { @@ -39,7 +39,7 @@ default MemberProfile createADefaultMemberProfileWithAnniversaryToday() { null, "Comedic Relief", null, "New York, New York", "billm@objectcomputing.com", "mr-billish-employee", today.minusYears(10), "is a clay figurine clown star of a parody of children's clay animation shows", - null, null, null, null, null, LocalDate.now())); + null, null, null, null, null, LocalDate.now(), false)); } default MemberProfile createASecondDefaultMemberProfile() { @@ -47,7 +47,7 @@ default MemberProfile createASecondDefaultMemberProfile() { null, "Office Opossum", null, "New York, New York", "slimjim@objectcomputing.com", "slim-jim-employee", LocalDate.now().minusDays(3).minusYears(5), "A Virginia opossum, one of North America's only marsupials", - null, null, null, null, null, LocalDate.now())); + null, null, null, null, null, LocalDate.now(), false)); } default MemberProfile createAThirdDefaultMemberProfile() { @@ -55,7 +55,7 @@ default MemberProfile createAThirdDefaultMemberProfile() { null, "magic factory owner", null, "Chocolate Factory", "wonkaw@objectcomputing.com", "willy-wonka-employee", LocalDate.now().minusDays(3).minusYears(5), "questionable employer, but gives free golden tickets", - null, null, null, null, null, LocalDate.now())); + null, null, null, null, null, LocalDate.now(), false)); } default MemberProfile createADefaultMemberProfileForPdl(MemberProfile memberProfile) { @@ -63,7 +63,7 @@ default MemberProfile createADefaultMemberProfileForPdl(MemberProfile memberProf null, "Comedic Relief PDL", memberProfile.getId(), "New York, New York", "billmpdl@objectcomputing.com", "mr-bill-employee-pdl", LocalDate.now().minusDays(3).minusYears(5), "is a clay figurine clown star of a parody of children's clay animation shows", - memberProfile.getId(), null, null, null, null, LocalDate.now())); + memberProfile.getId(), null, null, null, null, LocalDate.now(), false)); } default MemberProfile createASecondDefaultMemberProfileForPdl(MemberProfile memberProfile) { @@ -71,7 +71,7 @@ default MemberProfile createASecondDefaultMemberProfileForPdl(MemberProfile memb null, "Bully Relief PDL", memberProfile.getId(), "New York, New York", "sluggopdl@objectcomputing.com", "sluggo-employee-pdl", LocalDate.now(), "is the bully in a clay figurine clown star of a parody of children's clay animation shows", - memberProfile.getId(), null, null, null, null, LocalDate.now())); + memberProfile.getId(), null, null, null, null, LocalDate.now(), false)); } default MemberProfile createAThirdDefaultMemberProfileForPdl(MemberProfile memberProfile) { @@ -79,7 +79,7 @@ default MemberProfile createAThirdDefaultMemberProfileForPdl(MemberProfile membe null, "local kaiju", memberProfile.getId(), "Tokyo, Japan", "godzilla@objectcomputing.com", "godzilla", LocalDate.now(), "is a destroyer of words", - null, null, null, null, null, LocalDate.now())); + null, null, null, null, null, LocalDate.now(), false)); } default MemberProfile createADefaultSupervisor() { @@ -87,7 +87,7 @@ default MemberProfile createADefaultSupervisor() { null, "Supervisor Man", null, "New York, New York", "dubebro@objectcomputing.com", "dude-bro-supervisor", LocalDate.now().minusDays(3).minusYears(5), "is such like a bro dude, you know?", - null, null, null, null, null, LocalDate.now())); + null, null, null, null, null, LocalDate.now(), false)); } default MemberProfile createAnotherSupervisor() { @@ -95,7 +95,7 @@ default MemberProfile createAnotherSupervisor() { null, "Supervisor Lady", null, "New York, New York", "dudettegal@objectcomputing.com", "dudette-gal-supervisor", LocalDate.now().minusDays(5).minusYears(7), "is such like a gal dudette, you know?", - null, null, null, null, null, LocalDate.now())); + null, null, null, null, null, LocalDate.now(), false)); } default MemberProfile createAProfileWithSupervisorAndPDL(MemberProfile supervisorProfile, MemberProfile pdlProfile) { @@ -103,21 +103,21 @@ default MemberProfile createAProfileWithSupervisorAndPDL(MemberProfile superviso null, "Local fire hazard", pdlProfile.getId(), "New York, New York", "charizard@objectcomputing.com", "local-kaiju", LocalDate.now().minusDays(3).minusYears(5), "Needs a lot of supervision due to building being ultra flammable", - supervisorProfile.getId(), null, null, null, null, LocalDate.now())); + supervisorProfile.getId(), null, null, null, null, LocalDate.now(), false)); } default MemberProfile createAnotherProfileWithSupervisorAndPDL(MemberProfile supervisorProfile, MemberProfile pdlProfile) { return getMemberProfileRepository().save(new MemberProfile("Pikachu", null, "Pika", null, "Local lightning hazard", pdlProfile.getId(), "New York, New York", "pikachu@objectcomputing.com", "local-lightning-kaiju", LocalDate.now().minusDays(5).minusYears(3), "Pretty sparky", - supervisorProfile.getId(), null, null, null, null, LocalDate.now())); + supervisorProfile.getId(), null, null, null, null, LocalDate.now(), false)); } default MemberProfile createYetAnotherProfileWithSupervisorAndPDL(MemberProfile supervisorProfile, MemberProfile pdlProfile) { return getMemberProfileRepository().save(new MemberProfile("Squirtle", null, "Squirt", null, "Local water hazard", pdlProfile.getId(), "New York, New York", "squirtle@objectcomputing.com", "local-water-kaiju", LocalDate.now().minusDays(4).minusYears(6), "Rather moist", - supervisorProfile.getId(), null, null, null, null, LocalDate.now())); + supervisorProfile.getId(), null, null, null, null, LocalDate.now(), false)); } // this user is not connected to other users in the system default MemberProfile createAnUnrelatedUser() { @@ -125,7 +125,7 @@ default MemberProfile createAnUnrelatedUser() { null, "Comedic Relief", null, "New York, New York", "nobody@objectcomputing.com", "mr-bill-employee-unrelated", LocalDate.now().minusDays(3).minusMonths(1).minusYears(5), "is a clay figurine clown star of a parody of children's clay animation shows", - null, null, null, null, null, LocalDate.now())); + null, null, null, null, null, LocalDate.now(), false)); } default MemberProfile createAPastTerminatedMemberProfile() { @@ -133,7 +133,7 @@ default MemberProfile createAPastTerminatedMemberProfile() { null, "Bully Relief PDL", null, "New York, New York", "sluggopdl@objectcomputing.com", "sluggo-employee-pdl-past-terminated", LocalDate.now().minusDays(3).minusYears(5), "is the bully in a clay figurine clown star of a parody of children's clay animation shows", - null, LocalDate.now().minusMonths(1), null, null, null, LocalDate.now().minusMonths(1))); + null, LocalDate.now().minusMonths(1), null, null, null, LocalDate.now().minusMonths(1), false)); } default MemberProfile createAFutureTerminatedMemberProfile() { @@ -141,7 +141,7 @@ default MemberProfile createAFutureTerminatedMemberProfile() { null, "Bully Relief PDL", null, "New York, New York", "sluggopdl@objectcomputing.com", "sluggo-employee-pdl-future terminated", LocalDate.now().minusDays(3).minusYears(5), "is the bully in a clay figurine clown star of a parody of children's clay animation shows", - null, LocalDate.now().plusDays(7), null, null, null, LocalDate.now().plusDays(7))); + null, LocalDate.now().plusDays(7), null, null, null, LocalDate.now().plusDays(7), false)); } @@ -150,7 +150,16 @@ default MemberProfile createADefaultMemberProfileWithBirthDay() { null, "Comedic Relief", null, "New York, New York", "billm@objectcomputing.com", "mr-bill-employee-birthday", LocalDate.now().minusDays(3).minusYears(5), "is a clay figurine clown star of a parody of children's clay animation shows", - null, null, LocalDate.now(), null, null, LocalDate.now())); + null, null, LocalDate.now(), null, null, LocalDate.now(), false)); + } + + // This user is disconnected and doesn't want their birthday celebrated. + default MemberProfile createUserWithIgnoredBirthday() { + return getMemberProfileRepository().save(new MemberProfile("Gone", null, " Girl", + null, "Video Vogue", null, "New York, New York", + "girlg@objectcomputing.com", "stay-away-from-me", + LocalDate.now().minusDays(3).minusYears(5), "is gone, girl!", + null, null, null, null, null, LocalDate.now(), true)); } default MemberProfile createAPastMemberProfile() { @@ -158,7 +167,7 @@ default MemberProfile createAPastMemberProfile() { null, "Comedic Relief", null, "New York, New York", "billm@objectcomputing.com", "mr-bill-employee-past", LocalDate.now().minusYears(2), "is a clay figurine clown star of a parody of children's clay animation shows", - null, null, null, null, null, LocalDate.now().minusYears(2))); + null, null, null, null, null, LocalDate.now().minusYears(2), false)); } default MemberProfile createANewHireProfile() { @@ -166,7 +175,7 @@ default MemberProfile createANewHireProfile() { null, "Comedic Relief", null, "New York, New York", "billm@objectcomputing.com", "mr-bill-employee-new", LocalDate.now().minusMonths(2), "is a clay figurine clown star of a parody of children's clay animation shows", - null, null, null, null, null, LocalDate.now())); + null, null, null, null, null, LocalDate.now(), false)); } default MemberProfile createATerminatedNewHireProfile() { @@ -174,7 +183,7 @@ default MemberProfile createATerminatedNewHireProfile() { null, "Comedic Relief", null, "New York, New York", "billm@objectcomputing.com", "mr-bill-employee-term-new", LocalDate.now().minusMonths(2), "is a clay figurine clown star of a parody of children's clay animation shows", - null, LocalDate.now().minusMonths(1), null, true, null, LocalDate.now().minusMonths(1))); + null, LocalDate.now().minusMonths(1), null, true, null, LocalDate.now().minusMonths(1), false)); } default MemberProfile memberWithoutBoss(String name) { @@ -204,7 +213,8 @@ default MemberProfile memberWithSupervisor(String name, MemberProfile supervisor null, null, null, - LocalDate.now() + LocalDate.now(), + false ) ); } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileTest.java b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileTest.java index c74478a86..56e8e4b78 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileTest.java @@ -65,7 +65,7 @@ void testMemberProfileInstantiation() { MemberProfile memberProfile = new MemberProfile(firstName, null, lastName, null, null, null, null, workEmail, null, null, null, null, - null, null, null, null, null); + null, null, null, null, null, false); assertEquals(firstName, memberProfile.getFirstName()); assertEquals(lastName, memberProfile.getLastName()); assertEquals(workEmail, memberProfile.getWorkEmail()); @@ -79,7 +79,7 @@ void testConstraintViolation() { MemberProfile memberProfile = new MemberProfile(firstName, null, lastName, null, null, null, null, workEmail, null, null, null, null, - null, null, null, null, null); + null, null, null, null, null, false); assertEquals(firstName, memberProfile.getFirstName()); assertEquals(lastName, memberProfile.getLastName()); assertEquals(workEmail, memberProfile.getWorkEmail()); @@ -113,7 +113,7 @@ void testEquals() { LocalDate lastSeen = LocalDate.now(); MemberProfile memberProfile = new MemberProfile(id, firstName, middleName, lastName, suffix, title, pdlId, location, workEmail, - employeeId, startDate, bioText, supervisorId, terminationDate, birthDate, voluntary, excluded, lastSeen); + employeeId, startDate, bioText, supervisorId, terminationDate, birthDate, voluntary, excluded, lastSeen, false); assertEquals(id, memberProfile.getId()); assertEquals(firstName, memberProfile.getFirstName()); @@ -157,7 +157,7 @@ void testToString() { final LocalDate lastSeen = LocalDate.now(); MemberProfile memberProfile = new MemberProfile(id, firstName, middleName, lastName, suffix, title, pdlId, location, workEmail, - employeeId, startDate, bioText, supervisorId, terminationDate, birthDate, voluntary, excluded, lastSeen); + employeeId, startDate, bioText, supervisorId, terminationDate, birthDate, voluntary, excluded, lastSeen, false); String toString = memberProfile.toString(); assertTrue(toString.contains(id.toString())); @@ -184,7 +184,7 @@ void testToString() { void testSaveProfileWithExistingEmail() { MemberProfile existingProfile = createADefaultMemberProfile(); String workEmail = existingProfile.getWorkEmail(); - MemberProfile newProfile = new MemberProfile(UUID.randomUUID(), "John", null, "Smith", null, null, null, null, workEmail, null, null, null, null, null, null, null, null, null); + MemberProfile newProfile = new MemberProfile(UUID.randomUUID(), "John", null, "Smith", null, null, null, null, workEmail, null, null, null, null, null, null, null, null, null, false); // The current user must have permission to create new members. currentUserServices.currentUser = existingProfile; @@ -203,7 +203,7 @@ void testSaveProfileWithNewEmail() { LocalDate.now().minusDays(3).minusYears(5), "Needs supervision due to building being ultra flammable", supervisorProfile.getId(), null, null, null, null, - LocalDate.now()); + LocalDate.now(), false); // Need permission to create new profiles. currentUserServices.currentUser = supervisorProfile; @@ -239,7 +239,7 @@ void testUpdateProfileWithChangedPDL() { currentUserServices.currentUser = existingProfile; - MemberProfile updatedProfile = new MemberProfile(id, existingProfile.getFirstName(), null, existingProfile.getLastName(), null, null, pdlId, null, existingProfile.getWorkEmail(), null, null, null, null, null, null, null, null, null); + MemberProfile updatedProfile = new MemberProfile(id, existingProfile.getFirstName(), null, existingProfile.getLastName(), null, null, pdlId, null, existingProfile.getWorkEmail(), null, null, null, null, null, null, null, null, null, false); MemberProfile result = memberProfileServices.updateProfile(updatedProfile); @@ -264,7 +264,7 @@ void testUpdateProfileWithChangedSupervisor() { currentUserServices.currentUser = existingProfile; - MemberProfile updatedProfile = new MemberProfile(id, existingProfile.getFirstName(), null, existingProfile.getLastName(), null, null, null, null, existingProfile.getWorkEmail(), null, null, null, supervisorId, null, null, null, null, null); + MemberProfile updatedProfile = new MemberProfile(id, existingProfile.getFirstName(), null, existingProfile.getLastName(), null, null, null, null, existingProfile.getWorkEmail(), null, null, null, supervisorId, null, null, null, null, null, false); MemberProfile result = memberProfileServices.updateProfile(updatedProfile); @@ -356,7 +356,7 @@ void testEmailAssignmentWithValidPdlAndSupervisor() { void testEmailAssignmentWithInvalidPDL() { MemberProfile existingProfile = createADefaultMemberProfile(); UUID pdlId = UUID.randomUUID(); - MemberProfile member = new MemberProfile(existingProfile.getId(), existingProfile.getFirstName(), null, existingProfile.getLastName(), null, null, pdlId, null, existingProfile.getWorkEmail(), null, null, null, null, null, null, null, null, null); + MemberProfile member = new MemberProfile(existingProfile.getId(), existingProfile.getFirstName(), null, existingProfile.getLastName(), null, null, pdlId, null, existingProfile.getWorkEmail(), null, null, null, null, null, null, null, null, null, false); memberProfileServices.emailAssignment(member, true); @@ -367,7 +367,7 @@ void testEmailAssignmentWithInvalidPDL() { void testEmailAssignmentWithInvalidSupervisor() { MemberProfile existingProfile = createADefaultMemberProfile(); UUID supervisorId = UUID.randomUUID(); - MemberProfile member = new MemberProfile(existingProfile.getId(), existingProfile.getFirstName(), null, existingProfile.getLastName(), null, null, null, null, existingProfile.getWorkEmail(), null, null, null, supervisorId, null, null, null, null, null); + MemberProfile member = new MemberProfile(existingProfile.getId(), existingProfile.getFirstName(), null, existingProfile.getLastName(), null, null, null, null, existingProfile.getWorkEmail(), null, null, null, supervisorId, null, null, null, null, null, false); memberProfileServices.emailAssignment(member, true); @@ -377,7 +377,7 @@ void testEmailAssignmentWithInvalidSupervisor() { @Test void testEmailAssignmentWithInvalidMember() { MemberProfile member = new MemberProfile(UUID.randomUUID(), "John", null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null, false); memberProfileServices.emailAssignment(member, true); @@ -387,7 +387,7 @@ void testEmailAssignmentWithInvalidMember() { @Test void testEmailAssignmentWithNullRoleId() { MemberProfile member = new MemberProfile(UUID.randomUUID(), "John", null, "Smith", null, null, null, null, "john.smith@example.com", - null, null, null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null, false); memberProfileServices.emailAssignment(member, true); diff --git a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileTestUtil.java b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileTestUtil.java index 8a86defce..c436bea2d 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileTestUtil.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileTestUtil.java @@ -51,7 +51,7 @@ public static MemberProfile mkMemberProfile(String seed) { LocalDate.of(2019, 1, 1), "TestBio" + seed, null, - null,null, null, null, LocalDate.now()); + null,null, null, null, LocalDate.now(), false); } public static MemberProfile mkMemberProfile() { diff --git a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/birthday/BirthDayControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/birthday/BirthDayControllerTest.java index 31a7e4a99..8414bd4e3 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/birthday/BirthDayControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/birthday/BirthDayControllerTest.java @@ -81,4 +81,23 @@ void testGETFindByValueNameOfTheMonthNotAuthorized() { assertEquals(HttpStatus.FORBIDDEN, thrown.getStatus()); } + + @Test + void testGETFindByValueDoesNotIncludeIgnoredBirthdays() { + + MemberProfile memberProfileOfAdmin = createAnUnrelatedUser(); + assignAdminRole(memberProfileOfAdmin); + + MemberProfile memberProfile = createADefaultMemberProfileWithBirthDay(); + MemberProfile ignoredMember = createUserWithIgnoredBirthday(); + + final HttpRequest request = HttpRequest. + GET(String.format("/?month=%s", memberProfile.getBirthDate().getMonth().toString())).basicAuth(memberProfileOfAdmin.getWorkEmail(), ADMIN_ROLE); + + final HttpResponse> response = client.toBlocking().exchange(request, Argument.listOf(BirthDayResponseDTO.class)); + + assertEquals(1, response.body().size()); + assertEquals(memberProfile.getId(), response.body().get(0).getUserId()); + assertEquals(HttpStatus.OK, response.getStatus()); + } } From 34d888d8d2a18de1f9d9b35a2c8e48440bd35b28 Mon Sep 17 00:00:00 2001 From: Michael Kimberlin Date: Wed, 5 Mar 2025 08:12:03 -0600 Subject: [PATCH 59/62] Make the db column name snake case --- .../services/memberprofile/MemberProfile.java | 2 +- .../memberprofile/MemberProfileRepository.java | 16 ++++++++-------- .../db/common/V122__add_disable_bday_flag.sql | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfile.java b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfile.java index 89f615726..51a92cd0a 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfile.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfile.java @@ -155,7 +155,7 @@ public class MemberProfile { @TypeDef(type = DataType.DATE, converter = LocalDateConverter.class) private LocalDate lastSeen; - @Column(name="ignorebirthday") + @Column(name="ignore_birthday") @Schema(description = "flag indicating the member would like to have their birthday ignored") @Nullable private Boolean ignoreBirthday; diff --git a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileRepository.java b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileRepository.java index 79eddb96e..8d0420f23 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileRepository.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileRepository.java @@ -27,7 +27,7 @@ public interface MemberProfileRepository extends CrudRepository findWorkEmailByIdIn(Set ids); @Query(value = "WITH RECURSIVE subordinate AS (SELECT " + - "id, firstname, middlename, lastname, suffix, title, pdlid, location, workemail, employeeid, startdate, biotext, supervisorid, terminationdate, birthdate, voluntary, excluded, last_seen, ignoreBirthday, 0 as level " + + "id, firstname, middlename, lastname, suffix, title, pdlid, location, workemail, employeeid, startdate, biotext, supervisorid, terminationdate, birthdate, voluntary, excluded, last_seen, ignore_birthday, 0 as level " + "FROM member_profile " + "WHERE id = :id and terminationdate is NULL " + " UNION ALL " + "SELECT " + - "e.id, e.firstname, e.middlename, e.lastname, e.suffix, e.title, e.pdlid, e.location, e.workemail, e.employeeid, e.startdate, e.biotext, e.supervisorid, e.terminationdate, e.birthdate, e.voluntary, e.excluded, e.last_seen, e.ignoreBirthday, level + 1 " + + "e.id, e.firstname, e.middlename, e.lastname, e.suffix, e.title, e.pdlid, e.location, e.workemail, e.employeeid, e.startdate, e.biotext, e.supervisorid, e.terminationdate, e.birthdate, e.voluntary, e.excluded, e.last_seen, e.ignore_birthday, level + 1 " + "FROM member_profile e " + "JOIN subordinate s " + "ON s.supervisorid = e.id " + @@ -96,7 +96,7 @@ WHERE mp.id IN (:ids)""", "PGP_SYM_DECRYPT(cast(s.workemail as bytea), '${aes.key}') as workemail, " + "s.employeeid, s.startdate, " + "PGP_SYM_DECRYPT(cast(s.biotext as bytea), '${aes.key}') as biotext, " + - "s.supervisorid, s.terminationdate, s.birthdate, s.voluntary, s.excluded, s.last_seen, s.ignoreBirthday, " + + "s.supervisorid, s.terminationdate, s.birthdate, s.voluntary, s.excluded, s.last_seen, s.ignore_birthday, " + "s.level " + "FROM subordinate s " + "WHERE s.id <> :id " + @@ -106,11 +106,11 @@ WHERE mp.id IN (:ids)""", @Query( value = """ WITH RECURSIVE subordinate AS ( - SELECT id, firstname, middlename, lastname, suffix, title, pdlid, location, workemail, employeeid, startdate, biotext, supervisorid, terminationdate, birthdate, voluntary, excluded, last_seen, ignoreBirthday, 0 as level + SELECT id, firstname, middlename, lastname, suffix, title, pdlid, location, workemail, employeeid, startdate, biotext, supervisorid, terminationdate, birthdate, voluntary, excluded, last_seen, ignore_birthday, 0 as level FROM member_profile WHERE id = :id and terminationdate is NULL UNION ALL - SELECT e.id, e.firstname, e.middlename, e.lastname, e.suffix, e.title, e.pdlid, e.location, e.workemail, e.employeeid, e.startdate, e.biotext, e.supervisorid, e.terminationdate, e.birthdate, e.voluntary, e.excluded, e.last_seen, e.ignoreBirthday, level + 1 + SELECT e.id, e.firstname, e.middlename, e.lastname, e.suffix, e.title, e.pdlid, e.location, e.workemail, e.employeeid, e.startdate, e.biotext, e.supervisorid, e.terminationdate, e.birthdate, e.voluntary, e.excluded, e.last_seen, e.ignore_birthday, level + 1 FROM member_profile AS e JOIN subordinate AS s ON s.id = e.supervisorid WHERE e.terminationdate is NULL ) @@ -126,7 +126,7 @@ WITH RECURSIVE subordinate AS ( PGP_SYM_DECRYPT(cast(s.workemail as bytea), '${aes.key}') as workemail, s.employeeid, s.startdate, PGP_SYM_DECRYPT(cast(s.biotext as bytea), '${aes.key}') as biotext, - s.supervisorid, s.terminationdate, s.birthdate, s.voluntary, s.excluded, s.last_seen, s.ignoreBirthday, + s.supervisorid, s.terminationdate, s.birthdate, s.voluntary, s.excluded, s.last_seen, s.ignore_birthday, s.level FROM subordinate s WHERE s.id <> :id diff --git a/server/src/main/resources/db/common/V122__add_disable_bday_flag.sql b/server/src/main/resources/db/common/V122__add_disable_bday_flag.sql index 7158e914c..0bb092775 100644 --- a/server/src/main/resources/db/common/V122__add_disable_bday_flag.sql +++ b/server/src/main/resources/db/common/V122__add_disable_bday_flag.sql @@ -1 +1 @@ -ALTER TABLE member_profile ADD column ignoreBirthday boolean; +ALTER TABLE member_profile ADD column ignore_birthday boolean; From f0d9b18fdd1b8a017b01bfe9b6c25a43da16096d Mon Sep 17 00:00:00 2001 From: Michael Kimberlin Date: Wed, 5 Mar 2025 15:03:16 -0600 Subject: [PATCH 60/62] Fixed issues with duplicate current user profile in state, added preferences card to profile page with switch to turn off birthday celebration, and removed deprecated docker compose version --- docker-compose.yaml | 1 - .../MemberProfileController.java | 6 +- .../currentuser/CurrentUserController.java | 3 +- .../currentuser/CurrentUserDTO.java | 3 +- web-ui/src/context/actions.js | 1 + web-ui/src/context/reducer.js | 82 ++++++++-------- web-ui/src/pages/ProfilePage.jsx | 72 +++++++++++--- .../__snapshots__/ProfilePage.test.jsx.snap | 94 +++++++++++++++++++ 8 files changed, 204 insertions(+), 58 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 5d4c56db0..9dd01c2ab 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,3 @@ -version: '3' services: postgresql: image: postgres:17.2 diff --git a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileController.java b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileController.java index 0afea95cb..dab6c285a 100755 --- a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileController.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/MemberProfileController.java @@ -58,7 +58,7 @@ public HttpResponse getById(UUID id) { public List getSupervisorsForId(UUID id) { return memberProfileServices.getSupervisorsForId(id) .stream() - .map(this::fromEntity) + .map(MemberProfileController::fromEntity) .toList(); } @@ -83,7 +83,7 @@ public List findByValue(@Nullable String firstName, @QueryValue(value = "terminated", defaultValue = "false") Boolean terminated) { return memberProfileServices.findByValues(firstName, lastName, title, pdlId, workEmail, supervisorId, terminated) .stream() - .map(this::fromEntity) + .map(MemberProfileController::fromEntity) .toList(); } @@ -128,7 +128,7 @@ protected URI location(UUID id) { return URI.create("/member-profiles/" + id); } - private MemberProfileResponseDTO fromEntity(MemberProfile entity) { + public static MemberProfileResponseDTO fromEntity(MemberProfile entity) { MemberProfileResponseDTO dto = new MemberProfileResponseDTO(); dto.setId(entity.getId()); dto.setFirstName(entity.getFirstName()); diff --git a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserController.java b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserController.java index 68d185b64..49ce69571 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserController.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserController.java @@ -1,6 +1,7 @@ package com.objectcomputing.checkins.services.memberprofile.currentuser; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileController; import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; import com.objectcomputing.checkins.services.memberprofile.MemberProfileUtils; import com.objectcomputing.checkins.services.permissions.Permission; @@ -86,7 +87,7 @@ private CurrentUserDTO fromEntity(MemberProfile entity, String imageUrl, List { + member.birthDay = Array.isArray(member.birthDay) + ? new Date(member.birthDay.join('/')) + : member && member.birthDay + ? member.birthDay + : null; + member.startDate = Array.isArray(member.startDate) + ? new Date(member.startDate.join('/')) + : member && member.startDate + ? member.startDate + : new Date(); + member.terminationDate = Array.isArray(member.terminationDate) + ? new Date(member.terminationDate.join('/')) + : member && member.terminationDate + ? member.terminationDate + : null; +}; + export const reducer = (state, action) => { switch (action.type) { case MY_PROFILE_UPDATE: - state.userProfile = action.payload; - break; - case UPDATE_USER_BIO: - state.userProfile.memberProfile.bioText = action.payload; + convertMemberDates(action.payload.memberProfile); + state.userProfile = { ...action.payload }; + break; + case UPDATE_CURRENT_USER_PROFILE: + convertMemberDates(action.payload); + state.userProfile = { ...state.userProfile, memberProfile: { ...action.payload } }; + const profileId = action.payload.id; + const memberProfiles = state.memberProfiles.reduce((acc, current) => { + if(current.id !== profileId) { + acc.push({...current}); + } + return acc; + }, [{ ...action.payload }]) + state.memberProfiles = memberProfiles; break; case ADD_CHECKIN: if (state?.checkins?.length > 0) { @@ -144,43 +173,20 @@ export const reducer = (state, action) => { state.loading = { ...state.loading, memberProfiles: action.payload }; break; case UPDATE_MEMBER_PROFILES: - action.payload.forEach(member => { - member.birthDay = Array.isArray(member.birthDay) - ? new Date(member.birthDay.join('/')) - : member && member.birthDay - ? member.birthDay - : null; - member.startDate = Array.isArray(member.startDate) - ? new Date(member.startDate.join('/')) - : member && member.startDate - ? member.startDate - : new Date(); - member.terminationDate = Array.isArray(member.terminationDate) - ? new Date(member.terminationDate.join('/')) - : member && member.terminationDate - ? member.terminationDate - : null; + action.payload.forEach(convertMemberDates); + const currentProfileId = state?.userProfile?.memberProfile?.id; + const currentProfile = action.payload.find((current) => { + if(currentProfileId && current.id === currentProfileId) { + return current; + } }); + if(currentProfile) { + state.userProfile.memberProfile = { ...currentProfile }; + } state.memberProfiles = action.payload; break; case UPDATE_TERMINATED_MEMBERS: - action.payload.forEach(member => { - member.birthDay = Array.isArray(member.birthDay) - ? new Date(member.birthDay.join('/')) - : member && member.birthDay - ? member.birthDay - : null; - member.startDate = Array.isArray(member.startDate) - ? new Date(member.startDate.join('/')) - : member && member.startDate - ? member.startDate - : new Date(); - member.terminationDate = Array.isArray(member.terminationDate) - ? new Date(member.terminationDate.join('/')) - : member && member.terminationDate - ? member.terminationDate - : null; - }); + action.payload.forEach(convertMemberDates); state.terminatedMembers = action.payload; break; case UPDATE_TEAM_MEMBERS: diff --git a/web-ui/src/pages/ProfilePage.jsx b/web-ui/src/pages/ProfilePage.jsx index 7adee7e70..70983e770 100644 --- a/web-ui/src/pages/ProfilePage.jsx +++ b/web-ui/src/pages/ProfilePage.jsx @@ -5,10 +5,9 @@ import { AppContext } from '../context/AppContext'; import { selectCurrentUser, selectMyGuilds, - selectUserProfile, selectMyTeams } from '../context/selectors'; -import { UPDATE_GUILD, UPDATE_USER_BIO } from '../context/actions'; +import { UPDATE_GUILD, UPDATE_CURRENT_USER_PROFILE } from '../context/actions'; import { addGuildMember, deleteGuildMember } from '../api/guild'; import { updateMember } from '../api/member'; import { getEmployeeHours } from '../api/hours'; @@ -18,10 +17,15 @@ import SkillSection from '../components/skills/SkillSection'; import ProgressBar from '../components/contribution_hours/ProgressBar'; import VolunteerTables from '../components/volunteer/VolunteerTables'; -import { Info } from '@mui/icons-material'; +import { Info, ManageAccounts } from '@mui/icons-material'; import { Card, CardContent, CardHeader, Chip, TextField, Avatar } from '@mui/material'; import GroupIcon from '@mui/icons-material/Group'; import Autocomplete from '@mui/material/Autocomplete'; +import FormLabel from '@mui/material/FormLabel'; +import FormControl from '@mui/material/FormControl'; +import FormGroup from '@mui/material/FormGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Switch from '@mui/material/Switch'; import './ProfilePage.css'; @@ -32,13 +36,12 @@ const storeMember = debounce(realStoreMember, 1500); const ProfilePage = () => { const { state, dispatch } = useContext(AppContext); const memberProfile = selectCurrentUser(state); - const userProfile = selectUserProfile(state); const { csrf, guilds } = state; - const { id, bioText, pdlId } = memberProfile; - const { firstName, lastName, name } = userProfile; + const { id, bioText, pdlId, ignoreBirthday, firstName, lastName, name } = memberProfile; const [bio, setBio] = useState(); + const [prefs, setPrefs] = useState({ignoreBirthday}); const [myHours, setMyHours] = useState(null); const myTeams = selectMyTeams(state); @@ -62,24 +65,43 @@ const ProfilePage = () => { setBio(bioText); } updateBio(); - }, [bioText]); + }, [setBio, bioText]); - const updateProfile = newBio => { + useEffect(() => { + async function updatePrefs() { + setPrefs({...prefs, ignoreBirthday}) + } + updatePrefs() + }, [setPrefs, ignoreBirthday]); + + const updateProfile = newProfile => { dispatch({ - type: UPDATE_USER_BIO, - payload: newBio + type: UPDATE_CURRENT_USER_PROFILE, + payload: newProfile }); }; - const handleBioChange = e => { + const handleIgnoreBirthdayChange = useCallback(async e => { + if (!csrf) { + return; + } + const { checked } = e.target; + setPrefs({ ...prefs, ignoreBirthday: !checked }); + const newProfile = { ...memberProfile, ignoreBirthday: !checked }; + const { payload } = await realStoreMember(newProfile, csrf); + updateProfile(payload.data); + }, [csrf, prefs, setPrefs, memberProfile, realStoreMember, updateProfile]); + + const handleBioChange = useCallback(async e => { if (!csrf) { return; } const { value } = e.target; setBio(value); - storeMember({ ...memberProfile, bioText: value }, csrf); - updateProfile(value); - }; + const newProfile = { ...memberProfile, bioText: value }; + storeMember(newProfile, csrf); + updateProfile(newProfile); + }, [csrf, setBio, memberProfile, storeMember, updateProfile]); const addOrDeleteGuildMember = useCallback( async newVal => { @@ -146,6 +168,28 @@ const ProfilePage = () => {
+
+ + } + title="Preferences" + titleTypographyProps={{ variant: 'h5', component: 'h2' }} + /> + + + Celebrations + + + } + label="My Birthday" + /> + + + + +
+
+
+
+
+
+ +
+
+
+

+ Preferences +

+
+
+
+
+ + Celebrations + +
+ +
+
+
+
+
From f4094c2465d2e1115a931265f31aee8faf201b9f Mon Sep 17 00:00:00 2001 From: Michael Kimberlin Date: Wed, 5 Mar 2025 15:26:06 -0600 Subject: [PATCH 61/62] Fixed test compile error --- .../services/memberprofile/currentuser/CurrentUserDtoTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserDtoTest.java b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserDtoTest.java index 2896592e5..6de8a0bd0 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserDtoTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserDtoTest.java @@ -2,6 +2,7 @@ import com.objectcomputing.checkins.services.TestContainersSuite; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileResponseDTO; import io.micronaut.validation.validator.Validator; import jakarta.inject.Inject; import jakarta.validation.ConstraintViolation; @@ -54,7 +55,7 @@ void testPopulatedDTO() { assertEquals("some.last.name", dto.getLastName()); dto.setName(dto.getFirstName() + ' ' + dto.getLastName()); - dto.setMemberProfile(new MemberProfile()); + dto.setMemberProfile(new MemberProfileResponseDTO()); Set> violations = validator.validate(dto); assertTrue(violations.isEmpty()); From 71fc0be6380e30543d3bca4988f49b37027dc93d Mon Sep 17 00:00:00 2001 From: Michael Kimberlin Date: Wed, 5 Mar 2025 16:21:46 -0600 Subject: [PATCH 62/62] Bumped version to 0.8.15 --- server/build.gradle | 2 +- web-ui/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/build.gradle b/server/build.gradle index 15d991225..4aa56e47b 100755 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,7 +7,7 @@ plugins { id "jacoco" } -version "0.8.14" +version "0.8.15" group "com.objectcomputing.checkins" repositories { diff --git a/web-ui/package.json b/web-ui/package.json index 519af4d24..1f396ae4e 100644 --- a/web-ui/package.json +++ b/web-ui/package.json @@ -1,6 +1,6 @@ { "name": "web-ui", - "version": "0.8.14", + "version": "0.8.15", "private": true, "type": "module", "dependencies": {