Skip to content

Commit

Permalink
Merge branch 'develop' into feature-2226/display-review-period-status
Browse files Browse the repository at this point in the history
  • Loading branch information
mkimberlin authored Sep 17, 2024
2 parents d988692 + b346ccf commit 28b0625
Show file tree
Hide file tree
Showing 23 changed files with 564 additions and 33 deletions.
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
4 changes: 2 additions & 2 deletions server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
plugins {
id 'java-library'
id 'maven-publish'
id("com.github.johnrengelman.shadow") version "8.1.1"
id("io.micronaut.application") version "4.4.0"
id("com.gradleup.shadow") version "8.3.1"
id("io.micronaut.application") version "4.4.2"
id "jacoco"
id("org.openrewrite.rewrite") version "latest.release"
}
Expand Down
2 changes: 1 addition & 1 deletion server/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
micronautVersion=4.5.0
micronautVersion=4.6.2
seleniumVersion=4.21.0
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public enum Permission {
CAN_VIEW_FEEDBACK_ANSWER("View feedback answers", "Feedback"),
CAN_DELETE_ORGANIZATION_MEMBERS("Delete organization members", "User Management"),
CAN_CREATE_ORGANIZATION_MEMBERS("Create organization members", "User Management"),
CAN_IMPERSONATE_MEMBERS("Impersonate organization members", "User Management"),
CAN_IMPERSONATE_MEMBERS("Impersonate organization members", "Security"),
CAN_VIEW_ROLE_PERMISSIONS("View role permissions", "Security"),
CAN_ASSIGN_ROLE_PERMISSIONS("Assign role permissions", "Security"),
CAN_VIEW_PERMISSIONS("View all permissions", "Security"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.objectcomputing.checkins.services.pulse;

import com.objectcomputing.checkins.configuration.CheckInsConfiguration;
import com.objectcomputing.checkins.notifications.email.EmailSender;
import com.objectcomputing.checkins.notifications.email.MailJetFactory;
import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices;

import jakarta.inject.Named;

import java.util.stream.Collectors;
import java.util.List;

class PulseEmail {
private final EmailSender emailSender;
private final CheckInsConfiguration checkInsConfiguration;
private final MemberProfileServices memberProfileServices;

private final String SUBJECT = "Check Out the Pulse Survey!";

public PulseEmail(@Named(MailJetFactory.HTML_FORMAT) EmailSender emailSender,
CheckInsConfiguration checkInsConfiguration,
MemberProfileServices memberProfileServices) {
this.emailSender = emailSender;
this.checkInsConfiguration = checkInsConfiguration;
this.memberProfileServices = memberProfileServices;
}

private List<String> getActiveTeamMembers() {
List<String> profiles = memberProfileServices.findAll().stream()
.filter(p -> p.getTerminationDate() == null)
.map(p -> p.getWorkEmail())
.collect(Collectors.toList());
return profiles;
}

private String getEmailContent() {
/*
<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-divider border-color="#2559a7"></mj-divider>
<mj-text font-size="16px" font-family="'Helvetica Neue', Helvetica, Arial, sans-serif" color="#4d4c4f">Please fill out your Pulse survey, if you haven't already done so. We want to know how you're doing!</mj-text>
<mj-text font-size="16px" font-family="'Helvetica Neue', Helvetica, Arial, sans-serif" color="#4d4c4f">Click <a href="%s/pulse" target="_blank">here</a> to begin.</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
*/
return String.format("""
<html>
<body>
<div style="margin:0px auto;float: left; max-width:600px;">
<p style="border-top:solid 4px #2559a7;font-size:1px;margin:0px auto;width:100%%;"></p>
<p></p>
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#4d4c4f;">
Please fill out your Pulse survey, if you haven't already done so. We want to know how you're doing!</div>
<p></p>
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#4d4c4f;">
Click <a href="%s/pulse" target="_blank">here</a> to begin.</div>
</div>
</div>
</body>
</html>
""", checkInsConfiguration.getWebAddress());
}

public void send() {
final List<String> recipients = getActiveTeamMembers();
final String content = getEmailContent();
emailSender.sendEmail(null, null, SUBJECT, content,
recipients.toArray(new String[recipients.size()]));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.objectcomputing.checkins.services.pulse;

import java.time.LocalDate;

public interface PulseServices {
public void sendPendingEmail(LocalDate now);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package com.objectcomputing.checkins.services.pulse;

import com.objectcomputing.checkins.configuration.CheckInsConfiguration;
import com.objectcomputing.checkins.notifications.email.EmailSender;
import com.objectcomputing.checkins.notifications.email.MailJetFactory;
import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices;
import com.objectcomputing.checkins.services.settings.SettingsServices;
import com.objectcomputing.checkins.services.settings.Setting;
import com.objectcomputing.checkins.exceptions.NotFoundException;

import lombok.Getter;
import lombok.AllArgsConstructor;

import jakarta.inject.Named;
import jakarta.inject.Singleton;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.HashMap;
import java.time.LocalDate;
import java.time.DayOfWeek;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;

@Singleton
public class PulseServicesImpl implements PulseServices {
@Getter
@AllArgsConstructor
private class Frequency {
private final int count;
private final ChronoUnit units;
}

private static final Logger LOG = LoggerFactory.getLogger(PulseServicesImpl.class);
private final EmailSender emailSender;
private final CheckInsConfiguration checkInsConfiguration;
private final MemberProfileServices memberProfileServices;
private final SettingsServices settingsServices;
private final Map<String, Boolean> sent = new HashMap<String, Boolean>();

private final DayOfWeek emailDay = DayOfWeek.MONDAY;

private String setting = "bi-weekly";
private final Map<String, Frequency> frequency = Map.of(
"weekly", new Frequency(1, ChronoUnit.WEEKS),
"bi-weekly", new Frequency(2, ChronoUnit.WEEKS),
"monthly", new Frequency(1, ChronoUnit.MONTHS)
);

public PulseServicesImpl(
@Named(MailJetFactory.HTML_FORMAT) EmailSender emailSender,
CheckInsConfiguration checkInsConfiguration,
MemberProfileServices memberProfileServices,
SettingsServices settingsServices) {
this.emailSender = emailSender;
this.checkInsConfiguration = checkInsConfiguration;
this.memberProfileServices = memberProfileServices;
this.settingsServices = settingsServices;
}

public void sendPendingEmail(LocalDate check) {
if (check.getDayOfWeek() == emailDay) {
LOG.info("Checking for pending Pulse email");
// Start from the first of the year and move forward to ensure that we
// are sending email during the correct week.
LocalDate start = check.with(TemporalAdjusters.firstDayOfYear())
.with(TemporalAdjusters.firstInMonth(emailDay));

try {
Setting freq = settingsServices.findByName("PULSE_EMAIL_FREQUENCY");
if (frequency.containsKey(freq.getValue())) {
setting = freq.getValue();
} else {
LOG.error("Invalid Pulse Email Frequency Setting: " + freq.getValue());
}
} catch(NotFoundException ex) {
// Use the default setting.
LOG.error("Pulse Frequency Error: " + ex.toString());
}

LOG.info("Using Pulse Frequency: " + setting);
final Frequency freq = frequency.get(setting);
do {
if (start.getDayOfMonth() == check.getDayOfMonth()) {
LOG.info("Check day of month matches frequency day");
final String key = new StringBuilder(start.getMonth().toString())
.append("_")
.append(String.valueOf(start.getDayOfMonth()))
.toString();
if (sent.containsKey(key)) {
LOG.info("The Pulse Email has already been sent today");
} else {
LOG.info("Sending Pulse Email");
send();
sent.put(key, true);
}
break;
}
start = start.plus(freq.getCount(), freq.getUnits());

// Apply firstInMonth(emailDay) to support adding one month to the start
// date. When adding weeks, it remains on the original day. But, when
// adding months, it can move away from the first of the month and we
// need the day specified by emailDay.
if (freq.getUnits() == ChronoUnit.MONTHS) {
start = start.with(TemporalAdjusters.firstInMonth(emailDay));
}
} while(start.isBefore(check) || start.isEqual(check));
}
}

private void send() {
PulseEmail email = new PulseEmail(emailSender, checkInsConfiguration,
memberProfileServices);
email.send();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.objectcomputing.checkins.services.feedback_request.FeedbackRequest;
import com.objectcomputing.checkins.services.feedback_request.FeedbackRequestRepository;
import com.objectcomputing.checkins.services.feedback_request.FeedbackRequestServicesImpl;
import com.objectcomputing.checkins.services.pulse.PulseServices;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -16,11 +17,14 @@ public class CheckServicesImpl implements CheckServices {
private static final Logger LOG = LoggerFactory.getLogger(CheckServicesImpl.class);
private final FeedbackRequestServicesImpl feedbackRequestServices;
private final FeedbackRequestRepository feedbackRequestRepository;
private final PulseServices pulseServices;

public CheckServicesImpl(FeedbackRequestServicesImpl feedbackRequestServices,
FeedbackRequestRepository feedbackRequestRepository) {
FeedbackRequestRepository feedbackRequestRepository,
PulseServices pulseServices) {
this.feedbackRequestServices = feedbackRequestServices;
this.feedbackRequestRepository = feedbackRequestRepository;
this.pulseServices = pulseServices;
}

@Override
Expand All @@ -33,6 +37,7 @@ public boolean sendScheduledEmails() {
req.setStatus("sent");
feedbackRequestRepository.update(req);
}
pulseServices.sendPendingEmail(today);
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
@JsonSerialize(using = SettingOptionSerializer.class)
@JsonDeserialize(using = SettingOptionDeserializer.class)
public enum SettingOption {
LOGO_URL("The logo url", Category.THEME, Type.FILE);
LOGO_URL("The logo url", Category.THEME, Type.FILE),
PULSE_EMAIL_FREQUENCY("The Pulse Email Frequency (weekly, bi-weekly, monthly)", Category.CHECK_INS, Type.STRING);

private final String description;
private final Category category;
Expand All @@ -27,6 +28,18 @@ public enum SettingOption {
this.type = type;
}

public String getDescription() {
return description;
}

public Category getCategory() {
return category;
}

public Type getType() {
return type;
}

public static List<SettingOption> getOptions(){
return Arrays.asList(SettingOption.values());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.objectcomputing.checkins.services.permissions.Permission;
import com.objectcomputing.checkins.services.permissions.RequiredPermission;
import com.objectcomputing.checkins.exceptions.NotFoundException;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.annotation.*;
Expand All @@ -18,6 +19,7 @@
import java.net.URI;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

@Controller(SettingsController.PATH)
@ExecuteOn(TaskExecutors.BLOCKING)
Expand Down Expand Up @@ -66,8 +68,24 @@ public SettingsResponseDTO findByName(@PathVariable @NotNull String name) {
*/
@Get("/options")
@RequiredPermission(Permission.CAN_VIEW_SETTINGS)
public List<SettingOption> getOptions() {
return SettingOption.getOptions();
public List<SettingsResponseDTO> getOptions() {
List<SettingOption> options = SettingOption.getOptions();
return options.stream().map(option -> {
// Default to an empty value and "invalid" UUID.
// This can be used by the client to determine pre-existance.
String value = "";
UUID uuid = new UUID(0, 0);
try {
Setting s = settingsServices.findByName(option.name());
uuid = s.getId();
value = s.getValue();
} catch(NotFoundException ex) {
}
return new SettingsResponseDTO(
uuid, option.name(), option.getDescription(),
option.getCategory(), option.getType(),
value);
}).collect(Collectors.toList());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

Expand All @@ -12,6 +13,7 @@
@Setter
@Getter
@Introspected
@AllArgsConstructor
public class SettingsResponseDTO {

@NotNull
Expand All @@ -38,4 +40,6 @@ public class SettingsResponseDTO {
@Schema(description = "value of the setting")
private String value;

public SettingsResponseDTO() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class CheckInsConfigurationTest {
@Test
void checkConfigurationGetsParsed() {
try (var ctx = ApplicationContext.run(Map.of(
"datasources.enabled", false,
"datasources.default.enabled", false,
"check-ins.web-address", "http://google.com",
"check-ins.application.name", "Fancy app"
))) {
Expand All @@ -28,7 +28,7 @@ void checkConfigurationGetsParsed() {
@Test
void checkWebAddressGetsValidated() {
try (var ctx = ApplicationContext.run(Map.of(
"datasources.enabled", false,
"datasources.default.enabled", false,
"check-ins.web-address", "",
"check-ins.application.name", "Fancy app"
))) {
Expand All @@ -40,7 +40,7 @@ void checkWebAddressGetsValidated() {
@Test
void checkApplicationNameGetsValidated() {
try (var ctx = ApplicationContext.run(Map.of(
"datasources.enabled", false,
"datasources.default.enabled", false,
"check-ins.web-address", "http://google.com",
"check-ins.application.name", ""
))) {
Expand Down
Loading

0 comments on commit 28b0625

Please sign in to comment.