Skip to content

Commit

Permalink
Merge pull request #3286 from NCI-Agency/GH-3231-pending-assessments-…
Browse files Browse the repository at this point in the history
…email-notification

Send email notifications for pending assessments
  • Loading branch information
VassilIordanov authored Nov 18, 2020
2 parents 931ec0d + 49f3983 commit 8b61b70
Show file tree
Hide file tree
Showing 28 changed files with 1,512 additions and 352 deletions.
22 changes: 21 additions & 1 deletion insertBaseData-mssql.sql
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ INSERT INTO tasks (uuid, shortName, longName, category, createdAt, updatedAt, cu
VALUES
(N'953e0b0b-25e6-44b6-bc77-ef98251d046a', '1.2.A', 'Milestone the First in EF 1.2', 'Milestone', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, N'fe6b6b2f-d2a1-4ce1-9aa7-05361812a4d0', '{ "assessments":[{"questions":{ "question1": { "type": "special_field", "widget": "likertScale", "label": "Test Question 1", "helpText": "Please provide assessment for something important", "levels": [ { "color": "red", "endValue": 2, "label": "test" }, { "color": "#FFBF00", "endValue": 8, "label": "mid" }, { "color": "green", "endValue": 10, "label": "high" } ], "aggregation": { "widget": "likertScale" } }, "question2": { "type": "number", "label": "Test Question 2", "aggregation": { "widget": "numberAggregation" } }, "question3": { "type": "number", "label": "Test Question 3", "aggregation": { "widget": "numberAggregation" } } },"relatedObjectType":"report"}] }'),
(N'9d3da7f4-8266-47af-b518-995f587250c9', '1.2.B', 'Milestone the Second in EF 1.2', 'Milestone', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, N'fe6b6b2f-d2a1-4ce1-9aa7-05361812a4d0', '{ "assessments":[{"questions":{ "frenchFlag": { "type": "special_field", "widget": "likertScale", "label": "French Flag assessment", "helpText": "Please tell us which is the best color in the French flag", "levels": [ { "color": "blue", "endValue": 3.3, "label": "blue" }, { "color": "white", "endValue": 6.6, "label": "white" }, { "color": "red", "endValue": 10, "label": "red" } ] }, "levels": { "type": "enumset", "label": "Achieved levels", "choices": { "lvl1": { "label": "Level 1" }, "lvl2": { "label": "Level 2" }, "lvl3": { "label": "Level 3" } } }, "description": { "type": "special_field", "label": "Detail levels", "widget": "richTextEditor" } },"relatedObjectType":"report"}] }'),
(N'6bbb1be9-4655-48d7-83f2-bc474781544a', '1.2.C', 'Milestone the Third in EF 1.2', 'Milestone', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, N'fe6b6b2f-d2a1-4ce1-9aa7-05361812a4d0', '{ "assessments":[{"questions":{ "question1": { "type": "special_field", "widget": "likertScale", "label": "Monthly assessment Question 1", "helpText": "Please provide assessment for something important", "levels": [ { "color": "red", "endValue": 2, "label": "test" }, { "color": "#FFBF00", "endValue": 8, "label": "mid" }, { "color": "green", "endValue": 10, "label": "high" } ], "aggregation": { "widget": "likertScale" } }, "question2": { "type": "number", "label": "Monthly assessment Question 2", "aggregation": { "widget": "numberAggregation" } }, "question3": { "type": "number", "label": "Monthly assessment Question 3", "aggregation": { "widget": "numberAggregation" } } },"recurrence":"monthly"},{"questions":{ "question1": { "type": "special_field", "widget": "likertScale", "label": "Weekly assessment Question 1", "helpText": "Please provide assessment for something important", "levels": [ { "color": "red", "endValue": 2, "label": "test" }, { "color": "#FFBF00", "endValue": 8, "label": "mid" }, { "color": "green", "endValue": 10, "label": "high" } ], "aggregation": { "widget": "likertScale" } }, "question2": { "type": "number", "label": "Weekly assessment Question 2", "aggregation": { "widget": "numberAggregation" } }, "question3": { "type": "number", "label": "Weekly assessment Question 3", "aggregation": { "widget": "numberAggregation" } } },"recurrence":"weekly"},{"questions":{ "question1": { "type": "special_field", "widget": "likertScale", "label": "Instant assessment Question 1", "helpText": "Please provide assessment for something important", "levels": [ { "color": "red", "endValue": 2, "label": "test" }, { "color": "#FFBF00", "endValue": 8, "label": "mid" }, { "color": "green", "endValue": 10, "label": "high" } ], "aggregation": { "widget": "likertScale" } }, "question2": { "type": "number", "label": "Instant assessment Question 2", "aggregation": { "widget": "numberAggregation" } }, "question3": { "type": "number", "label": "Instant assessment Question 3", "aggregation": { "widget": "numberAggregation" } } },"relatedObjectType":"report"}] }');
(N'6bbb1be9-4655-48d7-83f2-bc474781544a', '1.2.C', 'Milestone the Third in EF 1.2', 'Milestone', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, N'fe6b6b2f-d2a1-4ce1-9aa7-05361812a4d0', '{ "assessments":[{"questions":{ "question1": { "type": "special_field", "widget": "likertScale", "label": "Monthly assessment Question 1", "helpText": "Please provide assessment for something important", "levels": [ { "color": "red", "endValue": 2, "label": "test" }, { "color": "#FFBF00", "endValue": 8, "label": "mid" }, { "color": "green", "endValue": 10, "label": "high" } ], "aggregation": { "widget": "likertScale" } }, "question2": { "type": "number", "label": "Monthly assessment Question 2", "aggregation": { "widget": "numberAggregation" } }, "question3": { "type": "number", "label": "Monthly assessment Question 3", "aggregation": { "widget": "numberAggregation" } } },"recurrence":"quarterly"},{"questions":{ "question1": { "type": "special_field", "widget": "likertScale", "label": "Weekly assessment Question 1", "helpText": "Please provide assessment for something important", "levels": [ { "color": "red", "endValue": 2, "label": "test" }, { "color": "#FFBF00", "endValue": 8, "label": "mid" }, { "color": "green", "endValue": 10, "label": "high" } ], "aggregation": { "widget": "likertScale" } }, "question2": { "type": "number", "label": "Weekly assessment Question 2", "aggregation": { "widget": "numberAggregation" } }, "question3": { "type": "number", "label": "Weekly assessment Question 3", "aggregation": { "widget": "numberAggregation" } } },"recurrence":"daily"},{"questions":{ "question1": { "type": "special_field", "widget": "likertScale", "label": "Instant assessment Question 1", "helpText": "Please provide assessment for something important", "levels": [ { "color": "red", "endValue": 2, "label": "test" }, { "color": "#FFBF00", "endValue": 8, "label": "mid" }, { "color": "green", "endValue": 10, "label": "high" } ], "aggregation": { "widget": "likertScale" } }, "question2": { "type": "number", "label": "Instant assessment Question 2", "aggregation": { "widget": "numberAggregation" } }, "question3": { "type": "number", "label": "Instant assessment Question 3", "aggregation": { "widget": "numberAggregation" } } },"relatedObjectType":"report"}] }');

INSERT INTO tasks (uuid, shortName, longName, category, createdAt, updatedAt, customFieldRef1Uuid)
VALUES
Expand Down Expand Up @@ -955,6 +955,26 @@ INSERT INTO noteRelatedObjects (noteUuid, relatedObjectType, relatedObjectUuid)
FROM tasks t
WHERE t.shortName = '1.2.B';

-- Add periodic assessment for a task
SET @authorUuid = (SELECT uuid FROM people WHERE name = 'ANDERSON, Andrew');
SET @noteUuid = lower(newid());
INSERT INTO notes (uuid, authorUuid, type, text, createdAt, updatedAt)
VALUES (@noteUuid, @authorUuid, 3, '{"status":"GREEN","issues":"<ol><li>one</li><li>two</li><li>three</li></ol>","__recurrence":"monthly","__periodStart":"' + FORMAT(DATEADD(month, DATEDIFF(month, 0, CURRENT_TIMESTAMP), 0), 'yyyy-MM-dd') + '"}', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
INSERT INTO noteRelatedObjects (noteUuid, relatedObjectType, relatedObjectUuid)
SELECT @noteUuid, 'tasks', t.uuid
FROM tasks t
WHERE t.shortName = '1.1.A';

-- Add periodic assessment for a person
SET @authorUuid = (SELECT uuid FROM people WHERE name = 'JACKSON, Jack');
SET @noteUuid = lower(newid());
INSERT INTO notes (uuid, authorUuid, type, text, createdAt, updatedAt)
VALUES (@noteUuid, @authorUuid, 3, '{"test3":"3","test2":"3","test1":"3","__recurrence":"quarterly","__periodStart":"' + FORMAT(DATEADD(quarter, DATEDIFF(quarter, 0, CURRENT_TIMESTAMP), 0), 'yyyy-MM-dd') + '"}', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
INSERT INTO noteRelatedObjects (noteUuid, relatedObjectType, relatedObjectUuid)
SELECT @noteUuid, 'people', p.uuid
FROM people p
WHERE p.name = 'ROGWELL, Roger';

-- LEAVE THIS AS LAST STATEMENT
-- Truncate all the dates (on reports etc.) to dates that could have been generated by
-- Java (millisecond precision) rather than by the database itself (microsecond precision)
Expand Down
4 changes: 3 additions & 1 deletion mssql2pg.pl
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
# Eliminate the weird "[key]" column naming
s/\[key\]/key/g;
# Quote mixed-case column and table names
s/(?<![":])\b([a-z]\w+[A-Z]\w+)/"$1"/g;
s/(?<![":-])\b([a-z]\w+[A-Z]\w+)/"$1"/g;
# Turn a couple very specific 1/0 booleans into true/false booleans
# This one is for "isAuthor" in "reportPeople"
s/(?<=\Q:reportuuid, \E[10]\Q, \E)([10])/$1 ? 'TRUE' : 'FALSE'/ie;
Expand All @@ -36,6 +36,8 @@
# This one is for populating report approval steps
s/(?<=\"reportPeople\".\"isAuthor\")\s+=\s+([10])/"= " . ($1 ? 'TRUE' : 'FALSE')/e;
# standard date-time math would be so nice...
s/DATEADD\s*\(([^,]*),\s*DATEDIFF\([^,]*, 0, CURRENT_TIMESTAMP\), 0\)/date_trunc('$1', CURRENT_TIMESTAMP)/g;
s/\+ FORMAT\((date_trunc\([^)]*\)), 'yyyy-MM-dd'\) \+/|| to_char($1, 'YYYY-MM-DD') ||/g;
s/DATEADD\s*\(([^,]*),\s*(-?\d+),\s*CURRENT_TIMESTAMP\)/CURRENT_TIMESTAMP + INTERVAL '$2 $1'/g;
s/cast\((\S+) as datetime2\((\d+)\)\)/"date_trunc(" . ($2 eq '3' ? "'milliseconds'" : "'second'") . ", $1)"/ie;
# Function to generate uuid's
Expand Down
54 changes: 32 additions & 22 deletions src/main/java/mil/dds/anet/AnetApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import mil.dds.anet.threads.AnetEmailWorker;
import mil.dds.anet.threads.FutureEngagementWorker;
import mil.dds.anet.threads.MaterializedViewRefreshWorker;
import mil.dds.anet.threads.PendingAssessmentsNotificationWorker;
import mil.dds.anet.threads.ReportApprovalWorker;
import mil.dds.anet.threads.ReportPublicationWorker;
import mil.dds.anet.utils.DaoUtils;
Expand Down Expand Up @@ -207,56 +208,64 @@ public void run(AnetConfiguration configuration, Environment environment)
} else {
logger.info("AnetApplication is starting scheduled workers");
// Schedule any tasks that need to run on an ongoing basis.
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
AnetEmailWorker emailWorker = new AnetEmailWorker(engine.getEmailDao(), configuration);
FutureEngagementWorker futureWorker = new FutureEngagementWorker(engine.getReportDao());
ReportPublicationWorker reportPublicationWorker =
new ReportPublicationWorker(engine.getReportDao(), configuration);
final ReportApprovalWorker reportApprovalWorker =
new ReportApprovalWorker(engine.getReportDao(), configuration);
final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

// Check for any reports that need to be published every 5 minutes.
// And run once in 5 seconds from boot-up. (give the server time to boot up).
final ReportPublicationWorker reportPublicationWorker =
new ReportPublicationWorker(configuration, engine.getReportDao());
scheduler.scheduleAtFixedRate(reportPublicationWorker, 5, 5, TimeUnit.MINUTES);
scheduler.schedule(reportPublicationWorker, 5, TimeUnit.SECONDS);

// Check for any emails that need to be sent every 5 minutes.
// And run once in 10 seconds from boot-up. (give the server time to boot up).
final AnetEmailWorker emailWorker = new AnetEmailWorker(configuration, engine.getEmailDao());
scheduler.scheduleAtFixedRate(emailWorker, 5, 5, TimeUnit.MINUTES);
scheduler.schedule(emailWorker, 10, TimeUnit.SECONDS);

// Check for any future engagements every 3 hours.
// And run once in 15 seconds from boot-up. (give the server time to boot up).
final FutureEngagementWorker futureWorker =
new FutureEngagementWorker(configuration, engine.getReportDao());
scheduler.scheduleAtFixedRate(futureWorker, 0, 3, TimeUnit.HOURS);
scheduler.schedule(futureWorker, 15, TimeUnit.SECONDS);

// Check for any reports that need to be approved every 5 minutes.
// And run once in 20 seconds from boot-up. (give the server time to boot up).
final ReportApprovalWorker reportApprovalWorker =
new ReportApprovalWorker(configuration, engine.getReportDao());
scheduler.scheduleAtFixedRate(reportApprovalWorker, 5, 5, TimeUnit.MINUTES);
scheduler.schedule(reportApprovalWorker, 5, TimeUnit.SECONDS);

runAccountDeactivationWorker(configuration, scheduler, engine);

// Check for any missing pending assessments every 6 hours.
// And run once in 25 seconds from boot-up. (give the server time to boot up).
final PendingAssessmentsNotificationWorker pendingAssessmentsNotificationWorker =
new PendingAssessmentsNotificationWorker(configuration);
scheduler.scheduleAtFixedRate(pendingAssessmentsNotificationWorker, 6, 6, TimeUnit.HOURS);
scheduler.schedule(pendingAssessmentsNotificationWorker, 25, TimeUnit.SECONDS);

if (DaoUtils.isPostgresql()) {
// Wait 60 seconds between updates of PostgreSQL materialized views,
// starting 30 seconds after boot-up.
final MaterializedViewRefreshWorker materializedViewRefreshWorker =
new MaterializedViewRefreshWorker(engine.getAdminDao());
new MaterializedViewRefreshWorker(configuration, engine.getAdminDao());
scheduler.scheduleWithFixedDelay(materializedViewRefreshWorker, 30, 60, TimeUnit.SECONDS);
}
}

// Create all of the HTTP Resources.
LoggingResource loggingResource = new LoggingResource();
PersonResource personResource = new PersonResource(engine, configuration);
TaskResource taskResource = new TaskResource(engine, configuration);
LocationResource locationResource = new LocationResource(engine);
OrganizationResource orgResource = new OrganizationResource(engine);
PositionResource positionResource = new PositionResource(engine);
ReportResource reportResource = new ReportResource(engine, configuration);
AdminResource adminResource = new AdminResource(engine, configuration);
HomeResource homeResource = new HomeResource(engine, configuration);
SavedSearchResource savedSearchResource = new SavedSearchResource(engine);
final LoggingResource loggingResource = new LoggingResource();
final PersonResource personResource = new PersonResource(engine, configuration);
final TaskResource taskResource = new TaskResource(engine, configuration);
final LocationResource locationResource = new LocationResource(engine);
final OrganizationResource orgResource = new OrganizationResource(engine);
final PositionResource positionResource = new PositionResource(engine);
final ReportResource reportResource = new ReportResource(engine, configuration);
final AdminResource adminResource = new AdminResource(engine, configuration);
final HomeResource homeResource = new HomeResource(engine, configuration);
final SavedSearchResource savedSearchResource = new SavedSearchResource(engine);
final TagResource tagResource = new TagResource(engine);
final AuthorizationGroupResource authorizationGroupResource =
new AuthorizationGroupResource(engine);
Expand All @@ -277,9 +286,10 @@ public void run(AnetConfiguration configuration, Environment environment)
}

private void runAccountDeactivationWorker(final AnetConfiguration configuration,
final ScheduledExecutorService scheduler, final AnetObjectEngine engine)
throws IllegalArgumentException {
// Check whether the application is configured to auto-check for account deactivation
final ScheduledExecutorService scheduler, final AnetObjectEngine engine) {
// Check whether the application is configured to auto-check for account deactivation.
// NOTE: if you change this, reloading the dictionary from the admin interface is *not*
// sufficient, you will have to restart ANET for this change to be reflected
if (configuration.getDictionaryEntry("automaticallyInactivateUsers") != null) {
// Check for any accounts which are scheduled to be deactivated as they reach the end-of-tour
// date.
Expand All @@ -288,7 +298,7 @@ private void runAccountDeactivationWorker(final AnetConfiguration configuration,
final AccountDeactivationWorker deactivationWarningWorker = new AccountDeactivationWorker(
configuration, engine.getPersonDao(), accountDeactivationWarningInterval);

// Run the email deactivation worker at the set interval. In development run it every minute.
// Run the email deactivation worker at the set interval
scheduler.scheduleAtFixedRate(deactivationWarningWorker, accountDeactivationWarningInterval,
accountDeactivationWarningInterval, TimeUnit.SECONDS);

Expand Down
7 changes: 6 additions & 1 deletion src/main/java/mil/dds/anet/AnetObjectEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public class AnetObjectEngine {
private final AuthorizationGroupDao authorizationGroupDao;
private final NoteDao noteDao;
private final JobHistoryDao jobHistoryDao;
private final MetricRegistry metricRegistry;
private ThreadLocal<Map<String, Object>> context;

ISearcher searcher;
Expand All @@ -82,7 +83,6 @@ public AnetObjectEngine(String dbUrl, Application<?> application, MetricRegistry
this.dbUrl = dbUrl;
injector = InjectorLookup.getInjector(application).get();
personDao = injector.getInstance(PersonDao.class);
personDao.setMetricRegistry(metricRegistry);
taskDao = injector.getInstance(TaskDao.class);
locationDao = injector.getInstance(LocationDao.class);
orgDao = injector.getInstance(OrganizationDao.class);
Expand All @@ -99,6 +99,7 @@ public AnetObjectEngine(String dbUrl, Application<?> application, MetricRegistry
authorizationGroupDao = injector.getInstance(AuthorizationGroupDao.class);
noteDao = injector.getInstance(NoteDao.class);
jobHistoryDao = injector.getInstance(JobHistoryDao.class);
this.metricRegistry = metricRegistry;
searcher = Searcher.getSearcher(DaoUtils.getDbType(dbUrl), injector);
instance = this;
}
Expand Down Expand Up @@ -179,6 +180,10 @@ public EmailDao getEmailDao() {
return emailDao;
}

public MetricRegistry getMetricRegistry() {
return metricRegistry;
}

public ISearcher getSearcher() {
return searcher;
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/mil/dds/anet/beans/JobHistory.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ public void setLastRun(Instant lastRun) {
this.lastRun = lastRun;
}

public static Instant getLastRun(JobHistory jobHistory) {
return jobHistory == null ? null : jobHistory.getLastRun();
}

}
16 changes: 14 additions & 2 deletions src/main/java/mil/dds/anet/beans/search/PositionSearchQuery.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public class PositionSearchQuery extends AbstractSearchQuery<PositionSearchSortB
@GraphQLQuery
@GraphQLInputField
private String authorizationGroupUuid;
@GraphQLQuery
@GraphQLInputField
private Boolean hasCounterparts;

public PositionSearchQuery() {
super(PositionSearchSortBy.NAME);
Expand Down Expand Up @@ -92,10 +95,18 @@ public void setAuthorizationGroupUuid(String authorizationGroupUuid) {
this.authorizationGroupUuid = authorizationGroupUuid;
}

public boolean getHasCounterparts() {
return Boolean.TRUE.equals(hasCounterparts);
}

public void setHasCounterparts(Boolean hasCounterparts) {
this.hasCounterparts = hasCounterparts;
}

@Override
public int hashCode() {
return Objects.hash(super.hashCode(), matchPersonName, organizationUuid, orgRecurseStrategy,
type, isFilled, locationUuid, authorizationGroupUuid);
type, isFilled, locationUuid, authorizationGroupUuid, hasCounterparts);
}

@Override
Expand All @@ -110,7 +121,8 @@ public boolean equals(Object obj) {
&& Objects.equals(getType(), other.getType())
&& Objects.equals(getIsFilled(), other.getIsFilled())
&& Objects.equals(getLocationUuid(), other.getLocationUuid())
&& Objects.equals(getAuthorizationGroupUuid(), other.getAuthorizationGroupUuid());
&& Objects.equals(getAuthorizationGroupUuid(), other.getAuthorizationGroupUuid())
&& Objects.equals(getHasCounterparts(), other.getHasCounterparts());
}

@Override
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/mil/dds/anet/config/AnetConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,9 @@ public Object getDictionaryEntry(String keyPath) {
Object elem = dictionary;
for (final String key : keyPath.split("\\.")) {
elem = ((Map<String, Object>) elem).get(key);
if (elem == null) {
break;
}
}
return elem;
}
Expand Down
Loading

0 comments on commit 8b61b70

Please sign in to comment.