Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send email notifications for pending assessments #3286

Merged
Merged
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