Skip to content

Commit

Permalink
[JN-1607] add filter to study staff emails (#1481)
Browse files Browse the repository at this point in the history
  • Loading branch information
connorlbark authored Feb 21, 2025
1 parent 5960629 commit e6c04fe
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@
import bio.terra.pearl.core.model.study.StudyEnvAttached;
import bio.terra.pearl.core.model.workflow.TaskStatus;
import bio.terra.pearl.core.model.workflow.TaskType;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
* configuration for notifications.
* This probably should have subclasses for each NotificationType with single-table inheritance,
Expand Down Expand Up @@ -53,6 +53,11 @@ public class Trigger extends BaseEntity implements VersionedEntityConfig, StudyE
private UUID emailTemplateId;
private EmailTemplate emailTemplate;
private String rule;

// for admin notifications, comma separated list of admin emails.
// will not send if the email does not have an associated admin account
// if blank, no admins will be notified
private String targetEmails;
/**
* notificationTypes of TASK_REMINDER, if specified, will limit to one type of task. if null,
* will apply to all tasks.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@
import bio.terra.pearl.core.model.admin.*;
import bio.terra.pearl.core.model.audit.DataAuditInfo;
import bio.terra.pearl.core.service.CascadeProperty;

import java.util.*;

import bio.terra.pearl.core.service.publishing.PortalEnvironmentChangeRecordService;
import bio.terra.pearl.core.service.workflow.ParticipantDataChangeService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;

@Service
public class AdminUserService extends AdminDataAuditedService<AdminUser, AdminUserDao> {
private final PortalAdminUserService portalAdminUserService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@
import bio.terra.pearl.core.service.study.StudyService;
import bio.terra.pearl.core.service.workflow.EnrolleeEvent;
import bio.terra.pearl.core.shared.ApplicationRoutingPaths;
import com.sendgrid.helpers.mail.Mail;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.text.StringSubstitutor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import com.sendgrid.helpers.mail.Mail;

import java.util.List;
import java.util.UUID;
Expand Down Expand Up @@ -112,6 +112,19 @@ public void sendEmailFromTrigger(Trigger trigger, EnrolleeEvent event) throws Ex
.orElseThrow(() -> new IllegalStateException("Portal not found"));
List<AdminUser> adminUsers = adminUserService.findAllWithRolesByPortal(portal.getId());


String emailFilter = trigger.getTargetEmails();
if (emailFilter == null) {
emailFilter = "";
}

List<String> emails = List.of(emailFilter.split(",")).stream().map(String::trim).toList();

adminUsers = adminUsers.stream()
.filter(adminUser -> emails.contains(adminUser.getUsername()))
.toList();


EmailTemplate emailTemplate = emailTemplateService.find(trigger.getEmailTemplateId())
.orElseThrow(() -> new NotFoundException("Email template not found"));
emailTemplateService.attachLocalizedTemplates(emailTemplate);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
databaseChangeLog:
- changeSet:
id: "trigger_target_emails"
author: connorlbark
changes:
- addColumn:
tableName: trigger
columns:
- column:
name: target_emails
type: text
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,9 @@ databaseChangeLog:
- include:
file: changesets/2025_01_27_prenroll_type.yaml
relativeToChangelogFile: true

- include:
file: changesets/2025_02_14_trigger_admin_email_filter.yaml
relativeToChangelogFile: true

# README: it is a best practice to put each DDL statement in its own change set. DDL statements
# are atomic. When they are grouped in a changeset and one fails the changeset cannot be
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;

class AdminEmailServiceTest extends BaseSpringBootTest {
@Autowired
Expand Down Expand Up @@ -54,21 +53,26 @@ public void testSendFromTrigger(TestInfo info) throws Exception {
EmailTemplate emailTemplate = emailTemplateFactory.buildPersisted(getTestName(info), bundle.getPortal().getId());
localizedEmailTemplateService.create(LocalizedEmailTemplate.builder().emailTemplateId(emailTemplate.getId()).language("en").subject("subject").body("body").build());


AdminUserBundle adminUserBundle1 = portalAdminUserFactory.buildPersistedWithPortals(getTestName(info), List.of(bundle.getPortal()));
AdminUserBundle adminUserBundle2 = portalAdminUserFactory.buildPersistedWithPortals(getTestName(info), List.of(bundle.getPortal()));
AdminUserBundle adminUserBundle3 = portalAdminUserFactory.buildPersistedWithPortals(getTestName(info), List.of(bundle.getPortal()));

Trigger trigger = triggerFactory.buildPersisted(
Trigger.builder()
.emailTemplateId(emailTemplate.getId())
.deliveryType(NotificationDeliveryType.EMAIL)
.actionType(TriggerActionType.ADMIN_NOTIFICATION)
.targetEmails(
adminUserBundle1.user().getUsername() + "," + adminUserBundle2.user().getUsername()
)
.triggerType(TriggerType.EVENT),
bundle.getStudyEnv().getId(),
bundle.getPortalEnv().getId());

EnrolleeBundle enrolleeBundle = enrolleeFactory.buildWithPortalUser(getTestName(info), bundle.getPortalEnv(), bundle.getStudyEnv());


AdminUserBundle adminUserBundle1 = portalAdminUserFactory.buildPersistedWithPortals(getTestName(info), List.of(bundle.getPortal()));
AdminUserBundle adminUserBundle2 = portalAdminUserFactory.buildPersistedWithPortals(getTestName(info), List.of(bundle.getPortal()));

List<Notification> notificationsBefore = notificationService.findAllByConfigId(trigger.getId(), false);

assertEquals(0, notificationsBefore.size());
Expand Down Expand Up @@ -99,6 +103,7 @@ public void testSendFromTrigger(TestInfo info) throws Exception {
assertEquals(NotificationType.ADMIN, notification.getNotificationType());
assertTrue(notification.getSentTo().contains(adminUserBundle1.user().getUsername()));
assertTrue(notification.getSentTo().contains(adminUserBundle2.user().getUsername()));
assertFalse(notification.getSentTo().contains(adminUserBundle3.user().getUsername()));

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
"eventType": "STUDY_ENROLLMENT",
"actionType": "ADMIN_NOTIFICATION",
"rule": "{profile.doNotEmailSolicit} = true",
"targetEmails": "heartdemostaff@gmail.com",
"populateFileName": "emails/adminNotificationTest.json"
}],
"enrolleeFiles": [
Expand Down
63 changes: 60 additions & 3 deletions ui-admin/src/study/notifications/TriggerDesignerEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import {
ParticipantTaskStatus, StudyEnvParams,
ParticipantTaskStatus,
StudyEnvParams,
Trigger,
TriggerActionType,
TriggerDeliveryType,
TriggerScope,
TriggerType
} from '@juniper/ui-core'
import React, { useId, useState } from 'react'
import React, {
useId,
useState
} from 'react'
import Select from 'react-select'
import useReactSingleSelect from 'util/react-select-utils'
import {
Expand All @@ -16,13 +20,17 @@ import {
InfoCardTitle
} from 'components/InfoCard'
import EmailTemplateEditor from 'study/notifications/EmailTemplateEditor'
import { paramsFromContext, StudyEnvContextT } from 'study/StudyEnvironmentRouter'
import {
paramsFromContext,
StudyEnvContextT
} from 'study/StudyEnvironmentRouter'
import InfoPopup from 'components/forms/InfoPopup'
import { NavLink } from 'react-router-dom'
import { Checkbox } from 'components/forms/Checkbox'
import { LazySearchQueryBuilder } from 'search/LazySearchQueryBuilder'
import { useLoadingEffect } from 'api/api-utils'
import Api from 'api/api'
import LoadingSpinner from 'util/LoadingSpinner'


export const TriggerDesignerEditor = (
Expand Down Expand Up @@ -390,6 +398,15 @@ const NotificationEditor = (
<div className="float-end position-relative">
<NavLink to='notifications'>View sent notifications</NavLink>
</div>
{
trigger.actionType === 'ADMIN_NOTIFICATION'
&& <div className='w-50 mb-2'>
<TargetEmailEditor
studyEnvContext={studyEnvContext}
trigger={trigger}
updateTrigger={updateTrigger}/>
</div>
}
<label className="form-label">
Notification Type <InfoPopup content={'Juniper only supports reminders via email.'}/>
<Select options={deliveryTypeOptions} isDisabled={true}
Expand Down Expand Up @@ -424,6 +441,46 @@ const NotificationEditor = (
</>
}

const TargetEmailEditor = (
{
studyEnvContext,
trigger,
updateTrigger
}: {
studyEnvContext: StudyEnvContextT,
trigger: Trigger;
updateTrigger: (string: keyof Trigger, value: unknown) => void;
}
) => {
const [adminUsers, setAdminUsers] = useState<string[]>([])

const { isLoading } = useLoadingEffect(async () => {
const adminUsers = await Api.fetchAdminUsersByPortal(studyEnvContext.portal.shortcode)
setAdminUsers(adminUsers.map(user => user.username))
}, [])


if (isLoading) {
return <LoadingSpinner/>
}

return <div>
<label className="form-label" htmlFor="targetEmailEditor">
Send notification to
</label>

<Select
options={adminUsers.map(username => ({ label: username, value: username }))}
inputId="targetEmailEditor"
value={adminUsers
.filter(username => trigger.targetEmails?.includes(username))
.map(username => ({ label: username, value: username }))}
isMulti={true}
onChange={options => updateTrigger('targetEmails', options.map(opt => opt!.value).join(','))}
/>
</div>
}

const statusOptions: { label: string, value: ParticipantTaskStatus }[] = [
{ label: 'New', value: 'NEW' },
{ label: 'In progress', value: 'IN_PROGRESS' },
Expand Down
1 change: 1 addition & 0 deletions ui-core/src/types/study.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export type Trigger = {
maxNumReminders: number
emailTemplateId: string
emailTemplate: EmailTemplate
targetEmails?: string
}

export type EmailTemplate = {
Expand Down

0 comments on commit e6c04fe

Please sign in to comment.